Backend: table groups with prefix/color, auto-numbering, has_active_order flag
TableGroup gains prefix and color columns for display in the PWA zone filter. Table creation now assigns a global auto-increment number; batch creation uses group-local label numbering (avoids gaps/conflicts when adding to existing groups). DELETE table now blocks if an active order exists (soft or hard delete). Hard delete cascades past orders before removing the table row. list_tables enriches each TableOut with has_active_order computed server-side. TableOut no longer requires number in the input payload; TableCreate simplified. Migration runner refactored to give each ALTER TABLE its own connection so a no-op (column already exists) doesn't leave a dirty transaction blocking later migrations. New migrations added for all new columns. Order.print_logs relationship gains cascade="all, delete-orphan" so print logs are removed when an order is deleted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ def create_group(body: TableGroupCreate, db: Session = Depends(get_db), user: Us
|
||||
if db.query(TableGroup).filter(TableGroup.name == body.name).first():
|
||||
raise HTTPException(status_code=400, detail="Group name already exists")
|
||||
sort_order = db.query(TableGroup).count()
|
||||
group = TableGroup(name=body.name, sort_order=sort_order)
|
||||
group = TableGroup(name=body.name, prefix=body.prefix, sort_order=sort_order)
|
||||
db.add(group)
|
||||
db.commit()
|
||||
db.refresh(group)
|
||||
@@ -59,17 +59,36 @@ def delete_group(group_id: int, db: Session = Depends(get_db), user: User = Depe
|
||||
|
||||
# ── Tables ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _next_global_number(db: Session) -> int:
|
||||
last = db.query(Table).order_by(Table.number.desc()).first()
|
||||
return (last.number + 1) if last else 1
|
||||
|
||||
|
||||
@router.get("/", response_model=List[TableOut])
|
||||
def list_tables(include_inactive: bool = False, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
q = db.query(Table)
|
||||
if not include_inactive:
|
||||
q = q.filter(Table.is_active == True)
|
||||
return q.order_by(Table.group_id, Table.number).all()
|
||||
tables = q.order_by(Table.group_id, Table.number).all()
|
||||
|
||||
active_table_ids = {
|
||||
row[0] for row in db.query(Order.table_id).filter(
|
||||
Order.status.in_(["open", "partially_paid"])
|
||||
).all()
|
||||
}
|
||||
|
||||
result = []
|
||||
for t in tables:
|
||||
out = TableOut.model_validate(t)
|
||||
out.has_active_order = t.id in active_table_ids
|
||||
result.append(out)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/", response_model=TableOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_table(body: TableCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
||||
table = Table(**body.model_dump())
|
||||
number = _next_global_number(db)
|
||||
table = Table(number=number, label=body.label, group_id=body.group_id, is_active=True)
|
||||
db.add(table)
|
||||
db.commit()
|
||||
db.refresh(table)
|
||||
@@ -80,12 +99,30 @@ def create_table(body: TableCreate, db: Session = Depends(get_db), user: User =
|
||||
def batch_create_tables(body: TableBatchCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
||||
if body.count < 1 or body.count > 200:
|
||||
raise HTTPException(status_code=400, detail="Count must be between 1 and 200")
|
||||
|
||||
# Group-local label numbering: find highest suffix already used in this group
|
||||
existing_in_group = (
|
||||
db.query(Table)
|
||||
.filter(Table.group_id == body.group_id)
|
||||
.all()
|
||||
) if body.group_id else []
|
||||
|
||||
# Extract trailing integers from existing labels that start with this prefix
|
||||
used = []
|
||||
for t in existing_in_group:
|
||||
if t.label and t.label.startswith(body.name_prefix):
|
||||
suffix = t.label[len(body.name_prefix):]
|
||||
if suffix.isdigit():
|
||||
used.append(int(suffix))
|
||||
start_label_n = (max(used) + 1) if used else 1
|
||||
|
||||
created = []
|
||||
for i in range(body.count):
|
||||
n = body.start_number + i
|
||||
label_n = start_label_n + i
|
||||
global_number = _next_global_number(db)
|
||||
table = Table(
|
||||
number=n,
|
||||
label=f"{body.name_prefix}{n}",
|
||||
number=global_number,
|
||||
label=f"{body.name_prefix}{label_n}",
|
||||
group_id=body.group_id,
|
||||
is_active=True,
|
||||
)
|
||||
@@ -115,13 +152,22 @@ def delete_table(table_id: int, hard: bool = False, db: Session = Depends(get_db
|
||||
table = db.query(Table).filter(Table.id == table_id).first()
|
||||
if not table:
|
||||
raise HTTPException(status_code=404, detail="Table not found")
|
||||
active_order = db.query(Order).filter(
|
||||
Order.table_id == table_id,
|
||||
Order.status.in_(["open", "partially_paid"])
|
||||
).first()
|
||||
if active_order:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete or deactivate a table with an active order"
|
||||
)
|
||||
if hard:
|
||||
active_order = db.query(Order).filter(
|
||||
Order.table_id == table_id,
|
||||
Order.status.in_(["open", "partially_paid"])
|
||||
).first()
|
||||
if active_order:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete table with active order")
|
||||
# Delete all past (non-active) orders for this table so FK constraint doesn't block deletion.
|
||||
# Active orders are already blocked above. Items/waiters/print_logs cascade via ORM.
|
||||
past_orders = db.query(Order).filter(Order.table_id == table_id).all()
|
||||
for order in past_orders:
|
||||
db.delete(order)
|
||||
db.flush()
|
||||
db.delete(table)
|
||||
else:
|
||||
table.is_active = False
|
||||
|
||||
Reference in New Issue
Block a user