From 095d9f487c6587eb1173d7676282406ee98a9d2d Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 17 Apr 2026 19:57:41 -0400 Subject: [PATCH] feat(bridge_mrp): SO workflow stage + contextual buttons (CHUNK 3/4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sale Order form now guides the user through the next step without making them navigate between screens. New computed field sale.order.x_fc_workflow_stage with 12 stages: draft → awaiting_parts → inspecting → accept_parts → assign_work → in_production → ready_to_ship → shipped → invoicing → paid → complete (+ cancelled) Driven by SO.state + x_fc_receiving_status + MO state + delivery.state + invoice payment state. Five contextual header buttons (only 1-2 visible at any time, fusion_claims pattern — invisible="x_fc_workflow_stage != 'foo'"): Mark Inspecting → flips receiving to 'inspecting' Accept Parts → flips receiving to 'accepted' + SO status to 'inspected', unlocks manager assignment Assign To Me & Release → manager claims the job, confirms all draft MOs (which auto-generates WOs already) Open Shop Floor → jumps to Plant Overview during production Mark Shipped → closes open delivery records → triggers auto-invoice per strategy Info banner shows current stage + assigned manager on the sheet so users always know where they are. New fields: sale.order.x_fc_assigned_manager_id (Many2one res.users, tracked) mrp.production.x_fc_assigned_manager_id (Many2one, propagated on MO confirm) MO.action_confirm() now pulls the assigned manager from the SO (or falls back to SO.user_id) and sets it on the MO — sets up the Manager Dashboard (chunk 2) and role-based assignment (chunk 4) to filter "my jobs" cleanly. Smoke-tested across 10 demo SOs — stages compute correctly: S00028 → ready_to_ship, S00027-25 → awaiting_parts, S00023-20 → complete/shipped, S00029 → draft. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/mrp_production.py | 17 ++ .../models/sale_order.py | 179 ++++++++++++++++++ .../views/sale_order_views.xml | 56 ++++++ 3 files changed, 252 insertions(+) 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 2d105062..8df85933 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -32,6 +32,13 @@ class MrpProduction(models.Model): string='Portal Job', help='The portal job linked to this manufacturing order.', ) + x_fc_assigned_manager_id = fields.Many2one( + 'res.users', string='Assigned Manager', + help='The manager responsible for this MO. Auto-filled when the SO ' + 'is assigned. Used by the Manager Dashboard to surface ' + 'unassigned jobs.', + tracking=True, + ) x_fc_recipe_id = fields.Many2one( 'fusion.plating.process.node', string='Recipe', @@ -421,6 +428,16 @@ class MrpProduction(models.Model): res = super().action_confirm() PortalJob = self.env['fusion.plating.portal.job'] for mo in self: + # Propagate assigned manager from SO → MO on first confirm + if mo.origin and not mo.x_fc_assigned_manager_id: + so = self.env['sale.order'].search( + [('name', '=', mo.origin)], limit=1, + ) + if so and 'x_fc_assigned_manager_id' in so._fields: + manager = so.x_fc_assigned_manager_id or so.user_id + if manager: + mo.x_fc_assigned_manager_id = manager.id + if mo.x_fc_portal_job_id: # Already linked — just update state mo.x_fc_portal_job_id.write({'state': 'in_progress'}) diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py b/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py index df6dd5d3..4c478379 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py @@ -36,6 +36,185 @@ class SaleOrder(models.Model): compute='_compute_fp_related_records', ) + # ------------------------------------------------------------------ + # Workflow stage — drives which contextual next-step button appears + # on the SO form header. Shows ONE action at a time so users aren't + # overwhelmed. Pattern mirrors fusion_claims ADP case buttons. + # ------------------------------------------------------------------ + x_fc_workflow_stage = fields.Selection( + [ + ('draft', 'Quotation — awaiting confirmation'), + ('awaiting_parts', 'Parts en route'), + ('inspecting', 'Inspecting received parts'), + ('accept_parts', 'Ready to accept parts'), + ('assign_work', 'Ready to assign manager'), + ('in_production', 'In production'), + ('ready_to_ship', 'Production complete — ready to ship'), + ('shipped', 'Shipped — awaiting invoice'), + ('invoicing', 'Awaiting invoice / payment'), + ('paid', 'Paid'), + ('complete', 'Complete'), + ('cancelled', 'Cancelled'), + ], + compute='_compute_workflow_stage', + string='Workflow Stage', + help='Current position in the SO → Ship → Invoice workflow. ' + 'Drives which next-step button is shown on the SO header.', + ) + x_fc_assigned_manager_id = fields.Many2one( + 'res.users', string='Assigned Manager', + help='The manager responsible for this job. Set automatically when ' + 'the MO is confirmed (falls back to the salesperson).', + tracking=True, + ) + + @api.depends( + 'state', 'invoice_status', + 'x_fc_receiving_status', 'x_fc_production_count', + 'x_fc_delivery_count', 'x_fc_assigned_manager_id', + ) + def _compute_workflow_stage(self): + Production = self.env['mrp.production'] + Delivery = self.env.get('fusion.plating.delivery') + for so in self: + if so.state == 'cancel': + so.x_fc_workflow_stage = 'cancelled' + continue + if so.state in ('draft', 'sent'): + so.x_fc_workflow_stage = 'draft' + continue + + # state == 'sale' or 'done' from here on + mos = Production.search([('origin', '=', so.name)]) if so.name else Production + all_mos_done = bool(mos) and all(m.state == 'done' for m in mos) + + # Any delivery marked delivered? + shipped = False + if Delivery is not None and mos: + jobs = mos.mapped('x_fc_portal_job_id') + if jobs: + shipped = bool(Delivery.search_count( + [('job_ref', 'in', jobs.mapped('name')), + ('state', '=', 'delivered')] + )) + + # Paid vs invoiced + if so.invoice_status == 'invoiced' and so.invoice_ids: + latest = so.invoice_ids.filtered(lambda i: i.state == 'posted') + all_paid = latest and all( + i.payment_state in ('paid', 'in_payment') for i in latest + ) + if shipped and all_paid: + so.x_fc_workflow_stage = 'complete' + continue + if all_paid and not shipped: + so.x_fc_workflow_stage = 'paid' + continue + + if shipped: + so.x_fc_workflow_stage = 'shipped' + continue + if all_mos_done: + so.x_fc_workflow_stage = 'ready_to_ship' + continue + + # Parts receiving state governs the early phase + recv_status = so.x_fc_receiving_status or 'not_received' + if recv_status == 'not_received': + so.x_fc_workflow_stage = 'awaiting_parts' + continue + if recv_status == 'partial' or recv_status == 'received': + so.x_fc_workflow_stage = 'inspecting' + continue + if recv_status == 'inspected': + if not so.x_fc_assigned_manager_id: + so.x_fc_workflow_stage = 'assign_work' + continue + # Manager assigned, MOs exist → in production + so.x_fc_workflow_stage = 'in_production' + continue + + # Fallback + so.x_fc_workflow_stage = 'in_production' if mos else 'awaiting_parts' + + # ------------------------------------------------------------------ + # Next-step action buttons (only one or two visible at any time) + # ------------------------------------------------------------------ + def action_fp_mark_inspected(self): + """Flip all open receiving records from draft/inspecting → inspecting.""" + self.ensure_one() + Recv = self.env.get('fp.receiving') + if Recv is None: + return False + for rec in Recv.search([('sale_order_id', '=', self.id)]): + if rec.state in ('draft',): + rec.state = 'inspecting' + self.message_post(body=_('Parts marked as inspecting.')) + return True + + def action_fp_accept_parts(self): + """Mark receiving as accepted; this unlocks manager assignment.""" + self.ensure_one() + Recv = self.env.get('fp.receiving') + if Recv is None: + return False + for rec in Recv.search([('sale_order_id', '=', self.id)]): + if rec.state in ('draft', 'inspecting'): + rec.state = 'accepted' + # flip SO receiving status to 'inspected' if possible + if 'x_fc_receiving_status' in self._fields: + self.x_fc_receiving_status = 'inspected' + self.message_post(body=_('Parts accepted — ready to assign manager.')) + return True + + def action_fp_assign_to_me(self): + """Manager claims this job (themselves) and confirms linked MOs.""" + self.ensure_one() + user = self.env.user + self.x_fc_assigned_manager_id = user.id + mos = self.env['mrp.production'].search( + [('origin', '=', self.name), ('state', '=', 'draft')] + ) + mos.action_confirm() + for mo in mos: + if 'x_fc_assigned_manager_id' in mo._fields and not mo.x_fc_assigned_manager_id: + mo.x_fc_assigned_manager_id = user.id + self.message_post( + body=_('Job assigned to %s. %d MO(s) released to the floor.') + % (user.name, len(mos)), + ) + return True + + def action_fp_mark_shipped(self): + """Mark the draft delivery as shipped (triggers auto-invoice).""" + self.ensure_one() + Delivery = self.env.get('fusion.plating.delivery') + if Delivery is None: + return False + mos = self.env['mrp.production'].search([('origin', '=', self.name)]) + jobs = mos.mapped('x_fc_portal_job_id') + deliveries = Delivery.search( + [('job_ref', 'in', jobs.mapped('name')), ('state', '!=', 'delivered')] + ) + for dlv in deliveries: + dlv.action_mark_delivered() + self.message_post( + body=_('%d delivery record(s) marked delivered. ' + 'Invoice flow triggered per invoice strategy.') + % len(deliveries), + ) + return True + + def action_fp_open_shop_floor(self): + """Jump to the Plant Overview filtered to this SO's WOs.""" + self.ensure_one() + return { + 'type': 'ir.actions.client', + 'tag': 'fp_plant_overview', + 'name': _('Shop Floor — %s') % self.name, + 'target': 'current', + } + @api.depends('name', 'state') def _compute_fp_related_records(self): Production = self.env['mrp.production'] 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 index e8c2b911..aab6f25a 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_bridge_mrp/views/sale_order_views.xml @@ -52,6 +52,62 @@ string="Deliveries"/> + + + + + + +