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>
9.2 KiB
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:
- Replace the hard-coded "Tooling Charge" with a configurable, searchable, creatable charge type + amount (Tooling, Rush, Setup, …).
- Add a single configurable tax type applied to (subtotal + charge).
- Reorder the summary: Subtotal → Additional Charge → Tax → Grand Total.
- 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-linetax_ids.compute_allplus 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 → SOorder_linewithproduct_id = product.id(one generic service product),price_unit = line.unit_price, andtax_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 storesx_fc_tooling_charge(:821).- The wizard line (
fp.direct.order.line) carriesquantity,unit_price, and a per-linetax_idspicker (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_unitalready reflects lot pricing (derived unit). Also stampx_fc_lot_total/x_fc_is_lot_pricedonto 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)], genericproduct. Only created whencharge_amount(orcharge_type_id) is set. - SO header: add
x_fc_additional_charge_type_id+x_fc_additional_charge_amountfor reference; leave the legacyx_fc_tooling_chargecolumn in place (unwritten).
6. Views
fp_express_order_views.xmlsummary card: reorder per D6; swap the tooling amount for thecharge_type_iddropdown +charge_amount; add thetax_iddropdown next to the computed Tax. Wizard line list: add the Lot price toggle + Lot Total (visible/required when toggled); makeunit_pricereadonly whenis_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_amountfromdefault_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_priceonchange = 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__.pyfusion_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 forfp.additional.charge.typefusion_plating_configurator/security/ir.model.access.csv(new model ACL)fusion_plating_configurator/data/seed "Tooling Charge" typefusion_plating_configurator/__manifest__.py(version bump; register new files)- tests