From ac64cc0792558e7c4922c6fc9c2f0a4c5c532967 Mon Sep 17 00:00:00 2001 From: HafidBuroiroh Date: Fri, 31 Oct 2025 15:36:09 +0700 Subject: push --- indoteknik_custom/models/product_template.py | 5 + indoteknik_custom/models/sourcing_job_order.py | 217 +++++++++++++++++-------- indoteknik_custom/views/sourcing.xml | 51 +++++- 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"✅ Berhasil mengonversi {len(created_products)} produk baru:
" + + "
".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 {rec.name} otomatis disetujui karena pembuat dan MD adalah orang yang sama ({self.env.user.name}).", - 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 {rec.name} 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"{self.env.user.name} menolak approval untuk SJO '{rec.name}'. " + 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'])]}"/> + - + @@ -151,7 +167,7 @@ - @@ -163,17 +179,36 @@ - - - - -