docs(plating): spec — configurable charge type + order-level tax + lot pricing

Direct/Express order entry: a searchable/creatable fp.additional.charge.type
replaces the fixed Tooling Charge; one order-level account.tax applies to
(subtotal + charge); per-line lot pricing (flat lot total, derived unit price,
qty preserved). Reordered summary. Quotes out of scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-29 21:17:55 -04:00
parent 1e9ffccd6b
commit 21300db8e8

View File

@@ -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