# Customizing sales tax calculation in ERPNext - Kategorie: ERPNext - Autor: Raffael Meyer - Veröffentlicht am: April 29, 2026 This guide shows developers how to customize ERPNext's sales tax behaviour. This guide assumes familiarity with the two-phase model (resolution → calculation) and the data-level setup walked through in the companion post [Configuring Sales Taxes in ERPNext](/blog/erpnext/configuring-sales-taxes). The guiding principle: **prefer data over code**. ERPNext's tax engine is country-blind by design. The five `charge_type` values plus `included_in_print_rate` and `add_deduct_tax` cover most real-world VAT/GST/sales-tax patterns. Reach for code-level overrides only when arithmetic, presentation, or lifecycle behaviour cannot be expressed declaratively. --- ## Customization ladder (least to most invasive) 1. **Data** — Tax Templates, Item Tax Templates, Tax Categories, Tax Rules. See [Configuring Sales Taxes](/blog/erpnext/configuring-sales-taxes). 2. **Settings** — flags on **Accounts Settings** and **Company**. See [Configuring Sales Taxes](/blog/erpnext/configuring-sales-taxes). 3. **Custom Fields** — extend DocTypes without touching core schema. 4. **Doc events** — hook into validate / submit / cancel lifecycle. 5. **`regional_overrides`** — country-scoped function substitution. 6. **Client / server scripts** — last resort for one-off transactional tweaks. Climb only as high as the requirement forces you to. Steps 1–2 cover the bulk of real-world tax requirements; everything below assumes you have already ruled them out. --- ## 1. Extending DocTypes with Custom Fields Custom fields are the right tool for adding country-specific data that the engine should ignore but downstream consumers (print formats, reports, external integrations) must see. Examples already in the codebase: - UAE adds _Tax Registration Number_, _Reverse Charge_, _VAT Emirate_, _Tourist Tax Return_ on **Sales Invoice**, **Purchase Invoice**, and **Address**. - Italy adds _Fiscal Regime_, _Fiscal Code_, _VAT Collectability_ on **Company**, plus per-item `tax_rate` and `tax_amount` hidden fields on **Sales Invoice Item**. Pattern: install custom fields via `frappe.custom.doctype.custom_field.custom_field.create_custom_fields(fields_dict)` from your country setup module's `update_regional_tax_settings(country, company)` function. --- ## 2. Doc event handlers Use doc events for lifecycle behaviour that must not run in the calculation hot path. Wire them in `hooks.py`: ```python doc_events = { "Sales Invoice": { "validate": "your_app.tax.validate_special_case", "on_submit": "your_app.tax.post_external_filing", }, } ``` Common patterns: - **Validate**: enforce country-specific constraints (e.g. require _VAT Emirate_ for UAE companies, or block submission if reverse-charge math doesn't net to zero). - **On submit**: trigger e-invoice XML generation, statutory reporting, or external authority filings (Italy's e-invoice flow is the canonical example). - **Before save**: stamp computed compliance fields (e.g. _Sequential Invoice Number_ for German DATEV-style numbering, see `apps/erpnext_germany/`). Doc events are filtered by the runtime `get_region()` lookup — handlers should early-return if `frappe.get_cached_value("Company", company, "country")` doesn't match the target country. --- ## 3. `regional_overrides` — country-scoped function substitution For arithmetic or presentation logic that genuinely differs by country, register a per-country override in `hooks.py`: ```python regional_overrides = { "Your Country": { "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "your_app.regional.your_country.utils.update_itemised_tax_data", }, } ``` The `@erpnext.allow_regional` decorator on the target function checks the calling company's country and substitutes the body if a registered override exists. Currently override-able tax functions (all in `taxes_and_totals.py`): - `get_regional_round_off_accounts(company, account_list)` — patch the round-off account list. - `update_itemised_tax_data(doc)` — re-shape per-item tax fields for serialisation. - `get_itemised_tax_breakup_header(item_doctype, tax_accounts)` — print-format header. - `get_itemised_tax_breakup_data(doc)` — print-format body. These hooks are intentionally narrow: they let you alter presentation and round-off destinations without touching the calculation engine. If your country needs alternative *arithmetic*, that's a strong signal you should re-express the requirement as data (Tax Templates, Item Tax Templates, paired Add/Deduct rows, inclusive-rate flags) before reaching for a core override. ### Adding a new country 1. Create `apps//regional//` with `__init__.py`, `setup.py`, `utils.py`. 2. Implement `update_regional_tax_settings(country, company)` in `setup.py` — install custom fields, seed **Tax Category** records, create **Tax Rule** records. 3. Implement override functions in `utils.py` matching the signatures of the four `@allow_regional` functions you target. 4. Register in `hooks.py`: ```python regional_overrides = { "Your Country": { "": "", }, } ``` 5. Optionally add `doc_events` keyed on the same country for lifecycle behaviour. --- ## 4. Item Tax Template behaviour you should know The **Item Tax Template** is the most subtle moving part for developers. Three behaviours catch people out: 1. **`not_applicable` ≠ rate of zero.** A row with _Not Applicable_ = "1" emits the sentinel "N/A", which short-circuits the tax row entirely for that item (`_get_tax_rate` returns the sentinel). A row with `tax_rate` = "0" still runs the formula, just with a zero result — this matters for inclusive-tax fraction algebra. 2. **The header template defines *which* accounts; the **Item Tax Template** defines *which rate* per account.** Example: a German invoice with `account_head` = "Umsatzsteuer 19 %" at "19" in the header template and an item bound to **Item Tax Template** "7 %" — the engine uses rate "0" for the 19 % row and rate "7" for the 7 % row on that item. The header rate is overridden, not the account. 3. **Pricing Rule does not touch `item_tax_template`.** That field is stamped before pricing rules run. Pricing Rules touch only price / discount / margin. The taxable amount may shift indirectly when margin pushes `item.rate` up, but the rate map is fixed. --- ## 5. The `dont_recompute_tax` flag Set `dont_recompute_tax` = "1" on a tax row when its `tax_amount` and `base_tax_amount` must be preserved verbatim through subsequent recalculations. Typical setters: - **POS Invoice Merge Log** consolidation — `pos_invoice_merge_log.py`. - **Tax Withholding Category** (TDS / 1099 / VAT WHT) — statutory amounts must not drift. - Generic invoice consolidation in `accounts_controller.py`. What freezes: `tax_amount`, `base_tax_amount`, the per-item breakup JSON. What still recomputes: `tax_amount_after_discount_amount`, totals, `base_*` derivations. Invariant: rows with `dont_recompute_tax` = "1" almost always also have `charge_type` = "Actual" — recomputation is semantically trivial because the amount is the literal value. --- ## 6. Subscription tax handling — building your own re-resolution **Subscription** snapshots its `sales_tax_template` once and copies it onto each generated **Sales Invoice** without re-running Tax Rule resolution. The implication for developers: if your billing flow needs address-aware or rate-aware re-resolution per cycle, you must build it yourself. Two approaches: - A scheduled job that updates `sales_tax_template` on active Subscriptions when upstream rates or rules change. - A custom `before_validate` hook on **Sales Invoice** that calls `get_party_details()` and overrides the snapshotted template per generated invoice. **Subscription Plan** has no tax fields — taxes live on the parent **Subscription** only, so any custom logic should target that DocType. --- ## 7. Multi-currency: do not double-round When extending tax logic for multi-currency documents, follow the existing pattern: - Compute in transaction currency at full precision. - Derive base values via `_set_in_company_currency(doc, fields)` — it does a two-step rounding (`flt(value, txn_precision) × conversion_rate → flt(result, base_precision)`) to prevent intermediate-precision loss. - Never reconstruct `base_grand_total` as `base_net_total + base_taxes` — always derive from `grand_total × conversion_rate`. For per-item base contributions, use error diffusion: track `_running_txn_tax_total` and `_running_base_tax_total`, emit each item's contribution as the delta. This guarantees the per-item base values sum exactly to the row total even on extreme conversion rates (the `apps/erpnext/erpnext/controllers/tests/test_item_wise_tax_details.py` USD→KRW test illustrates this). --- ## Common pitfalls - **Don't monkey-patch `taxes_and_totals.py`.** Use `regional_overrides` for the four exposed functions or wrap the call site via doc events. Direct patches break on upgrade. - **Don't compute taxes on the JS side and stuff them into the doc.** The Python engine re-runs at validate time as the source of truth and will overwrite your client-side numbers. Mirror any change in both `taxes_and_totals.py` and `taxes_and_totals.js` if you must. - **Don't set `dont_recompute_tax` = "1" "just to be safe".** It freezes the breakup; if downstream code expects fresh values (e.g. after a quantity edit), the freeze causes silent staleness. --- ## Where to look in the source | Concern | File | |---|---| | Calculation engine | `apps/erpnext/erpnext/controllers/taxes_and_totals.py` | | Calculation engine (JS mirror) | `apps/erpnext/erpnext/public/js/controllers/taxes_and_totals.js` | | Resolution: party + address | `apps/erpnext/erpnext/accounts/party.py` | | Resolution: Tax Rule scoring | `apps/erpnext/erpnext/accounts/doctype/tax_rule/tax_rule.py` | | Resolution: Item Tax Template | `apps/erpnext/erpnext/stock/get_item_details.py` | | Auto-append helpers | `apps/erpnext/erpnext/controllers/accounts_controller.py` | | `@allow_regional` decorator | `apps/erpnext/erpnext/__init__.py` | | `regional_overrides` registry | `apps/erpnext/erpnext/hooks.py` | | Country setup modules | `apps/erpnext/erpnext/regional//setup.py` | | Setup wizard tax loader | `apps/erpnext/erpnext/setup/setup_wizard/operations/taxes_setup.py` | | Seed data | `apps/erpnext/erpnext/setup/setup_wizard/data/country_wise_tax.json` | --- Full, layouted page for human readers: https://www.alyf.de/blog/erpnext/customizing-sales-tax-calculation