feat(plating): Sub 5 — order-line fields (serial, job#, thickness, revision)
Four new fields on every sale.order.line, propagated through to MO, Delivery, and Invoice for end-to-end traceability: - fp.serial registry (new model in configurator) with smart-button traceability to Sale Order, MO, Delivery, Invoice, Part. M2O on SO line; optional; user types a customer serial or clicks Generate Serial for a sequence-backed one. Reverse O2M links split across configurator (invoice) / bridge_mrp (MO) / logistics (delivery) so module load order is respected. - x_fc_job_number on SO line, auto-sequenced FP-JOB-NNNNN on SO confirm. Editable — shops can override for customer/legacy schemes. - fp.coating.thickness (new child of fp.coating.config) with per- config discrete thickness options; x_fc_thickness_id on SO line domain-filtered to the line's coating. Auto-clears when coating changes. - x_fc_revision_snapshot Char on SO line, frozen from x_fc_part_catalog_id.revision at save. Protects historical SOs from later catalog edits. Secondary "Revision" picker on the tree view lets users switch between prior revisions of the same part number; the Part M2O still surfaces only is_latest_revision rows. Reports (CoC, packing slip, invoice, BoL) pick up all four via the Sub 2 customer_line_header macro — one macro edit, four reports. Smoke on entech: 11 assertions pass including revision snapshot, generate-serial button, typed-serial create-on-fly, coating→thickness domain reset, SO confirm auto job#, and MO traceability carry. Module version bumps: fusion_plating_configurator → 19.0.12.0.0 fusion_plating_bridge_mrp → 19.0.11.0.0 fusion_plating_logistics → 19.0.2.0.0 (+depends configurator) fusion_plating_reports → 19.0.5.1.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
"name": "Fusion Plating — MRP Bridge",
|
||||
'version': '19.0.10.0.0',
|
||||
'version': '19.0.11.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
|
||||
'description': """
|
||||
@@ -74,6 +74,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'views/fp_job_consumption_views.xml',
|
||||
'views/fp_work_role_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/fp_serial_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
||||
@@ -23,3 +23,4 @@ from . import fp_qc_template
|
||||
from . import fp_quality_check
|
||||
from . import fp_thickness_reading
|
||||
from . import res_partner
|
||||
from . import fp_serial
|
||||
|
||||
26
fusion_plating/fusion_plating_bridge_mrp/models/fp_serial.py
Normal file
26
fusion_plating/fusion_plating_bridge_mrp/models/fp_serial.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
#
|
||||
# Sub 5 — attach MO reverse links to fp.serial. Defined here rather than
|
||||
# in fusion_plating_configurator because configurator loads before
|
||||
# bridge_mrp; declaring the O2M at configurator setup time would fail
|
||||
# because mrp.production.x_fc_serial_id wouldn't exist yet.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpSerial(models.Model):
|
||||
_inherit = 'fp.serial'
|
||||
|
||||
production_ids = fields.One2many(
|
||||
'mrp.production', 'x_fc_serial_id',
|
||||
string='Manufacturing Orders',
|
||||
)
|
||||
production_count = fields.Integer(compute='_compute_production_count')
|
||||
|
||||
@api.depends('production_ids')
|
||||
def _compute_production_count(self):
|
||||
for rec in self:
|
||||
rec.production_count = len(rec.production_ids)
|
||||
@@ -111,6 +111,29 @@ class MrpProduction(models.Model):
|
||||
'before this one. Copied from the first SO line that set it.',
|
||||
)
|
||||
|
||||
# ---- Sub 5 — traceability fields copied from the source SO line --------
|
||||
# Populated by bridge_mrp's _prepare_mo_vals override, which pulls these
|
||||
# from the first linked SO line. Lets the fp.serial registry show every
|
||||
# MO it spawned via a direct FK rather than heuristic origin matching.
|
||||
x_fc_serial_id = fields.Many2one(
|
||||
'fp.serial', string='Serial Number',
|
||||
ondelete='set null', index=True,
|
||||
help='Serial number from the source SO line.',
|
||||
)
|
||||
x_fc_job_number = fields.Char(
|
||||
string='Job #', index=True,
|
||||
help='Shop-floor job number from the source SO line.',
|
||||
)
|
||||
x_fc_thickness_id = fields.Many2one(
|
||||
'fp.coating.thickness', string='Thickness',
|
||||
ondelete='set null',
|
||||
help='Target coating thickness from the source SO line. Prints on the traveller.',
|
||||
)
|
||||
x_fc_revision_snapshot = fields.Char(
|
||||
string='Revision (snapshot)',
|
||||
help='Revision letter captured on the source SO line at save time.',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# T1.4 — Rework / strip-and-replate
|
||||
# ------------------------------------------------------------------
|
||||
@@ -1287,7 +1310,7 @@ class MrpProduction(models.Model):
|
||||
('active', '=', True),
|
||||
], order='id', limit=1)
|
||||
|
||||
return {
|
||||
vals = {
|
||||
'company_id': mo.company_id.id or self.env.company.id,
|
||||
'partner_id': job.partner_id.id,
|
||||
'job_ref': job.name,
|
||||
@@ -1298,6 +1321,20 @@ class MrpProduction(models.Model):
|
||||
'assigned_driver_id': driver.id if driver else False,
|
||||
'state': 'draft',
|
||||
}
|
||||
# Sub 5 — carry serial / job# / thickness / revision from the MO
|
||||
# onto the draft delivery for end-to-end traceability.
|
||||
Delivery = self.env.get('fusion.plating.delivery')
|
||||
if Delivery is not None:
|
||||
for fname in ('x_fc_serial_id', 'x_fc_job_number',
|
||||
'x_fc_thickness_id', 'x_fc_revision_snapshot'):
|
||||
if fname in Delivery._fields and fname in mo._fields:
|
||||
value = mo[fname]
|
||||
if value:
|
||||
if hasattr(value, 'id'):
|
||||
vals[fname] = value.id
|
||||
else:
|
||||
vals[fname] = value
|
||||
return vals
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# #3 — Render the cert PDF + cross-link it everywhere it's needed
|
||||
|
||||
@@ -212,6 +212,13 @@ class SaleOrder(models.Model):
|
||||
start_node = ln.x_fc_start_at_node_id
|
||||
break
|
||||
|
||||
# Sub 5 — carry serial / job# / thickness / revision from
|
||||
# the first line of the group. Single-line groups pick it up
|
||||
# cleanly; multi-line groups inherit the primary line's
|
||||
# tracking data. These fields are metadata only (reports and
|
||||
# smart buttons), so a "first line wins" rule is safe.
|
||||
primary = lines[0]
|
||||
|
||||
mo_vals = {
|
||||
'product_id': product.id,
|
||||
'product_qty': qty,
|
||||
@@ -224,6 +231,14 @@ class SaleOrder(models.Model):
|
||||
mo_vals['x_fc_recipe_id'] = recipe.id
|
||||
if start_node:
|
||||
mo_vals['x_fc_start_at_node_id'] = start_node.id
|
||||
if 'x_fc_serial_id' in Production._fields and primary.x_fc_serial_id:
|
||||
mo_vals['x_fc_serial_id'] = primary.x_fc_serial_id.id
|
||||
if 'x_fc_job_number' in Production._fields and primary.x_fc_job_number:
|
||||
mo_vals['x_fc_job_number'] = primary.x_fc_job_number
|
||||
if 'x_fc_thickness_id' in Production._fields and primary.x_fc_thickness_id:
|
||||
mo_vals['x_fc_thickness_id'] = primary.x_fc_thickness_id.id
|
||||
if 'x_fc_revision_snapshot' in Production._fields and primary.x_fc_revision_snapshot:
|
||||
mo_vals['x_fc_revision_snapshot'] = primary.x_fc_revision_snapshot
|
||||
mo = Production.create(mo_vals)
|
||||
created.append((mo, tag, len(lines)))
|
||||
self.env.cr.execute('RELEASE SAVEPOINT %s' % savepoint_name)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?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.
|
||||
Sub 5 — Serial Number registry views.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_serial_form" model="ir.ui.view">
|
||||
<field name="name">fp.serial.form</field>
|
||||
<field name="model">fp.serial</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Serial Number">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text-o"
|
||||
invisible="not sale_order_id">
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Sale Order</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_productions" type="object"
|
||||
class="oe_stat_button" icon="fa-cogs"
|
||||
invisible="production_count == 0">
|
||||
<field name="production_count" widget="statinfo" string="MOs"/>
|
||||
</button>
|
||||
<button name="action_view_deliveries" type="object"
|
||||
class="oe_stat_button" icon="fa-truck"
|
||||
invisible="delivery_count == 0">
|
||||
<field name="delivery_count" widget="statinfo" string="Deliveries"/>
|
||||
</button>
|
||||
<button name="action_view_invoices" type="object"
|
||||
class="oe_stat_button" icon="fa-money"
|
||||
invisible="invoice_count == 0">
|
||||
<field name="invoice_count" widget="statinfo" string="Invoices"/>
|
||||
</button>
|
||||
<button name="action_view_part" type="object"
|
||||
class="oe_stat_button" icon="fa-cube"
|
||||
invisible="not part_id">
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Part</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Serial #"/>
|
||||
<h1><field name="name" placeholder="e.g. SN-12345"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="customer_id" readonly="1"/>
|
||||
<field name="part_id" readonly="1"/>
|
||||
<field name="sale_order_id" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="sale_order_line_id" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" nolabel="1"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_serial_list" model="ir.ui.view">
|
||||
<field name="name">fp.serial.list</field>
|
||||
<field name="model">fp.serial</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Serial Numbers">
|
||||
<field name="name"/>
|
||||
<field name="customer_id"/>
|
||||
<field name="part_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="production_count" string="MOs"/>
|
||||
<field name="delivery_count" string="Deliveries"/>
|
||||
<field name="invoice_count" string="Invoices"/>
|
||||
<field name="create_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_serial_search" model="ir.ui.view">
|
||||
<field name="name">fp.serial.search</field>
|
||||
<field name="model">fp.serial</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="customer_id"/>
|
||||
<field name="part_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
<group>
|
||||
<filter name="group_customer" string="Customer"
|
||||
context="{'group_by': 'customer_id'}"/>
|
||||
<filter name="group_part" string="Part"
|
||||
context="{'group_by': 'part_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_serial" model="ir.actions.act_window">
|
||||
<field name="name">Serial Numbers</field>
|
||||
<field name="res_model">fp.serial</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_serial_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_fp_serial"
|
||||
name="Serial Numbers"
|
||||
parent="fusion_plating_configurator.menu_fp_sales"
|
||||
action="action_fp_serial"
|
||||
sequence="60"/>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user