Files
shanxia2.0/backend/app/api/bx/expense_item.py
T
zlwuandCursor dbba755fd8 feat(bx): add list Excel export for reimbursement module pages
Wire ListExportDropdown on advance/reimburse/payment/expense/view/wallet lists, with matching POST export endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 18:34:22 +08:00

158 lines
5.7 KiB
Python

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)