wip(strategy-map): v4 口径落码(分线营收/新式毛利率/管理指标/C1 hold/57 归档)
This commit is contained in:
@@ -108,7 +108,10 @@ def completeness_summary(
|
||||
"display": "—",
|
||||
}
|
||||
|
||||
# ---- 目标级 · 附注口径:全量 4 图,判据 = 有KPI挂接(23-KPI 挂接 或 map_objectives.kpis 非空)----
|
||||
# ---- 目标级 · 附注口径:全量 4 图,判据 = 有 KPI 挂接 ----
|
||||
# 判据复现(definition 件 §5 机读件 20261018-c1-spec/map-objectives-dump.csv 逐行):
|
||||
# 23KPI 覆盖 26 + 非23KPI 但 map_objectives.kpis 非空 4 + 缺口 1 = 30/31。
|
||||
# 即「被 23-KPI 集挂接」与「kpis 非空」取并集;与主口径判据(仅 23-KPI 挂接)不是同一判据。
|
||||
# 附注与主数同属地图 65 的账套(全量 4 图);无地图 65 的账套不适用,展示「—」而非 0。
|
||||
footnote_total = len(rows)
|
||||
footnote_covered = sum(1 for r in rows if r["covered_23kpi"] or r["has_kpi"])
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
"""战略地图 · 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,
|
||||
},
|
||||
}
|
||||
+42
-6
@@ -11,6 +11,8 @@ from app.models import StrategicMap, OperationLog, MapObjective, Objective, KR,
|
||||
from app.utils.kr_name import sanitize_kr_name
|
||||
from app.utils.kpi_value_scope import actual_only_clause
|
||||
import copy
|
||||
# CMA-KPI-20261003 ③ 落码:考核型目标读路径分流(kpi_targets 优先;未命中回退哨兵在 utils 内)
|
||||
from app.utils.kpi_target_reader import fetch_target_map, pick_cycle_target, target_type_for_frequency
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -85,11 +87,10 @@ def list_maps(db: Session = Depends(get_db), entity_id: int = Depends(get_entity
|
||||
|
||||
@router.get("/strategy/revenue-caliber")
|
||||
def strategy_revenue_caliber(entity_id: int = Depends(get_entity_id)):
|
||||
"""战略地图摘要条 · 收入口径状态(真数据铁律:未闭合不出单一汇总数)。
|
||||
"""战略地图摘要条 · 收入口径状态(口径已闭合,金额取 /maps/summary)。
|
||||
|
||||
2026-10-18 改造单 v2 §4:A1 收入口径未闭合前,前端只显示「口径待定」+
|
||||
分业务位(酒类代销 / IT自营,amount=None ⇒ 显示「—/无数据」);
|
||||
后端不得擅自选总额/净额,也不得把两块合并成一个「营业收入」。
|
||||
2026-10-19 A1 终稿:两家均总额法;增值税销售货物 13%。
|
||||
本端点只出口径与业务线维度(不含金额);金额一律由 /maps/summary 现算。
|
||||
"""
|
||||
from app.data import revenue_caliber
|
||||
return revenue_caliber.snapshot(entity_id)
|
||||
@@ -983,6 +984,8 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).all() if all_kpi_codes else []
|
||||
kpi_map = {k.kpi_code: k for k in kpi_defs}
|
||||
# ③ 落码:考核型目标一次性取新路径(战略回顾当期)
|
||||
_tmap = fetch_target_map(db, entity_id, [k.id for k in kpi_defs])
|
||||
|
||||
# 查询最新KPI实际值
|
||||
kpi_ids = [k.id for k in kpi_defs]
|
||||
@@ -1051,10 +1054,17 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
|
||||
held = code in DATA_HOLD_KPIS
|
||||
actual = None if held else lv.get("actual_value")
|
||||
# 多粒度目标优先: 月目标(多数KPI月度) → 兼容legacy target_value (OpenCode审查#5)
|
||||
target = kpi_def.target_monthly or kpi_def.target_value
|
||||
# ③ 落码:新路径命中即用;未命中回退兼容列(哨兵在 utils,不 or 0)
|
||||
# COMPAT-FALLBACK 到期 2026-11-02:新表未命中 → 回退兼容列(顺序回退,不 or 0)
|
||||
target = pick_cycle_target(_tmap, kpi_def.id, "monthly", kpi_def.target_monthly)
|
||||
if target is None:
|
||||
target = pick_cycle_target(_tmap, kpi_def.id,
|
||||
target_type_for_frequency(kpi_def.frequency),
|
||||
kpi_def.target_value)
|
||||
# 判断红黄绿灯(绿≥90% / 黄60-90% / 红<60%)
|
||||
# 方向修正: 越低越好型(费用率/成本率/损耗/逾期/投诉/流失) 与 负值目标 (OpenCode审查#5)
|
||||
level = "gray"
|
||||
ratio_val = None
|
||||
if actual is not None and target:
|
||||
name = (kpi_def.kpi_name or "") + " " + (kpi_def.kpi_code or "")
|
||||
lower_better = any(k in name for k in
|
||||
@@ -1065,6 +1075,7 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
|
||||
ratio = actual / target
|
||||
if ratio < 0:
|
||||
ratio = 0 # 负值目标(如亏损控制) 完成度不为负
|
||||
ratio_val = ratio
|
||||
if ratio >= 0.9:
|
||||
level = "green"
|
||||
elif ratio >= 0.6:
|
||||
@@ -1074,6 +1085,11 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
|
||||
else:
|
||||
level = "gray"
|
||||
|
||||
# C1 数据前置:held KPI 不着色、不透出达成率(level=pending 由前端灰虚线呈现)
|
||||
if held:
|
||||
level = "pending"
|
||||
ratio_val = None
|
||||
|
||||
if level == "red":
|
||||
worst_level = "red"
|
||||
elif level == "yellow" and worst_level != "red":
|
||||
@@ -1087,14 +1103,29 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
|
||||
"actual_value": actual,
|
||||
"unit": kpi_def.unit,
|
||||
"level": level,
|
||||
# 达成率(按指标方向归一,None=无目标/无实测,调用方须显式显示"无数据")
|
||||
"ratio": round(ratio_val, 4) if ratio_val is not None else None,
|
||||
})
|
||||
|
||||
kpi_ratios = [k["ratio"] for k in obj_kpis if k.get("ratio") is not None]
|
||||
obj_ratio = round(sum(kpi_ratios) / len(kpi_ratios), 4) if kpi_ratios else None
|
||||
|
||||
# C1 数据前置:目标含 held KPI ⇒ 整节点「数据待核」,不计入任何达成率汇总
|
||||
hold_reason = next((DATA_HOLD_KPIS[c] for c in obj.get("kpis", []) if c in DATA_HOLD_KPIS), None)
|
||||
|
||||
obj_item = {
|
||||
"name": obj.get("name", ""),
|
||||
"icon": obj.get("icon", ""),
|
||||
"kpis": obj_kpis,
|
||||
"level": worst_level,
|
||||
"level": "pending" if hold_reason else worst_level,
|
||||
"has_data": len(obj_kpis) > 0,
|
||||
# 目标达成率 = 有实测值 KPI 的方向归一达成率均值;None ⇒ 前端显示"无数据"
|
||||
"ratio": None if hold_reason else obj_ratio,
|
||||
"kpi_total": len(obj_kpis),
|
||||
"kpi_with_data": 0 if hold_reason else len(kpi_ratios),
|
||||
"has_actual": False if hold_reason else bool(kpi_ratios),
|
||||
"data_hold": bool(hold_reason),
|
||||
"hold_reason": hold_reason,
|
||||
# 行动方案汇总(OpenCode审查#6): 按本目标KPI统计
|
||||
"action_plan_summary": {
|
||||
"total": len([p for p in action_plans_data if p["kpi_id"] in [k["kpi_id"] for k in obj_kpis]]),
|
||||
@@ -1105,6 +1136,11 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
|
||||
}
|
||||
objectives.append(obj_item)
|
||||
|
||||
# C1 数据前置:数据待核目标单列,不进达标/关注/风险任何一档
|
||||
if hold_reason:
|
||||
total_hold += 1
|
||||
continue
|
||||
|
||||
if worst_level == "green":
|
||||
total_ok += 1
|
||||
elif worst_level == "yellow":
|
||||
|
||||
@@ -1,63 +1,109 @@
|
||||
"""战略地图 · 收入口径(revenue caliber)状态 —— 单一事实源
|
||||
"""战略地图 · 收入 / 毛利 / 管理指标口径(单一事实源,勿在别处写死)
|
||||
|
||||
口径来源:改造单 `project-20261018-cma-strategy-map-ux-revamp-fullstack` 增量 v2 §4。
|
||||
截至本文件落地(2026-10-18T23:00),A1 收入口径**尚未闭合**:
|
||||
- 业务称谓「代理」≠ 会计判定(总额法 total / 净额法 net / 混合 mixed 未裁);
|
||||
- 财务净额件与项目侧「三问」总额判定并存,正由 A1 补充单裁决。
|
||||
口径来源(引用勿改):
|
||||
/srv/hermes/mailbox/finance/pending-tasks/_reports/
|
||||
project-20261018-strategy-map-data-a1-addendum-finance-v2-return.md
|
||||
改造单 project-20261018-cma-strategy-map-ux-revamp-fullstack 增量 v4
|
||||
|
||||
因此本模块只声明**状态**与**业务线维度**,不存任何金额:
|
||||
- 接口照常返回 `revenue_caliber` 与分业务位(amount=None + status=pending);
|
||||
- 前端不得写死任一方法,不得把两块合并成一个「营业收入」;
|
||||
- 口径闭合后只改本文件(status/caliber/decided_by/closed_at),并回填金额取数逻辑,
|
||||
不需要改前端。
|
||||
终稿结论(2026-10-19):
|
||||
- 陕西博海、陕西酣客 **均总额法(主要责任人)**;
|
||||
- 增值税 = 销售货物 13%(账内 11 张凭证实测,6% 代理服务零证据);
|
||||
- 9,492.6 万 = 冲减采购成本/存货成本(唯一落位,**无「补贴收入/其他收益」行**),
|
||||
本模块不产生该数、也不允许任何调用点拼出该串;
|
||||
- F2 毛利率新式 = (收入净额 − 折扣后净进价成本) / 收入净额 × 100;
|
||||
旧式(含 `k` 的 5001/571103 组合)作废,**不得在本文件或调用点复现**;
|
||||
- 历史期缺同票折扣发票(四件套 0/4)⇒ 金额只作现值展示并带「测算/待票」标识。
|
||||
|
||||
铁律:未闭合前,页面与接口都**不得**输出单一汇总收入数。
|
||||
本模块只放**口径与科目映射**,不存金额;金额一律由 API 对库内 voucher_details 现算
|
||||
(filter: summary NOT LIKE '%结转%',否则损益双向抵销归零)。
|
||||
"""
|
||||
|
||||
# 口径状态:unclosed / closed
|
||||
STATUS = "unclosed"
|
||||
CALIBER = None # closed 时取 "total" / "net" / "mixed"
|
||||
CALIBER_LABEL = "口径待定"
|
||||
DECIDED_BY = None # closed 时填裁决单/ASK 编号
|
||||
CLOSED_AT = None # ISO 时间
|
||||
REASON = (
|
||||
"A1 收入口径尚未闭合(截至 2026-10-18T23:00):业务称谓「代理」≠ 会计判定,"
|
||||
"总额法/净额法/混合法并存,待 A1 补充单裁决"
|
||||
)
|
||||
# ── 收入口径状态(v4 起为已闭合)──────────────────────────────────────────────
|
||||
STATUS = "closed"
|
||||
CALIBER = "total" # total / net / mixed
|
||||
CALIBER_LABEL = "总额法(会计)· 增值税13%"
|
||||
DECIDED_BY = "A1 终稿(project-20261018-strategy-map-data-a1-addendum-finance-v2-return.md)"
|
||||
REASON = "两家均主要责任人 ⇒ 总额法;增值税销售货物 13% 实测,无税会差异"
|
||||
|
||||
# 分业务(不得合并;amount 由 closed 后的取数逻辑回填)
|
||||
BUSINESS_LINES = [
|
||||
{"key": "liquor_agency", "label": "酒类代销"},
|
||||
{"key": "it_self", "label": "IT自营"},
|
||||
# ── 按实体的科目映射(各账套科目体系不同:酣客 5001/5401,博海 6001/6401)──────
|
||||
SUBJECT_MAP = {
|
||||
1: {
|
||||
"revenue": "5001", # 主营业务收入
|
||||
"cost": "5401", # 主营业务成本
|
||||
"purchase_discount": "530111", # 商品折扣(采购)
|
||||
"purchase_gift": "530103", # 采购获赠(捐赠收益)
|
||||
"lines": [
|
||||
{"key": "liquor", "label": "酒类"},
|
||||
{"key": "other", "label": "其他"},
|
||||
],
|
||||
},
|
||||
2: {
|
||||
"revenue": "6001", # 主营业务收入
|
||||
"cost": "6401", # 主营业务成本
|
||||
"purchase_discount": "6603", # 财务费用-现金折扣(贷方=采购折扣)
|
||||
"purchase_gift": None,
|
||||
"lines": [
|
||||
{"key": "self_it", "label": "自营(IT)"},
|
||||
{"key": "agency_liquor", "label": "代销(酒类)"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# 酒类判定(财务 A1 件同一口径):摘要含以下任一关键词 ⇒ 酒类业务线
|
||||
WINE_KEYWORDS = ("酣客", "酒窖", "酣亲", "秦莉", "牛彦平", "横山")
|
||||
|
||||
# 采购来源(博海酒类采购:陕西酣客 / 酣客酒业)
|
||||
LIQUOR_SUPPLIERS = ("酣客", "酒业")
|
||||
|
||||
|
||||
def wine_sql(column: str = "summary") -> str:
|
||||
"""SQL 片段:酒类业务线判定(与财务回执同口径,勿改)。"""
|
||||
return "(" + " OR ".join(f"{column} LIKE '%{k}%'" for k in WINE_KEYWORDS) + ")"
|
||||
|
||||
|
||||
def supplier_sql(column: str = "summary") -> str:
|
||||
return "(" + " OR ".join(f"{column} LIKE '%{k}%'" for k in LIQUOR_SUPPLIERS) + ")"
|
||||
|
||||
|
||||
# ── 口径切换期标识位(v4 §4:不硬编码日期;生效日以任总拍板 ASK 为准)──────────
|
||||
CALIBER_SWITCH = {
|
||||
"has_switch": True,
|
||||
"label": "口径变更",
|
||||
"effective_date": None, # 未拍板前恒为 None —— 禁止写死日期
|
||||
"comparability": "口径变更·不可比",
|
||||
"note": "同比/达成率/预算执行率在切换期内不可比;生效日以任总拍板(ASK)为准",
|
||||
}
|
||||
|
||||
# ── 缺件披露(v4 §5:界面不得呈现为「已齐」)─────────────────────────────────
|
||||
MISSING_DOCS = [
|
||||
"经销/代理协议",
|
||||
"厂家价格政策",
|
||||
"逐笔折扣发票+合同审批单",
|
||||
"上游退货条款",
|
||||
"发票台账(invoice_check 空)",
|
||||
]
|
||||
|
||||
|
||||
def business_lines(entity_id: int) -> list:
|
||||
cfg = SUBJECT_MAP.get(entity_id)
|
||||
return list(cfg["lines"]) if cfg else []
|
||||
|
||||
|
||||
def snapshot(entity_id: int) -> dict:
|
||||
"""返回对外口径快照(无金额;未闭合时 amount 一律 None)。"""
|
||||
"""对外口径快照(无金额;金额见 /api/cma/maps/summary)。"""
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"status": STATUS,
|
||||
"caliber": CALIBER,
|
||||
"caliber_label": CALIBER_LABEL,
|
||||
"caliber_options": [
|
||||
{"key": "total", "label": "总额法"},
|
||||
{"key": "net", "label": "净额法"},
|
||||
{"key": "mixed", "label": "混合法"},
|
||||
],
|
||||
"decided_by": DECIDED_BY,
|
||||
"closed_at": CLOSED_AT,
|
||||
"reason": REASON,
|
||||
"allow_single_total": False, # 分业务线分列;不合并成单一「营业收入」
|
||||
"businesses": [
|
||||
{
|
||||
"key": b["key"],
|
||||
"label": b["label"],
|
||||
"amount": None,
|
||||
"unit": "万元",
|
||||
"status": "pending",
|
||||
"missing_reason": "收入口径未闭合" if STATUS != "closed" else None,
|
||||
}
|
||||
for b in BUSINESS_LINES
|
||||
{"key": b["key"], "label": b["label"], "unit": "万元"}
|
||||
for b in business_lines(entity_id)
|
||||
],
|
||||
"allow_single_total": STATUS == "closed",
|
||||
"note": "未闭合前不显示单一汇总数;分业务分列,不合并为「营业收入」",
|
||||
"switch": CALIBER_SWITCH,
|
||||
"missing_docs": MISSING_DOCS,
|
||||
"note": "分业务线全额售价分列;禁净额、禁合并为单一「营业收入」",
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from dotenv import load_dotenv
|
||||
from app.database import init_db, get_db
|
||||
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json, ext_input_ledgers, treasury, doc_center, map_completeness
|
||||
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json, ext_input_ledgers, treasury, doc_center, map_completeness, map_summary
|
||||
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
||||
from scripts.erp_sync import run_sync as run_erp_sync
|
||||
from app.auth_middleware import require_auth, require_auth_401
|
||||
@@ -57,6 +57,7 @@ app.include_router(alignment.router)
|
||||
app.include_router(org.router)
|
||||
app.include_router(objectives.router)
|
||||
app.include_router(map_completeness.router) # 战略地图数据完备度(口径 CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0)
|
||||
app.include_router(map_summary.router) # 战略地图摘要条(收入分线/新式毛利率/管理指标 · 口径 CMA-A1 终稿)
|
||||
app.include_router(versions.router)
|
||||
app.include_router(budget.router)
|
||||
app.include_router(value_sources.router)
|
||||
|
||||
Reference in New Issue
Block a user