DEV Community

Carlogiovanni Falocco
Carlogiovanni Falocco

Posted on

InvoiceLens: Italy turned every invoice into XML. Code checks the VAT, a local Gemma sorts the rest

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

Once a month I download a ZIP from the Agenzia delle Entrate portal. Inside are my invoices for the month as XML files, some wrapped in digitally signed .p7m envelopes, mixed with the delivery receipts of SDI, the state exchange system. Electronic invoicing is mandatory in Italy, so every supplier invoice I get arrives this way, in the FatturaPA format.
All the data is in there, but none of it is readable without a viewer. So every month I opened the files one by one and copied amounts into a spreadsheet for my accountant.

InvoiceLens is a small web app that does this for me, on my own machine:

I drop the whole monthly ZIP (or single .xml, .p7m or PDF files). SDI receipts are skipped, and invoices I already imported are not added twice.

Plain code reads every number from the XML and flags what needs a second look: duplicate invoices, the same number with conflicting amounts, the same amount billed twice a few days apart, VAT that does not match the rate, totals that do not add up, overdue payments. Each flag shows the exact numbers and the rule that raised it.

Gemma, running locally, puts each expense in a category with a one-line reason I can read and override.

Each invoice in plain words: "Accademia Digitale S.r.l. billed you €199.00 plus €40.00 VAT (22%) for Corso online sviluppo AI… Pay €239.00 by 10 Nov 2026 by bank transfer. InvoiceLens found 1 thing to check before you pay or deduct it."

One accountant pack per month: a ZIP with a printable summary in Italian (print it to PDF), an Excel workbook, a CSV that opens directly in Italian Excel, the list of things to check with the rule behind each one, and an .ics file with the due dates.

Indicative tax hints per category, such as phone and internet being 80% deductible with half the VAT. They are labelled as general rules for the ordinary regime, which do not apply to the regime forfettario. My accountant has the last word.

Delete all my data: everything lives in one local SQLite file, and one button wipes it and compacts the file.
Most tools for this start from a photo of a receipt and ask a model to read the numbers. In Italy the numbers already arrive as structured XML, so InvoiceLens never asks the model to read them. Code reads them, and Gemma only does the parts that need language.
Who would pay: Italian freelancers and small studios with a partita IVA, who get this folder of XML every month, and the accountants who receive those folders from many clients. The price I have in mind is €5 a month or €49 a year: it saves the monthly copy-paste and never asks you to upload client data anywhere. My first tester named a higher number (see below).

Demo

InvoiceLens dashboard with the sample invoices
The GIF was recorded with the model switched off, so its categories read rules (the keyword fallback). The screenshots below come from the same app pointed at hosted Gemma 4, so every category carries the model's name and its reason.
Each invoice opens into a plain-language explanation, the category with its reason and an indicative tax hint, and every check with the rule behind it:
An expanded invoice: plain-language explanation, category, tax hint and the VAT check with its rule
The summary page from the accountant pack, built from the sample invoices:
The printable Italian summary from the accountant pack
Try it: invoicelens-demo.onrender.com (public demo on Render, the ten fake sample invoices only, uploads switched off; the free plan sleeps after 15 minutes, so the first visit can take a minute).
The real setup is meant for your own laptop, where invoices never leave the machine:

docker compose up -d
docker compose exec ollama ollama pull gemma3:4b
open http://localhost:8000   # then "Try with sample invoices"
Enter fullscreen mode Exit fullscreen mode

The sample set is ten fake invoices for an invented freelancer, with problems planted in it: the same hosting invoice twice (same supplier, same number), a course invoice that says €40.00 of VAT where 22% of €199.00 is €43.78, and a few payments already past their due date. InvoiceLens flags all of them, and it does so before the model is ever called.

Code

GitHub logo carlogiovannifalocco-byte / invoice-lens

Repository created as requested

InvoiceLens

CI Docker image Deploy to Render

Your Italian e-invoices, sorted by an open model that never leaves your machine.

Every Italian freelancer receives dozens of fatture elettroniche a month as FatturaPA XML (often wrapped in signed .p7m files) plus the odd PDF. InvoiceLens reads them, categorizes each expense with Gemma running locally, flags what needs a second look, and gives you a monthly picture you can hand to your accountant.

InvoiceLens demo

