Skip to Content

Odoo Accounting

Odoo Fiscal Positions: Tax and Account Mapping

A fiscal position is the rule that says this customer is taxed differently. It swaps taxes and accounts at the moment a document is created, which is exactly why a wrong one is hard to spot: nothing errors, the invoice simply carries the wrong tax.

Written by Hassam SaleemCA Affiliate (ICAP), Odoo Accounting Lead

Part of our guide to Odoo Accounting

The two maps

A fiscal position holds two independent mappings, and it helps to keep them separate in your head:

  • Tax mapping. The product says 20% standard rate. The customer is an export customer. The

fiscal position swaps that tax for the zero-rated export tax.

  • Account mapping. The product category posts revenue to one account. For this customer, it

should post to a different one, perhaps to separate domestic from export revenue in the ledger.

Most setups need the first. Groups with segmented reporting need both.

The substitution happens when the document line is created. That timing is the thing to remember: a

fiscal position is not a report-time filter and not a validation rule, it is a write-time

replacement.

Automatic application

A fiscal position can be set by hand on a customer or applied automatically. Automatic application

is driven by a small set of fields on the position:

  • auto_apply switches automatic detection on
  • country_id or country_group_id narrows it to a country or a group of countries
  • state_ids narrows further by state or region
  • vat_required requires the customer to have a VAT number recorded
  • sequence decides evaluation order, and the first match wins

That last one matters more than it looks. If two positions could both match a customer, the one

with the lower sequence takes it. Overlapping positions are a common cause of "the wrong tax

appeared on one invoice out of ten".

What changed in Odoo 19

If you are writing data files, import templates or integration code, this is the part that will

break.

Odoo 17 and 18 model tax mapping as a separate model. tax_ids is a one2many to

account.fiscal.position.tax, and each row names a source and a destination:

tax_ids = fields.One2many('account.fiscal.position.tax', 'position_id', string='Tax Mapping')

class AccountFiscalPositionTax(models.Model):
    _name = 'account.fiscal.position.tax'
    tax_src_id  = fields.Many2one('account.tax', required=True)
    tax_dest_id = fields.Many2one('account.tax')

So the familiar idiom for creating a mapping is a list of nested commands naming tax_src_id and

tax_dest_id.

Odoo 19 removed account.fiscal.position.tax entirely. tax_ids on the fiscal position is now

a plain many2many to account.tax, and the relationship between a source tax and its replacement is

carried on the tax itself through original_tax_ids.

The practical consequences:

1. Any v17 or v18 data file or script that writes tax_src_id fails on 19 with an invalid field

error naming account.tax. It is not a silent failure, which is the good news.

2. Account mapping is untouched. account.fiscal.position.account still exists in 17, 18 and 19.

Only the tax half of the feature was rewritten.

3. A migrated position with no tax mapping strips tax. If a migration drops the mapping rows

rather than converting them, the position still exists and still applies, and it now applies

nothing. See the FAQ above: this posts clean invoices with no tax on them.

If you are upgrading from 17 or 18 to 19 and you use fiscal positions at all, this is worth an

explicit test rather than a spot check.

Testing a fiscal position properly

The failure mode here is silence, so the test has to be deliberate:

  • Create a customer that matches the position's conditions, including the VAT number if

vat_required is set.

  • Create an invoice with a product that carries the source tax.
  • Confirm the tax actually swapped, and that the amount is what you expect rather than blank.
  • If you use account mapping, check the journal items land in the mapped account, not just that the

invoice total looks right.

Doing this on the product you actually sell, rather than a test product with a hand-set tax, is what

catches the empty-mapping case.

Where this connects

Fiscal positions sit directly on top of the chart of accounts and the tax configuration. If the

account structure underneath is still moving, fiscal position mapping will keep needing rework, so

it is usually the step after the chart is settled rather than alongside it.

FAQ

Questions, answered.

What does an Odoo fiscal position actually do?

It performs a substitution at document creation time. Two independent maps:

  • Tax mapping replaces the tax set on the product with a different tax, or with no tax at all
  • Account mapping replaces the income or expense account the line would otherwise post to

It does not recalculate anything after the fact. The substitution happens when the invoice or order line is built, so changing a fiscal position later does not retrospectively fix documents already created.

Why is my fiscal position not applying automatically?

Automatic application depends on auto_apply plus the matching conditions on the position, and all of the set conditions must be satisfied.

  • auto_apply has to be on. Without it the position exists but is manual only
  • The country or country group must match the customer's address
  • If vat_required is set, the customer needs a VAT number on file
  • Positions are evaluated in sequence order and the first match wins

Expert tip. Check the customer record before the fiscal position. Most "the rule is broken" reports are a partner with a missing country or a missing VAT number, not a misconfigured position.

Does a fiscal position with no tax mapping remove tax?

Yes, and this is the most expensive mistake in the whole feature. An empty tax mapping is not read as "leave the tax alone". It is read as a valid instruction, and lines fall through with no tax applied.

Invoices post successfully, look normal, and are under-taxed. Always test a fiscal position on a real product before it goes anywhere near a customer.

Did fiscal positions change in Odoo 19?

The tax mapping did. Odoo 17 and 18 store tax mapping as rows on a dedicated account.fiscal.position.tax model, each row naming a source tax and a destination tax. Odoo 19 removed that model entirely and made tax_ids a direct many2many to account.tax, with the destination tax carrying an original_tax_ids reference back.

Account mapping did not change: account.fiscal.position.account is still there in all three versions. So if you have a script or a data file that writes tax mapping, it needs rewriting for 19. Account mapping code carries over unchanged.

With CODEerts

Want a hand with this?

We are certified Odoo partners and we do this work for a living.

See how we can helpBook a call