Files
geMoldInsight/src/api/inventory/finance_routes.py
T
2026-03-15 15:47:43 +08:00

362 lines
13 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from typing import Optional, List
from datetime import datetime
from database.database import get_db_session
from services.auth_service import get_current_active_user
from models.database import (
User,
Customer,
Supplier,
SalesOrder,
PurchaseOrder,
FinanceTransaction,
FinanceAllocation,
)
from .schemas import (
ReceiptCreate,
PaymentCreate,
FinanceTransactionResponse,
FinanceSummaryResponse,
ReceivableItemResponse,
PayableItemResponse,
)
from .utils import generate_order_no
router = APIRouter(prefix="/finance", tags=["财务管理"])
def _build_transaction_response(txn: FinanceTransaction) -> FinanceTransactionResponse:
allocations = [
{
"id": item.id,
"order_type": item.order_type,
"order_id": item.order_id,
"allocated_amount": item.allocated_amount,
}
for item in txn.allocations
]
return FinanceTransactionResponse(
id=txn.id,
txn_no=txn.txn_no,
txn_type=txn.txn_type,
partner_type=txn.partner_type,
partner_id=txn.partner_id,
amount=txn.amount,
txn_date=txn.txn_date,
method=txn.method,
account_name=txn.account_name,
status=txn.status,
remark=txn.remark,
created_at=txn.created_at,
allocations=allocations,
)
def _validate_allocation_total(transaction_amount: float, allocation_amounts: List[float]):
allocated_total = sum(allocation_amounts)
if allocated_total - transaction_amount > 1e-6:
raise HTTPException(status_code=400, detail="核销总额不能大于单据金额")
@router.post("/receipts", response_model=FinanceTransactionResponse, status_code=201)
async def create_receipt(
payload: ReceiptCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
customer_result = await db_session.execute(
select(Customer).where(Customer.id == payload.customer_id, Customer.is_active == True)
)
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=404, detail="客户不存在")
_validate_allocation_total(payload.amount, [item.allocated_amount for item in payload.allocations])
txn = FinanceTransaction(
txn_no=generate_order_no("RC"),
txn_type="receipt",
partner_type="customer",
partner_id=payload.customer_id,
amount=payload.amount,
txn_date=payload.txn_date or datetime.now(),
method=payload.method,
account_name=payload.account_name,
status="confirmed",
remark=payload.remark,
operator_id=current_user.id,
)
db_session.add(txn)
await db_session.flush()
for allocation in payload.allocations:
if allocation.order_type != "sales":
raise HTTPException(status_code=400, detail="收款单只允许核销销售订单")
order_result = await db_session.execute(
select(SalesOrder).where(SalesOrder.id == allocation.order_id, SalesOrder.customer_id == payload.customer_id)
)
sales_order = order_result.scalar_one_or_none()
if not sales_order:
raise HTTPException(status_code=404, detail=f"销售订单不存在: {allocation.order_id}")
remaining = (sales_order.total_amount or 0) - (sales_order.received_amount or 0)
if allocation.allocated_amount - remaining > 1e-6:
raise HTTPException(status_code=400, detail=f"销售订单核销超额: {sales_order.order_no}")
db_session.add(
FinanceAllocation(
transaction_id=txn.id,
order_type="sales",
order_id=sales_order.id,
allocated_amount=allocation.allocated_amount,
)
)
sales_order.received_amount = (sales_order.received_amount or 0) + allocation.allocated_amount
await db_session.commit()
result = await db_session.execute(
select(FinanceTransaction)
.options(selectinload(FinanceTransaction.allocations))
.where(FinanceTransaction.id == txn.id)
)
created = result.scalar_one()
return _build_transaction_response(created)
@router.post("/payments", response_model=FinanceTransactionResponse, status_code=201)
async def create_payment(
payload: PaymentCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
supplier_result = await db_session.execute(
select(Supplier).where(Supplier.id == payload.supplier_id, Supplier.is_active == True)
)
supplier = supplier_result.scalar_one_or_none()
if not supplier:
raise HTTPException(status_code=404, detail="供应商不存在")
_validate_allocation_total(payload.amount, [item.allocated_amount for item in payload.allocations])
txn = FinanceTransaction(
txn_no=generate_order_no("PY"),
txn_type="payment",
partner_type="supplier",
partner_id=payload.supplier_id,
amount=payload.amount,
txn_date=payload.txn_date or datetime.now(),
method=payload.method,
account_name=payload.account_name,
status="confirmed",
remark=payload.remark,
operator_id=current_user.id,
)
db_session.add(txn)
await db_session.flush()
for allocation in payload.allocations:
if allocation.order_type != "purchase":
raise HTTPException(status_code=400, detail="付款单只允许核销采购订单")
order_result = await db_session.execute(
select(PurchaseOrder).where(PurchaseOrder.id == allocation.order_id, PurchaseOrder.supplier_id == payload.supplier_id)
)
purchase_order = order_result.scalar_one_or_none()
if not purchase_order:
raise HTTPException(status_code=404, detail=f"采购订单不存在: {allocation.order_id}")
remaining = (purchase_order.total_amount or 0) - (purchase_order.paid_amount or 0)
if allocation.allocated_amount - remaining > 1e-6:
raise HTTPException(status_code=400, detail=f"采购订单核销超额: {purchase_order.order_no}")
db_session.add(
FinanceAllocation(
transaction_id=txn.id,
order_type="purchase",
order_id=purchase_order.id,
allocated_amount=allocation.allocated_amount,
)
)
purchase_order.paid_amount = (purchase_order.paid_amount or 0) + allocation.allocated_amount
await db_session.commit()
result = await db_session.execute(
select(FinanceTransaction)
.options(selectinload(FinanceTransaction.allocations))
.where(FinanceTransaction.id == txn.id)
)
created = result.scalar_one()
return _build_transaction_response(created)
@router.get("/transactions", response_model=List[FinanceTransactionResponse])
async def list_transactions(
txn_type: Optional[str] = None,
status: Optional[str] = "confirmed",
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
query = (
select(FinanceTransaction)
.options(selectinload(FinanceTransaction.allocations))
.order_by(FinanceTransaction.created_at.desc())
)
if txn_type:
query = query.where(FinanceTransaction.txn_type == txn_type)
if status:
query = query.where(FinanceTransaction.status == status)
query = query.offset(skip).limit(limit)
result = await db_session.execute(query)
rows = result.scalars().all()
return [_build_transaction_response(item) for item in rows]
@router.post("/transactions/{transaction_id}/void")
async def void_transaction(
transaction_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
result = await db_session.execute(
select(FinanceTransaction)
.options(selectinload(FinanceTransaction.allocations))
.where(FinanceTransaction.id == transaction_id)
)
txn = result.scalar_one_or_none()
if not txn:
raise HTTPException(status_code=404, detail="财务单据不存在")
if txn.status == "voided":
return {"message": "单据已作废"}
for allocation in txn.allocations:
if allocation.order_type == "sales":
sales_result = await db_session.execute(select(SalesOrder).where(SalesOrder.id == allocation.order_id))
sales_order = sales_result.scalar_one_or_none()
if sales_order:
sales_order.received_amount = max((sales_order.received_amount or 0) - allocation.allocated_amount, 0)
elif allocation.order_type == "purchase":
purchase_result = await db_session.execute(select(PurchaseOrder).where(PurchaseOrder.id == allocation.order_id))
purchase_order = purchase_result.scalar_one_or_none()
if purchase_order:
purchase_order.paid_amount = max((purchase_order.paid_amount or 0) - allocation.allocated_amount, 0)
txn.status = "voided"
await db_session.commit()
return {"message": "单据已作废"}
@router.get("/receivables", response_model=List[ReceivableItemResponse])
async def list_receivables(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
result = await db_session.execute(
select(SalesOrder, Customer)
.join(Customer, SalesOrder.customer_id == Customer.id)
.where((SalesOrder.total_amount - SalesOrder.received_amount) > 0)
.order_by(SalesOrder.created_at.desc())
.offset(skip)
.limit(limit)
)
rows = []
for order, customer in result.all():
receivable_amount = (order.total_amount or 0) - (order.received_amount or 0)
rows.append(
ReceivableItemResponse(
order_id=order.id,
order_no=order.order_no,
customer_id=customer.id,
customer_name=customer.name,
order_date=order.order_date,
total_amount=order.total_amount or 0,
received_amount=order.received_amount or 0,
receivable_amount=receivable_amount,
status=order.status,
)
)
return rows
@router.get("/payables", response_model=List[PayableItemResponse])
async def list_payables(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
result = await db_session.execute(
select(PurchaseOrder, Supplier)
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
.where((PurchaseOrder.total_amount - PurchaseOrder.paid_amount) > 0)
.order_by(PurchaseOrder.created_at.desc())
.offset(skip)
.limit(limit)
)
rows = []
for order, supplier in result.all():
payable_amount = (order.total_amount or 0) - (order.paid_amount or 0)
rows.append(
PayableItemResponse(
order_id=order.id,
order_no=order.order_no,
supplier_id=supplier.id,
supplier_name=supplier.name,
order_date=order.order_date,
total_amount=order.total_amount or 0,
paid_amount=order.paid_amount or 0,
payable_amount=payable_amount,
status=order.status,
)
)
return rows
@router.get("/summary", response_model=FinanceSummaryResponse)
async def get_finance_summary(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
receivable_total = await db_session.scalar(
select(func.coalesce(func.sum(SalesOrder.total_amount - SalesOrder.received_amount), 0))
.where((SalesOrder.total_amount - SalesOrder.received_amount) > 0)
) or 0
payable_total = await db_session.scalar(
select(func.coalesce(func.sum(PurchaseOrder.total_amount - PurchaseOrder.paid_amount), 0))
.where((PurchaseOrder.total_amount - PurchaseOrder.paid_amount) > 0)
) or 0
now = datetime.now()
month_start = datetime(now.year, now.month, 1)
monthly_receipt_total = await db_session.scalar(
select(func.coalesce(func.sum(FinanceTransaction.amount), 0))
.where(FinanceTransaction.txn_type == "receipt")
.where(FinanceTransaction.status == "confirmed")
.where(FinanceTransaction.txn_date >= month_start)
) or 0
monthly_payment_total = await db_session.scalar(
select(func.coalesce(func.sum(FinanceTransaction.amount), 0))
.where(FinanceTransaction.txn_type == "payment")
.where(FinanceTransaction.status == "confirmed")
.where(FinanceTransaction.txn_date >= month_start)
) or 0
return FinanceSummaryResponse(
receivable_total=round(float(receivable_total), 2),
payable_total=round(float(payable_total), 2),
monthly_receipt_total=round(float(monthly_receipt_total), 2),
monthly_payment_total=round(float(monthly_payment_total), 2),
overdue_receivable_count=0,
overdue_payable_count=0,
)