Henrique Poltronieri
All work

2025—2026
Completed

Restaurant ERP

Sales, stock and customers in one system. Registering a sale moves inventory, and every dashboard number is derived from that same record.

RoleFull system

Year2025—2026

SourceGitHub ↗

Overview

A management system for a small restaurant: products and stock, a customer registry, sales, and a dashboard of what happened. It runs on Flask with SQLAlchemy, SQLite while developing and MySQL in the configured production setup.

Problem

The version of this that people actually use is a spreadsheet, where stock and sales are typed twice. They disagree within a week. Once they disagree, nothing derived from them — revenue, low-stock warnings, best sellers — can be trusted.

Solution

One operation writes once. Registering a sale creates the sale record and reduces the stock of every product in it, in the same request. The dashboard does not store any numbers of its own: revenue, sales count, low-stock alerts, and top products are all queries over the sales and products tables.

My role

I built the whole thing — the models, the routes, JWT authentication for the private routes, and the Jinja2 templates on top.

Architecture

Client Jinja2 templates · JavaScript server-rendered
Auth JWT protects private routes
API Flask 3 (Python 3.12) routes and rules
Mapping SQLAlchemy 2 ORM models
Storage SQLite (dev) · MySQL (prod) one schema

Technologies

  • LanguagePython 3.12
  • FrameworkFlask 3
  • ORMSQLAlchemy 2
  • AuthJWT
  • DatabaseMySQL · SQLite
  • ViewsJinja2 · Bootstrap 5

Challenges

A sale touches two things at once. If the sale is written and the stock update fails, the system starts lying — so the two have to move together.

Running the same models against SQLite locally and MySQL in production without the schema drifting apart.

Deciding which routes are public and which need a token, and making the failure obvious instead of silent.

What I learned

Reports are a reading problem, not a writing problem. Every metric I was tempted to store turned out to be a query I could write instead, and the version that stores nothing is the one that never goes stale.

Next