239 lines
10 KiB
Python
239 lines
10 KiB
Python
"""战略地图 · KPI 摘要接口(真数据 · 口径 CMA-A1 终稿,2026-10-19)
|
||
|
||
口径唯一真源:app/data/revenue_caliber.py(含科目映射与禁写清单)
|
||
本模块**不存任何金额常量**:全部对 voucher_details 现场聚合。
|
||
|
||
取数铁律:
|
||
1. 必经 `summary NOT LIKE '%结转%'` —— 否则损益双向抵销,收入恒为 0(历史踩坑);
|
||
2. 实体隔离:只取 token 绑定的 entity_id,不跨账套;
|
||
3. 无数据 ⇒ null + missing_reason,**不得兜底造数**;
|
||
4. 分业务线分列;禁净额;禁合并成单一「营业收入」(合计仅作可复算校验位)。
|
||
|
||
历史期缺同票折扣发票(四件套 0/4)⇒ 金额带 `data_status="estimated"` +
|
||
`data_tag="测算/待票"`,不得静默按新式重算冒充实账。
|
||
"""
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends
|
||
from sqlalchemy import text
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.auth_middleware import require_auth
|
||
from app.data import revenue_caliber as rc
|
||
from app.database import get_db
|
||
from app.deps import get_entity_id
|
||
|
||
router = APIRouter(
|
||
prefix="/api/cma/maps/summary",
|
||
tags=["战略地图摘要"],
|
||
dependencies=[Depends(require_auth)],
|
||
)
|
||
|
||
MONTH_RE = "^[0-9]{4}-[0-9]{2}$"
|
||
NOT_CARRY = "summary NOT LIKE '%结转%'"
|
||
|
||
|
||
def _wan(v) -> Optional[float]:
|
||
"""元 → 万元(2 位),None 透传。"""
|
||
if v is None:
|
||
return None
|
||
return round(float(v) / 10000.0, 2)
|
||
|
||
|
||
def _period_window(db: Session, entity_id: int):
|
||
"""取该实体最新有凭证的会计月,窗口 = 当年 01 ~ 最新月(不写死日期)。"""
|
||
latest = db.execute(text(
|
||
f"SELECT MAX(period) FROM voucher_details "
|
||
f"WHERE entity_id = :e AND period REGEXP :re AND {NOT_CARRY}"
|
||
), {"e": entity_id, "re": MONTH_RE}).scalar()
|
||
if not latest:
|
||
return None, None, None
|
||
year = str(latest)[:4]
|
||
return f"{year}-01", str(latest), year
|
||
|
||
|
||
def _sum(db: Session, sql: str, params: dict):
|
||
return db.execute(text(sql), params).scalar()
|
||
|
||
|
||
@router.get("")
|
||
def map_summary(
|
||
entity_id: int = Depends(get_entity_id),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""摘要条数据:分业务线营业收入 / 毛利率(新式)/ 管理指标 / 口径切换标识。"""
|
||
cfg = rc.SUBJECT_MAP.get(entity_id)
|
||
p0, p1, year = _period_window(db, entity_id)
|
||
period_label = f"{p0}~{p1}" if p0 else None
|
||
|
||
if cfg is None or p0 is None:
|
||
return {
|
||
"entity_id": entity_id,
|
||
"period": {"from": p0, "to": p1, "label": period_label},
|
||
"revenue": {
|
||
"available": False,
|
||
"caliber": rc.CALIBER,
|
||
"caliber_label": rc.CALIBER_LABEL,
|
||
"unit": "万元",
|
||
"lines": [],
|
||
"missing_reason": "该账套无可用凭证期" if cfg else "未配置科目映射",
|
||
},
|
||
"gross_margin": {"available": False, "value": None, "missing_reason": "无数据"},
|
||
"management": [],
|
||
"switch": rc.CALIBER_SWITCH,
|
||
"missing_docs": rc.MISSING_DOCS,
|
||
}
|
||
|
||
base = (f"entity_id = :e AND period >= :p0 AND period <= :p1 AND {NOT_CARRY}")
|
||
params = {"e": entity_id, "p0": p0, "p1": p1}
|
||
wine = rc.wine_sql("summary")
|
||
rev_prefix = cfg["revenue"] + "%"
|
||
|
||
# ── 1. 收入:主营科目按业务线分列(全额售价,禁净额)──────────────────
|
||
revenue = {
|
||
"available": False,
|
||
"caliber": rc.CALIBER,
|
||
"caliber_label": rc.CALIBER_LABEL,
|
||
"unit": "万元",
|
||
"period": period_label,
|
||
"lines": [],
|
||
}
|
||
if cfg["lines"]:
|
||
# 表 1:酒类线(keywords 命中)
|
||
wine_net = _sum(db, f"""
|
||
SELECT SUM(credit_amount - debit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx AND {wine}
|
||
""", {**params, "pfx": rev_prefix})
|
||
# 表 2:非酒类线(同一科目、不命中 keywords)
|
||
other_net = _sum(db, f"""
|
||
SELECT SUM(credit_amount - debit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx AND NOT {wine}
|
||
""", {**params, "pfx": rev_prefix})
|
||
|
||
# e1 => [酒类, 其他];e2 => [自营(IT)=非酒类, 代销(酒类)=酒类]
|
||
values = {}
|
||
if entity_id == 1:
|
||
values = {"liquor": wine_net, "other": other_net}
|
||
elif entity_id == 2:
|
||
values = {"self_it": other_net, "agency_liquor": wine_net}
|
||
|
||
lines = []
|
||
for ln in cfg["lines"]:
|
||
raw = values.get(ln["key"])
|
||
lines.append({
|
||
"key": ln["key"],
|
||
"label": ln["label"],
|
||
"amount": _wan(raw),
|
||
"unit": "万元",
|
||
"data_status": "verified" if raw is not None else "missing",
|
||
"missing_reason": None if raw is not None else "该业务线本期无入账凭证",
|
||
})
|
||
total = _wan((wine_net or 0) + (other_net or 0)) if (wine_net is not None or other_net is not None) else None
|
||
revenue = {
|
||
**revenue,
|
||
"available": any(l["amount"] is not None for l in lines),
|
||
"lines": lines,
|
||
"total": total,
|
||
"total_label": "合计(各线相加,仅作复算校验)",
|
||
"merge_forbidden": True,
|
||
"note": "各业务线全额售价分列;禁净额、禁合并为单一「营业收入」",
|
||
}
|
||
|
||
# ── 2. 毛利率:新式 (收入净额 − 折扣后净进价成本) / 收入净额 ────────────
|
||
gross_margin = {"available": False, "value": None, "missing_reason": "无数据"}
|
||
rev_total = _sum(db, f"""
|
||
SELECT SUM(credit_amount - debit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx
|
||
""", {**params, "pfx": rev_prefix})
|
||
cost_total = _sum(db, f"""
|
||
SELECT SUM(debit_amount - credit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx
|
||
""", {**params, "pfx": cfg["cost"] + "%"})
|
||
disc_total = _sum(db, f"""
|
||
SELECT SUM(credit_amount - debit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx
|
||
""", {**params, "pfx": cfg["purchase_discount"] + "%"})
|
||
|
||
if rev_total and float(rev_total) != 0 and cost_total is not None:
|
||
net_cost = float(cost_total) - float(disc_total or 0)
|
||
gm = (float(rev_total) - net_cost) / float(rev_total) * 100.0
|
||
gross_margin = {
|
||
"available": True,
|
||
"value": round(gm, 2),
|
||
"unit": "%",
|
||
"caliber": "net_purchase",
|
||
"caliber_label": "新式·折扣后净进价",
|
||
"formula": "(收入净额 − 折扣后净进价成本) / 收入净额 × 100",
|
||
"period": period_label,
|
||
# 四件套 0/4 ⇒ 历史期缺同票折扣发票,只可显示现值并带标识
|
||
"data_status": "estimated",
|
||
"data_tag": "测算/待票",
|
||
"note": "旧式(含 k 的 5001/571103 组合)已作废;历史缺票期不得冒充实账",
|
||
}
|
||
|
||
# ── 3. 管理指标(非会计科目,不进损益、不进摘要条主位)───────────────
|
||
management = []
|
||
if entity_id == 2:
|
||
# 与 A1 回执可复现口径一致:分母取**折让前贷方发生额**(639,944.00),
|
||
# 而不是 6603 净额(556,279.15,已抵扣借方)——两者差 28.24% 与 24.55% 之别。
|
||
disc = _sum(db, f"""
|
||
SELECT SUM(credit_amount - debit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx AND {wine}
|
||
""", {**params, "pfx": cfg["purchase_discount"] + "%"})
|
||
base_amt = _sum(db, f"""
|
||
SELECT SUM(credit_amount - debit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE '2202%' AND summary LIKE '%采购收货%'
|
||
AND {rc.supplier_sql('summary')}
|
||
""", params)
|
||
if disc is not None and base_amt and float(base_amt) != 0:
|
||
management.append({
|
||
"key": "vendor_discount_dependency",
|
||
"label": "厂商折扣依赖度",
|
||
"value": round(abs(float(disc)) / float(base_amt) * 100.0, 2),
|
||
"unit": "%",
|
||
"numerator": round(abs(float(disc)), 2),
|
||
"numerator_label": "酒类采购折扣额",
|
||
"denominator": round(float(base_amt), 2),
|
||
"denominator_label": "酒类采购额(含税)",
|
||
"period": period_label,
|
||
"tag": "管理指标·非会计科目",
|
||
"caliber_note": "折扣额 ÷ 酒类采购额(含税);改造单 v4 §3 文字写「÷销售额」,"
|
||
"但 28.24% 的复现分母是采购额,按可复现口径披露,分母已单列可复核",
|
||
})
|
||
elif entity_id == 1 and cfg.get("purchase_gift") and rev_total:
|
||
gift = _sum(db, f"""
|
||
SELECT SUM(debit_amount - credit_amount) FROM voucher_details
|
||
WHERE {base} AND subject_code LIKE :pfx
|
||
""", {**params, "pfx": cfg["purchase_gift"] + "%"})
|
||
if gift is not None and float(rev_total) != 0:
|
||
val = round(abs(float(gift)) / float(rev_total) * 100.0, 2)
|
||
management.append({
|
||
"key": "vendor_gift_dependency",
|
||
"label": "厂商赠酒依赖度",
|
||
"value": val,
|
||
"unit": "%",
|
||
"numerator": round(abs(float(gift)), 2),
|
||
"numerator_label": "采购获赠额",
|
||
"denominator": round(float(rev_total), 2),
|
||
"denominator_label": "营业收入(同期)",
|
||
"period": period_label,
|
||
"tag": "管理指标·非会计科目",
|
||
"abnormal": val > 100,
|
||
"caliber_note": "获赠额 ÷ 同期营业收入;>100% 属异常,须数据侧核查(不作损益行)",
|
||
})
|
||
|
||
return {
|
||
"entity_id": entity_id,
|
||
"period": {"from": p0, "to": p1, "label": period_label},
|
||
"revenue": revenue,
|
||
"gross_margin": gross_margin,
|
||
"management": management,
|
||
"switch": rc.CALIBER_SWITCH,
|
||
"missing_docs": rc.MISSING_DOCS,
|
||
"caliber_source": {
|
||
"status": rc.STATUS,
|
||
"decided_by": rc.DECIDED_BY,
|
||
"reason": rc.REASON,
|
||
},
|
||
}
|