changes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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.3–6.10 (8 modules, light) | 5.5 |
|
||||
| Configurator | 1 |
|
||||
| Notifications | 1 |
|
||||
| Shopfloor (full rewrite) | 6 |
|
||||
| Reports | 3 |
|
||||
| KPI + Receiving + other | 1.5 |
|
||||
| Scripts + tests rewrite | 2 |
|
||||
| Migration script (§7) | 3 |
|
||||
| Test on entech-clone | 5 |
|
||||
| Cutover + burn-in | 3 |
|
||||
| **Total** | **39 working days ≈ 8 weeks** |
|
||||
|
||||
Calendar time with iteration: **9–11 weeks**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration strategy
|
||||
|
||||
entech is in production. Migration must be reversible until we're confident, and
|
||||
must preserve every link operators / accounting depend on.
|
||||
|
||||
### 7.1 Approach: Big-bang with scripted migration + 2-week shadow period
|
||||
|
||||
1. Build new models alongside existing ones in a feature branch. Both coexist
|
||||
in the codebase.
|
||||
2. Run migration script on a **clone** of entech (not entech itself). Validate
|
||||
E2E: every report renders, every cert PDF reproduces, every link resolves.
|
||||
3. Cutover weekend on entech: ~4 hour window. Steps in §7.4.
|
||||
4. **Shadow period (weeks 1–2 post-cutover):** keep `mrp.production` /
|
||||
`mrp.workorder` tables as read-only snapshots. If anything goes wrong, we can
|
||||
revert to the snapshot via a reverse migration script.
|
||||
5. After 2 weeks of stable operation, drop the MRP tables.
|
||||
|
||||
### 7.2 Migration script
|
||||
|
||||
Location: `fusion_plating_jobs/migrations/19.0.8.0.0/post-migration.py`
|
||||
|
||||
For each `mrp.production` row:
|
||||
1. Create `fp.job` with same id (use `fp_job_id_seq` aligned to MRP id space, or
|
||||
keep separate sequence and store the legacy id in `legacy_mrp_production_id` for
|
||||
audit).
|
||||
2. Copy fields: name, partner_id, product_id, product_qty → qty, dates, origin,
|
||||
state (mapping in §7.3), all `x_fc_*` extension fields.
|
||||
3. For each child `mrp.workorder`, create `fp.job.step` with all `x_fc_*` fields.
|
||||
4. Migrate `mrp.workorder.time_ids` (if present) to `fp.job.step.timelog`.
|
||||
5. Rebind every cross-reference: cert.production_id, batch.production_id,
|
||||
delivery.job_ref, portal_job.x_fc_production_id, etc.
|
||||
6. Preserve chatter: copy `mail.message` records from MO/WO to corresponding
|
||||
job/step (Odoo's `res_id` + `model` rebinding).
|
||||
7. Audit log: write `fp_migration.log` with row counts, mismatches, warnings.
|
||||
|
||||
### 7.3 State mapping
|
||||
|
||||
| MRP state | fp.job state |
|
||||
|---|---|
|
||||
| `draft` | `draft` |
|
||||
| `confirmed` | `confirmed` |
|
||||
| `progress` | `in_progress` |
|
||||
| `to_close` | `in_progress` (will be `done` after final ops) |
|
||||
| `done` | `done` |
|
||||
| `cancel` | `cancelled` |
|
||||
|
||||
| MRP WO state | fp.job.step state |
|
||||
|---|---|
|
||||
| `pending` | `pending` |
|
||||
| `waiting` | `pending` |
|
||||
| `ready` | `ready` |
|
||||
| `progress` | `in_progress` |
|
||||
| `done` | `done` |
|
||||
| `cancel` | `cancelled` |
|
||||
|
||||
### 7.4 Cutover plan (single weekend window)
|
||||
|
||||
**Friday 6pm:** Stop operators. Final MOs of the week wrapped up.
|
||||
**Friday 8pm:** Backup full database. Tag as `pre_fp_job_migration`.
|
||||
**Friday 9pm:** Deploy new module bundle to entech. Run migration script. Estimated 30 min.
|
||||
**Friday 10pm:** Smoke test — open a recent job, print sticker, scan, render CoC,
|
||||
generate margin report. If anything fails, restore from 8pm backup; abort.
|
||||
**Saturday/Sunday:** Live shop is offline anyway (weekend). Time to fix anything
|
||||
that surfaced.
|
||||
**Monday 7am:** Operators come in. Manager + tech on site for the first 2 hours.
|
||||
**Following 2 weeks:** Daily check-ins. Active monitoring of error logs. MRP
|
||||
tables still present (read-only).
|
||||
|
||||
### 7.5 Rollback plan
|
||||
|
||||
If the cutover fails or unrecoverable issues surface within 7 days:
|
||||
1. Stop operators.
|
||||
2. Restore Friday 8pm DB backup.
|
||||
3. Revert deploy to previous module bundle.
|
||||
4. Reopen as previous-MRP system.
|
||||
|
||||
After 7 days, rollback becomes "forward fix only" — too much new shop activity to
|
||||
restore.
|
||||
|
||||
### 7.6 Data preservation guarantees
|
||||
|
||||
- **Every** historical job remains queryable. Old MOs become old `fp.job` records.
|
||||
- **Every** chatter message preserved (Odoo's `mail.message.res_id` rebinding).
|
||||
- **Every** PDF attachment preserved (`ir.attachment.res_id` rebinding).
|
||||
- **Every** cert, thickness reading, batch, hold preserved with intact links.
|
||||
- **Audit-trail integrity for Nadcap / aerospace customers** is critical; the
|
||||
migration script must verify zero loss before commit. We'll add a pre-migration
|
||||
audit script that snapshots counts and a post-migration audit that re-validates.
|
||||
|
||||
---
|
||||
|
||||
## 8. Test strategy
|
||||
|
||||
### 8.1 Unit tests
|
||||
- `fp.job` state machine transitions
|
||||
- `fp.job.step` state machine transitions
|
||||
- `_generate_steps_from_recipe` with: simple recipe, nested sub_processes,
|
||||
opt-in/out overrides, start-at-node rework, missing work-centre mapping
|
||||
- Migration script: round-trip a snapshot of entech data through migrate, verify
|
||||
every row's fields match expectation
|
||||
|
||||
### 8.2 Integration tests (end-to-end on a fresh DB)
|
||||
- Quote → SO → confirm → job created → recipe assigned → steps generated → start
|
||||
step → log time → finish step → all steps done → job done → portal job
|
||||
ready_to_ship → delivery created → CoC generated → invoice posted → portal job
|
||||
complete
|
||||
- Quality hold raised mid-job → blocks finish → released → job continues
|
||||
- Rework (start-at-node) job → only later steps generated
|
||||
- Cancel a confirmed job → all steps cancelled → portal job cancelled → no cert
|
||||
|
||||
### 8.3 E2E on entech-clone
|
||||
- Restore entech production DB to a staging container.
|
||||
- Run migration script.
|
||||
- Replay last 30 days of operator actions through the new UI.
|
||||
- Run every report, every cert, every notification trigger.
|
||||
- Diff against pre-migration snapshot: identify any data drift.
|
||||
|
||||
### 8.4 Performance baseline
|
||||
- Plant Overview load time with 1000 active steps: target < 1.5s
|
||||
- Job form open with 50-step recipe: target < 800ms
|
||||
- Report rendering (CoC, traveller, sticker): target < 3s
|
||||
|
||||
---
|
||||
|
||||
## 9. Risk register
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Migration script silently drops chatter on a record | Medium | Medium | Pre/post-migration audit scripts compare message counts |
|
||||
| A module we forgot still references mrp.workorder, fails at runtime | Medium | Medium | CI grep in pre-deploy that fails on `mrp.workorder` / `mrp.production` references in our code |
|
||||
| Cert PDF differs vs current (audit/Nadcap impact) | Low | High | Render 100 sample CoCs pre-migration, render same post-migration, byte-diff |
|
||||
| Operators confused by new UI | High | Medium | Beta with 2 operators 1 week before cutover; live training Monday morning |
|
||||
| Plant Overview slower than current | Medium | Low | Performance baseline test in §8.4; index `fp_job_step.state`, `work_centre_id` |
|
||||
| Account hold gate breaks → invoices post that shouldn't | Low | High | Unit tests + integration test for held-customer invoice attempt |
|
||||
| Rollback needed >7 days post-cutover | Low | High | Forward-fix only after day 7; treat the new model as canonical |
|
||||
| `mrp` Odoo module updates break our coexistence | Low | Low | We're un-depending; if mrp updates, we don't care |
|
||||
| Missed dependency causes module install failure | Medium | Low | Test install on fresh DB before cutover |
|
||||
|
||||
---
|
||||
|
||||
## 10. Open decisions (sign-off needed)
|
||||
|
||||
These need a yes/no from you before I write the implementation plan:
|
||||
|
||||
### Q1. Naming: `fp.job` vs `fp.work.order`?
|
||||
Operators currently say "WO #00033". The label format is independent of the model
|
||||
name — we can call the model `fp.job` and still print "WO #00033" on the sticker.
|
||||
|
||||
**Recommendation:** `fp.job`. Cleaner, doesn't pretend to be Odoo MRP, "work order"
|
||||
is a UI label not a model name.
|
||||
|
||||
**Your call:** ___ `fp.job` ___ `fp.work.order` ___ other:______
|
||||
|
||||
### Q2. Sticker/sequence label format: keep `WH/MO/00033` or switch to `WH/JOB/00033`?
|
||||
Existing labels printed on real boxes around the shop say `WH/MO/00033`. Reprinting
|
||||
all of them is a real cost.
|
||||
|
||||
**Recommendation:** Keep `WH/MO/...` for migrated records (preserve the label that's
|
||||
on the box). Use `WH/JOB/...` for new records going forward. Both formats render
|
||||
identically on the sticker as "WO #...".
|
||||
|
||||
**Your call:** ___ keep `WH/MO/...` for everything ___ switch new ones to `WH/JOB/...` ___ switch all (re-label boxes) ___ other:______
|
||||
|
||||
### Q3. Drop `mrp` module dependency entirely, or keep it installed but unused?
|
||||
Removing `mrp` removes the standard Manufacturing app from the menu — managers
|
||||
who occasionally peek at standard MRP views lose them. Keeping it installed means
|
||||
~150MB of unused tables in the DB.
|
||||
|
||||
**Recommendation:** Keep `mrp` *installed* (low cost) but drop it from our
|
||||
modules' `depends`. We don't use it; it sits idle. We can revisit removing later.
|
||||
|
||||
**Your call:** ___ keep installed ___ uninstall fully ___ other:______
|
||||
|
||||
### Q4. `fp.work.centre` — new model, or extend `mrp.workcenter`?
|
||||
We could keep `mrp.workcenter` as the work-centre table even though we drop the rest
|
||||
of MRP. Saves us 1 model worth of refactor.
|
||||
|
||||
**Recommendation:** New `fp.work.centre`. Plating work centres are different from
|
||||
assembly work centres (kind = wet_line / bake / mask / rack), and we already have
|
||||
`x_fc_facility_id`, `x_fc_fp_work_center_id` extensions on `mrp.workcenter`. Cleaner
|
||||
to start fresh.
|
||||
|
||||
**Your call:** ___ new `fp.work.centre` ___ keep `mrp.workcenter` ___ other:______
|
||||
|
||||
### Q5. Step model granularity — operations only, or full recipe tree?
|
||||
**Option A:** `fp.job.step` = operations only (matches current MRP WO behaviour).
|
||||
Container nodes (recipe / sub_process) and step nodes (instructions) are pulled
|
||||
from the recipe template at render time.
|
||||
**Option B:** `fp.job.step` mirrors the full recipe tree (operations + containers + steps).
|
||||
|
||||
**Recommendation:** Option A. Simpler model, current working pattern, render
|
||||
hierarchy via JOIN at view time. Steelhead's UX achievable without DB-level tree.
|
||||
|
||||
**Your call:** ___ A (ops only) ___ B (full tree) ___ other:______
|
||||
|
||||
### Q6. Migration approach — big-bang weekend cutover, or parallel-run?
|
||||
Parallel-run = both systems live for 2 weeks, jobs created in both, comparing
|
||||
output. More robust but more complex.
|
||||
|
||||
**Recommendation:** Big-bang with shadow period (§7.1). Simpler, lower error
|
||||
surface. Cutover is ~4 hours on a weekend.
|
||||
|
||||
**Your call:** ___ big-bang ___ parallel-run ___ other:______
|
||||
|
||||
### Q7. Implementation pace — single sprint or phased?
|
||||
- Single sprint: 8–10 weeks one engineer, full cutover at end.
|
||||
- Phased: ship `fp.job`/`fp.job.step` models first (week 4); migrate one module
|
||||
per week thereafter; cutover at the end (week 12+). More UI churn for operators
|
||||
during the transition.
|
||||
|
||||
**Recommendation:** Single sprint. Operators only switch UI once.
|
||||
|
||||
**Your call:** ___ single sprint ___ phased ___ other:______
|
||||
|
||||
---
|
||||
|
||||
### Decisions locked (2026-04-25)
|
||||
|
||||
| # | Decision | Locked answer |
|
||||
|---|---|---|
|
||||
| Q1 | Model name | **`fp.job`** |
|
||||
| Q2 | Sticker label format | **Keep `WH/MO/...` for migrated records; new records use `WH/JOB/...`. Both render as "WO #..." on the sticker.** |
|
||||
| Q3 | `mrp` Odoo module | **Keep installed but drop from our `depends`** |
|
||||
| Q4 | Work centre model | **New `fp.work.centre`** |
|
||||
| Q5 | Step model granularity | **Option A — operations only (flat list, hierarchy via JOIN at view time)** |
|
||||
| Q6 | Migration approach | **Big-bang weekend cutover with 2-week shadow period** |
|
||||
| Q7 | Implementation pace | **Single sprint, ~8 weeks engineering** |
|
||||
|
||||
## 11. Next steps
|
||||
|
||||
After you sign off on §10:
|
||||
|
||||
1. I write the **implementation plan** (`docs/superpowers/plans/...`) — concrete
|
||||
per-day task breakdown, branch strategy, commit cadence.
|
||||
2. We create a feature branch: `feat/fp-native-job-model`.
|
||||
3. We start with §6.1 (core models) and follow the dependency order through §6.16.
|
||||
4. End-of-week demos to you against an entech-clone.
|
||||
5. Cutover weekend scheduled with 4 weeks notice to give the shop time to plan.
|
||||
|
||||
If something in §10 or anywhere else is wrong / unclear / debatable, flag it now —
|
||||
fixing the spec is cheap; fixing committed code is not.
|
||||
Reference in New Issue
Block a user