summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-24 11:24:58 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-24 11:24:58 +0700
commitf4a1e2917d550eb205e33b058f07e7edbf8029c8 (patch)
tree757cef98c3c94ce7327fff3bbadd06a269f4fa88
parent51ebcf6f9b57c61efabb9e4e0b4fc8d5bc058512 (diff)
<hafid> sjo half
-rwxr-xr-xindoteknik_custom/__manifest__.py3
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/sourcing.py65
-rw-r--r--indoteknik_custom/models/sourcing_job_order.py339
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv2
-rw-r--r--indoteknik_custom/views/ir_sequence.xml9
-rw-r--r--indoteknik_custom/views/sourcing.xml182
7 files changed, 534 insertions, 67 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index cf7cf1e4..3c3c9a95 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -183,7 +183,8 @@
'views/letter_receivable.xml',
'views/letter_receivable_mail_template.xml',
# 'views/reimburse.xml',
- 'views/sj_tele.xml'
+ 'views/sj_tele.xml',
+ 'views/sourcing.xml'
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 6dc61277..cb110342 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -160,3 +160,4 @@ from . import update_date_planned_po_wizard
from . import unpaid_invoice_view
from . import letter_receivable
from . import sj_tele
+from . import sourcing_job_order
diff --git a/indoteknik_custom/models/sourcing.py b/indoteknik_custom/models/sourcing.py
deleted file mode 100644
index 62540700..00000000
--- a/indoteknik_custom/models/sourcing.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from odoo import models, fields, api
-from odoo.exceptions import UserError
-import requests
-import json
-import logging, subprocess
-
-_logger = logging.getLogger(__name__)
-
-class SourcingOrderJob(models.Model):
- _name = 'sourcing.job.order'
- _descriptions = 'Sourcing Job Order MD'
- _rec_name = 'name'
- _inherit = ['mail.thread', 'mail.activity.mixin']
-
- name = fields.Char(string='Job Number', default='name', copy=False, readonly=True)
- leads_id = fields.Many2One('crm.lead', string='Leads Number')
- product_name = fields.Char(string='Nama Barang', required=True)
- descriptions = fields.Text(string='Deskripsi / Spesifikasi', required=True)
- quantity = fields.Float(string='Quantity Product', required=True)
- eta_sales = fiels.Date(string='Expected Ready')
- sla_product = fields.Date(string='Product Ready Date')
- price = fields.Float(string='Harga Product Vendor')
- vendor_id = fields.Many2One('res.partner', string="Vendor")
- tax_id = fields.Many2One('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)])
- user_id = fields.Many2One('res.users', string='MD Person')
- subtotal = fields.Float(string='Total Purchase', compute='_compute_subtotal_purchase')
- state = fields.Selection([
- ('draft', 'Untaken'),
- ('taken', 'On Sourcing'),
- ('done', 'Complete'),
- ('cancel', 'Cancel')
- ], string='Status')
-
- @api.depends('quantity', 'price', 'tax_id')
- def _compute_subtotal_purchase(self):
- """Menghitung subtotal termasuk pajak."""
- for record in self:
- subtotal = (record.quantity or 0.0) * (record.price or 0.0)
- if record.tax_id:
- tax_amount = subtotal * (record.tax_id.amount / 100)
- subtotal += tax_amount
-
- record.subtotal = subtotal
-
- @api.model
- def create(self, vals):
- if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or self.env.user.has_group('indoteknik_custom.group_role_merchandiser')):
- raise UserError("❌ Hanya Sales dan Merchandiser yang boleh membuat Sourcing Job.")
-
- if vals.get('name', 'New') == 'New':
- vals['name'] = self.env['ir.sequence'].next_by_code('sourcing.job.order') or 'New'
-
- if self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
- vals['user_id'] = self.env.user.id
- vals['state'] = 'taken'
-
- return super().create(vals)
-
- def write(self, vals):
- if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or self.env.user.has_group('indoteknik_custom.group_role_merchandiser')):
- raise UserError("❌ Hanya Sales dan Merchandiser yang boleh membuat Sourcing Job.")
-
- return super().write(vals)
-
-
diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py
new file mode 100644
index 00000000..3d4a404e
--- /dev/null
+++ b/indoteknik_custom/models/sourcing_job_order.py
@@ -0,0 +1,339 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class SourcingJobOrder(models.Model):
+ _name = 'sourcing.job.order'
+ _description = 'Sourcing Job Order MD'
+ _rec_name = 'name'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ name = fields.Char(string='Job Number', default='New', copy=False, readonly=True)
+ leads_id = fields.Many2one('crm.lead', string='Leads Number')
+ user_id = fields.Many2one('res.users', string='MD Person', tracking=True)
+ state = fields.Selection([
+ ('draft', 'Untaken'),
+ ('taken', 'On Sourcing'),
+ ('done', 'Complete'),
+ ('cancel', 'Cancelled')
+ ], string='Status', default='draft', tracking=True)
+ approval_sales = fields.Selection([
+ ('draft', 'Requested'),
+ ('approve', 'Approved'),
+ ], string='Approval Sales', tracking=True)
+ takeover_request = fields.Many2one(
+ 'res.users',
+ string='Takeover Requested By',
+ readonly=True,
+ tracking=True,
+ help='MD yang meminta takeover'
+ )
+ can_request_takeover = fields.Boolean(
+ compute="_compute_can_request_takeover"
+ )
+ can_approve_takeover = fields.Boolean(
+ compute="_compute_can_approve_takeover"
+ )
+
+ eta_sales = fields.Date(string='Expected Ready')
+ sla_product = fields.Date(string='Product Ready Date')
+ cancel_reason = fields.Text(string="Reason for Cancel", tracking=True)
+ product_assets = fields.Binary(string="Product Assets (PDF)")
+
+ line_ids = fields.One2many('sourcing.job.order.line', 'order_id', string='Products')
+
+ total_amount = fields.Float(string="Total Purchase", compute='_compute_total_amount')
+
+ line_sales_input_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='Sales Input Lines',
+ domain=['|', ('price', '=', 0), ('price', '=', False)]
+ )
+ line_md_edit_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='MD Edit Lines'
+ )
+ line_sales_view_ids = fields.One2many(
+ 'sourcing.job.order.line', 'order_id',
+ string='Sales View Lines',
+ domain=[('price', '>', 0)]
+ )
+
+ has_price_in_lines = fields.Boolean(
+ string='Has Line with Price',
+ compute='_compute_has_price_in_lines',
+ )
+
+
+ @api.depends('line_ids.subtotal')
+ def _compute_total_amount(self):
+ for rec in self:
+ rec.total_amount = sum(line.subtotal for line in rec.line_ids)
+
+ @api.depends('line_ids.price', 'line_ids.vendor_id')
+ def _compute_has_price_in_lines(self):
+ for rec in self:
+ # Cek apakah ada minimal satu line yang sudah punya price > 0 dan vendor_id
+ has_price = any(
+ (line.price and line.price > 0 and line.vendor_id)
+ for line in rec.line_ids
+ )
+ rec.has_price_in_lines = bool(has_price)
+
+ @api.depends('user_id', 'takeover_request')
+ def _compute_can_request_takeover(self):
+ for rec in self:
+ current_user = self.env.user
+ rec.can_request_takeover = (
+ rec.user_id and rec.user_id != current_user
+ )
+
+ @api.depends('user_id', 'takeover_request')
+ def _compute_can_approve_takeover(self):
+ for rec in self:
+ current_user = self.env.user
+ rec.can_approve_takeover = (
+ rec.user_id == current_user and bool(rec.takeover_request)
+ )
+
+ @api.model
+ def create(self, vals):
+ """Hanya Sales & Merchandiser yang boleh membuat job."""
+ if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or
+ self.env.user.has_group('indoteknik_custom.group_role_merchandiser')):
+ raise UserError("❌ Hanya Sales dan Merchandiser yang boleh membuat Sourcing Job.")
+
+ if vals.get('name', 'New') == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code('sourcing.job.order') or 'New'
+
+ if self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ vals['user_id'] = self.env.user.id
+ vals['state'] = 'taken'
+
+ rec = super().create(vals)
+ return rec
+
+ def write(self, vals):
+ context_action = self.env.context.get('from_action_take', False)
+
+ if not (self.env.user.has_group('indoteknik_custom.group_role_sales') or
+ self.env.user.has_group('indoteknik_custom.group_role_merchandiser')):
+ raise UserError("❌ Hanya Sales dan Merchandiser yang boleh mengedit Sourcing Job.")
+
+ for rec in self:
+ if not rec.user_id and rec.create_uid != self.env.user and not vals.get('user_id') and not context_action:
+ raise UserError("❌ SJO ini belum memiliki MD Person. Tidak dapat melakukan edit.")
+
+ if rec.user_id != self.env.user and rec.create_uid != self.env.user and not context_action:
+ raise UserError("❌ Hanya MD Person dan Creator SJO ini yang bisa melakukan Edit.")
+
+ return super().write(vals)
+
+ def action_take(self):
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang dapat mengambil Sourcing Job.")
+ if rec.state != 'draft':
+ continue
+ rec.with_context(from_action_take=True).write({
+ 'state': 'taken',
+ 'user_id': self.env.user.id
+ })
+ rec.message_post(body=("Job <b>%s</b> diambil oleh %s") % (rec.name, self.env.user.name))
+
+ def action_multi_take(self):
+ untaken = self.filtered(lambda r: r.state == 'draft')
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang bisa mengambil Sourcing Job.")
+ if not untaken:
+ raise UserError("Tidak ada record draft untuk diambil.")
+
+ untaken.write({
+ 'state': 'taken',
+ 'user_id': self.env.user.id,
+ })
+
+ def action_cancel(self):
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang dapat mengcancel Sourcing Job.")
+
+ if rec.user_id and rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Cancel.")
+
+ if not rec.cancel_reason:
+ raise UserError("⚠️ Isi alasan pembatalan terlebih dahulu.")
+
+ rec.write({'state': 'cancel'})
+ rec.message_post(body=("Job <b>%s</b> dibatalkan oleh %s<br/>Alasan: %s") %
+ (rec.name, self.env.user.name, rec.cancel_reason))
+
+ def action_request_takeover(self):
+ for rec in self:
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("❌ Hanya Merchandiser yang dapat Request Takeover Sourcing Job.")
+
+ if rec.takeover_request:
+ raise UserError(f"SJO ini sudah memiliki request takeover dari {rec.takeover_request.name}. Tunggu approval dulu.")
+
+ rec.takeover_request = self.env.user
+
+ activity_type = self.env.ref('mail.mail_activity_data_todo')
+ self.env['mail.activity'].create({
+ 'activity_type_id': activity_type.id,
+ 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.",
+ 'res_id': rec.id,
+ 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'),
+ 'user_id': rec.user_id.id,
+ })
+
+ rec.message_post(
+ body=f"<b>{self.env.user.name}</b> mengirimkan request takeover kepada <b>{rec.user_id.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Request Sent',
+ 'message': f"Request takeover telah dikirim ke {rec.user_id.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+ def action_ask_approval(self):
+ for rec in self:
+ if rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person Sourcing Job ini yang dapat Request Approval.")
+
+ rec.approval_sales = 'requested'
+
+ activity_type = self.env.ref('mail.mail_activity_data_todo')
+ self.env['mail.activity'].create({
+ 'activity_type_id': activity_type.id,
+ 'note': f"{self.env.user.name} meminta approval untuk mengambil alih SJO '{rec.name}'.",
+ 'res_id': rec.id,
+ 'res_model_id': self.env['ir.model']._get_id('sourcing.job.order'),
+ 'user_id': rec.user_id.id,
+ })
+
+ rec.message_post(
+ body=f"<b>{self.env.user.name}</b> mengirimkan request approval kepada <b>{rec.user_id.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Request Sent',
+ 'message': f"Request takeover telah dikirim ke {rec.user_id.name}.",
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+
+
+ def action_approve_takeover(self):
+ for rec in self:
+ if self.env.user != rec.user_id:
+ raise UserError("Hanya MD person yang saat ini memegang SJO yang dapat menyetujui takeover ini.")
+ if not rec.takeover_request:
+ raise UserError("Tidak ada request takeover yang perlu disetujui.")
+
+ new_user = rec.takeover_request
+
+ rec.user_id = new_user
+ rec.takeover_request = False
+
+ activities = self.env['mail.activity'].search([
+ ('res_id', '=', rec.id),
+ ('res_model', '=', 'sourcing.job.order'),
+ ('user_id', '=', self.env.user.id)
+ ])
+ activities.unlink()
+
+ rec.message_post(
+ body=f"Takeover disetujui oleh <b>{self.env.user.name}</b>. Sourcing Job berpindah ke <b>{new_user.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+ def action_reject_takeover(self):
+ for rec in self:
+ if self.env.user != rec.user_id:
+ raise UserError("Hanya MD person yang saat ini memegang SJO yang dapat menolak takeover ini.")
+ if not rec.takeover_request:
+ raise UserError("Tidak ada request takeover yang perlu ditolak.")
+
+ requester = rec.takeover_request
+ rec.takeover_request = False
+
+ activities = self.env['mail.activity'].search([
+ ('res_id', '=', rec.id),
+ ('res_model', '=', 'sourcing.job.order'),
+ ('user_id', '=', self.env.user.id)
+ ])
+ activities.unlink()
+
+ rec.message_post(
+ body=f"Takeover dari <b>{requester.name}</b> ditolak oleh <b>{self.env.user.name}</b>.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+
+class SourcingJobOrderLine(models.Model):
+ _name = 'sourcing.job.order.line'
+ _description = 'Sourcing Job Order Line'
+
+ order_id = fields.Many2one('sourcing.job.order', string='Job Order', ondelete='cascade')
+ product_name = fields.Char(string='Nama Barang', required=True)
+ code = fields.Char(string='SKU')
+ descriptions = fields.Text(string='Deskripsi / Spesifikasi')
+ quantity = fields.Float(string='Quantity Product', required=True)
+ price = fields.Float(string='Purchase Price')
+ tax_id = fields.Many2one('account.tax', string='Tax', domain=[('active', '=', True)])
+ vendor_id = fields.Many2one('res.partner', string="Vendor")
+ product_category = fields.Many2one('product.category', string="Product Category")
+ state = fields.Selection([
+ ('draft', 'Unsource'),
+ ('sourcing', 'On Sourcing'),
+ ('done', 'Done Sourcing'),
+ ('cancel', 'Unavailable')
+ ], default='draft', tracking=True)
+ product_type = fields.Selection([
+ ('consu', 'Consumable'),
+ ('servis', 'Service'),
+ ('product', 'Storable Product'),
+ ], default='product')
+ subtotal = fields.Float(string='Subtotal', compute='_compute_subtotal')
+ show_for_sales = fields.Boolean(
+ string="Show for Sales",
+ compute="_compute_show_for_sales",
+ )
+
+ @api.depends('quantity', 'price', 'tax_id')
+ def _compute_subtotal(self):
+ """Menghitung subtotal termasuk pajak."""
+ for line in self:
+ subtotal = (line.quantity or 0.0) * (line.price or 0.0)
+ if line.tax_id:
+ subtotal += subtotal * (line.tax_id.amount / 100)
+ line.subtotal = subtotal
+
+ @api.constrains('product_type', 'product_category')
+ def _check_required_fields_for_md(self):
+ for rec in self:
+ is_md = self.env.user.has_group('indoteknik_custom.group_role_merchandiser')
+ if is_md and (not rec.product_type or not rec.product_category):
+ raise ValidationError("MD wajib mengisi Product Type dan Product Category!")
+
+ @api.depends('price', 'vendor_id', 'order_id')
+ def _compute_show_for_sales(self):
+ for rec in self:
+ rec.show_for_sales = bool(
+ rec.order_id and rec.price not in (None, 0) and rec.vendor_id
+ )
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index ea6670eb..17372e48 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -202,3 +202,5 @@ access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view,
access_surat_piutang_user,surat.piutang user,model_surat_piutang,base.group_user,1,1,1,1
access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,base.group_user,1,1,1,1
access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1
+access_sourcing_job_order,access.sourcing_job_order,model_sourcing_job_order,base.group_system,1,1,1,1
+access_sourcing_job_order_line_user,sourcing.job.order.line,model_sourcing_job_order_line,base.group_user,1,1,1,1
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index 4b8fec53..5ea7324b 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -218,7 +218,14 @@
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
-
+ <record id="seq_sourcing_job_order" model="ir.sequence">
+ <field name="name">Sourcing Job Order</field>
+ <field name="code">sourcing.job.order</field>
+ <field name="prefix">SJO/%(year)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next_actual">1</field>
+ <field name="number_increment">1</field>
+ </record>
<record id="seq_refund_sale_order" model="ir.sequence">
<field name="name">Refund Sales Order</field>
<field name="code">refund.sale.order</field>
diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml
index e69de29b..f9f8f386 100644
--- a/indoteknik_custom/views/sourcing.xml
+++ b/indoteknik_custom/views/sourcing.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="view_sourcing_job_order_tree" model="ir.ui.view">
+ <field name="name">sourcing.job.order.tree</field>
+ <field name="model">sourcing.job.order</field>
+ <field name="arch" type="xml">
+ <tree string="Sourcing Job Orders"
+ decoration-success="state=='done'"
+ decoration-danger="state=='cancel'"
+ decoration-warning="state=='taken'">
+ <field name="name"/>
+ <!-- <field name="leads_id"/> -->
+ <field name="user_id" widget="many2one_avatar_user"/>
+ <field name="state" widget="badge"/>
+ <field name="create_date"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_sourcing_job_order_form" model="ir.ui.view">
+ <field name="name">sourcing.job.order.form</field>
+ <field name="model">sourcing.job.order</field>
+ <field name="arch" type="xml">
+ <form string="Sourcing Job Order">
+ <header>
+ <button name="action_take"
+ string="Take"
+ type="object"
+ class="btn-primary"
+ icon="fa-hand-paper-o"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('state', '!=', 'draft')]}"/>
+
+ <button name="action_cancel"
+ string="Cancel"
+ type="object"
+ class="btn-secondary"
+ icon="fa-times"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('state', 'in', ['cancel', 'done'])]}"/>
+ <button name="action_request_takeover"
+ string="Request Takeover"
+ type="object"
+ class="btn-primary"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_request_takeover', '=', False)]}"/>
+
+ <button name="action_approve_takeover"
+ string="Approve Takeover"
+ type="object"
+ class="btn-success"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+
+ <button name="action_reject_takeover"
+ string="Reject Takeover"
+ type="object"
+ class="btn-danger"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+
+ <field name="state" widget="statusbar"
+ statusbar_visible="draft,taken,done,cancel"
+ statusbar_colors='{"draft": "blue", "taken": "orange", "done": "green", "cancel": "red"}'/>
+ </header>
+
+ <sheet>
+ <h1>
+ <field name="name" readonly="1"/>
+ </h1>
+
+ <group>
+ <group>
+ <!-- <field name="leads_id"/> -->
+ <field name="eta_sales"/>
+ <field name="takeover_request" invisible="1"/>
+ <field name="can_request_takeover" invisible="1"/>
+ <field name="can_approve_takeover" invisible="1"/>
+ <field name="has_price_in_lines" invisible="1"/>
+ <field name="sla_product" groups="indoteknik_custom.group_role_merchandiser"/>
+ </group>
+ <group>
+ <field name="create_uid" readonly="1" widget="many2one_avatar_user"/>
+ <field name="user_id" readonly="1" widget="many2one_avatar_user"/>
+ </group>
+ </group>
+
+ <notebook>
+ <page string="Product Line" groups="indoteknik_custom.group_role_sales" attrs="{'invisible': [('has_price_in_lines', '=', True)]}">
+ <field name="line_sales_input_ids">
+ <tree editable="bottom">
+ <field name="product_name"/>
+ <field name="descriptions"/>
+ <field name="quantity"/>
+ </tree>
+ </field>
+ </page>
+
+ <!-- MD EDIT -->
+ <page string="MD Lines" groups="indoteknik_custom.group_role_merchandiser">
+ <field name="line_md_edit_ids">
+ <tree editable="bottom">
+ <field name="code"/>
+ <field name="product_name"/>
+ <field name="descriptions"/>
+ <field name="quantity"/>
+ <field name="price"/>
+ <field name="vendor_id"/>
+ <field name="tax_id"/>
+ <field name="subtotal"/>
+ </tree>
+ </field>
+ </page>
+
+ <!-- SALES VIEW -->
+ <page string="Product Line" groups="indoteknik_custom.group_role_sales" attrs="{'invisible': [('has_price_in_lines', '=', False)]}">
+ <field name="line_sales_view_ids">
+ <tree>
+ <field name="code" readonly="1"/>
+ <field name="product_name" readonly="1"/>
+ <field name="descriptions" readonly="1"/>
+ <field name="quantity" readonly="1"/>
+ <field name="price" readonly="1"/>
+ <field name="vendor_id" readonly="1"/>
+ <field name="tax_id" readonly="1"/>
+ <field name="subtotal" readonly="1"/>
+ </tree>
+ </field>
+ </page>
+
+ <page string="Documents">
+ <field name="product_assets" widget="pdf_viewer"/>
+ </page>
+
+ <page string="Cancel Reason" attrs="{'invisible': [('state', 'in', ['done'])]}">
+ <group>
+ <field name="cancel_reason"/>
+ </group>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_sourcing_job_order_multi_take" model="ir.actions.server">
+ <field name="name">Take Selected Jobs</field>
+ <field name="model_id" ref="model_sourcing_job_order"/>
+ <field name="binding_model_id" ref="model_sourcing_job_order"/>
+ <field name="binding_type">action</field>
+ <field name="state">code</field>
+ <field name="code">action = records.action_multi_take()</field>
+ <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_merchandiser'))]"/>
+ </record>
+
+ <record id="action_sourcing_job_order" model="ir.actions.act_window">
+ <field name="name">Sourcing Job Orders</field>
+ <field name="res_model">sourcing.job.order</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Buat Sourcing Job Order baru di sini ✨
+ </p>
+ </field>
+ </record>
+
+ <menuitem id="menu_md_root"
+ name="MD"
+ parent="crm.crm_menu_root"
+ sequence="80"/>
+
+ <menuitem id="menu_sourcing_job_order"
+ name="Sourcing Job Order"
+ parent="menu_md_root"
+ action="action_sourcing_job_order"
+ sequence="90"/>
+</odoo>