# Deadline Architecture — Per-Part Effective Deadlines **Date:** 2026-04-29 **Module:** `fusion_plating_configurator` (+ `fusion_plating_invoicing` for partner fields) **Status:** Approved by user, ready to implement ## Problem Three deadline fields exist today (`commitment_date`, `x_fc_internal_deadline`, `x_fc_part_deadline`) but the per-line `x_fc_part_deadline` is a free-form override with no inheritance rule. There is no way to express: - "This part on this order needs more time than the rest" without manual date entry on every line - "This specific part normally takes 14 days from start" as a part-catalog default - "When does the order *actually finish*" when lines have varying deadlines (the order's `commitment_date` doesn't roll up) - "We'll be late" warning when planned line deadlines exceed the customer-promise Customer profile already carries duration defaults (`x_fc_default_customer_deadline_days`, `x_fc_default_internal_deadline_days`) that cascade to the order header, but those defaults stop at the header — they don't reach the line. ## Goals 1. Customer profile remains the single source of manual durations. 2. Per-line effective deadline auto-populates with no manual input required, using a layered fallback chain. 3. User can override at the line level via either an absolute date OR a days-offset. 4. A part can carry its own typical lead time so it auto-applies whenever that part lands on a line. 5. Order header gets a computed completion date and a "we'll be late" forecast that distinguishes from the customer-promise date. ## Non-Goals - Rescheduling logic (auto-shift `planned_start_date` when shop is overloaded) — separate project. - Customer-portal display of effective deadlines — touches `fusion_plating_portal`; not in this spec. - Per-process-variant lead time — explicitly rejected during brainstorming; revisit only if shops report estimation drift. ## Concept Map | Field | Lives on | Type | Source | |---|---|---|---| | `x_fc_default_customer_deadline_days` | `res.partner` | Integer | Manual (existing) | | `x_fc_default_internal_deadline_days` | `res.partner` | Integer | Manual (existing) | | `x_fc_default_lead_time_days` | `fp.part.catalog` | Integer | Manual (**NEW**) | | `x_fc_planned_start_date` | `sale.order` | Date | Manual or scheduler (existing) | | `commitment_date` (Customer Deadline) | `sale.order` | Date | Manual; cascaded from customer (existing — unchanged) | | `x_fc_internal_deadline` | `sale.order` | Date | Manual; cascaded from customer (existing — unchanged) | | `x_fc_part_deadline` | `sale.order.line` | Date | Manual override (existing — semantics now one of two override forms) | | `x_fc_part_deadline_offset_days` | `sale.order.line` | Integer | Manual override (**NEW**) — alternative to absolute date | | `x_fc_effective_part_deadline` | `sale.order.line` | Date | **Computed (NEW)** — see resolution chain | | `x_fc_effective_internal_deadline` | `sale.order.line` | Date | **Computed (NEW)** — line's customer date minus order buffer | | `x_fc_order_completion_date` | `sale.order` | Date | **Computed (NEW)** = `max(line.x_fc_effective_part_deadline)` | | `x_fc_is_late_forecast` | `sale.order` | Boolean | **Computed (NEW)** = `order_completion_date > commitment_date` | ## Resolution Chain — `x_fc_effective_part_deadline` First match wins: ``` 1. line.x_fc_part_deadline # explicit absolute-date override 2. line.x_fc_part_deadline_offset_days # "+N days from order" → commitment_date + offset_days 3. line.x_fc_part_catalog_id.x_fc_default_lead_time_days # part-specific lead time → planned_start_date + part.default_lead_time_days 4. order.commitment_date # falls through to order promise # (which is itself derived from customer profile) ``` Every fallback ultimately routes back to the customer profile via the order header's `commitment_date`. No record is ever blank — even a brand-new line with no overrides immediately shows the order's customer-promise date. ## Resolution — `x_fc_effective_internal_deadline` ``` buffer_days = commitment_date - x_fc_internal_deadline (= the gap implied by customer profile: customer_days - internal_days) x_fc_effective_internal_deadline = x_fc_effective_part_deadline - buffer_days ``` If the customer profile says "5 days customer / 3 days internal", every line's internal target = its effective customer date minus 2 days. Automatic, no per-line entry. ## Order-Level Rollups ```python x_fc_order_completion_date = max(line.x_fc_effective_part_deadline) for line in order_line if not line.x_fc_archived x_fc_is_late_forecast = bool(x_fc_order_completion_date > commitment_date) ``` Suppress `is_late_forecast` when `x_fc_is_blanket_order=True` (blanket orders span months and would always trigger). ## UI Changes — Apply on All Three Surfaces Per the project parity rule (memory: feedback_direct_order_three_surface_parity), changes apply to: 1. **SO line view** — `views/sale_order_views.xml` (`x_fc_*` prefix) 2. **Direct Order wizard** — `wizard/fp_direct_order_wizard_views.xml` (bare names) 3. **Quote Configurator** — n/a, single-line model — skip unless quote needs deadline at line level (it doesn't today) ### SO line columns | Column | Default | Purpose | |---|---|---| | `x_fc_part_deadline` (Part Deadline Override) | optional, hide | Absolute-date manual override | | `x_fc_part_deadline_offset_days` (Days Offset) | optional, hide | "+5" form for users who think in days | | `x_fc_effective_part_deadline` (Effective Deadline) | optional, **show** | Computed; visible badge with `late` decoration when `> commitment_date` | | `x_fc_effective_internal_deadline` (Shop Target) | optional, hide | Available for shop-floor views | Tooltip on **Effective Deadline**: "Manual override → offset → part lead time → order date". ### SO header — Scheduling group Add two readonly fields next to existing deadline group: - `x_fc_order_completion_date` — computed; readonly - `x_fc_is_late_forecast` — computed; renders as a `⚠ Late` badge when `True` ### Part catalog form Add `Default Lead Time (days)` Integer field next to `Surface Area` in the existing geometry group. Tooltip: "Optional: how many days from `planned_start_date` this part typically needs. Used as a smart default on order lines when no explicit deadline is set." ### Customer profile (`res.partner`) No UI change. Existing `x_fc_default_customer_deadline_days` and `x_fc_default_internal_deadline_days` already drive everything via the order header cascade. ## Edge Cases 1. **`planned_start_date` blank** — fall back to `fields.Date.today()` for any anchor calculation (matches existing `_fp_recompute_default_deadlines` behaviour). 2. **Customer profile values both = 0** — `commitment_date` ends up = `planned_start_date`. Don't error; user must set or override. 3. **`commitment_date` blank** (orphan order) — `effective_part_deadline` falls back to `planned_start_date` so the field is never null. 4. **Both `x_fc_part_deadline` AND `x_fc_part_deadline_offset_days` set on a line** — absolute date wins (rule 1 of the chain). Add a soft `@api.constrains` that suggests clearing one (`UserError` would be too aggressive; use a chatter log instead). 5. **Negative buffer** (customer profile has `internal_days > customer_days`) — `effective_internal_deadline` would land *after* `effective_part_deadline`. Clamp internal to ≤ effective_part_deadline so it can never exceed the customer date. 6. **Empty order** — `order_completion_date = False`, `is_late_forecast = False`. 7. **Archived lines** — exclude from `order_completion_date` rollup (`x_fc_archived = False` filter). 8. **Blanket orders** — `is_late_forecast` suppressed (always returns `False`) since blanket spans are intentionally long. ## Migration | Field | Action | |---|---| | `fp.part.catalog.x_fc_default_lead_time_days` | New Integer, default 0. No backfill — 0 = "no part default" | | `sale.order.line.x_fc_part_deadline_offset_days` | New Integer, default 0 | | `sale.order.line.x_fc_effective_part_deadline` | New Date, computed stored. Auto-populates on first `_recompute` | | `sale.order.line.x_fc_effective_internal_deadline` | New Date, computed stored. Auto-populates on first `_recompute` | | `sale.order.x_fc_order_completion_date` | New Date, computed stored. Auto-populates on first `_recompute` | | `sale.order.x_fc_is_late_forecast` | New Boolean, computed (non-stored OK; cheap to recompute) | No data migration script needed. Existing `commitment_date`, `x_fc_internal_deadline`, `x_fc_part_deadline` keep their semantics. The new compute fields populate via Odoo ORM's standard recompute pass during module upgrade. ## Reporting / Downstream - Existing reports (Plant Overview kanban, KPI dashboards, late-orders filter) keep using `commitment_date` for "promise to customer". - New shop-scheduling consumers should use `x_fc_effective_part_deadline` per line and `x_fc_order_completion_date` per order. - `x_fc_is_late_forecast=True` becomes a natural filter for "orders we'll be late on" — list view decoration, dashboard tile, future notification. ## Implementation Files | File | Change | |---|---| | `fusion_plating_configurator/models/fp_part_catalog.py` | Add `x_fc_default_lead_time_days` field | | `fusion_plating_configurator/views/fp_part_catalog_views.xml` | Add field to form (geometry group) | | `fusion_plating_configurator/models/sale_order_line.py` | Add `x_fc_part_deadline_offset_days`, `x_fc_effective_part_deadline`, `x_fc_effective_internal_deadline` + computes | | `fusion_plating_configurator/models/sale_order.py` | Add `x_fc_order_completion_date`, `x_fc_is_late_forecast` + computes | | `fusion_plating_configurator/views/sale_order_views.xml` | Add new line columns + header rollup fields + late badge | | `fusion_plating_configurator/wizard/fp_direct_order_line.py` | Mirror line fields (bare names: `part_deadline_offset_days`, `effective_part_deadline`, `effective_internal_deadline`) | | `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` | Add wizard line columns | | `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` | Map bare names → `x_fc_*` when creating SO | | `fusion_plating_configurator/__manifest__.py` | Bump version to `19.0.18.6.0` | ## Test Plan 1. **Customer cascade unchanged** — create SO, pick partner with deadline-days set, plan a start date → header `commitment_date` and `x_fc_internal_deadline` auto-populate (no regression). 2. **Line with no overrides** — add part to line → `x_fc_effective_part_deadline = commitment_date`. 3. **Part with `default_lead_time_days = 14`** — line picks that part → `effective_part_deadline = planned_start + 14`. Order's `completion_date` reflects the later of header or line. 4. **Per-line offset** — set `part_deadline_offset_days = 3` → `effective = commitment_date + 3 days`. 5. **Per-line absolute override** — set `x_fc_part_deadline = X` → `effective = X` regardless of other settings. 6. **Mixed-deadline order** — two lines with different `effective_part_deadline` → `order_completion_date = max(both)`. `is_late_forecast = True` if max > `commitment_date`. 7. **Blanket order** — `is_late_forecast = False` even when completion > commitment. 8. **Archived line** — excluded from `order_completion_date` calc. 9. **Direct Order wizard parity** — same auto-fill + override behaviour on the wizard before creating an SO; values transfer correctly. ## Out of Scope (Defer) - Rescheduling auto-adjust on `planned_start_date` change. - Customer-portal effective-deadline display. - Per-process-variant lead time. - Notification/email when `is_late_forecast` flips to True.