What it does

  • Imports a whole month at once: drop the .zip you download from the Agenzia delle Entrate Fatture e Corrispettivi portal. SDI receipts are skipped, and invoices you already imported are not added twice.
  • Reads FatturaPA XML and signed .p7m (DER or base64) deterministically: supplier, VAT number, lines, VAT summary, payment terms. Multiple invoices in one file are supported.
  • Reads PDF invoices by extracting the text and asking the model to fill a fixed JSON schema (it is told never to…

MIT licensed: FastAPI, SQLite, openpyxl and one HTML page with no build step. The 46 tests include signing a sample with a throwaway certificate to check .p7m unwrapping, opening the generated Excel file, and making sure a supplier named =1+1 stays text in the exports instead of becoming a spreadsheet formula.

How I Built It

Hosted on Render

The public demo runs on Render's free plan from the render.yaml blueprint in the repo. It builds the same Docker image people run locally and starts it in demo mode: only the ten fake invoices, uploads and deletes switched off. Gemma comes from Google AI Studio's OpenAI-compatible endpoint (gemma-4-26b-a4b-it), so the demo does not depend on my laptop. At start-up the invoices appear at once with the keyword rules, then Gemma recategorizes them in a background thread, at most one call every 4 seconds to stay under the free tier's rate limit. If a call fails, that invoice keeps its keyword category and the table says rules, so a visitor can always tell which label came from the model: on the first boot, eight of the ten came from Gemma and two stayed on the rules (both still in the right category). The health check (/healthz) never calls the model, the monthly summary is cached while the numbers do not change, and Render redeploys main only after the GitHub Actions checks pass (autoDeployTrigger: checksPass).
The design rule: the model never reads a number that is already structured.

What the code does (deterministic):

Parses FatturaPA with defusedxml and opens .p7m envelopes (DER or base64) with openssl cms. Signatures are unwrapped, not verified.

Keeps amounts as Decimal. Checks every VAT line (taxable Γ— rate, with a two-cent tolerance), finds duplicates (same supplier VAT number, invoice number and year), the same number with a different total, and the same total from the same supplier within 10 days, checks totals, and computes due and overdue payments.

Unpacks the ZIP, skips SDI metadata and receipts, and caps archive size.

Builds the accountant pack, the plain-language explanations and the tax hints from the parsed fields. None of them calls the model, so they work the same with Gemma offline.

What Gemma does (the fuzzy parts):

Categorizes each invoice into one of 12 categories, as JSON with a confidence and a short reason. "Postazione coworking mensile" is exactly the kind of text a small model reads well.

Extracts PDF invoices: pypdf pulls the text and Gemma fills a fixed schema, with an explicit instruction never to invent missing values.

Writes the monthly summary from numbers the code already computed.
InvoiceLens talks to any OpenAI-compatible endpoint. The default is gemma3:4b on Ollama. If it is down, invoices are still parsed and checked, and categories fall back to keyword rules. Every category shows its source: the model's name, rules or manual.

What I learned running Gemma on a CPU-only laptop

On 18 October I ran the ten sample invoices through gemma3:4b (Ollama 0.40.5) on my CPU-only Windows laptop. These are the real numbers.
Speed. Once the model was loaded, each invoice took 18 to 40 seconds. Loading it took about 135 seconds, and the monthly summary about 220 seconds. Fine for a monthly batch you leave running, not for clicking around and waiting.
Accuracy. Gemma categorized 8 of the 10 invoices; the other two timed out and fell back to keyword rules (the first one while the model was still loading). Six of the eight were right: train ticket as travel, accountant as professional services, monitor and keyboard as hardware, coworking desk as office, AI course as training, electricity as utilities.
The two mistakes taught me the most. The fibre line and the hosting renewal were both labelled utilities. But the reasons were correct: "fibre internet, recurring cost" and "hosting and domain renewal". Gemma understood the invoices. My category list was the problem: it gave the model bare names like telecom and software with no definitions, so "recurring service" sounded like a utility.
The summary. The Italian paragraph mixed in English ("invoice") and addressed me in the plural ("Avete"). Worse, it called the Accademia Digitale invoice late. It is not late; it has a VAT mismatch. I had passed the model nested JSON with every anomaly in one list, and a 4B model mixed up which problem belonged to which supplier.
Here is what commit 8fde19d changed:
| What I saw | What changed |
|---|---|
| 2 of 10 invoices timed out | Default timeout 300 s, one retry on timeout, model warm-up in the background at startup |
| Hosting and fibre as utilities | The prompt now defines every category |
| Summary mixed languages, invented a late payment | The model gets plain-sentence facts, max four sentences, informal Italian with no English words |
The category prompt now looks like this:

CATEGORY_HINTS = {

"software": "licences, SaaS, cloud, web hosting, domain names",

"utilities": "electricity, gas, water",

"telecom": "phone lines, mobile plans, fibre or home internet",

# ... one line for each of the 12 categories
}
Enter fullscreen mode Exit fullscreen mode

The summary input is now one sentence per fact, built by code: VAT does not match the rate: Accademia Digitale (VAT at 22% on 199.00 should be 43.78, invoice says 40.00). The prompt also says not to call an invoice late unless it is listed under "Overdue".
I then ran the same ten invoices against Gemma 4 (gemma-4-26b-a4b-it, hosted on Google AI Studio, the setup behind the public demo), with the new prompts. All ten were categorized by the model and all ten were right, at 6 to 10 seconds each: hosting is now software and fibre is telecom. The October summary listed only the two invoices that are really overdue, put the VAT problem on Accademia Digitale, and was written in plain Italian with no English words. It took 3 minutes 41 seconds, so the demo caches it.
That is a bigger model than the one on my laptop, so it does not prove the 4B model is fixed too. Re-running gemma3:4b on the laptop with the new prompts is next.
The bigger lesson: every mistake landed somewhere a human checks at a glance, a label with its reason next to it or a paragraph of prose. None of them touched a VAT total, because the model never computes one.

Why Does Open Innovation Matter?

  • Privacy. Invoices carry client names, prices, tax IDs and sometimes bank details. With the default setup nothing leaves the laptop. Sending clients' invoices to a third-party API is a much harder conversation, especially for an accountant.
  • Cost. There is no per-invoice fee. A 4B model runs on a laptop people already own, which is what makes a €5 a month price possible with no API bill underneath it.
  • Control. The model is a config value. I can point InvoiceLens at llama.cpp, vLLM or a hosted Gemma, or later fine-tune a model on one user's own categories, without touching the app.
  • Stability. The weights do not change under me. The fixes in 8fde19d were written for this exact model, and they stay valid until I decide to upgrade. ## My First User A freelancer friend who works with several clients and handles his own invoices tried InvoiceLens on 15 sample invoices: invoices he issued, invoices he received for software and online services, and documents with different amounts. The test was whether InvoiceLens could spot a possible duplicate and flag an inconsistency between the amounts of two documents. He compared the results with the original documents before trusting them. What he said (translated from Italian): > "What I care about is not having to check every invoice by hand. If InvoiceLens can flag the documents I need to check right away, it can save me time and help me avoid mistakes." Would he pay? Hypothetically, about €9 a month, for a service that is reliable, easy to use and analyses many invoices at once. That is more than the €5 I had in mind. Before subscribing he would want to check its accuracy on his own documents, which is fair. ### What he asked for, and what I built | He asked for | What InvoiceLens does now | |---|---| | Duplicate and amount checks | Duplicates, VAT and totals were already there. New: two invoices with the same number but different totals, and the same supplier billing the same amount twice within 10 days under a different number | | Many invoices at once | Already there: drop the monthly ZIP from the Agenzia delle Entrate portal; invoices already imported are skipped | | A summary report in PDF or Excel | New: the accountant pack, with an Excel workbook and a printable summary page (print to PDF from the browser) | | More transparency on each flag | New: every flag shows the exact numbers and the rule that raised it, for example "taxable Γ— rate, rounded to the cent, must equal the VAT on the invoice within €0.02" | | Data protection, and deleting documents for good | New: the footer shows the one local file where invoices are stored, and "Delete all my data" wipes it and compacts it so deleted rows do not linger | Most of this is in commit da0d0c6, together with the plain-language view and the tax hints. The two new amount checks came after, in pull request #11. ## Prize Categories
  • Best Use of Gemma: gemma3:4b, running locally on Ollama, categorizes every invoice, extracts PDF invoices and writes the monthly summary. The post reports what it actually did on a CPU-only laptop and what I changed because of it. The public demo uses Gemma 4 (gemma-4-26b-a4b-it) through Google AI Studio.
  • Best Use of Render: the public demo is deployed from the render.yaml blueprint on the free plan, with Gemma recategorizing in the background within the free-tier rate limit, a model-free health check and redeploys gated on green GitHub Actions checks.

Top comments (0)