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"/> + + + + + + +