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,304 @@
|
||||
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
|
||||
Reference in New Issue
Block a user