Initial commit: aplicatie de cantarire si etichetare

Aplicatie Flask + SQLite pentru productie alimentara: cantarirea
componentelor unui produs finit si tiparirea etichetei prin QZ Tray.

- produse finite (retete) formate din componente cantarite
- designer vizual de etichete (mm/px, zona sigura, campuri variabile,
  cod QR cu continut din mai multe bucati)
- legare produs finit -> sablon de eticheta
- navigare pe doua taburi: Cantarire / Setari
- administrare: componente, produse finite, sabloane, campuri, istoric
- backup, resetare baza de date, checklist de pregatire, /healthz
- CLI de administrare si serviciu systemd pentru productie
- vendor/syncpulse-scale-bridge (GPL-3.0-or-later) pentru citirea cantarului
This commit is contained in:
2026-10-18 23:38:45 +03:00
commit bff6d70b17
107 changed files with 21428 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
"""Conexiunea SQLite, migrarea de la modelul vechi si setarile aplicatiei."""
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Any, Optional
import click
from flask import current_app, g
SCHEMA_PATH = Path(__file__).with_name("schema.sql")
SCHEMA_VERSION = 5
DEFAULT_SETTINGS: dict[str, str] = {
"company_name": "Firma SRL",
"company_address": "",
"company_phone": "",
"active_scale_profile": "",
"default_template_id": "",
"auto_print": "1",
"default_operator": "",
}
DEFAULT_TEMPLATE_NAME = "Eticheta produs finit"
def _default_design_json(width: float = 100.0, height: float = 60.0) -> str:
"""Designul implicit (import lazy: `designer` depinde de `repositories`)."""
from . import designer
return designer.dump_design(designer.default_design(width, height))
def get_db() -> sqlite3.Connection:
if "db" not in g:
path = Path(current_app.config["DATABASE"])
path.parent.mkdir(parents=True, exist_ok=True)
db = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES)
db.row_factory = sqlite3.Row
db.execute("PRAGMA foreign_keys = ON")
db.execute("PRAGMA journal_mode = WAL")
g.db = db
return g.db
def close_db(_exc: Optional[BaseException] = None) -> None:
db = g.pop("db", None)
if db is not None:
db.close()
def _table_names(db: sqlite3.Connection) -> set[str]:
return {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'")}
def _migrate_legacy(db: sqlite3.Connection) -> None:
"""Modelul vechi (`products` + `weighings`) devine `components` + istoric arhivat."""
tables = _table_names(db)
if "components" not in tables and "products" in tables:
db.execute("ALTER TABLE products RENAME TO components")
if "packages" not in tables and "weighings" in tables:
db.execute("ALTER TABLE weighings RENAME TO legacy_weighings")
def _schema_version(db: sqlite3.Connection) -> int:
"""Versiunea schemei salvata in setari (0 daca lipseste)."""
if "settings" not in _table_names(db):
return 0
row = db.execute("SELECT value FROM settings WHERE key = 'schema_version'").fetchone()
try:
return int(row["value"]) if row is not None else 0
except (TypeError, ValueError):
return 0
def _seed_settings(db: sqlite3.Connection) -> None:
for key, value in DEFAULT_SETTINGS.items():
db.execute("INSERT OR IGNORE INTO settings(key, value) VALUES (?, ?)", (key, value))
def _seed_template(db: sqlite3.Connection) -> None:
row = db.execute(
"SELECT id FROM label_templates ORDER BY is_default DESC, id LIMIT 1"
).fetchone()
if row is None:
cur = db.execute(
"INSERT INTO label_templates(name, width_mm, height_mm, body_html, css,"
" design_json, is_default) VALUES (?, ?, ?, ' ', '', ?, 1)",
(DEFAULT_TEMPLATE_NAME, 100.0, 60.0, _default_design_json(100.0, 60.0)),
)
db.execute(
"INSERT OR REPLACE INTO settings(key, value) VALUES ('default_template_id', ?)",
(str(cur.lastrowid),),
)
return
# Sabloanele vechi (doar HTML/CSS) primesc designul implicit, ca eticheta sa iasa mereu.
legacy = db.execute(
"SELECT id, width_mm, height_mm FROM label_templates"
" WHERE design_json IS NULL OR TRIM(design_json) = ''"
).fetchall()
for item in legacy:
width = float(item["width_mm"] or 100.0)
height = float(item["height_mm"] or 60.0)
db.execute(
"UPDATE label_templates SET design_json = ?, updated_at = datetime('now')"
" WHERE id = ?",
(_default_design_json(width, height), item["id"]),
)
def _add_missing_columns(db: sqlite3.Connection) -> None:
"""Coloane adaugate dupa prima versiune a schemei."""
tables = _table_names(db)
if "label_templates" in tables:
columns = {row[1] for row in db.execute("PRAGMA table_info(label_templates)")}
if "design_json" not in columns:
db.execute(
"ALTER TABLE label_templates ADD COLUMN design_json TEXT NOT NULL DEFAULT ''"
)
if "recipes" in tables:
columns = {row[1] for row in db.execute("PRAGMA table_info(recipes)")}
# Sablonul de eticheta al fiecarui produs finit (v5).
if "template_id" not in columns:
db.execute(
"ALTER TABLE recipes ADD COLUMN template_id INTEGER"
" REFERENCES label_templates(id) ON DELETE SET NULL"
)
def init_db() -> None:
"""Creeaza/migreaza tabelele, setarile si sablonul implicit."""
db = get_db()
_migrate_legacy(db)
db.executescript(SCHEMA_PATH.read_text(encoding="utf-8"))
_add_missing_columns(db)
_seed_settings(db)
_seed_template(db)
db.execute(
"INSERT OR REPLACE INTO settings(key, value) VALUES ('schema_version', ?)",
(str(SCHEMA_VERSION),),
)
db.commit()
def get_setting(key: str, default: str = "") -> str:
row = get_db().execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
return row["value"] if row is not None else default
def set_setting(key: str, value: Any) -> None:
db = get_db()
db.execute(
"INSERT INTO settings(key, value) VALUES (?, ?)"
" ON CONFLICT(key) DO UPDATE SET value = excluded.value",
(key, str(value)),
)
db.commit()
def all_settings() -> dict[str, str]:
return {row["key"]: row["value"] for row in get_db().execute("SELECT key, value FROM settings")}
def init_db_command() -> None:
init_db()
click.echo("Baza de date a fost initializata.")
def register(app) -> None:
app.teardown_appcontext(close_db)
app.cli.add_command(click.command("init-db")(init_db_command))