增加财务模块
This commit is contained in:
@@ -24,6 +24,7 @@ from .stock_movement_routes import router as stock_movement_router
|
|||||||
from .purchase_order_routes import router as purchase_order_router
|
from .purchase_order_routes import router as purchase_order_router
|
||||||
from .sales_order_routes import router as sales_order_router
|
from .sales_order_routes import router as sales_order_router
|
||||||
from .dashboard_routes import router as dashboard_router
|
from .dashboard_routes import router as dashboard_router
|
||||||
|
from .finance_routes import router as finance_router
|
||||||
|
|
||||||
inventory_router = APIRouter(prefix="/api", tags=["进销存"])
|
inventory_router = APIRouter(prefix="/api", tags=["进销存"])
|
||||||
|
|
||||||
@@ -36,5 +37,6 @@ inventory_router.include_router(stock_movement_router)
|
|||||||
inventory_router.include_router(purchase_order_router)
|
inventory_router.include_router(purchase_order_router)
|
||||||
inventory_router.include_router(sales_order_router)
|
inventory_router.include_router(sales_order_router)
|
||||||
inventory_router.include_router(dashboard_router)
|
inventory_router.include_router(dashboard_router)
|
||||||
|
inventory_router.include_router(finance_router)
|
||||||
|
|
||||||
__all__ = ["inventory_router"]
|
__all__ = ["inventory_router"]
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
@@ -14,6 +14,17 @@ from .sales_order_schemas import (
|
|||||||
SalesOrderResponse,
|
SalesOrderResponse,
|
||||||
SalesOrderItemCreate
|
SalesOrderItemCreate
|
||||||
)
|
)
|
||||||
|
from .finance_schemas import (
|
||||||
|
FinanceAllocationCreate,
|
||||||
|
FinanceTransactionCreate,
|
||||||
|
ReceiptCreate,
|
||||||
|
PaymentCreate,
|
||||||
|
FinanceAllocationResponse,
|
||||||
|
FinanceTransactionResponse,
|
||||||
|
FinanceSummaryResponse,
|
||||||
|
ReceivableItemResponse,
|
||||||
|
PayableItemResponse
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ProductCreate", "ProductResponse",
|
"ProductCreate", "ProductResponse",
|
||||||
@@ -24,4 +35,8 @@ __all__ = [
|
|||||||
"StockMovementCreate", "StockMovementResponse",
|
"StockMovementCreate", "StockMovementResponse",
|
||||||
"PurchaseOrderCreate", "PurchaseOrderResponse", "PurchaseOrderItemCreate",
|
"PurchaseOrderCreate", "PurchaseOrderResponse", "PurchaseOrderItemCreate",
|
||||||
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate",
|
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate",
|
||||||
|
"FinanceAllocationCreate", "FinanceTransactionCreate",
|
||||||
|
"ReceiptCreate", "PaymentCreate",
|
||||||
|
"FinanceAllocationResponse", "FinanceTransactionResponse",
|
||||||
|
"FinanceSummaryResponse", "ReceivableItemResponse", "PayableItemResponse",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List, Literal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
TxnType = Literal["receipt", "payment"]
|
||||||
|
PartnerType = Literal["customer", "supplier"]
|
||||||
|
OrderType = Literal["sales", "purchase"]
|
||||||
|
TxnStatus = Literal["confirmed", "voided"]
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceAllocationCreate(BaseModel):
|
||||||
|
order_type: OrderType
|
||||||
|
order_id: int
|
||||||
|
allocated_amount: float = Field(gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceTransactionCreate(BaseModel):
|
||||||
|
amount: float = Field(gt=0)
|
||||||
|
txn_date: Optional[datetime] = None
|
||||||
|
method: str = "bank"
|
||||||
|
account_name: Optional[str] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
allocations: List[FinanceAllocationCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiptCreate(FinanceTransactionCreate):
|
||||||
|
customer_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentCreate(FinanceTransactionCreate):
|
||||||
|
supplier_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceAllocationResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
order_type: str
|
||||||
|
order_id: int
|
||||||
|
allocated_amount: float
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceTransactionResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
txn_no: str
|
||||||
|
txn_type: TxnType
|
||||||
|
partner_type: PartnerType
|
||||||
|
partner_id: int
|
||||||
|
amount: float
|
||||||
|
txn_date: datetime
|
||||||
|
method: str
|
||||||
|
account_name: Optional[str]
|
||||||
|
status: TxnStatus
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
allocations: List[FinanceAllocationResponse] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceSummaryResponse(BaseModel):
|
||||||
|
receivable_total: float
|
||||||
|
payable_total: float
|
||||||
|
monthly_receipt_total: float
|
||||||
|
monthly_payment_total: float
|
||||||
|
overdue_receivable_count: int = 0
|
||||||
|
overdue_payable_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ReceivableItemResponse(BaseModel):
|
||||||
|
order_id: int
|
||||||
|
order_no: str
|
||||||
|
customer_id: int
|
||||||
|
customer_name: str
|
||||||
|
order_date: datetime
|
||||||
|
total_amount: float
|
||||||
|
received_amount: float
|
||||||
|
receivable_amount: float
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class PayableItemResponse(BaseModel):
|
||||||
|
order_id: int
|
||||||
|
order_no: str
|
||||||
|
supplier_id: int
|
||||||
|
supplier_name: str
|
||||||
|
order_date: datetime
|
||||||
|
total_amount: float
|
||||||
|
paid_amount: float
|
||||||
|
payable_amount: float
|
||||||
|
status: str
|
||||||
@@ -29,15 +29,19 @@ DEFAULT_PERMISSIONS = [
|
|||||||
{"code": "manage_suppliers", "name": "管理供应商", "module": "inventory"},
|
{"code": "manage_suppliers", "name": "管理供应商", "module": "inventory"},
|
||||||
{"code": "view_customers", "name": "查看客户", "module": "inventory"},
|
{"code": "view_customers", "name": "查看客户", "module": "inventory"},
|
||||||
{"code": "manage_customers", "name": "管理客户", "module": "inventory"},
|
{"code": "manage_customers", "name": "管理客户", "module": "inventory"},
|
||||||
|
{"code": "view_finance", "name": "查看财务", "module": "finance"},
|
||||||
|
{"code": "manage_receipts", "name": "管理收款", "module": "finance"},
|
||||||
|
{"code": "manage_payments", "name": "管理付款", "module": "finance"},
|
||||||
|
{"code": "void_finance_transaction", "name": "作废财务单据", "module": "finance"},
|
||||||
{"code": "view_users", "name": "查看用户", "module": "admin"},
|
{"code": "view_users", "name": "查看用户", "module": "admin"},
|
||||||
{"code": "manage_users", "name": "管理用户", "module": "admin"},
|
{"code": "manage_users", "name": "管理用户", "module": "admin"},
|
||||||
{"code": "manage_roles", "name": "管理角色", "module": "admin"},
|
{"code": "manage_roles", "name": "管理角色", "module": "admin"},
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_ROLES = [
|
DEFAULT_ROLES = [
|
||||||
{"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_users", "manage_users", "manage_roles"]},
|
{"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_finance", "manage_receipts", "manage_payments", "void_finance_transaction", "view_users", "manage_users", "manage_roles"]},
|
||||||
{"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers"]},
|
{"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance", "manage_receipts", "manage_payments"]},
|
||||||
{"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers"]},
|
{"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -681,6 +681,45 @@ class SalesOrder(Base):
|
|||||||
return f"<SalesOrder(order_no='{self.order_no}', status='{self.status}')>"
|
return f"<SalesOrder(order_no='{self.order_no}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceTransaction(Base):
|
||||||
|
__tablename__ = "finance_transactions"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
txn_no = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
|
txn_type = Column(String(20), nullable=False, index=True)
|
||||||
|
partner_type = Column(String(20), nullable=False, index=True)
|
||||||
|
partner_id = Column(Integer, nullable=False, index=True)
|
||||||
|
amount = Column(Float, nullable=False)
|
||||||
|
txn_date = Column(DateTime, default=func.now(), index=True)
|
||||||
|
method = Column(String(30), default="bank")
|
||||||
|
account_name = Column(String(100), nullable=True)
|
||||||
|
status = Column(String(20), default="confirmed", index=True)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now(), index=True)
|
||||||
|
|
||||||
|
allocations = relationship("FinanceAllocation", back_populates="transaction", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<FinanceTransaction(txn_no='{self.txn_no}', txn_type='{self.txn_type}', amount={self.amount})>"
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceAllocation(Base):
|
||||||
|
__tablename__ = "finance_allocations"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
transaction_id = Column(Integer, ForeignKey("finance_transactions.id"), nullable=False, index=True)
|
||||||
|
order_type = Column(String(20), nullable=False, index=True)
|
||||||
|
order_id = Column(Integer, nullable=False, index=True)
|
||||||
|
allocated_amount = Column(Float, nullable=False)
|
||||||
|
created_at = Column(DateTime, default=func.now(), index=True)
|
||||||
|
|
||||||
|
transaction = relationship("FinanceTransaction", back_populates="allocations")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<FinanceAllocation(transaction_id={self.transaction_id}, order_type='{self.order_type}', amount={self.allocated_amount})>"
|
||||||
|
|
||||||
|
|
||||||
class AnalysisMetrics(Base):
|
class AnalysisMetrics(Base):
|
||||||
"""分析指标表"""
|
"""分析指标表"""
|
||||||
__tablename__ = "analysis_metrics"
|
__tablename__ = "analysis_metrics"
|
||||||
|
|||||||
+96
-11
@@ -1473,6 +1473,10 @@ const InventoryView = {
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
activeTab: 'dashboard',
|
activeTab: 'dashboard',
|
||||||
dashboard: null,
|
dashboard: null,
|
||||||
|
financeSummary: null,
|
||||||
|
financeTransactions: [],
|
||||||
|
receivables: [],
|
||||||
|
payables: [],
|
||||||
products: [],
|
products: [],
|
||||||
suppliers: [],
|
suppliers: [],
|
||||||
customers: [],
|
customers: [],
|
||||||
@@ -1552,6 +1556,26 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadFinance = async () => {
|
||||||
|
state.loading = true;
|
||||||
|
try {
|
||||||
|
const [summary, transactions, receivables, payables] = await Promise.all([
|
||||||
|
apiRequest('/api/finance/summary'),
|
||||||
|
apiRequest('/api/finance/transactions?status=confirmed&limit=20'),
|
||||||
|
apiRequest('/api/finance/receivables?limit=20'),
|
||||||
|
apiRequest('/api/finance/payables?limit=20')
|
||||||
|
]);
|
||||||
|
state.financeSummary = summary;
|
||||||
|
state.financeTransactions = transactions;
|
||||||
|
state.receivables = receivables;
|
||||||
|
state.payables = payables;
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '加载财务数据');
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const switchTab = (tab) => {
|
const switchTab = (tab) => {
|
||||||
state.activeTab = tab;
|
state.activeTab = tab;
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
@@ -1561,6 +1585,7 @@ const InventoryView = {
|
|||||||
case 'customers': loadCustomers(); break;
|
case 'customers': loadCustomers(); break;
|
||||||
case 'inventory': loadInventory(); break;
|
case 'inventory': loadInventory(); break;
|
||||||
case 'movements': loadMovements(); break;
|
case 'movements': loadMovements(); break;
|
||||||
|
case 'finance': loadFinance(); break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1589,13 +1614,13 @@ const InventoryView = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(state.form)
|
body: JSON.stringify(state.form)
|
||||||
});
|
});
|
||||||
showNotification('产品更新成功', 'success');
|
addNotification('产品更新成功', 'success');
|
||||||
} else {
|
} else {
|
||||||
await apiRequest('/api/products', {
|
await apiRequest('/api/products', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(state.form)
|
body: JSON.stringify(state.form)
|
||||||
});
|
});
|
||||||
showNotification('产品创建成功', 'success');
|
addNotification('产品创建成功', 'success');
|
||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
loadProducts();
|
loadProducts();
|
||||||
@@ -1608,7 +1633,7 @@ const InventoryView = {
|
|||||||
if (!confirm('确定要删除这个产品吗?')) return;
|
if (!confirm('确定要删除这个产品吗?')) return;
|
||||||
try {
|
try {
|
||||||
await apiRequest(`/api/products/${id}`, { method: 'DELETE' });
|
await apiRequest(`/api/products/${id}`, { method: 'DELETE' });
|
||||||
showNotification('产品已删除', 'success');
|
addNotification('产品已删除', 'success');
|
||||||
loadProducts();
|
loadProducts();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleApiError(e, '删除产品');
|
handleApiError(e, '删除产品');
|
||||||
@@ -1622,13 +1647,13 @@ const InventoryView = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(state.form)
|
body: JSON.stringify(state.form)
|
||||||
});
|
});
|
||||||
showNotification('供应商更新成功', 'success');
|
addNotification('供应商更新成功', 'success');
|
||||||
} else {
|
} else {
|
||||||
await apiRequest('/api/suppliers', {
|
await apiRequest('/api/suppliers', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(state.form)
|
body: JSON.stringify(state.form)
|
||||||
});
|
});
|
||||||
showNotification('供应商创建成功', 'success');
|
addNotification('供应商创建成功', 'success');
|
||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
loadSuppliers();
|
loadSuppliers();
|
||||||
@@ -1641,7 +1666,7 @@ const InventoryView = {
|
|||||||
if (!confirm('确定要删除这个供应商吗?')) return;
|
if (!confirm('确定要删除这个供应商吗?')) return;
|
||||||
try {
|
try {
|
||||||
await apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' });
|
await apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' });
|
||||||
showNotification('供应商已删除', 'success');
|
addNotification('供应商已删除', 'success');
|
||||||
loadSuppliers();
|
loadSuppliers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleApiError(e, '删除供应商');
|
handleApiError(e, '删除供应商');
|
||||||
@@ -1655,13 +1680,13 @@ const InventoryView = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(state.form)
|
body: JSON.stringify(state.form)
|
||||||
});
|
});
|
||||||
showNotification('客户更新成功', 'success');
|
addNotification('客户更新成功', 'success');
|
||||||
} else {
|
} else {
|
||||||
await apiRequest('/api/customers', {
|
await apiRequest('/api/customers', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(state.form)
|
body: JSON.stringify(state.form)
|
||||||
});
|
});
|
||||||
showNotification('客户创建成功', 'success');
|
addNotification('客户创建成功', 'success');
|
||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
@@ -1674,7 +1699,7 @@ const InventoryView = {
|
|||||||
if (!confirm('确定要删除这个客户吗?')) return;
|
if (!confirm('确定要删除这个客户吗?')) return;
|
||||||
try {
|
try {
|
||||||
await apiRequest(`/api/customers/${id}`, { method: 'DELETE' });
|
await apiRequest(`/api/customers/${id}`, { method: 'DELETE' });
|
||||||
showNotification('客户已删除', 'success');
|
addNotification('客户已删除', 'success');
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleApiError(e, '删除客户');
|
handleApiError(e, '删除客户');
|
||||||
@@ -1687,7 +1712,7 @@ const InventoryView = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ...state.form, movement_type: 'in' })
|
body: JSON.stringify({ ...state.form, movement_type: 'in' })
|
||||||
});
|
});
|
||||||
showNotification('入库成功', 'success');
|
addNotification('入库成功', 'success');
|
||||||
closeModal();
|
closeModal();
|
||||||
loadInventory();
|
loadInventory();
|
||||||
loadMovements();
|
loadMovements();
|
||||||
@@ -1702,7 +1727,7 @@ const InventoryView = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ...state.form, movement_type: 'out' })
|
body: JSON.stringify({ ...state.form, movement_type: 'out' })
|
||||||
});
|
});
|
||||||
showNotification('出库成功', 'success');
|
addNotification('出库成功', 'success');
|
||||||
closeModal();
|
closeModal();
|
||||||
loadInventory();
|
loadInventory();
|
||||||
loadMovements();
|
loadMovements();
|
||||||
@@ -1751,6 +1776,7 @@ const InventoryView = {
|
|||||||
<button :class="['tab', { active: state.activeTab === 'suppliers' }]" @click="switchTab('suppliers')">供应商</button>
|
<button :class="['tab', { active: state.activeTab === 'suppliers' }]" @click="switchTab('suppliers')">供应商</button>
|
||||||
<button :class="['tab', { active: state.activeTab === 'customers' }]" @click="switchTab('customers')">客户</button>
|
<button :class="['tab', { active: state.activeTab === 'customers' }]" @click="switchTab('customers')">客户</button>
|
||||||
<button :class="['tab', { active: state.activeTab === 'movements' }]" @click="switchTab('movements')">变动记录</button>
|
<button :class="['tab', { active: state.activeTab === 'movements' }]" @click="switchTab('movements')">变动记录</button>
|
||||||
|
<button :class="['tab', { active: state.activeTab === 'finance' }]" @click="switchTab('finance')">财务</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state.loading" class="loading-state">
|
<div v-if="state.loading" class="loading-state">
|
||||||
@@ -1940,6 +1966,65 @@ const InventoryView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="state.activeTab === 'finance'">
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🧾</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.receivable_total || 0) }}</div>
|
||||||
|
<div class="stat-label">应收总额</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">💸</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.payable_total || 0) }}</div>
|
||||||
|
<div class="stat-label">应付总额</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">💵</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.monthly_receipt_total || 0) }}</div>
|
||||||
|
<div class="stat-label">本月收款</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🏦</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.monthly_payment_total || 0) }}</div>
|
||||||
|
<div class="stat-label">本月付款</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" style="margin-top: 16px;">
|
||||||
|
<h3 style="margin-bottom: 12px;">最近财务流水</h3>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>单号</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>往来方</th>
|
||||||
|
<th>金额</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>日期</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="txn in state.financeTransactions" :key="txn.id">
|
||||||
|
<td>{{ txn.txn_no }}</td>
|
||||||
|
<td>{{ txn.txn_type === 'receipt' ? '收款' : '付款' }}</td>
|
||||||
|
<td>{{ txn.partner_type === 'customer' ? '客户' : '供应商' }}#{{ txn.partner_id }}</td>
|
||||||
|
<td>{{ formatCurrency(txn.amount) }}</td>
|
||||||
|
<td>{{ txn.status === 'confirmed' ? '已确认' : '已作废' }}</td>
|
||||||
|
<td>{{ formatDateTime(txn.txn_date) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'movements'" class="table-container">
|
<div v-else-if="state.activeTab === 'movements'" class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user