```
- [ ] **Step 3: Register in manifest**
Add to `assets.web.assets_backend`:
```python
'fusion_plating_configurator/static/src/js/express_bake_pill.js',
'fusion_plating_configurator/static/src/xml/express_bake_pill.xml',
```
- [ ] **Step 4: Verify reload**
```bash
docker exec odoo-dev-app odoo -c /etc/odoo/odoo.conf -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | grep -iE "error" | head -10
```
Expected: clean.
- [ ] **Step 5: Commit**
```bash
git add fusion_plating_configurator/static/src/js/express_bake_pill.js fusion_plating_configurator/static/src/xml/express_bake_pill.xml fusion_plating_configurator/__manifest__.py
git commit -m "feat(configurator): OWL widget FpExpressBakePill (click-to-edit bake pill)"
```
---
### Task C6: Express form view — header + lines + footer
This is the big view file. Single task, but the body is substantial. The view extends `view_fp_direct_order_wizard_form` is **not** the right approach — Express is a fresh view that targets the same model with a different layout. So this is a new `` not an inheritance.
**Files:**
- Create: `fusion_plating_configurator/views/fp_express_order_views.xml`
- Modify: `__manifest__.py` (add to `data`)
- [ ] **Step 1: Create the Express form view**
`fusion_plating_configurator/views/fp_express_order_views.xml` (full file — paste verbatim):
```xml
fp.express.order.formfp.direct.order.wizard10+ New Express Orderfp.direct.order.wizardformcurrent{'default_view_source': 'express'}
```
- [ ] **Step 2: Add `action_switch_to_legacy` method**
Edit `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` — add:
```python
def action_switch_to_legacy(self):
"""Re-open this draft in the legacy view."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'fp.direct.order.wizard',
'res_id': self.id,
'view_mode': 'form',
'view_id': self.env.ref(
'fusion_plating_configurator.view_fp_direct_order_wizard_form'
).id,
'target': 'current',
}
```
Also add the mirror `action_switch_to_express` (for the legacy view to call):
```python
def action_switch_to_express(self):
"""Re-open this draft in the Express view."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'fp.direct.order.wizard',
'res_id': self.id,
'view_mode': 'form',
'view_id': self.env.ref(
'fusion_plating_configurator.view_fp_express_order_form'
).id,
'target': 'current',
}
```
- [ ] **Step 3: Register the view in the manifest**
Add to `__manifest__.py` `data` (after the quick-create view registered in Task C2):
```python
'views/fp_express_order_views.xml',
```
Confirm the menu parent `fusion_plating_configurator.menu_fp_sales` exists by greping. If the menu xmlid is different, adjust to match the existing parent.
- [ ] **Step 4: Verify view loads + menu appears**
```bash
docker exec odoo-dev-app odoo -c /etc/odoo/odoo.conf -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | grep -iE "error|warning" | head -20
```
Expected: clean. Open `http://localhost:8069/web` → Plating → Sales & Quoting → "+ New Express Order" should appear.
- [ ] **Step 5: Commit**
```bash
git add fusion_plating_configurator/views/fp_express_order_views.xml fusion_plating_configurator/wizard/fp_direct_order_wizard.py fusion_plating_configurator/__manifest__.py
git commit -m "feat(configurator): Express Orders form view + action + menu item"
```
---
## Phase D — Drafts list + view switching
### Task D1: Drafts list `view_source` badge column + dual-routing
**Files:**
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` (add `action_open_draft`)
- Create: `fusion_plating_configurator/tests/test_express_drafts_routing.py`
- [ ] **Step 1: Write the failing test**
```python
# tests/test_express_drafts_routing.py
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install', 'fp_express')
class TestExpressDraftsRouting(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({'name': 'C'})
def test_express_draft_routes_to_express_view(self):
wiz = self.env['fp.direct.order.wizard'].create({
'partner_id': self.partner.id, 'view_source': 'express',
})
action = wiz.action_open_draft()
expected = self.env.ref('fusion_plating_configurator.view_fp_express_order_form').id
self.assertEqual(action['view_id'], expected)
def test_legacy_draft_routes_to_legacy_view(self):
wiz = self.env['fp.direct.order.wizard'].create({
'partner_id': self.partner.id, 'view_source': 'legacy',
})
action = wiz.action_open_draft()
expected = self.env.ref('fusion_plating_configurator.view_fp_direct_order_wizard_form').id
self.assertEqual(action['view_id'], expected)
```
Wire up.
- [ ] **Step 2: Run test to verify it fails**
Same command.
Expected: `AttributeError: action_open_draft`.
- [ ] **Step 3: Implement `action_open_draft`**
Add to `fusion_plating_configurator/wizard/fp_direct_order_wizard.py`:
```python
def action_open_draft(self):
"""Route the drafts-list row-click to the form view matching view_source."""
self.ensure_one()
view_xmlid = (
'fusion_plating_configurator.view_fp_express_order_form'
if self.view_source == 'express'
else 'fusion_plating_configurator.view_fp_direct_order_wizard_form'
)
return {
'type': 'ir.actions.act_window',
'res_model': 'fp.direct.order.wizard',
'res_id': self.id,
'view_mode': 'form',
'view_id': self.env.ref(view_xmlid).id,
'target': 'current',
}
```
- [ ] **Step 4: Run test to verify it passes**
Same command.
Expected: `OK`.
- [ ] **Step 5: Add `view_source` badge column to the drafts list**
Edit `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` — find the `view_fp_direct_order_wizard_list` record and add a new field inside ``:
```xml
```
Also change the list's row-click to route via the method:
```xml
```
(Standard Odoo list view supports `action="..."` + `type="object"` to override the row-click target.)
- [ ] **Step 6: Verify view loads**
```bash
docker exec odoo-dev-app odoo -c /etc/odoo/odoo.conf -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | grep -iE "error" | head -10
```
Expected: clean.
- [ ] **Step 7: Commit**
```bash
git add fusion_plating_configurator/wizard/fp_direct_order_wizard.py fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml fusion_plating_configurator/tests/test_express_drafts_routing.py fusion_plating_configurator/tests/__init__.py
git commit -m "feat(configurator): drafts list view_source badge + dual-routing click action"
```
---
### Task D2: Phase 2 deprecation banner on legacy view
**Files:**
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`
This task is shipped LATER (Phase 2, ~Week 3 after launch). Tracking here for completeness so the writing-plans output captures the full timeline.
- [ ] **Step 1: Add the banner**
Find the legacy form's `` opening tag in `view_fp_direct_order_wizard_form`. Just before it, add:
```xml
Legacy view. This form is being retired
in favour of the new Express Orders view, which is faster
for batch entry.
```
- [ ] **Step 2: Verify view loads + banner appears**
```bash
docker exec odoo-dev-app odoo -c /etc/odoo/odoo.conf -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | grep -iE "error" | head -10
```
Expected: clean. Manually verify banner appears on the legacy form.
- [ ] **Step 3: Commit**
```bash
git add fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml
git commit -m "feat(configurator): Phase 2 deprecation banner on legacy direct-order view"
```
---
## Phase E — Smoke test runbook
### Task E1: End-to-end smoke test
This is a manual checklist, not an automated test. Run after all Phase A–D tasks pass on local dev.
**Files:**
- Create: `fusion_plating_configurator/docs/express_orders_smoke_test.md` (in-repo runbook)
- [ ] **Step 1: Create the runbook**
`fusion_plating_configurator/docs/express_orders_smoke_test.md`:
```markdown
# Express Orders — Smoke Test Runbook
Run after deploy to confirm end-to-end flow. ~10 minutes.
## Setup
1. Open http://localhost:8069/web → Plating → Sales & Quoting → **+ New Express Order**
2. Confirm the form opens with the Express layout (NOT the legacy form)
## Test 1 — Happy path with mask + bake
1. Pick customer "WESTIN HEALTHCARE INC."
2. Confirm `partner_shipping_id` defaults to the customer's first address
3. Confirm `pricelist_id` defaults to the customer's default pricelist
4. Type PO# "PO-SMOKE-01"
5. Upload any PDF as the PO attachment
6. Add a line — pick part "ENG-1042 Rev B" (or create one via the inline + Create modal)
7. Confirm auto-fill: description, thickness, masking checkbox, bake pill all populate from part defaults
8. Set quantity 5, price 42.00
9. Click "Confirm Order"
10. Expected:
- SO is created
- `fp.job` row exists for the SO
- Job's recipe is the one linked to the part
- No `fp.job.node.override` rows (masking on, bake non-empty)
- Bake step's `instructions` field shows the bake text
- Job chatter has the audit post: "Bake step instructions set to: ..."
## Test 2 — Masking opt-out
1. Repeat Test 1 but uncheck the masking checkbox before confirming
2. Expected:
- `fp.job.node.override` rows exist with `included=False` for masking AND de_masking nodes
- Job chatter audit: "Masking + de-masking steps opted out (per SO line)"
## Test 3 — Bake opt-out
1. Repeat Test 1 but clear the bake pill (set to "no bake") before confirming
2. Expected:
- `fp.job.node.override` row exists with `included=False` for baking node
- Job chatter audit: "Baking steps opted out (per SO line)"
## Test 4 — Bulk-add serials
1. On a line, click the `+ bulk` button on the serial row
2. Bulk-add wizard opens
3. Switch to Range Fill mode, prefix SN-, start 1, end 5
4. Click Add Serials
5. Expected: line's `serial_ids` populated with SN-001 … SN-005
## Test 5 — DWG / OPEN buttons
1. On a line with a part selected, click DWG → upload a PDF
2. Expected: file attaches to `part.drawing_attachment_ids`, button shows updated count
3. Click OPEN
4. Expected: part form opens in a modal
## Test 6 — Currency switch
1. On a draft, change the pricelist from CAD to a USD pricelist
2. Confirm prompt: "Update Prices?" → click Yes
3. Expected: line `unit_price` values are recomputed in USD; currency pill on Grand Total flips to USD
## Test 7 — Round-trip with legacy view
1. Create a draft in Express view, save (no confirm)
2. Go to "Direct Order Drafts" list
3. Confirm the draft shows badge "Express"
4. Open the draft → confirms Express view loads
5. Click "Switch to Legacy View" header button
6. Confirm same data renders in the legacy form
7. Go back to drafts list, click the row → still routes to Express (because view_source='express')
```
- [ ] **Step 2: Commit**
```bash
git add fusion_plating_configurator/docs/
git commit -m "docs(configurator): Express Orders smoke test runbook"
```
---
## Phase F — Deferred: Phase 3 & 4 (post-launch)
These two tasks ship LATER (Weeks 4–5+ after launch). Captured here so the plan is complete.
### Task F1 (Phase 3, Week 4): Hide legacy menu + auto-redirect
**Files:**
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard.py`
Steps:
1. Add `groups="base.group_no_one"` (debug-mode-only) to the `+ New Direct Order` menuitem so it's hidden in normal mode.
2. Patch `action_open_draft` to ALWAYS return the Express view:
```python
def action_open_draft(self):
self.ensure_one()
# Phase 3 (2026-XX-XX): all drafts auto-route to Express
return {
'type': 'ir.actions.act_window',
'res_model': 'fp.direct.order.wizard',
'res_id': self.id,
'view_mode': 'form',
'view_id': self.env.ref(
'fusion_plating_configurator.view_fp_express_order_form'
).id,
'target': 'current',
}
```
3. Bump module version (e.g. 19.0.23.0.0).
4. Commit: `chore(configurator): Phase 3 — hide legacy menu, auto-redirect drafts`
### Task F2 (Phase 4, Week 5+): Full removal
**Files:**
- Delete: `view_fp_direct_order_wizard_form` record
- Delete: `action_fp_direct_order_wizard` record
- Delete: `+ New Direct Order` menuitem
- Create: `fusion_plating_configurator/migrations/19.0.24.0.0/post-migrate.py`
The post-migration:
```python
def migrate(cr, version):
# 1. Drop the view_source column (no longer used)
cr.execute("ALTER TABLE fp_direct_order_wizard DROP COLUMN IF EXISTS view_source")
# 2. Sweep orphan landing actions — any user whose landing pointed at the
# retired legacy action gets re-pointed at the Express action.
cr.execute("""
UPDATE res_users
SET x_fc_plating_landing_action_id = (
SELECT res_id FROM ir_model_data
WHERE module = 'fusion_plating_configurator'
AND name = 'action_fp_express_order'
AND model = 'ir.actions.act_window'
LIMIT 1
)
WHERE x_fc_plating_landing_action_id IN (
SELECT res_id FROM ir_model_data
WHERE module = 'fusion_plating_configurator'
AND name = 'action_fp_direct_order_wizard'
AND model = 'ir.actions.act_window'
)
""")
```
Also remove `view_source` field from the model definition. Commit: `chore(configurator): Phase 4 — remove legacy direct-order view + view_source column`
---
## Self-Review
**Spec coverage check** — every NEW field + helper + view + button from the spec has a task above:
- `fp.part.catalog.default_specification_text/default_bake_instructions/default_masking_enabled` → A1 ✓
- `fp.direct.order.line.customer_line_ref/masking_enabled/bake_instructions` → A2 ✓
- `sale.order.line.x_fc_*` (3 fields) → A3 ✓
- `sale.order.x_fc_material_process/x_fc_internal_notes/x_fc_print_terms` → A4 ✓
- `fp.direct.order.wizard.material_process/pricelist_id/validity_date/internal_notes/terms_and_conditions/view_source` + rename notes + retire currency_id → A5 ✓
- `_fp_all_nodes_with_kind` helper → B1 ✓
- `_fp_apply_express_overrides_to_job` helper + 4 quadrants + audit + idempotency → B2 ✓
- Hook into `_fp_auto_create_job` → B3 ✓
- Onchange auto-fill cascade → B4 ✓
- `_prepare_order_line_vals` carry-through → B5 ✓
- Part-default write-back on confirm → B6 ✓
- `action_open_serial_bulk_add` → B7 ✓
- `action_upload_drawing` + `action_open_part` → B8 ✓
- Currency picker context-aware display_name → C1 ✓
- Quick-create part view → C2 ✓
- SCSS tokens + light/dark → C3 ✓
- OWL widget FpExpressPartCell → C4 ✓
- OWL widget FpExpressBakePill → C5 ✓
- Express form view + menu + action + `action_switch_to_legacy`/`action_switch_to_express` → C6 ✓
- Drafts list dual-routing + badge → D1 ✓
- Phase 2 deprecation banner → D2 ✓
- Smoke test runbook → E1 ✓
- Phase 3 + 4 deferred tasks → F1, F2 ✓
**No placeholder check** — all steps have concrete code, exact file paths, and runnable commands.
**Type consistency** — method names verified consistent: `_fp_apply_express_overrides_to_job`, `_fp_all_nodes_with_kind`, `action_open_serial_bulk_add`, `action_upload_drawing`, `action_open_part`, `action_open_draft`, `action_switch_to_legacy`, `action_switch_to_express`, `action_quick_create_done`. Field names verified consistent across tasks: `x_fc_masking_enabled`, `x_fc_bake_instructions`, `x_fc_customer_line_ref`, `default_specification_text`, `default_bake_instructions`, `default_masking_enabled`, `material_process`, `pricelist_id`, `validity_date`, `internal_notes`, `terms_and_conditions`, `view_source`, `x_fc_material_process`, `x_fc_internal_notes`, `x_fc_print_terms`.
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-26-express-orders-plan.md`. Two execution options:
**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration.
**2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints.
Which approach?