from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.bx.export_common import ( ExportColumn, ExportValidationError, BxExportBodyBase, dump_export_row, respond_xlsx, status_label, ) from app.core.bx_auth import BxUserContext, get_bx_current_user from app.core.database import get_db from app.core.tenant_context import get_tenant_no from app.schemas.bx.common import ApiResponse from app.schemas.bx.expense_item import ExpenseItemCreate, ExpenseItemOut, ExpenseItemUpdate from app.services.bx.expense_item import ExpenseItemService router = APIRouter(prefix="/expense-items", tags=["费用项目管理"]) @router.get("/tree", response_model=ApiResponse[List[ExpenseItemOut]]) def get_expense_item_tree( db: Session = Depends(get_db), user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) tree = ExpenseItemService.get_item_tree(db, tenant_no, user=user) return ApiResponse(data=[ExpenseItemOut.model_validate(i) for i in tree]) EXPENSE_ITEM_EXPORT_COLUMNS = [ ExportColumn(title="项目名称", field="itemName"), ExportColumn(title="项目编码", field="itemCode"), ExportColumn(title="层级", field="level"), ExportColumn(title="子项数", field="childCount"), ExportColumn(title="排序", field="sortOrder"), ExportColumn(title="状态", field="status"), ] class ExpenseItemExportBody(BxExportBodyBase): name: Optional[str] = None status: Optional[str] = None def _flatten_expense_tree(nodes: list, out: list) -> None: for n in nodes: out.append(n) children = getattr(n, "children", None) or (n.get("children") if isinstance(n, dict) else None) or [] if children: _flatten_expense_tree(list(children), out) @router.post("/export") def export_expense_items( body: ExpenseItemExportBody, db: Session = Depends(get_db), user: BxUserContext = Depends(get_bx_current_user), ): try: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) tree = ExpenseItemService.get_item_tree(db, tenant_no, user=user) flat: list = [] _flatten_expense_tree(list(tree), flat) outs = [ExpenseItemOut.model_validate(i) for i in flat] if body.ids: id_set = {str(i) for i in body.ids} outs = [o for o in outs if str(o.id) in id_set] if body.name: kw = body.name.strip() outs = [o for o in outs if kw in (o.item_name or "")] if body.status not in (None, ""): outs = [o for o in outs if str(o.status) == str(body.status)] rows = [] for o in outs: data = dump_export_row(o, status_fields=()) data["status"] = status_label(data.get("status")) # childCount may be computed if "childCount" not in data: data["childCount"] = len(getattr(o, "children", None) or []) rows.append(data) return respond_xlsx( db, tenant_no=tenant_no, sheet_title="费用科目", columns=EXPENSE_ITEM_EXPORT_COLUMNS, rows=rows, filename_stem="费用科目", requested_columns=body.columns, ) except ExportValidationError as e: raise HTTPException(status_code=400, detail=str(e)) from e @router.get("/{item_id}", response_model=ApiResponse[ExpenseItemOut]) def get_expense_item( item_id: str, db: Session = Depends(get_db), user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) item = ExpenseItemService.get_by_id(db, item_id, tenant_no, user=user) if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在") return ApiResponse(data=ExpenseItemOut.model_validate(item)) @router.post("/", response_model=ApiResponse[ExpenseItemOut]) def create_expense_item( body: ExpenseItemCreate, db: Session = Depends(get_db), user: BxUserContext = Depends(get_bx_current_user), ): try: item = ExpenseItemService.create(db, body.model_dump(exclude_unset=True), user=user) db.commit() db.refresh(item) except ValueError as exc: db.rollback() raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return ApiResponse(data=ExpenseItemOut.model_validate(item)) @router.put("/{item_id}", response_model=ApiResponse[ExpenseItemOut]) def update_expense_item( item_id: str, body: ExpenseItemUpdate, db: Session = Depends(get_db), user: BxUserContext = Depends(get_bx_current_user), ): try: item = ExpenseItemService.update( db, item_id, body.model_dump(exclude_unset=True), user=user ) if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在") db.commit() db.refresh(item) except ValueError as exc: db.rollback() raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return ApiResponse(data=ExpenseItemOut.model_validate(item)) @router.delete("/{item_id}", response_model=ApiResponse[None]) def delete_expense_item( item_id: str, db: Session = Depends(get_db), user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) item = ExpenseItemService.get_by_id(db, item_id, tenant_no, user=user) if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在") ExpenseItemService.soft_delete(db, item_id, user=user) db.commit() return ApiResponse(data=None)