Files

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} 查询未指定原生执行器"}