summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2026-03-10 10:46:23 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2026-03-10 10:46:23 +0700
commitf58fe20f96995228651a5a1a09c8c17a23e13838 (patch)
treedc6700fe931d229523c478ac6f290b3e45ae871c
parentd91af3da1edea3c6b8074726e5306ea42a7c0a4b (diff)
<hafid> final change request
-rwxr-xr-xindoteknik_custom/models/product_template.py6
-rwxr-xr-xindoteknik_custom/models/sale_order.py18
-rw-r--r--indoteknik_custom/models/sourcing_job_order.py126
-rwxr-xr-xindoteknik_custom/views/sale_order.xml8
-rw-r--r--indoteknik_custom/views/sourcing.xml46
5 files changed, 149 insertions, 55 deletions
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index 56022d06..ecaf9106 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -91,7 +91,8 @@ class ProductTemplate(models.Model):
group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
active_model = self.env.context.get('active_model')
- if self.env.user.id not in users_in_group.mapped('id') and active_model == None:
+ from_sourcing = self.env.context.get('from_sourcing_approval')
+ if self.env.user.id not in users_in_group.mapped('id') and active_model == None and not from_sourcing:
raise UserError('Hanya MD yang bisa membuat Product')
result = super(ProductTemplate, self).create(vals)
return result
@@ -983,7 +984,8 @@ class ProductProduct(models.Model):
group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
active_model = self.env.context.get('active_model')
users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
- if self.env.user.id not in users_in_group.mapped('id') and active_model == None:
+ from_sourcing = self.env.context.get('from_sourcing_approval')
+ if self.env.user.id not in users_in_group.mapped('id') and active_model == None and not from_sourcing:
raise UserError('Hanya MD yang bisa membuat Product')
result = super(ProductProduct, self).create(vals)
return result
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 031007ae..567259af 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -404,6 +404,7 @@ class SaleOrder(models.Model):
client_order_ref = fields.Char(tracking=True)
+ sourcing_job_count = fields.Integer(string='Sourcing Count', compute='_compute_sourcing_count')
def action_set_shipping_id(self):
for rec in self:
@@ -3695,6 +3696,17 @@ class SaleOrder(models.Model):
'context': {'default_sale_order_ids': [self.id]},
}
+ def action_view_related_sjo(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Sourcing Job',
+ 'res_model': 'sourcing.job.order',
+ 'view_mode': 'tree,form',
+ 'domain': [('so_id', '=', self.id)],
+ 'context': {'default_so_id': self.id},
+ }
+
def _compute_refund_ids(self):
for order in self:
refunds = self.env['refund.sale.order'].search([
@@ -3708,6 +3720,12 @@ class SaleOrder(models.Model):
('sale_order_ids', 'in', order.id)
])
+ def _compute_sourcing_count(self):
+ for order in self:
+ order.sourcing_job_count = self.env['sourcing.job.order'].search_count([
+ ('so_id', '=', order.id)
+ ])
+
@api.depends('invoice_ids')
def _compute_advance_payment_move(self):
for order in self:
diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py
index 6bb59c62..0e5334a8 100644
--- a/indoteknik_custom/models/sourcing_job_order.py
+++ b/indoteknik_custom/models/sourcing_job_order.py
@@ -89,11 +89,13 @@ class SourcingJobOrder(models.Model):
@api.depends('eta_sales', 'eta_complete', 'create_date', 'state')
def _compute_progress_status(self):
for rec in self:
- if rec.eta_sales:
- # Ada tanggal expected
- if rec.state == 'taken':
- rec.progress_status = '🟡 On Track'
- elif rec.state == 'done' and rec.eta_complete:
+
+ if rec.state == 'cancel':
+ rec.progress_status = '⚫ Cancelled'
+ continue
+
+ if rec.state == 'done':
+ if rec.eta_sales and rec.eta_complete:
delta = (rec.eta_complete - rec.eta_sales).days
if delta < 0:
rec.progress_status = f'🟢 Early {abs(delta)} hari'
@@ -101,22 +103,15 @@ class SourcingJobOrder(models.Model):
rec.progress_status = '🔵 Ontime'
else:
rec.progress_status = f'🔴 Delay {delta} hari'
- elif rec.state == 'cancel':
- rec.progress_status = '⚫ Cancelled'
- else:
- rec.progress_status = '🟡 On Track'
- else:
- # Tidak ada ETA, hitung durasi
- if rec.state == 'done' and rec.eta_complete:
- if rec.create_date:
- durasi = (rec.eta_complete - rec.create_date.date()).days
- rec.progress_status = f'✅ Selesai dalam {durasi} hari'
- else:
- rec.progress_status = '✅ Selesai'
- elif rec.state == 'cancel':
- rec.progress_status = '⚫ Cancelled'
+ elif rec.create_date and rec.eta_complete:
+ durasi = (rec.eta_complete - rec.create_date.date()).days
+ rec.progress_status = f'✅ Selesai dalam {durasi} hari'
else:
- rec.progress_status = '🟡 On Track'
+ rec.progress_status = '✅ Selesai'
+ continue
+
+ if rec.state in ['taken', 'partial', 'draft']:
+ rec.progress_status = '🟡 On Track'
@api.depends('line_ids.price', 'line_ids.vendor_id')
def _compute_has_price_in_lines(self):
@@ -413,6 +408,7 @@ class SourcingJobOrderLine(models.Model):
vals['show_salesperson'] = order.so_id.user_id.id
rec = super().create(vals)
+ rec._check_line_limit()
return rec
def write(self, vals):
@@ -427,8 +423,30 @@ class SourcingJobOrderLine(models.Model):
res = super().write(vals)
if 'state' in vals:
self._update_parent_state()
+ self._check_line_limit()
return res
+ def _check_line_limit(self):
+ for rec in self:
+ if not rec.order_id or not rec.order_id.so_id:
+ continue
+
+ so = rec.order_id.so_id
+
+ so_line_count = len(so.order_line)
+
+ sourcing_lines = self.search([
+ ('order_id', '=', rec.order_id.id),
+ ('state', '!=', 'cancel')
+ ])
+
+ if len(sourcing_lines) > so_line_count:
+ raise UserError(
+ f"Jumlah Sourcing Line tidak boleh melebihi Sales Order Line.\n\n"
+ f"Sales Order Line : {so_line_count}\n"
+ f"Sourcing Line : {len(sourcing_lines)}"
+ )
+
def _update_parent_state(self):
for rec in self:
order = rec.order_id
@@ -617,18 +635,40 @@ class SourcingJobOrderLine(models.Model):
if line.state != 'sourcing':
raise UserError("⚠️ Hanya line status 'Sourcing' yang bisa minta approval.")
- if (
- not line.vendor_id
- or not line.product_name_md
- or not line.brand_id
- or not line.price or line.price <= 0
- or not line.tax_id
- or not line.subtotal or line.subtotal <= 0
- or not line.product_type
- or not line.product_category
- or not line.product_class
- ):
- raise UserError("❌ Lengkapi data sebelum Ask Approval sales")
+ missing_fields = []
+
+ if not line.vendor_id:
+ missing_fields.append("Vendor")
+
+ if not line.product_name_md:
+ missing_fields.append("Product Name")
+
+ if not line.brand_id:
+ missing_fields.append("Manufactures")
+
+ if not line.price or line.price <= 0:
+ missing_fields.append("Price")
+
+ if not line.tax_id:
+ missing_fields.append("Tax")
+
+ if not line.subtotal or line.subtotal <= 0:
+ missing_fields.append("Subtotal")
+
+ if not line.product_type:
+ missing_fields.append("Product Type")
+
+ if not line.product_category:
+ missing_fields.append("Product Category")
+
+ if not line.product_class:
+ missing_fields.append("Product Class")
+
+ if missing_fields:
+ raise UserError(
+ "❌ Lengkapi data berikut sebelum Ask Approval Sales:\n- " +
+ "\n- ".join(missing_fields)
+ )
line.state = 'sent'
@@ -730,7 +770,7 @@ class SourcingJobOrderLine(models.Model):
'consu': 'consu',
}
- product = ProductProduct.create({
+ product = ProductProduct.with_context(from_sourcing_approval=True).create({
'name': rec.product_name_md,
'default_code': rec.code or False,
'description': rec.descriptions_md or '',
@@ -751,6 +791,11 @@ class SourcingJobOrderLine(models.Model):
rec.product_id = product.id
+ self.env.user.notify_success(
+ message=f"Produk baru '{product.name}' berhasil dibuat dengan SKU {product.default_code}.",
+ title="Product Created"
+ )
+
jakarta_tz = rec.order_id._get_jakarta_today()
purchase_price = PurchasePricelist.search([
@@ -830,9 +875,24 @@ class SourcingJobOrderLine(models.Model):
title="Approved"
)
+ so = self.mapped('so_id')[:1]
+ if so:
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Sales Order',
+ 'res_model': 'sale.order',
+ 'view_mode': 'form',
+ 'res_id': so.id,
+ 'target': 'current',
+ }
+
return {'type': 'ir.actions.client', 'tag': 'reload'}
def action_multi_approve(self):
+ so_ids = self.mapped('so_id').ids
+
+ if len(set(so_ids)) > 1:
+ raise UserError("❌ Multi approve hanya bisa dilakukan jika semua line berasal dari Sales Order yang sama.")
self.action_approve_approval()
def action_reject_approval(self):
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 33cd51e4..80eea779 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -83,6 +83,14 @@
attrs="{'invisible': [('refund_count', '=', 0)]}">
<field name="refund_count" widget="statinfo" string="Refund"/>
</button>
+
+ <button type="object"
+ name="action_view_related_sjo"
+ class="oe_stat_button"
+ icon="fa-briefcase"
+ attrs="{'invisible': [('sourcing_job_count', '=', 0)]}">
+ <field name="sourcing_job_count" widget="statinfo" string="SJO"/>
+ </button>
</div>
<field name="payment_term_id" position="after">
<field name="create_uid" invisible="1"/>
diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml
index ebdbaaf7..006817cf 100644
--- a/indoteknik_custom/views/sourcing.xml
+++ b/indoteknik_custom/views/sourcing.xml
@@ -420,9 +420,9 @@
<field name="arch" type="xml">
<tree string="Sourcing Job Order"
decoration-warning="state=='sourcing'"
- decoration-success="state== 'done'"
+ decoration-success="state== 'approve'"
decoration-danger="state=='cancel'"
- decoration-info="state in ('sent','approve')">
+ decoration-info="state == 'sent'">
<field name="order_id" />
<field name="md_person_ids" widget="many2one_avatar_user"/>
<field name="show_salesperson" widget="many2one_avatar_user"/>
@@ -508,7 +508,7 @@
<field name="name">sourcing.job.order.line.form</field>
<field name="model">sourcing.job.order.line</field>
<field name="arch" type="xml">
- <form string="Sourcing Job">
+ <form string="Sourcing Job" create="0">
<header>
<button name="action_take"
string="Take"
@@ -577,7 +577,7 @@
<widget name="web_ribbon"
title="COMPLETE"
bg_color="bg-success"
- attrs="{'invisible': [('state', '!=', 'done')]}"/>
+ attrs="{'invisible': [('state', '!=', 'approve')]}"/>
<widget name="web_ribbon"
title="CANCEL"
@@ -593,20 +593,20 @@
<field name="is_given" invisible="1"/>
<field name="is_receiver" invisible="1"/>
<field name="md_person_ids" widget="many2one_avatar_user" readonly="1"/>
- <field name="brand_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="product_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="descriptions_md" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="code" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="product_name_md" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="price" required="1" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
+ <field name="brand_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="product_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="descriptions_md" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="code" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="product_name_md" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="price" required="1" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
<field name="now_price" force_save="1" readonly="1"/>
<field name="last_updated_price" force_save="1"/>
- <field name="vendor_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="tax_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="subtotal" readonly="1" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="product_category" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="product_type" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
- <field name="product_class" widget="many2many_tags" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/>
+ <field name="vendor_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="tax_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="subtotal" readonly="1" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="product_category" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="product_type" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
+ <field name="product_class" widget="many2many_tags" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
</group>
<group>
@@ -645,7 +645,7 @@
<notebook>
<page string="Cancel Reason" attrs="{'invisible': [('state', 'in', ['approve'])]}">
<group>
- <field name="reason"/>
+ <field name="reason" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/>
</group>
</page>
</notebook>
@@ -700,6 +700,7 @@
<field name="product_name"/>
<field name="brand"/>
<field name="so_id"/>
+ <field name="show_salesperson"/>
<filter name="filter_sent"
string="Sent"
@@ -711,6 +712,10 @@
context="{'group_by':'so_id'}"/>
</group>
+ <filter name="filter_my_line"
+ string="My Line"
+ domain="[('show_salesperson','=',uid)]"/>
+
</search>
</field>
</record>
@@ -753,6 +758,7 @@
<field name="binding_model_id" ref="model_sourcing_job_order_line"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
+ <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_sales'))]"/>
<field name="code">
records.action_multi_approve()
</field>
@@ -761,16 +767,16 @@
<record id="action_sourcing_line_approval" model="ir.actions.act_window">
<field name="name">Approval Sourcing Line</field>
<field name="res_model">sourcing.job.order.line</field>
- <field name="view_mode">tree</field>
- <field name="view_id" ref="view_sjo_line_approval_tree"/>
+ <field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_sjo_line_approval_search"/>
+ <field name="domain">[('state','in',('sent','approve'))]</field>
<field name="context">
{
'search_default_filter_sent': 1,
+ 'search_default_filter_my_line': 1,
'search_default_group_so': 1
}
</field>
- <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_sales'))]"/>
</record>
<menuitem id="menu_sourcing_line_approval"