Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-04-25-fp-native-job-model-design.md
gsinghpal f7a4cba5a8 docs(jobs): split fp.job §5.1 fields by module ownership (Task 1.4)
Originally Task 1.4 was to add all spec §5.1 extension fields to
fp.job in core. The dependency-graph audit during implementation
revealed that 6 of those fields point to models in dependent
modules (configurator, quality, portal, logistics, bridge_mrp).
Adding them in core would invert the dependency graph.

Spec §5.1 now has a Module column. Core-safe fields stay in
fusion_plating/models/fp_job.py; cross-module fields are deferred
to their owning modules via _inherit = 'fp.job' in Phase 2.

Plan Task 1.4 narrative updated to reflect the reduced scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:54:35 -04:00

660 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<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.