feat(bridge_mrp): SO smart buttons for full production lifecycle

Sale Order form now hubs the full flow — Manufacturing, Work Orders,
Portal Jobs, Quality Holds, Certificates, Deliveries — hidden when
count == 0. Clicking each jumps to the filtered list/form so users
can drill in without leaving the SO.

Counts are computed on the fly from: mrp.production.origin == SO.name,
production.workorder_ids, production.x_fc_portal_job_id, quality.hold
production_id, fp.certificate.sale_order_id, fp.delivery.job_ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-17 02:33:21 -04:00
parent 838b41cb89
commit adc27c637a
4 changed files with 226 additions and 0 deletions

View File

@@ -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

View File

@@ -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',
}