DEV Community

Cover image for Pricor: local AI reads the supplier spreadsheet, an exact engine does the math
Teo Furlan
Teo Furlan

Posted on

Pricor: local AI reads the supplier spreadsheet, an exact engine does the math

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle.

What I Built

A supplier sends you a spreadsheet. The price column says 12.100,00. The header says c/IVA (VAT included). Another column says 10+5, and another says 100. What does one unit cost you?

It costs 85.50: two chained discounts (not 15%), VAT removed, the box split into 100 units. Small Argentine shops do this by hand every time a supplier list arrives, for every supplier, in every supplier's own format. And next week, one of the columns will have moved.

Pricor (price + order) is a local app that turns those files into consistent, importable product records: supplier code, description, net unit cost and currency, with row-level traceability. It reads Excel or CSV, proposes what each column means, asks about whatever the file does not say, and shows every number from the original cell to the final cost before you approve it. Rejected rows stay visible and exportable; nothing is dropped silently.

Pricor prepares the file; it does not replace the shop's management system. Those systems already import price updates, but only in their own fixed formats.

Who would pay? The side hustle I have in mind is setting up and supporting a shop's recurring supplier imports, built on this tool. That is a hypothesis, not validated demand: I have not had a paying customer or measured time saved yet.

Demo

Pricor demo: a supplier row becomes 85.50 per unit, through columns read by local models, questions the user answers, and an exact receipt

The clip above (21 seconds, silent as a GIF) was built from screens recorded from the running app with Laya and Gemma 4 E4B running locally, on a small fictional list ("Ferretería Norte (demo)") written for the recording.

A longer browser rehearsal on two lists from a real public supplier price page covered the full loop:

  1. First import. Gemma's suggestions arrive with no answer preselected. I confirmed the columns and assumptions, inspected the calculation trace, approved, and downloaded the CSV/XLSX exports.
  2. Next week's list. The same supplier's saved plan was reused. Pricor noticed that the price column had moved and followed the saved mapping instead of reading the wrong column.
  3. Price report. Comparing the two approved lists: 514 increases, 118 decreases, 420 unchanged, 8 new products, 5 discontinued, and one +200% anomaly, across 1,057 and 1,060 accepted products.

To be clear about that run: the second list was simulated by a tool that perturbs a real list and records every change in a manifest. The exports were checked against every changed price and every added or removed code in that manifest, not just the totals. It demonstrates the behavior; it is not evidence of a real supplier's price update.

The arithmetic behind the example at the top, as the receipt in the demo shows it:

12,100.00   per box, 21% VAT included
 × 0.90 × 0.95   chained discounts of 10% and 5%
 ÷ 1.21          remove VAT
 ÷ 100           units per box
=    85.50 net per unit
Enter fullscreen mode Exit fullscreen mode

No model calculates that result.

Code

Repository: github.com/teofurlan/pricor

The application is Apache-2.0: backend, frontend, tests, the corpus download tooling (supplier files are linked from their public pages, not redistributed), the Laya fine-tuning tooling and the benchmark. The fine-tuned Laya weights are not published yet; the training script reproduces them.

How I Built It

The whole design is one rule: models interpret, the engine executes, the user decides.

Spreadsheet → deterministic profile → Laya → Gemma → user confirmation
                                                        ↓
                                     validated operation plan
                                                        ↓
                                      Decimal engine → preview → export
Enter fullscreen mode Exit fullscreen mode
  • A deterministic profiler finds the header row, row types, apparent column types and number formats (12.100,00 is not 12.1).
  • A fine-tuned multilingual Laya model classifies each column into a closed set of roles. Columns it is unsure about go to Gemma 4 E4B, running locally through Ollama. Whatever is still uncertain goes to the user.
  • Questions are generated deterministically from what the plan is missing: does the price include VAT? Is it per pack or per unit? Which currency? Gemma may suggest an answer and show its (uncalibrated) confidence, but nothing is accepted on the user's behalf.
  • The executor only understands a closed catalog of operations: parse numbers, apply chained discounts, remove VAT, split packs into units, map values, normalize text and, optionally, convert currency. It never runs model-generated code.
  • FastAPI serves a React frontend. SQLite stores approved plan versions and price history. Exports come with a separate rejected-rows report.

