DEV Community

Cover image for Job Costing in Odoo for Canadian Construction Firms: Analytic Plans, Holdbacks and Progress Billing
Mohit
Mohit

Posted on

Job Costing in Odoo for Canadian Construction Firms: Analytic Plans, Holdbacks and Progress Billing

Most construction ERP rollouts I have seen go wrong in the same quiet way. The accounting migration lands fine, the projects get created, invoices go out — and four months later the controller still cannot tell you which of the eleven active jobs is making money, because every cost hit a single analytic account called "Projects".

That is not a module problem. It is a structure decision made in week two of configuration and then inherited for years. The discovery phase is where it should be settled, and for contractors it comes down to four questions the system has to answer on demand.

The four questions a contractor's system has to answer

  1. What has this job cost me so far, split by cost code, and how does that compare to the estimate?
  2. What have I committed but not yet been billed for — the open POs and unapproved subcontractor progress claims?
  3. What can I legally bill this month, and what portion of it must sit as holdback?
  4. When my sub sends a proper invoice, what is my payment clock? Odoo answers the first and third out of the box if you set it up correctly. The second needs work. The fourth is a legal deadline, not a feature, and it is the one that generates penalties.

Two analytic plans, not one

Odoo 19's analytic accounting works on plans that group analytic accounts, and a journal item can carry a distribution across several plans at once. That second part is what makes proper job costing possible, and it is the part teams skip.

Enable it under Accounting ‣ Configuration ‣ Settings ‣ Analytics, then build two plans under Accounting ‣ Configuration ‣ Analytic Plans:

Plan: Jobs                      Default Applicability: Mandatory
  ├── 2026-014  Riverbend Retrofit
  ├── 2026-021  Kanata Warehouse Phase 2
  └── OH-000    Overhead / unassigned

Plan: Cost Codes                Default Applicability: Mandatory
  ├── 01  General conditions
  ├── 03  Concrete
  ├── 15  Mechanical
  ├── 16  Electrical
  └── 99  Rework and warranty
Enter fullscreen mode Exit fullscreen mode

Every cost line then carries both a job and a cost code, and you can slice either way without a custom report. Setting Default Applicability to Mandatory means an entry cannot be confirmed with the field empty — annoying for a week, and the reason your numbers are still trustworthy in year three.

Use the Applicability tab to stop the mandatory rule from firing where it makes no sense. A line for corporate insurance or a bank fee does not belong to a job. Set an applicability line with the financial account prefixes that should be exempt, or the AP clerk will start assigning everything to OH-000 to get past the validation, which is worse than having no rule.

Keep the cost-code list short at the start. In the rollouts we have run, contractors who launched with 12 to 20 codes were still using them a year later; the firm that imported its full 180-code legacy list ended up with 90% of costs sitting in four codes and no useful comparison against estimate.

Automating the distribution you can predict

Some costs always land the same way. Under Accounting ‣ Configuration ‣ Analytic Distribution Models you can pin a distribution to an account prefix, a partner, a product or a product category — so equipment depreciation splits 60/30/10 across divisions automatically, and the clerk never sees the prompt. Models can be sequenced and combined across plans.

This is the difference between a costing structure people maintain and one they resent. If a contractor is picking the same cost code 40 times a week, that is a distribution model waiting to be created, and it is exactly the kind of detail worth nailing down during odoo implementation services rather than six months after go-live.

Progress billing: milestones or percent complete

Odoo supports milestone invoicing natively. Create the service product with Product Type: Service, set Create on Order to Project and Task, and set the Invoicing Policy to Based on Milestones. Milestones carry a Delivered %; marking one reached updates the delivered quantity on the sales order line, and you invoice from there. Reaching a milestone does not create the invoice by itself — someone still clicks Create Invoice, which is the behaviour you want when a payment certifier is in the loop.

That fits fixed-price work with defined phases. It does not fit a CCDC-style monthly progress claim priced by unit quantities installed, where the number changes every month based on site measurement. For those, the pattern that has held up for us is a sales order line per schedule-of-values item, with quantities updated monthly from the field measurement, then invoiced on delivered quantity. It is more clicking. It also matches what the consultant actually signs.

Standard Odoo will not give you a construction-grade schedule of values with retainage columns, previous-billings carry-forward and stored materials, and you should budget for a light custom report if the owner's certifier expects that layout. Pretending the standard invoice PDF will pass is how a project loses a month.

Holdback: 10%, and Odoo has no field for it

Ontario's Construction Act defines holdback as 10 per cent of the value of the services or materials supplied under a contract. Other provinces run similar regimes under their own lien legislation with their own release rules, so confirm the number and the release trigger for each province you work in before configuring anything.

There is no holdback checkbox in Odoo. Two workable approaches:

Invoice the full progress claim and let the 10% sit open in AR. Simple, and it wrecks your aging report — every certified job shows a permanently overdue balance, and collections calls start going out on money you are not entitled to yet.

Invoice the full claim with a negative holdback line routed to a dedicated Holdback Receivable account. The customer's open balance equals what they actually owe this month, and the retained amount accumulates in its own account you can age separately by job. Release is a positive line on the certification invoice.

We use the second. It needs one product ("Holdback retained — 10%") whose income account points at the holdback asset account, and your accountant will need convincing that a product is the right vehicle. The alternative — a manual journal entry per claim — gets skipped the first busy month, and then the balance is wrong.

Either way, model it before the first invoice. Retrofitting holdback across nine months of posted claims is a reconciliation exercise nobody has time for.

The payment clocks are legal deadlines, not reminders

Since prompt payment came into force, the dates on your documents carry consequences. Under the Ontario Act, an owner has 28 days after receiving a proper invoice to pay the contractor (s. 6.4); the contractor then has 7 days to pay subcontractors out of what it received (s. 6.5); and where the owner does not pay, the contractor must give subs notice of non-payment within 14 days. Adjudication exists to resolve the fight quickly, which means your records get read by someone who was not there.

Practically, in Odoo:

  • Store the proper invoice received date as its own field on vendor bills. Bill Date is whatever the sub typed; the clock runs from receipt.
  • Automate the sub-payment reminder off that field, not off the standard due date.
  • Keep the certifier's approval as an attachment on the invoice, not in someone's mailbox. None of this is exotic configuration. It is the sort of thing that gets left out of scope because it looks administrative, and then becomes the reason a payment dispute costs more than the ERP did. Our notes on implementation best practices cover the same idea in a general setting: the fields that carry legal weight get configured first.

What to have working before go-live

  • Two analytic plans, mandatory, with applicability exemptions for non-job accounts
  • A cost-code list you can defend at 20 items or fewer
  • Distribution models for the five or six recurring costs
  • One holdback product and account, with a documented release procedure
  • Progress-claim format shown to one real certifier and confirmed acceptable
  • Proper-invoice-received date captured on vendor bills Six items. Getting them right is roughly two weeks of a configuration consultant's time, and it is the cheapest two weeks in the project — far cheaper than the three most common implementation mistakes, all of which show up as bad numbers rather than broken software. Before you sign off on a construction rollout, sit with your controller, open a job that is already half-built, and see whether the system can tell you what it has cost. If it cannot, the structure is wrong, and no amount of module selection will fix it.

Top comments (0)