Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-29-configurable-charge-tax-lot-pricing-design.md
gsinghpal 21300db8e8 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>
2026-05-29 21:17:55 -04:00

163 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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