Files
ske087 bff6d70b17 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
2026-10-18 23:38:45 +03:00

305 lines
12 KiB
Python

from __future__ import annotations
import pytest
from cantarire.repositories import list_packages
def test_pages_load(client):
for path in ("/", "/componente", "/componente/nou", "/retete", "/retete/nou",
"/istoric", "/setari", "/sabloane", "/sabloane/1/designer", "/monitor"):
response = client.get(path)
assert response.status_code == 200, path
def test_html_css_pages_are_gone(client):
"""Modul HTML/CSS a fost scos; rutele vechi nu mai exista."""
for path in ("/sabloane/nou", "/sabloane/1/editare"):
assert client.get(path).status_code == 404, path
html = client.get("/sabloane").get_data(as_text=True)
assert "HTML/CSS" not in html
def test_dashboard_lists_recipes(client, recipe):
html = client.get("/").get_data(as_text=True)
assert "PF-001" in html
assert "Fasole cu carne" in html
assert "Începe pachetul" in html or "Pachet nou" in html
def test_component_crud(client):
created = client.post("/componente/nou", data={
"code": "GAR", "name": "Garnitura", "unit": "g", "active": "on",
})
assert created.status_code == 302
listing = client.get("/componente").get_data(as_text=True)
assert "GAR" in listing and "Garnitura" in listing
duplicate = client.post("/componente/nou", data={"code": "gar", "name": "Alt", "unit": "g"})
assert duplicate.status_code == 200
assert "Exista deja un component" in duplicate.get_data(as_text=True)
def test_recipe_has_ordered_components(client, recipe):
with client.application.app_context():
from cantarire.repositories import get_recipe
stored = get_recipe(recipe["id"])
assert [c["name"] for c in stored["components"]] == ["Fasole", "Carne"]
assert [c["target_weight"] for c in stored["components"]] == [250, 200]
def test_recipe_requires_a_component(client):
response = client.post("/retete/nou", data={
"code": "PF-EMPTY", "name": "Fara componente", "unit": "g",
})
assert response.status_code == 200
assert "cel putin un component" in response.get_data(as_text=True)
def test_two_step_weighing_completes_and_builds_label(client, recipe):
started = client.post("/api/packages", json={"recipe_id": recipe["id"], "lot": "LOT-1"})
assert started.status_code == 201, started.get_json()
data = started.get_json()
package_id = data["package"]["id"]
assert data["step"]["component_name"] == "Fasole"
assert data["step"]["position"] == 1
assert data["planned_total"] == 2
first = client.post(f"/api/packages/{package_id}/items", json={"weight": "250"})
assert first.status_code == 201, first.get_json()
body = first.get_json()
assert body["step"]["component_name"] == "Carne"
assert body["step"]["position"] == 2
assert body["total_weight"] == 250
second = client.post(f"/api/packages/{package_id}/items", json={"weight": "200"})
assert second.status_code == 201, second.get_json()
done = second.get_json()
assert done["complete"] is True
assert done["step"] is None
assert done["package"]["total_weight"] == 450
assert done["package"]["status"] == "done"
label = done["label"]
assert label is not None
assert "Fasole cu carne" in label["html"]
assert "250 g" in label["html"] and "Fasole" in label["html"]
assert "200 g" in label["html"] and "Carne" in label["html"]
assert label["fields"]["breakdown"] == "250 g Fasole, 200 g Carne"
assert label["print_url"] == f"/labels/{package_id}/print"
page = client.get(label["print_url"] + "?auto=0")
assert page.status_code == 200
html = page.get_data(as_text=True)
assert "Fasole cu carne" in html and "450" in html
def test_scale_reading_is_used_when_no_weight_is_sent(client, recipe, scale):
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
response = client.post(f"/api/packages/{package_id}/items", json={})
assert response.status_code == 201, response.get_json()
body = response.get_json()
assert body["package"]["items"][0]["weight"] == 250
assert body["package"]["items"][0]["source"] == "scale"
assert scale.calls >= 1
def test_unstable_scale_requires_force(client, recipe, scale):
scale.reading.update({"stable": False})
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
refused = client.post(f"/api/packages/{package_id}/items", json={})
assert refused.status_code == 422
assert "stabila" in refused.get_json()["error"].lower()
forced = client.post(f"/api/packages/{package_id}/items", json={"force": True})
assert forced.status_code == 201
def test_stale_reading_is_rejected(client, recipe, scale):
scale.reading.update({"stale": True})
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
response = client.post(f"/api/packages/{package_id}/items", json={"force": True})
assert response.status_code == 409
assert "veche" in response.get_json()["error"].lower()
def test_undo_removes_last_component(client, recipe):
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
client.post(f"/api/packages/{package_id}/items", json={"weight": "250"})
undone = client.post(f"/api/packages/{package_id}/undo", json={})
assert undone.status_code == 200
body = undone.get_json()
assert body["items"] == []
assert body["step"]["position"] == 1
def test_void_package(client, recipe):
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
voided = client.post(f"/api/packages/{package_id}/void", json={})
assert voided.status_code == 200
with client.application.app_context():
rows = list_packages(status="void")
assert [row["id"] for row in rows] == [package_id]
assert client.get("/api/package").get_json()["state"] is None
def test_only_one_open_package(client, recipe):
client.post("/api/packages", json={"recipe_id": recipe["id"]})
second = client.post("/api/packages", json={"recipe_id": recipe["id"]})
assert second.status_code == 409
assert "deja un pachet" in second.get_json()["error"].lower()
def test_weight_is_converted_to_recipe_unit(client, fasole):
client.post("/retete/nou", data={
"code": "PF-KG", "name": "Amestec kg", "unit": "kg",
"component_1_id": str(fasole), "active": "on",
})
with client.application.app_context():
from cantarire.repositories import list_recipes
recipe = [r for r in list_recipes() if r["code"] == "PF-KG"][0]
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
response = client.post(f"/api/packages/{package_id}/items",
json={"weight": "0.25", "unit": "kg"})
assert response.status_code == 201
assert response.get_json()["complete"] is True
assert response.get_json()["package"]["total_weight"] == 0.25
def test_invalid_weight(client, recipe):
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
response = client.post(f"/api/packages/{package_id}/items", json={"weight": "abc"})
assert response.status_code == 400
def test_history_filter_and_csv(client, recipe):
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"], "lot": "LOT-A"}).get_json()["package"]["id"]
client.post(f"/api/packages/{package_id}/items", json={"weight": "250"})
client.post(f"/api/packages/{package_id}/items", json={"weight": "200"})
filtered = client.get("/istoric?q=LOT-A").get_data(as_text=True)
assert "LOT-A" in filtered
assert "Fasole cu carne" in filtered
empty = client.get("/istoric?q=NOPE").get_data(as_text=True)
assert "Nicio înregistrare" in empty
export = client.get("/istoric/export.csv?q=LOT-A")
assert export.status_code == 200
assert export.mimetype.startswith("text/csv")
body = export.get_data(as_text=True)
assert "LOT-A" in body
assert "Fasole" in body
assert "Cod produs finit" in body
def test_template_crud_and_preview(client):
preview = client.get("/sabloane/1/previzualizare").get_data(as_text=True)
assert "Fasole cu carne" in preview
assert "250" in preview and "200" in preview
created = client.post("/sabloane/designer/nou", data={
"name": "Etichetă mică",
"width_mm": "60",
"height_mm": "40",
})
assert created.status_code == 302
assert "Etichetă mică" in client.get("/sabloane").get_data(as_text=True)
def test_every_template_renders_a_label(client):
"""Orice sablon produce o eticheta, chiar si unul vechi fara design."""
with client.application.app_context():
from cantarire.db import get_db
get_db().execute(
"INSERT INTO label_templates(name, width_mm, height_mm, body_html, css,"
" design_json) VALUES ('Vechi', 100, 60, '<div>x</div>', '', '')"
)
get_db().commit()
html = client.get("/sabloane").get_data(as_text=True)
assert "Vechi" in html
assert "fără design" in html
def test_printed_acknowledgement(client, recipe):
package_id = client.post("/api/packages", json={"recipe_id": recipe["id"]}).get_json()["package"]["id"]
client.post(f"/api/packages/{package_id}/items", json={"weight": "250"})
client.post(f"/api/packages/{package_id}/items", json={"weight": "200"})
ack = client.post(f"/api/packages/{package_id}/printed")
assert ack.status_code == 200
package = ack.get_json()["package"]
assert package["print_count"] == 1
assert package["printed_at"]
def test_api_scale_status_and_weight(client):
status = client.get("/api/scale").get_json()
assert status["ok"] is True
assert status["profiles"][0]["name"] == "test-scale"
reading = client.get("/api/weight").get_json()
assert reading["ok"] is True
assert reading["value"] == 250.0
def test_qz_tray_assets_are_served(client):
lib = client.get("/static/js/vendor/qz-tray.js")
assert lib.status_code == 200
assert "PATCHED" in lib.get_data(as_text=True)
module = client.get("/static/js/qz-print.js")
assert module.status_code == 200
script = module.get_data(as_text=True)
assert "window.CantarirePrint" in script
assert "qz.printers.find()" in script
assert "type: 'pdf'" in script
def test_qz_ui_is_wired(client):
dashboard = client.get("/").get_data(as_text=True)
assert "vendor/qz-tray.js" in dashboard
assert "js/qz-print.js" in dashboard
assert "data-qz-printer" in dashboard
assert "js/weigh.js" in dashboard
def test_monitor_page_runs_probe(client, scale):
page = client.get("/monitor")
assert page.status_code == 200
assert "Monitor serial" in page.get_data(as_text=True)
result = client.post("/monitor", data={
"port": "/dev/ttyUSB0", "baud": "9600", "framing": "8N1", "seconds": "2",
})
assert result.status_code == 200
html = result.get_data(as_text=True)
assert scale.probed == {"port": "/dev/ttyUSB0", "baud": 9600, "framing": "8N1",
"seconds": 2.0}
assert "toledo_cont" in html
def test_monitor_requires_port(client):
page = client.post("/monitor", data={"port": "", "baud": "9600", "framing": "8N1"})
assert page.status_code == 200
assert "Alege portul serial" in page.get_data(as_text=True)
def test_settings_save(client):
response = client.post("/setari", data={
"company_name": "Firma Test",
"active_scale_profile": "test-scale",
"default_operator": "Maria",
"auto_print": "on",
})
assert response.status_code == 302
page = client.get("/setari").get_data(as_text=True)
assert "Firma Test" in page
assert "Maria" in page