[KongchangAI]
· 3 min read· 1,699 words

Local Receipt Recognition in Panama with Gemma for Automated Tax Tables

Local Receipt Recognition in Panama with Gemma for Automated Tax Tables

Open-source Cuadre uses a local Gemma model to transcribe Panamanian receipts while code handles calculations and validation, enabling privacy-friendly ITBMS deduction automation.

Cuadre is an open-source project that automates tax organization of paper receipts for Panamanian freelancers and small businesses. Users drop receipt photos into a folder and run one command; a local Gemma model transcribes each receipt's merchant, RUC, date, subtotal, and ITBMS tax, then code handles calculation, validation, and summarization. Outputs include an Excel-ready CSV, a monthly categorized summary report, and a list of receipts needing human review. The core principle is "the model transcribes, the code computes": JSON Schema constrains model output, Decimal arithmetic avoids rounding errors, and RUC format checks, duplicate detection, and date validity checks catch model mistakes. The entire pipeline runs locally—data never leaves the machine and costs are just electricity—making it especially suited for accountants processing about 3000 receipts a month. The project has only been validated on six fictitious receipts so far; real-world scenarios and willingness to pay remain to be tested.

In Panama, freelancers and small businesses get a paper receipt almost every time they buy something. It shows the merchant's tax ID (RUC) and a line for ITBMS—Panama's sales tax, 7% on most goods, 10% on hotels, 15% on tobacco. The ITBMS paid on business purchases can usually be credited against the monthly tax owed (whether a given purchase qualifies is up to the accountant). But to claim it, someone has to sit down at the end of the month and type every receipt into a spreadsheet. A shoebox of thermal paper becomes rows of merchant, RUC, date, subtotal, tax, and total.

The person doing this is usually the freelancer themselves at 11 p.m., or their accountant—who receives blurry WhatsApp photos from 30 different clients.

An open-source project called Cuadre tries to automate this with a locally running Gemma model. The approach is straightforward: drop receipt photos into a folder, run one command, and get three things—a recibos.csv that opens directly in Excel (one row per receipt), a resumen.md summarizing by month and category (including total ITBMS paid), and a list of receipts that need human review, each with a reason.

Photos are read by Gemma on the author's own laptop, and data never leaves the machine. The project targets three groups: accountants bookkeeping for small-business clients (who transcribe receipts by hand every month and can't hand client data to an uncontrolled cloud service), freelancers who file ITBMS monthly and want to recover tax paid on purchases, and small shops that pile receipts in a box until the accountant asks for them.

Cuadre project source screenshot

Demo: traps in six fictitious receipts

To prove the system handles real-world messiness, the author built a demo with six fictitious Panamanian receipts, each with a pitfall: a supermarket receipt with tax-exempt food items, a Fonda lunch where tax needs rounding, a stationery store receipt, a hotel receipt taxed at 10%, the same stationery receipt photographed twice, and a handwritten corner-store receipt with no RUC and no tax line.

In the actual output, the system computed ITBMS paid as B/. 10.98, matching the manual calculation exactly. The supermarket receipt's tax was only 3.5% of the subtotal; instead of treating that as an error, the system flagged it as "may contain tax-exempt items"—which matches how Panama actually works. On the hotel receipt, Gemma truncated the end of the RUC; the code detected that the result didn't match Panama's RUC format and automatically marked the receipt as "needs review." The duplicate photo was detected and not double-counted. The handwritten corner-store receipt was correctly flagged as an anomaly for lacking a tax line and RUC.

The final CSV handed to the accountant includes fields like filename, date, merchant, RUC, check digit, invoice number, category, subtotal, tax, total, status, and warnings. Rows with status "ok" can be used directly; rows marked "revisar" (needs review) come with a specific reason, such as "RUC «155612233-2» does not match Panama's RUC format."

Design principle: the model transcribes, the code computes

The entire system depends only on the Python standard library at runtime, ships with 42 tests, and caches by photo hash—when you run it again next week with new receipts, only the new photos get read.

There's just one core principle: the model transcribes, the code computes. Small vision models are great at reading printed text but unreliable at arithmetic, and a tax report can't contain invented numbers. So Gemma does exactly one thing: copy what's printed on the paper. Everything else is handled by plain code.

The division of labor is: Gemma 4 E2B reads photos locally via Ollama—the only step that needs a vision model; the output structure is constrained by a JSON Schema in Ollama's format field, so Gemma can't return extra fields or invent categories; amount handling is delegated to the monto_de_linea() function, where Gemma copies the whole line and the code separates the tax rate (7%) from the amount (0.83); dates are handled by fecha(), following Panama's day-first convention; summation, tax-rate validation, RUC format, date validity, and duplicate detection are done by validar.py, with suspicious receipts going to "needs review" instead of being silently counted; totals are computed by informe.py using Decimal to avoid rounding drift, with duplicates excluded.

Gemma runs with temperature: 0 and thinking mode off—transcription needs no reasoning, and the same photo always yields the same reading. On a 16GB laptop with no GPU, one receipt takes 6 to 27 seconds.

JSON Schema constrained output

