From 7da51b4ec8708616735295e30602f1ad99a649ca Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 26 May 2026 19:50:01 -0400 Subject: [PATCH] docs(specs): Express Orders design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates the brainstorming session into a single design spec. Covers: header layout + field-to-model mapping, line widget (multi-row Part cell, masking + bake pills, serial bulk-add trigger), masking/baking override flow at SO confirm, currency/pricelist picker mechanics, inline part create + drawing upload + open-part buttons, and phase-out path for the legacy Direct Order view. Reuses fp.direct.order.wizard model end-to-end (Q1=D). New fields: material_process, customer_line_ref, masking_enabled, bake_instructions, default_specification_text, default_bake_instructions, default_masking_enabled, x_fc_internal_notes, x_fc_print_terms. Rename: wizard.notes → terms_and_conditions. Retire: wizard.currency_id. Mockup at .claude/mockups/express_orders.html (interactive, light + dark). --- .../specs/2026-05-26-express-orders-design.md | 635 ++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-26-express-orders-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-05-26-express-orders-design.md b/fusion_plating/docs/superpowers/specs/2026-05-26-express-orders-design.md new file mode 100644 index 00000000..24b9a9fb --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-26-express-orders-design.md @@ -0,0 +1,635 @@ +# Express Orders — Design Spec + +**Date:** 2026-05-26 +**Status:** Approved (brainstorming complete, ready for implementation planning) +**Modules touched:** `fusion_plating_configurator` (primary), `fusion_plating_jobs` (job-creation hook), `fusion_plating` (recipe walker helper) +**Backing model:** `fp.direct.order.wizard` (shared with legacy Direct Order view) +**Visual reference:** `.claude/mockups/express_orders.html` (light + dark mode, three tabs — Screen / Data Flow / Comparison) +**Inspiration:** Customer-supplied Excel mockup showing spreadsheet-style flat order entry — header grid on top, line table in the middle, footer with totals + T&C + notes + +## 1. Goal + +Replace the slow, form-per-part Direct Order wizard with a spreadsheet-style entry surface that supports fast batch entry for repeat customers. All lines on one screen, every column inline, type-once-and-remember per-part defaults, no navigation to the part record for routine work. + +The Express view writes to the **same `fp.direct.order.wizard` model** as the legacy view — there is no parallel model, no parallel storage, no migration concerns. Drafts created in one view can be re-opened in the other. + +## 2. Locked decisions (from clarifying-question pass) + +| # | Question | Decision | Source | +|---|---|---|---| +| 1 | Model strategy | **D** — new view on existing `fp.direct.order.wizard` | Q1 | +| 2 | Specification storage | Free-text on the part — new `default_specification_text` Text on `fp.part.catalog` | Q2 | +| 3 | Per-line customer sub-job ref | New `x_fc_customer_line_ref` Char on `sale.order.line` | Q3 | +| 4 | Masking checkbox scope | Both masking AND de-masking together (paired opt-out) | Q4 | +| 5 | Baking field shape | Free-text per line, auto-fill from `part.default_bake_instructions`; empty = exclude bake nodes; non-empty = include + push to `step.instructions` | Q5 | +| 6 | Currency mechanic | Pricelist-per-currency picker on the wizard; selector lists currencies the company has pricelists for | Q6 | +| 7–14 | 8 default-interpretation items | PO Pending = keep existing flag + chase mechanism; Material Process = new informational Char; Upload Part Drawing = per-line button writing to part's drawing M2M; Create Part = per-line modal opening `fp.part.catalog` form; Lead Time = reuse existing min/max; Blanket SO = reuse existing Boolean; Delivery Method = reuse existing Selection; phase-out path = both views visible initially, retire old view after Express is stable | Q7–14 | + +## 3. Architecture — single helper at SO confirm + +The flags typed on the Express line (`x_fc_masking_enabled`, `x_fc_bake_instructions`, `x_fc_customer_line_ref`) persist as plain fields on `sale.order.line`. **No staging models, no pending-override tables.** At SO confirm time, the existing `_fp_auto_create_job()` chain clones the recipe into `fp.job` + `fp.job.step` rows; a NEW helper method `sale.order.line._fp_apply_express_overrides_to_job(job)` runs immediately after, reads the three flags, and writes: + +- `fp.job.node.override` rows (`included=False`) for masking/de_masking/baking nodes when applicable +- `fp.job.step.instructions` text on the bake step when bake instructions are non-empty + +``` +SO confirm + ↓ +sale_order.action_confirm() (existing) + ↓ +_fp_auto_create_job() (existing — clones recipe → fp.job + fp.job.step rows) + ↓ +[NEW] for each contributing line: + line._fp_apply_express_overrides_to_job(job) + ↓ +job ready for shop floor +``` + +Single execution point. Idempotent (re-running pre-deletes prior overrides this helper wrote). Helper lives on `sale.order.line` so it's unit-testable in isolation and reusable from any future job-creation path. ~40 lines of Python. + +## 4. Header layout & field-to-model mapping + +### Layout — 4×4 grid, 14 fields + +``` +┌────────────────────────────────┬────────────────────────────────┐ +│ Customer * (span 2) │ Shipping Address (span 2) │ +├────────────────────────────────┼──────────────┬─────────────────┤ +│ PO Block * (span 2) │ Customer │ Job Sorting │ +│ PO # │ Job # │ │ +│ PDF attachment + Replace │ │ │ +│ ☐ PO Pending → expected date │ │ │ +├──────────────┬──────────────┬──┴────┬─────────┴─────────────────┤ +│ Material/ │ Lead Time │ Pmt │ Delivery Method │ +│ Process Tag │ (X to Y) │ Terms │ │ +├──────────────┼──────────────┼───────┼────────────────────────────┤ +│ Blanket SO │ Currency / │ Quote │ Invoice Strategy │ +│ │ Pricelist │ Valid │ │ +└──────────────┴──────────────┴───────┴────────────────────────────┘ +``` + +### Field mapping + +| # | Display | Wizard field | SO field on confirm | Status | +|---|---|---|---|---| +| 1 | Customer * | `partner_id` | `partner_id` | Existing | +| 2 | Shipping Address | `partner_shipping_id` | `partner_shipping_id` | Existing wizard, **NEW on view** | +| 3 | PO # * | `po_number` | `x_fc_po_number` | Existing | +| 3a | PO Attachment | `po_attachment_file` + `po_attachment_filename` | `x_fc_po_attachment_id` + `x_fc_po_received` | Existing | +| 3b | PO Pending toggle | `po_pending` | `x_fc_po_pending` | Existing | +| 3c | PO Expected By | `po_expected_date` | `x_fc_po_expected_date` | Existing | +| 4 | Customer Job # | `customer_job_number` | `x_fc_customer_job_number` | Existing | +| 5 | Job Sorting | `job_sort_id` | `x_fc_job_sort_id` | Existing | +| 6 | Material/Process Tag | `material_process` | `x_fc_material_process` | **NEW** Char | +| 7 | Lead Time (X to Y) | `lead_time_min_days` + `lead_time_max_days` | `x_fc_lead_time_min_days` + `x_fc_lead_time_max_days` | Existing, rendered inline as range | +| 8 | Payment Terms | `payment_term_id` | `payment_term_id` | Existing | +| 9 | Delivery Method | `delivery_method` | `x_fc_delivery_method` | Existing | +| 10 | Blanket SO | `is_blanket_order` | `x_fc_is_blanket_order` | Existing | +| 11 | Currency / Pricelist | `pricelist_id` (replacing existing `currency_id`) | `pricelist_id` | **NEW field on wizard**, existing on SO | +| 12 | Quote Validity | `validity_date` | `validity_date` | Existing on SO, **NEW on wizard** | +| 13 | Invoice Strategy | `invoice_strategy` | `x_fc_invoice_strategy` | Existing | + +### Footer — Order-Level Notes + Terms & Conditions + Totals + +Left column (stacked cards): + +- **Order-Level Notes** — internal, never prints. New `wizard.internal_notes` → new `sale.order.x_fc_internal_notes`. +- **Terms & Conditions** — customer-facing, prints on quote / SO / invoice. Re-purpose existing `wizard.notes` → write to existing `sale.order.note` (Odoo native Html). Default-seeded from `company.invoice_terms_html`, with partner-level override via `res.partner.invoice_terms`. New `x_fc_print_terms` Boolean default True controls whether T&C prints. +- **Reset to company default** action — re-reads `company.invoice_terms_html` and writes it back to `sale.order.note`. + +Right column (totals card): + +- Subtotal · Tooling Charge · Tax · **Grand Total + currency pill** — all standard Odoo monetary fields with `currency_field='currency_id'`. + +### Cleanup decision on the existing `notes` field + +The existing `fp.direct.order.wizard.notes` field's placeholder says "Internal notes for the estimator / planner - not shown to the customer" but the action method writes that value to `sale.order.note`, which DOES print on customer PDFs. **Fix as part of Section 4**: rename `wizard.notes` → `wizard.terms_and_conditions` (still writes to `sale.order.note`), and add fresh `wizard.internal_notes` → new `sale.order.x_fc_internal_notes`. Matches the mockup's two-block footer. + +## 5. Line widget design + +### Column layout + +``` +┌──┬─────────────────────┬───────────────┬────────┬──────────┬────┬───────────┬───────────┬─────┬─────┬───────┬──────────┬────────────┐ +│# │ Part Number │ Specification │ Line │ Thickness│Mask│ Bake │ Internal │ Qty │ UOM │ Price │ Subtotal │ Actions │ +│ │ (3-row stacked cell)│ (customer- │ Job # │ │ ✓ │ pill │ Notes │ │ │ │ │ DWG / OPEN │ +│ │ │ facing text) │ ABC │ │ │ │ │ │ │ │ │ │ +└──┴─────────────────────┴───────────────┴────────┴──────────┴────┴───────────┴───────────┴─────┴─────┴───────┴──────────┴────────────┘ +``` + +### Part Number cell — 3 rows stacked in one cell + +| Row | Content | Source | +|---|---|---| +| 1 (bold) | Part # / Revision | `part_catalog_id.part_number` / `part_catalog_id.revision` | +| 2 (italic) | Part description | `part_catalog_id.name` | +| 3 (muted) | Serial #s, comma-separated, with `+ bulk` button | `serial_ids` joined; `+ bulk` opens existing `fp.serial.bulk.add.wizard` | + +Multi-row rendered via a small custom OWL widget `fp_express_part_cell` (~80 lines). Other columns are stock Odoo list cells. + +### Line # column + +The narrow column carrying the row handle does double duty: **default state shows the line number (small, bold, muted); on row hover the number swaps to a `⋮⋮` drag grip**. Pure CSS hover swap, no JS. + +### Column → field mapping + +| Col | Display | Wizard line field (`fp.direct.order.line`) | SO line field on confirm (`sale.order.line`) | Status | +|---|---|---|---|---| +| # | Line # / drag | `sequence` | `sequence` | Existing | +| Part — row 1 | Part # / Rev | M2O traversal via `part_catalog_id` | `x_fc_part_catalog_id.part_number` / `x_fc_revision_snapshot` | Existing | +| Part — row 2 | Description | `part_catalog_id.name` | derived | Existing | +| Part — row 3 | Serial #s + `+ bulk` | `serial_ids` (M2M) | `x_fc_serial_ids` | Existing | +| Specification | customer-facing text | `line_description` | `name` (Odoo native) | Existing | +| Line Job # | customer sub-ref | `customer_line_ref` | `x_fc_customer_line_ref` | **NEW** (Char) | +| Thickness | range | `thickness_range` | `x_fc_thickness_range` | Existing | +| Mask ✓ | masking toggle | `masking_enabled` Boolean default True | `x_fc_masking_enabled` | **NEW** (Boolean) | +| Bake pill | bake free-text | `bake_instructions` Text | `x_fc_bake_instructions` | **NEW** (Text) | +| Internal Notes | shop-floor notes | `internal_description` | `x_fc_internal_description` | Existing | +| Qty | quantity | `quantity` | `product_uom_qty` | Existing | +| UOM | unit | derived from product | `product_uom` | Existing — read-only | +| Price | unit price | `unit_price` | `price_unit` | Existing | +| Subtotal | extended | `line_subtotal` (computed) | `price_subtotal` (computed) | Existing | +| Action: DWG | upload drawing → part | NEW button method | writes to `part_catalog_id.drawing_attachment_ids` (M2M) | **NEW** UI | +| Action: OPEN | open part form | NEW button method | navigates to `fp.part.catalog` form, `target='new'` | **NEW** UI | + +**Optional-show columns** (Odoo `optional="hide"` — operator adds via column-toggle menu): Shop Job # (`job_number`), Process / Recipe (`process_variant_id`), Effective Process, Tax IDs, Part Deadline override + offset, WO Group Tag, Rush Order. + +### Widget behaviours + +**A. Part picker (Many2one autocomplete)** + +- Domain: `[('partner_id', '=', parent.partner_id), ('is_latest_revision', '=', True)]` +- `_rec_names_search = ['part_number', 'name', 'default_code']` (audit existing model) +- **NEW: `name_search` override** that bumps parts frequently ordered by `parent.partner_id` to the top +- Quick-create: `+ Create new part ""` opens the inline create modal (Section 8) + +**B. Auto-fill cascade on part pick** (existing `_onchange_part_catalog_id`, extended) + +When the line's `part_catalog_id` is set: + +| Line field | Filled from | +|---|---| +| description (line.name) | `part.default_specification_text` (NEW) → fallback: last SO line's `name` for this part | +| thickness_range | `part.x_fc_default_thickness_range` | +| process_variant_id | `part.default_process_variant_id` or last-used variant | +| masking_enabled | `part.default_masking_enabled` (NEW Boolean on part, default True) | +| bake_instructions | `part.default_bake_instructions` (NEW) → fallback: last SO line's `bake_instructions` for this part | +| tax_ids | from last SO line for this part + fiscal position | +| unit_price | from last SO line + customer pricelist | + +**C. Masking toggle** — plain Boolean cell with `widget="boolean_toggle"`. Default True. Unchecked → at job creation, helper spawns `fp.job.node.override(included=False)` for every masking + de_masking node (Section 6). + +**D. Bake pill** — small custom OWL widget `fp_bake_pill` (~50 lines) wrapping a Text field. Empty → italic muted "no bake" pill. Non-empty → coloured pill showing the text. Click pill → inline popover with textarea + Save / Clear. At job creation, empty pill opts out of bake nodes; non-empty keeps them AND writes the text to `step.instructions` (Section 6). + +**E. Bulk-add serials** — `+ bulk` button next to the serial input opens existing `fp.serial.bulk.add.wizard`. NEW thin helper `action_open_serial_bulk_add` on `fp.direct.order.line` (mirror on `sale.order.line` so the button works post-confirm too): + +```python +def action_open_serial_bulk_add(self): + return self.env.ref('fusion_plating_configurator.action_fp_serial_bulk_add_wizard')\ + .read()[0] | {'context': { + 'default_target_model': 'fp.direct.order.line', + 'default_target_id': self.id, + 'default_qty_expected': self.quantity, + }} +``` + +**F. DWG button** — opens HTML file picker, uploads file, creates `ir.attachment` with `res_model='fp.part.catalog'`, `res_id=part.id`, links to `part.drawing_attachment_ids` via `(4, att.id)`. Drawing lives on the PART (not the line) so future orders reuse it. Audit posted to part's chatter. + +**G. OPEN button** — returns window action to `fp.part.catalog` form, `res_id=part.id`, `target='new'`. Closes modal returns to Express view with line still in place. + +### Row management + +- Add: standard `+ Add a line` button (no auto-add — out of scope for v1) +- Delete: small × on row hover +- Reorder: drag grip (replaces line # on hover) +- Validation: existing `is_missing_info` boolean compute drives `decoration-warning` amber row tint. Extended to flag missing `part_catalog_id`, `quantity <= 0`. `unit_price=0` warns but doesn't block (some shops quote $0 for samples). + +### Rendering implementation + +- Line list = Odoo native `` (same as legacy wizard's list) +- Multi-row Part cell = single custom OWL widget (~80 lines) +- Bake pill = custom OWL widget (~50 lines) +- Masking = stock `boolean_toggle` +- DWG / OPEN = standard `