feat: Phase 2B — staff payroll / hourly rate
- Users: add hourly_rate (Float, nullable) — manager-only, not shown to waiter - WaiterShift: add hourly_rate_snapshot — copied from user.hourly_rate at shift open, never updated - Shift start (both /start and /manager/start): snapshot rate at shift creation - compute_shift_pay(): calculates worked hours (minus breaks) × rate_snapshot; returns None if unset - _enrich_shift(): now includes hourly_rate_snapshot, duration_hours, shift_pay - shifts_report in reports.py: delegates to _enrich_shift so all shift endpoints are consistent - Shifts CSV export: adds duration_hours, hourly_rate, shift_pay columns - StaffTab: hourly_rate field in waiter edit modal (Μισθοδοσία section, col 3) - ShiftsOverview report: Αμοιβή/ώρα + Αποδοχές columns; untracked shows 'Δεν παρακολουθείται' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,10 +35,46 @@ def compute_shift_total(shift_id: int, db: Session) -> float:
|
||||
return round(sum(i.unit_price * i.quantity for i in items), 2)
|
||||
|
||||
|
||||
def compute_shift_pay(shift: WaiterShift) -> dict:
|
||||
"""Return duration_hours and shift_pay. shift_pay is None if no rate snapshot."""
|
||||
now = datetime.now(timezone.utc)
|
||||
start = shift.started_at
|
||||
if start and start.tzinfo is None:
|
||||
start = start.replace(tzinfo=timezone.utc)
|
||||
end = shift.ended_at
|
||||
if end and end.tzinfo is None:
|
||||
end = end.replace(tzinfo=timezone.utc)
|
||||
reference = end or now
|
||||
|
||||
total_seconds = (reference - start).total_seconds() if start else 0
|
||||
|
||||
# Subtract completed breaks
|
||||
break_seconds = 0
|
||||
for b in shift.breaks:
|
||||
bs = b.started_at
|
||||
be = b.ended_at
|
||||
if bs and be:
|
||||
if bs.tzinfo is None:
|
||||
bs = bs.replace(tzinfo=timezone.utc)
|
||||
if be.tzinfo is None:
|
||||
be = be.replace(tzinfo=timezone.utc)
|
||||
break_seconds += (be - bs).total_seconds()
|
||||
|
||||
worked_seconds = max(0, total_seconds - break_seconds)
|
||||
duration_hours = round(worked_seconds / 3600, 4)
|
||||
|
||||
shift_pay = None
|
||||
if shift.hourly_rate_snapshot is not None:
|
||||
shift_pay = round(duration_hours * shift.hourly_rate_snapshot, 2)
|
||||
|
||||
return {"duration_hours": round(duration_hours, 2), "shift_pay": shift_pay}
|
||||
|
||||
|
||||
def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
||||
w = shift.waiter
|
||||
wname = (w.full_name or w.username) if w else f"#{shift.waiter_id}"
|
||||
total = compute_shift_total(shift.id, db) if shift.ended_at is None else (shift.total_collected or 0.0)
|
||||
pay_data = compute_shift_pay(shift)
|
||||
return {
|
||||
"id": shift.id,
|
||||
"waiter_id": shift.waiter_id,
|
||||
@@ -51,6 +87,9 @@ def _enrich_shift(shift: WaiterShift, db: Session) -> dict:
|
||||
"net_to_deliver": round(total + (shift.starting_cash or 0.0), 2),
|
||||
"is_active": shift.ended_at is None,
|
||||
"notes": shift.notes,
|
||||
"hourly_rate_snapshot": shift.hourly_rate_snapshot,
|
||||
"duration_hours": pay_data["duration_hours"],
|
||||
"shift_pay": pay_data["shift_pay"],
|
||||
"breaks": [
|
||||
{"id": b.id, "shift_id": b.shift_id, "started_at": _dt(b.started_at), "ended_at": _dt(b.ended_at)}
|
||||
for b in shift.breaks
|
||||
@@ -97,10 +136,12 @@ def start_shift(
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Waiter already has an active shift")
|
||||
|
||||
target_user = db.query(User).filter(User.id == target_id).first()
|
||||
shift = WaiterShift(
|
||||
waiter_id=target_id,
|
||||
business_day_id=active_day.id,
|
||||
starting_cash=body.starting_cash,
|
||||
hourly_rate_snapshot=target_user.hourly_rate if target_user else None,
|
||||
)
|
||||
db.add(shift)
|
||||
db.commit()
|
||||
@@ -169,6 +210,7 @@ def manager_start_shift(
|
||||
waiter_id=body.waiter_id,
|
||||
business_day_id=active_day.id,
|
||||
starting_cash=body.starting_cash,
|
||||
hourly_rate_snapshot=target.hourly_rate,
|
||||
)
|
||||
db.add(shift)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user