From 6d907899674c4d6371ab3ef193c556b812652a05 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 13:27:29 -0400 Subject: [PATCH] =?UTF-8?q?feat(plating):=20MO=20smart=20buttons=20?= =?UTF-8?q?=E2=80=94=20Sale=20Order=20+=20Work=20Orders=20+=20Receiving?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manager / operator opening an MO had no way to jump back to the originating SO, see the WO list, or check the receiving record without going through menus. Add three smart buttons in the MO form's button-box: • [📄 Sale Order] — opens the source SO (resolved via mo.origin) • [⚙ Work Orders 9] — list view filtered by production_id • [🚚 Receiving 1] — opens the fp.receiving record (or list when multiple), filtered by mo.x_fc_sale_order_id New computed fields on mrp.production (non-stored — recomputed on view load, no migration cost): • x_fc_sale_order_id — Many2one resolved from origin • x_fc_workorder_count — len(workorder_ids) • x_fc_receiving_count — search_count on fp.receiving Each button hides itself when count is zero / link unresolvable, so brand-new draft MOs without a source SO don't show stale buttons. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_bridge_mrp/__manifest__.py | 2 +- .../models/mrp_production.py | 93 +++++++++++++++++++ .../views/mrp_production_views.xml | 24 +++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py index 8e882288..80e9e11b 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py +++ b/fusion_plating/fusion_plating_bridge_mrp/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Fusion Plating — MRP Bridge", - 'version': '19.0.6.9.0', + 'version': '19.0.6.10.0', 'category': 'Manufacturing/Plating', 'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.', 'description': """ diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py index 0caf28db..158630e8 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -131,10 +131,103 @@ class MrpProduction(models.Model): compute='_compute_consumption_count', ) + # Smart-button companions: source SO, WO count, receiving count. + # Surfaced on the MO form so operators / managers can jump back to + # the originating sale order, drill into the WO list, or inspect + # the receiving record without hunting through menus. + x_fc_sale_order_id = fields.Many2one( + 'sale.order', string='Source SO', + compute='_compute_sale_order_id', store=False, + help='The sale order this MO was created from (resolved via ' + 'mo.origin → sale.order.name).', + ) + x_fc_workorder_count = fields.Integer( + string='# Work Orders', + compute='_compute_workorder_count', + ) + x_fc_receiving_count = fields.Integer( + string='# Receiving', + compute='_compute_receiving_count', + ) + def _compute_consumption_count(self): for mo in self: mo.x_fc_consumption_count = len(mo.x_fc_consumption_ids) + @api.depends('origin') + def _compute_sale_order_id(self): + SO = self.env['sale.order'] + for mo in self: + mo.x_fc_sale_order_id = ( + SO.search([('name', '=', mo.origin)], limit=1) + if mo.origin else False + ) + + @api.depends('workorder_ids') + def _compute_workorder_count(self): + for mo in self: + mo.x_fc_workorder_count = len(mo.workorder_ids) + + def _compute_receiving_count(self): + Recv = self.env.get('fp.receiving') + for mo in self: + if Recv is None or not mo.x_fc_sale_order_id: + mo.x_fc_receiving_count = 0 + continue + mo.x_fc_receiving_count = Recv.search_count( + [('sale_order_id', '=', mo.x_fc_sale_order_id.id)] + ) + + def action_view_sale_order(self): + self.ensure_one() + if not self.x_fc_sale_order_id: + return False + return { + 'type': 'ir.actions.act_window', + 'name': _('Sale Order'), + 'res_model': 'sale.order', + 'res_id': self.x_fc_sale_order_id.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_view_workorders(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': _('Work Orders — %s') % self.name, + 'res_model': 'mrp.workorder', + 'view_mode': 'list,form', + 'domain': [('production_id', '=', self.id)], + 'context': {'default_production_id': self.id, + 'search_default_production_id': self.id}, + 'target': 'current', + } + + def action_view_receiving(self): + self.ensure_one() + Recv = self.env.get('fp.receiving') + if Recv is None or not self.x_fc_sale_order_id: + return False + recvs = Recv.search([('sale_order_id', '=', self.x_fc_sale_order_id.id)]) + if len(recvs) == 1: + return { + 'type': 'ir.actions.act_window', + 'name': _('Receiving — %s') % recvs.name, + 'res_model': 'fp.receiving', + 'res_id': recvs.id, + 'view_mode': 'form', + 'target': 'current', + } + return { + 'type': 'ir.actions.act_window', + 'name': _('Receiving for %s') % self.x_fc_sale_order_id.name, + 'res_model': 'fp.receiving', + 'view_mode': 'list,form', + 'domain': [('sale_order_id', '=', self.x_fc_sale_order_id.id)], + 'target': 'current', + } + @api.depends( 'x_fc_consumption_ids.total_cost', 'workorder_ids.duration', diff --git a/fusion_plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml b/fusion_plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml index b6e7ce3b..6ed31fcf 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml +++ b/fusion_plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml @@ -64,6 +64,30 @@ + + + + + +