From Invoice Data to Cost Control: Budget Planning in Project Development

In a previous article I wrote about turning invoice documents into structured data: OCR versus LLM extraction, arithmetic validation, and the review queue you will always need. That pipeline solves a real problem, but it stops one step short of the one that actually keeps project managers awake. Nobody loses sleep over whether the invoice number was read correctly. They lose sleep over whether the project is still inside its budget — and whether they will find out in time to do anything about it.

Extraction is the enabling technology. Cost control is the application. This article is about the second half: what you build on top of clean invoice data so that a project's financial position is a query rather than a monthly reconstruction exercise in Excel.

Three-part diagram of project cost controlling: a bar chart showing budget, actuals, open commitment, accrual, and forecast for five cost positions, with MEP / building services flagged because its forecast exceeds the budget; a timeline showing that a commitment is known at contract award months before the first invoice reaches accounting; and an allocation funnel routing incoming invoices to cost positions by reference, by supplier and context, or by content with human confirmation

The gap between "extracted" and "known"

Suppose the pipeline is running perfectly. Every incoming invoice yields supplier, number, date, net, VAT, gross, and line items, validated and posted. What do you know about your project?

Almost nothing. You know what has been billed, by whom, and when it arrived. You do not know:

  • which project, phase, or work package the cost belongs to,
  • whether it was expected — ordered, approved, and reserved against a budget line,
  • what has been committed but not yet invoiced,
  • what has been delivered but not yet billed,
  • and, most importantly, what the project will cost by the time it is finished.

Bookkeeping answers "what did we pay." Controlling answers "where do we stand and where will we land." The distance between those two questions is where cost overruns hide. In project development it is a wide gap: work runs for months, suppliers invoice on their own rhythm, and the accounting view lags reality by weeks.

Four numbers, not one

Every cost position in a project carries four values, and confusing them is the single most common source of false comfort:

  • Budget — what was planned for this position, at the granularity you actually steer at.
  • Commitment — what is contractually promised: signed contracts, purchase orders, awarded lots. The money is gone the moment you sign, not the moment the invoice arrives.
  • Actuals — what has been invoiced and booked. This is what your extraction pipeline delivers.
  • Forecast (cost at completion) — what the position will ultimately cost, including the work still ahead.

"Budget minus actuals equals what's left" is the lie at the center of most project cost reports. A position with a €200k budget and €40k invoiced looks comfortable at 20% consumed — right up until you notice €190k of it is already under contract. The available headroom is €10k, and it has been €10k since the day the contract was signed.

This is why commitment tracking, not extraction accuracy, is usually the highest-value thing to build next. It moves the moment of truth from invoice receipt to contract award, which is typically several months earlier — the difference between reacting and steering.

The allocation problem

To compare an invoice against a budget you must first decide which budget it belongs to. This is the hard, unglamorous core of the whole system, and it is where the technology from the extraction stage gets a genuine second use.

Allocation happens on three levels, and each has a different reliability profile:

  1. By reference. The invoice cites a purchase order, contract, or project number. This is deterministic, auditable, and by far the best case. Extraction gives you the reference string; a lookup against your own master data resolves it. Nothing here needs AI — and nothing here should use it.
  2. By supplier and context. No reference on the document, but the supplier only works on one active project, or the delivery address matches a site. Rule-based inference with a confidence level attached.
  3. By content. A generic supplier, several active projects, and only the line-item descriptions to go on. This is where an LLM classifier is genuinely useful: given the line items and a list of open cost positions with their descriptions, propose an allocation with a rationale.

The rule I would apply without exception: level 1 and 2 post automatically; level 3 proposes and a human confirms. A misallocated cost is worse than an unallocated one. An unallocated cost sits visibly in a clearing position and annoys somebody until it is resolved. A misallocated cost silently makes one project look healthy and another look sick, and it will be discovered — if at all — during a painful year-end review.

The same discipline from the extraction article applies here: the model proposes, deterministic code validates, a human decides on anything uncertain. Ask the model to classify, never to compute.

Split invoices and the granularity trap

A single invoice frequently belongs to several cost positions: one line for the site, one for the office, three work packages on one contractor's monthly statement. The pipeline must therefore treat allocation as a property of the line item, not the document, and it must enforce that the splits sum exactly to the invoice total. That is another arithmetic invariant to add to the ones already validating the extraction itself.

The related trap is granularity. There is a strong temptation to budget at high resolution — hundreds of positions, each precisely planned — because it feels rigorous. In practice, budget granularity should match the level at which you can actually make decisions. If you would never move money between two positions, they are one position. Over-granular budgets do not improve control; they generate allocation ambiguity, endless reclassification, and reports nobody reads.

Pitfalls that show up in real projects

