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:
@@ -5,30 +5,48 @@ from sqlalchemy.orm import Session
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models.order import Order, OrderItem, OrderWaiter
|
from models.order import Order, OrderItem, OrderWaiter, OrderAuditLog
|
||||||
from models.user import User, AssistantAssignment
|
from models.user import User, WaiterZone
|
||||||
|
from models.table import Table
|
||||||
from models.product import Product
|
from models.product import Product
|
||||||
from schemas.order import OrderCreate, OrderOut, OrderItemOut, AddItemsRequest, PayItemsRequest, AssignWaiterRequest, OrderWaiterOut
|
from schemas.order import OrderCreate, OrderOut, OrderItemOut, AddItemsRequest, AddItemsResponse, PayItemsRequest, AssignWaiterRequest, OrderWaiterOut
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class PrintOrderRequest(BaseModel):
|
||||||
|
printer_id: int
|
||||||
from routers.deps import get_current_user, require_manager
|
from routers.deps import get_current_user, require_manager
|
||||||
from services.printer_service import route_and_print
|
from services.printer_service import route_and_print, route_and_print_sync, print_order_receipt
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _can_access_order(order: Order, user: User, db: Session) -> bool:
|
def _can_access_order(order: Order, user: User, db: Session) -> bool:
|
||||||
|
"""Zone-based access: any waiter whose zone covers the order's table group may act on it."""
|
||||||
if user.role in ("manager", "sysadmin"):
|
if user.role in ("manager", "sysadmin"):
|
||||||
return True
|
return True
|
||||||
if order.opened_by == user.id:
|
zones = db.query(WaiterZone).filter(WaiterZone.waiter_id == user.id).all()
|
||||||
|
if not zones:
|
||||||
|
return False
|
||||||
|
if any(z.group_id is None for z in zones):
|
||||||
return True
|
return True
|
||||||
if any(ow.waiter_id == user.id for ow in order.waiters):
|
table = db.query(Table).filter(Table.id == order.table_id).first()
|
||||||
return True
|
if not table:
|
||||||
# Assistant check: user is assistant to any waiter assigned to this order
|
return False
|
||||||
assigned_ids = {ow.waiter_id for ow in order.waiters}
|
allowed_group_ids = {z.group_id for z in zones}
|
||||||
assistant_of = db.query(AssistantAssignment).filter(
|
return table.group_id in allowed_group_ids
|
||||||
AssistantAssignment.assistant_waiter_id == user.id,
|
|
||||||
AssistantAssignment.primary_waiter_id.in_(assigned_ids),
|
|
||||||
).first()
|
def _audit(db: Session, order_id: int, event_type: str, waiter_id: int = None,
|
||||||
return assistant_of is not None
|
item_ids: list = None, amount: float = None, payment_method: str = None, note: str = None):
|
||||||
|
db.add(OrderAuditLog(
|
||||||
|
order_id=order_id,
|
||||||
|
event_type=event_type,
|
||||||
|
waiter_id=waiter_id,
|
||||||
|
item_ids=json.dumps(item_ids) if item_ids is not None else None,
|
||||||
|
amount=amount,
|
||||||
|
payment_method=payment_method,
|
||||||
|
note=note,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[OrderOut])
|
@router.get("/", response_model=List[OrderOut])
|
||||||
@@ -83,16 +101,16 @@ def open_order(body: OrderCreate, db: Session = Depends(get_db), user: User = De
|
|||||||
db.add(order)
|
db.add(order)
|
||||||
db.flush()
|
db.flush()
|
||||||
db.add(OrderWaiter(order_id=order.id, waiter_id=user.id))
|
db.add(OrderWaiter(order_id=order.id, waiter_id=user.id))
|
||||||
|
_audit(db, order.id, "ORDER_OPENED", waiter_id=user.id)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(order)
|
db.refresh(order)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/items", response_model=OrderOut)
|
@router.post("/{order_id}/items", response_model=AddItemsResponse)
|
||||||
def add_items(
|
def add_items(
|
||||||
order_id: int,
|
order_id: int,
|
||||||
body: AddItemsRequest,
|
body: AddItemsRequest,
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: User = Depends(get_current_user),
|
user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
@@ -109,7 +127,6 @@ def add_items(
|
|||||||
product = db.query(Product).filter(Product.id == item_in.product_id).first()
|
product = db.query(Product).filter(Product.id == item_in.product_id).first()
|
||||||
if not product or not product.is_available:
|
if not product or not product.is_available:
|
||||||
raise HTTPException(status_code=400, detail=f"Product {item_in.product_id} not available")
|
raise HTTPException(status_code=400, detail=f"Product {item_in.product_id} not available")
|
||||||
# Calculate extra cost from selected options
|
|
||||||
extra_cost = sum(
|
extra_cost = sum(
|
||||||
(o.price_delta or o.extra_cost or 0.0)
|
(o.price_delta or o.extra_cost or 0.0)
|
||||||
for o in (item_in.selected_options or [])
|
for o in (item_in.selected_options or [])
|
||||||
@@ -119,7 +136,7 @@ def add_items(
|
|||||||
product_id=item_in.product_id,
|
product_id=item_in.product_id,
|
||||||
added_by=user.id,
|
added_by=user.id,
|
||||||
quantity=item_in.quantity,
|
quantity=item_in.quantity,
|
||||||
unit_price=product.base_price + extra_cost, # price snapshot with options
|
unit_price=product.base_price + extra_cost,
|
||||||
selected_options=json.dumps([o.model_dump() for o in item_in.selected_options]) if item_in.selected_options else None,
|
selected_options=json.dumps([o.model_dump() for o in item_in.selected_options]) if item_in.selected_options else None,
|
||||||
removed_ingredients=json.dumps(item_in.removed_ingredients) if item_in.removed_ingredients else None,
|
removed_ingredients=json.dumps(item_in.removed_ingredients) if item_in.removed_ingredients else None,
|
||||||
notes=item_in.notes,
|
notes=item_in.notes,
|
||||||
@@ -128,13 +145,13 @@ def add_items(
|
|||||||
db.flush()
|
db.flush()
|
||||||
new_item_ids.append(item.id)
|
new_item_ids.append(item.id)
|
||||||
|
|
||||||
|
_audit(db, order_id, "ITEMS_ADDED", waiter_id=user.id, item_ids=new_item_ids)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(order)
|
db.refresh(order)
|
||||||
|
|
||||||
# Printer routing runs in background — must never block the order save
|
print_results = route_and_print_sync(order_id, new_item_ids, db)
|
||||||
background_tasks.add_task(route_and_print, order_id, new_item_ids)
|
|
||||||
|
|
||||||
return order
|
return {"order": order, "print_results": print_results}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{order_id}/items/{item_id}", response_model=OrderItemOut)
|
@router.put("/{order_id}/items/{item_id}", response_model=OrderItemOut)
|
||||||
@@ -155,6 +172,7 @@ def cancel_item(order_id: int, item_id: int, db: Session = Depends(get_db), user
|
|||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
item.status = "cancelled"
|
item.status = "cancelled"
|
||||||
|
_audit(db, order_id, "ITEM_CANCELLED", waiter_id=user.id, item_ids=[item_id])
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -171,16 +189,25 @@ def pay_items(order_id: int, body: PayItemsRequest, db: Session = Depends(get_db
|
|||||||
OrderItem.order_id == order_id,
|
OrderItem.order_id == order_id,
|
||||||
OrderItem.status == "active",
|
OrderItem.status == "active",
|
||||||
).all()
|
).all()
|
||||||
|
now = datetime.utcnow()
|
||||||
|
total_paid = 0.0
|
||||||
for item in items:
|
for item in items:
|
||||||
item.status = "paid"
|
item.status = "paid"
|
||||||
|
item.paid_by = user.id
|
||||||
|
item.paid_at = now
|
||||||
|
item.payment_method = body.payment_method
|
||||||
|
total_paid += item.unit_price * item.quantity
|
||||||
|
|
||||||
active_remaining = db.query(OrderItem).filter(
|
active_remaining = db.query(OrderItem).filter(
|
||||||
OrderItem.order_id == order_id, OrderItem.status == "active"
|
OrderItem.order_id == order_id, OrderItem.status == "active"
|
||||||
).count()
|
).count()
|
||||||
order.status = "paid" if active_remaining == 0 else "partially_paid"
|
order.status = "paid" if active_remaining == 0 else "partially_paid"
|
||||||
|
|
||||||
|
paid_ids = [i.id for i in items]
|
||||||
|
_audit(db, order_id, "PAYMENT", waiter_id=user.id, item_ids=paid_ids,
|
||||||
|
amount=total_paid, payment_method=body.payment_method)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"status": order.status, "paid_item_ids": [i.id for i in items]}
|
return {"status": order.status, "paid_item_ids": paid_ids}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/close")
|
@router.post("/{order_id}/close")
|
||||||
@@ -195,6 +222,7 @@ def close_order(order_id: int, db: Session = Depends(get_db), user: User = Depen
|
|||||||
order.status = "closed"
|
order.status = "closed"
|
||||||
order.closed_at = datetime.utcnow()
|
order.closed_at = datetime.utcnow()
|
||||||
order.closed_by = user.id
|
order.closed_by = user.id
|
||||||
|
_audit(db, order_id, "ORDER_CLOSED", waiter_id=user.id)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"status": "closed"}
|
return {"status": "closed"}
|
||||||
|
|
||||||
@@ -207,6 +235,7 @@ def cancel_order(order_id: int, db: Session = Depends(get_db), user: User = Depe
|
|||||||
order.status = "cancelled"
|
order.status = "cancelled"
|
||||||
order.closed_at = datetime.utcnow()
|
order.closed_at = datetime.utcnow()
|
||||||
order.closed_by = user.id
|
order.closed_by = user.id
|
||||||
|
_audit(db, order_id, "ORDER_CANCELLED", waiter_id=user.id)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -234,3 +263,58 @@ def remove_waiter(order_id: int, waiter_id: int, db: Session = Depends(get_db),
|
|||||||
raise HTTPException(status_code=404, detail="Assignment not found")
|
raise HTTPException(status_code=404, detail="Assignment not found")
|
||||||
db.delete(assignment)
|
db.delete(assignment)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{order_id}/print")
|
||||||
|
def print_order(
|
||||||
|
order_id: int,
|
||||||
|
body: PrintOrderRequest,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: User = Depends(require_manager),
|
||||||
|
):
|
||||||
|
from models.printer import Printer
|
||||||
|
|
||||||
|
order = db.query(Order).filter(Order.id == order_id).first()
|
||||||
|
if not order:
|
||||||
|
raise HTTPException(status_code=404, detail="Order not found")
|
||||||
|
|
||||||
|
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
|
||||||
|
if not printer:
|
||||||
|
raise HTTPException(status_code=404, detail="Printer not found or inactive")
|
||||||
|
|
||||||
|
table = db.query(Table).filter(Table.id == order.table_id).first()
|
||||||
|
table_name = (table.label or f"T{table.number}") if table else f"#{order.table_id}"
|
||||||
|
|
||||||
|
opener = db.query(User).filter(User.id == order.opened_by).first()
|
||||||
|
waiter_name = opener.username if opener else f"#{order.opened_by}"
|
||||||
|
|
||||||
|
items_data = []
|
||||||
|
for item in order.items:
|
||||||
|
if item.status == "cancelled":
|
||||||
|
continue
|
||||||
|
product_name = item.product.name if item.product else f"#{item.product_id}"
|
||||||
|
items_data.append({
|
||||||
|
"name": product_name,
|
||||||
|
"quantity": item.quantity,
|
||||||
|
"unit_price": item.unit_price,
|
||||||
|
"total": item.unit_price * item.quantity,
|
||||||
|
"status": item.status,
|
||||||
|
})
|
||||||
|
|
||||||
|
grand_total = sum(i["total"] for i in items_data)
|
||||||
|
|
||||||
|
receipt = {
|
||||||
|
"order_id": order.id,
|
||||||
|
"table_name": table_name,
|
||||||
|
"waiter_name": waiter_name,
|
||||||
|
"opened_at": order.opened_at.strftime("%d/%m/%Y %H:%M"),
|
||||||
|
"closed_at": order.closed_at.strftime("%d/%m/%Y %H:%M") if order.closed_at else None,
|
||||||
|
"status": order.status,
|
||||||
|
"items": items_data,
|
||||||
|
"total": grand_total,
|
||||||
|
"notes": order.notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
background_tasks.add_task(print_order_receipt, printer.ip_address, printer.port, receipt)
|
||||||
|
return {"status": "printing"}
|
||||||
|
|||||||
@@ -42,6 +42,22 @@ class OrderItemOut(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
added_at: datetime
|
added_at: datetime
|
||||||
printed: bool
|
printed: bool
|
||||||
|
paid_by: Optional[int] = None
|
||||||
|
paid_at: Optional[datetime] = None
|
||||||
|
payment_method: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class PrintResultOut(BaseModel):
|
||||||
|
printer_name: str
|
||||||
|
success: bool
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AddItemsResponse(BaseModel):
|
||||||
|
order: "OrderOut"
|
||||||
|
print_results: List[PrintResultOut]
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
@@ -52,6 +68,7 @@ class OrderCreate(BaseModel):
|
|||||||
|
|
||||||
class PayItemsRequest(BaseModel):
|
class PayItemsRequest(BaseModel):
|
||||||
item_ids: List[int]
|
item_ids: List[int]
|
||||||
|
payment_method: Optional[str] = None # 'cash' | 'card' | 'other' — optional for now
|
||||||
|
|
||||||
|
|
||||||
class AssignWaiterRequest(BaseModel):
|
class AssignWaiterRequest(BaseModel):
|
||||||
@@ -63,6 +80,21 @@ class OrderWaiterOut(BaseModel):
|
|||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
order_id: int
|
||||||
|
event_type: str
|
||||||
|
waiter_id: Optional[int] = None
|
||||||
|
waiter_name: Optional[str] = None # resolved server-side
|
||||||
|
item_ids: Optional[str] = None
|
||||||
|
amount: Optional[float] = None
|
||||||
|
payment_method: Optional[str] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
class OrderOut(BaseModel):
|
class OrderOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
table_id: int
|
table_id: int
|
||||||
@@ -74,5 +106,6 @@ class OrderOut(BaseModel):
|
|||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
items: List[OrderItemOut] = []
|
items: List[OrderItemOut] = []
|
||||||
waiters: List[OrderWaiterOut] = []
|
waiters: List[OrderWaiterOut] = []
|
||||||
|
audit_logs: List[AuditLogOut] = []
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|||||||
@@ -160,6 +160,173 @@ def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db:
|
|||||||
p.cut()
|
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 ────────────────────────────────────────────────────────────
|
# ── Routing logic ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def route_and_print(order_id: int, item_ids: List[int]):
|
def route_and_print(order_id: int, item_ids: List[int]):
|
||||||
@@ -169,10 +336,29 @@ def route_and_print(order_id: int, item_ids: List[int]):
|
|||||||
"""
|
"""
|
||||||
db: Session = SessionLocal()
|
db: Session = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
_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()
|
order = db.query(Order).filter(Order.id == order_id).first()
|
||||||
if not order:
|
if not order:
|
||||||
logger.error("route_and_print: order %s not found", order_id)
|
logger.error("route_and_print: order %s not found", order_id)
|
||||||
return
|
return results
|
||||||
|
|
||||||
items = db.query(OrderItem).filter(OrderItem.id.in_(item_ids)).all()
|
items = db.query(OrderItem).filter(OrderItem.id.in_(item_ids)).all()
|
||||||
|
|
||||||
@@ -193,6 +379,7 @@ def route_and_print(order_id: int, item_ids: List[int]):
|
|||||||
printer = db.query(Printer).filter(Printer.id == printer_id, Printer.is_active == True).first()
|
printer = db.query(Printer).filter(Printer.id == printer_id, Printer.is_active == True).first()
|
||||||
if not printer:
|
if not printer:
|
||||||
logger.warning("Printer %s not found or inactive", printer_id)
|
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
|
continue
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
@@ -202,7 +389,6 @@ def route_and_print(order_id: int, item_ids: List[int]):
|
|||||||
_print_kitchen_ticket(p, order, zone_items, db)
|
_print_kitchen_ticket(p, order, zone_items, db)
|
||||||
p.close()
|
p.close()
|
||||||
success = True
|
success = True
|
||||||
# Mark items as printed
|
|
||||||
for item in zone_items:
|
for item in zone_items:
|
||||||
item.printed = True
|
item.printed = True
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -220,7 +406,6 @@ def route_and_print(order_id: int, item_ids: List[int]):
|
|||||||
db.add(log)
|
db.add(log)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
except Exception as e:
|
results.append({"printer_name": printer.name, "success": success, "error": error_msg})
|
||||||
logger.exception("Unexpected error in route_and_print for order %s: %s", order_id, e)
|
|
||||||
finally:
|
return results
|
||||||
db.close()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user