diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py index 3d374ed1..a1feb446 100644 --- a/fusion_plating/fusion_plating_configurator/__manifest__.py +++ b/fusion_plating/fusion_plating_configurator/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Configurator', - 'version': '19.0.7.2.0', + 'version': '19.0.8.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.', 'description': """ @@ -51,6 +51,8 @@ Provides: 'views/fp_sale_description_template_views.xml', 'wizard/fp_direct_order_wizard_views.xml', 'wizard/fp_add_from_so_wizard_views.xml', + 'wizard/fp_add_from_quote_wizard_views.xml', + 'report/report_so_acknowledgement.xml', 'wizard/fp_part_catalog_import_wizard_views.xml', 'data/fp_sale_description_template_data.xml', ], diff --git a/fusion_plating/fusion_plating_configurator/models/__init__.py b/fusion_plating/fusion_plating_configurator/models/__init__.py index c20f6134..47e3702d 100644 --- a/fusion_plating/fusion_plating_configurator/models/__init__.py +++ b/fusion_plating/fusion_plating_configurator/models/__init__.py @@ -13,5 +13,6 @@ from . import fp_sale_description_template from . import fp_quote_configurator from . import sale_order from . import sale_order_line +from . import fp_sale_assembly from . import res_partner from . import fp_process_node diff --git a/fusion_plating/fusion_plating_configurator/models/fp_sale_assembly.py b/fusion_plating/fusion_plating_configurator/models/fp_sale_assembly.py new file mode 100644 index 00000000..7b15181a --- /dev/null +++ b/fusion_plating/fusion_plating_configurator/models/fp_sale_assembly.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# Part of the Fusion Plating product family. + +from odoo import api, fields, models + + +class FpSaleAssembly(models.Model): + """Hierarchical kit / assembly on a sale order line. + + A sale.order.line can carry child parts that make up an assembly. + Useful when the customer sends a kit (e.g. housing + cover + two + bolts) and each sub-part needs its own receive count + processing + status but they all bill as one kit. + + Phase D11 shipped minimal: just the data model. Full UX (hierarchy + kanban, procurement tracking) is a follow-on. + """ + _name = 'fp.sale.assembly' + _description = 'Fusion Plating - Sales Order Assembly' + _order = 'sequence, id' + + name = fields.Char(string='Assembly Name', required=True) + sequence = fields.Integer(default=10) + sale_order_line_id = fields.Many2one( + 'sale.order.line', string='Parent SO Line', + required=True, ondelete='cascade', + ) + order_id = fields.Many2one( + 'sale.order', related='sale_order_line_id.order_id', + store=True, readonly=True, + ) + partner_id = fields.Many2one( + related='order_id.partner_id', store=True, readonly=True, + ) + line_ids = fields.One2many( + 'fp.sale.assembly.line', 'assembly_id', + string='Assembly Lines', + ) + ship_to = fields.Char(string='Ship To') + count = fields.Integer(string='Count', default=1) + procured_count = fields.Integer( + string='Procured Count', + compute='_compute_procured_count', + ) + completed_at = fields.Datetime(string='Completed At') + + @api.depends('line_ids.procured_qty') + def _compute_procured_count(self): + for rec in self: + rec.procured_count = sum(rec.line_ids.mapped('procured_qty')) + + +class FpSaleAssemblyLine(models.Model): + _name = 'fp.sale.assembly.line' + _description = 'Fusion Plating - Assembly Line' + _order = 'sequence, id' + + name = fields.Char(string='Part Number', required=True) + sequence = fields.Integer(default=10) + assembly_id = fields.Many2one( + 'fp.sale.assembly', required=True, ondelete='cascade', + ) + part_catalog_id = fields.Many2one( + 'fp.part.catalog', string='Part', + ) + qty_per_assembly = fields.Float(string='Qty / Assembly', default=1.0) + procured_qty = fields.Float(string='Procured Qty', default=0.0) diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order.py b/fusion_plating/fusion_plating_configurator/models/sale_order.py index 78523a69..81d32a16 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order.py @@ -167,6 +167,29 @@ class SaleOrder(models.Model): string='Part Numbers', compute='_compute_part_numbers_summary', ) + x_fc_signed_at = fields.Datetime( + string='Signed On', tracking=True, + help='When the customer signed / accepted this quote.', + ) + x_fc_signed_by = fields.Char( + string='Signed By', tracking=True, + help='Name of the customer signatory.', + ) + x_fc_is_signed = fields.Boolean( + string='Signed', compute='_compute_is_signed', store=True, + ) + + @api.depends('x_fc_signed_at') + def _compute_is_signed(self): + for rec in self: + rec.x_fc_is_signed = bool(rec.x_fc_signed_at) + + def action_mark_signed(self): + self.ensure_one() + self.write({ + 'x_fc_signed_at': fields.Datetime.now(), + 'x_fc_signed_by': self.partner_id.name, + }) @api.depends('state') def _compute_email_status(self): @@ -253,6 +276,114 @@ class SaleOrder(models.Model): 'context': {'search_default_group_production_id': 1}, } + # ---- Quick-nav counts for smart buttons (Phase D9 / D14) ---- + x_fc_invoice_count = fields.Integer( + string='Invoices', compute='_compute_nav_counts', + ) + x_fc_ncr_count = fields.Integer( + string='NCRs', compute='_compute_nav_counts', + ) + x_fc_picking_count = fields.Integer( + string='Pickings', compute='_compute_nav_counts', + ) + x_fc_attachment_count = fields.Integer( + string='Files', compute='_compute_nav_counts', + ) + + def _compute_nav_counts(self): + NCR = self.env.get('fusion.plating.ncr') + for rec in self: + rec.x_fc_invoice_count = len(rec.invoice_ids) + rec.x_fc_picking_count = len(rec.picking_ids) + rec.x_fc_attachment_count = self.env['ir.attachment'].sudo().search_count([ + ('res_model', '=', 'sale.order'), + ('res_id', '=', rec.id), + ]) + if NCR and 'sale_order_id' in NCR._fields: + rec.x_fc_ncr_count = NCR.sudo().search_count([ + ('sale_order_id', '=', rec.id), + ]) + else: + rec.x_fc_ncr_count = 0 + + def action_view_invoices(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Invoices', + 'res_model': 'account.move', + 'view_mode': 'list,form', + 'domain': [('id', 'in', self.invoice_ids.ids)], + } + + def action_view_pickings(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Deliveries / Pickings', + 'res_model': 'stock.picking', + 'view_mode': 'list,form', + 'domain': [('id', 'in', self.picking_ids.ids)], + } + + def action_view_ncrs(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'NCRs', + 'res_model': 'fusion.plating.ncr', + 'view_mode': 'list,form', + 'domain': [('sale_order_id', '=', self.id)], + } + + def action_view_files(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Files', + 'res_model': 'ir.attachment', + 'view_mode': 'kanban,list,form', + 'domain': [ + ('res_model', '=', 'sale.order'), + ('res_id', '=', self.id), + ], + 'context': { + 'default_res_model': 'sale.order', + 'default_res_id': self.id, + }, + } + + def action_view_bom_items(self): + """Open SO lines grouped by part catalog (Phase D2).""" + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'BOM Items - %s' % self.name, + 'res_model': 'sale.order.line', + 'view_mode': 'kanban,list,form', + 'views': [ + (self.env.ref('fusion_plating_configurator.view_sale_order_line_bom_kanban').id, 'kanban'), + (False, 'list'), + (False, 'form'), + ], + 'domain': [('order_id', '=', self.id)], + } + + def action_view_wo_perspective(self): + """Open SO lines grouped by WO tag (Phase D10).""" + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Lines by WO - %s' % self.name, + 'res_model': 'sale.order.line', + 'view_mode': 'kanban,list', + 'views': [ + (self.env.ref('fusion_plating_configurator.view_sale_order_line_wo_kanban').id, 'kanban'), + (False, 'list'), + ], + 'domain': [('order_id', '=', self.id)], + } + @api.depends('commitment_date') def _compute_deadline_countdown(self): from datetime import datetime diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order_line.py b/fusion_plating/fusion_plating_configurator/models/sale_order_line.py index c6e923ee..ec71b8f3 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order_line.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order_line.py @@ -46,3 +46,17 @@ class SaleOrderLine(models.Model): string='Linked Quote', help='Quote that seeded this line. Links back for audit trail.', ) + x_fc_archived = fields.Boolean( + string='Archived', + default=False, + help='Archived lines are hidden from the default list view but ' + 'preserved for audit. Useful when a part is cancelled mid-order.', + ) + + def action_archive_line(self): + self.write({'x_fc_archived': True}) + return True + + def action_unarchive_line(self): + self.write({'x_fc_archived': False}) + return True diff --git a/fusion_plating/fusion_plating_configurator/report/report_so_acknowledgement.xml b/fusion_plating/fusion_plating_configurator/report/report_so_acknowledgement.xml new file mode 100644 index 00000000..b0ca8732 --- /dev/null +++ b/fusion_plating/fusion_plating_configurator/report/report_so_acknowledgement.xml @@ -0,0 +1,162 @@ + + + + + + Sales Order Acknowledgement + sale.order + qweb-pdf + fusion_plating_configurator.report_fp_so_acknowledgement_doc + fusion_plating_configurator.report_fp_so_acknowledgement_doc + + report + 'Acknowledgement - %s' % object.name + + + + + diff --git a/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv b/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv index d76b9be3..a49ee5f5 100644 --- a/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv +++ b/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv @@ -23,6 +23,14 @@ access_fp_direct_order_line_estimator,fp.direct.order.line.estimator,model_fp_di access_fp_direct_order_line_manager,fp.direct.order.line.manager,model_fp_direct_order_line,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_add_from_so_wizard_estimator,fp.add.from.so.wizard.estimator,model_fp_add_from_so_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1 access_fp_add_from_so_wizard_manager,fp.add.from.so.wizard.manager,model_fp_add_from_so_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1 +access_fp_add_from_quote_wizard_estimator,fp.add.from.quote.wizard.estimator,model_fp_add_from_quote_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1 +access_fp_add_from_quote_wizard_manager,fp.add.from.quote.wizard.manager,model_fp_add_from_quote_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1 +access_fp_sale_assembly_user,fp.sale.assembly.user,model_fp_sale_assembly,base.group_user,1,0,0,0 +access_fp_sale_assembly_estimator,fp.sale.assembly.estimator,model_fp_sale_assembly,fusion_plating_configurator.group_fp_estimator,1,1,1,1 +access_fp_sale_assembly_manager,fp.sale.assembly.manager,model_fp_sale_assembly,fusion_plating.group_fusion_plating_manager,1,1,1,1 +access_fp_sale_assembly_line_user,fp.sale.assembly.line.user,model_fp_sale_assembly_line,base.group_user,1,0,0,0 +access_fp_sale_assembly_line_estimator,fp.sale.assembly.line.estimator,model_fp_sale_assembly_line,fusion_plating_configurator.group_fp_estimator,1,1,1,1 +access_fp_sale_assembly_line_manager,fp.sale.assembly.line.manager,model_fp_sale_assembly_line,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_part_import_wizard_estimator,fp.part.catalog.import.wizard.estimator,model_fp_part_catalog_import_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1 access_fp_part_import_wizard_manager,fp.part.catalog.import.wizard.manager,model_fp_part_catalog_import_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1 access_fp_customer_price_list_operator,fp.customer.price.list.operator,model_fp_customer_price_list,fusion_plating.group_fusion_plating_operator,1,0,0,0 diff --git a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml index fbbe8c35..19f29706 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -41,6 +41,48 @@ + + + + +