summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-31 15:36:09 +0700
committerHafidBuroiroh <hafidburoiroh09@gmail.com>2025-10-31 15:36:09 +0700
commitac64cc0792558e7c4922c6fc9c2f0a4c5c532967 (patch)
tree1f8cc6ed359f978dce52fab629ea09bd331ba996
parenta41ed8ed1db45b019d3c3f7d31a01d9658c55f78 (diff)
push
-rwxr-xr-xindoteknik_custom/models/product_template.py5
-rw-r--r--indoteknik_custom/models/sourcing_job_order.py217
-rw-r--r--indoteknik_custom/views/sourcing.xml51
3 files changed, 196 insertions, 77 deletions
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index 13e99707..fcea54fa 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -928,6 +928,11 @@ class ProductProduct(models.Model):
qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
qty_pcs_box = fields.Float("Pcs Box")
barcode_box = fields.Char("Barcode Box")
+ sourcing_job_id = fields.Many2one(
+ "sourcing.job.order",
+ string="Sourcing Job",
+ readonly=True,
+ )
def generate_product_sla(self):
product_variant_ids = self.env.context.get('active_ids', [])
diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py
index 98f356de..5fd3067b 100644
--- a/indoteknik_custom/models/sourcing_job_order.py
+++ b/indoteknik_custom/models/sourcing_job_order.py
@@ -39,7 +39,6 @@ class SourcingJobOrder(models.Model):
)
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)")
@@ -59,7 +58,7 @@ class SourcingJobOrder(models.Model):
line_sales_view_ids = fields.One2many(
'sourcing.job.order.line', 'order_id',
string='Sales View Lines',
- domain=[('state', 'in', ['sourcing', 'done', 'cancel'])]
+ domain=[('state', 'in', ['sourcing', 'done', 'cancel', 'convert'])]
)
line_sales_view_cancel_ids = fields.One2many(
'sourcing.job.order.line', 'order_id',
@@ -67,6 +66,18 @@ class SourcingJobOrder(models.Model):
domain=[('state', '=', 'cancel')]
)
+ converted_product_ids = fields.One2many(
+ "product.product",
+ "sourcing_job_id",
+ string="Converted Products",
+ readonly=True,
+ )
+
+ converted_product_count = fields.Integer(
+ compute="_compute_converted_product_count",
+ string="Converted Product Count",
+ )
+
has_price_in_lines = fields.Boolean(
string='Has Line with Price',
compute='_compute_has_price_in_lines',
@@ -114,6 +125,23 @@ class SourcingJobOrder(models.Model):
rec.create_uid == current_user and
rec.user_id == current_user
)
+
+ @api.depends("converted_product_ids")
+ def _compute_converted_product_count(self):
+ for rec in self:
+ rec.converted_product_count = len(rec.converted_product_ids)
+
+ def action_open_converted_products(self):
+ """Open converted products related to this SJO."""
+ self.ensure_one()
+ return {
+ 'name': 'Converted Products',
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'tree,form',
+ 'res_model': 'product.product',
+ 'domain': [('id', 'in', self.converted_product_ids.ids)],
+ 'context': {'default_sourcing_job_id': self.id},
+ }
@api.depends('line_md_edit_ids.state')
def _compute_can_convert_to_product(self):
@@ -204,9 +232,8 @@ class SourcingJobOrder(models.Model):
for line in rec.line_md_edit_ids:
old_line = old_lines.get(line.id)
if not old_line:
- continue # line baru, skip validasi perubahan
+ continue
- # đŸ’Ĩ Validasi vendor berubah tapi price tidak berubah
if (
old_line['vendor_id'] != (line.vendor_id.id if line.vendor_id else False)
and old_line['price'] == line.price
@@ -215,7 +242,6 @@ class SourcingJobOrder(models.Model):
f"âš ī¸ Harga untuk produk {line.product_name} belum diperbarui setelah mengganti Vendor."
)
- # Catat perubahan state, vendor, atau harga untuk log note
sub_changes = []
if old_line['state'] != line.state:
@@ -274,7 +300,7 @@ class SourcingJobOrder(models.Model):
if rec.user_id and rec.user_id != self.env.user:
raise UserError("❌ Hanya MD Person yang memiliki SJO ini yang boleh melakukan Confirm.")
- invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel', 'convert'))
if invalid_lines:
line_names = ', '.join(invalid_lines.mapped('product_name'))
raise UserError(
@@ -292,16 +318,12 @@ class SourcingJobOrder(models.Model):
subtype_xmlid="mail.mt_comment"
)
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'titl': 'Auto Approved',
- 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
- 'type': 'success',
- 'sticky': False,
- }
- }
+ self.env.user.notify_success(
+ message=f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
+ title="Auto Approved",
+ )
+
+ return {'type': 'ir.actions.client','tag': 'reload',}
def action_cancel(self):
for rec in self:
@@ -341,16 +363,12 @@ class SourcingJobOrder(models.Model):
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,
- }
- }
+ self.env.user.notify_success(
+ message=f"Request takeover telah dikirim ke {rec.user_id.name}.",
+ title="Request Sent",
+ )
+
+ return {'type': 'ir.actions.client','tag': 'reload',}
def action_approve_takeover(self):
for rec in self:
@@ -398,11 +416,75 @@ class SourcingJobOrder(models.Model):
subtype_xmlid="mail.mt_comment"
)
+ def action_convert_all_lines(self):
+ for rec in self:
+ if rec.user_id != self.env.user:
+ raise UserError("❌ Hanya MD Person dari Sourcing Job ini yang dapat melakukan konversi produk.")
+ done_lines = rec.line_ids.filtered(lambda l: l.state == 'done')
+ if not done_lines:
+ raise UserError("âš ī¸ Tidak ada line dengan status 'Done Sourcing' untuk dikonversi.")
+
+ ProductProduct = self.env['product.product']
+ existing_skus = []
+ created_products = []
+
+ for line in done_lines:
+ if not line.code:
+ continue
+ existing = ProductProduct.search([('default_code', '=', line.code)], limit=1)
+ if existing:
+ existing_skus.append(line.code)
+ continue
+ type_map = {
+ 'servis': 'service',
+ 'product': 'product',
+ 'consu': 'consu',}
+
+ new_product = ProductProduct.create({
+ 'name': line.product_name,
+ 'default_code': line.code,
+ 'description': line.descriptions or '',
+ 'type': type_map.get(line.product_type, 'product'),
+ 'categ_id': line.product_category.id if line.product_category else False,
+ 'standard_price': line.price if line.price else 0,
+ 'public_categ_ids': [(6, 0, [line.product_class.id])] if line.product_class else False,
+ 'active': True,
+ 'sourcing_job_id': rec.id,
+ })
+
+ line.state = 'convert'
+ created_products.append(new_product.name)
+
+ if created_products:
+ rec.message_post(
+ body=(
+ f"✅ <b>Berhasil mengonversi {len(created_products)} produk baru:</b><br/>"
+ + "<br/>".join(created_products)
+ ),
+ subtype_xmlid="mail.mt_comment",
+ )
+
+ if existing_skus:
+ raise UserError(
+ "âš ī¸ SKU berikut sudah ada dan tidak dikonversi:\n- "
+ + "\n- ".join(existing_skus)
+ )
+
+ if not created_products:
+ raise UserError("â„šī¸ Tidak ada produk baru yang dikonversi (semua SKU sudah ada).")
+
+ self.env.user.notify_success(
+ message=f"{len(created_products)} produk berhasil dibuat di Master Product.",
+ title="Konversi Selesai",
+ )
+
+ return {'type': 'ir.actions.client','tag': 'reload',}
+
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.")
- invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
+ invalid_lines = rec.line_md_edit_ids.filtered(lambda l: l.state not in ('done', 'cancel', 'convert'))
if invalid_lines:
line_names = ', '.join(invalid_lines.mapped('product_name'))
raise UserError(
@@ -412,26 +494,6 @@ class SourcingJobOrder(models.Model):
if rec.line_md_edit_ids and all(line.state == 'cancel' for line in rec.line_md_edit_ids):
raise UserError("âš ī¸ Tidak dapat melakukan Request Approval. Semua line pada SJO ini Unavailable.")
- if rec.create_uid == rec.user_id:
- rec.approval_sales = 'approve'
- rec.state = 'done'
-
- rec.message_post(
- body=f"Sourcing Job <b>{rec.name}</b> otomatis disetujui karena pembuat dan MD adalah orang yang sama (<b>{self.env.user.name}</b>).",
- subtype_xmlid="mail.mt_comment"
- )
-
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Auto Approved',
- 'message': f"Sourcing Job '{rec.name}' otomatis disetujui dan diselesaikan.",
- 'type': 'success',
- 'sticky': False,
- }
- }
-
rec.approval_sales = 'draft'
activity_type = self.env.ref('mail.mail_activity_data_todo')
@@ -446,16 +508,11 @@ class SourcingJobOrder(models.Model):
subtype_xmlid="mail.mt_comment"
)
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Request Sent',
- 'message': f"Request Approval telah dikirim ke {rec.create_uid.name}.",
- 'type': 'success',
- 'sticky': False,
- }
- }
+ self.env.user.notify_success(
+ message=f"Request Approval telah dikirim ke {rec.create_uid.name}.",
+ title="Request Sent",
+ )
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
def action_confirm_approval(self):
for rec in self:
@@ -478,16 +535,38 @@ class SourcingJobOrder(models.Model):
note=f"✅ Sourcing Job <b>{rec.name}</b> telah disetujui oleh {self.env.user.name}.",
)
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Approval Confirmed',
- 'message': f"Sourcing Job '{rec.name}' telah disetujui dan dikirim ke {rec.user_id.name}.",
- 'type': 'success',
- 'sticky': False,
- }
- }
+ self.env.user.notify_info(
+ message=f"Sourcing Job '{rec.name}' telah disetujui dan dikirim ke {rec.user_id.name}.",
+ title="Approval Confirmed",
+ )
+
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
+
+ def action_reject_by_sales(self):
+ for rec in self:
+ if rec.create_uid != self.env.user:
+ raise UserError("❌ Hanya Sales (pembuat SJO ini) yang dapat melakukan Reject Approval.")
+
+ rec.approval_sales = False
+
+ activities = self.env['mail.activity'].search([
+ ('res_model', '=', rec._name),
+ ('res_id', '=', rec.id),
+ ])
+ activities.unlink()
+
+ rec.message_post(
+ body=f"<b>{self.env.user.name}</b> menolak approval untuk SJO '<b>{rec.name}</b>'. "
+ f"Status dikembalikan untuk MD dapat melakukan Sourcing ulang.",
+ subtype_xmlid="mail.mt_comment"
+ )
+
+ self.env.user.notify_info(
+ message=f"Approval SJO '{rec.name}' telah ditolak. MD dapat melakukan Sourcing ulang.",
+ title="Approval Ditolak",
+ )
+
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
class SourcingJobOrderLine(models.Model):
@@ -505,7 +584,7 @@ class SourcingJobOrderLine(models.Model):
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")
- product_class = fields.Many2one('product.public.category', string="Categories")
+ product_class = fields.Many2many('product.public.category', string="Categories")
state = fields.Selection([
('draft', 'Unsource'),
('sourcing', 'On Sourcing'),
@@ -537,7 +616,7 @@ class SourcingJobOrderLine(models.Model):
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 or not rec.product_class or not rec.code):
+ if is_md and (not rec.product_type or rec.product_category == False or rec.product_class == False or not rec.code):
raise UserError("MD wajib mengisi SKU, Product Type, Product Category, dan Categories!")
@api.depends('state')
diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml
index 3965c62f..f7e04f04 100644
--- a/indoteknik_custom/views/sourcing.xml
+++ b/indoteknik_custom/views/sourcing.xml
@@ -70,7 +70,7 @@
type="object"
class="btn-primary"
groups="indoteknik_custom.group_role_merchandiser"
- attrs="{'invisible': [('is_creator_same_user', '=', False)]}"/>
+ attrs="{'invisible': ['|', ('is_creator_same_user', '=', False), ('state', 'in', ['done', 'cancel'])]}"/>
<button name="action_confirm_approval"
string="Approve"
@@ -93,6 +93,13 @@
groups="indoteknik_custom.group_role_merchandiser"
attrs="{'invisible': [('can_approve_takeover', '=', False)]}"/>
+ <button name="action_convert_all_lines"
+ string="Convert Lines"
+ type="object"
+ class="btn-success"
+ groups="indoteknik_custom.group_role_merchandiser"
+ attrs="{'invisible': [('can_convert_to_product', '=', False)]}"/>
+
<button name="action_reject_takeover"
string="Reject Takeover"
type="object"
@@ -106,6 +113,15 @@
</header>
<sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="action_open_converted_products"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-cubes"
+ attrs="{'invisible': [('converted_product_count', '=', 0)]}">
+ <field name="converted_product_count" widget="statinfo" string="Products"/>
+ </button>
+ </div>
<widget name="web_ribbon"
title="COMPLETE"
bg_color="bg-success"
@@ -128,7 +144,7 @@
<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"/>
+ <field name="can_convert_to_product" invisible="1"/>
</group>
<group>
<field name="create_uid" readonly="1" widget="many2one_avatar_user"/>
@@ -151,7 +167,7 @@
<!-- MD EDIT -->
<page string="MD Lines" groups="indoteknik_custom.group_role_merchandiser">
<field name="line_md_edit_ids">
- <tree editable="bottom"
+ <tree
decoration-success="state in ('done', 'convert')"
decoration-danger="state=='cancel'"
decoration-warning="state=='sourcing'">
@@ -163,17 +179,36 @@
<field name="vendor_id"/>
<field name="tax_id"/>
<field name="subtotal"/>
- <field name="product_category"/>
- <field name="product_type"/>
- <field name="product_class"/>
<field name="state"/>
- <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
- <button name="action_convert_to_product"
+ <field name="sla"/>
+ <button name="action_convert_to_product"
string="Convert"
type="object"
icon="fa-exchange"
attrs="{'invisible': [('state', '!=', 'done')]}"/>
</tree>
+ <form string="MD Line">
+ <group>
+ <group>
+ <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" readonly="1"/>
+ <field name="sla"/>
+ </group>
+ <group>
+ <field name="product_category"/>
+ <field name="product_type"/>
+ <field name="product_class" widget="many2many_tags"/>
+ <field name="state"/>
+ <field name="reason" attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
+ </group>
+ </group>
+ </form>
</field>
</page>