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,175 @@
|
||||
"""Randarea etichetelor de produs finit si contractul de printare.
|
||||
|
||||
Eticheta este generata din designul vizual (blocuri in milimetri) si contine:
|
||||
- denumirea produsului finit si greutatea totala
|
||||
- defalcarea pe componente (ex. 250 g Fasole, 200 g Carne)
|
||||
- lot, data, operator
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from . import designer
|
||||
from .repositories import ValidationError, list_label_fields
|
||||
from .units import format_weight as _format_weight
|
||||
|
||||
|
||||
def _format_datetime(value: Any, fmt: str = "%d.%m.%Y %H:%M") -> str:
|
||||
if not value:
|
||||
return ""
|
||||
try:
|
||||
return datetime.fromisoformat(str(value)).strftime(fmt)
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
|
||||
|
||||
class LabelTemplateError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
def sample_package() -> dict[str, Any]:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
return {
|
||||
"id": 0,
|
||||
"created_at": now,
|
||||
"completed_at": now,
|
||||
"status": "done",
|
||||
"recipe_code": "PF-DEMO",
|
||||
"recipe_name": "Fasole cu carne",
|
||||
"unit": "g",
|
||||
"target_weight": 450,
|
||||
"total_weight": 450,
|
||||
"lot": "LOT-DEMO",
|
||||
"operator": "Operator",
|
||||
"notes": "",
|
||||
"print_count": 0,
|
||||
"items": [
|
||||
{"position": 1, "component_name": "Fasole", "weight": 250, "unit": "g"},
|
||||
{"position": 2, "component_name": "Carne", "weight": 200, "unit": "g"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def sample_recipe() -> dict[str, Any]:
|
||||
return {
|
||||
"id": 0,
|
||||
"code": "PF-DEMO",
|
||||
"name": "Fasole cu carne",
|
||||
"unit": "g",
|
||||
"target_weight": 450,
|
||||
"note": "",
|
||||
"components": [
|
||||
{"position": 1, "name": "Fasole", "code": "FAS", "target_weight": 250},
|
||||
{"position": 2, "name": "Carne", "code": "CAR", "target_weight": 200},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_context(
|
||||
package: dict[str, Any],
|
||||
items: list[dict[str, Any]],
|
||||
recipe: Optional[dict[str, Any]],
|
||||
settings: dict[str, str],
|
||||
template: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Contextul vechi Jinja, pastrat pentru compatibilitate cu integrarile existente."""
|
||||
return {
|
||||
"package": package,
|
||||
"items": items,
|
||||
"recipe": recipe or {
|
||||
"id": None,
|
||||
"code": package.get("recipe_code", ""),
|
||||
"name": package.get("recipe_name", ""),
|
||||
"unit": package.get("unit", ""),
|
||||
"components": [],
|
||||
},
|
||||
"company": {
|
||||
"name": settings.get("company_name", ""),
|
||||
"address": settings.get("company_address", ""),
|
||||
"phone": settings.get("company_phone", ""),
|
||||
},
|
||||
"label": {
|
||||
"width_mm": float((template or {}).get("width_mm", 100)),
|
||||
"height_mm": float((template or {}).get("height_mm", 60)),
|
||||
},
|
||||
"settings": settings,
|
||||
}
|
||||
|
||||
|
||||
def design_for(template: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Designul sablonului; daca lipseste (sablon vechi), se foloseste cel implicit."""
|
||||
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:
|
||||
return designer.default_design(width, height)
|
||||
return design
|
||||
|
||||
|
||||
def render_body(
|
||||
template: dict[str, Any],
|
||||
package: dict[str, Any],
|
||||
items: Optional[list[dict[str, Any]]] = None,
|
||||
recipe: Optional[dict[str, Any]] = None,
|
||||
settings: Optional[dict[str, str]] = None,
|
||||
) -> str:
|
||||
item_list = items if items is not None else package.get("items") or []
|
||||
design = design_for(template)
|
||||
fields = designer.resolve_fields(package, item_list, recipe, settings or {})
|
||||
fields = designer.apply_custom_fields(fields, list_label_fields())
|
||||
return designer.render_design(design, fields, item_list)
|
||||
|
||||
|
||||
def template_css(template: dict[str, Any]) -> str:
|
||||
css = template.get("css") or ""
|
||||
return f"{designer.DESIGN_CSS}\n{css}".strip()
|
||||
|
||||
|
||||
def validate_template(template: dict[str, Any], settings: Optional[dict[str, str]] = None) -> None:
|
||||
"""Revalideaza designul si il randeaza cu date demo, ca sa prindem erorile."""
|
||||
design = designer.validate_design(
|
||||
design_for(template),
|
||||
float(template.get("width_mm") or 100),
|
||||
float(template.get("height_mm") or 60),
|
||||
)
|
||||
demo = designer.apply_custom_fields(designer.demo_fields(), list_label_fields())
|
||||
designer.render_design(design, demo, designer.demo_items())
|
||||
|
||||
|
||||
def print_payload(
|
||||
package: dict[str, Any],
|
||||
template: dict[str, Any],
|
||||
settings: dict[str, str],
|
||||
recipe: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
items = package.get("items") or []
|
||||
html = render_body(template, package, items, recipe, settings)
|
||||
breakdown = ", ".join(
|
||||
f"{_format_weight(item['weight'])} {item['unit']} {item['component_name']}"
|
||||
for item in items
|
||||
)
|
||||
return {
|
||||
"job_id": f"p{package['id']}-{int(package.get('print_count') or 0) + 1}",
|
||||
"package_id": package["id"],
|
||||
"template": {
|
||||
"id": template["id"],
|
||||
"name": template["name"],
|
||||
"width_mm": float(template["width_mm"]),
|
||||
"height_mm": float(template["height_mm"]),
|
||||
},
|
||||
"html": html,
|
||||
"css": template_css(template),
|
||||
"fields": {
|
||||
"recipe_code": package.get("recipe_code", ""),
|
||||
"recipe_name": package.get("recipe_name", ""),
|
||||
"lot": package.get("lot", ""),
|
||||
"operator": package.get("operator", ""),
|
||||
"total_weight": _format_weight(package.get("total_weight")),
|
||||
"unit": package.get("unit", ""),
|
||||
"completed_at": package.get("completed_at", ""),
|
||||
"breakdown": breakdown,
|
||||
},
|
||||
"print_url": f"/labels/{package['id']}/print",
|
||||
"ack_url": f"/api/packages/{package['id']}/printed",
|
||||
}
|
||||
Reference in New Issue
Block a user