diff --git a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py index c7745c92..05709e63 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py +++ b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py @@ -53,6 +53,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'views/mrp_workcenter_views.xml', 'views/mrp_workorder_views.xml', 'views/mrp_production_views.xml', + 'views/sale_order_views.xml', 'views/fp_quality_hold_views.xml', 'views/fp_batch_views.xml', 'views/fp_workorder_priority_views.xml', diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py b/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py index 92278b8b..180afc4c 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/__init__.py @@ -14,3 +14,4 @@ from . import fp_batch from . import fp_job_node_override from . import fp_job_consumption from . import account_move +from . import sale_order diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py b/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py new file mode 100644 index 00000000..df6dd5d3 --- /dev/null +++ b/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py @@ -0,0 +1,166 @@ +# -*- 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 SaleOrder(models.Model): + """Add smart-button counts + actions so the SO form is a hub for the + full production lifecycle: MOs, WOs, Portal Jobs, Quality Holds, + Certificates, Deliveries. + """ + _inherit = 'sale.order' + + x_fc_production_ids = fields.One2many( + 'mrp.production', compute='_compute_fp_related_records', + string='Manufacturing Orders', + ) + x_fc_production_count = fields.Integer( + compute='_compute_fp_related_records', + ) + x_fc_workorder_count = fields.Integer( + compute='_compute_fp_related_records', + ) + x_fc_portal_job_count = fields.Integer( + compute='_compute_fp_related_records', + ) + x_fc_quality_hold_count = fields.Integer( + compute='_compute_fp_related_records', + ) + x_fc_certificate_count = fields.Integer( + compute='_compute_fp_related_records', + ) + x_fc_delivery_count = fields.Integer( + compute='_compute_fp_related_records', + ) + + @api.depends('name', 'state') + def _compute_fp_related_records(self): + Production = self.env['mrp.production'] + PortalJob = self.env['fusion.plating.portal.job'] + QualityHold = self.env['fusion.plating.quality.hold'] + Certificate = self.env.get('fp.certificate') + Delivery = self.env.get('fusion.plating.delivery') + for so in self: + mos = Production.search([('origin', '=', so.name)]) if so.name else Production + so.x_fc_production_ids = mos + so.x_fc_production_count = len(mos) + so.x_fc_workorder_count = sum(len(mo.workorder_ids) for mo in mos) + + job_ids = mos.mapped('x_fc_portal_job_id').ids + if so.name and not job_ids: + # Fallback: portal jobs named after the MO that share origin + jobs = PortalJob.search([('name', 'in', mos.mapped('name'))]) + job_ids = jobs.ids + so.x_fc_portal_job_count = len(job_ids) + + so.x_fc_quality_hold_count = QualityHold.search_count( + [('production_id', 'in', mos.ids)] + ) if mos else 0 + + so.x_fc_certificate_count = ( + Certificate.search_count([('sale_order_id', '=', so.id)]) + if Certificate is not None and so.id else 0 + ) + + if Delivery is not None and job_ids: + job_names = PortalJob.browse(job_ids).mapped('name') + so.x_fc_delivery_count = Delivery.search_count( + [('job_ref', 'in', job_names)] + ) if job_names else 0 + else: + so.x_fc_delivery_count = 0 + + # ------------------------------------------------------------------ + # Smart button actions + # ------------------------------------------------------------------ + def action_view_productions(self): + self.ensure_one() + mos = self.env['mrp.production'].search([('origin', '=', self.name)]) + action = { + 'type': 'ir.actions.act_window', + 'name': _('Manufacturing Orders — %s') % self.name, + 'res_model': 'mrp.production', + 'domain': [('id', 'in', mos.ids)], + 'context': {'default_origin': self.name}, + } + if len(mos) == 1: + action.update({'view_mode': 'form', 'res_id': mos.id}) + else: + action['view_mode'] = 'list,form' + return action + + def action_view_workorders(self): + self.ensure_one() + mos = self.env['mrp.production'].search([('origin', '=', self.name)]) + wos = mos.mapped('workorder_ids') + action = { + 'type': 'ir.actions.act_window', + 'name': _('Work Orders — %s') % self.name, + 'res_model': 'mrp.workorder', + 'domain': [('id', 'in', wos.ids)], + 'view_mode': 'list,form,kanban', + } + if len(wos) == 1: + action.update({'view_mode': 'form', 'res_id': wos.id}) + return action + + def action_view_portal_jobs(self): + self.ensure_one() + mos = self.env['mrp.production'].search([('origin', '=', self.name)]) + jobs = mos.mapped('x_fc_portal_job_id') + if not jobs: + jobs = self.env['fusion.plating.portal.job'].search( + [('name', 'in', mos.mapped('name'))] + ) + action = { + 'type': 'ir.actions.act_window', + 'name': _('Portal Jobs — %s') % self.name, + 'res_model': 'fusion.plating.portal.job', + 'domain': [('id', 'in', jobs.ids)], + 'view_mode': 'list,form', + } + if len(jobs) == 1: + action.update({'view_mode': 'form', 'res_id': jobs.id}) + return action + + def action_view_quality_holds(self): + self.ensure_one() + mos = self.env['mrp.production'].search([('origin', '=', self.name)]) + return { + 'type': 'ir.actions.act_window', + 'name': _('Quality Holds — %s') % self.name, + 'res_model': 'fusion.plating.quality.hold', + 'domain': [('production_id', 'in', mos.ids)], + 'view_mode': 'list,form', + } + + def action_view_certificates(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': _('Certificates — %s') % self.name, + 'res_model': 'fp.certificate', + 'domain': [('sale_order_id', '=', self.id)], + 'view_mode': 'list,form', + 'context': {'default_sale_order_id': self.id, + 'default_partner_id': self.partner_id.id}, + } + + def action_view_fp_deliveries(self): + self.ensure_one() + mos = self.env['mrp.production'].search([('origin', '=', self.name)]) + jobs = mos.mapped('x_fc_portal_job_id') + if not jobs: + jobs = self.env['fusion.plating.portal.job'].search( + [('name', 'in', mos.mapped('name'))] + ) + return { + 'type': 'ir.actions.act_window', + 'name': _('Deliveries — %s') % self.name, + 'res_model': 'fusion.plating.delivery', + 'domain': [('job_ref', 'in', jobs.mapped('name'))], + 'view_mode': 'list,form', + } diff --git a/fusion_plating/fusion_plating_bridge_mrp/views/sale_order_views.xml b/fusion_plating/fusion_plating_bridge_mrp/views/sale_order_views.xml new file mode 100644 index 00000000..e8c2b911 --- /dev/null +++ b/fusion_plating/fusion_plating_bridge_mrp/views/sale_order_views.xml @@ -0,0 +1,58 @@ + + + + + + sale.order.form.fp.bridge.mrp + sale.order + + + + + + + + + + + + + +