feat: bump client-services (accumulated feature work + deploy fixes)

Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 10:00:14 +03:00
parent 02ec1aa28f
commit 34ae328b0d
182 changed files with 34874 additions and 3556 deletions

View File

@@ -13,6 +13,7 @@ from typing import Optional, List
from database import get_db
from models.order import Order, OrderItem, OrderWaiter, PrintLog
from models.prep_zone import PrepZone
from models.user import User
from models.table import Table
from models.printer import Printer
@@ -20,15 +21,21 @@ from models.shift import WaiterShift
from models.business_day import BusinessDay
from schemas.order import OrderOut
from schemas.table import TableOut
from routers.deps import require_manager
from routers.deps import require_reports
from services.printer_service import (
print_waiter_report, print_printer_report, print_order_receipt, load_divider_style,
print_products_report, print_categories_report, print_tables_report,
print_prep_zone_summary,
)
router = APIRouter()
def _item_val(item) -> float:
"""Effective line value: unit_price + any price adjustment, times quantity."""
return ((item.unit_price or 0.0) + (item.price_adjustment or 0.0)) * item.quantity
def _dt(dt):
if dt is None:
return None
@@ -42,7 +49,7 @@ def shift_summary(
report_date: Optional[date] = Query(default=None, alias="date"),
waiter_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
"""Payments collected per waiter — based on paid_by on order items."""
if from_dt and to_dt:
@@ -80,7 +87,7 @@ def shift_summary(
"order_data": {},
}
summary[wid]["items"] += item.quantity
val = item.unit_price * item.quantity
val = _item_val(item)
summary[wid]["total"] += val
oid = item.order_id
@@ -117,7 +124,7 @@ def shift_orders_summary(
waiter_id: Optional[int] = None,
business_day_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
"""Items sent (added) per waiter — regardless of payment status."""
q = db.query(OrderItem).filter(OrderItem.status.in_(["active", "paid"]))
@@ -181,7 +188,7 @@ def shift_orders_summary(
"order_data": {},
}
summary[wid]["items"] += item.quantity
val = item.unit_price * item.quantity
val = _item_val(item)
summary[wid]["total"] += val
oid = item.order_id
@@ -237,7 +244,7 @@ def order_history(
page: int = 1,
page_size: int = 50,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from sqlalchemy.orm import joinedload
from models.table import Table as TableModel
@@ -318,6 +325,7 @@ def order_history(
"added_at": _dt_local(item.added_at),
"quantity": item.quantity,
"unit_price": float(item.unit_price),
"price_adjustment": float(item.price_adjustment or 0.0),
"status": item.status,
"paid_by": item.paid_by,
"paid_by_name": _wname(item.paid_by),
@@ -366,7 +374,7 @@ def order_history(
@router.get("/tables/summary")
def tables_summary(db: Session = Depends(get_db), user: User = Depends(require_manager)):
def tables_summary(db: Session = Depends(get_db), user: User = Depends(require_reports)):
tables = db.query(Table).filter(Table.is_active == True).all()
result = []
for table in tables:
@@ -387,7 +395,7 @@ def printer_totals(
from_date: Optional[str] = Query(default=None, alias="from"),
to_date: Optional[str] = Query(default=None, alias="to"),
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
"""Returns totals per printer based on print_log entries in the date range."""
q = db.query(PrintLog).filter(PrintLog.success == True)
@@ -437,11 +445,13 @@ def printer_totals(
item_ids = json.loads(log.item_ids)
except Exception:
item_ids = []
if item_ids is None:
item_ids = []
for item_id in item_ids:
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
if item and item.status in ("active", "paid"):
summary[pid]["items"] += item.quantity
val = item.unit_price * item.quantity
val = _item_val(item)
summary[pid]["total"] += val
order_map[pid][oid]["total"] += val
product_name = item.product.name if item.product else f"#{item.product_id}"
@@ -510,7 +520,7 @@ def _build_printer_block(printer_id: int, printer_name: str, logs, tables, db, m
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
if item and item.status in ("active", "paid"):
items_count += item.quantity
val = item.unit_price * item.quantity
val = _item_val(item)
grand_total += val
product_name = item.product.name if item.product else f"#{item.product_id}"
if oid in order_map:
@@ -518,7 +528,7 @@ def _build_printer_block(printer_id: int, printer_name: str, logs, tables, db, m
order_map[oid]["items"].append({
"name": product_name,
"quantity": item.quantity,
"unit_price": float(item.unit_price),
"unit_price": float((item.unit_price or 0.0) + (item.price_adjustment or 0.0)),
"total": round(val, 2),
})
# Accumulate item breakdown (always, regardless of mode)
@@ -551,7 +561,7 @@ def print_waiter(
body: PrintWaiterReportBody,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
if not printer:
@@ -577,7 +587,7 @@ def print_waiter(
order_data = []
for o in orders:
active_items = [i for i in o.items if i.status in ("active", "paid")]
total = sum(i.unit_price * i.quantity for i in active_items)
total = sum(_item_val(i) for i in active_items)
order_data.append({
"id": o.id,
"time_open": local_strftime(o.opened_at, "%H:%M"),
@@ -605,7 +615,7 @@ def print_waiter(
"to_dt": local_strftime(to_dt, "%d/%m/%Y %H:%M"),
}
background_tasks.add_task(print_waiter_report, printer.ip_address, printer.port, report, body.mode)
background_tasks.add_task(print_waiter_report, printer.ip_address, printer.port, report, body.mode, printer.codepage_n)
return {"status": "printing"}
@@ -614,7 +624,7 @@ def print_printer_totals(
body: PrintPrinterReportBody,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
# The physical printer that will receive the paper
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
@@ -674,7 +684,7 @@ def print_printer_totals(
"div_style": load_divider_style(db),
}
background_tasks.add_task(print_printer_report, printer.ip_address, printer.port, report, body.mode)
background_tasks.add_task(print_printer_report, printer.ip_address, printer.port, report, body.mode, printer.codepage_n)
return {"status": "printing"}
@@ -710,7 +720,7 @@ def print_products(
body: PrintAnalyticsBody,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from models.product import Product, Category
@@ -737,7 +747,7 @@ def print_products(
if pid not in sold:
sold[pid] = {"qty": 0, "revenue": 0.0}
sold[pid]["qty"] += item.quantity
sold[pid]["revenue"] += item.unit_price * item.quantity
sold[pid]["revenue"] += _item_val(item)
if body.mode == "full":
# All active products, 0-sold included
@@ -762,7 +772,7 @@ def print_products(
"line_width": printer.line_width,
"div_style": load_divider_style(db),
}
background_tasks.add_task(print_products_report, printer.ip_address, printer.port, report)
background_tasks.add_task(print_products_report, printer.ip_address, printer.port, report, printer.codepage_n)
return {"status": "printing"}
@@ -771,7 +781,7 @@ def print_categories(
body: PrintAnalyticsBody,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from models.product import Product, Category
@@ -805,11 +815,11 @@ def print_categories(
cat = categories_db.get(cid)
summary[cid] = {"name": cat.name if cat else f"#{cid}", "units_sold": 0, "revenue": 0.0, "products": {}}
summary[cid]["units_sold"] += item.quantity
summary[cid]["revenue"] += item.unit_price * item.quantity
summary[cid]["revenue"] += _item_val(item)
if pid not in summary[cid]["products"]:
summary[cid]["products"][pid] = {"name": product.name, "qty": 0, "revenue": 0.0}
summary[cid]["products"][pid]["qty"] += item.quantity
summary[cid]["products"][pid]["revenue"] += item.unit_price * item.quantity
summary[cid]["products"][pid]["revenue"] += _item_val(item)
total_rev = sum(v["revenue"] for v in summary.values())
total_qty = sum(v["units_sold"] for v in summary.values())
@@ -841,7 +851,7 @@ def print_categories(
"line_width": printer.line_width,
"div_style": load_divider_style(db),
}
background_tasks.add_task(print_categories_report, printer.ip_address, printer.port, report)
background_tasks.add_task(print_categories_report, printer.ip_address, printer.port, report, printer.codepage_n)
return {"status": "printing"}
@@ -850,7 +860,7 @@ def print_tables(
body: PrintAnalyticsBody,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
printer = db.query(Printer).filter(Printer.id == body.printer_id, Printer.is_active == True).first()
if not printer:
@@ -879,7 +889,7 @@ def print_tables(
"order_count": 0, "revenue": 0.0, "durations": [],
}
summary[tid]["order_count"] += 1
summary[tid]["revenue"] += sum(i.unit_price * i.quantity for i in order.items if i.status in ("active", "paid"))
summary[tid]["revenue"] += sum(_item_val(i) for i in order.items if i.status in ("active", "paid"))
if order.closed_at and order.opened_at:
summary[tid]["durations"].append((order.closed_at - order.opened_at).total_seconds() / 60)
@@ -899,7 +909,7 @@ def print_tables(
"line_width": printer.line_width,
"div_style": load_divider_style(db),
}
background_tasks.add_task(print_tables_report, printer.ip_address, printer.port, report)
background_tasks.add_task(print_tables_report, printer.ip_address, printer.port, report, printer.codepage_n)
return {"status": "printing"}
@@ -915,7 +925,7 @@ def shifts_report(
to_dt: Optional[str] = Query(default=None, alias="to"),
active_only: bool = False,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from routers.shifts import _enrich_shift
@@ -946,7 +956,7 @@ def product_performance(
business_day_id: Optional[int] = None,
category_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from models.product import Product
from models.waste import WasteLog
@@ -1000,7 +1010,7 @@ def product_performance(
"order_ids": set(),
}
qty = item.quantity
revenue = item.unit_price * qty
revenue = _item_val(item)
summary[pid]["qty_sold"] += qty
summary[pid]["revenue"] += revenue
summary[pid]["order_ids"].add(item.order_id)
@@ -1054,7 +1064,7 @@ def table_performance(
to_dt: Optional[str] = Query(default=None, alias="to"),
business_day_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
q = db.query(Order).filter(Order.status.in_(["closed", "paid"]))
if from_dt:
@@ -1081,7 +1091,7 @@ def table_performance(
}
summary[tid]["order_count"] += 1
summary[tid]["revenue"] += sum(
i.unit_price * i.quantity for i in order.items if i.status in ("active", "paid")
_item_val(i) for i in order.items if i.status in ("active", "paid")
)
if order.closed_at and order.opened_at:
summary[tid]["durations"].append(
@@ -1109,7 +1119,7 @@ def traffic_analysis(
to_dt: Optional[str] = Query(default=None, alias="to"),
business_day_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
q = db.query(Order)
if from_dt:
@@ -1126,7 +1136,7 @@ def traffic_analysis(
for order in orders:
revenue = sum(
i.unit_price * i.quantity for i in order.items if i.status in ("active", "paid")
_item_val(i) for i in order.items if i.status in ("active", "paid")
)
h = order.opened_at.hour
d = order.opened_at.weekday()
@@ -1155,7 +1165,7 @@ def business_days_list(
from_dt: Optional[str] = Query(default=None, alias="from"),
to_dt: Optional[str] = Query(default=None, alias="to"),
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
q = db.query(BusinessDay)
if from_dt:
@@ -1180,7 +1190,7 @@ def business_days_list(
for i in o.items:
if i.status not in ("active", "paid"):
continue
rev = i.unit_price * i.quantity
rev = _item_val(i)
revenue += rev
if i.unit_cost is not None:
cost = i.unit_cost * i.quantity
@@ -1214,7 +1224,7 @@ def business_days_list(
@router.get("/business-days/current")
def current_business_day(
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
day = db.query(BusinessDay).filter(BusinessDay.status == "open").order_by(BusinessDay.opened_at.desc()).first()
if not day:
@@ -1236,7 +1246,7 @@ def current_business_day(
for i in o.items:
if i.status not in ("active", "paid"):
continue
rev = i.unit_price * i.quantity
rev = _item_val(i)
revenue += rev
if i.unit_cost is not None:
cost = i.unit_cost * i.quantity
@@ -1304,7 +1314,7 @@ def revenue_trends(
to_dt: Optional[str] = Query(default=None, alias="to"),
granularity: str = Query(default="daily"), # daily | weekly | monthly
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
q = db.query(Order).filter(Order.status.in_(["closed", "paid"]))
if from_dt:
@@ -1329,7 +1339,7 @@ def revenue_trends(
for i in order.items:
if i.status not in ("active", "paid"):
continue
rev = i.unit_price * i.quantity
rev = _item_val(i)
buckets[key]["revenue"] += rev
if i.unit_cost is not None:
buckets[key]["profit"] += rev - i.unit_cost * i.quantity
@@ -1360,7 +1370,7 @@ def category_performance(
to_dt: Optional[str] = Query(default=None, alias="to"),
business_day_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from models.product import Product, Category
@@ -1397,7 +1407,7 @@ def category_performance(
"product_ids": set(),
}
qty = item.quantity
rev = item.unit_price * qty
rev = _item_val(item)
summary[cid]["units_sold"] += qty
summary[cid]["revenue"] += rev
summary[cid]["product_ids"].add(item.product_id)
@@ -1438,7 +1448,7 @@ def cancellations_log(
business_day_id: Optional[int] = None,
waiter_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
q = db.query(OrderItem).filter(OrderItem.status == "cancelled")
@@ -1495,7 +1505,7 @@ def cancellations_log(
"product_name": product_name,
"quantity": item.quantity,
"unit_price": item.unit_price,
"value": round(item.unit_price * item.quantity, 2),
"value": round(_item_val(item), 2),
"cancelled_by": cancelled_by_name,
"cancel_reason": getattr(item, "cancel_reason", None),
"cancelled_at": _dt(cancelled_at) if cancelled_at else _dt(item.added_at),
@@ -1516,7 +1526,7 @@ def printer_history(
business_day_id: Optional[int] = None,
printer_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
q = db.query(PrintLog)
if from_dt:
@@ -1549,6 +1559,8 @@ def printer_history(
item_ids = json.loads(log.item_ids)
except Exception:
item_ids = []
if item_ids is None:
item_ids = []
items = []
for iid in item_ids:
oi = db.query(OrderItem).filter(OrderItem.id == iid).first()
@@ -1560,7 +1572,7 @@ def printer_history(
order_total = None
if order:
order_total = sum(
i.unit_price * i.quantity
_item_val(i)
for i in order.items
if i.status in ("active", "paid")
)
@@ -1593,16 +1605,16 @@ def printer_history(
@router.get("/meta/waiters")
def meta_waiters(
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
waiters = db.query(User).filter(User.role == "waiter", User.is_active == True).all()
waiters = db.query(User).filter(User.perm_access_waiter_app == True, User.is_active == True).all()
return {"waiters": [{"id": w.id, "name": w.full_name or w.username} for w in waiters]}
@router.get("/meta/tables")
def meta_tables(
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
tables = db.query(Table).filter(Table.is_active == True).all()
return {"tables": [{"id": t.id, "name": t.label or f"T{t.number}", "group": t.group.name if t.group else None} for t in tables]}
@@ -1611,7 +1623,7 @@ def meta_tables(
@router.get("/meta/printers")
def meta_printers(
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
printers = db.query(Printer).filter(Printer.is_active == True).all()
return {"printers": [{"id": p.id, "name": p.name} for p in printers]}
@@ -1620,7 +1632,7 @@ def meta_printers(
@router.get("/meta/products")
def meta_products(
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from models.product import Product, Category
products = db.query(Product).filter(Product.lifecycle_status == "active").order_by(Product.name).all()
@@ -1665,7 +1677,7 @@ def shifts_export(
from_dt: Optional[str] = Query(default=None, alias="from"),
to_dt: Optional[str] = Query(default=None, alias="to"),
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
data = shifts_report(waiter_id=waiter_id, business_day_id=business_day_id, from_dt=from_dt, to_dt=to_dt, active_only=False, db=db, user=user)
rows = []
@@ -1697,7 +1709,7 @@ def orders_export(
order_status: Optional[str] = Query(default=None, alias="status"),
table_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
orders = order_history(from_date=from_date, to_date=to_date, waiter_id=waiter_id, order_status=order_status, table_id=table_id, page=1, page_size=10000, db=db, user=user)
tables_db = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
@@ -1712,7 +1724,7 @@ def orders_export(
"opened_at": _dt(o.opened_at),
"closed_at": _dt(o.closed_at) if o.closed_at else "",
"status": o.status,
"total": round(sum(i.unit_price * i.quantity for i in o.items if i.status in ("active", "paid")), 2),
"total": round(sum(_item_val(i) for i in o.items if i.status in ("active", "paid")), 2),
})
date_str = (from_date or "")[:10]
return _csv_response(rows, f"orders-{date_str}.csv")
@@ -1725,7 +1737,7 @@ def products_export(
business_day_id: Optional[int] = None,
category_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
data = product_performance(from_dt=from_dt, to_dt=to_dt, business_day_id=business_day_id, category_id=category_id, db=db, user=user)
rows = [{
@@ -1747,7 +1759,7 @@ def printers_export(
printer_id: Optional[int] = None,
business_day_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
data = printer_history(from_dt=from_dt, to_dt=to_dt, business_day_id=business_day_id, printer_id=printer_id, db=db, user=user)
rows = [{
@@ -1770,7 +1782,7 @@ def cancellations_export(
business_day_id: Optional[int] = None,
waiter_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
data = cancellations_log(from_dt=from_dt, to_dt=to_dt, business_day_id=business_day_id, waiter_id=waiter_id, db=db, user=user)
rows = [{
@@ -1789,6 +1801,192 @@ def cancellations_export(
return _csv_response(rows, f"cancellations-{date_str}.csv")
# ── Prep Zones report ──────────────────────────────────────────────────────────
@router.get("/prep-zones")
def prep_zones_report(
from_dt: Optional[str] = Query(default=None, alias="from"),
to_dt: Optional[str] = Query(default=None, alias="to"),
business_day_id: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_reports),
):
"""
Per-prep-zone stats: item totals, full product list, and full order list
for the requested date range / business day.
"""
zones = db.query(PrepZone).order_by(PrepZone.id).all()
# Build product → zones mapping
product_zone_map: dict[int, list[int]] = {}
for zone in zones:
for product in zone.products:
if product.id not in product_zone_map:
product_zone_map[product.id] = []
product_zone_map[product.id].append(zone.id)
# Query order items in the requested range
q = db.query(OrderItem).join(Order)
if from_dt:
q = q.filter(Order.opened_at >= datetime.fromisoformat(from_dt))
if to_dt:
q = q.filter(Order.opened_at <= datetime.fromisoformat(to_dt))
if business_day_id:
q = q.filter(Order.business_day_id == business_day_id)
q = q.filter(OrderItem.status.in_(["active", "paid"]))
items = q.all()
# Aggregate per zone
zone_stats: dict[int, dict] = {
zone.id: {
"id": zone.id,
"name": zone.name,
"notification_name": zone.notification_name,
"printers": [{"id": p.id, "name": p.name} for p in zone.printers],
"item_count": 0,
"total_value": 0.0,
"product_counts": {}, # product_name → {count, value}
"orders_map": {}, # order_id → {order_id, table, opened_at, items_count, value}
}
for zone in zones
}
for item in items:
zids = product_zone_map.get(item.product_id, [])
pname = item.product.name if item.product else f"#{item.product_id}"
ivalue = round(_item_val(item), 2)
order = item.order
table_label = None
if order and order.table:
t = order.table
table_label = t.label or str(t.number)
for zid in zids:
if zid not in zone_stats:
continue
zs = zone_stats[zid]
zs["item_count"] += item.quantity
zs["total_value"] += ivalue
if pname not in zs["product_counts"]:
zs["product_counts"][pname] = {"count": 0, "value": 0.0}
zs["product_counts"][pname]["count"] += item.quantity
zs["product_counts"][pname]["value"] += ivalue
if order:
oid = order.id
if oid not in zs["orders_map"]:
zs["orders_map"][oid] = {
"order_id": oid,
"table": table_label or f"#{oid}",
"opened_at": order.opened_at.isoformat() if order.opened_at else None,
"items_count": 0,
"value": 0.0,
}
zs["orders_map"][oid]["items_count"] += item.quantity
zs["orders_map"][oid]["value"] += ivalue
result = []
for zs in zone_stats.values():
all_products = sorted(
[
{"name": k, "count": v["count"], "value": round(v["value"], 2)}
for k, v in zs["product_counts"].items()
],
key=lambda x: -x["count"],
)
all_orders = sorted(
[
{**o, "value": round(o["value"], 2)}
for o in zs["orders_map"].values()
],
key=lambda x: x["opened_at"] or "",
)
result.append({
"id": zs["id"],
"name": zs["name"],
"notification_name": zs["notification_name"],
"printers": zs["printers"],
"item_count": zs["item_count"],
"total_value": round(zs["total_value"], 2),
"all_products": all_products,
"all_orders": all_orders,
})
return {"zones": result}
class PrintPrepZoneBody(BaseModel):
printer_id: int
zone_id: int
from_dt: Optional[str] = None
to_dt: Optional[str] = None
business_day_id: Optional[int] = None
@router.post("/print/prep-zone")
def print_prep_zone(
body: PrintPrepZoneBody,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user: User = Depends(require_reports),
):
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")
zone = db.query(PrepZone).filter(PrepZone.id == body.zone_id).first()
if not zone:
raise HTTPException(status_code=404, detail="Prep zone not found")
# Resolve period label
if body.business_day_id:
from models.business_day import BusinessDay
bd = db.query(BusinessDay).filter(BusinessDay.id == body.business_day_id).first()
period_label = bd.opened_at.strftime("%d/%m/%Y") if bd and bd.opened_at else f"#{body.business_day_id}"
elif body.from_dt and body.to_dt:
d1 = datetime.fromisoformat(body.from_dt).strftime("%d/%m/%Y")
d2 = datetime.fromisoformat(body.to_dt).strftime("%d/%m/%Y")
period_label = f"{d1} - {d2}"
else:
period_label = "Όλες"
# Build product→zone mapping for this zone
product_ids = {product.id for product in zone.products}
q = db.query(OrderItem).join(Order)
if body.from_dt:
q = q.filter(Order.opened_at >= datetime.fromisoformat(body.from_dt))
if body.to_dt:
q = q.filter(Order.opened_at <= datetime.fromisoformat(body.to_dt))
if body.business_day_id:
q = q.filter(Order.business_day_id == body.business_day_id)
q = q.filter(OrderItem.status.in_(["active", "paid"]))
items = q.all()
product_counts: dict[str, dict] = {}
for item in items:
if item.product_id not in product_ids:
continue
pname = item.product.name if item.product else f"#{item.product_id}"
if pname not in product_counts:
product_counts[pname] = {"count": 0, "value": 0.0}
product_counts[pname]["count"] += item.quantity
product_counts[pname]["value"] += _item_val(item)
report_items = sorted(
[{"name": k, "count": v["count"], "value": round(v["value"], 2)} for k, v in product_counts.items()],
key=lambda x: -x["count"],
)
report = {
"zone_name": zone.name,
"period_label": period_label,
"items": report_items,
"line_width": printer.line_width,
"div_style": load_divider_style(db),
}
background_tasks.add_task(print_prep_zone_summary, printer.ip_address, printer.port, report, printer.codepage_n)
return {"status": "printing"}
# ---------------------------------------------------------------------------
# Phase 2L — Discount audit report
# ---------------------------------------------------------------------------
@@ -1800,7 +1998,7 @@ def discounts_report(
business_day_id: Optional[int] = None,
applied_by: Optional[int] = None,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
user: User = Depends(require_reports),
):
from models.order import OrderDiscount, OrderItem
@@ -1836,10 +2034,10 @@ def discounts_report(
if d.item_id:
item = db.query(OrderItem).filter(OrderItem.id == d.item_id).first()
if item:
base = item.unit_price * item.quantity
base = _item_val(item)
return round(base * d.discount_value / 100, 2)
elif order:
base = sum(i.unit_price * i.quantity for i in order.items if i.status != "cancelled")
base = sum(_item_val(i) for i in order.items if i.status != "cancelled")
return round(base * d.discount_value / 100, 2)
return 0.0
@@ -1852,7 +2050,7 @@ def discounts_report(
applier = users_map.get(d.applied_by)
applier_name = (applier.full_name or applier.username) if applier else f"#{d.applied_by}"
table_name = tables_map.get(order.table_id) if order and order.table_id else None
order_total = sum(i.unit_price * i.quantity for i in order.items if i.status != "cancelled") if order else None
order_total = sum(_item_val(i) for i in order.items if i.status != "cancelled") if order else None
discount_amount = _compute_discount_amount(d)
total_discount_value += discount_amount