Wire ListExportDropdown on advance/reimburse/payment/expense/view/wallet lists, with matching POST export endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
5.7 KiB
Python
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)
|