Backend:
- New models: Tab, TabEntry, TabPayment (3 new tables)
- Tab: open|closed|forgiven, one open tab per customer enforced
- TabEntry: what went ON the tab (per-item snapshot with description)
- TabPayment: what came OFF the tab; balance = sum(entries) - sum(payments), never stored
- New router /api/tabs/:
- GET / — list open tabs (sorted by balance desc); ?tab_status= override
- GET /{id} — full tab detail with entries + payments
- POST / — open new tab for a customer (400 if one already exists)
- POST /{id}/entries — add entry directly (amount + description)
- POST /{id}/pay — record payment; validates amount <= balance
- POST /{id}/close — close tab (requires balance = 0)
- POST /{id}/forgive — write off remaining balance
- GET /customer/{customer_id} — all tabs for a customer (open first)
- POST /api/orders/{id}/items/{item_id}/tab:
- Requires order has a customer assigned
- Finds or auto-creates open tab for that customer
- Sets order_item.status = "tabbed" (new valid status value)
- Creates TabEntry with auto-generated description
- Broadcasts order_updated SSE event
- Migrations: CREATE TABLE IF NOT EXISTS for tabs, tab_entries, tab_payments
Frontend:
- TabsPage (/tabs): open tabs list sorted by balance; each card shows charges/payments
history, live balance, Πληρωμή modal (defaults to full balance), Κλείσιμο button
(only shown when balance=0), Χάρισμα Υπολοίπου with confirm modal + reason
- CustomersPage CustomerDetail: open tab banner showing balance + entry count
(appears between stats and contact info when customer has an open tab)
- OrderDetailPage: 📋 Καρτέλα button appears on active items when order has a customer;
tabbed items show a 📋 Καρτέλα badge; tabItem mutation calls /items/{id}/tab
- Sidebar: CreditCard icon for Καρτέλες (after Πελάτες)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
1.5 KiB
Python
64 lines
1.5 KiB
Python
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
from datetime import datetime
|
|
|
|
|
|
class TabEntryOut(BaseModel):
|
|
id: int
|
|
tab_id: int
|
|
order_id: Optional[int] = None
|
|
order_item_id: Optional[int] = None
|
|
amount: float
|
|
description: Optional[str] = None
|
|
created_by_id: int
|
|
created_by_name: Optional[str] = None
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TabPaymentOut(BaseModel):
|
|
id: int
|
|
tab_id: int
|
|
amount: float
|
|
payment_method: Optional[str] = None
|
|
received_by_id: int
|
|
received_by_name: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TabOut(BaseModel):
|
|
id: int
|
|
customer_id: int
|
|
customer_name: Optional[str] = None
|
|
customer_phone: Optional[str] = None
|
|
status: str
|
|
opened_at: datetime
|
|
closed_at: Optional[datetime] = None
|
|
closed_by_id: Optional[int] = None
|
|
notes: Optional[str] = None
|
|
balance: float = 0.0 # computed: sum(entries) - sum(payments)
|
|
total_charged: float = 0.0
|
|
total_paid: float = 0.0
|
|
entries: List[TabEntryOut] = []
|
|
payments: List[TabPaymentOut] = []
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TabPayRequest(BaseModel):
|
|
amount: float
|
|
payment_method: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class TabForgiveRequest(BaseModel):
|
|
reason: Optional[str] = None
|
|
|
|
|
|
class TabCloseRequest(BaseModel):
|
|
notes: Optional[str] = None
|