From d13517071c4efe9aedbd97143e8274f7e1087f20 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 12 Apr 2026 19:24:54 -0400 Subject: [PATCH] feat(receiving): SO auto-create + MRP soft gate + menu structure Co-Authored-By: Claude Opus 4.6 (1M context) --- .../models/mrp_production.py | 52 +++++++++++++++- .../models/sale_order.py | 60 ++++++++++++++++++- .../views/fp_receiving_menu.xml | 49 ++++++++++++++- .../views/sale_order_views.xml | 25 +++++++- 4 files changed, 182 insertions(+), 4 deletions(-) diff --git a/fusion-plating/fusion_plating_receiving/models/mrp_production.py b/fusion-plating/fusion_plating_receiving/models/mrp_production.py index 0c8fe912..e2fc38f1 100644 --- a/fusion-plating/fusion_plating_receiving/models/mrp_production.py +++ b/fusion-plating/fusion_plating_receiving/models/mrp_production.py @@ -2,4 +2,54 @@ # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. -# Placeholder — implemented in a later task. + +import logging + +from odoo import models, _ + +_logger = logging.getLogger(__name__) + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + def action_confirm(self): + """Soft gate: warn if parts haven't been received yet. + + Checks the linked sale order's receiving status. If parts + are not yet received, logs a warning. This is informational + only -- it does not block confirmation. The gate is soft + because handshake deals and urgent jobs need flexibility. + """ + for production in self: + so = production._get_source_sale_order() + if so and so.x_fc_receiving_status in ('not_received', False): + _logger.warning( + 'MO %s: parts not yet received for SO %s (receiving status: %s). ' + 'Proceeding with confirmation.', + production.name, so.name, so.x_fc_receiving_status, + ) + production.message_post( + body=_( + 'Warning: Parts not yet received for sale order ' + '%s. ' + 'Manufacturing confirmed without receiving verification.' + ) % (so.id, so.name), + ) + return super().action_confirm() + + def _get_source_sale_order(self): + """Find the sale order linked to this MO via origin field.""" + self.ensure_one() + if not self.origin: + return False + # origin may contain SO name like "S00001" or configurator ref "CFG-00001" + so = self.env['sale.order'].search( + [('name', '=', self.origin)], limit=1, + ) + if not so: + # Try matching by origin containing the SO name + so = self.env['sale.order'].search( + [('name', 'ilike', self.origin)], limit=1, + ) + return so or False diff --git a/fusion-plating/fusion_plating_receiving/models/sale_order.py b/fusion-plating/fusion_plating_receiving/models/sale_order.py index 0c8fe912..e9267a1b 100644 --- a/fusion-plating/fusion_plating_receiving/models/sale_order.py +++ b/fusion-plating/fusion_plating_receiving/models/sale_order.py @@ -2,4 +2,62 @@ # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. -# Placeholder — implemented in a later task. + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + x_fc_receiving_ids = fields.One2many( + 'fp.receiving', 'sale_order_id', string='Receiving Records', + ) + x_fc_receiving_count = fields.Integer( + string='Receiving Count', compute='_compute_receiving_count', + ) + + @api.depends('x_fc_receiving_ids') + def _compute_receiving_count(self): + for rec in self: + rec.x_fc_receiving_count = len(rec.x_fc_receiving_ids) + + def action_confirm(self): + """Override to auto-create receiving record on SO confirmation.""" + res = super().action_confirm() + for order in self: + # Only create if no receiving record exists yet + if not order.x_fc_receiving_ids: + total_qty = sum(order.order_line.mapped('product_uom_qty')) + receiving_vals = { + 'sale_order_id': order.id, + 'expected_qty': int(total_qty), + 'line_ids': [], + } + # Auto-create lines from SO lines + for line in order.order_line: + receiving_vals['line_ids'].append((0, 0, { + 'part_number': order.x_fc_part_catalog_id.part_number if order.x_fc_part_catalog_id else '', + 'description': line.name or '', + 'expected_qty': int(line.product_uom_qty), + })) + self.env['fp.receiving'].create(receiving_vals) + return res + + def action_view_receiving(self): + """Smart button action to view receiving records.""" + self.ensure_one() + if self.x_fc_receiving_count == 1: + return { + 'type': 'ir.actions.act_window', + 'res_model': 'fp.receiving', + 'res_id': self.x_fc_receiving_ids[0].id, + 'view_mode': 'form', + 'target': 'current', + } + return { + 'type': 'ir.actions.act_window', + 'res_model': 'fp.receiving', + 'view_mode': 'list,form', + 'domain': [('sale_order_id', '=', self.id)], + 'target': 'current', + } diff --git a/fusion-plating/fusion_plating_receiving/views/fp_receiving_menu.xml b/fusion-plating/fusion_plating_receiving/views/fp_receiving_menu.xml index 85d8c23e..10f0ee28 100644 --- a/fusion-plating/fusion_plating_receiving/views/fp_receiving_menu.xml +++ b/fusion-plating/fusion_plating_receiving/views/fp_receiving_menu.xml @@ -1,2 +1,49 @@ - + + + + + + Pending Inspection + fp.receiving + list,form + [('state', '=', 'draft')] + + + + Discrepancies + fp.receiving + list,form + [('state', '=', 'discrepancy')] + + + + + + + + + + + + diff --git a/fusion-plating/fusion_plating_receiving/views/sale_order_views.xml b/fusion-plating/fusion_plating_receiving/views/sale_order_views.xml index 85d8c23e..d4a77f95 100644 --- a/fusion-plating/fusion_plating_receiving/views/sale_order_views.xml +++ b/fusion-plating/fusion_plating_receiving/views/sale_order_views.xml @@ -1,2 +1,25 @@ - + + + + + + sale.order.form.fp.receiving + sale.order + + + + + + + + +