This commit is contained in:
gsinghpal
2026-04-24 21:04:38 -04:00
parent 0eab4b4efb
commit 41d0908ade
4083 changed files with 1230780 additions and 287 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,651 @@
# 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.
| Field | Type | Notes |
|---|---|---|
| `name` | Char | Sequence: `WH/JOB/00033`. The legacy "WH/MO/00033" labels stay only on migrated records (see §7). |
| `state` | Selection | `draft`, `confirmed`, `in_progress`, `done`, `cancelled`, `on_hold` |
| `partner_id` | Many2one(res.partner) | Customer; copied from SO |
| `product_id` | Many2one(product.product) | Reference part product (for inventory only) |
| `part_catalog_id` | Many2one(fp.part.catalog) | The actual part being plated; primary identifier |
| `qty` | Float | Quantity to plate |
| `qty_done` | Float | Quantity completed |
| `qty_scrapped` | Float | Quantity scrapped (rolled up from holds) |
| `date_deadline` | Datetime | Promised completion date |
| `date_planned_start` | Datetime | Planned start |
| `date_started` | Datetime | Actual start (first step start) |
| `date_finished` | Datetime | Actual completion |
| `origin` | Char | SO name for traceability |
| `sale_order_id` | Many2one(sale.order) | Source SO |
| `sale_order_line_ids` | Many2many(sale.order.line) | Lines that fed this job (group_tag collapse) |
| `recipe_id` | Many2one(fusion.plating.process.node) | The recipe template used |
| `step_ids` | One2many(fp.job.step, job_id) | The operations |
| `step_count` | Integer | Computed |
| `step_done_count` | Integer | Computed |
| `step_progress_pct` | Float | Computed: `step_done_count / step_count * 100` |
| `current_step_id` | Many2one(fp.job.step) | The operation currently in progress (or next ready) |
| `coating_config_id` | Many2one(fp.coating.config) | The coating spec |
| `facility_id` | Many2one(fp.facility) | Hard gate at confirm |
| `manager_id` | Many2one(res.users) | Plating manager |
| `priority` | Selection | `low`, `normal`, `high`, `rush` (operator-relevant ordering) |
| `customer_spec_id` | Many2one(fp.customer.spec) | Optional spec |
| `portal_job_id` | Many2one(fp.portal.job) | Customer portal binding (renamed from `x_fc_portal_job_id`) |
| `delivery_id` | Many2one(fp.delivery) | The shipment |
| `invoice_ids` | Many2many(account.move) | Linked invoices |
| `certificate_ids` | One2many(fp.certificate, job_id) | Certs generated |
| `batch_ids` | One2many(fp.batch, job_id) | Batches that ran through |
| `quality_hold_ids` | One2many(fp.quality.hold, job_id) | Holds raised |
| `consumption_ids` | One2many(fp.job.consumption, job_id) | Consumables |
| `qc_check_id` | Many2one(fp.quality.check) | Active QC check |
| `quoted_revenue` | Monetary | From SO |
| `actual_cost` | Monetary | Computed from steps + consumables |
| `margin` | Monetary | Computed |
| `margin_pct` | Float | Computed |
| `start_at_node_id` | Many2one(fusion.plating.process.node) | Rework: start at this recipe node |
| `override_ids` | One2many(fp.job.node.override, job_id) | Per-job opt-in/out |
| `current_location` | Char | Computed: "Queued: Bath 3" / "In progress: Oven A" / "Ready to ship" |
| `mail.thread, mail.activity.mixin` | Inherits | Chatter |
**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, default_oven_id` | Many2one | Single-line shop convenience |
| `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/<id>` instead of `/fp/wo/<id>`.
- 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.36.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: **911 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 12 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: 810 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.