spec(deadlines): per-part effective deadlines with customer-profile cascade
Resolution chain: explicit override → days offset → part lead time → order commitment. Adds x_fc_default_lead_time_days on part catalog; per-line effective_part_deadline + effective_internal_deadline computes; order-level completion_date rollup + is_late_forecast warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user