feat: Phase 2K — business day close summary (expanded)
Backend — /api/business-day/summary now returns full financial summary: - COGS: trackable cost (sum of unit_cost snapshots on paid items), uncosted item count and revenue, gross_profit, cogs_has_gap flag - Labor: total_labor_cost (sum of shift_pay for the day), labor_untracked_shifts count - Waste: waste_item_count + waste_estimated_cost from waste_log for this business day - Expenses: expenses_total, expenses_paid_today, expenses_due for expenses logged today - Tabbed revenue: revenue from items with status='tabbed' (deferred, not yet collected) - Net estimate: revenue - COGS - labor - waste - expenses_paid; net_has_unknowns flag when any component has gaps (uncosted items or untracked shifts) - compute_shift_pay import hoisted out of loop (bug fix) Frontend — WorkdaySummaryModal OverviewTab fully expanded: - New FinancialRow helper for consistent two-column P&L rows with accent/warn/indent - REVENUE section: total sales, tabbed deduction, net collected - COGS section: trackable cost, uncosted items warning, gross profit with gap indicator - LABOR section: tracked labor cost, untracked shifts warning - WASTE section: item count + estimated cost (hidden when zero) - EXPENSES section: total, paid today, still due (hidden when zero) - NET ESTIMATE: prominent bottom line, amber warning when unknowns exist - Payment breakdown bar (existing) - Cash reconciliation summary when store cash was counted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -300,8 +300,8 @@ def business_day_summary(
|
||||
else:
|
||||
e["other"] += val
|
||||
|
||||
from routers.shifts import compute_shift_pay
|
||||
for shift in shifts:
|
||||
from routers.shifts import compute_shift_pay
|
||||
e = _waiter_entry(shift.waiter_id)
|
||||
if e["shift_started_at"] is None or (shift.started_at and _dt(shift.started_at) < e["shift_started_at"]):
|
||||
e["shift_started_at"] = _dt(shift.started_at)
|
||||
@@ -422,6 +422,62 @@ def business_day_summary(
|
||||
total_waiter_collected = sum(s.total_collected or 0.0 for s in shifts)
|
||||
store_expected_closing = (day.store_opening_cash or 0.0) + total_waiter_collected
|
||||
|
||||
# ── Phase 2K: full financial summary ──────────────<E29480><E29480>─────────────────────
|
||||
|
||||
# COGS from Phase 2A cost snapshots on paid items
|
||||
from models.waste import WasteLog
|
||||
from models.expenses import Expense
|
||||
from models.tabs import Tab, TabEntry
|
||||
|
||||
cogs_trackable = 0.0
|
||||
cogs_uncosted_revenue = 0.0
|
||||
cogs_uncosted_count = 0
|
||||
for item in paid_items:
|
||||
rev = item.unit_price * item.quantity
|
||||
if item.unit_cost is not None:
|
||||
cogs_trackable += item.unit_cost * item.quantity
|
||||
else:
|
||||
cogs_uncosted_revenue += rev
|
||||
cogs_uncosted_count += 1
|
||||
gross_profit = round(total_revenue - cogs_trackable, 2)
|
||||
cogs_has_gap = cogs_uncosted_count > 0
|
||||
|
||||
# LABOR from shifts
|
||||
total_labor_cost = 0.0
|
||||
labor_untracked_shifts = 0
|
||||
for shift in shifts:
|
||||
pay_data = compute_shift_pay(shift)
|
||||
if pay_data["shift_pay"] is not None:
|
||||
total_labor_cost += pay_data["shift_pay"]
|
||||
else:
|
||||
labor_untracked_shifts += 1
|
||||
|
||||
# WASTE from Phase 2H
|
||||
waste_entries = db.query(WasteLog).filter(WasteLog.business_day_id == day.id).all()
|
||||
waste_item_count = len(waste_entries)
|
||||
waste_estimated_cost = round(sum(w.total_cost for w in waste_entries if w.total_cost is not None), 2)
|
||||
|
||||
# EXPENSES logged today
|
||||
expenses_today = db.query(Expense).filter(Expense.business_day_id == day.id).all()
|
||||
expenses_total = round(sum(e.total_amount for e in expenses_today), 2)
|
||||
expenses_paid_today = round(sum(e.paid_amount for e in expenses_today), 2)
|
||||
expenses_due = round(expenses_total - expenses_paid_today, 2)
|
||||
|
||||
# TABBED REVENUE (items moved to tabs — not yet collected)
|
||||
tabbed_items = [i for i in all_items if i.status == "tabbed"]
|
||||
tabbed_revenue = round(sum(i.unit_price * i.quantity for i in tabbed_items), 2)
|
||||
|
||||
# NET estimate (revenue - COGS - labor - waste - expenses paid)
|
||||
net_estimate = round(
|
||||
total_revenue
|
||||
- cogs_trackable
|
||||
- total_labor_cost
|
||||
- waste_estimated_cost
|
||||
- expenses_paid_today,
|
||||
2
|
||||
)
|
||||
net_has_unknowns = cogs_has_gap or labor_untracked_shifts > 0
|
||||
|
||||
return {
|
||||
"day_id": day.id,
|
||||
"status": day.status,
|
||||
@@ -438,12 +494,28 @@ def business_day_summary(
|
||||
"with_unpaid_items": with_unpaid_items,
|
||||
"active_shifts": active_shift_count,
|
||||
},
|
||||
# Phase 2E
|
||||
# Phase 2E — cash reconciliation
|
||||
"store_opening_cash": day.store_opening_cash,
|
||||
"store_closing_cash": day.store_closing_cash,
|
||||
"store_cash_discrepancy": day.store_cash_discrepancy,
|
||||
"store_expected_closing": round(store_expected_closing, 2),
|
||||
"total_waiter_collected": round(total_waiter_collected, 2),
|
||||
# Phase 2K — full financial summary
|
||||
"cogs_trackable": round(cogs_trackable, 2),
|
||||
"cogs_uncosted_revenue": round(cogs_uncosted_revenue, 2),
|
||||
"cogs_uncosted_count": cogs_uncosted_count,
|
||||
"cogs_has_gap": cogs_has_gap,
|
||||
"gross_profit": gross_profit,
|
||||
"total_labor_cost": round(total_labor_cost, 2),
|
||||
"labor_untracked_shifts": labor_untracked_shifts,
|
||||
"waste_item_count": waste_item_count,
|
||||
"waste_estimated_cost": waste_estimated_cost,
|
||||
"expenses_total": expenses_total,
|
||||
"expenses_paid_today": expenses_paid_today,
|
||||
"expenses_due": expenses_due,
|
||||
"tabbed_revenue": tabbed_revenue,
|
||||
"net_estimate": net_estimate,
|
||||
"net_has_unknowns": net_has_unknowns,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user