refactor(backend): update reimbursement and related services
- endpoints/reimbursements.py: update reimbursement API endpoint - schemas/reimbursement.py: update reimbursement data schemas - services/expense_claims.py: update expense claims service - services/ontology.py: update ontology service - services/user_agent.py: update user agent service
This commit is contained in:
@@ -2,13 +2,18 @@ from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import CurrentUserContext, get_current_user, get_db
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.reimbursement import (
|
||||
ExpenseClaimAttachmentActionResponse,
|
||||
ExpenseClaimActionResponse,
|
||||
ExpenseClaimAttachmentRead,
|
||||
ExpenseClaimItemCreate,
|
||||
ExpenseClaimItemActionResponse,
|
||||
ExpenseClaimItemUpdate,
|
||||
ExpenseClaimRead,
|
||||
ReimbursementCreate,
|
||||
@@ -113,6 +118,238 @@ def update_expense_claim_item(
|
||||
return claim
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claims/{claim_id}/items",
|
||||
response_model=ExpenseClaimRead,
|
||||
summary="新增草稿费用明细",
|
||||
description="在草稿报销单中新增一条费用明细,供用户继续补充附件与字段。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "报销单不存在。",
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorResponse,
|
||||
"description": "草稿状态校验失败或字段校验失败。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def create_expense_claim_item(
|
||||
claim_id: str,
|
||||
payload: ExpenseClaimItemCreate | None,
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> ExpenseClaimRead:
|
||||
service = ExpenseClaimService(db)
|
||||
try:
|
||||
claim = service.create_claim_item(
|
||||
claim_id=claim_id,
|
||||
payload=payload,
|
||||
current_user=current_user,
|
||||
)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
|
||||
if claim is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
|
||||
return claim
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/claims/{claim_id}/items/{item_id}",
|
||||
response_model=ExpenseClaimItemActionResponse,
|
||||
summary="删除草稿费用明细",
|
||||
description="删除草稿报销单中的一条费用明细,并同步清理该行已上传的附件与 OCR 元数据。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "报销单或费用明细不存在。",
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorResponse,
|
||||
"description": "草稿状态校验失败。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def delete_expense_claim_item(
|
||||
claim_id: str,
|
||||
item_id: str,
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> ExpenseClaimItemActionResponse:
|
||||
service = ExpenseClaimService(db)
|
||||
try:
|
||||
payload = service.delete_claim_item(
|
||||
claim_id=claim_id,
|
||||
item_id=item_id,
|
||||
current_user=current_user,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
|
||||
if payload is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
|
||||
return ExpenseClaimItemActionResponse(**payload)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claims/{claim_id}/items/{item_id}/attachment",
|
||||
response_model=ExpenseClaimAttachmentActionResponse,
|
||||
summary="上传费用明细附件",
|
||||
description="为草稿费用明细上传真实附件文件,并返回附件元信息与 AI 校验结果。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "报销单或费用明细不存在。",
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorResponse,
|
||||
"description": "草稿状态校验失败或文件不合法。",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def upload_expense_claim_item_attachment(
|
||||
claim_id: str,
|
||||
item_id: str,
|
||||
file: Annotated[UploadFile, File(description="待上传的附件文件。")],
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> ExpenseClaimAttachmentActionResponse:
|
||||
service = ExpenseClaimService(db)
|
||||
try:
|
||||
payload = service.upload_claim_item_attachment(
|
||||
claim_id=claim_id,
|
||||
item_id=item_id,
|
||||
filename=str(file.filename or "attachment.bin"),
|
||||
content=await file.read(),
|
||||
media_type=file.content_type,
|
||||
current_user=current_user,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
|
||||
if payload is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
|
||||
return ExpenseClaimAttachmentActionResponse(**payload)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/claims/{claim_id}/items/{item_id}/attachment/meta",
|
||||
response_model=ExpenseClaimAttachmentRead,
|
||||
summary="读取费用明细附件元信息",
|
||||
description="返回当前费用明细已上传附件的文件信息与 AI 校验结果。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "报销单、费用明细或附件不存在。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_expense_claim_item_attachment_meta(
|
||||
claim_id: str,
|
||||
item_id: str,
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> ExpenseClaimAttachmentRead:
|
||||
service = ExpenseClaimService(db)
|
||||
try:
|
||||
payload = service.get_claim_item_attachment_meta(
|
||||
claim_id=claim_id,
|
||||
item_id=item_id,
|
||||
current_user=current_user,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except FileNotFoundError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
|
||||
if payload is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
|
||||
return ExpenseClaimAttachmentRead(**payload)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/claims/{claim_id}/items/{item_id}/attachment",
|
||||
response_class=FileResponse,
|
||||
summary="读取费用明细附件内容",
|
||||
description="用于详情页预览当前费用明细已上传的附件文件。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "报销单、费用明细或附件不存在。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_expense_claim_item_attachment(
|
||||
claim_id: str,
|
||||
item_id: str,
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> FileResponse:
|
||||
service = ExpenseClaimService(db)
|
||||
try:
|
||||
payload = service.get_claim_item_attachment_content(
|
||||
claim_id=claim_id,
|
||||
item_id=item_id,
|
||||
current_user=current_user,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except FileNotFoundError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
|
||||
if payload is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
|
||||
|
||||
file_path, media_type, filename = payload
|
||||
return FileResponse(file_path, media_type=media_type, filename=filename)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/claims/{claim_id}/items/{item_id}/attachment",
|
||||
response_model=ExpenseClaimAttachmentActionResponse,
|
||||
summary="删除费用明细附件",
|
||||
description="删除草稿费用明细当前已上传的附件文件,并清空票据关联。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "报销单、费用明细或附件不存在。",
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorResponse,
|
||||
"description": "当前状态不允许删除附件。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def delete_expense_claim_item_attachment(
|
||||
claim_id: str,
|
||||
item_id: str,
|
||||
db: DbSession,
|
||||
current_user: CurrentUser,
|
||||
) -> ExpenseClaimAttachmentActionResponse:
|
||||
service = ExpenseClaimService(db)
|
||||
try:
|
||||
payload = service.delete_claim_item_attachment(
|
||||
claim_id=claim_id,
|
||||
item_id=item_id,
|
||||
current_user=current_user,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except FileNotFoundError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||||
|
||||
if payload is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Claim not found")
|
||||
return ExpenseClaimAttachmentActionResponse(**payload)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claims/{claim_id}/submit",
|
||||
response_model=ExpenseClaimRead,
|
||||
|
||||
Reference in New Issue
Block a user