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:
@@ -0,0 +1,217 @@
|
||||
"""Administrarea sabloanelor de eticheta (designer vizual)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from flask import (Blueprint, Response, flash, jsonify, redirect, render_template, request,
|
||||
url_for)
|
||||
|
||||
from .. import designer
|
||||
from ..db import all_settings
|
||||
from ..labels import print_payload, sample_package, sample_recipe
|
||||
from ..repositories import (
|
||||
ValidationError,
|
||||
create_template,
|
||||
delete_template,
|
||||
get_template,
|
||||
list_label_fields,
|
||||
list_templates,
|
||||
save_template_design,
|
||||
update_template,
|
||||
)
|
||||
|
||||
bp = Blueprint("label_templates", __name__, url_prefix="/sabloane")
|
||||
|
||||
|
||||
@bp.get("")
|
||||
def index():
|
||||
return render_template("templates/list.html", templates=list_templates())
|
||||
|
||||
|
||||
@bp.post("/<int:template_id>/stergere")
|
||||
def remove(template_id: int):
|
||||
try:
|
||||
delete_template(template_id)
|
||||
flash("Sabloul a fost sters.", "success")
|
||||
except ValidationError as exc:
|
||||
flash(str(exc), "error")
|
||||
return redirect(url_for("label_templates.index"))
|
||||
|
||||
|
||||
@bp.get("/<int:template_id>/previzualizare")
|
||||
def preview(template_id: int):
|
||||
template = get_template(template_id)
|
||||
if template is None:
|
||||
flash("Sabloul nu exista.", "error")
|
||||
return redirect(url_for("label_templates.index"))
|
||||
demo = sample_package()
|
||||
payload = print_payload(demo, template, all_settings(), sample_recipe())
|
||||
return render_template("label_print.html", payload=payload, auto=False, preview=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Designer vizual
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _designer_context(template: dict) -> dict:
|
||||
width = float(template.get("width_mm") or 100)
|
||||
height = float(template.get("height_mm") or 60)
|
||||
design = designer.load_design(template.get("design_json") or "", width, height)
|
||||
if design is None:
|
||||
design = designer.default_design(width, height)
|
||||
custom_rows = list_label_fields()
|
||||
demo_fields = designer.apply_custom_fields(designer.demo_fields(), custom_rows)
|
||||
fields = [
|
||||
{"key": key, "label": label, "sample": sample}
|
||||
for key, label, sample in designer.FIELD_CATALOG
|
||||
]
|
||||
fields += designer.custom_field_catalog(custom_rows, demo_fields)
|
||||
return {
|
||||
"template": template,
|
||||
"design": design,
|
||||
"fields": fields,
|
||||
"demo_fields": demo_fields,
|
||||
"demo_items": designer.demo_items(),
|
||||
"block_types": designer.BLOCK_TYPES,
|
||||
"safe_mm": designer.SAFE_MARGIN_MM,
|
||||
"custom_count": len(custom_rows),
|
||||
"has_design": bool(template.get("design_json")),
|
||||
}
|
||||
|
||||
|
||||
@bp.get("/<int:template_id>/designer")
|
||||
def designer_page(template_id: int):
|
||||
template = get_template(template_id)
|
||||
if template is None:
|
||||
flash("Sabloul nu exista.", "error")
|
||||
return redirect(url_for("label_templates.index"))
|
||||
return render_template("templates/designer.html", **_designer_context(template))
|
||||
|
||||
|
||||
@bp.post("/<int:template_id>/designer")
|
||||
def designer_save(template_id: int):
|
||||
template = get_template(template_id)
|
||||
if template is None:
|
||||
return jsonify({"ok": False, "error": "Sabloul nu exista."}), 404
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
try:
|
||||
design = designer.validate_design(
|
||||
data.get("design"),
|
||||
float(template.get("width_mm") or 100),
|
||||
float(template.get("height_mm") or 60),
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
|
||||
save_template_design(template_id, designer.dump_design(design))
|
||||
|
||||
# Dimensiunea din designer devine dimensiunea sablonului.
|
||||
width = float(design["width_mm"])
|
||||
height = float(design["height_mm"])
|
||||
if (width, height) != (float(template["width_mm"]), float(template["height_mm"])):
|
||||
update_template(template_id, {
|
||||
"name": template["name"],
|
||||
"width_mm": width,
|
||||
"height_mm": height,
|
||||
"body_html": template["body_html"] or " ",
|
||||
"css": template["css"],
|
||||
"design_json": designer.dump_design(design),
|
||||
"is_default": template["is_default"],
|
||||
})
|
||||
|
||||
html, css = designer.render_preview(design, list_label_fields())
|
||||
return jsonify({"ok": True, "blocks": len(design["blocks"]), "html": html, "css": css})
|
||||
|
||||
|
||||
@bp.post("/<int:template_id>/designer/stergere")
|
||||
@bp.post("/<int:template_id>/designer/reset")
|
||||
def designer_reset(template_id: int):
|
||||
"""Reseteaza designul la varianta implicita (nu lasă șablonul fără etichetă)."""
|
||||
template = get_template(template_id)
|
||||
if template is None:
|
||||
flash("Sabloul nu exista.", "error")
|
||||
return redirect(url_for("label_templates.index"))
|
||||
width = float(template.get("width_mm") or 100)
|
||||
height = float(template.get("height_mm") or 60)
|
||||
design = designer.default_design(width, height)
|
||||
save_template_design(template_id, designer.dump_design(design))
|
||||
flash("Designul a fost resetat la varianta implicita.", "success")
|
||||
return redirect(url_for("label_templates.designer_page", template_id=template_id))
|
||||
|
||||
|
||||
@bp.get("/designer/qr.svg")
|
||||
def designer_qr():
|
||||
"""Cod QR inline pentru previzualizarea din editor."""
|
||||
data = (request.args.get("data") or "")[:500]
|
||||
svg = designer.qr_svg(data)
|
||||
return Response(svg, mimetype="image/svg+xml",
|
||||
headers={"Cache-Control": "public, max-age=3600"})
|
||||
|
||||
|
||||
@bp.post("/designer/nou")
|
||||
def designer_create():
|
||||
"""Creeaza un sablon nou cu designer, cu numele ales de utilizator."""
|
||||
width = _size_or_default(request.form.get("width_mm"), 100.0)
|
||||
height = _size_or_default(request.form.get("height_mm"), 60.0)
|
||||
design = designer.default_design(width, height)
|
||||
|
||||
name = (request.form.get("name") or "").strip()[:80]
|
||||
existing = {t["name"].strip().lower() for t in list_templates()}
|
||||
if name:
|
||||
if name.lower() in existing:
|
||||
flash(f"Exista deja un sablon numit {name!r}. Alege alt nume.", "error")
|
||||
return redirect(url_for("label_templates.index"))
|
||||
else:
|
||||
# Fara nume: generam unul disponibil, ca sa nu blocam crearea.
|
||||
name = "Sablon designer"
|
||||
base = name
|
||||
counter = 2
|
||||
while name.lower() in existing:
|
||||
name = f"{base} {counter}"
|
||||
counter += 1
|
||||
|
||||
template_id = create_template({
|
||||
"name": name,
|
||||
"width_mm": design["width_mm"],
|
||||
"height_mm": design["height_mm"],
|
||||
"body_html": " ",
|
||||
"css": designer.DESIGN_CSS,
|
||||
"design_json": designer.dump_design(design),
|
||||
"is_default": request.form.get("is_default"),
|
||||
})
|
||||
flash(f"Sablonul {name} a fost creat.", "success")
|
||||
return redirect(url_for("label_templates.designer_page", template_id=template_id))
|
||||
|
||||
|
||||
def _size_or_default(value: Any, default: float) -> float:
|
||||
try:
|
||||
number = float(str(value).replace(",", "."))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return number if 10 <= number <= 500 else default
|
||||
|
||||
|
||||
@bp.post("/<int:template_id>/redenumire")
|
||||
def rename(template_id: int):
|
||||
"""Schimba numele sablonului (din pagina de designer sau din lista)."""
|
||||
template = get_template(template_id)
|
||||
if template is None:
|
||||
flash("Sabloul nu exista.", "error")
|
||||
return redirect(url_for("label_templates.index"))
|
||||
|
||||
name = (request.form.get("name") or "").strip()
|
||||
if not name:
|
||||
flash("Numele sablonului este obligatoriu.", "error")
|
||||
elif name == template["name"]:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
update_template(template_id, {**template, "name": name})
|
||||
flash(f"Sabloul a fost redenumit in {name}.", "success")
|
||||
except ValidationError as exc:
|
||||
flash(str(exc), "error")
|
||||
|
||||
if request.form.get("next") == "designer":
|
||||
return redirect(url_for("label_templates.designer_page", template_id=template_id))
|
||||
return redirect(url_for("label_templates.index"))
|
||||
Reference in New Issue
Block a user