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

@@ -53,6 +53,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'views/mrp_workcenter_views.xml',
'views/mrp_workorder_views.xml',
'views/mrp_production_views.xml',
'views/sale_order_views.xml',
'views/fp_quality_hold_views.xml',
'views/fp_batch_views.xml',
'views/fp_workorder_priority_views.xml',

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

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Adds production-lifecycle smart buttons to the Sale Order form:
Manufacturing, Work Orders, Portal Job, Quality Holds, Certificates, Deliveries.
-->
<odoo>
<record id="view_sale_order_form_fp_bridge_mrp" model="ir.ui.view">
<field name="name">sale.order.form.fp.bridge.mrp</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_productions" type="object"
class="oe_stat_button" icon="fa-industry"
invisible="x_fc_production_count == 0">
<field name="x_fc_production_count" widget="statinfo"
string="Manufacturing"/>
</button>
<button name="action_view_workorders" type="object"
class="oe_stat_button" icon="fa-cogs"
invisible="x_fc_workorder_count == 0">
<field name="x_fc_workorder_count" widget="statinfo"
string="Work Orders"/>
</button>
<button name="action_view_portal_jobs" type="object"
class="oe_stat_button" icon="fa-globe"
invisible="x_fc_portal_job_count == 0">
<field name="x_fc_portal_job_count" widget="statinfo"
string="Portal Jobs"/>
</button>
<button name="action_view_quality_holds" type="object"
class="oe_stat_button" icon="fa-exclamation-triangle"
invisible="x_fc_quality_hold_count == 0">
<field name="x_fc_quality_hold_count" widget="statinfo"
string="Quality Holds"/>
</button>
<button name="action_view_certificates" type="object"
class="oe_stat_button" icon="fa-certificate"
invisible="x_fc_certificate_count == 0">
<field name="x_fc_certificate_count" widget="statinfo"
string="Certificates"/>
</button>
<button name="action_view_fp_deliveries" type="object"
class="oe_stat_button" icon="fa-truck"
invisible="x_fc_delivery_count == 0">
<field name="x_fc_delivery_count" widget="statinfo"
string="Deliveries"/>
</button>
</xpath>
</field>
</record>
</odoo>