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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
166
fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py
Normal file
166
fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py
Normal 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',
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user