Contact

Matching invoice lines to inventory items is harder than reading them

Extraction turns a page into lines. Matching turns each line into an inventory item the restaurant already has, with a quantity in the unit the recipes use. On bilz.ai, extraction was the part people worried about and matching was the part that took longer. A line reads “TOM ROMA 5KG CS”, and the question is whether that is the plum tomato in the inventory, how many kilograms arrived, and what that does to the cost of every dish that uses tomatoes.

Why it is harder than reading

Extraction has a ground truth on the page. The printed quantity is the quantity, and a person can check it in a second. Matching has a ground truth only inside the restaurant’s own data, and that data is inconsistent in ways nobody has written down.

Supplier naming is the first problem. Each supplier has its own abbreviations, its own word order, and its own product codes that mean nothing outside its catalogue. The same supplier changes its naming when it changes its ordering system. Across three suppliers the same tomato is “Tomato Roma 5kg”, “TOM ROMA CS 5” and a code with no text at all, and the inventory has it as “Plum tomatoes”.

Pack sizes and units are the second. A line says 2 at 12.50, the 2 is cases, the case is 5 kg, and the recipes count tomatoes in grams. The pack size is sometimes in the product text, sometimes in a separate column, and sometimes nowhere on the invoice because the supplier assumes the buyer knows. Pieces, kilograms, litres, cases, trays, bunches and “each” all appear, and the conversion between them is specific to the product and often to the supplier.

The third is that the inventory itself is not clean. Restaurants build it over years, and it has three entries for the same tomato because three people created them. Matching cannot fix that, but it has to be honest when it meets it.

Generating candidates

We split matching into two steps, because one model call that goes straight from line text to item is slow and impossible to debug. The first step produces a short list of candidates. The second scores them.

Candidates come from three sources. Normalisation strips the supplier’s abbreviations, expands the ones in a learned dictionary, pulls out the pack size and unit, and runs a text search over the inventory on what is left. Embeddings catch what text search misses, where “courgette” and “zucchini” are the same item and no abbreviation table says so. Supplier history is the strongest source of the three: if this supplier’s line with this product code was matched to this item last month, that item is the first candidate, and usually the only one needed.

Supplier history is why the system gets faster over time. In the first weeks most lines are new. After a couple of months of deliveries most lines have been seen before, and the work concentrates on new products and the suppliers who renamed something.

tomato, 5 kg threshold tomato 5 kg tomato paste potato 5 kg
An invoice line produces candidates from normalised text, embeddings and supplier history, and the scorer either confirms one or sends the line to a person.

Scoring and when to ask

The scorer sees the line, the candidates, and what is known about each: the text similarity, whether the pack size is consistent with the item’s unit, whether the price is within the range this item has been bought at before, and whether this supplier has sold this item before. A model does well at this step when it is given those signals explicitly, and badly when it is left to infer them from raw text. Its output is a ranked list with a confidence and a reason in one sentence.

The threshold decides what happens next. Above it, the match is applied and the reason is stored. Below it, the line goes to a person with the candidates ranked, which is a faster job than a search box. We set the threshold from the evaluation set, at the point where confirmed matches are wrong less often than a person’s own matches, and we set it separately for lines with supplier history and lines without, because the two populations look nothing alike.

A unit price far outside the item’s history is a separate signal. It can mean a wrong match, a wrong extraction or a real price increase, and the person needs to see it in all three cases. bilz.ai flags price increases on purpose, so that check is tuned to surface them.

Learning from corrections

Every correction a person makes is a record: this supplier, this line text, this product code, matched to this item, with this pack size. The record goes into supplier history straight away, so the same line next week matches without asking. The abbreviation dictionary grows from the same records.

The corrections are also the evaluation set growing. A line that was matched wrongly and corrected is the most valuable case we can have, because it is a real failure with a known answer. We fold those into the set and rerun it before any change ships.

Two kinds of correction need care. A person who picks a candidate quickly because it is close enough teaches the system to be close enough, so the review screen makes creating a new item as easy as picking an existing one. And a person who corrects the inventory, merging the three tomatoes into one, invalidates history that pointed at the merged entries, so the merge has to rewrite those records or they end up pointing at nothing.

What a wrong match costs

A wrong extraction affects one invoice. A wrong match affects everything downstream of the item.

If a case of plum tomatoes is matched to cherry tomatoes, the plum tomato stock does not go up, the cherry tomato stock goes up by the wrong amount, and the next stock count shows a discrepancy on both. The unit price on the cherry tomato item moves to a price that was never paid for it, and every recipe that uses cherry tomatoes is recosted at that price. Margins on those dishes shift. A report shows a dish becoming less profitable in a week when nothing in the kitchen changed.

That chain is why the confirmation step exists, and why bilz.ai does not change a recipe’s cost basis on an unconfirmed match. It is also why every stock movement keeps a link back to the invoice line and the match decision that produced it. When a chef says the number on the report is wrong, the answer has to trace back to one document and one decision, and that decision has to be reversible from the app.

What to expect in the first weeks

The first weeks are mostly review. That has to be said in advance, because a system that asks about every line looks broken to someone who was promised automation. The measurement that matters is the share of lines matched without a person, tracked per supplier, and the error rate on the ones that were. The first number climbs as supplier history fills in. The second is the one to watch when it moves.

If you have lines that someone is matching to a catalogue by hand, whether invoices, orders or product feeds, the AI agents and automation page describes the work, and you can write to us with an example of the data.

Technology partner

bilz.ai

An AI back office for European restaurants that tracks food cost, supplier invoices, recipes, stock and profit. We worked with it as technology partner.

About bilz.ai

More writing

  1. Keeping a fleet of wearables synced through phones that want to sleep

    BLE reconnection, iOS background modes and Android Doze, device buffering, idempotent uploads, clock drift, and fleet monitoring that catches a quiet device.

  2. Building an evaluation set from past cases, and keeping it alive

    How we build and keep an evaluation set for an LLM feature: choosing cases, labelling with the people who do the job, size, and growing it from production.

  3. Classifying messages at line rate

    Putting a classifier inline in a messaging platform: the millisecond budget, small models on your own traffic, the uncertain tail, fail-open versus fail-closed.