276 lines
9.2 KiB
Python
276 lines
9.2 KiB
Python
"""报表与 BI 大盘服务:
|
|
1. 经营大盘总览 (Dashboard Summary)
|
|
2. 销售排行榜分析 (Sales Ranking)
|
|
3. 进销存综合周转与估值 (Inventory & Valuation)
|
|
4. 预置系统报表与元数据动态执行引擎
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
|
|
from django.db.models import Sum, F, Count, Q
|
|
from django.utils import timezone
|
|
|
|
from apps.sales.models import SalesBill, SalesBillLine
|
|
from apps.purchase.models import PurchaseBill
|
|
from apps.inventory.models import Stock, Warehouse
|
|
from apps.finance.models import Receivable, Payable, Account, VoucherEntry
|
|
from .models import ReportDefinition
|
|
|
|
|
|
SYSTEM_REPORTS = [
|
|
{
|
|
"code": "sales_product_ranking",
|
|
"name": "商品销售排行分析",
|
|
"category": "sales",
|
|
"description": "按商品统计销售数量、销售总额与客单均价",
|
|
"query_config": {
|
|
"group_by": "product",
|
|
"metrics": ["quantity", "total_amount"],
|
|
},
|
|
},
|
|
{
|
|
"code": "sales_customer_ranking",
|
|
"name": "客户贡献排行分析",
|
|
"category": "sales",
|
|
"description": "按客户统计采购额及订单频次",
|
|
"query_config": {
|
|
"group_by": "customer",
|
|
"metrics": ["total_amount", "bill_count"],
|
|
},
|
|
},
|
|
{
|
|
"code": "inventory_valuation",
|
|
"name": "库存商品总值分析",
|
|
"category": "inventory",
|
|
"description": "按仓库统计库存数量、均摊成本与总估值",
|
|
"query_config": {
|
|
"group_by": "warehouse",
|
|
"metrics": ["on_hand", "total_value"],
|
|
},
|
|
},
|
|
{
|
|
"code": "receivable_outstanding",
|
|
"name": "客户待收账款分析",
|
|
"category": "finance",
|
|
"description": "统计未结清应收账款及客户分布",
|
|
"query_config": {
|
|
"group_by": "customer",
|
|
"metrics": ["total_amount", "paid_amount", "balance"],
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
def init_system_reports(tenant) -> int:
|
|
"""初始化系统内置报表定义。"""
|
|
created_count = 0
|
|
for r in SYSTEM_REPORTS:
|
|
obj, was_created = ReportDefinition.objects.get_or_create(
|
|
tenant=tenant,
|
|
code=r["code"],
|
|
defaults={
|
|
"name": r["name"],
|
|
"category": r["category"],
|
|
"description": r["description"],
|
|
"is_system": True,
|
|
"query_config": r["query_config"],
|
|
},
|
|
)
|
|
if was_created:
|
|
created_count += 1
|
|
return created_count
|
|
|
|
|
|
def get_dashboard_summary(tenant) -> dict:
|
|
"""获取经销商经营中枢大盘总览指标。"""
|
|
today = date.today()
|
|
month_start = today.replace(day=1)
|
|
|
|
# 1. 销售指标 (已确认销售单)
|
|
sales_qs = SalesBill.objects.filter(tenant=tenant, state="confirmed")
|
|
month_sales_qs = sales_qs.filter(bill_date__gte=month_start)
|
|
|
|
total_sales_amount = month_sales_qs.aggregate(s=Sum("total_amount"))["s"] or Decimal("0")
|
|
total_sales_count = month_sales_qs.count()
|
|
|
|
# 2. 采购指标
|
|
purchase_qs = PurchaseBill.objects.filter(tenant=tenant, state="confirmed")
|
|
month_purchase_qs = purchase_qs.filter(bill_date__gte=month_start)
|
|
total_purchase_amount = month_purchase_qs.aggregate(s=Sum("total_amount"))["s"] or Decimal("0")
|
|
total_purchase_count = month_purchase_qs.count()
|
|
|
|
# 3. 库存指标
|
|
stock_qs = Stock.objects.filter(tenant=tenant)
|
|
sku_count = stock_qs.filter(on_hand__gt=0).values("product_id").distinct().count()
|
|
total_stock_qty = stock_qs.aggregate(s=Sum("on_hand"))["s"] or Decimal("0")
|
|
|
|
# 计算库存资产估值 = sum(on_hand * avg_cost)
|
|
inventory_val = Decimal("0")
|
|
for s in stock_qs.filter(on_hand__gt=0):
|
|
inventory_val += s.on_hand * s.avg_cost
|
|
|
|
# 4. 往来资金指标
|
|
ar_total = Receivable.objects.filter(tenant=tenant, status__in=["open", "partial"]).aggregate(
|
|
b=Sum(F("total_amount") - F("paid_amount"))
|
|
)["b"] or Decimal("0")
|
|
|
|
ap_total = Payable.objects.filter(tenant=tenant, status__in=["open", "partial"]).aggregate(
|
|
b=Sum(F("total_amount") - F("paid_amount"))
|
|
)["b"] or Decimal("0")
|
|
|
|
return {
|
|
"as_of_date": str(today),
|
|
"month_sales": {
|
|
"amount": total_sales_amount,
|
|
"count": total_sales_count,
|
|
},
|
|
"month_purchase": {
|
|
"amount": total_purchase_amount,
|
|
"count": total_purchase_count,
|
|
},
|
|
"inventory": {
|
|
"sku_count": sku_count,
|
|
"total_quantity": total_stock_qty,
|
|
"valuation": inventory_val,
|
|
},
|
|
"finance": {
|
|
"receivable_balance": ar_total,
|
|
"payable_balance": ap_total,
|
|
},
|
|
}
|
|
|
|
|
|
def get_sales_rank(
|
|
tenant,
|
|
*,
|
|
start_date: Optional[date] = None,
|
|
end_date: Optional[date] = None,
|
|
rank_by: str = "product",
|
|
top_n: int = 10,
|
|
) -> list[dict]:
|
|
"""销售排行榜:按商品 (product) 或客户 (customer) 统计销售额。"""
|
|
lines = SalesBillLine.objects.filter(
|
|
tenant=tenant,
|
|
bill__state="confirmed",
|
|
)
|
|
if start_date:
|
|
lines = lines.filter(bill__bill_date__gte=start_date)
|
|
if end_date:
|
|
lines = lines.filter(bill__bill_date__lte=end_date)
|
|
|
|
if rank_by == "customer":
|
|
agg = lines.values(
|
|
"bill__customer__id",
|
|
"bill__customer__code",
|
|
"bill__customer__name",
|
|
).annotate(
|
|
total_amount=Sum("amount"),
|
|
total_quantity=Sum("quantity"),
|
|
bill_count=Count("bill_id", distinct=True),
|
|
).order_by("-total_amount")[:top_n]
|
|
|
|
return [
|
|
{
|
|
"customer_id": row["bill__customer__id"],
|
|
"customer_code": row["bill__customer__code"],
|
|
"customer_name": row["bill__customer__name"],
|
|
"total_amount": row["total_amount"] or Decimal("0"),
|
|
"total_quantity": row["total_quantity"] or Decimal("0"),
|
|
"bill_count": row["bill_count"],
|
|
}
|
|
for row in agg
|
|
]
|
|
else:
|
|
agg = lines.values(
|
|
"product__id",
|
|
"product__code",
|
|
"product__name",
|
|
).annotate(
|
|
total_amount=Sum("amount"),
|
|
total_quantity=Sum("quantity"),
|
|
).order_by("-total_amount")[:top_n]
|
|
|
|
return [
|
|
{
|
|
"product_id": row["product__id"],
|
|
"product_code": row["product__code"],
|
|
"product_name": row["product__name"],
|
|
"total_amount": row["total_amount"] or Decimal("0"),
|
|
"total_quantity": row["total_quantity"] or Decimal("0"),
|
|
}
|
|
for row in agg
|
|
]
|
|
|
|
|
|
def get_inventory_status_report(tenant) -> dict:
|
|
"""各仓库库存分布与估值报告。"""
|
|
warehouses = Warehouse.objects.filter(tenant=tenant, is_active=True)
|
|
wh_data = []
|
|
grand_qty = Decimal("0")
|
|
grand_val = Decimal("0")
|
|
|
|
for wh in warehouses:
|
|
stocks = Stock.objects.filter(tenant=tenant, warehouse=wh, on_hand__gt=0).select_related("product")
|
|
wh_qty = Decimal("0")
|
|
wh_val = Decimal("0")
|
|
items = []
|
|
for s in stocks:
|
|
val = s.on_hand * s.avg_cost
|
|
wh_qty += s.on_hand
|
|
wh_val += val
|
|
items.append({
|
|
"product_code": s.product.code,
|
|
"product_name": s.product.name,
|
|
"on_hand": s.on_hand,
|
|
"locked": s.locked,
|
|
"avg_cost": s.avg_cost,
|
|
"valuation": val,
|
|
})
|
|
|
|
wh_data.append({
|
|
"warehouse_code": wh.code,
|
|
"warehouse_name": wh.name,
|
|
"total_quantity": wh_qty,
|
|
"total_valuation": wh_val,
|
|
"items": items,
|
|
})
|
|
grand_qty += wh_qty
|
|
grand_val += wh_val
|
|
|
|
return {
|
|
"grand_total_quantity": grand_qty,
|
|
"grand_total_valuation": grand_val,
|
|
"warehouses": wh_data,
|
|
}
|
|
|
|
|
|
def execute_report_definition(tenant, report_def: ReportDefinition, params: Optional[dict] = None) -> dict:
|
|
"""根据元数据定义执行报表查询。"""
|
|
code = report_def.code
|
|
if code == "sales_product_ranking":
|
|
return {"rows": get_sales_rank(tenant, rank_by="product")}
|
|
elif code == "sales_customer_ranking":
|
|
return {"rows": get_sales_rank(tenant, rank_by="customer")}
|
|
elif code == "inventory_valuation":
|
|
return get_inventory_status_report(tenant)
|
|
elif code == "receivable_outstanding":
|
|
recv_qs = Receivable.objects.filter(tenant=tenant, status__in=["open", "partial"]).select_related("customer")
|
|
rows = []
|
|
for r in recv_qs:
|
|
rows.append({
|
|
"bill_no": r.bill_no,
|
|
"customer_name": r.customer.name,
|
|
"customer_code": r.customer.code,
|
|
"bill_date": str(r.bill_date),
|
|
"total_amount": r.total_amount,
|
|
"paid_amount": r.paid_amount,
|
|
"balance": r.balance,
|
|
})
|
|
return {"rows": rows}
|
|
else:
|
|
return {"rows": [], "message": f"自定义报表 {code} 查询未指定原生执行器"}
|