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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user