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

9.2 KiB
Raw Blame History

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_idfp.additional.charge.type (searchable, quick-create).
  • charge_amount (Monetary, currency_field='currency_id').
  • tax_idaccount.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