diff --git a/fusion_plating/docs/superpowers/specs/2026-04-22-sub5-order-line-fields-design.md b/fusion_plating/docs/superpowers/specs/2026-04-22-sub5-order-line-fields-design.md
new file mode 100644
index 00000000..58939843
--- /dev/null
+++ b/fusion_plating/docs/superpowers/specs/2026-04-22-sub5-order-line-fields-design.md
@@ -0,0 +1,411 @@
+# 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 of `fp.coating.config`, one row per allowed thickness.
+- **Revision picker UX** — the SO line Part M2O filters to `is_latest_revision=True` by 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.
+
+```python
+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.
+
+```python
+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
+
+```python
+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 populates `x_fc_revision_snapshot` from
+ `x_fc_part_catalog_id.revision` whenever the part changes (or the field is empty).
+- Onchange on `x_fc_part_catalog_id` clears `x_fc_thickness_id` if the picked thickness doesn't
+ belong to the line's coating config.
+
+### 2.4 Revision picker mechanics
+
+Two approaches on the SO line form:
+
+1. **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).
+2. **Revision Selector** — a new non-stored compute field on `sale.order.line`:
+ ```python
+ x_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)
+ ```
+ When the user changes `x_fc_revision_pick` to a different revision of the same part number,
+ the inverse re-points `x_fc_part_catalog_id` to 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_vals` override in
+ `fusion_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_line` in `fusion_plating_configurator`;
+ extend that hook to carry the four new fields.
+
+### 2.6 `fp.coating.config` additions
+
+```python
+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`:
+
+```xml
+
+ Fusion Plating Serial Number
+ fp.serial
+ FP-SN-
+ 5
+
+
+ Fusion Plating Job Number
+ fp.job.number
+ FP-JOB-
+ 5
+
+```
+
+### 2.8 Job# auto-assign
+
+Override `sale.order.action_confirm()` to assign `x_fc_job_number` to each line that doesn't
+have one:
+
+```python
+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:
+1. Creates `fp.serial` with `name = next_by_code('fp.serial')`, `sale_order_line_id = self.id`.
+2. Writes `self.x_fc_serial_id = new_serial.id`.
+3. 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` (widget `many2one`, 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.rule` on `fp.serial` via `company_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.serial`
+ created and linked.
+- Confirm SO → assert `x_fc_job_number` auto-populated with `FP-JOB-NNNNN` sequence.
+- MO auto-created → assert `x_fc_serial_id`, `x_fc_job_number`, `x_fc_thickness_id`,
+ `x_fc_revision_snapshot` all carried.
+- Delivery created → same four carried.
+- Invoice posted → account.move.line has same four fields.
+- Open the `fp.serial` record → 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 `revision` Char 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.serial` with 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.serial` created with that exact name, linked.
+
+### 7.6 Unique constraint
+- Create two `fp.serial` records with the same name in the same company → second raises
+ unique violation.
+
+---
+
+## 8. Defensive Measures (Forward-Looking, Per User's No-Refix Rule)
+
+1. **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.
+2. **`x_fc_revision_snapshot` is populated at save** — survives revision edits and catalog
+ cleanups. No migration will ever need to reconstruct it from history.
+3. **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.
+4. **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.
+5. **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.
+6. **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:
+ 1. `fusion_plating_configurator` (new models + sequences)
+ 2. `fusion_plating_bridge_mrp` (MO fields + carry-over hook)
+ 3. `fusion_plating_logistics` (Delivery fields + carry-over)
+ 4. `fusion_plating_reports` (macro update)
+- Each `-u MODULE --stop-after-init` then restart.
+
+---
+
+*End of spec.*