Plans are saved per supplier. Before reusing one, a fingerprint checks each column's position, normalized header and apparent type. If a column the plan needs is missing, the affected rule is blocked instead of quietly producing a cost. Currency conversion keeps the original value and currency, and snapshots the exchange rate used for that run.

Memory is explicit, local and cited

Pricor can remember things like "this supplier's prices always include VAT". Ollama produces bge-m3 embeddings, SQLite stores them, and Python ranks matches by cosine similarity.

A memory is created only when the user writes one, or accepts a Gemma suggestion and clicks Remember this. Never automatically. The most relevant memories are passed to Gemma as numbered, untrusted data; Gemma must cite any memory a suggestion relies on, and the UI shows the citation. A reply citing a memory that was not supplied is discarded entirely. If memory is unavailable, the import continues with a notice. A Memory panel lets the user view, edit and delete everything.

In one manual check (not a benchmark), with one note about one supplier and one about another, Gemma's VAT and per-pack suggestions for the first supplier cited the right note and not the other.

The benchmark did not justify automating everything

The benchmark in the repo has only 29 held-out columns from two distributors, so read it as a sanity check, not a leaderboard:

Classifier Correct Accuracy
Keyword/type heuristics 24/29 82.8%
Laya zero-shot 18/29 62.1%
Laya fine-tuned 22/29 75.9%
Gemma prompt-only 25/29 86.2%

In the whole-sheet cascade, 11 columns were resolved automatically (10 correctly) and 18 of 29 still went to the user. That is the honest result: on this data, the app should ask. Laya took about 35 ms per column on GPU; Gemma averaged about 11.4 s per sheet (RTX 4060 Laptop GPU). Those are different units, not a speedup claim.

The caveats are documented next to the numbers: thresholds were chosen on partly synthetic validation data, the test set lacks some roles, the heuristics were written after their held-out headers had been seen, and two labels were corrected after the first run. A heuristics-plus-Laya agreement stage I tried afterwards showed no advantage, so the shipped cascade does not use it.

Bugs worth telling

Percentages. Excel stores a cell displayed as 21% as 0.21. Lose the number format and 21% VAT becomes 0.21% VAT, which still produces a plausible-looking cost. The reader now keeps that meaning at the spreadsheet boundary, without touching the engine's percentage-point contract.

A second discount column. While recording the demo, I found that when a list has two separate discount columns, the plan builder uses only the first one. Chained discounts in one cell (10+5) are handled correctly, which is what the demo uses, but a second column is currently ignored without a question, which breaks the app's own "nothing silent" rule. Fixing that is next on the list.

The backend suite passes 1,219 tests (three live-model tests deselected) and the frontend suite 194. None of that proves compatibility with every Excel file a supplier can produce.

Why Does Open Innovation Matter?

The open models are in the import path, not bolted on as a chatbot. Laya and Gemma read the supplier's data, and their limits directly decide when the app has to ask the user.

  • Privacy. A shop's supplier lists and costs never leave its own computer to reach a hosted model provider.
  • Cost. A shop imports the same suppliers every week. With local inference, reusing a plan has no per-call fee. Electricity, hardware and support are not free, but there is no meter running.
  • Offline. Imports work without internet. Only the optional online exchange-rate refresh needs a connection; a cached or manually entered rate works offline.
  • Fine-tuning and measurement. Open weights let me fine-tune Laya for this narrow task and publish exactly how I measured it. It improved over its zero-shot baseline but did not beat Gemma or the heuristics on this small set, and I can say so because everything is inspectable and replaceable.
  • Licensing. Gemma 4 is Apache-2.0, so it can ship inside something a shop pays for.

The next step is not more features: it is a supervised trial with a retailer, using a list they already handle. Compare Pricor's output against their current import, watch where the confirmation steps confuse them, and ask what recurring support would be worth paying for. Until then, the business case stays provisional.

Prize Categories

  • Best Use of Gemma: Gemma 4 E4B runs locally through Ollama. It classifies the columns Laya is unsure about, suggests answers to the plan's questions, and uses explicitly saved memories as cited context. It is not fine-tuned, and it never does price arithmetic.

I am also entering for the overall prize. I do not claim any other partner integration.

Top comments (0)