Tax data model
src/domain/taxes/types.ts:
payload.taxes) and wired to products via product.tax_ids. Fiscal positions and their tax mappings come from payload.fiscal_positions and payload.fiscal_position_taxes.
Core calculator
src/domain/taxes/calc.ts — pure functions, no DOM, no stores:
Calculation rules (matching Odoo POS)
- Apply discount first:
discountedUnit = unitPrice × (1 − discount/100) - Sort taxes by
sequencethenid price_include: true— back-solve base from included priceprice_include: false— add tax on top of baseinclude_base_amount: true— next tax’s base = previous base + previous tax amount (tax-on-tax)- Fixed taxes —
amount × qty(excluded) or backed out from included price - Rounding:
line: round each tax amount tocurrency.rounding, then sumglobal: keep full precision per line, round only at order totals
Fiscal position tax remapping
Certain taxes may not apply to takeout or delivery orders (e.g., a service charge that only applies to dine-in). The frontend mirrors Odoo’saccount.fiscal.position.map_tax() to remap taxes in real time as the order type or customer changes.
Resolution chain
src/domain/taxes/fiscalPosition.ts — resolveFiscalPositionId():
Step 4 is the fallback for when no customer is selected and no config-level default FP is set. Without it, takeout orders would have no fiscal position and the Propina Legal tax would remain.
Tax mapping
mapTaxIds() applies the fiscal position’s tax mappings to each line’s tax IDs before calculation:
Frontend reactivity
The effective fiscal position is computed as a derived value inside theusePOS computed memo:
activeOrder changes, the memo recomputes, and the tax breakdown updates immediately — no server round-trip required.
Backend mirror
Both_resolve_fiscal_position_id (in api_orders.py) and _hub_resolve_fiscal_position_id (in pos_order.py) implement the same 4-step chain server-side. The frontend resolution exists to provide instant feedback; the backend is the authority at finalization.
Configuration
Thedefault_takeout_fiscal_position_id field lives on nu.pos.config (the shared configuration model). Set it in the Odoo backend under the “Fiscal” tab of the shared POS config form.
Order totals integration
src/domain/orders/services/orderMath.ts:
calculateTotals(lines, pricingContext, fiscalPositionId?) accepts an optional fiscal position ID. When provided, each line’s tax IDs are remapped through mapTaxIds() before computing taxes. The original taxIds on lines are never mutated.
Test scenarios
The test suite insrc/domain/taxes/calc.test.ts covers 9 scenarios aligned with Odoo 12:
Fiscal position tests in
src/domain/taxes/fiscalPosition.test.ts cover 19 scenarios:
Order math FP integration tests in
orderMath.test.ts cover tax removal, replacement, and passthrough via fiscal position mappings.
These expected totals should be verified against Odoo 12 native POS for the same product/tax setup. The calculator is designed to produce identical results to Odoo’s built-in tax engine.