Files
data-entry-app/backend/app/api/throughput.py
T
2026-05-31 20:19:44 +12:00

246 lines
8.7 KiB
Python

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