feat(budget): activate P1 auto-collect + attribution subject mapping (yanxue-20260914-budget-p13p14-activation)
This commit is contained in:
@@ -78,39 +78,77 @@ def build_dimension_attribution(db: Session, kpi_id: int, period: str) -> list:
|
||||
return result
|
||||
|
||||
|
||||
def _subject_patterns(db: Session, kpi_id: int) -> list:
|
||||
"""科目取数口径解析(2026-09-14 P1③④ 激活):
|
||||
|
||||
优先用已推导的取数映射 kpi_value_sources.filter_rule.subject_codes —— 那是把
|
||||
kpi_subject_map(新准则 6xxx) 机械桥接到 voucher_details 实际科目(5xxx/1xxx/2xxx) 的结果;
|
||||
无映射时退回 kpi_subject_map 原始科目(新老科目体系不一致时明细多为 0)。
|
||||
"""
|
||||
from app.models import KPIValueSource, KPISubjectMap
|
||||
|
||||
src = db.query(KPIValueSource).filter(
|
||||
KPIValueSource.kpi_id == kpi_id,
|
||||
KPIValueSource.source_table == "voucher_details",
|
||||
KPIValueSource.status.in_(("active", "draft")),
|
||||
).order_by(KPIValueSource.status.asc()).first()
|
||||
rule = (src.filter_rule or {}) if src else {}
|
||||
labels = {m.subject_code: (m.remark or m.subject_code)
|
||||
for m in db.query(KPISubjectMap).filter(KPISubjectMap.kpi_id == kpi_id).all()}
|
||||
if rule.get("subject_codes"):
|
||||
return [{
|
||||
"code": code,
|
||||
"label": labels.get(str(code).rstrip("%"), str(code)),
|
||||
"weight": float((rule.get("weights") or {}).get(code, 1.0)),
|
||||
"measure": rule.get("measure", "credit"),
|
||||
"source": "kpi_value_sources",
|
||||
} for code in rule["subject_codes"]]
|
||||
return [{
|
||||
"code": m.subject_code,
|
||||
"label": m.remark or m.subject_code,
|
||||
"weight": float(m.weight if m.weight is not None else 1.0),
|
||||
"measure": "credit",
|
||||
"source": "kpi_subject_map",
|
||||
} for m in db.query(KPISubjectMap).filter(KPISubjectMap.kpi_id == kpi_id).all()]
|
||||
|
||||
|
||||
def build_subject_attribution(db: Session, kpi_id: int, period: str) -> list:
|
||||
"""科目明细拆解 — 查 kpi_subject_map → voucher_details 汇总科目发生额"""
|
||||
mappings = db.query(KPISubjectMap).filter(KPISubjectMap.kpi_id == kpi_id).all()
|
||||
if not mappings:
|
||||
"""科目明细拆解 — 科目口径取 _subject_patterns(优先修正后的取数映射),按 entity 隔离"""
|
||||
patterns = _subject_patterns(db, kpi_id)
|
||||
if not patterns:
|
||||
return []
|
||||
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
entity_id = kpi.entity_id if kpi else None
|
||||
|
||||
result = []
|
||||
for m in mappings:
|
||||
for p in patterns:
|
||||
q = db.query(
|
||||
func.coalesce(func.sum(VoucherDetail.debit_amount), 0),
|
||||
func.coalesce(func.sum(VoucherDetail.credit_amount), 0),
|
||||
).filter(
|
||||
VoucherDetail.subject_code == m.subject_code,
|
||||
VoucherDetail.period == period,
|
||||
)
|
||||
func.coalesce(func.sum(VoucherDetail.debit_amount), 0),
|
||||
func.count(VoucherDetail.id),
|
||||
).filter(VoucherDetail.period == period)
|
||||
if "%" in p["code"]:
|
||||
q = q.filter(VoucherDetail.subject_code.like(p["code"]))
|
||||
else:
|
||||
q = q.filter(VoucherDetail.subject_code == p["code"])
|
||||
if entity_id is not None:
|
||||
q = q.filter(VoucherDetail.entity_id == entity_id)
|
||||
row = q.first()
|
||||
debit_sum = float(row[0] or 0)
|
||||
credit_sum = float(row[1] or 0)
|
||||
# 方向: credit贷方(收入/流入) / debit借方(费用/流出)
|
||||
if m.calc_type == "ratio":
|
||||
amount = credit_sum - debit_sum
|
||||
elif m.calc_type in ("avg", "other"):
|
||||
amount = (credit_sum - debit_sum) / 2
|
||||
else: # sum
|
||||
amount = credit_sum - debit_sum
|
||||
amount = round(amount * float(m.weight or 1.0), 2)
|
||||
credit_sum = float(row[0] or 0)
|
||||
debit_sum = float(row[1] or 0)
|
||||
rows = int(row[2] or 0)
|
||||
# 口径: 损益类取贷方发生额(月结结转腿=本期发生额) / 余额类取净额(借-贷)
|
||||
amount = (debit_sum - credit_sum) if p["measure"] == "net" else credit_sum
|
||||
amount = round(amount * p["weight"], 2)
|
||||
|
||||
result.append({
|
||||
"subject_code": m.subject_code,
|
||||
"subject_name": m.remark or m.subject_code,
|
||||
"subject_code": p["code"],
|
||||
"subject_name": p["label"],
|
||||
"amount_diff": amount,
|
||||
"calc_type": m.calc_type,
|
||||
"weight": float(m.weight or 1.0),
|
||||
"calc_type": p["measure"],
|
||||
"weight": p["weight"],
|
||||
"rows": rows,
|
||||
})
|
||||
|
||||
total = sum(abs(r["amount_diff"]) for r in result) or 0
|
||||
|
||||
Reference in New Issue
Block a user