138 lines
4.9 KiB
Python
138 lines
4.9 KiB
Python
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
|
|
from sqlalchemy import or_, select
|
||
|
|
from sqlalchemy.orm import Session, joinedload
|
||
|
|
|
||
|
|
from app.api.deps import AuthSession, get_auth_session
|
||
|
|
from app.db.session import get_db
|
||
|
|
from app.models.mix import Mix
|
||
|
|
from app.models.product import Product
|
||
|
|
from app.schemas.editor import EditorMixUpdate, EditorProductRow, EditorProductUpdate
|
||
|
|
from app.services.client_access_service import has_access_level
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/api/editor", tags=["editor"])
|
||
|
|
|
||
|
|
|
||
|
|
def _serialize_row(product: Product) -> dict:
|
||
|
|
return {
|
||
|
|
"id": product.id,
|
||
|
|
"tenant_id": product.tenant_id,
|
||
|
|
"client_name": product.client_name,
|
||
|
|
"item_id": product.item_id,
|
||
|
|
"name": product.name,
|
||
|
|
"mix_id": product.mix_id,
|
||
|
|
"mix_client_name": product.mix.client_name if product.mix else "",
|
||
|
|
"mix_name": product.mix.name if product.mix else "",
|
||
|
|
"sale_type": product.sale_type,
|
||
|
|
"unit_of_measure": product.unit_of_measure,
|
||
|
|
"visible": product.visible,
|
||
|
|
"product_notes": product.notes,
|
||
|
|
"mix_notes": product.mix.notes if product.mix else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _require_editor_session(
|
||
|
|
session: AuthSession = Depends(get_auth_session),
|
||
|
|
db: Session = Depends(get_db),
|
||
|
|
) -> AuthSession:
|
||
|
|
if session.role == "internal":
|
||
|
|
permissions = session.module_permissions or {}
|
||
|
|
if not has_access_level(permissions.get("client_access"), "manage"):
|
||
|
|
raise HTTPException(status_code=403, detail="Lean access is required")
|
||
|
|
if not has_access_level(permissions.get("products"), "edit"):
|
||
|
|
raise HTTPException(status_code=403, detail="products edit access is required")
|
||
|
|
if not has_access_level(permissions.get("mix_master"), "edit"):
|
||
|
|
raise HTTPException(status_code=403, detail="mix_master edit access is required")
|
||
|
|
if not session.tenant_id:
|
||
|
|
raise HTTPException(status_code=403, detail="Internal user context is missing")
|
||
|
|
return session
|
||
|
|
|
||
|
|
raise HTTPException(status_code=403, detail="Lean access is required")
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/products", response_model=list[EditorProductRow])
|
||
|
|
def list_editor_products(
|
||
|
|
q: str | None = Query(default=None, max_length=255),
|
||
|
|
client_name: str | None = Query(default=None, max_length=255),
|
||
|
|
limit: int = Query(default=500, ge=1, le=1000),
|
||
|
|
session: AuthSession = Depends(_require_editor_session),
|
||
|
|
db: Session = Depends(get_db),
|
||
|
|
):
|
||
|
|
statement = (
|
||
|
|
select(Product)
|
||
|
|
.where(Product.tenant_id == session.tenant_id)
|
||
|
|
.options(joinedload(Product.mix))
|
||
|
|
.join(Product.mix)
|
||
|
|
.order_by(Product.client_name, Product.name, Product.id)
|
||
|
|
.limit(limit)
|
||
|
|
)
|
||
|
|
|
||
|
|
if client_name:
|
||
|
|
statement = statement.where(Product.client_name == client_name)
|
||
|
|
|
||
|
|
if q:
|
||
|
|
term = f"%{q.strip()}%"
|
||
|
|
statement = statement.where(
|
||
|
|
or_(
|
||
|
|
Product.client_name.ilike(term),
|
||
|
|
Product.name.ilike(term),
|
||
|
|
Product.item_id.ilike(term),
|
||
|
|
Product.unit_of_measure.ilike(term),
|
||
|
|
Mix.name.ilike(term),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
return [_serialize_row(product) for product in db.scalars(statement).all()]
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/products/{product_id}", response_model=EditorProductRow)
|
||
|
|
def update_editor_product(
|
||
|
|
product_id: int,
|
||
|
|
payload: EditorProductUpdate,
|
||
|
|
session: AuthSession = Depends(_require_editor_session),
|
||
|
|
db: Session = Depends(get_db),
|
||
|
|
):
|
||
|
|
product = db.scalar(
|
||
|
|
select(Product)
|
||
|
|
.where(Product.id == product_id, Product.tenant_id == session.tenant_id)
|
||
|
|
.options(joinedload(Product.mix))
|
||
|
|
)
|
||
|
|
if product is None:
|
||
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
||
|
|
|
||
|
|
if payload.mix_id is not None:
|
||
|
|
mix = db.scalar(select(Mix).where(Mix.id == payload.mix_id, Mix.tenant_id == session.tenant_id))
|
||
|
|
if mix is None:
|
||
|
|
raise HTTPException(status_code=404, detail="Mix not found")
|
||
|
|
|
||
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
||
|
|
setattr(product, field, value)
|
||
|
|
|
||
|
|
db.commit()
|
||
|
|
db.refresh(product)
|
||
|
|
return _serialize_row(product)
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/mixes/{mix_id}", response_model=list[EditorProductRow])
|
||
|
|
def update_editor_mix(
|
||
|
|
mix_id: int,
|
||
|
|
payload: EditorMixUpdate,
|
||
|
|
session: AuthSession = Depends(_require_editor_session),
|
||
|
|
db: Session = Depends(get_db),
|
||
|
|
):
|
||
|
|
mix = db.scalar(select(Mix).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id))
|
||
|
|
if mix is None:
|
||
|
|
raise HTTPException(status_code=404, detail="Mix not found")
|
||
|
|
|
||
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
||
|
|
setattr(mix, field, value)
|
||
|
|
|
||
|
|
db.commit()
|
||
|
|
|
||
|
|
products = db.scalars(
|
||
|
|
select(Product)
|
||
|
|
.where(Product.tenant_id == session.tenant_id, Product.mix_id == mix_id)
|
||
|
|
.options(joinedload(Product.mix))
|
||
|
|
.order_by(Product.client_name, Product.name, Product.id)
|
||
|
|
).all()
|
||
|
|
return [_serialize_row(product) for product in products]
|