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>
32 KiB
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_mrpwhose 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. Replacesmrp.production.fp.job.step— one record per operation within the job. Replacesmrp.workorder.- The recipe template (
fusion.plating.process.node) is unchanged — it's already domain-correct.fp.job.stepinstantiates from it viarecipe_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
- New models:
fp.job,fp.job.step. Field schema in §4. - SO → job hook:
sale.order.action_confirm()createsfp.jobdirectly (replaces the current SO → MO bridge infusion_plating_bridge_mrp/models/sale_order.py). - Recipe → steps generator:
fp.job._generate_steps_from_recipe()walks the recipe tree and createsfp.job.steprows (replaces_generate_workorders_from_recipe). - Refactor 12 modules to use
fp.job/fp.job.stepinstead ofmrp.production/mrp.workorder. Module-by-module plan in §6. - Migrate live data from entech: every
mrp.productionrow becomes anfp.jobrow; everymrp.workorderrow becomes anfp.job.step. Migration spec in §7. - New views: Job form (with embedded process tree), job kanban, step form (manager-only), redesigned tablet station and plant overview.
- Reports rewritten against new model: WO sticker, job traveller, WO margin, bill of lading, packing slip.
- Cert generation, notifications, deliveries, invoicing, batches, holds —
all rebound to
fp.job/fp.job.step. - Drop
sale_mrpandmrpdependencies from thefusion_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
mrpmodule 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 doneready= predecessors done (or first step of job)in_progress= operator started timerpaused= operator stopped timer without finishing (intentional break, end of shift)done= operator finished + sign-off (if required) recordedskipped= opt-in step that wasn't activated for this job, OR start-at-node skipped this stepcancelled= 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) — unchangedfusion.plating.process.node.input(operator inputs) — unchangedfp.batch,fp.certificate,fp.thickness.reading— only their backlink fields renamefp.portal.job— only itsx_fc_production_idfield renames tojob_idfp.delivery— only its job-back-reference rebindsfp.quality.hold,fp.ncr,fp.capa— only backlinks rebindfp.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.centremodels. - Sequences (
ir.sequencerecords forfp.job,fp.job.step). - Security (groups already exist: Operator/Supervisor/Manager/Admin; ACLs added).
- Move
fusion.plating.job.node.overridefrombridge_mrpinto core, rebind tofp.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_jobonsale.order. - Migrate
_generate_workorders_from_recipe→fp.job._generate_steps_from_recipe. Simpler: no MRP work-centre mapping fallback, nomrp.workorder.createquirks. - Move all
x_fc_*fields that currently sit onmrp.productiontofp.jobnatively. - Move all
x_fc_*fields onmrp.workordertofp.job.stepnatively. - Drop
sale_mrpfrom__manifest__.pydepends. Dropmrpif 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.holdaddsjob_id,step_idfields (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_idfp.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_refresolution rebinds: from MO.name to fp.job.name (same string format if we useWH/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_productionbutton on SO becomesaction_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_doneandfp.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.stepinreadyorin_progress, grouped byfp.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.steprecords as cards with state colours, hierarchy fromrecipe_node_id.parent_idchain. - Manager Dashboard: list of jobs with progress %, current step location, manager actions (assign worker, take over, raise hold).
- All RPC routes (
/fp/shopfloor/start_woetc.) renamed and rebound.
6.12 fusion_plating_reports
Effort: 3 days
- WO Box Sticker — already mostly model-agnostic; rebind the inner template's
_moresolution. Print URL/fp/job/<id>instead of/fp/wo/<id>. - Job Traveller — loops over
fp.job.step_idsinstead ofmrp.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.workorderhides (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_complianceand 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/anddocs/superpowers/tests/querymrp.production/mrp.workorder. Rewrite to queryfp.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
- Build new models alongside existing ones in a feature branch. Both coexist in the codebase.
- Run migration script on a clone of entech (not entech itself). Validate E2E: every report renders, every cert PDF reproduces, every link resolves.
- Cutover weekend on entech: ~4 hour window. Steps in §7.4.
- Shadow period (weeks 1–2 post-cutover): keep
mrp.production/mrp.workordertables as read-only snapshots. If anything goes wrong, we can revert to the snapshot via a reverse migration script. - 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:
- Create
fp.jobwith same id (usefp_job_id_seqaligned to MRP id space, or keep separate sequence and store the legacy id inlegacy_mrp_production_idfor audit). - Copy fields: name, partner_id, product_id, product_qty → qty, dates, origin,
state (mapping in §7.3), all
x_fc_*extension fields. - For each child
mrp.workorder, createfp.job.stepwith allx_fc_*fields. - Migrate
mrp.workorder.time_ids(if present) tofp.job.step.timelog. - Rebind every cross-reference: cert.production_id, batch.production_id, delivery.job_ref, portal_job.x_fc_production_id, etc.
- Preserve chatter: copy
mail.messagerecords from MO/WO to corresponding job/step (Odoo'sres_id+modelrebinding). - Audit log: write
fp_migration.logwith 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:
- Stop operators.
- Restore Friday 8pm DB backup.
- Revert deploy to previous module bundle.
- 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.jobrecords. - Every chatter message preserved (Odoo's
mail.message.res_idrebinding). - Every PDF attachment preserved (
ir.attachment.res_idrebinding). - 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.jobstate machine transitionsfp.job.stepstate machine transitions_generate_steps_from_recipewith: 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.stepmodels 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:
- I write the implementation plan (
docs/superpowers/plans/...) — concrete per-day task breakdown, branch strategy, commit cadence. - We create a feature branch:
feat/fp-native-job-model. - We start with §6.1 (core models) and follow the dependency order through §6.16.
- End-of-week demos to you against an entech-clone.
- 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.