# Native Plating Job Model — Design **Date:** 2026-04-25 **Module scope:** All modules in `fusion_plating/` that touch `mrp.production` or `mrp.workorder` (~12 modules). Net effect: replace Odoo MRP integration with a native plating job model. **Status:** Design approved 2026-04-25. All §10 decisions locked per recommendations (see §10 footer). Implementation plan written: `docs/superpowers/plans/2026-04-25-fp-native-job-model.md`. **Owner:** Nexa Systems **Predecessor:** entech is currently in production on the MRP-bridged model (`fusion_plating_bridge_mrp`). This spec replaces that model. --- ## 1. Why we're doing this Plating is **not assembly**. Odoo MRP is built around BoM consumption, multi-level assembly, and material-driven scheduling. Plating is process: parts in → baths → parts out. The "BoM" is bath chemistry (already its own model), not stock items. Steelhead, ProShop, PROPLATE, JobBOSS — none of them model plating as MRP. They all use a job/router model because plating IS different. We bridged into Odoo MRP three months ago to get free machinery (kanban, work-centre cost, MRP menus). The cost has been: - Operators see **N work orders for one job** (8 WOs for a typical recipe). - Three different views to understand one job (MO form, WO form, Plant Overview). - 5,000+ LOC in `fusion_plating_bridge_mrp` whose only job is keeping MRP and our plating models in sync. - Recipe overrides, work-centre mapping fallbacks, WO state syncing — most of our hardest bugs come from this sync. - Future features (operator mobile app, AI step suggestions, IoT auto-actions) get more expensive every time we add coupling. **With the user's "I don't care about MRP dependencies" call, the trade-off flips.** Native model is simpler today, simpler tomorrow, and the data model finally matches the domain. ## 2. Goal Replace `mrp.production` and `mrp.workorder` (in the plating context) with a native job model: - **`fp.job`** — one record per plating job. Replaces `mrp.production`. - **`fp.job.step`** — one record per operation within the job. Replaces `mrp.workorder`. - The recipe template (`fusion.plating.process.node`) is **unchanged** — it's already domain-correct. `fp.job.step` instantiates from it via `recipe_node_id`. Operator UX target: scan a sticker → land on the job page → see the process tree → tap an operation → start/finish/hold. **One screen, one mental model.** Manager UX target: list of jobs (kanban or tree), drill into a job, see the same process tree with cost/time aggregates. ## 3. In scope 1. **New models:** `fp.job`, `fp.job.step`. Field schema in §4. 2. **SO → job hook:** `sale.order.action_confirm()` creates `fp.job` directly (replaces the current SO → MO bridge in `fusion_plating_bridge_mrp/models/sale_order.py`). 3. **Recipe → steps generator:** `fp.job._generate_steps_from_recipe()` walks the recipe tree and creates `fp.job.step` rows (replaces `_generate_workorders_from_recipe`). 4. **Refactor 12 modules** to use `fp.job` / `fp.job.step` instead of `mrp.production` / `mrp.workorder`. Module-by-module plan in §6. 5. **Migrate live data** from entech: every `mrp.production` row becomes an `fp.job` row; every `mrp.workorder` row becomes an `fp.job.step`. Migration spec in §7. 6. **New views:** Job form (with embedded process tree), job kanban, step form (manager-only), redesigned tablet station and plant overview. 7. **Reports** rewritten against new model: WO sticker, job traveller, WO margin, bill of lading, packing slip. 8. **Cert generation, notifications, deliveries, invoicing, batches, holds** — all rebound to `fp.job` / `fp.job.step`. 9. **Drop** `sale_mrp` and `mrp` dependencies from the `fusion_plating_*` chain except where genuinely needed (TBD per §10). ## 4. Out of scope - Recipe template model (`fusion.plating.process.node`) stays as-is. No schema changes, no migration needed for it. - The customer portal (`fusion_plating_portal`) UI — only its model link (`x_fc_production_id` → `x_fc_job_id`). - Bath chemistry model (`fusion.plating.bath.log`) and IoT data ingestion — they link to baths/tanks, not to MOs/WOs. - KPI definitions (`fusion.plating.kpi`) — only the rollup queries. - Customer-facing terminology — operators still say "WO #00033". The label format is preserved; only the underlying model changes. - Removing `mrp` module entirely from Odoo. We just stop depending on it from our modules. If managers still want the standard Manufacturing menu for any reason, they can — but our flow doesn't use it. --- ## 5. Data Model ### 5.1 `fp.job` Replaces `mrp.production` for plating jobs. One record per shop-floor job. **Module ownership:** `fp.job` lives in `fusion_plating` core. Cross-module fields (referencing models from `fusion_plating_configurator`, `_portal`, `_logistics`, `_quality`, `_bridge_mrp`) **cannot** live in core without inverting the dependency graph. Each owning module extends `fp.job` via `_inherit` to add its field. The Phase 2 module `fusion_plating_jobs` becomes the umbrella that pulls all the extensions together. Ownership is called out in the **Module** column below. | Field | Type | Module | Notes | |---|---|---|---| | `name` | Char | core | Sequence: `WH/JOB/00033`. The legacy "WH/MO/00033" labels stay only on migrated records (see §7). | | `state` | Selection | core | `draft`, `confirmed`, `in_progress`, `done`, `cancelled`, `on_hold` | | `partner_id` | Many2one(res.partner) | core | Customer; copied from SO | | `product_id` | Many2one(product.product) | core | Reference part product (for inventory only) | | `qty` | Float | core | Quantity to plate | | `qty_done` | Float | core | Quantity completed | | `qty_scrapped` | Float | core | Quantity scrapped (rolled up from holds) | | `date_deadline` | Datetime | core | Promised completion date | | `date_planned_start` | Datetime | core | Planned start | | `date_started` | Datetime | core | Actual start (first step start) | | `date_finished` | Datetime | core | Actual completion | | `origin` | Char | core | SO name for traceability | | `sale_order_id` | Many2one(sale.order) | core | Source SO (sale_management is in core depends) | | `sale_order_line_ids` | Many2many(sale.order.line) | core | Lines that fed this job (group_tag collapse) | | `recipe_id` | Many2one(fusion.plating.process.node) | core | The recipe template used | | `step_ids` | One2many(fp.job.step, job_id) | core | The operations | | `step_count` | Integer | core | Computed | | `step_done_count` | Integer | core | Computed | | `step_progress_pct` | Float | core | Computed: `step_done_count / step_count * 100` | | `current_step_id` | Many2one(fp.job.step) | core | The operation currently in progress (or next ready) | | `facility_id` | Many2one(fusion.plating.facility) | core | Hard gate at confirm | | `manager_id` | Many2one(res.users) | core | Plating manager | | `priority` | Selection | core | `low`, `normal`, `high`, `rush` (operator-relevant ordering) | | `invoice_ids` | Many2many(account.move) | core | Linked invoices (account is reachable via sale_management → sale → account) | | `quoted_revenue` | Monetary | core | From SO | | `actual_cost` | Monetary | core | Computed from steps + consumables | | `margin` | Monetary | core | Computed | | `margin_pct` | Float | core | Computed | | `start_at_node_id` | Many2one(fusion.plating.process.node) | core | Rework: start at this recipe node | | `current_location` | Char | core | Computed: "Queued: Bath 3" / "In progress: Oven A" / "Ready to ship" | | `mail.thread, mail.activity.mixin` | Inherits | core | Chatter | | `part_catalog_id` | Many2one(fp.part.catalog) | **`fusion_plating_configurator`** (`_inherit = 'fp.job'`) | The actual part being plated; primary identifier | | `coating_config_id` | Many2one(fp.coating.config) | **`fusion_plating_configurator`** | The coating spec | | `customer_spec_id` | Many2one(fusion.plating.customer.spec) | **`fusion_plating_quality`** | Optional spec | | `portal_job_id` | Many2one(fusion.plating.portal.job) | **`fusion_plating_portal`** | Customer portal binding | | `delivery_id` | Many2one(fusion.plating.delivery) | **`fusion_plating_logistics`** | The shipment | | `qc_check_id` | Many2one(fusion.plating.quality.check) | **`fusion_plating_jobs`** (Phase 2) | Active QC check; model lives in current bridge_mrp, will move to jobs module | | `certificate_ids` | One2many(fp.certificate, job_id) | **`fusion_plating_certificates`** | Certs generated | | `batch_ids` | One2many(fp.batch, job_id) | **`fusion_plating_batch`** | Batches that ran through | | `quality_hold_ids` | One2many(fp.quality.hold, job_id) | **`fusion_plating_quality`** | Holds raised | | `consumption_ids` | One2many(fp.job.consumption, job_id) | **`fusion_plating_jobs`** (Phase 2) | Consumables | | `override_ids` | One2many(fp.job.node.override, job_id) | **`fusion_plating_jobs`** (Phase 2) | Per-job opt-in/out | **State machine:** ``` draft → confirmed → in_progress → done ↓ ↑ cancelled (can revert to confirmed if rework) on_hold can be entered from confirmed/in_progress ``` ### 5.2 `fp.job.step` Replaces `mrp.workorder`. One record per operation node from the recipe. | Field | Type | Notes | |---|---|---| | `name` | Char | Operation name (from recipe node) | | `job_id` | Many2one(fp.job, ondelete=cascade) | Parent job | | `sequence` | Integer | Order (10, 20, 30...) | | `recipe_node_id` | Many2one(fusion.plating.process.node) | Source recipe operation | | `state` | Selection | `pending`, `ready`, `in_progress`, `paused`, `done`, `skipped`, `cancelled` | | `kind` | Selection | `wet`, `bake`, `mask`, `rack`, `inspect`, `other` (computed from equipment/name; today on WO as `x_fc_kind`) | | `work_centre_id` | Many2one(fp.work.centre) | New native model — see §5.3 | | `bath_id, tank_id, rack_id, oven_id` | Many2one | Equipment — same as today | | `masking_material_id` | Many2one | Mask kind | | `assigned_user_id` | Many2one(res.users) | Operator assignment | | `started_by_user_id` | Many2one(res.users) | Audit | | `finished_by_user_id` | Many2one(res.users) | Audit | | `signoff_user_id` | Many2one(res.users) | Audit (sign-off gates) | | `date_started` | Datetime | First start (timer) | | `date_finished` | Datetime | Last finish | | `duration_expected` | Float | Minutes | | `duration_actual` | Float | Minutes (sum of intervals) | | `time_log_ids` | One2many(fp.job.step.timelog) | Start/stop intervals — gives us the granularity Odoo had | | `instructions` | Html | Step-level instructions (formatted from child `step` nodes of the recipe) | | `thickness_target` | Float | For plating WOs | | `thickness_uom` | Selection | µm/mil/in | | `dwell_time_minutes` | Float | Recipe-spec'd dwell | | `bake_setpoint_temp` | Float | Bake WOs only | | `bake_actual_duration` | Float | Bake WOs only | | `bake_chart_recorder_ref` | Char | Nadcap audit | | `requires_signoff` | Boolean | Related from recipe_node_id | | `auto_complete` | Boolean | Related from recipe_node_id | | `is_manual` | Boolean | Related from recipe_node_id | | `customer_visible` | Boolean | Related from recipe_node_id | | `contract_review_user_ids` | Many2many(res.users) | For contract review approver gate | | `work_role_id` | Many2one(fp.work.role) | Required role for assignee | | `cost_per_hour` | Monetary | From work centre (or operator) | | `cost_total` | Monetary | Computed: duration × rate | | `quality_hold_ids` | One2many(fp.quality.hold, step_id) | Holds raised at this step | | `is_release_ready` | Boolean | Computed: required fields filled per kind | **State machine:** ``` pending → ready → in_progress → done ↓ ↓ ↑ skipped paused ↓ cancelled ``` - `pending` = step exists but earlier siblings not done - `ready` = predecessors done (or first step of job) - `in_progress` = operator started timer - `paused` = operator stopped timer without finishing (intentional break, end of shift) - `done` = operator finished + sign-off (if required) recorded - `skipped` = opt-in step that wasn't activated for this job, OR start-at-node skipped this step - `cancelled` = job cancelled or rework removed this step ### 5.3 `fp.work.centre` (new model) We replace `mrp.workcenter` with our own model since work centres for plating are domain-specific (a tank line, a bake oven, a rack station — not assembly cells). | Field | Type | Notes | |---|---|---| | `name` | Char | "Bath Line 1", "Oven A", "Rack Station" | | `code` | Char | Short code | | `facility_id` | Many2one(fp.facility) | Which facility | | `kind` | Selection | `wet_line`, `bake`, `mask`, `rack`, `inspect`, `other` | | `cost_per_hour` | Monetary | For margin calculations | | `default_bath_id, default_tank_id` | Many2one(`fusion.plating.bath`/`.tank`) | Single-line shop convenience | | `default_oven_id` | Many2one(`fusion.plating.bake.oven`) | **Deferred to `fusion_plating_jobs` bridge module via `_inherit`** — `bake.oven` is defined in `fusion_plating_shopfloor` which `fusion_plating` core cannot depend on. Bridge module *can* depend on shopfloor and adds this field there. | | `active` | Boolean | | This replaces `x_fc_mrp_workcenter_id` mapping that the recipe operations have today. ### 5.4 `fp.job.step.timelog` Granular start/stop tracking. Each timer pause creates a record. | Field | Type | Notes | |---|---|---| | `step_id` | Many2one(fp.job.step) | | | `user_id` | Many2one(res.users) | | | `date_started` | Datetime | | | `date_finished` | Datetime | NULL while running | | `duration_minutes` | Float | Computed | ### 5.5 What stays unchanged - `fusion.plating.process.node` (recipe template) — unchanged - `fusion.plating.process.node.input` (operator inputs) — unchanged - `fp.batch`, `fp.certificate`, `fp.thickness.reading` — only their backlink fields rename - `fp.portal.job` — only its `x_fc_production_id` field renames to `job_id` - `fp.delivery` — only its job-back-reference rebinds - `fp.quality.hold`, `fp.ncr`, `fp.capa` — only backlinks rebind - `fp.notification.template`, `fp.notification.log` — trigger event names update - IoT (`fusion_plating_iot`), bath chemistry (`fusion.plating.bath.log`), KPIs (`fusion.plating.kpi`) — none touch jobs/steps directly, no changes --- ## 6. Module-by-module refactor plan Listed in dependency order — refactor can/should happen along this gradient. ### 6.1 `fusion_plating` (core) **Effort: 3 days** - Define `fp.job`, `fp.job.step`, `fp.job.step.timelog`, `fp.work.centre` models. - Sequences (`ir.sequence` records for `fp.job`, `fp.job.step`). - Security (groups already exist: Operator/Supervisor/Manager/Admin; ACLs added). - Move `fusion.plating.job.node.override` from `bridge_mrp` into core, rebind to `fp.job`. ### 6.2 `fusion_plating_bridge_mrp` → `fusion_plating_jobs` (rename + gut) **Effort: 5 days** The current bridge module becomes the home of SO→job hooks, recipe→steps generator, and computed rollups. Most of its weight (the WO sync logic) goes away. - Rename module dir to `fusion_plating_jobs`. - Migrate `_fp_auto_create_mo` → `_fp_auto_create_job` on `sale.order`. - Migrate `_generate_workorders_from_recipe` → `fp.job._generate_steps_from_recipe`. Simpler: no MRP work-centre mapping fallback, no `mrp.workorder.create` quirks. - Move all `x_fc_*` fields that currently sit on `mrp.production` to `fp.job` natively. - Move all `x_fc_*` fields on `mrp.workorder` to `fp.job.step` natively. - Drop `sale_mrp` from `__manifest__.py` depends. Drop `mrp` if confirmed (§10 Q3). - Quality check, racking inspection, cert generator, delivery hooks — all rebind. ### 6.3 `fusion_plating_batch` **Effort: 0.5 day** - `workorder_id` → `step_id` (Many2one fp.job.step) - `production_id` (related) → `job_id` (related from step_id.job_id) - Views and access rules trivial rename. ### 6.4 `fusion_plating_quality` **Effort: 1 day** - `fp.quality.hold` adds `job_id`, `step_id` fields (replaces production_id, workorder_id from bridge). - NCR and CAPA reference holds via current relations — no schema change. - Views: hold form refers to job/step instead of MO/WO. ### 6.5 `fusion_plating_certificates` **Effort: 0.5 day** - `fp.certificate.production_id` → `fp.certificate.job_id` - `fp.thickness.reading.production_id` → `fp.thickness.reading.job_id` - Cert auto-creation hook moves from MO done to `fp.job.button_mark_done`. ### 6.6 `fusion_plating_invoicing` **Effort: 0.5 day** - Invoice → portal job linkage stays; just walks via job instead of MO. - Account hold gating (on SO confirm, invoice post, delivery) stays — same partner-level field. ### 6.7 `fusion_plating_logistics` **Effort: 0.5 day** - `fp.delivery.job_ref` resolution rebinds: from MO.name to fp.job.name (same string format if we use `WH/JOB/00033`). - Delivery auto-creation hook on `fp.job.button_mark_done`. ### 6.8 `fusion_plating_portal` **Effort: 0.5 day** - `fp.portal.job.x_fc_production_id` → `job_id` (Many2one fp.job). - Portal templates: same — they read MO.name; if we keep `WH/JOB/...` format, no UI change. - Auto-create on `fp.job.action_confirm()`. ### 6.9 `fusion_plating_configurator` **Effort: 1 day** - Configurator wizard already creates SO lines; no direct MO touch. - The `action_view_mrp_production` button on SO becomes `action_view_jobs`. - Recipe assignment paths stay (part catalog → coating config → job). ### 6.10 `fusion_plating_notifications` **Effort: 1 day** - Trigger event names: `mo_confirmed` → `job_confirmed`, `mo_complete` → `job_complete`, `wo_started` → `step_started`, etc. - Existing customer-facing template content unchanged (uses partner/SO/cert vars). - Hook from `fp.job.button_mark_done` and `fp.job.step.button_finish`. ### 6.11 `fusion_plating_shopfloor` **Effort: 6 days** ← biggest single chunk This is where operators live. Full rewrite of the operator UI. - **Plant Overview** (kanban): one card per `fp.job.step` in `ready` or `in_progress`, grouped by `fp.work.centre`. Drag-drop changes work centre on the step. - **Tablet Station**: scan job sticker → land on job page. Job page shows: process tree (the IS-the-job tree) + ready/in-progress steps highlighted + start/finish/hold/sign-off buttons. - **Process Tree client action**: now the *primary* job view (not a separate drill-down). Renders `fp.job.step` records as cards with state colours, hierarchy from `recipe_node_id.parent_id` chain. - **Manager Dashboard**: list of jobs with progress %, current step location, manager actions (assign worker, take over, raise hold). - All RPC routes (`/fp/shopfloor/start_wo` etc.) renamed and rebound. ### 6.12 `fusion_plating_reports` **Effort: 3 days** - WO Box Sticker — already mostly model-agnostic; rebind the inner template's `_mo` resolution. Print URL `/fp/job/` instead of `/fp/wo/`. - Job Traveller — loops over `fp.job.step_ids` instead of `mrp.production.workorder_ids`. - WO Margin Report — same rollup, different model. - BoL, Packing Slip, Invoice — all read from sale_order anyway, only the cross-ref to job needs updating. - Hide-default-reports XML: drop the `mrp.production` / `mrp.workorder` hides (they're no longer in our app). ### 6.13 `fusion_plating_kpi` **Effort: 0.5 day** - KPI rollup queries: `mrp.production` → `fp.job`, `mrp.workorder` → `fp.job.step`. - KPI definitions stay; SQL/ORM domain rewrites. ### 6.14 `fusion_plating_receiving` **Effort: 0.5 day** - Soft gate hook on `fp.job.action_confirm` (currently on MO confirm). - `fp.racking.inspection.production_id` → `job_id`. ### 6.15 Other modules with MRP touchpoints - `fusion_plating_aerospace`, `fusion_plating_nuclear`, `fusion_plating_cgp`, `fusion_plating_safety`: minimal — none reference MO/WO directly per the audit. Verify in §7 testing. - `fusion_plating_compliance` and bridge_quality, bridge_documents, bridge_maintenance, bridge_sign: light touch — rebind any MO/WO refs to job/step. ### 6.16 Data scripts / tests **Effort: 2 days** - 10+ scripts in `scripts/` and `docs/superpowers/tests/` query `mrp.production`/`mrp.workorder`. Rewrite to query `fp.job`/`fp.job.step`. ### Total effort estimate | Phase | Days | |---|---| | Core models + sequences + security | 3 | | `fusion_plating_jobs` (gut + rebuild) | 5 | | Modules 6.3–6.10 (8 modules, light) | 5.5 | | Configurator | 1 | | Notifications | 1 | | Shopfloor (full rewrite) | 6 | | Reports | 3 | | KPI + Receiving + other | 1.5 | | Scripts + tests rewrite | 2 | | Migration script (§7) | 3 | | Test on entech-clone | 5 | | Cutover + burn-in | 3 | | **Total** | **39 working days ≈ 8 weeks** | Calendar time with iteration: **9–11 weeks**. --- ## 7. Migration strategy entech is in production. Migration must be reversible until we're confident, and must preserve every link operators / accounting depend on. ### 7.1 Approach: Big-bang with scripted migration + 2-week shadow period 1. Build new models alongside existing ones in a feature branch. Both coexist in the codebase. 2. Run migration script on a **clone** of entech (not entech itself). Validate E2E: every report renders, every cert PDF reproduces, every link resolves. 3. Cutover weekend on entech: ~4 hour window. Steps in §7.4. 4. **Shadow period (weeks 1–2 post-cutover):** keep `mrp.production` / `mrp.workorder` tables as read-only snapshots. If anything goes wrong, we can revert to the snapshot via a reverse migration script. 5. After 2 weeks of stable operation, drop the MRP tables. ### 7.2 Migration script Location: `fusion_plating_jobs/migrations/19.0.8.0.0/post-migration.py` For each `mrp.production` row: 1. Create `fp.job` with same id (use `fp_job_id_seq` aligned to MRP id space, or keep separate sequence and store the legacy id in `legacy_mrp_production_id` for audit). 2. Copy fields: name, partner_id, product_id, product_qty → qty, dates, origin, state (mapping in §7.3), all `x_fc_*` extension fields. 3. For each child `mrp.workorder`, create `fp.job.step` with all `x_fc_*` fields. 4. Migrate `mrp.workorder.time_ids` (if present) to `fp.job.step.timelog`. 5. Rebind every cross-reference: cert.production_id, batch.production_id, delivery.job_ref, portal_job.x_fc_production_id, etc. 6. Preserve chatter: copy `mail.message` records from MO/WO to corresponding job/step (Odoo's `res_id` + `model` rebinding). 7. Audit log: write `fp_migration.log` with row counts, mismatches, warnings. ### 7.3 State mapping | MRP state | fp.job state | |---|---| | `draft` | `draft` | | `confirmed` | `confirmed` | | `progress` | `in_progress` | | `to_close` | `in_progress` (will be `done` after final ops) | | `done` | `done` | | `cancel` | `cancelled` | | MRP WO state | fp.job.step state | |---|---| | `pending` | `pending` | | `waiting` | `pending` | | `ready` | `ready` | | `progress` | `in_progress` | | `done` | `done` | | `cancel` | `cancelled` | ### 7.4 Cutover plan (single weekend window) **Friday 6pm:** Stop operators. Final MOs of the week wrapped up. **Friday 8pm:** Backup full database. Tag as `pre_fp_job_migration`. **Friday 9pm:** Deploy new module bundle to entech. Run migration script. Estimated 30 min. **Friday 10pm:** Smoke test — open a recent job, print sticker, scan, render CoC, generate margin report. If anything fails, restore from 8pm backup; abort. **Saturday/Sunday:** Live shop is offline anyway (weekend). Time to fix anything that surfaced. **Monday 7am:** Operators come in. Manager + tech on site for the first 2 hours. **Following 2 weeks:** Daily check-ins. Active monitoring of error logs. MRP tables still present (read-only). ### 7.5 Rollback plan If the cutover fails or unrecoverable issues surface within 7 days: 1. Stop operators. 2. Restore Friday 8pm DB backup. 3. Revert deploy to previous module bundle. 4. Reopen as previous-MRP system. After 7 days, rollback becomes "forward fix only" — too much new shop activity to restore. ### 7.6 Data preservation guarantees - **Every** historical job remains queryable. Old MOs become old `fp.job` records. - **Every** chatter message preserved (Odoo's `mail.message.res_id` rebinding). - **Every** PDF attachment preserved (`ir.attachment.res_id` rebinding). - **Every** cert, thickness reading, batch, hold preserved with intact links. - **Audit-trail integrity for Nadcap / aerospace customers** is critical; the migration script must verify zero loss before commit. We'll add a pre-migration audit script that snapshots counts and a post-migration audit that re-validates. --- ## 8. Test strategy ### 8.1 Unit tests - `fp.job` state machine transitions - `fp.job.step` state machine transitions - `_generate_steps_from_recipe` with: simple recipe, nested sub_processes, opt-in/out overrides, start-at-node rework, missing work-centre mapping - Migration script: round-trip a snapshot of entech data through migrate, verify every row's fields match expectation ### 8.2 Integration tests (end-to-end on a fresh DB) - Quote → SO → confirm → job created → recipe assigned → steps generated → start step → log time → finish step → all steps done → job done → portal job ready_to_ship → delivery created → CoC generated → invoice posted → portal job complete - Quality hold raised mid-job → blocks finish → released → job continues - Rework (start-at-node) job → only later steps generated - Cancel a confirmed job → all steps cancelled → portal job cancelled → no cert ### 8.3 E2E on entech-clone - Restore entech production DB to a staging container. - Run migration script. - Replay last 30 days of operator actions through the new UI. - Run every report, every cert, every notification trigger. - Diff against pre-migration snapshot: identify any data drift. ### 8.4 Performance baseline - Plant Overview load time with 1000 active steps: target < 1.5s - Job form open with 50-step recipe: target < 800ms - Report rendering (CoC, traveller, sticker): target < 3s --- ## 9. Risk register | Risk | Likelihood | Impact | Mitigation | |---|---|---|---| | Migration script silently drops chatter on a record | Medium | Medium | Pre/post-migration audit scripts compare message counts | | A module we forgot still references mrp.workorder, fails at runtime | Medium | Medium | CI grep in pre-deploy that fails on `mrp.workorder` / `mrp.production` references in our code | | Cert PDF differs vs current (audit/Nadcap impact) | Low | High | Render 100 sample CoCs pre-migration, render same post-migration, byte-diff | | Operators confused by new UI | High | Medium | Beta with 2 operators 1 week before cutover; live training Monday morning | | Plant Overview slower than current | Medium | Low | Performance baseline test in §8.4; index `fp_job_step.state`, `work_centre_id` | | Account hold gate breaks → invoices post that shouldn't | Low | High | Unit tests + integration test for held-customer invoice attempt | | Rollback needed >7 days post-cutover | Low | High | Forward-fix only after day 7; treat the new model as canonical | | `mrp` Odoo module updates break our coexistence | Low | Low | We're un-depending; if mrp updates, we don't care | | Missed dependency causes module install failure | Medium | Low | Test install on fresh DB before cutover | --- ## 10. Open decisions (sign-off needed) These need a yes/no from you before I write the implementation plan: ### Q1. Naming: `fp.job` vs `fp.work.order`? Operators currently say "WO #00033". The label format is independent of the model name — we can call the model `fp.job` and still print "WO #00033" on the sticker. **Recommendation:** `fp.job`. Cleaner, doesn't pretend to be Odoo MRP, "work order" is a UI label not a model name. **Your call:** ___ `fp.job` ___ `fp.work.order` ___ other:______ ### Q2. Sticker/sequence label format: keep `WH/MO/00033` or switch to `WH/JOB/00033`? Existing labels printed on real boxes around the shop say `WH/MO/00033`. Reprinting all of them is a real cost. **Recommendation:** Keep `WH/MO/...` for migrated records (preserve the label that's on the box). Use `WH/JOB/...` for new records going forward. Both formats render identically on the sticker as "WO #...". **Your call:** ___ keep `WH/MO/...` for everything ___ switch new ones to `WH/JOB/...` ___ switch all (re-label boxes) ___ other:______ ### Q3. Drop `mrp` module dependency entirely, or keep it installed but unused? Removing `mrp` removes the standard Manufacturing app from the menu — managers who occasionally peek at standard MRP views lose them. Keeping it installed means ~150MB of unused tables in the DB. **Recommendation:** Keep `mrp` *installed* (low cost) but drop it from our modules' `depends`. We don't use it; it sits idle. We can revisit removing later. **Your call:** ___ keep installed ___ uninstall fully ___ other:______ ### Q4. `fp.work.centre` — new model, or extend `mrp.workcenter`? We could keep `mrp.workcenter` as the work-centre table even though we drop the rest of MRP. Saves us 1 model worth of refactor. **Recommendation:** New `fp.work.centre`. Plating work centres are different from assembly work centres (kind = wet_line / bake / mask / rack), and we already have `x_fc_facility_id`, `x_fc_fp_work_center_id` extensions on `mrp.workcenter`. Cleaner to start fresh. **Your call:** ___ new `fp.work.centre` ___ keep `mrp.workcenter` ___ other:______ ### Q5. Step model granularity — operations only, or full recipe tree? **Option A:** `fp.job.step` = operations only (matches current MRP WO behaviour). Container nodes (recipe / sub_process) and step nodes (instructions) are pulled from the recipe template at render time. **Option B:** `fp.job.step` mirrors the full recipe tree (operations + containers + steps). **Recommendation:** Option A. Simpler model, current working pattern, render hierarchy via JOIN at view time. Steelhead's UX achievable without DB-level tree. **Your call:** ___ A (ops only) ___ B (full tree) ___ other:______ ### Q6. Migration approach — big-bang weekend cutover, or parallel-run? Parallel-run = both systems live for 2 weeks, jobs created in both, comparing output. More robust but more complex. **Recommendation:** Big-bang with shadow period (§7.1). Simpler, lower error surface. Cutover is ~4 hours on a weekend. **Your call:** ___ big-bang ___ parallel-run ___ other:______ ### Q7. Implementation pace — single sprint or phased? - Single sprint: 8–10 weeks one engineer, full cutover at end. - Phased: ship `fp.job`/`fp.job.step` models first (week 4); migrate one module per week thereafter; cutover at the end (week 12+). More UI churn for operators during the transition. **Recommendation:** Single sprint. Operators only switch UI once. **Your call:** ___ single sprint ___ phased ___ other:______ --- ### Decisions locked (2026-04-25) | # | Decision | Locked answer | |---|---|---| | Q1 | Model name | **`fp.job`** | | Q2 | Sticker label format | **Keep `WH/MO/...` for migrated records; new records use `WH/JOB/...`. Both render as "WO #..." on the sticker.** | | Q3 | `mrp` Odoo module | **Keep installed but drop from our `depends`** | | Q4 | Work centre model | **New `fp.work.centre`** | | Q5 | Step model granularity | **Option A — operations only (flat list, hierarchy via JOIN at view time)** | | Q6 | Migration approach | **Big-bang weekend cutover with 2-week shadow period** | | Q7 | Implementation pace | **Single sprint, ~8 weeks engineering** | ## 11. Next steps After you sign off on §10: 1. I write the **implementation plan** (`docs/superpowers/plans/...`) — concrete per-day task breakdown, branch strategy, commit cadence. 2. We create a feature branch: `feat/fp-native-job-model`. 3. We start with §6.1 (core models) and follow the dependency order through §6.16. 4. End-of-week demos to you against an entech-clone. 5. Cutover weekend scheduled with 4 weeks notice to give the shop time to plan. If something in §10 or anywhere else is wrong / unclear / debatable, flag it now — fixing the spec is cheap; fixing committed code is not.