Ollama's format field lets the caller pass in a JSON Schema, and the server constrains the model's output at the decoding stage to valid JSON matching that structure. This is fundamentally different from "requesting JSON in the prompt": the latter is only a soft constraint—the model can still add explanatory text, omit fields, or return invalid escapes; the former uses constrained decoding to mask tokens that violate the grammar or structure at sampling time, making format-level errors nearly impossible. Cuadre uses this to ensure Gemma can only return predefined fields and can't invent categories or stuff in extra content. It's worth noting that the Schema constrains structure, not semantic correctness—fields can be present and correctly typed while values are still copied wrong, which is exactly why the validar.py validation step exists.

Decimal and floating-point rounding drift

Python's float follows IEEE 754 double-precision floating point, which can't exactly represent decimal fractions like 0.1 or 0.83, producing tiny errors when accumulated. A single receipt won't show it, but with three thousand receipts a month, each with multiple amount fields, the errors add up, causing the total ITBMS to disagree with manual calculations or the tax authority's system—and tax filings don't allow that kind of "close enough." decimal.Decimal stores values in decimal and supports controllable rounding modes, making it suitable for monetary calculations. Cuadre uses Decimal for summation in informe.py and excludes duplicates, ensuring the summary matches a receipt-by-receipt manual calculation exactly—the precise B/. 10.98 match in the demo is a direct result of this design.

Three things testing taught the author

The first version looked fine on paper. Run on real images, Gemma exposed three problems, each leading to a code adjustment.

In the itbms field, Gemma copied the label "ITBMS 7%" instead of the amount on all six receipts. The code doesn't invent numbers, so all six came out "unbalanced." The fix: have Gemma return the full printed line (ITBMS 7% 0.83), and let the code extract the last amount and ignore the percentage.

On the hotel receipt, Gemma dropped the last part of the RUC. There was nothing to fix on the model side—the RUC format check caught it. That's exactly what checks are for.

On the handwritten corner-store receipt, Gemma put the date 10/10/26 into the subtotal field, and the code read it as B/. 26.00. The fix rule: lines that look like dates are never amounts; if the date is missing and appears in another field, the code recovers it from there.

After these adjustments, the results stabilized: ITBMS totals exact, duplicates excluded, tax-exempt items recognized, and every suspicious receipt flagged for review. The author kept one deliberate limitation: Cuadre only checks the shape of the RUC, not the check digit, because the tax authority's algorithm hasn't been verified. The system says so explicitly rather than pretending it can check.

Why open-source models are needed here

Receipts are personal data. They reveal where someone shops, what they bought, when, and how much they spent. A freelancer's receipts also expose who their clients are. For accountants, uploading those photos to a cloud API means handing client data to a third party. Running Gemma with Ollama, Cuadre's only network connection is localhost:11434, and the code doesn't accept any other address.

The cost structure also demands locality. An accountant with 30 clients and about 100 receipts per client needs to read roughly 3000 receipts a month. With a local model, that's just laptop electricity. No per-receipt billing that grows with clients, and no quota exhausted at month-end—exactly when everyone files.

The author also emphasizes the value of not being locked in: if a vendor raises prices or discontinues a model, Cuadre keeps working, the model sits on disk, and switching models is just a parameter (on a machine with more memory, --modelo gemma4:e4b). And because the runtime is open, the author can constrain output with schemas, disable thinking, fix temperature, and see precisely how Gemma fails—then build checks for each failure. The next step could even be fine-tuning Gemma on anonymized Panamanian receipts to make it truncate the RUC less.

The author also acknowledges the trade-off: large hosted models would probably make fewer transcription errors. But for this task, privacy and near-zero per-receipt cost matter more, and the small model's errors happen to be exactly the kind code can catch.

ITBMS and input tax credit

ITBMS (Impuesto de Transferencia de Bienes Muebles y Servicios) is Panama's VAT-style turnover tax on movable property transfers and services, generally 7%, 10% on hotel stays, and 15% on specific goods like tobacco. Like many VAT systems, ITBMS paid on business purchases is an input tax that can be credited against output tax at filing time, with tax owed only on the difference. This is the economic motivation behind Cuadre: every correctly transcribed receipt corresponds to a creditable input tax, and a missed or misrecorded one means real money lost. But eligibility depends on whether the purchase relates to business activity, which Panamanian tax law specifies in detail—hence the article's emphasis that "which purchases qualify is up to the accountant." The tool gets the numbers right; compliance judgment stays with the professional.

Next steps

So far Cuadre has only been tested on six fictitious receipts. The plan is to run it on real receipts (without publishing the images), show it to accountants to see if they'd actually pay for it, and open-source the codebase.

It's worth noting that the author built this project using Claude Code (Claude Opus 5.5) as a coding agent—it implemented the code, tests, and documentation, generated the demo receipts, and drafted the article based on the test log. The author chose the idea and the Panamanian tax rules to check, and reviewed the results.

#Gemma#local LLM#receipt OCR#ITBMS#Panama tax#Ollama#open-source#JSON Schema#Decimal rounding#privacy
Share:

Related articles