feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务 - 后端: 成本分析服务、AI问答服务 - 后端: 科目映射CRUD API、分析API、QA API - 后端: 集成测试(认证/任务/凭证) 49个测试全部通过 - 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面 - 前端: AuthGuard认证守卫、Dashboard AI聊天功能 - 前端: Playwright E2E测试 16 passed, 1 skipped - 基础设施: Docker Compose、Nginx反向代理、.env.example - 文档: 用户手册、管理员手册、发布检查清单
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
凭证 API
|
||||
|
||||
提供凭证生成、查询、确认和金蝶导出接口
|
||||
"""
|
||||
|
||||
import io
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.tenant import get_current_company_id
|
||||
from app.models.voucher import Voucher, VoucherStatus
|
||||
from app.models.account_mapping import AccountMapping
|
||||
from app.services.voucher.generator import VoucherGeneratorService
|
||||
from app.services.voucher.kingdee_exporter import KingdeeExporterService
|
||||
from app.services.voucher.template import VoucherTemplate
|
||||
|
||||
router = APIRouter(prefix="/api/vouchers", tags=["凭证管理"])
|
||||
|
||||
|
||||
class VoucherEntryResponse(BaseModel):
|
||||
"""凭证分录响应"""
|
||||
account_code: str
|
||||
account_name: str
|
||||
debit_amount: float
|
||||
credit_amount: float
|
||||
summary: str
|
||||
department: str = ""
|
||||
|
||||
|
||||
class VoucherResponse(BaseModel):
|
||||
"""凭证响应"""
|
||||
id: int
|
||||
voucher_number: str
|
||||
voucher_date: str
|
||||
period: str
|
||||
summary: str
|
||||
entries: List[Dict[str, Any]]
|
||||
total_debit: float
|
||||
total_credit: float
|
||||
status: str
|
||||
confirmed_by: Optional[int] = None
|
||||
confirmed_at: Optional[str] = None
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class GenerateVoucherRequest(BaseModel):
|
||||
"""生成凭证请求"""
|
||||
task_id: int
|
||||
period: str = ""
|
||||
|
||||
|
||||
class ConfirmVoucherRequest(BaseModel):
|
||||
"""确认凭证请求"""
|
||||
user_id: int
|
||||
|
||||
|
||||
class AccountMappingResponse(BaseModel):
|
||||
"""科目映射响应"""
|
||||
id: int
|
||||
standard_field: str
|
||||
debit_account: str
|
||||
debit_account_name: str
|
||||
credit_account: str
|
||||
credit_account_name: str
|
||||
cost_center: Optional[str] = None
|
||||
is_active: bool
|
||||
|
||||
|
||||
class AccountMappingRequest(BaseModel):
|
||||
"""科目映射请求"""
|
||||
standard_field: str
|
||||
debit_account: str
|
||||
debit_account_name: str
|
||||
credit_account: str
|
||||
credit_account_name: str
|
||||
cost_center: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/generate", response_model=VoucherResponse)
|
||||
async def generate_voucher(
|
||||
request: GenerateVoucherRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> VoucherResponse:
|
||||
"""生成会计凭证"""
|
||||
service = VoucherGeneratorService(db)
|
||||
|
||||
# 检查是否已有凭证
|
||||
existing = await service.get_voucher(request.task_id)
|
||||
if existing:
|
||||
return _to_response(existing)
|
||||
|
||||
voucher = await service.generate_voucher(
|
||||
task_id=request.task_id,
|
||||
company_id=company_id,
|
||||
period=request.period,
|
||||
)
|
||||
return _to_response(voucher)
|
||||
|
||||
|
||||
@router.get("/task/{task_id}", response_model=Optional[VoucherResponse])
|
||||
async def get_task_voucher(
|
||||
task_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> Optional[VoucherResponse]:
|
||||
"""获取任务的凭证"""
|
||||
service = VoucherGeneratorService(db)
|
||||
voucher = await service.get_voucher(task_id)
|
||||
if not voucher:
|
||||
return None
|
||||
return _to_response(voucher)
|
||||
|
||||
|
||||
@router.get("/{voucher_id}", response_model=VoucherResponse)
|
||||
async def get_voucher(
|
||||
voucher_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> VoucherResponse:
|
||||
"""获取凭证详情"""
|
||||
voucher = await db.get(Voucher, voucher_id)
|
||||
if not voucher:
|
||||
raise HTTPException(status_code=404, detail="凭证不存在")
|
||||
return _to_response(voucher)
|
||||
|
||||
|
||||
@router.post("/{voucher_id}/confirm", response_model=VoucherResponse)
|
||||
async def confirm_voucher(
|
||||
voucher_id: int,
|
||||
request: ConfirmVoucherRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> VoucherResponse:
|
||||
"""确认凭证"""
|
||||
service = VoucherGeneratorService(db)
|
||||
voucher = await service.confirm_voucher(voucher_id, request.user_id)
|
||||
if not voucher:
|
||||
raise HTTPException(status_code=404, detail="凭证不存在")
|
||||
return _to_response(voucher)
|
||||
|
||||
|
||||
@router.get("/task/{task_id}/export")
|
||||
async def export_voucher(
|
||||
task_id: int,
|
||||
format: str = Query("csv", description="导出格式: csv 或 excel"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> StreamingResponse:
|
||||
"""导出凭证为金蝶格式"""
|
||||
service = KingdeeExporterService(db)
|
||||
try:
|
||||
return await service.export_task_vouchers(task_id, format)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
# ===== 科目映射管理 =====
|
||||
|
||||
@router.get("/account-mappings/list", response_model=List[AccountMappingResponse])
|
||||
async def list_account_mappings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> List[AccountMappingResponse]:
|
||||
"""获取科目映射列表"""
|
||||
result = await db.execute(
|
||||
select(AccountMapping).where(AccountMapping.company_id == company_id)
|
||||
)
|
||||
mappings = result.scalars().all()
|
||||
return [AccountMappingResponse(**m.__dict__) for m in mappings]
|
||||
|
||||
|
||||
@router.post("/account-mappings", response_model=AccountMappingResponse)
|
||||
async def create_account_mapping(
|
||||
request: AccountMappingRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> AccountMappingResponse:
|
||||
"""创建科目映射"""
|
||||
mapping = AccountMapping(
|
||||
company_id=company_id,
|
||||
standard_field=request.standard_field,
|
||||
debit_account=request.debit_account,
|
||||
debit_account_name=request.debit_account_name,
|
||||
credit_account=request.credit_account,
|
||||
credit_account_name=request.credit_account_name,
|
||||
cost_center=request.cost_center,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(mapping)
|
||||
await db.commit()
|
||||
await db.refresh(mapping)
|
||||
return AccountMappingResponse(**mapping.__dict__)
|
||||
|
||||
|
||||
@router.put("/account-mappings/{mapping_id}", response_model=AccountMappingResponse)
|
||||
async def update_account_mapping(
|
||||
mapping_id: int,
|
||||
request: AccountMappingRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> AccountMappingResponse:
|
||||
"""更新科目映射"""
|
||||
mapping = await db.get(AccountMapping, mapping_id)
|
||||
if not mapping:
|
||||
raise HTTPException(status_code=404, detail="科目映射不存在")
|
||||
|
||||
mapping.standard_field = request.standard_field
|
||||
mapping.debit_account = request.debit_account
|
||||
mapping.debit_account_name = request.debit_account_name
|
||||
mapping.credit_account = request.credit_account
|
||||
mapping.credit_account_name = request.credit_account_name
|
||||
mapping.cost_center = request.cost_center
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(mapping)
|
||||
return AccountMappingResponse(**mapping.__dict__)
|
||||
|
||||
|
||||
@router.delete("/account-mappings/{mapping_id}")
|
||||
async def delete_account_mapping(
|
||||
mapping_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
company_id: int = Depends(get_current_company_id),
|
||||
) -> Dict[str, str]:
|
||||
"""删除科目映射"""
|
||||
mapping = await db.get(AccountMapping, mapping_id)
|
||||
if not mapping:
|
||||
raise HTTPException(status_code=404, detail="科目映射不存在")
|
||||
|
||||
await db.delete(mapping)
|
||||
await db.commit()
|
||||
return {"message": "已删除"}
|
||||
|
||||
|
||||
@router.get("/templates/list")
|
||||
async def get_voucher_templates() -> Dict[str, Any]:
|
||||
"""获取默认凭证模板"""
|
||||
return VoucherTemplate.get_all_templates()
|
||||
|
||||
|
||||
def _to_response(voucher: Voucher) -> VoucherResponse:
|
||||
"""转换凭证模型为响应"""
|
||||
return VoucherResponse(
|
||||
id=voucher.id,
|
||||
voucher_number=voucher.voucher_number,
|
||||
voucher_date=voucher.voucher_date,
|
||||
period=voucher.period,
|
||||
summary=voucher.summary,
|
||||
entries=voucher.entries or [],
|
||||
total_debit=voucher.total_debit,
|
||||
total_credit=voucher.total_credit,
|
||||
status=voucher.status,
|
||||
confirmed_by=voucher.confirmed_by,
|
||||
confirmed_at=voucher.confirmed_at.isoformat() if voucher.confirmed_at else None,
|
||||
created_at=voucher.created_at.isoformat() if voucher.created_at else "",
|
||||
updated_at=voucher.updated_at.isoformat() if voucher.updated_at else "",
|
||||
)
|
||||
Reference in New Issue
Block a user