fp.serial registry with smart-button traceability (SO, MO, Delivery, Invoice, Part), fp.coating.thickness child table per coating config, four new fields on sale.order.line propagating through to MO / Delivery / Invoice via existing hooks. Revision picker with latest- only default + switcher + snapshot Char. Reports pick up all four via Sub 2's customer-line-header macro. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
Sub 5 — Order-Line Fields (Serial, Job#, Thickness, Revision)
Date: 2026-04-22
Module scope: fusion_plating_configurator (primary; owns SO line, part catalog, coating
config); field mirrors on mrp.production, fusion.plating.delivery, account.move.line via
fusion_plating_bridge_mrp, fusion_plating_logistics, and fusion_plating_configurator
respectively.
Status: Design approved; implementing in this session.
Predecessor context: Fine-Tuning Initiative, entry in fusion_plating/CLAUDE.md; builds on
Sub 2 (part data model) and Sub 3 (default process).
1. Scope
Four new fields on every sale.order.line plus propagation to MO, Delivery, and Invoice,
plus two new supporting models:
| Field | Model | Shape | Default |
|---|---|---|---|
x_fc_serial_id |
sale.order.line |
M2O → fp.serial |
None, optional, never auto-generated |
x_fc_job_number |
sale.order.line |
Char | Auto-sequenced on SO confirm, editable |
x_fc_thickness_id |
sale.order.line |
M2O → fp.coating.thickness (domain = coating config) |
None, optional |
x_fc_revision_snapshot |
sale.order.line |
Char | Filled from part_catalog_id.revision at save |
Plus:
- New model
fp.serial— registry of serial numbers with smart-button traceability. - New model
fp.coating.thickness— child offp.coating.config, one row per allowed thickness. - Revision picker UX — the SO line Part M2O filters to
is_latest_revision=Trueby default; a secondary "Revision" selector lets users pick a prior revision.
Out of scope
- Per-piece serial tracking (Sub 5 is one serial per SO line, matching Option A from brainstorm).
- Job# uniqueness enforcement (soft warn only, not SQL constraint).
- Customer-specific thickness overrides (Sub 6 territory if ever needed).
- Retroactive backfill of serial/job#/thickness on historical SOs/MOs.
2. Data Model
2.1 fp.serial (new model)
Registry of serial numbers. One record per "occurrence of a part on an order line" — same part ordered six months later gets a different serial.
class FpSerial(models.Model):
_name = 'fp.serial'
_description = 'Fusion Plating — Serial Number'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc, id desc'
name = fields.Char(required=True, tracking=True,
help='Customer-supplied serial or auto-generated sequence.')
company_id = fields.Many2one('res.company', required=True,
default=lambda s: s.env.company)
sale_order_line_id = fields.Many2one('sale.order.line',
string='Source Sale Order Line',
ondelete='set null', tracking=True)
sale_order_id = fields.Many2one(related='sale_order_line_id.order_id', store=True)
customer_id = fields.Many2one(related='sale_order_line_id.order_id.partner_id', store=True)
part_id = fields.Many2one(related='sale_order_line_id.x_fc_part_catalog_id', store=True)
# Reverse links (populated by x_fc_serial_id planted on each model).
production_ids = fields.One2many('mrp.production',
'x_fc_serial_id',
string='Manufacturing Orders')
delivery_ids = fields.One2many('fusion.plating.delivery',
'x_fc_serial_id',
string='Deliveries')
invoice_line_ids = fields.One2many('account.move.line',
'x_fc_serial_id',
string='Invoice Lines')
production_count = fields.Integer(compute='_compute_counts')
delivery_count = fields.Integer(compute='_compute_counts')
invoice_count = fields.Integer(compute='_compute_counts')
_sql_constraints = [
('fp_serial_name_company_uniq', 'unique(company_id, name)',
'Serial number must be unique within the company.'),
]
Smart-button form view with links to Sale Order, MO, Delivery, Invoice, Part.
2.2 fp.coating.thickness (new model)
Child of fp.coating.config, one row per allowed thickness for that coating.
class FpCoatingThickness(models.Model):
_name = 'fp.coating.thickness'
_description = 'Coating Thickness Option'
_order = 'coating_config_id, sequence, value'
coating_config_id = fields.Many2one('fp.coating.config',
required=True, ondelete='cascade')
value = fields.Float(digits=(10, 4), required=True)
uom = fields.Selection(
[('mils', 'mils (0.001 in)'),
('microns', 'microns (µm)'),
('inches', 'inches'),
('mm', 'mm')],
required=True, default='mils')
sequence = fields.Integer(default=10)
display_name = fields.Char(compute='_compute_display_name', store=True)
active = fields.Boolean(default=True)
@api.depends('value', 'uom')
def _compute_display_name(self):
for rec in self:
rec.display_name = f'{rec.value:g} {dict(self._fields["uom"].selection).get(rec.uom, rec.uom) if rec.uom else ""}'.strip()
2.3 sale.order.line additions
x_fc_serial_id = fields.Many2one('fp.serial', string='Serial Number',
ondelete='set null', copy=False, tracking=True)
x_fc_job_number = fields.Char(string='Job #', copy=False, tracking=True,
index=True)
x_fc_thickness_id = fields.Many2one(
'fp.coating.thickness', string='Thickness',
domain="[('coating_config_id', '=', x_fc_coating_config_id)]",
ondelete='set null', tracking=True)
x_fc_revision_snapshot = fields.Char(
string='Revision (snapshot)', copy=False, readonly=True,
help='Revision letter/number at the moment this line was saved. '
'Preserved even if the part catalog revision is later edited '
'or the catalog row is removed.')
create()+write()override populatesx_fc_revision_snapshotfromx_fc_part_catalog_id.revisionwhenever the part changes (or the field is empty).- Onchange on
x_fc_part_catalog_idclearsx_fc_thickness_idif the picked thickness doesn't belong to the line's coating config.
2.4 Revision picker mechanics
Two approaches on the SO line form:
- Part M2O domain — the Part field uses
domain="[('is_latest_revision', '=', True)]"context-filtered. The dropdown only shows one entry per part number (the latest rev). - Revision Selector — a new non-stored compute field on
sale.order.line:When the user changesx_fc_available_revisions = fields.Many2many( 'fp.part.catalog', compute='_compute_available_revisions') x_fc_revision_pick = fields.Many2one( 'fp.part.catalog', string='Revision', domain="[('id', 'in', x_fc_available_revisions)]", compute='_compute_revision_pick', inverse='_inverse_revision_pick', store=False)x_fc_revision_pickto a different revision of the same part number, the inverse re-pointsx_fc_part_catalog_idto that revision.
2.5 mrp.production, fusion.plating.delivery, account.move.line
Each gains the same four fields:
x_fc_serial_id(M2O, index for fast reverse lookup)x_fc_job_number(Char)x_fc_thickness_id(M2O)x_fc_revision_snapshot(Char)
Populated when the downstream record is created from its upstream SO line:
- MO: inject in
sale_mrp's procurement-to-MO hook (_prepare_mo_valsoverride infusion_plating_bridge_mrp). - Delivery: already created from the MO in
fusion_plating_logistics— add field carry-over in the create hook. - Invoice line: Sub 2 already overrides
_prepare_invoice_lineinfusion_plating_configurator; extend that hook to carry the four new fields.
2.6 fp.coating.config additions
thickness_option_ids = fields.One2many(
'fp.coating.thickness', 'coating_config_id',
string='Thickness Options')
2.7 Sequences
Two new sequences in data/fp_sub5_sequence_data.xml:
<record id="seq_fp_serial" model="ir.sequence">
<field name="name">Fusion Plating Serial Number</field>
<field name="code">fp.serial</field>
<field name="prefix">FP-SN-</field>
<field name="padding">5</field>
</record>
<record id="seq_fp_job_number" model="ir.sequence">
<field name="name">Fusion Plating Job Number</field>
<field name="code">fp.job.number</field>
<field name="prefix">FP-JOB-</field>
<field name="padding">5</field>
</record>
2.8 Job# auto-assign
Override sale.order.action_confirm() to assign x_fc_job_number to each line that doesn't
have one:
def action_confirm(self):
res = super().action_confirm()
for so in self:
for line in so.order_line:
if not line.x_fc_job_number and not line.display_type:
line.x_fc_job_number = self.env['ir.sequence'].next_by_code('fp.job.number')
return res
2.9 "Generate Serial" button
On the sale.order.line form (expanded row), a button action_generate_serial that:
- Creates
fp.serialwithname = next_by_code('fp.serial'),sale_order_line_id = self.id. - Writes
self.x_fc_serial_id = new_serial.id. - Returns
False(stays on SO form).
Typing a serial into the m2o widget uses standard "Create 'SN-12345'" which creates the
fp.serial with that exact name.
3. Views
3.1 Sale Order — order-lines tree
Inherit view_order_form — add columns:
x_fc_serial_id(widgetmany2one, with create option)x_fc_job_number(Char)x_fc_thickness_id(with domain)x_fc_revision_snapshot(readonly)
On the line's optional drawer (expand-row): add "Generate Serial" button + "Revision" picker.
3.2 Direct-order wizard line
Extend fp_direct_order_wizard_views.xml to add the same 4 fields.
3.3 Coating Config form
New tab Thickness Options with inline O2M editor over thickness_option_ids.
3.4 fp.serial form + list + search
- List: Reference, Customer, Part, SO, Created On, State.
- Form: Header with name + state; body with source links; smart buttons for SO / MO / Delivery / Invoice / Part.
- Menu: Plating → Sales → Serials.
3.5 MO, Delivery, Invoice form inherits
Add a small read-only block "Traceability" showing the four fields when populated.
4. Reports
All four fields are added to Sub 2's customer-line-header macro so they print automatically on
every report that already uses it:
- CoC (
fusion_plating_reports.report_coc) — Serial, Job#, Thickness, Rev snapshot - Packing slip (
report_fp_packing_slip) — Serial, Job#, Rev snapshot - Invoice (
report_fp_invoice) — Serial, Job#, Rev snapshot - WO Traveller (
report_fp_job_traveller) — Job#, Thickness, Rev snapshot
One macro edit, all four reports pick it up.
5. Security
fp.serial— read for operator; CRUD for supervisor/manager/admin (same pattern as existing configurator models).fp.coating.thickness— read for operator; CRUD for manager/admin.- Company-scoped
ir.ruleonfp.serialviacompany_id in company_ids.
6. Migration
- New fields and models only; no backfill.
- Versions:
fusion_plating_configurator→19.0.12.0.0,fusion_plating_bridge_mrp→ bump if sale_mrp hook change,fusion_plating_logistics→ bump if delivery carry-over added,fusion_plating_reports→ bump for macro updates. fusion_plating_quality(Sub 4) untouched.
7. Testing Strategy
7.1 End-to-end traceability
- Create SO line with part + coating + thickness; user types "SN-12345" → assert
fp.serialcreated and linked. - Confirm SO → assert
x_fc_job_numberauto-populated withFP-JOB-NNNNNsequence. - MO auto-created → assert
x_fc_serial_id,x_fc_job_number,x_fc_thickness_id,x_fc_revision_snapshotall carried. - Delivery created → same four carried.
- Invoice posted → account.move.line has same four fields.
- Open the
fp.serialrecord → smart-button counts show 1 SO, 1 MO, 1 Delivery, 1 Invoice.
7.2 Revision picker
- Part XYZ has revisions A, B, C (C is latest).
- SO line default → Part dropdown shows only Rev C.
- User switches Revision picker to Rev B → Part M2O re-points to Rev B's catalog row,
x_fc_revision_snapshot = 'B'. - Save → admin later edits Rev B's
revisionChar to "B1" → SO line's snapshot still reads "B".
7.3 Thickness dropdown
- Coating config "ENP Class 4" has 3 thickness options (0.0005", 0.001", 0.0015").
- SO line picks coating → thickness dropdown shows exactly those 3 options.
- User changes coating config → thickness field resets (onchange clears it if no longer valid).
7.4 Generate Serial button
- Click on SO line → new
fp.serialwith name "FP-SN-00001" created and assigned.
7.5 Free-text serial
- User types "CUST-999" into x_fc_serial_id → Odoo offers "Create 'CUST-999'" → click →
fp.serialcreated with that exact name, linked.
7.6 Unique constraint
- Create two
fp.serialrecords with the same name in the same company → second raises unique violation.
8. Defensive Measures (Forward-Looking, Per User's No-Refix Rule)
- Serial registry is Many2one, not Char — when Sub 6 (contact profiles) adds per- customer notification routing, the serial record is a stable join target for audit queries.
x_fc_revision_snapshotis populated at save — survives revision edits and catalog cleanups. No migration will ever need to reconstruct it from history.- Job# auto-assign lives in
action_confirm()— a single hook point. Changing the sequence format or making it optional-by-default is one function edit. - Thickness options are a child table, not a JSON blob on the config — future per-customer / per-part overrides can be layered as an additional domain filter without schema change.
- Reports consume the existing macro — no report-side changes beyond the macro edit means future Sub 8 (receiving/inspection restructure) can safely re-lay out reports.
- All four fields are
copy=False— duplicating an SO line gets fresh serial / job# / snapshot rather than stale data. Prevents silent cross-order data bleed.
9. File Manifest
fusion_plating_configurator/
├── __manifest__.py (version bump, data list additions)
├── models/
│ ├── __init__.py (+ fp_serial, fp_coating_thickness)
│ ├── fp_serial.py NEW
│ ├── fp_coating_thickness.py NEW
│ ├── fp_coating_config.py (thickness_option_ids inverse)
│ ├── sale_order.py (action_confirm override for job#)
│ ├── sale_order_line.py (4 new fields + compute)
│ └── account_move_line.py (4 new fields)
├── wizard/
│ ├── fp_direct_order_line.py (4 new fields mirror)
│ └── fp_direct_order_wizard.py (carry to SO line on create)
├── views/
│ ├── fp_serial_views.xml NEW
│ ├── fp_coating_config_views.xml (thickness tab)
│ ├── sale_order_views.xml (4 columns in tree)
│ └── res_config_settings_views.xml (—no change—)
├── wizard/
│ └── fp_direct_order_wizard_views.xml (4 columns)
├── data/
│ └── fp_sub5_sequence_data.xml NEW (fp.serial, fp.job.number)
├── security/
│ └── ir.model.access.csv (+ fp.serial, fp.coating.thickness)
fusion_plating_bridge_mrp/
├── __manifest__.py (version bump)
├── models/
│ └── mrp_production.py (4 new fields + _prepare_mo_vals carry)
fusion_plating_logistics/
├── __manifest__.py (version bump)
├── models/
│ └── fp_delivery.py (4 new fields + create hook)
fusion_plating_reports/
├── __manifest__.py (version bump)
└── report/
└── customer_line_header.xml (add Serial / Job# / Thickness / Rev)
10. Rollout
- Target: entech (LXC 111)
- Update order:
fusion_plating_configurator(new models + sequences)fusion_plating_bridge_mrp(MO fields + carry-over hook)fusion_plating_logistics(Delivery fields + carry-over)fusion_plating_reports(macro update)
- Each
-u MODULE --stop-after-initthen restart.
End of spec.