Backend: synchronous print status on add_items

add_items now runs printer routing synchronously and returns
{ order, print_results } so the waiter PWA can show per-printer
ack or failure without guessing. Extracted _do_route_and_print
so the background-task path (route_and_print) is unchanged for
other callers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 17:35:08 +03:00
parent da29d73520
commit 26c4818aa1
3 changed files with 375 additions and 73 deletions

View File

@@ -160,6 +160,173 @@ def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db:
p.cut()
# ── On-demand report / receipt prints ────────────────────────────────────────
def print_waiter_report(ip: str, port: int, report: dict, mode: str):
"""Print a waiter shift/period report. mode='simple'|'extensive'."""
try:
p = _get_printer(ip, port)
p._raw(b'\x1b\x61\x01')
p._raw(b'\x1b\x21\x30')
_raw_text(p, "ΑΝΑΦΟΡΑ ΣΕΡΒΙΤΟΡΟΥ\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x61\x00')
p._raw(b'\x1b\x21\x10')
_raw_text(p, f"Σερβιτορος: {report['waiter_name']}\n")
_raw_text(p, f"Απο: {report['from_dt']}\n")
_raw_text(p, f"Εως: {report['to_dt']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x21\x10')
_raw_text(p, f"Παραγγελιες: {report['orders']}\n")
_raw_text(p, f"Αντικειμενα: {report['items']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x61\x01')
p._raw(b'\x1b\x21\x30')
_raw_text(p, f"ΣΥΝΟΛΟ: {report['total']:.2f}e\n")
p._raw(b'\x1b\x21\x00')
if mode == "extensive" and report.get("order_data"):
_divider(p)
p._raw(b'\x1b\x61\x00')
_raw_text(p, "ΑΝΑΛΥΤΙΚΑ\n")
_divider(p)
for od in report["order_data"]:
# Build right-aligned total: "HH:MM - HH:MM - TABLE . . . 9.99e"
time_open = od.get("time_open", "")
time_close = od.get("time_close", "")
table = od["table"]
value = f"{od['total']:.2f}e"
times_part = f"{time_open} - {time_close}" if time_close else time_open
prefix = f"{times_part} - {table}"
gap = LINE_WIDTH - len(prefix) - len(value)
if gap < 3:
line = f"{prefix} {value}"
else:
dots = (". " * ((gap // 2) + 1))[:gap]
line = f"{prefix}{dots}{value}"
_raw_text(p, line + "\n")
p._raw(b'\n\n\n')
p.cut()
p.close()
except Exception as e:
logger.error("print_waiter_report failed for %s:%s%s", ip, port, e)
def print_printer_report(ip: str, port: int, report: dict, mode: str):
"""Print a per-printer totals report. mode='simple'|'extensive'."""
try:
p = _get_printer(ip, port)
p._raw(b'\x1b\x61\x01')
p._raw(b'\x1b\x21\x30')
_raw_text(p, "ΑΝΑΦΟΡΑ ΕΚΤΥΠΩΤΗ\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x61\x00')
p._raw(b'\x1b\x21\x10')
_raw_text(p, f"Εκτυπωτης: {report['printer_name']}\n")
_raw_text(p, f"Απο: {report['from_dt']}\n")
_raw_text(p, f"Εως: {report['to_dt']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x21\x10')
_raw_text(p, f"Εργασιες εκτ.: {report['print_jobs']}\n")
_raw_text(p, f"Παραγγελιες: {report['orders']}\n")
_raw_text(p, f"Αντικειμενα: {report['items']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x61\x01')
p._raw(b'\x1b\x21\x30')
_raw_text(p, f"ΣΥΝΟΛΟ: {report['total']:.2f}e\n")
p._raw(b'\x1b\x21\x00')
if mode == "extensive" and report.get("order_data"):
_divider(p)
p._raw(b'\x1b\x61\x00')
_raw_text(p, "ΑΝΑΛΥΤΙΚΑ\n")
_divider(p)
for od in report["order_data"]:
# Header line: "HH:MM - TABLE . . . . 9.99e"
prefix = f"{od['time']} - {od['table']}"
value = f"{od['total']:.2f}e"
gap = LINE_WIDTH - len(prefix) - len(value)
if gap < 3:
header_line = f"{prefix} {value}"
else:
dots = (". " * ((gap // 2) + 1))[:gap]
header_line = f"{prefix}{dots}{value}"
p._raw(b'\x1b\x45\x01')
_raw_text(p, header_line + "\n")
p._raw(b'\x1b\x45\x00')
# Indented items
for item in od.get("items", []):
_raw_text(p, f" {item['quantity']} x {item['name']}\n")
p._raw(b'\n\n\n')
p.cut()
p.close()
except Exception as e:
logger.error("print_printer_report failed for %s:%s%s", ip, port, e)
def print_order_receipt(ip: str, port: int, receipt: dict):
"""Print a manager-triggered order receipt."""
try:
p = _get_printer(ip, port)
p._raw(b'\x1b\x61\x01')
p._raw(b'\x1b\x21\x30')
_raw_text(p, f"ΠΑΡΑΓΓΕΛΙΑ #{receipt['order_id']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x61\x00')
p._raw(b'\x1b\x21\x10')
_raw_text(p, f"Τραπεζι: {receipt['table_name']}\n")
_raw_text(p, f"Σερβιτορος: {receipt['waiter_name']}\n")
_raw_text(p, f"Ανοιχτηκε: {receipt['opened_at']}\n")
if receipt.get("closed_at"):
_raw_text(p, f"Εκλεισε: {receipt['closed_at']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
for item in receipt.get("items", []):
p._raw(b'\x1b\x21\x10')
_raw_text(p, _item_line(item["name"], item["quantity"]) + "\n")
p._raw(b'\x1b\x21\x00')
_raw_text(p, f" {item['unit_price']:.2f}e x{item['quantity']} = {item['total']:.2f}e\n")
_divider(p)
if receipt.get("notes"):
p._raw(b'\x1b\x21\x10')
_raw_text(p, f"Σημ: {receipt['notes']}\n")
p._raw(b'\x1b\x21\x00')
_divider(p)
p._raw(b'\x1b\x61\x01')
p._raw(b'\x1b\x21\x30')
_raw_text(p, f"ΣΥΝΟΛΟ: {receipt['total']:.2f}e\n")
p._raw(b'\x1b\x21\x00')
p._raw(b'\n\n\n')
p.cut()
p.close()
except Exception as e:
logger.error("print_order_receipt failed for %s:%s%s", ip, port, e)
# ── Routing logic ────────────────────────────────────────────────────────────
def route_and_print(order_id: int, item_ids: List[int]):
@@ -169,58 +336,76 @@ def route_and_print(order_id: int, item_ids: List[int]):
"""
db: Session = SessionLocal()
try:
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
logger.error("route_and_print: order %s not found", order_id)
return
items = db.query(OrderItem).filter(OrderItem.id.in_(item_ids)).all()
# Group items by printer zone
zone_map: dict[int, List[OrderItem]] = {}
unzoned: List[OrderItem] = []
for item in items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if product and product.printer_zone_id:
zone_map.setdefault(product.printer_zone_id, []).append(item)
else:
unzoned.append(item)
if unzoned:
logger.warning("order %s has %d item(s) with no printer zone — skipped", order_id, len(unzoned))
for printer_id, zone_items in zone_map.items():
printer = db.query(Printer).filter(Printer.id == printer_id, Printer.is_active == True).first()
if not printer:
logger.warning("Printer %s not found or inactive", printer_id)
continue
success = False
error_msg = None
try:
p = _get_printer(printer.ip_address, printer.port)
_print_kitchen_ticket(p, order, zone_items, db)
p.close()
success = True
# Mark items as printed
for item in zone_items:
item.printed = True
db.commit()
except Exception as e:
error_msg = str(e)
logger.error("Print failed for printer %s (%s:%s): %s", printer.name, printer.ip_address, printer.port, e)
log = PrintLog(
order_id=order_id,
printer_id=printer_id,
item_ids=json.dumps([i.id for i in zone_items]),
success=success,
error_message=error_msg,
)
db.add(log)
db.commit()
_do_route_and_print(order_id, item_ids, db)
except Exception as e:
logger.exception("Unexpected error in route_and_print for order %s: %s", order_id, e)
finally:
db.close()
def route_and_print_sync(order_id: int, item_ids: List[int], db: Session) -> List[dict]:
"""
Synchronous variant used when the caller needs print results.
Returns a list of per-printer result dicts:
{ printer_name, success, error }
"""
return _do_route_and_print(order_id, item_ids, db)
def _do_route_and_print(order_id: int, item_ids: List[int], db: Session) -> List[dict]:
results = []
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
logger.error("route_and_print: order %s not found", order_id)
return results
items = db.query(OrderItem).filter(OrderItem.id.in_(item_ids)).all()
# Group items by printer zone
zone_map: dict[int, List[OrderItem]] = {}
unzoned: List[OrderItem] = []
for item in items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if product and product.printer_zone_id:
zone_map.setdefault(product.printer_zone_id, []).append(item)
else:
unzoned.append(item)
if unzoned:
logger.warning("order %s has %d item(s) with no printer zone — skipped", order_id, len(unzoned))
for printer_id, zone_items in zone_map.items():
printer = db.query(Printer).filter(Printer.id == printer_id, Printer.is_active == True).first()
if not printer:
logger.warning("Printer %s not found or inactive", printer_id)
results.append({"printer_name": f"#{printer_id}", "success": False, "error": "Printer not found or inactive"})
continue
success = False
error_msg = None
try:
p = _get_printer(printer.ip_address, printer.port)
_print_kitchen_ticket(p, order, zone_items, db)
p.close()
success = True
for item in zone_items:
item.printed = True
db.commit()
except Exception as e:
error_msg = str(e)
logger.error("Print failed for printer %s (%s:%s): %s", printer.name, printer.ip_address, printer.port, e)
log = PrintLog(
order_id=order_id,
printer_id=printer_id,
item_ids=json.dumps([i.id for i in zone_items]),
success=success,
error_message=error_msg,
)
db.add(log)
db.commit()
results.append({"printer_name": printer.name, "success": success, "error": error_msg})
return results