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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user