from __future__ import annotations from datetime import date from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import AuthSession, require_client_module_access from app.db.session import get_db from app.models.throughput import ProductionThroughput, ThroughputProduct from app.schemas.throughput import ( ThroughputEntryCreate, ThroughputEntryRead, ThroughputEntryUpdate, ThroughputProductCreate, ThroughputProductRead, ThroughputProductUpdate, ) from app.services.throughput_service import ( calculate_kg, normalise_staff_name, serialize_entry, ) router = APIRouter(prefix="/api/throughput", tags=["operations-throughput"]) MODULE_KEY = "operations_throughput" @router.get("/products", response_model=list[ThroughputProductRead]) def list_products( include_inactive: bool = Query(default=False), session: AuthSession = Depends(require_client_module_access(MODULE_KEY)), db: Session = Depends(get_db), ): stmt = select(ThroughputProduct).where(ThroughputProduct.tenant_id == session.tenant_id) if not include_inactive: stmt = stmt.where(ThroughputProduct.active.is_(True)) stmt = stmt.order_by(ThroughputProduct.name) return db.scalars(stmt).all() @router.post("/products", response_model=ThroughputProductRead, status_code=status.HTTP_201_CREATED) def create_product( payload: ThroughputProductCreate, session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "edit")), db: Session = Depends(get_db), ): if payload.item_id: existing = db.scalar( select(ThroughputProduct).where( ThroughputProduct.tenant_id == session.tenant_id, ThroughputProduct.item_id == payload.item_id, ) ) if existing is not None: raise HTTPException(status_code=409, detail="A product with this item_id already exists") product = ThroughputProduct( tenant_id=session.tenant_id, item_id=payload.item_id, name=payload.name, default_bag_size=payload.default_bag_size, is_bulka_default=payload.is_bulka_default, active=payload.active, is_stock_item=payload.is_stock_item, notes=payload.notes, ) db.add(product) db.commit() db.refresh(product) return product @router.patch("/products/{product_id}", response_model=ThroughputProductRead) def update_product( product_id: int, payload: ThroughputProductUpdate, session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "edit")), db: Session = Depends(get_db), ): product = db.scalar( select(ThroughputProduct).where( ThroughputProduct.id == product_id, ThroughputProduct.tenant_id == session.tenant_id, ) ) if product is None: raise HTTPException(status_code=404, detail="Product not found") for field, value in payload.model_dump(exclude_unset=True).items(): setattr(product, field, value) db.commit() db.refresh(product) return product @router.get("/entries", response_model=list[ThroughputEntryRead]) def list_entries( date_from: date | None = Query(default=None), date_to: date | None = Query(default=None), product_id: int | None = Query(default=None), staff_name: str | None = Query(default=None), quantity_type: str | None = Query(default=None), limit: int = Query(default=200, ge=1, le=1000), session: AuthSession = Depends(require_client_module_access(MODULE_KEY)), db: Session = Depends(get_db), ): stmt = select(ProductionThroughput).where(ProductionThroughput.tenant_id == session.tenant_id) if date_from is not None: stmt = stmt.where(ProductionThroughput.production_date >= date_from) if date_to is not None: stmt = stmt.where(ProductionThroughput.production_date <= date_to) if product_id is not None: stmt = stmt.where(ProductionThroughput.product_id == product_id) if staff_name: stmt = stmt.where(ProductionThroughput.staff_name == staff_name.strip()) if quantity_type in {"bags", "kg"}: stmt = stmt.where(ProductionThroughput.quantity_type == quantity_type) stmt = stmt.order_by(ProductionThroughput.production_date.desc(), ProductionThroughput.id.desc()).limit(limit) return [serialize_entry(entry) for entry in db.scalars(stmt).all()] @router.post("/entries", response_model=ThroughputEntryRead, status_code=status.HTTP_201_CREATED) def create_entry( payload: ThroughputEntryCreate, session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "edit")), db: Session = Depends(get_db), ): product = None if payload.product_id is not None: product = db.scalar( select(ThroughputProduct).where( ThroughputProduct.id == payload.product_id, ThroughputProduct.tenant_id == session.tenant_id, ) ) if product is None: raise HTTPException(status_code=400, detail="product_id does not match an existing product") snapshot = payload.product_name_snapshot or (product.name if product else None) if not snapshot: raise HTTPException(status_code=400, detail="product_name_snapshot or product_id is required") bag_size = payload.bag_size if bag_size is None and product is not None: bag_size = product.default_bag_size if payload.quantity_type == "bags" and (bag_size is None or bag_size <= 0): raise HTTPException(status_code=400, detail="bag_size is required when quantity_type is 'bags'") calculated = calculate_kg(payload.quantity, payload.quantity_type, bag_size) entry = ProductionThroughput( tenant_id=session.tenant_id, production_date=payload.production_date, product_id=product.id if product else None, product_name_snapshot=snapshot, bag_size=bag_size, scales_checked=payload.scales_checked, label_correct=payload.label_correct, bag_sealed=payload.bag_sealed, pallet_good_condition=payload.pallet_good_condition, sample_box_no=payload.sample_box_no, test_weight_1=payload.test_weight_1, test_weight_2=payload.test_weight_2, test_weight_3=payload.test_weight_3, test_weight_4=payload.test_weight_4, test_weight_5=payload.test_weight_5, quantity=payload.quantity, quantity_type=payload.quantity_type, calculated_kg=calculated, staff_name=normalise_staff_name(payload.staff_name), notes=payload.notes, created_by=session.email, ) db.add(entry) db.commit() db.refresh(entry) return serialize_entry(entry) @router.get("/entries/{entry_id}", response_model=ThroughputEntryRead) def get_entry( entry_id: int, session: AuthSession = Depends(require_client_module_access(MODULE_KEY)), db: Session = Depends(get_db), ): entry = db.scalar( select(ProductionThroughput).where( ProductionThroughput.id == entry_id, ProductionThroughput.tenant_id == session.tenant_id, ) ) if entry is None: raise HTTPException(status_code=404, detail="Entry not found") return serialize_entry(entry) @router.patch("/entries/{entry_id}", response_model=ThroughputEntryRead) def update_entry( entry_id: int, payload: ThroughputEntryUpdate, session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "edit")), db: Session = Depends(get_db), ): entry = db.scalar( select(ProductionThroughput).where( ProductionThroughput.id == entry_id, ProductionThroughput.tenant_id == session.tenant_id, ) ) if entry is None: raise HTTPException(status_code=404, detail="Entry not found") data = payload.model_dump(exclude_unset=True) if "staff_name" in data: data["staff_name"] = normalise_staff_name(data["staff_name"]) for field, value in data.items(): setattr(entry, field, value) entry.calculated_kg = calculate_kg(entry.quantity, entry.quantity_type, entry.bag_size) db.commit() db.refresh(entry) return serialize_entry(entry) @router.delete("/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_entry( entry_id: int, session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "manage")), db: Session = Depends(get_db), ): entry = db.scalar( select(ProductionThroughput).where( ProductionThroughput.id == entry_id, ProductionThroughput.tenant_id == session.tenant_id, ) ) if entry is None: raise HTTPException(status_code=404, detail="Entry not found") db.delete(entry) db.commit() return None