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:
2026-10-18 23:38:45 +03:00
commit bff6d70b17
107 changed files with 21428 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
{% extends "base.html" %}
{% block title %}Istoric produse finite{% endblock %}
{% block content %}
<section class="page-head">
<div>
<h1>Istoric produse finite</h1>
<p class="subtitle">Filtrează, verifică și exportă pachetele finalizate.</p>
</div>
</section>
<section class="card">
<form method="get" class="filters">
<div class="field">
<label for="date_from">De la</label>
<input id="date_from" name="date_from" type="date" value="{{ filters.date_from }}">
</div>
<div class="field">
<label for="date_to">Până la</label>
<input id="date_to" name="date_to" type="date" value="{{ filters.date_to }}">
</div>
<div class="field">
<label for="recipe_id">Produs finit</label>
<select id="recipe_id" name="recipe_id">
<option value="">— toate —</option>
{% for r in recipes %}
<option value="{{ r.id }}" {{ 'selected' if filters.recipe_id == r.id }}>{{ r.code }} · {{ r.name }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="status">Stare</label>
<select id="status" name="status">
<option value="">— toate —</option>
<option value="done" {{ 'selected' if filters.status == 'done' }}>finalizat</option>
<option value="open" {{ 'selected' if filters.status == 'open' }}>în lucru</option>
<option value="void" {{ 'selected' if filters.status == 'void' }}>anulat</option>
</select>
</div>
<div class="field grow">
<label for="q">Căutare</label>
<input id="q" name="q" type="search" value="{{ filters.query }}" placeholder="produs, lot, operator…">
</div>
<div class="field actions-field">
<button class="btn primary" type="submit">Filtrează</button>
<a class="btn" href="{{ url_for('history.index') }}">Resetează</a>
</div>
</form>
</section>
<section class="card">
<div class="card-head">
<h2>{{ packages|length }} pachete · total {{ total }}</h2>
<a class="btn ghost"
href="{{ url_for('history.export_csv', date_from=filters.date_from, date_to=filters.date_to, recipe_id=filters.recipe_id or '', status=filters.status, q=filters.query) }}">Export CSV</a>
</div>
{% if packages %}
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Data</th><th>Produs finit</th><th>Componente</th><th class="num">Total</th>
<th>Lot</th><th>Operator</th><th>Stare</th><th>Tipărit</th><th></th>
</tr>
</thead>
<tbody>
{% for p in packages %}
<tr class="{{ 'row-muted' if p.status != 'done' }}">
<td class="nowrap">{{ (p.completed_at or p.created_at)|dt }}</td>
<td>{{ p.recipe_code }} · {{ p.recipe_name }}</td>
<td class="muted small">{{ p.item_summary or "-" }}</td>
<td class="num">{{ (p.total_weight or 0)|weight }} {{ p.unit }}</td>
<td>{{ p.lot or "-" }}</td>
<td>{{ p.operator or "-" }}</td>
<td>
{% if p.status == "done" %}<span class="badge ok">finalizat</span>
{% elif p.status == "open" %}<span class="badge warn">în lucru</span>
{% else %}<span class="badge err">anulat</span>{% endif %}
</td>
<td class="nowrap">
{% if p.printed_at %}<span class="badge ok">{{ p.print_count }}×</span>
{% else %}<span class="muted">—</span>{% endif %}
</td>
<td class="right">
{% if p.status == "done" %}
<a class="link" target="_blank" rel="noopener"
href="{{ url_for('printing.print_label', package_id=p.id, auto='0') }}">Etichetă</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="muted">Nicio înregistrare pentru filtrele selectate.</p>
{% endif %}
</section>
{% endblock %}