diff options
| author | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2025-10-31 15:36:09 +0700 |
|---|---|---|
| committer | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2025-10-31 15:36:09 +0700 |
| commit | ac64cc0792558e7c4922c6fc9c2f0a4c5c532967 (patch) | |
| tree | 1f8cc6ed359f978dce52fab629ea09bd331ba996 | |
| parent | a41ed8ed1db45b019d3c3f7d31a01d9658c55f78 (diff) | |
push
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 5 | ||||
| -rw-r--r-- | indoteknik_custom/models/sourcing_job_order.py | 217 | ||||
| -rw-r--r-- | 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"â
<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> |
