"""API JSON pentru fluxul de cantarire si pentru plugin-ul de printare.""" from __future__ import annotations import math from typing import Any from flask import Blueprint, current_app, jsonify, request from ..db import all_settings, get_setting from ..labels import LabelTemplateError, print_payload, validate_template from ..repositories import ( ValidationError, add_package_item, complete_package, get_default_template, get_open_package, get_package, get_recipe, get_template, mark_printed, package_state, start_package, undo_package_item, void_package, ) from ..scale import get_scale bp = Blueprint("api", __name__, url_prefix="/api") def _error(message: str, status: int = 400, **extra: Any): return jsonify({"ok": False, "error": message, **extra}), status def _active_profile() -> str: return get_setting("active_scale_profile", "") def _state(package_id: int) -> dict[str, Any]: state = package_state(package_id) if state is None: raise ValidationError("Pachetul nu exista.") return state def _parse_weight(raw: Any) -> float: if isinstance(raw, str): raw = raw.strip().replace(",", ".") try: value = float(raw) except (TypeError, ValueError) as exc: raise ValidationError("Greutatea introdusa nu este un numar valid.") from exc if not math.isfinite(value) or value <= 0: raise ValidationError("Greutatea trebuie sa fie un numar pozitiv.") return round(value, 3) def _resolve_template(template_id: Any, recipe: dict | None = None) -> dict | None: """Prioritate: cerere explicita -> sablonul produsului finit -> setare -> implicit.""" if template_id not in (None, ""): try: template = get_template(int(template_id)) except (TypeError, ValueError): template = None if template: return template recipe_template = (recipe or {}).get("template_id") if recipe_template: template = get_template(int(recipe_template)) if template: return template configured = get_setting("default_template_id", "") if configured.isdigit(): template = get_template(int(configured)) if template: return template return get_default_template() def _label_for(package_id: int, template_id: Any = None) -> dict | None: package = get_package(package_id) if package is None: return None recipe = get_recipe(package["recipe_id"]) if package["recipe_id"] else None template = _resolve_template(template_id or package.get("template_id"), recipe) if template is None: raise LabelTemplateError("Nu exista niciun sablon de eticheta configurat.") validate_template(template, all_settings()) payload = print_payload(package, template, all_settings(), recipe) if package.get("template_id") != template["id"]: from ..db import get_db db = get_db() db.execute("UPDATE packages SET template_id = ? WHERE id = ?", (template["id"], package_id)) db.commit() return payload # -- scale ------------------------------------------------------------------- @bp.get("/scale") def scale_status(): scale = get_scale(current_app) return jsonify({ "ok": scale.available, "error": scale.error, "simulate": bool(current_app.config["SIMULATE"]), "profiles": scale.profiles(), "active_profile": _active_profile(), }) @bp.get("/scale/profiles") def scale_profiles(): return jsonify({"ok": True, "profiles": get_scale(current_app).profiles()}) @bp.get("/weight") def weight(): scale = get_scale(current_app) profile = request.args.get("profile", "").strip() or _active_profile() result = scale.read( profile, max_age_ms=int(request.args.get("max_age_ms") or current_app.config["WEIGHT_MAX_AGE_MS"]), wait_ms=int(request.args.get("wait_ms") or current_app.config["WEIGHT_WAIT_MS"]), ) return jsonify(result) # -- packages ---------------------------------------------------------------- @bp.get("/package") def current_package(): package = get_open_package() if package is None: return jsonify({"ok": True, "state": None}) return jsonify({"ok": True, "state": _state(package["id"])}) @bp.get("/package/") def package_detail(package_id: int): try: return jsonify({"ok": True, "state": _state(package_id)}) except ValidationError as exc: return _error(str(exc), 404) @bp.post("/packages") def package_start(): data = request.get_json(silent=True) or {} try: recipe = get_recipe(int(data.get("recipe_id") or 0)) except (TypeError, ValueError): recipe = None if recipe is None or not recipe["active"]: return _error("Alege un produs finit activ.") template = _resolve_template(data.get("template_id"), recipe) try: package_id = start_package( recipe["id"], operator=str(data.get("operator") or ""), lot=str(data.get("lot") or ""), notes=str(data.get("notes") or ""), template_id=template["id"] if template else None, ) except ValidationError as exc: return _error(str(exc), 409) return jsonify({"ok": True, **_state(package_id)}), 201 @bp.post("/packages//items") def package_add_item(package_id: int): data = request.get_json(silent=True) or {} package = get_package(package_id) if package is None: return _error("Pachetul nu exista.", 404) if package["status"] != "open": return _error("Pachetul este deja finalizat.", 409) force = bool(data.get("force")) raw_weight = data.get("weight") reading: dict[str, Any] | None = None unit = str(data.get("unit") or package["unit"]).strip().lower() source = "manual" stable = True scale_profile = "" raw_hex = "" if raw_weight in (None, ""): scale = get_scale(current_app) reading = scale.read( str(data.get("profile") or "").strip() or _active_profile(), max_age_ms=current_app.config["WEIGHT_MAX_AGE_MS"], wait_ms=current_app.config["WEIGHT_WAIT_MS"], ) if not reading.get("ok"): return _error(reading.get("error") or "Cantarul nu poate fi citit.", 409, reading=reading) if reading.get("stale"): return _error("Citirea cantarului este prea veche. Asteapta o citire noua.", 409, reading=reading) if not reading.get("stable") and not force: return _error("Greutatea nu este stabila. Asteapta stabilizarea sau accepta valoarea.", 422, reading=reading) try: value = _parse_weight(reading.get("value")) except ValidationError: return _error("Cantarul nu a returnat o greutate valida.", 409, reading=reading) unit = str(reading.get("unit") or package["unit"]) source = "scale" stable = bool(reading.get("stable")) scale_profile = reading.get("profile") or "" raw_hex = reading.get("raw_hex") or "" else: try: value = _parse_weight(raw_weight) except ValidationError as exc: return _error(str(exc)) try: add_package_item( package_id, weight=value, unit=unit, source=source, stable=stable, scale_profile=scale_profile, raw_hex=raw_hex, ) state = _state(package_id) label = None if state["step"] is None and state["open"]: complete_package(package_id) state = _state(package_id) label = _label_for(package_id) except (ValidationError, LabelTemplateError) as exc: return _error(str(exc), 409, reading=reading) response = {"ok": True, **state, "reading": reading, "label": label} return jsonify(response), 201 @bp.post("/packages//undo") def package_undo(package_id: int): try: undo_package_item(package_id) return jsonify({"ok": True, **_state(package_id)}) except ValidationError as exc: return _error(str(exc), 409) @bp.post("/packages//void") def package_void(package_id: int): if get_package(package_id) is None: return _error("Pachetul nu exista.", 404) void_package(package_id) return jsonify({"ok": True, "state": None}) @bp.get("/packages//label") def package_label(package_id: int): try: label = _label_for(package_id, request.args.get("template_id")) except LabelTemplateError as exc: return _error(str(exc), 409) if label is None: return _error("Pachetul nu exista.", 404) return jsonify({"ok": True, "label": label}) @bp.post("/packages//printed") def package_printed(package_id: int): package = mark_printed(package_id) if package is None: return _error("Pachetul nu exista.", 404) return jsonify({"ok": True, "package": package})