bff6d70b17
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
173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
"""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))
|