baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
"""报表与 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} 查询未指定原生执行器"}
|
||||
Reference in New Issue
Block a user