Skip to Content

Odoo Accounting

Odoo Analytic Accounting: Plans, Distribution and Reporting

Analytic accounting is the answer to every question your chart of accounts cannot answer: which project, which department, which vehicle. It is a second dimension on the ledger, and it is the right place for the detail people otherwise try to encode into account codes.

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

Part of our guide to Odoo Accounting

The model in one pass

Analytic accounting adds a reporting dimension that sits beside the general ledger rather than

inside it. Three pieces:

  • account.analytic.plan is a dimension. Department, Project, Cost Centre. Plans can be nested

under a parent plan.

  • account.analytic.account is a value within a plan. Marketing, Project Alpha, Van 3.
  • Analytic distribution is the assignment on a journal item, stored as a mapping of analytic

account to percentage.

The general ledger still answers "how much did we spend on fuel". Analytic answers "how much of that

fuel was Project Alpha", without adding a single account to the chart.

That last point is the reason the feature exists. The alternative people reach for first is encoding

the dimension into account codes, which multiplies the chart, freezes the structure, and still only

supports one dimension.

Distribution is a percentage split

An analytic distribution on a line is not a single account. It is a set of accounts with

percentages, which means one cost can be split across several destinations:

  • 60 percent to Project Alpha, 40 percent to Project Beta, both in the Project plan
  • and simultaneously 100 percent to Marketing in the Department plan

Percentages within one plan should total 100. Nothing stops you posting a distribution that totals

less, and the shortfall becomes cost that exists in the ledger but appears nowhere in the analytic

report. If your analytic totals do not tie back to the general ledger, incomplete distributions are

the first thing to check.

The root-plan storage rule

This is the part that is worth knowing before you write a single line of reporting code, because it

is invisible from the interface and it silently returns empty results.

Distribution is not stored in a generic table keyed by plan. Each plan is backed by a column on

the analytic line, and the column belongs to the plan's root, not to the plan the account sits

in.

In the Odoo source the column name is derived like this:

def _strict_column_name(self):
    return 'account_id' if self == project_plan else f"x_plan{self.id}_id"

def _column_name(self):
    return self.root_id._strict_column_name()

Read that second method carefully. _column_name resolves root_id first and asks the root for the

name. So:

  • The default project plan is special and uses account_id
  • Every other root plan uses x_plan<root id>_id
  • A child plan has no column of its own

Concretely: if you have a root plan with id 2 and a child plan under it with id 3, an analytic

account belonging to the child plan stores its distribution in x_plan2_id. There is no

x_plan3_id column at all.

The failure this produces is not an error. A query built from the child plan's id targets a column

that does not exist, or worse, targets nothing and quietly returns no rows. The report renders, it

is simply empty, and the natural conclusion is that nobody has entered any data.

Always resolve the root plan before building a column name.

Plans are columns, so deleting one destroys data

The same fact has a second consequence. Because a plan is backed by a real column, removing the plan

removes the column and everything stored in it.

There is no active field on account.analytic.plan, so there is no archive-instead-of-delete

option the way there is on most Odoo models. The choice is keep it or lose the data.

In practice: retire a plan by ceasing to use it. Do not delete it to tidy the configuration screen.

Writing distribution programmatically

One caution if you are bulk-updating analytic distribution rather than setting it by hand.

Writing analytic_distribution on account.move.line through the ORM triggers the surrounding

recomputation, and on posted invoices that can reach lines you did not intend to touch, including

early payment discount lines. For a one-off correction that is fine. For a bulk re-plan across

thousands of lines it is not, and Odoo core itself uses raw SQL for exactly this reason.

If you are re-planning historical data, treat it as a migration with a backup and a verification

query, not as a mass edit from a list view.

Where this connects

Analytic accounting is the dimension the chart of accounts should not be carrying. If your account

codes have department or project encoded into them, this is the feature that lets you flatten the

chart back down. It works best once the chart itself is settled, because moving both at once makes

the reconciliation between them very hard to check.

FAQ

Questions, answered.

What is the difference between an analytic plan and an analytic account?

A plan is the dimension. An account is a value within it.

  • Plan: Department, Project, Vehicle, Cost Centre
  • Account: Sales, Marketing, Operations, within the Department plan

Plans can be nested, so a plan can have a parent. That nesting is what makes the storage rule below behave the way it does, and it is the part most people never look at.

Can one journal item carry more than one analytic account?

Yes, in two different senses, and it is worth separating them.

  • Across plans: a line can carry one account from each plan at once, so a cost can be both Marketing and Project Alpha
  • Within one plan: distribution is percentage based, so a cost can be split 60 percent to one account and 40 percent to another

The percentages within a single plan should total 100. Odoo will let you post a distribution that does not, which means partially unallocated cost that only shows up when someone reconciles the analytic report against the general ledger.

Why does my analytic report query return nothing for a child plan?

Because analytic distribution is not stored under the plan you are looking at. It is stored under that plan's root.

An account belonging to a child plan writes into the root plan's column. If the root plan has id 2 and you query the child plan's id 3, you are querying a column that does not exist.

Expert tip. Resolve the root plan first, then build the column name from that. Querying by the plan the user picked in the interface is the single most common reason a custom analytic report comes back empty.

Can I delete an analytic plan?

You can, and you should be extremely careful, because a plan is backed by a real database column.

Deleting a plan drops that column and the distribution data stored in it. There is no active flag on account.analytic.plan to fall back on, so there is no soft-delete equivalent to archiving.

If a plan is no longer needed, the safe move is to stop using it and leave it in place. Removing it is a data-destroying operation, not a tidy-up.

Keep reading

More on Odoo Accounting.

The full guide, plus the other articles in this cluster.

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