x
This commit is contained in:
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Dict, Tuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from database.database import get_db_session
|
from database.database import get_db_session
|
||||||
@@ -11,8 +11,11 @@ from models.database import (
|
|||||||
User,
|
User,
|
||||||
Customer,
|
Customer,
|
||||||
Supplier,
|
Supplier,
|
||||||
|
Product,
|
||||||
SalesOrder,
|
SalesOrder,
|
||||||
|
SalesOrderItem,
|
||||||
PurchaseOrder,
|
PurchaseOrder,
|
||||||
|
PurchaseOrderItem,
|
||||||
FinanceTransaction,
|
FinanceTransaction,
|
||||||
FinanceAllocation,
|
FinanceAllocation,
|
||||||
)
|
)
|
||||||
@@ -23,6 +26,10 @@ from .schemas import (
|
|||||||
FinanceSummaryResponse,
|
FinanceSummaryResponse,
|
||||||
ReceivableItemResponse,
|
ReceivableItemResponse,
|
||||||
PayableItemResponse,
|
PayableItemResponse,
|
||||||
|
FinancePartnerStatementResponse,
|
||||||
|
PartnerStatementItemResponse,
|
||||||
|
FinancePartnerProductStatementResponse,
|
||||||
|
PartnerProductStatementItemResponse,
|
||||||
)
|
)
|
||||||
from .utils import generate_order_no
|
from .utils import generate_order_no
|
||||||
|
|
||||||
@@ -62,6 +69,31 @@ def _validate_allocation_total(transaction_amount: float, allocation_amounts: Li
|
|||||||
raise HTTPException(status_code=400, detail="核销总额不能大于单据金额")
|
raise HTTPException(status_code=400, detail="核销总额不能大于单据金额")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_period_scope(year: Optional[int], quarter: Optional[int]) -> Tuple[int, Optional[int], str, datetime, datetime]:
|
||||||
|
now = datetime.now()
|
||||||
|
selected_year = year or now.year
|
||||||
|
if selected_year < 2000 or selected_year > 2100:
|
||||||
|
raise HTTPException(status_code=400, detail="年份超出支持范围")
|
||||||
|
|
||||||
|
if quarter is not None and quarter not in [1, 2, 3, 4]:
|
||||||
|
raise HTTPException(status_code=400, detail="季度必须是1-4")
|
||||||
|
|
||||||
|
if quarter is None:
|
||||||
|
period_start = datetime(selected_year, 1, 1)
|
||||||
|
period_end = datetime(selected_year + 1, 1, 1)
|
||||||
|
period_label = f"{selected_year}年"
|
||||||
|
else:
|
||||||
|
start_month = (quarter - 1) * 3 + 1
|
||||||
|
period_start = datetime(selected_year, start_month, 1)
|
||||||
|
if quarter == 4:
|
||||||
|
period_end = datetime(selected_year + 1, 1, 1)
|
||||||
|
else:
|
||||||
|
period_end = datetime(selected_year, start_month + 3, 1)
|
||||||
|
period_label = f"{selected_year}年Q{quarter}"
|
||||||
|
|
||||||
|
return selected_year, quarter, period_label, period_start, period_end
|
||||||
|
|
||||||
|
|
||||||
@router.post("/receipts", response_model=FinanceTransactionResponse, status_code=201)
|
@router.post("/receipts", response_model=FinanceTransactionResponse, status_code=201)
|
||||||
async def create_receipt(
|
async def create_receipt(
|
||||||
payload: ReceiptCreate,
|
payload: ReceiptCreate,
|
||||||
@@ -198,6 +230,8 @@ async def create_payment(
|
|||||||
async def list_transactions(
|
async def list_transactions(
|
||||||
txn_type: Optional[str] = None,
|
txn_type: Optional[str] = None,
|
||||||
status: Optional[str] = "confirmed",
|
status: Optional[str] = "confirmed",
|
||||||
|
year: Optional[int] = Query(None, ge=2000, le=2100),
|
||||||
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(20, ge=1, le=100),
|
limit: int = Query(20, ge=1, le=100),
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
@@ -212,6 +246,9 @@ async def list_transactions(
|
|||||||
query = query.where(FinanceTransaction.txn_type == txn_type)
|
query = query.where(FinanceTransaction.txn_type == txn_type)
|
||||||
if status:
|
if status:
|
||||||
query = query.where(FinanceTransaction.status == status)
|
query = query.where(FinanceTransaction.status == status)
|
||||||
|
if year is not None or quarter is not None:
|
||||||
|
_, _, _, period_start, period_end = _resolve_period_scope(year, quarter)
|
||||||
|
query = query.where(FinanceTransaction.txn_date >= period_start).where(FinanceTransaction.txn_date < period_end)
|
||||||
query = query.offset(skip).limit(limit)
|
query = query.offset(skip).limit(limit)
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
rows = result.scalars().all()
|
rows = result.scalars().all()
|
||||||
@@ -254,19 +291,23 @@ async def void_transaction(
|
|||||||
|
|
||||||
@router.get("/receivables", response_model=List[ReceivableItemResponse])
|
@router.get("/receivables", response_model=List[ReceivableItemResponse])
|
||||||
async def list_receivables(
|
async def list_receivables(
|
||||||
|
year: Optional[int] = Query(None, ge=2000, le=2100),
|
||||||
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(50, ge=1, le=200),
|
limit: int = Query(50, ge=1, le=200),
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
result = await db_session.execute(
|
query = (
|
||||||
select(SalesOrder, Customer)
|
select(SalesOrder, Customer)
|
||||||
.join(Customer, SalesOrder.customer_id == Customer.id)
|
.join(Customer, SalesOrder.customer_id == Customer.id)
|
||||||
.where((SalesOrder.total_amount - SalesOrder.received_amount) > 0)
|
.where((SalesOrder.total_amount - SalesOrder.received_amount) > 0)
|
||||||
.order_by(SalesOrder.created_at.desc())
|
.order_by(SalesOrder.created_at.desc())
|
||||||
.offset(skip)
|
|
||||||
.limit(limit)
|
|
||||||
)
|
)
|
||||||
|
if year is not None or quarter is not None:
|
||||||
|
_, _, _, period_start, period_end = _resolve_period_scope(year, quarter)
|
||||||
|
query = query.where(SalesOrder.order_date >= period_start).where(SalesOrder.order_date < period_end)
|
||||||
|
result = await db_session.execute(query.offset(skip).limit(limit))
|
||||||
rows = []
|
rows = []
|
||||||
for order, customer in result.all():
|
for order, customer in result.all():
|
||||||
receivable_amount = (order.total_amount or 0) - (order.received_amount or 0)
|
receivable_amount = (order.total_amount or 0) - (order.received_amount or 0)
|
||||||
@@ -288,19 +329,23 @@ async def list_receivables(
|
|||||||
|
|
||||||
@router.get("/payables", response_model=List[PayableItemResponse])
|
@router.get("/payables", response_model=List[PayableItemResponse])
|
||||||
async def list_payables(
|
async def list_payables(
|
||||||
|
year: Optional[int] = Query(None, ge=2000, le=2100),
|
||||||
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(50, ge=1, le=200),
|
limit: int = Query(50, ge=1, le=200),
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
result = await db_session.execute(
|
query = (
|
||||||
select(PurchaseOrder, Supplier)
|
select(PurchaseOrder, Supplier)
|
||||||
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
||||||
.where((PurchaseOrder.total_amount - PurchaseOrder.paid_amount) > 0)
|
.where((PurchaseOrder.total_amount - PurchaseOrder.paid_amount) > 0)
|
||||||
.order_by(PurchaseOrder.created_at.desc())
|
.order_by(PurchaseOrder.created_at.desc())
|
||||||
.offset(skip)
|
|
||||||
.limit(limit)
|
|
||||||
)
|
)
|
||||||
|
if year is not None or quarter is not None:
|
||||||
|
_, _, _, period_start, period_end = _resolve_period_scope(year, quarter)
|
||||||
|
query = query.where(PurchaseOrder.order_date >= period_start).where(PurchaseOrder.order_date < period_end)
|
||||||
|
result = await db_session.execute(query.offset(skip).limit(limit))
|
||||||
rows = []
|
rows = []
|
||||||
for order, supplier in result.all():
|
for order, supplier in result.all():
|
||||||
payable_amount = (order.total_amount or 0) - (order.paid_amount or 0)
|
payable_amount = (order.total_amount or 0) - (order.paid_amount or 0)
|
||||||
@@ -322,6 +367,8 @@ async def list_payables(
|
|||||||
|
|
||||||
@router.get("/summary", response_model=FinanceSummaryResponse)
|
@router.get("/summary", response_model=FinanceSummaryResponse)
|
||||||
async def get_finance_summary(
|
async def get_finance_summary(
|
||||||
|
year: Optional[int] = Query(None, ge=2000, le=2100),
|
||||||
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
):
|
):
|
||||||
@@ -350,12 +397,339 @@ async def get_finance_summary(
|
|||||||
.where(FinanceTransaction.txn_date >= month_start)
|
.where(FinanceTransaction.txn_date >= month_start)
|
||||||
) or 0
|
) or 0
|
||||||
|
|
||||||
|
selected_year, selected_quarter, period_label, period_start, period_end = _resolve_period_scope(year, quarter)
|
||||||
|
period_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 >= period_start)
|
||||||
|
.where(FinanceTransaction.txn_date < period_end)
|
||||||
|
) or 0
|
||||||
|
period_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 >= period_start)
|
||||||
|
.where(FinanceTransaction.txn_date < period_end)
|
||||||
|
) or 0
|
||||||
|
|
||||||
return FinanceSummaryResponse(
|
return FinanceSummaryResponse(
|
||||||
receivable_total=round(float(receivable_total), 2),
|
receivable_total=round(float(receivable_total), 2),
|
||||||
payable_total=round(float(payable_total), 2),
|
payable_total=round(float(payable_total), 2),
|
||||||
monthly_receipt_total=round(float(monthly_receipt_total), 2),
|
monthly_receipt_total=round(float(monthly_receipt_total), 2),
|
||||||
monthly_payment_total=round(float(monthly_payment_total), 2),
|
monthly_payment_total=round(float(monthly_payment_total), 2),
|
||||||
|
selected_year=selected_year,
|
||||||
|
selected_quarter=selected_quarter,
|
||||||
|
period_label=period_label,
|
||||||
|
period_receipt_total=round(float(period_receipt_total), 2),
|
||||||
|
period_payment_total=round(float(period_payment_total), 2),
|
||||||
overdue_receivable_count=0,
|
overdue_receivable_count=0,
|
||||||
overdue_payable_count=0,
|
overdue_payable_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/partner-statement/{partner_type}", response_model=FinancePartnerStatementResponse)
|
||||||
|
async def get_partner_statement(
|
||||||
|
partner_type: str,
|
||||||
|
year: Optional[int] = Query(None, ge=2000, le=2100),
|
||||||
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
if partner_type not in ["customer", "supplier"]:
|
||||||
|
raise HTTPException(status_code=400, detail="partner_type 必须是 customer 或 supplier")
|
||||||
|
|
||||||
|
selected_year, selected_quarter, period_label, period_start, period_end = _resolve_period_scope(year, quarter)
|
||||||
|
stats_map: Dict[int, Dict] = {}
|
||||||
|
|
||||||
|
if partner_type == "customer":
|
||||||
|
order_rows = await db_session.execute(
|
||||||
|
select(SalesOrder, Customer)
|
||||||
|
.join(Customer, SalesOrder.customer_id == Customer.id)
|
||||||
|
.where(SalesOrder.order_date >= period_start)
|
||||||
|
.where(SalesOrder.order_date < period_end)
|
||||||
|
.where(Customer.is_active == True)
|
||||||
|
)
|
||||||
|
for order, customer in order_rows.all():
|
||||||
|
partner_stat = stats_map.setdefault(
|
||||||
|
customer.id,
|
||||||
|
{
|
||||||
|
"partner_id": customer.id,
|
||||||
|
"partner_name": customer.name,
|
||||||
|
"order_count": 0,
|
||||||
|
"transaction_count": 0,
|
||||||
|
"order_total": 0.0,
|
||||||
|
"settled_total": 0.0,
|
||||||
|
"transaction_total": 0.0,
|
||||||
|
"outstanding_total": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
total_amount = float(order.total_amount or 0)
|
||||||
|
settled_amount = float(order.received_amount or 0)
|
||||||
|
outstanding = max(total_amount - settled_amount, 0.0)
|
||||||
|
partner_stat["order_count"] += 1
|
||||||
|
partner_stat["order_total"] += total_amount
|
||||||
|
partner_stat["settled_total"] += settled_amount
|
||||||
|
partner_stat["outstanding_total"] += outstanding
|
||||||
|
|
||||||
|
transaction_rows = await db_session.execute(
|
||||||
|
select(FinanceTransaction)
|
||||||
|
.where(FinanceTransaction.partner_type == "customer")
|
||||||
|
.where(FinanceTransaction.txn_type == "receipt")
|
||||||
|
.where(FinanceTransaction.status == "confirmed")
|
||||||
|
.where(FinanceTransaction.txn_date >= period_start)
|
||||||
|
.where(FinanceTransaction.txn_date < period_end)
|
||||||
|
)
|
||||||
|
for txn in transaction_rows.scalars().all():
|
||||||
|
partner_stat = stats_map.setdefault(
|
||||||
|
txn.partner_id,
|
||||||
|
{
|
||||||
|
"partner_id": txn.partner_id,
|
||||||
|
"partner_name": f"客户#{txn.partner_id}",
|
||||||
|
"order_count": 0,
|
||||||
|
"transaction_count": 0,
|
||||||
|
"order_total": 0.0,
|
||||||
|
"settled_total": 0.0,
|
||||||
|
"transaction_total": 0.0,
|
||||||
|
"outstanding_total": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
partner_stat["transaction_count"] += 1
|
||||||
|
partner_stat["transaction_total"] += float(txn.amount or 0)
|
||||||
|
else:
|
||||||
|
order_rows = await db_session.execute(
|
||||||
|
select(PurchaseOrder, Supplier)
|
||||||
|
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
||||||
|
.where(PurchaseOrder.order_date >= period_start)
|
||||||
|
.where(PurchaseOrder.order_date < period_end)
|
||||||
|
.where(Supplier.is_active == True)
|
||||||
|
)
|
||||||
|
for order, supplier in order_rows.all():
|
||||||
|
partner_stat = stats_map.setdefault(
|
||||||
|
supplier.id,
|
||||||
|
{
|
||||||
|
"partner_id": supplier.id,
|
||||||
|
"partner_name": supplier.name,
|
||||||
|
"order_count": 0,
|
||||||
|
"transaction_count": 0,
|
||||||
|
"order_total": 0.0,
|
||||||
|
"settled_total": 0.0,
|
||||||
|
"transaction_total": 0.0,
|
||||||
|
"outstanding_total": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
total_amount = float(order.total_amount or 0)
|
||||||
|
settled_amount = float(order.paid_amount or 0)
|
||||||
|
outstanding = max(total_amount - settled_amount, 0.0)
|
||||||
|
partner_stat["order_count"] += 1
|
||||||
|
partner_stat["order_total"] += total_amount
|
||||||
|
partner_stat["settled_total"] += settled_amount
|
||||||
|
partner_stat["outstanding_total"] += outstanding
|
||||||
|
|
||||||
|
transaction_rows = await db_session.execute(
|
||||||
|
select(FinanceTransaction)
|
||||||
|
.where(FinanceTransaction.partner_type == "supplier")
|
||||||
|
.where(FinanceTransaction.txn_type == "payment")
|
||||||
|
.where(FinanceTransaction.status == "confirmed")
|
||||||
|
.where(FinanceTransaction.txn_date >= period_start)
|
||||||
|
.where(FinanceTransaction.txn_date < period_end)
|
||||||
|
)
|
||||||
|
for txn in transaction_rows.scalars().all():
|
||||||
|
partner_stat = stats_map.setdefault(
|
||||||
|
txn.partner_id,
|
||||||
|
{
|
||||||
|
"partner_id": txn.partner_id,
|
||||||
|
"partner_name": f"供应商#{txn.partner_id}",
|
||||||
|
"order_count": 0,
|
||||||
|
"transaction_count": 0,
|
||||||
|
"order_total": 0.0,
|
||||||
|
"settled_total": 0.0,
|
||||||
|
"transaction_total": 0.0,
|
||||||
|
"outstanding_total": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
partner_stat["transaction_count"] += 1
|
||||||
|
partner_stat["transaction_total"] += float(txn.amount or 0)
|
||||||
|
|
||||||
|
missing_partner_ids = [pid for pid, item in stats_map.items() if "#" in item["partner_name"]]
|
||||||
|
if missing_partner_ids:
|
||||||
|
if partner_type == "customer":
|
||||||
|
name_rows = await db_session.execute(
|
||||||
|
select(Customer.id, Customer.name).where(Customer.id.in_(missing_partner_ids))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
name_rows = await db_session.execute(
|
||||||
|
select(Supplier.id, Supplier.name).where(Supplier.id.in_(missing_partner_ids))
|
||||||
|
)
|
||||||
|
name_map = {row[0]: row[1] for row in name_rows.all()}
|
||||||
|
for pid in missing_partner_ids:
|
||||||
|
if pid in name_map:
|
||||||
|
stats_map[pid]["partner_name"] = name_map[pid]
|
||||||
|
|
||||||
|
items = [
|
||||||
|
PartnerStatementItemResponse(
|
||||||
|
partner_id=item["partner_id"],
|
||||||
|
partner_name=item["partner_name"],
|
||||||
|
order_count=item["order_count"],
|
||||||
|
transaction_count=item["transaction_count"],
|
||||||
|
order_total=round(float(item["order_total"]), 2),
|
||||||
|
settled_total=round(float(item["settled_total"]), 2),
|
||||||
|
transaction_total=round(float(item["transaction_total"]), 2),
|
||||||
|
outstanding_total=round(float(item["outstanding_total"]), 2),
|
||||||
|
period_year=selected_year,
|
||||||
|
period_quarter=selected_quarter,
|
||||||
|
)
|
||||||
|
for item in sorted(stats_map.values(), key=lambda x: (x["outstanding_total"], x["order_total"]), reverse=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
return FinancePartnerStatementResponse(
|
||||||
|
partner_type=partner_type,
|
||||||
|
year=selected_year,
|
||||||
|
quarter=selected_quarter,
|
||||||
|
period_label=period_label,
|
||||||
|
order_total=round(float(sum(item.order_total for item in items)), 2),
|
||||||
|
settled_total=round(float(sum(item.settled_total for item in items)), 2),
|
||||||
|
transaction_total=round(float(sum(item.transaction_total for item in items)), 2),
|
||||||
|
outstanding_total=round(float(sum(item.outstanding_total for item in items)), 2),
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/partner-product-statement/{partner_type}", response_model=FinancePartnerProductStatementResponse)
|
||||||
|
async def get_partner_product_statement(
|
||||||
|
partner_type: str,
|
||||||
|
partner_id: Optional[int] = Query(None, ge=1),
|
||||||
|
year: Optional[int] = Query(None, ge=2000, le=2100),
|
||||||
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
if partner_type not in ["customer", "supplier"]:
|
||||||
|
raise HTTPException(status_code=400, detail="partner_type 必须是 customer 或 supplier")
|
||||||
|
|
||||||
|
selected_year, selected_quarter, period_label, period_start, period_end = _resolve_period_scope(year, quarter)
|
||||||
|
stats_map: Dict[Tuple[int, int], Dict] = {}
|
||||||
|
|
||||||
|
if partner_type == "customer":
|
||||||
|
query = (
|
||||||
|
select(SalesOrderItem, SalesOrder, Product, Customer)
|
||||||
|
.join(SalesOrder, SalesOrderItem.order_id == SalesOrder.id)
|
||||||
|
.join(Product, SalesOrderItem.product_id == Product.id)
|
||||||
|
.join(Customer, SalesOrder.customer_id == Customer.id)
|
||||||
|
.where(SalesOrder.order_date >= period_start)
|
||||||
|
.where(SalesOrder.order_date < period_end)
|
||||||
|
.where(Customer.is_active == True)
|
||||||
|
)
|
||||||
|
if partner_id:
|
||||||
|
query = query.where(Customer.id == partner_id)
|
||||||
|
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
for item, order, product, customer in result.all():
|
||||||
|
map_key = (customer.id, product.id)
|
||||||
|
stat = stats_map.setdefault(
|
||||||
|
map_key,
|
||||||
|
{
|
||||||
|
"partner_id": customer.id,
|
||||||
|
"partner_name": customer.name,
|
||||||
|
"product_id": product.id,
|
||||||
|
"product_sku": product.sku,
|
||||||
|
"product_name": product.name,
|
||||||
|
"order_ids": set(),
|
||||||
|
"order_quantity": 0.0,
|
||||||
|
"order_amount": 0.0,
|
||||||
|
"settled_amount": 0.0,
|
||||||
|
"outstanding_amount": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
item_amount = float(item.amount or 0)
|
||||||
|
order_total = float(order.total_amount or 0)
|
||||||
|
order_settled = max(float(order.received_amount or 0), 0.0)
|
||||||
|
ratio = (item_amount / order_total) if order_total > 1e-9 else 0.0
|
||||||
|
item_settled = min(item_amount, order_settled * ratio)
|
||||||
|
item_outstanding = max(item_amount - item_settled, 0.0)
|
||||||
|
|
||||||
|
stat["order_ids"].add(order.id)
|
||||||
|
stat["order_quantity"] += float(item.quantity or 0)
|
||||||
|
stat["order_amount"] += item_amount
|
||||||
|
stat["settled_amount"] += item_settled
|
||||||
|
stat["outstanding_amount"] += item_outstanding
|
||||||
|
else:
|
||||||
|
query = (
|
||||||
|
select(PurchaseOrderItem, PurchaseOrder, Product, Supplier)
|
||||||
|
.join(PurchaseOrder, PurchaseOrderItem.order_id == PurchaseOrder.id)
|
||||||
|
.join(Product, PurchaseOrderItem.product_id == Product.id)
|
||||||
|
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
||||||
|
.where(PurchaseOrder.order_date >= period_start)
|
||||||
|
.where(PurchaseOrder.order_date < period_end)
|
||||||
|
.where(Supplier.is_active == True)
|
||||||
|
)
|
||||||
|
if partner_id:
|
||||||
|
query = query.where(Supplier.id == partner_id)
|
||||||
|
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
for item, order, product, supplier in result.all():
|
||||||
|
map_key = (supplier.id, product.id)
|
||||||
|
stat = stats_map.setdefault(
|
||||||
|
map_key,
|
||||||
|
{
|
||||||
|
"partner_id": supplier.id,
|
||||||
|
"partner_name": supplier.name,
|
||||||
|
"product_id": product.id,
|
||||||
|
"product_sku": product.sku,
|
||||||
|
"product_name": product.name,
|
||||||
|
"order_ids": set(),
|
||||||
|
"order_quantity": 0.0,
|
||||||
|
"order_amount": 0.0,
|
||||||
|
"settled_amount": 0.0,
|
||||||
|
"outstanding_amount": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
item_amount = float(item.amount or 0)
|
||||||
|
order_total = float(order.total_amount or 0)
|
||||||
|
order_settled = max(float(order.paid_amount or 0), 0.0)
|
||||||
|
ratio = (item_amount / order_total) if order_total > 1e-9 else 0.0
|
||||||
|
item_settled = min(item_amount, order_settled * ratio)
|
||||||
|
item_outstanding = max(item_amount - item_settled, 0.0)
|
||||||
|
|
||||||
|
stat["order_ids"].add(order.id)
|
||||||
|
stat["order_quantity"] += float(item.quantity or 0)
|
||||||
|
stat["order_amount"] += item_amount
|
||||||
|
stat["settled_amount"] += item_settled
|
||||||
|
stat["outstanding_amount"] += item_outstanding
|
||||||
|
|
||||||
|
items = [
|
||||||
|
PartnerProductStatementItemResponse(
|
||||||
|
partner_id=item["partner_id"],
|
||||||
|
partner_name=item["partner_name"],
|
||||||
|
product_id=item["product_id"],
|
||||||
|
product_sku=item["product_sku"],
|
||||||
|
product_name=item["product_name"],
|
||||||
|
order_count=len(item["order_ids"]),
|
||||||
|
order_quantity=round(float(item["order_quantity"]), 2),
|
||||||
|
order_amount=round(float(item["order_amount"]), 2),
|
||||||
|
settled_amount=round(float(item["settled_amount"]), 2),
|
||||||
|
outstanding_amount=round(float(item["outstanding_amount"]), 2),
|
||||||
|
period_year=selected_year,
|
||||||
|
period_quarter=selected_quarter,
|
||||||
|
)
|
||||||
|
for item in sorted(
|
||||||
|
stats_map.values(),
|
||||||
|
key=lambda x: (x["outstanding_amount"], x["order_amount"]),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return FinancePartnerProductStatementResponse(
|
||||||
|
partner_type=partner_type,
|
||||||
|
year=selected_year,
|
||||||
|
quarter=selected_quarter,
|
||||||
|
period_label=period_label,
|
||||||
|
partner_id=partner_id,
|
||||||
|
order_amount_total=round(float(sum(item.order_amount for item in items)), 2),
|
||||||
|
settled_amount_total=round(float(sum(item.settled_amount for item in items)), 2),
|
||||||
|
outstanding_amount_total=round(float(sum(item.outstanding_amount for item in items)), 2),
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ from .finance_schemas import (
|
|||||||
FinanceTransactionResponse,
|
FinanceTransactionResponse,
|
||||||
FinanceSummaryResponse,
|
FinanceSummaryResponse,
|
||||||
ReceivableItemResponse,
|
ReceivableItemResponse,
|
||||||
PayableItemResponse
|
PayableItemResponse,
|
||||||
|
PartnerStatementItemResponse,
|
||||||
|
FinancePartnerStatementResponse,
|
||||||
|
PartnerProductStatementItemResponse,
|
||||||
|
FinancePartnerProductStatementResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -39,4 +43,6 @@ __all__ = [
|
|||||||
"ReceiptCreate", "PaymentCreate",
|
"ReceiptCreate", "PaymentCreate",
|
||||||
"FinanceAllocationResponse", "FinanceTransactionResponse",
|
"FinanceAllocationResponse", "FinanceTransactionResponse",
|
||||||
"FinanceSummaryResponse", "ReceivableItemResponse", "PayableItemResponse",
|
"FinanceSummaryResponse", "ReceivableItemResponse", "PayableItemResponse",
|
||||||
|
"PartnerStatementItemResponse", "FinancePartnerStatementResponse",
|
||||||
|
"PartnerProductStatementItemResponse", "FinancePartnerProductStatementResponse",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ class FinanceSummaryResponse(BaseModel):
|
|||||||
payable_total: float
|
payable_total: float
|
||||||
monthly_receipt_total: float
|
monthly_receipt_total: float
|
||||||
monthly_payment_total: float
|
monthly_payment_total: float
|
||||||
|
selected_year: int
|
||||||
|
selected_quarter: Optional[int] = None
|
||||||
|
period_label: str
|
||||||
|
period_receipt_total: float = 0
|
||||||
|
period_payment_total: float = 0
|
||||||
overdue_receivable_count: int = 0
|
overdue_receivable_count: int = 0
|
||||||
overdue_payable_count: int = 0
|
overdue_payable_count: int = 0
|
||||||
|
|
||||||
@@ -92,3 +97,55 @@ class PayableItemResponse(BaseModel):
|
|||||||
paid_amount: float
|
paid_amount: float
|
||||||
payable_amount: float
|
payable_amount: float
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class PartnerStatementItemResponse(BaseModel):
|
||||||
|
partner_id: int
|
||||||
|
partner_name: str
|
||||||
|
order_count: int
|
||||||
|
transaction_count: int
|
||||||
|
order_total: float
|
||||||
|
settled_total: float
|
||||||
|
transaction_total: float
|
||||||
|
outstanding_total: float
|
||||||
|
period_year: int
|
||||||
|
period_quarter: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FinancePartnerStatementResponse(BaseModel):
|
||||||
|
partner_type: PartnerType
|
||||||
|
year: int
|
||||||
|
quarter: Optional[int] = None
|
||||||
|
period_label: str
|
||||||
|
order_total: float
|
||||||
|
settled_total: float
|
||||||
|
transaction_total: float
|
||||||
|
outstanding_total: float
|
||||||
|
items: List[PartnerStatementItemResponse] = []
|
||||||
|
|
||||||
|
|
||||||
|
class PartnerProductStatementItemResponse(BaseModel):
|
||||||
|
partner_id: int
|
||||||
|
partner_name: str
|
||||||
|
product_id: int
|
||||||
|
product_sku: Optional[str] = None
|
||||||
|
product_name: str
|
||||||
|
order_count: int
|
||||||
|
order_quantity: float
|
||||||
|
order_amount: float
|
||||||
|
settled_amount: float
|
||||||
|
outstanding_amount: float
|
||||||
|
period_year: int
|
||||||
|
period_quarter: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FinancePartnerProductStatementResponse(BaseModel):
|
||||||
|
partner_type: PartnerType
|
||||||
|
year: int
|
||||||
|
quarter: Optional[int] = None
|
||||||
|
period_label: str
|
||||||
|
partner_id: Optional[int] = None
|
||||||
|
order_amount_total: float
|
||||||
|
settled_amount_total: float
|
||||||
|
outstanding_amount_total: float
|
||||||
|
items: List[PartnerProductStatementItemResponse] = []
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
class StockMovementCreate(BaseModel):
|
class StockMovementCreate(BaseModel):
|
||||||
product_id: int
|
product_id: int
|
||||||
|
product_sku: Optional[str] = None
|
||||||
warehouse_id: int
|
warehouse_id: int
|
||||||
movement_type: str
|
movement_type: str
|
||||||
quantity: int
|
quantity: int
|
||||||
@@ -14,6 +15,8 @@ class StockMovementCreate(BaseModel):
|
|||||||
|
|
||||||
class StockMovementResponse(BaseModel):
|
class StockMovementResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
product_id: int
|
||||||
|
product_sku: Optional[str]
|
||||||
product_name: str
|
product_name: str
|
||||||
movement_type: str
|
movement_type: str
|
||||||
quantity: int
|
quantity: int
|
||||||
|
|||||||
@@ -31,10 +31,40 @@ async def create_stock_movement(
|
|||||||
):
|
):
|
||||||
if movement_data.movement_type not in ["in", "out", "adjust"]:
|
if movement_data.movement_type not in ["in", "out", "adjust"]:
|
||||||
raise HTTPException(status_code=400, detail="无效的变动类型")
|
raise HTTPException(status_code=400, detail="无效的变动类型")
|
||||||
|
if movement_data.quantity <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="数量必须大于0")
|
||||||
|
|
||||||
|
warehouse_result = await db_session.execute(
|
||||||
|
select(Warehouse)
|
||||||
|
.where(Warehouse.id == movement_data.warehouse_id)
|
||||||
|
.where(Warehouse.is_active == True)
|
||||||
|
)
|
||||||
|
warehouse = warehouse_result.scalar_one_or_none()
|
||||||
|
if not warehouse:
|
||||||
|
raise HTTPException(status_code=404, detail="仓库不存在")
|
||||||
|
|
||||||
|
product_result = await db_session.execute(
|
||||||
|
select(Product)
|
||||||
|
.where(Product.id == movement_data.product_id)
|
||||||
|
.where(Product.is_active == True)
|
||||||
|
)
|
||||||
|
product = product_result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
sku_candidate = movement_data.product_sku or str(movement_data.product_id)
|
||||||
|
product_by_sku_result = await db_session.execute(
|
||||||
|
select(Product)
|
||||||
|
.where(Product.sku == sku_candidate)
|
||||||
|
.where(Product.is_active == True)
|
||||||
|
)
|
||||||
|
product = product_by_sku_result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="产品不存在,请选择系统中的产品")
|
||||||
|
|
||||||
|
resolved_product_id = product.id
|
||||||
|
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
select(Inventory)
|
select(Inventory)
|
||||||
.where(Inventory.product_id == movement_data.product_id)
|
.where(Inventory.product_id == resolved_product_id)
|
||||||
.where(Inventory.warehouse_id == movement_data.warehouse_id)
|
.where(Inventory.warehouse_id == movement_data.warehouse_id)
|
||||||
)
|
)
|
||||||
inventory = result.scalar_one_or_none()
|
inventory = result.scalar_one_or_none()
|
||||||
@@ -43,7 +73,7 @@ async def create_stock_movement(
|
|||||||
if movement_data.movement_type == "out":
|
if movement_data.movement_type == "out":
|
||||||
raise HTTPException(status_code=400, detail="库存不足")
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
inventory = Inventory(
|
inventory = Inventory(
|
||||||
product_id=movement_data.product_id,
|
product_id=resolved_product_id,
|
||||||
warehouse_id=movement_data.warehouse_id,
|
warehouse_id=movement_data.warehouse_id,
|
||||||
quantity=0
|
quantity=0
|
||||||
)
|
)
|
||||||
@@ -64,7 +94,7 @@ async def create_stock_movement(
|
|||||||
after_qty = inventory.quantity
|
after_qty = inventory.quantity
|
||||||
|
|
||||||
movement = StockMovement(
|
movement = StockMovement(
|
||||||
product_id=movement_data.product_id,
|
product_id=resolved_product_id,
|
||||||
warehouse_id=movement_data.warehouse_id,
|
warehouse_id=movement_data.warehouse_id,
|
||||||
movement_type=movement_data.movement_type,
|
movement_type=movement_data.movement_type,
|
||||||
quantity=movement_data.quantity,
|
quantity=movement_data.quantity,
|
||||||
@@ -79,11 +109,10 @@ async def create_stock_movement(
|
|||||||
db_session.add(movement)
|
db_session.add(movement)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
product = await db_session.execute(select(Product).where(Product.id == movement_data.product_id))
|
|
||||||
product = product.scalar_one()
|
|
||||||
|
|
||||||
return StockMovementResponse(
|
return StockMovementResponse(
|
||||||
id=movement.id,
|
id=movement.id,
|
||||||
|
product_id=product.id,
|
||||||
|
product_sku=product.sku,
|
||||||
product_name=product.name,
|
product_name=product.name,
|
||||||
movement_type=movement.movement_type,
|
movement_type=movement.movement_type,
|
||||||
quantity=movement.quantity,
|
quantity=movement.quantity,
|
||||||
@@ -122,6 +151,8 @@ async def list_stock_movements(
|
|||||||
for movement, product in result.all():
|
for movement, product in result.all():
|
||||||
movements.append(StockMovementResponse(
|
movements.append(StockMovementResponse(
|
||||||
id=movement.id,
|
id=movement.id,
|
||||||
|
product_id=product.id,
|
||||||
|
product_sku=product.sku,
|
||||||
product_name=product.name,
|
product_name=product.name,
|
||||||
movement_type=movement.movement_type,
|
movement_type=movement.movement_type,
|
||||||
quantity=movement.quantity,
|
quantity=movement.quantity,
|
||||||
|
|||||||
+235
-20
@@ -1474,9 +1474,17 @@ const InventoryView = {
|
|||||||
activeTab: 'dashboard',
|
activeTab: 'dashboard',
|
||||||
dashboard: null,
|
dashboard: null,
|
||||||
financeSummary: null,
|
financeSummary: null,
|
||||||
|
financePeriod: {
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
quarter: ''
|
||||||
|
},
|
||||||
financeTransactions: [],
|
financeTransactions: [],
|
||||||
receivables: [],
|
receivables: [],
|
||||||
payables: [],
|
payables: [],
|
||||||
|
customerFinanceStatement: [],
|
||||||
|
supplierFinanceStatement: [],
|
||||||
|
customerProductStatement: [],
|
||||||
|
supplierProductStatement: [],
|
||||||
products: [],
|
products: [],
|
||||||
suppliers: [],
|
suppliers: [],
|
||||||
customers: [],
|
customers: [],
|
||||||
@@ -1512,6 +1520,17 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadWarehouses = async () => {
|
||||||
|
state.loading = true;
|
||||||
|
try {
|
||||||
|
state.warehouses = await apiRequest('/api/warehouses');
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '加载仓库');
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadSuppliers = async () => {
|
const loadSuppliers = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -1559,16 +1578,39 @@ const InventoryView = {
|
|||||||
const loadFinance = async () => {
|
const loadFinance = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
try {
|
try {
|
||||||
const [summary, transactions, receivables, payables] = await Promise.all([
|
const selectedYear = Number(state.financePeriod.year) || new Date().getFullYear();
|
||||||
apiRequest('/api/finance/summary'),
|
const selectedQuarter = state.financePeriod.quarter ? Number(state.financePeriod.quarter) : null;
|
||||||
apiRequest('/api/finance/transactions?status=confirmed&limit=20'),
|
const periodQuery = selectedQuarter
|
||||||
apiRequest('/api/finance/receivables?limit=20'),
|
? `year=${selectedYear}&quarter=${selectedQuarter}`
|
||||||
apiRequest('/api/finance/payables?limit=20')
|
: `year=${selectedYear}`;
|
||||||
|
|
||||||
|
const [
|
||||||
|
summary,
|
||||||
|
transactions,
|
||||||
|
receivables,
|
||||||
|
payables,
|
||||||
|
customerStatement,
|
||||||
|
supplierStatement,
|
||||||
|
customerProductStatement,
|
||||||
|
supplierProductStatement
|
||||||
|
] = await Promise.all([
|
||||||
|
apiRequest(`/api/finance/summary?${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/transactions?status=confirmed&limit=20&${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/receivables?limit=20&${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/payables?limit=20&${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/partner-statement/customer?${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/partner-statement/supplier?${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/partner-product-statement/customer?${periodQuery}`),
|
||||||
|
apiRequest(`/api/finance/partner-product-statement/supplier?${periodQuery}`)
|
||||||
]);
|
]);
|
||||||
state.financeSummary = summary;
|
state.financeSummary = summary;
|
||||||
state.financeTransactions = transactions;
|
state.financeTransactions = transactions;
|
||||||
state.receivables = receivables;
|
state.receivables = receivables;
|
||||||
state.payables = payables;
|
state.payables = payables;
|
||||||
|
state.customerFinanceStatement = customerStatement.items || [];
|
||||||
|
state.supplierFinanceStatement = supplierStatement.items || [];
|
||||||
|
state.customerProductStatement = customerProductStatement.items || [];
|
||||||
|
state.supplierProductStatement = supplierProductStatement.items || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleApiError(e, '加载财务数据');
|
handleApiError(e, '加载财务数据');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1576,6 +1618,12 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshFinanceByPeriod = () => {
|
||||||
|
if (state.activeTab === 'finance') {
|
||||||
|
loadFinance();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const switchTab = (tab) => {
|
const switchTab = (tab) => {
|
||||||
state.activeTab = tab;
|
state.activeTab = tab;
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
@@ -1589,13 +1637,26 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openModal = (type, item = null) => {
|
const openModal = async (type, item = null) => {
|
||||||
state.modalType = type;
|
state.modalType = type;
|
||||||
state.editingItem = item;
|
state.editingItem = item;
|
||||||
if (item) {
|
if (item) {
|
||||||
state.form = { ...item };
|
state.form = { ...item };
|
||||||
} else {
|
} else {
|
||||||
state.form = {};
|
state.form = {};
|
||||||
|
if (type === 'stockIn' || type === 'stockOut') {
|
||||||
|
if (!state.products.length) {
|
||||||
|
await loadProducts();
|
||||||
|
}
|
||||||
|
if (!state.warehouses.length) {
|
||||||
|
await loadWarehouses();
|
||||||
|
}
|
||||||
|
state.form = {
|
||||||
|
product_id: state.products[0]?.id || null,
|
||||||
|
warehouse_id: state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null,
|
||||||
|
quantity: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.showModal = true;
|
state.showModal = true;
|
||||||
};
|
};
|
||||||
@@ -1759,7 +1820,8 @@ const InventoryView = {
|
|||||||
saveCustomer,
|
saveCustomer,
|
||||||
deleteCustomer,
|
deleteCustomer,
|
||||||
stockIn,
|
stockIn,
|
||||||
stockOut
|
stockOut,
|
||||||
|
refreshFinanceByPeriod
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -1967,6 +2029,27 @@ const InventoryView = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'finance'">
|
<div v-else-if="state.activeTab === 'finance'">
|
||||||
|
<div class="table-container" style="margin-bottom: 16px;">
|
||||||
|
<div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<label style="margin-right:8px;">年份</label>
|
||||||
|
<input v-model.number="state.financePeriod.year" type="number" min="2000" max="2100" class="form-input" style="width:120px; display:inline-block;" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="margin-right:8px;">季度</label>
|
||||||
|
<select v-model="state.financePeriod.quarter" class="form-input" style="width:140px; display:inline-block;">
|
||||||
|
<option value="">全年</option>
|
||||||
|
<option value="1">Q1</option>
|
||||||
|
<option value="2">Q2</option>
|
||||||
|
<option value="3">Q3</option>
|
||||||
|
<option value="4">Q4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" @click="refreshFinanceByPeriod">刷新统计</button>
|
||||||
|
<span style="color:var(--text-secondary);">统计周期:{{ state.financeSummary?.period_label || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">🧾</div>
|
<div class="stat-icon">🧾</div>
|
||||||
@@ -1985,19 +2068,135 @@ const InventoryView = {
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">💵</div>
|
<div class="stat-icon">💵</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value">{{ formatCurrency(state.financeSummary?.monthly_receipt_total || 0) }}</div>
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.period_receipt_total || 0) }}</div>
|
||||||
<div class="stat-label">本月收款</div>
|
<div class="stat-label">周期收款</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">🏦</div>
|
<div class="stat-icon">🏦</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value">{{ formatCurrency(state.financeSummary?.monthly_payment_total || 0) }}</div>
|
<div class="stat-value">{{ formatCurrency(state.financeSummary?.period_payment_total || 0) }}</div>
|
||||||
<div class="stat-label">本月付款</div>
|
<div class="stat-label">周期付款</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
<th>应收余额</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in state.customerFinanceStatement" :key="'customer-' + item.partner_id">
|
||||||
|
<td>{{ item.partner_name }}</td>
|
||||||
|
<td>{{ item.order_count }}</td>
|
||||||
|
<td>{{ item.transaction_count }}</td>
|
||||||
|
<td>{{ formatCurrency(item.order_total) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.settled_total) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.transaction_total) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.outstanding_total) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</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>
|
||||||
|
<th>应付余额</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in state.supplierFinanceStatement" :key="'supplier-' + item.partner_id">
|
||||||
|
<td>{{ item.partner_name }}</td>
|
||||||
|
<td>{{ item.order_count }}</td>
|
||||||
|
<td>{{ item.transaction_count }}</td>
|
||||||
|
<td>{{ formatCurrency(item.order_total) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.settled_total) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.transaction_total) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.outstanding_total) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" style="margin-top: 16px;">
|
||||||
|
<h3 style="margin-bottom: 12px;">客户-商品追溯(周期)</h3>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>客户</th>
|
||||||
|
<th>SKU</th>
|
||||||
|
<th>商品</th>
|
||||||
|
<th>订单数</th>
|
||||||
|
<th>数量</th>
|
||||||
|
<th>订单金额</th>
|
||||||
|
<th>已结款</th>
|
||||||
|
<th>未结款</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in state.customerProductStatement" :key="'customer-product-' + item.partner_id + '-' + item.product_id">
|
||||||
|
<td>{{ item.partner_name }}</td>
|
||||||
|
<td>{{ item.product_sku || '-' }}</td>
|
||||||
|
<td>{{ item.product_name }}</td>
|
||||||
|
<td>{{ item.order_count }}</td>
|
||||||
|
<td>{{ formatNumber(item.order_quantity) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.order_amount) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.settled_amount) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.outstanding_amount) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" style="margin-top: 16px;">
|
||||||
|
<h3 style="margin-bottom: 12px;">供应商-商品追溯(周期)</h3>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>供应商</th>
|
||||||
|
<th>SKU</th>
|
||||||
|
<th>商品</th>
|
||||||
|
<th>订单数</th>
|
||||||
|
<th>数量</th>
|
||||||
|
<th>订单金额</th>
|
||||||
|
<th>已结款</th>
|
||||||
|
<th>未结款</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in state.supplierProductStatement" :key="'supplier-product-' + item.partner_id + '-' + item.product_id">
|
||||||
|
<td>{{ item.partner_name }}</td>
|
||||||
|
<td>{{ item.product_sku || '-' }}</td>
|
||||||
|
<td>{{ item.product_name }}</td>
|
||||||
|
<td>{{ item.order_count }}</td>
|
||||||
|
<td>{{ formatNumber(item.order_quantity) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.order_amount) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.settled_amount) }}</td>
|
||||||
|
<td>{{ formatCurrency(item.outstanding_amount) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-container" style="margin-top: 16px;">
|
<div class="table-container" style="margin-top: 16px;">
|
||||||
<h3 style="margin-bottom: 12px;">最近财务流水</h3>
|
<h3 style="margin-bottom: 12px;">最近财务流水</h3>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
@@ -2039,7 +2238,7 @@ const InventoryView = {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="movement in state.movements" :key="movement.id">
|
<tr v-for="movement in state.movements" :key="movement.id">
|
||||||
<td>{{ movement.product_name }}</td>
|
<td>{{ movement.product_name }}{{ movement.product_sku ? ' (' + movement.product_sku + ')' : '' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span :class="['badge', movement.movement_type === 'in' ? 'badge-success' : movement.movement_type === 'out' ? 'badge-error' : 'badge-warning']">
|
<span :class="['badge', movement.movement_type === 'in' ? 'badge-success' : movement.movement_type === 'out' ? 'badge-error' : 'badge-warning']">
|
||||||
{{ movement.movement_type === 'in' ? '入库' : movement.movement_type === 'out' ? '出库' : '调整' }}
|
{{ movement.movement_type === 'in' ? '入库' : movement.movement_type === 'out' ? '出库' : '调整' }}
|
||||||
@@ -2158,12 +2357,20 @@ const InventoryView = {
|
|||||||
<!-- 入库表单 -->
|
<!-- 入库表单 -->
|
||||||
<form v-else-if="state.modalType === 'stockIn'" @submit.prevent="stockIn">
|
<form v-else-if="state.modalType === 'stockIn'" @submit.prevent="stockIn">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">产品ID *</label>
|
<label class="form-label">产品 *</label>
|
||||||
<input v-model.number="state.form.product_id" type="number" class="form-input" required placeholder="产品ID" />
|
<select v-model.number="state.form.product_id" class="form-input" required>
|
||||||
|
<option v-for="product in state.products" :key="'stockin-product-' + product.id" :value="product.id">
|
||||||
|
{{ product.sku }} - {{ product.name }}(ID: {{ product.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">仓库ID *</label>
|
<label class="form-label">仓库 *</label>
|
||||||
<input v-model.number="state.form.warehouse_id" type="number" class="form-input" required placeholder="仓库ID (默认1)" />
|
<select v-model.number="state.form.warehouse_id" class="form-input" required>
|
||||||
|
<option v-for="warehouse in state.warehouses" :key="'stockin-warehouse-' + warehouse.id" :value="warehouse.id">
|
||||||
|
{{ warehouse.name }}{{ warehouse.code ? ' (' + warehouse.code + ')' : '' }}{{ warehouse.is_default ? ' [默认]' : '' }}(ID: {{ warehouse.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">数量 *</label>
|
<label class="form-label">数量 *</label>
|
||||||
@@ -2186,12 +2393,20 @@ const InventoryView = {
|
|||||||
<!-- 出库表单 -->
|
<!-- 出库表单 -->
|
||||||
<form v-else-if="state.modalType === 'stockOut'" @submit.prevent="stockOut">
|
<form v-else-if="state.modalType === 'stockOut'" @submit.prevent="stockOut">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">产品ID *</label>
|
<label class="form-label">产品 *</label>
|
||||||
<input v-model.number="state.form.product_id" type="number" class="form-input" required placeholder="产品ID" />
|
<select v-model.number="state.form.product_id" class="form-input" required>
|
||||||
|
<option v-for="product in state.products" :key="'stockout-product-' + product.id" :value="product.id">
|
||||||
|
{{ product.sku }} - {{ product.name }}(ID: {{ product.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">仓库ID *</label>
|
<label class="form-label">仓库 *</label>
|
||||||
<input v-model.number="state.form.warehouse_id" type="number" class="form-input" required placeholder="仓库ID (默认1)" />
|
<select v-model.number="state.form.warehouse_id" class="form-input" required>
|
||||||
|
<option v-for="warehouse in state.warehouses" :key="'stockout-warehouse-' + warehouse.id" :value="warehouse.id">
|
||||||
|
{{ warehouse.name }}{{ warehouse.code ? ' (' + warehouse.code + ')' : '' }}{{ warehouse.is_default ? ' [默认]' : '' }}(ID: {{ warehouse.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">数量 *</label>
|
<label class="form-label">数量 *</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user