feat(plating): MO smart buttons — Sale Order + Work Orders + Receiving

Manager / operator opening an MO had no way to jump back to the
originating SO, see the WO list, or check the receiving record
without going through menus. Add three smart buttons in the MO
form's button-box:

  • [📄 Sale Order] — opens the source SO (resolved via mo.origin)
  • [⚙ Work Orders 9] — list view filtered by production_id
  • [🚚 Receiving 1] — opens the fp.receiving record (or list when
    multiple), filtered by mo.x_fc_sale_order_id

New computed fields on mrp.production (non-stored — recomputed on
view load, no migration cost):
  • x_fc_sale_order_id      — Many2one resolved from origin
  • x_fc_workorder_count    — len(workorder_ids)
  • x_fc_receiving_count    — search_count on fp.receiving

Each button hides itself when count is zero / link unresolvable, so
brand-new draft MOs without a source SO don't show stale buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 13:27:29 -04:00
parent 5c3e7a3cf3
commit 6d90789967
3 changed files with 118 additions and 1 deletions

View File

@@ -5,7 +5,7 @@
{
"name": "Fusion Plating — MRP Bridge",
'version': '19.0.6.9.0',
'version': '19.0.6.10.0',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """

View File

@@ -131,10 +131,103 @@ class MrpProduction(models.Model):
compute='_compute_consumption_count',
)
# Smart-button companions: source SO, WO count, receiving count.
# Surfaced on the MO form so operators / managers can jump back to
# the originating sale order, drill into the WO list, or inspect
# the receiving record without hunting through menus.
x_fc_sale_order_id = fields.Many2one(
'sale.order', string='Source SO',
compute='_compute_sale_order_id', store=False,
help='The sale order this MO was created from (resolved via '
'mo.origin → sale.order.name).',
)
x_fc_workorder_count = fields.Integer(
string='# Work Orders',
compute='_compute_workorder_count',
)
x_fc_receiving_count = fields.Integer(
string='# Receiving',
compute='_compute_receiving_count',
)
def _compute_consumption_count(self):
for mo in self:
mo.x_fc_consumption_count = len(mo.x_fc_consumption_ids)
@api.depends('origin')
def _compute_sale_order_id(self):
SO = self.env['sale.order']
for mo in self:
mo.x_fc_sale_order_id = (
SO.search([('name', '=', mo.origin)], limit=1)
if mo.origin else False
)
@api.depends('workorder_ids')
def _compute_workorder_count(self):
for mo in self:
mo.x_fc_workorder_count = len(mo.workorder_ids)
def _compute_receiving_count(self):
Recv = self.env.get('fp.receiving')
for mo in self:
if Recv is None or not mo.x_fc_sale_order_id:
mo.x_fc_receiving_count = 0
continue
mo.x_fc_receiving_count = Recv.search_count(
[('sale_order_id', '=', mo.x_fc_sale_order_id.id)]
)
def action_view_sale_order(self):
self.ensure_one()
if not self.x_fc_sale_order_id:
return False
return {
'type': 'ir.actions.act_window',
'name': _('Sale Order'),
'res_model': 'sale.order',
'res_id': self.x_fc_sale_order_id.id,
'view_mode': 'form',
'target': 'current',
}
def action_view_workorders(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Work Orders — %s') % self.name,
'res_model': 'mrp.workorder',
'view_mode': 'list,form',
'domain': [('production_id', '=', self.id)],
'context': {'default_production_id': self.id,
'search_default_production_id': self.id},
'target': 'current',
}
def action_view_receiving(self):
self.ensure_one()
Recv = self.env.get('fp.receiving')
if Recv is None or not self.x_fc_sale_order_id:
return False
recvs = Recv.search([('sale_order_id', '=', self.x_fc_sale_order_id.id)])
if len(recvs) == 1:
return {
'type': 'ir.actions.act_window',
'name': _('Receiving — %s') % recvs.name,
'res_model': 'fp.receiving',
'res_id': recvs.id,
'view_mode': 'form',
'target': 'current',
}
return {
'type': 'ir.actions.act_window',
'name': _('Receiving for %s') % self.x_fc_sale_order_id.name,
'res_model': 'fp.receiving',
'view_mode': 'list,form',
'domain': [('sale_order_id', '=', self.x_fc_sale_order_id.id)],
'target': 'current',
}
@api.depends(
'x_fc_consumption_ids.total_cost',
'workorder_ids.duration',

View File

@@ -64,6 +64,30 @@
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<!-- Sale Order — back to the customer's order -->
<button name="action_view_sale_order" type="object"
class="oe_stat_button" icon="fa-file-text-o"
invisible="not x_fc_sale_order_id">
<div class="o_stat_info">
<span class="o_stat_value">
<field name="x_fc_sale_order_id" nolabel="1" readonly="1"/>
</span>
<span class="o_stat_text">Sale Order</span>
</div>
</button>
<!-- Work Orders — drill into the WO list -->
<button name="action_view_workorders" type="object"
class="oe_stat_button" icon="fa-cogs">
<field name="x_fc_workorder_count" widget="statinfo"
string="Work Orders"/>
</button>
<!-- Receiving — link to the parts-receiving record(s) -->
<button name="action_view_receiving" type="object"
class="oe_stat_button" icon="fa-truck"
invisible="x_fc_receiving_count == 0">
<field name="x_fc_receiving_count" widget="statinfo"
string="Receiving"/>
</button>
<button name="action_configure_recipe_steps" type="object"
class="oe_stat_button" icon="fa-sliders"
invisible="not x_fc_recipe_id">