The POS tax calculator mirrors Odoo 12 POS behavior exactly, supporting percent/fixed taxes, price-included taxes, tax-on-tax stacking, discounts, fiscal position remapping, and per-line or global rounding.Documentation Index
Fetch the complete documentation index at: https://docs.laportenard.com/llms.txt
Use this file to discover all available pages before exploring further.
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:
| Mapping result | Behavior |
|---|---|
taxDestId is a number | Replace the source tax with the destination tax |
taxDestId is false | Remove the tax entirely (e.g., a service charge on takeout) |
| No mapping for a tax | Pass through unchanged |
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:
| Field | Calculation |
|---|---|
amount_untaxed | Sum of per-line total_excluded |
amount_tax | Sum of per-line tax totals |
amount_total | Sum of per-line total_included |
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:
| # | Scenario | Expected |
|---|---|---|
| 1 | Single excluded % tax (18%) | unit=100 → excl=100, tax=18, incl=118 |
| 2 | Single included % tax (18%) | unit=118 → excl=100, tax=18, incl=118 |
| 3 | Discount + excluded % tax | unit=100, qty=2, disc=10%, tax=18% → base=180, tax=32.40, total=212.40 |
| 4 | Two excluded taxes, no tax-on-tax | taxA 10% + taxB 18% → tax=28, total=128 |
| 5 | Tax-on-tax (include_base_amount=true) | taxA 10% then taxB 18% on (base+taxA) |
| 6 | Fixed excluded tax | fixed=5, qty=3, unit=20 → base=60, tax=15, total=75 |
| 7 | Mixed included + excluded | taxA 18% incl + taxB 10% excl |
| 8 | Rounding edge | unit=0.99, qty=3, tax=18% → exact 0.01 rounding |
| 9 | Global vs line rounding | Different totals for 'line' vs 'global' |
src/domain/taxes/fiscalPosition.test.ts cover 19 scenarios:
| Area | Scenarios |
|---|---|
parseFiscalPositionData | Bootstrap parsing, empty payload |
resolveFiscalPositionId | Config default, customer override, para_llevar, takeout fallback, delivery, no data |
mapTaxIds | Remove tax, replace tax, passthrough, no FP, no map, empty IDs |
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.