diff --git a/fusion_plating/docs/superpowers/specs/2026-05-29-configurable-charge-tax-lot-pricing-design.md b/fusion_plating/docs/superpowers/specs/2026-05-29-configurable-charge-tax-lot-pricing-design.md new file mode 100644 index 00000000..ff09aa09 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-29-configurable-charge-tax-lot-pricing-design.md @@ -0,0 +1,162 @@ +# Configurable Additional Charge + Order-Level Tax + Lot Pricing + +**Date:** 2026-05-29 +**Status:** Approved design — pending spec review → implementation plan +**Module:** `fusion_plating_configurator` (direct/express order entry) + +## Goal + +In the Direct/Express order-entry summary: +1. Replace the hard-coded **"Tooling Charge"** with a **configurable, searchable, creatable + charge type** + amount (Tooling, Rush, Setup, …). +2. Add a **single configurable tax type** applied to **(subtotal + charge)**. +3. Reorder the summary: Subtotal → Additional Charge → Tax → Grand Total. +4. Support **lot pricing** per line: enter a flat lot total for a batch (e.g. 500 parts for + $1000) while keeping the real part count for production. + +## Background — current state (verified 2026-05-29) + +In `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` (`fp.direct.order.wizard`): +- `tooling_charge` (Monetary, `:312`) — a single amount with the fixed label "Tooling Charge". +- `_compute_totals` (`:393`): `total_subtotal` = Σ(qty×unit_price) of lines (excl. tooling); + `total_tax` = Σ per-line `tax_ids.compute_all` **plus** tooling taxed at the *first taxed + line's* rate; `total_amount` = subtotal + tax + tooling (charge added **after** tax). +- `action_create_order` (`:857`, `:907`): each part line → SO `order_line` with + `product_id = product.id` (one generic service product), `price_unit = line.unit_price`, + and `tax_ids = line.tax_ids` (per-line tax). The tooling charge → its own SO line named + literally `'Tooling Charge'`, taxed from the first taxed line. SO header also stores + `x_fc_tooling_charge` (`:821`). +- The wizard **line** (`fp.direct.order.line`) carries `quantity`, `unit_price`, and a + per-line `tax_ids` picker (Sub 9 override). + +So today: one fixed-label charge, per-line taxes, charge taxed after the fact, no lot pricing. + +## Locked decisions + +| # | Decision | +|---|---| +| D1 | **Charge = a type, one per order.** New model `fp.additional.charge.type` (searchable + quick-create). Wizard: `charge_type_id` + `charge_amount` replace `tooling_charge`. | +| D2 | **Charge type carries an optional `default_amount`** that pre-fills `charge_amount` when picked (editable). | +| D3 | **Single order-level tax.** New `tax_id` (M2O `account.tax`, sale taxes) on the wizard, applied to **(subtotal + charge)**. It **replaces** per-line taxes — the per-line `tax_ids` picker is removed from the wizard line UI, and on SO-create every part line **and** the charge line get this one tax. | +| D4 | **Tax default:** `tax_id` defaults from the company default sale tax (`company_id.account_sale_tax_id`) so it's never blank. (Fiscal-position mapping is a later refinement.) | +| D5 | **Lot pricing per line.** Wizard line gets `is_lot_priced` (Boolean) + `lot_total` (Monetary). When on: `unit_price = lot_total / quantity` (onchange), `unit_price` read-only, **quantity preserved** (production count). Line total ≈ lot_total; clean on even division, may round a cent or two otherwise (accepted). | +| D6 | **Summary order:** Subtotal → Additional Charge (type + amount) → Tax (type + computed amount) → Total Lines → Total Quantity → Grand Total. | +| D7 | **Scope:** the Direct/Express order wizard only. Quote configurator untouched. | + +## Design + +### 1. New model — `fp.additional.charge.type` + +`fusion_plating_configurator/models/fp_additional_charge_type.py`: + +| Field | Type | Notes | +|---|---|---| +| `name` | Char (required) | shown in the searchable dropdown; quick-create on a typed name | +| `default_amount` | Monetary | optional pre-fill; `currency_id` = company currency | +| `currency_id` | M2O `res.currency` | default `company_id.currency_id` (for the Monetary) | +| `active` | Boolean | archivable | +| `sequence` | Integer | ordering | + +`_rec_name = 'name'`; quick-create enabled (default M2O behavior). ACL: read for internal +users, write/create for estimator + manager (mirror `fp.sale.description.template`). Menu under +**Configuration → Pricing & Billing → Additional Charge Types**. Seed one record +**"Tooling Charge"** (no default amount) so existing behavior carries forward. + +### 2. Wizard header fields (`fp.direct.order.wizard`) + +Replace `tooling_charge` with: +- `charge_type_id` → `fp.additional.charge.type` (searchable, quick-create). +- `charge_amount` (Monetary, `currency_field='currency_id'`). +- `tax_id` → `account.tax`, `domain=[('type_tax_use','=','sale')]`, default = company default + sale tax (§D4). + +`@api.onchange('charge_type_id')`: when a type is picked and `charge_amount` is 0/empty, +pre-fill `charge_amount = charge_type_id.default_amount`. + +Keep `tooling_charge` column undropped for back-compat with in-flight drafts, but it's no +longer written/read by new flows (transient model — no migration needed). + +### 3. Wizard line fields (`fp.direct.order.line`) + +Add: +- `is_lot_priced` (Boolean, default False) — "Lot price" toggle. +- `lot_total` (Monetary, `currency_field='currency_id'`). + +`@api.onchange('is_lot_priced', 'lot_total', 'quantity')`: when `is_lot_priced` and +`quantity > 0`, set `unit_price = lot_total / quantity`. (`unit_price` stays a normal field; +the onchange drives it. In the view, `unit_price` is `readonly` when `is_lot_priced`.) When +the toggle is off, `unit_price` is editable as today. Remove the per-line `tax_ids` picker from +the line view (D3) — the field stays on the model (harmless) but is no longer the tax source. + +### 4. `_compute_totals` rewrite + +``` +subtotal = Σ(line.quantity × line.unit_price) # lot lines already carry derived unit_price +charge = charge_amount +taxable = subtotal + charge +tax = tax_id.compute_all(taxable, currency, quantity=1, partner)['total_included'] + − …['total_excluded'] # 0 when tax_id is blank +grand = subtotal + charge + tax +``` + +Drops the per-line `compute_all` loop and the "tooling taxed at first line" logic. `depends` +becomes `line_ids.quantity`, `line_ids.unit_price`, `charge_amount`, `tax_id`, `currency_id`. + +### 5. SO creation (`action_create_order`) + +- **Part lines:** change `'tax_ids': line.tax_ids…` → `'tax_ids': [(6, 0, tax_id.ids)] if + tax_id else False` (the one order-level tax). `price_unit` already reflects lot pricing + (derived unit). Also stamp `x_fc_lot_total` / `x_fc_is_lot_priced` onto the SO line for + reference/reporting (so an invoice could show "Lot of 500 — $1000"). +- **Charge line:** name = `charge_type_id.name` (not "Tooling Charge"), `price_unit = + charge_amount`, `tax_ids = [(6,0, tax_id.ids)]`, generic `product`. Only created when + `charge_amount` (or `charge_type_id`) is set. +- SO header: add `x_fc_additional_charge_type_id` + `x_fc_additional_charge_amount` for + reference; leave the legacy `x_fc_tooling_charge` column in place (unwritten). + +### 6. Views + +- `fp_express_order_views.xml` summary card: reorder per D6; swap the tooling amount for the + `charge_type_id` dropdown + `charge_amount`; add the `tax_id` dropdown next to the computed + Tax. Wizard line list: add the **Lot price** toggle + **Lot Total** (visible/required when + toggled); make `unit_price` readonly when `is_lot_priced`; remove the per-line tax column. +- New tree/form for `fp.additional.charge.type` + the Configuration menu item. + +### 7. Out of scope / deferred + +- Quote configurator (`fp.quote.configurator`) — order entry only. +- Multiple additional charges per order (one slot). +- Multiple taxes at once (single pick; grouped/compound taxes cover GST+PST cases). +- Per-charge-type product mapping (all charge lines reuse the generic service product). +- Fiscal-position-based tax defaulting (company default sale tax for now). + +## Testing + +`TransactionCase` in `fusion_plating_configurator/tests/`: +- **Charge type:** quick-create produces a record; picking it pre-fills `charge_amount` from + `default_amount`. +- **Totals:** with subtotal $50, charge $0, a 13% tax → tax $6.50, grand $56.50 (matches the + current screenshot). With charge $100 and 13% tax → tax = 13% × (subtotal + 100); grand = + subtotal + 100 + tax (proves tax is on subtotal **+ charge**). +- **Lot pricing:** `is_lot_priced=True`, quantity 500, lot_total 1000 → `unit_price` onchange = + 2.00; line subtotal = 1000; quantity stays 500. +- **SO create:** every part line + the charge line carries `tax_id`; the charge line's name = + the charge type's name. + +## Files to touch + +- **New:** `fusion_plating_configurator/models/fp_additional_charge_type.py` +- `fusion_plating_configurator/models/__init__.py` +- `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` (fields, onchange, + `_compute_totals`, `action_create_order`) +- `fusion_plating_configurator/wizard/fp_direct_order_line.py` (`is_lot_priced`, `lot_total`, + onchange) +- `fusion_plating_configurator/models/sale_order.py` + `sale_order_line.py` (SO ref fields: + `x_fc_additional_charge_type_id`/`amount`, `x_fc_lot_total`/`x_fc_is_lot_priced`) +- `fusion_plating_configurator/views/fp_express_order_views.xml` (summary reorder + fields, + line lot toggle, remove per-line tax) +- `fusion_plating_configurator/views/` new view + menu for `fp.additional.charge.type` +- `fusion_plating_configurator/security/ir.model.access.csv` (new model ACL) +- `fusion_plating_configurator/data/` seed "Tooling Charge" type +- `fusion_plating_configurator/__manifest__.py` (version bump; register new files) +- tests