增加财务模块

This commit is contained in:
cjw
2026-03-15 15:47:43 +08:00
parent 4453c9eb9b
commit 48853a988d
7 changed files with 614 additions and 14 deletions
+2
View File
@@ -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"]
+361
View File
@@ -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,
)
+15
View File
@@ -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
+7 -3
View File
@@ -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"]},
] ]
+39
View File
@@ -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
View File
@@ -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>