These are the ones that reliably break a naive implementation:

  1. Period cutoff. An invoice for June arrives in August. If costs land in the period they were received rather than the period they were incurred, every monthly report is wrong in both directions. Track service date separately from invoice date and booking date — your extraction schema needs all three.
  2. The accrual gap. Work delivered but not yet invoiced is a real cost that is invisible in the actuals. Without an accrual estimate, project costs look artificially low every single month, and the gap closes in a nasty jump at year end.
  3. Partial invoices and progress billing. Construction and development contracts bill in stages against a cumulative sum, often with previous payments deducted on the same document. Naive summation double-counts spectacularly. The pipeline needs to understand cumulative versus incremental billing.
  4. Retentions. A percentage withheld as security is deducted from the payment but is still a committed cost. It must reduce cash, not the commitment, or your forecast quietly understates the project.
  5. Credit notes and reversals. Signs flip. A credit note allocated to the wrong position is a misallocation that improves a number, which means nobody reports it.
  6. Change orders. The budget is not static. Every scope change alters both the commitment and the target, and a controlling system that only tracks the original budget loses touch with reality after the first change order. Version the budget and keep the original as a separate baseline.
  7. Duplicate risk, again. Deduplication mattered in the extraction pipeline to avoid paying twice. Here it also matters to avoid reporting twice — a duplicate that gets caught before payment but after posting still corrupts the cost position.
  8. Forecast as a data field, not a formula. Cost at completion cannot be derived from invoices alone. Someone with knowledge of the remaining work has to estimate it. The system's job is to make that estimate cheap to maintain, timestamped, and comparable over time — not to invent it.

What the model looks like

The structure is small enough to state directly. Costs attach to positions; positions carry a budget and roll up into a project.

from datetime import date
from decimal import Decimal
from pydantic import BaseModel

class CostEntry(BaseModel):
    position_id: str
    amount_net: Decimal
    kind: str                  # "commitment" | "actual" | "accrual"
    service_date: date         # when incurred, not when received
    source_document: str       # invoice or contract reference

class Position(BaseModel):
    position_id: str
    name: str
    budget: Decimal            # current, after approved change orders
    baseline_budget: Decimal   # original, never modified
    forecast_at_completion: Decimal | None

The report every project manager actually wants is then a rollup with one derived number that matters more than the rest:

def status(pos: Position, entries: list[CostEntry]) -> dict:
    def total(kind: str) -> Decimal:
        return sum((e.amount_net for e in entries if e.kind == kind), Decimal(0))

    actual, commitment, accrual = total("actual"), total("commitment"), total("accrual")
    # commitments are consumed by the invoices that bill against them
    open_commitment = max(commitment - actual, Decimal(0))
    forecast = pos.forecast_at_completion or (actual + accrual + open_commitment)

    return {
        "budget": pos.budget,
        "actual": actual,
        "open_commitment": open_commitment,
        "forecast": forecast,
        "variance": pos.budget - forecast,   # negative = overrun, this is the number
        "uncommitted_headroom": pos.budget - actual - open_commitment,
    }

variance is the whole point. Not "how much have we spent," but "how much will we be over or under when this is done." A report that surfaces this per position, sorted by the worst variance, is more useful than any dashboard with more charts on it.

The natural companion is an early-warning rule that runs whenever a document is posted: flag a position when forecast exceeds budget, when a new commitment consumes the remaining headroom, or when an incoming invoice has no matching commitment at all — the last one usually means someone ordered something without a purchase order, which is worth knowing immediately rather than at month end.

Where AI helps, and where it hurts

Having built on both sides of this line, the split is fairly clean.

Genuinely useful:

  • Allocation proposals for invoices without a usable reference, based on line-item content and the list of open positions.
  • Contract and order extraction. The same technology that reads invoices reads purchase orders and contracts — which is how commitments get into the system without manual entry. This is the highest-leverage extension of the extraction pipeline, and it is largely the same code.
  • Anomaly surfacing. "This supplier has never billed above €5k on this project" or "unit price is 40% above the contracted rate" are pattern questions, and pattern-matching over your own history is what this technology is good at.
  • Explaining variance in prose. Turning a rollup into a readable monthly commentary is a legitimate use — as long as it is summarizing numbers that deterministic code produced.

Actively harmful:

  • Letting a model compute or forecast. Every number in a cost report must come from arithmetic over stored entries. If a figure in a board report cannot be traced to a source document and a rule, it does not belong there.
  • Automatic allocation without confirmation in ambiguous cases, for the reasons above.
  • Model-reported confidence as a routing signal. As in extraction, self-reported certainty is poorly calibrated. Route on structural facts — reference present or absent, amount thresholds, prior history with this supplier — not on how sure the model claims to be.

Conclusion

Invoice extraction is worth building, but on its own it produces a faster version of bookkeeping. The step that changes how a project is run is what comes after: allocating every cost to a position, tracking commitments from the moment of signature rather than the moment of invoicing, closing the accrual gap, and maintaining a forecast that says where the project will land rather than where it has been.

The technology carries over almost entirely. The same extraction stack that reads invoices reads contracts and purchase orders, the same schema-plus-validation discipline applies, and the same rule holds throughout: the model proposes, arithmetic decides, a human confirms anything ambiguous. What changes is the question you can answer. Instead of "what did this cost," you get "what will this cost" — early enough to still do something about it.