- New reservations module: model, schema, router (CRUD + status updates + upcoming alerts) and background task for auto-expiring stale reservations - Reports: print_products, print_categories, print_tables analytics endpoints plus meta_products and business_day_summary for workday close/view flow - printer_service: configurable font sizes/weights, donut/bar chart print layout helpers, analytics print blocks per printer - tables/schemas: surfaced color, zone, and other new fields on Table, Product, User, Printer - demo_seed.py for quick dev DB population; wipe_database.py utility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
94 lines
2.5 KiB
Python
94 lines
2.5 KiB
Python
from pydantic import BaseModel, field_validator
|
|
from typing import Optional
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
class ReservationCreate(BaseModel):
|
|
guest_name: str
|
|
party_size: int
|
|
phone: Optional[str] = None
|
|
email: Optional[str] = None
|
|
note: Optional[str] = None
|
|
reserved_for: datetime
|
|
table_id: Optional[int] = None
|
|
source: str = "manager_app"
|
|
online_customer_id: Optional[str] = None
|
|
|
|
@field_validator("reserved_for", mode="before")
|
|
@classmethod
|
|
def strip_timezone(cls, v):
|
|
if isinstance(v, str):
|
|
dt = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
|
elif isinstance(v, datetime):
|
|
dt = v
|
|
else:
|
|
return v
|
|
# Store as naive local — strip any tz info so SQLite keeps it as-is
|
|
if dt.tzinfo is not None:
|
|
dt = dt.astimezone().replace(tzinfo=None)
|
|
return dt
|
|
|
|
|
|
class ReservationUpdate(BaseModel):
|
|
guest_name: Optional[str] = None
|
|
party_size: Optional[int] = None
|
|
phone: Optional[str] = None
|
|
email: Optional[str] = None
|
|
note: Optional[str] = None
|
|
reserved_for: Optional[datetime] = None
|
|
table_id: Optional[int] = None
|
|
status: Optional[str] = None
|
|
|
|
@field_validator("reserved_for", mode="before")
|
|
@classmethod
|
|
def strip_timezone(cls, v):
|
|
if v is None:
|
|
return v
|
|
if isinstance(v, str):
|
|
dt = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
|
elif isinstance(v, datetime):
|
|
dt = v
|
|
else:
|
|
return v
|
|
if dt.tzinfo is not None:
|
|
dt = dt.astimezone().replace(tzinfo=None)
|
|
return dt
|
|
|
|
|
|
class ReservationStatusUpdate(BaseModel):
|
|
status: str # arrived | cancelled | no_show | pending
|
|
|
|
|
|
class ReservationOut(BaseModel):
|
|
id: int
|
|
guest_name: str
|
|
party_size: int
|
|
phone: Optional[str] = None
|
|
email: Optional[str] = None
|
|
note: Optional[str] = None
|
|
reserved_for: datetime
|
|
table_id: Optional[int] = None
|
|
status: str
|
|
source: str
|
|
created_by_user_id: Optional[int] = None
|
|
online_customer_id: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
# Denormalized for convenience
|
|
table_label: Optional[str] = None
|
|
created_by_name: Optional[str] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# Compact summary embedded in TableOut for the RESERVED badge
|
|
class ReservationSummary(BaseModel):
|
|
id: int
|
|
guest_name: str
|
|
party_size: int
|
|
reserved_for: datetime
|
|
status: str
|
|
|
|
model_config = {"from_attributes": True}
|