feat(manual-batch): canonical plate + manual mode as default
ci / smoke (push) Canceled after 0s

- canonical_plate (uppercase alnum, strips space/dot/hyphen) at all plate
  write sites; pretty_plate for render (history, XLSX, operator tile) so
  B 1234 XYZ and B1234XYZ are one plate everywhere
- group_dos_by_plate compares canonical plates -> no false mixed_plates
- warn-only (never blocking) plate format hint in operator + monitoring modals
- batch.default_mode: manual (auto merged truck loads when a sack sat in the
  counting ROI); operator banner explains plate -> start -> stop
- docs + AGENTS known-limitation note; 85 tests pass
This commit is contained in:
andrew committed 2026-10-05 09:36:32 +07:00
1 parent 7ad995d8d1
commit 6c5b1c8b30
11 files changed
+128 -32

No files matched your search

+11 -9
View File
@@ -71,12 +71,14 @@ DASHBOARD_HOST = CFG.dashboard.host
FLASK_DEBUG = CFG.dashboard.debug
from src.do_batch import (
canonical_plate,
empty_do_settings,
group_dos_by_plate,
is_valid_batch_mode,
is_valid_ocr_engine,
net_counts,
normalize_batch_mode,
pretty_plate,
OCR_ENGINES,
retention_cutoff_date,
should_discard_batch,
@@ -475,7 +477,7 @@ def api_batch_start():
elif mode == "manual":
# Legacy manual mode: batch start locked until plate entered.
req_data = request.get_json(silent=True) or request.form or {}
plate = str(req_data.get("plate", "")).strip().upper()
plate = canonical_plate(req_data.get("plate", ""))
if not plate:
return jsonify({
"success": False,
@@ -982,7 +984,7 @@ def api_do_upload():
""",
(counting_date, rel,
ocr.get("no_do", "") or "",
ocr.get("plate", "") or "",
canonical_plate(ocr.get("plate")),
int(ocr.get("expected_sack") or 0),
int(ocr.get("expected_box") or 0),
ocr_raw),
@@ -992,7 +994,7 @@ def api_do_upload():
"id": do_id,
"photo_url": f"/api/do/photo/{do_id}",
"no_do": ocr.get("no_do", "") or "",
"plate": ocr.get("plate", "") or "",
"plate": pretty_plate(ocr.get("plate")),
"expected_sack": int(ocr.get("expected_sack") or 0),
"expected_box": int(ocr.get("expected_box") or 0),
"ocr_text": ocr.get("ocr_text", "") or "",
@@ -1067,7 +1069,7 @@ def api_do_staged():
"id": r["id"],
"photo_url": f"/api/do/photo/{r['id']}",
"no_do": r["no_do"] or ocr.get("no_do", ""),
"plate": r["plate"] or ocr.get("plate", ""),
"plate": pretty_plate(r["plate"] or ocr.get("plate")),
"expected_sack": r["expected_sack"],
"expected_box": r["expected_box"],
"status": r["status"],
@@ -1111,7 +1113,7 @@ def api_do_item(do_id):
req = request.get_json(silent=True) or {}
no_do = str(req.get("no_do", "")).strip()
plate = str(req.get("plate", "")).strip().upper()
plate = canonical_plate(req.get("plate", ""))
try:
expected_sack = int(req.get("expected_sack", 0) or 0)
expected_box = int(req.get("expected_box", 0) or 0)
@@ -1267,7 +1269,7 @@ def api_current_batch():
"unloading": data.get("unloading", 0),
"net_sack": net_sack,
"net_box": net_box,
"plate": data.get("plate", "") or "",
"plate": pretty_plate(data.get("plate")),
"do_numbers": data.get("do_numbers") or [],
"expected_sack": int(data.get("expected_sack", 0) or 0),
"expected_box": int(data.get("expected_box", 0) or 0),
@@ -1344,7 +1346,7 @@ def api_previous_batch():
"count": row["count"],
"box_loading": row["box_loading"],
"model_mode": row["model_mode"],
"plate": row["plate"] if "plate" in row.keys() else "",
"plate": pretty_plate(row["plate"]),
"do_numbers": do_numbers,
"expected_sack": row["expected_sack"] if "expected_sack" in row.keys() else 0,
"expected_box": row["expected_box"] if "expected_box" in row.keys() else 0,
@@ -1507,7 +1509,7 @@ def api_day_detail(date):
"count": row["count"],
"box_loading": row["box_loading"],
"model_mode": row["model_mode"],
"plate": row["plate"] if "plate" in row.keys() else "",
"plate": pretty_plate(row["plate"]),
"do_numbers": do_numbers,
"expected_sack": row["expected_sack"] if "expected_sack" in row.keys() else 0,
"expected_box": row["expected_box"] if "expected_box" in row.keys() else 0,
@@ -1990,7 +1992,7 @@ def export_day_xlsx(date):
ws.cell(row=r_idx, column=1, value=row["batch_number"])
ws.cell(row=r_idx, column=2, value=row["count"])
ws.cell(row=r_idx, column=3, value=row["box_loading"])
ws.cell(row=r_idx, column=4, value=row["plate"] if "plate" in row.keys() else "")
ws.cell(row=r_idx, column=4, value=pretty_plate(row["plate"]))
ws.cell(row=r_idx, column=5, value=", ".join(do_numbers))
ws.cell(row=r_idx, column=6, value=row["expected_sack"] if "expected_sack" in row.keys() else 0)
ws.cell(row=r_idx, column=7, value=row["expected_box"] if "expected_box" in row.keys() else 0)