fix(reports): 全表真实取数——清零示例兜底(BS_DEMO/CF_DEMO/_get_demo_block_total/0.50) + 上期同比/勾稽/预测标注 + CF官方口径单位收口
ci/woodpecker/push/main Pipeline failed
ci/woodpecker/push/main Pipeline failed
This commit is contained in:
+308
-114
@@ -551,11 +551,15 @@ def get_profit_statement(
|
||||
return _build_new_format_profit(db, period, entity_id)
|
||||
|
||||
|
||||
def _build_new_format_profit(db: Session, period: str, entity_id: int = 1) -> dict:
|
||||
"""构建新30号准则五板块利润表(含附注明细)(账套隔离 2026-08-31)"""
|
||||
def _new_format_blocks(db: Session, period: str, entity_id: int = 1) -> dict:
|
||||
"""新30号准则五板块取数核心(真实科目;无数据板块 subtotal=None)
|
||||
|
||||
批① 2026-10-15:`_get_demo_block_total` 示例兜底已摘除,示例值不进对外数字。
|
||||
"""
|
||||
blocks = []
|
||||
total_net_profit = 0
|
||||
all_items_have_data = True
|
||||
blocks_with_data = 0
|
||||
|
||||
for block_key in ["operating", "investing", "financing", "tax", "discontinued"]:
|
||||
block_cfg = BLOCK_INFO[block_key]
|
||||
@@ -577,28 +581,86 @@ def _build_new_format_profit(db: Session, period: str, entity_id: int = 1) -> di
|
||||
"effective": round(amount * item_cfg["sign"], 2) if amount is not None else None,
|
||||
})
|
||||
|
||||
# Fallback: 使用PRD示例数据
|
||||
# 批① 2026-10-15:去 PRD 示例兜底 —— 无真实数据 ⇒ 小计 null(显式「无数据」),
|
||||
# 严禁示例值进入对外数字(原 `_get_demo_block_total(block_key)` 调用已摘除)。
|
||||
if not block_has_data:
|
||||
all_items_have_data = False
|
||||
block_subtotal = _get_demo_block_total(block_key)
|
||||
block_subtotal = None
|
||||
|
||||
block_result = {
|
||||
"key": block_key,
|
||||
"name": block_cfg["name"],
|
||||
"short_name": block_cfg["short_name"],
|
||||
"subtotal": round(block_subtotal, 2),
|
||||
"subtotal": round(block_subtotal, 2) if block_subtotal is not None else None,
|
||||
"subtotal_name": block_cfg["result_name"],
|
||||
"items": items,
|
||||
"expanded": True,
|
||||
"has_real_data": block_has_data,
|
||||
"data_state": "voucher" if block_has_data else "no_data",
|
||||
}
|
||||
blocks.append(block_result)
|
||||
if block_subtotal is not None:
|
||||
total_net_profit += block_subtotal
|
||||
else:
|
||||
pass # 无真实数据板块:小计为 None,不计入合计(见 reconciliation.blocks_missing)
|
||||
if block_has_data:
|
||||
blocks_with_data += 1
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"blocks": blocks,
|
||||
"net_profit": round(total_net_profit, 2),
|
||||
"all_items_have_data": all_items_have_data,
|
||||
"blocks_with_data": blocks_with_data,
|
||||
}
|
||||
|
||||
|
||||
def _build_new_format_profit(db: Session, period: str, entity_id: int = 1) -> dict:
|
||||
"""构建新30号准则五板块利润表(含附注明细 + 上期/同比;账套隔离 2026-08-31)
|
||||
|
||||
批①② 2026-10-15(cma-reports-real-data-all):
|
||||
① 去 PRD 示例兜底:无数据板块 subtotal=null + data_state=no_data(示例值零注入);
|
||||
② 上期/同比照本期取数口径实取:无上期数据 ⇒ 显式 null(不顶替、不编造);
|
||||
③ 勾稽显式:net_profit_complete / reconciliation(净利润 = 有数据板块小计之和)。
|
||||
"""
|
||||
cur = _new_format_blocks(db, period, entity_id)
|
||||
blocks = cur["blocks"]
|
||||
total_net_profit = cur["net_profit"]
|
||||
all_items_have_data = cur["all_items_have_data"]
|
||||
|
||||
# ② 上期(月):同口径实取;无上期 ⇒ None(显式 null)
|
||||
prev_period = prev_period_of(period)
|
||||
prev_core = _new_format_blocks(db, prev_period, entity_id)
|
||||
prev_by_key = {b["key"]: b for b in prev_core["blocks"]}
|
||||
for b in blocks:
|
||||
pb = prev_by_key.get(b["key"])
|
||||
prev_sub = pb["subtotal"] if (pb and pb.get("has_real_data")) else None
|
||||
b["prev_subtotal"] = prev_sub
|
||||
if b["subtotal"] is not None and prev_sub is not None:
|
||||
b["variance"] = round(b["subtotal"] - prev_sub, 2)
|
||||
b["variance_pct"] = (round((b["subtotal"] - prev_sub) / abs(prev_sub) * 100, 2)
|
||||
if prev_sub else None)
|
||||
else:
|
||||
b["variance"] = None
|
||||
b["variance_pct"] = None
|
||||
b["prev_data_state"] = "voucher" if prev_sub is not None else "no_data"
|
||||
|
||||
prev_net = prev_core["net_profit"] if prev_core["blocks_with_data"] else None
|
||||
cur_sum = round(sum(b["subtotal"] for b in blocks if b["subtotal"] is not None), 2)
|
||||
_assessable = any(b["subtotal"] is not None for b in blocks)
|
||||
reconciliation = {
|
||||
"formula": "净利润 = 五板块小计之和(缺数据板块不计入并在 blocks_missing 列出)",
|
||||
"blocks_sum": cur_sum,
|
||||
"net_profit": round(total_net_profit, 2),
|
||||
# 全板块无真实数据 ⇒ 0==0 不是「勾稽成立」,显式不可判定(禁假信号)
|
||||
"assessable": _assessable,
|
||||
"balanced": (abs(cur_sum - round(total_net_profit, 2)) < 0.01) if _assessable else None,
|
||||
"blocks_missing": [b["key"] for b in blocks if b["subtotal"] is None],
|
||||
}
|
||||
|
||||
# 附注明细(对外法定报表披露要求)
|
||||
notes = _build_profit_notes(db, period, blocks, total_net_profit, entity_id)
|
||||
|
||||
# 合计行:净利润 = 一二三+四+五
|
||||
return {
|
||||
"period": period,
|
||||
"format": "new",
|
||||
@@ -606,9 +668,21 @@ def _build_new_format_profit(db: Session, period: str, entity_id: int = 1) -> di
|
||||
"blocks": blocks,
|
||||
"net_profit": round(total_net_profit, 2),
|
||||
"net_profit_name": "净利润",
|
||||
"net_profit_complete": all_items_have_data,
|
||||
"all_items_have_data": all_items_have_data,
|
||||
"prev_period": prev_period,
|
||||
"prev": {
|
||||
"period": prev_period,
|
||||
"net_profit": prev_net,
|
||||
"blocks": {k: (v["subtotal"] if v.get("has_real_data") else None)
|
||||
for k, v in prev_by_key.items()},
|
||||
"has_real_data": bool(prev_core["blocks_with_data"]),
|
||||
},
|
||||
"reconciliation": reconciliation,
|
||||
"data_state": "voucher" if all_items_have_data else "partial_no_data",
|
||||
"demo_free": True,
|
||||
"source": "subjects/kpi_values + voucher_details 真实发生额(按 entity 隔离;本报表零示例值)",
|
||||
"notes": notes,
|
||||
"prev_period": None, # TODO: P1追溯调整
|
||||
}
|
||||
|
||||
|
||||
@@ -781,18 +855,29 @@ def mpm_calculate(
|
||||
|
||||
# 获取基准值:净利润
|
||||
net_profit = _calc_new_net_profit(db, req.period, entity_id)
|
||||
if net_profit is None:
|
||||
net_profit = 0
|
||||
|
||||
# 经营现金流(自由现金流的基准)
|
||||
operating_cf = _get_kpi_val(db, "F_OPERATING_CF", req.period, entity_id)
|
||||
|
||||
# 确定基准值
|
||||
# 确定基准值(批① 2026-10-15:原 `operating_cf or net_profit` 静默 fallback 改为显式标注)
|
||||
base_fallback = False
|
||||
if req.indicator_type == "free_cash_flow":
|
||||
base_value = operating_cf or net_profit # fallback
|
||||
if operating_cf is not None:
|
||||
base_value = operating_cf
|
||||
base_source = "F_OPERATING_CF(真实)"
|
||||
base_label = "经营现金流"
|
||||
elif net_profit is not None:
|
||||
base_value = net_profit
|
||||
base_source = "净利润(经营现金流未采集 ⇒ 退回,显式标注)"
|
||||
base_label = "经营现金流(缺 ⇒ 退回净利润)"
|
||||
base_fallback = True
|
||||
else:
|
||||
base_value = None
|
||||
base_source = "无数据(经营现金流与净利润均未采集)"
|
||||
base_label = "经营现金流"
|
||||
else:
|
||||
base_value = net_profit
|
||||
base_source = indicator_cfg["base_label"]
|
||||
base_label = indicator_cfg["base_label"]
|
||||
|
||||
# 获取调整项(来自请求或默认)
|
||||
@@ -800,7 +885,7 @@ def mpm_calculate(
|
||||
|
||||
# 自动填充调整项金额
|
||||
reconciliation_items = []
|
||||
running_total = base_value
|
||||
running_total = 0.0 if base_value is None else base_value
|
||||
|
||||
# 第一步:基准值
|
||||
reconciliation_items.append({
|
||||
@@ -808,10 +893,10 @@ def mpm_calculate(
|
||||
"code": "_base",
|
||||
"name": base_label,
|
||||
"sign": 1,
|
||||
"amount": round(base_value, 2),
|
||||
"effective": round(base_value, 2),
|
||||
"amount": round(base_value, 2) if base_value is not None else None,
|
||||
"effective": round(base_value, 2) if base_value is not None else None,
|
||||
"is_base": True,
|
||||
"running_total": round(base_value, 2),
|
||||
"running_total": round(running_total, 2),
|
||||
})
|
||||
|
||||
for adj in adjustments:
|
||||
@@ -845,22 +930,25 @@ def mpm_calculate(
|
||||
|
||||
reconciliation_items.append(item)
|
||||
|
||||
# 最终结果
|
||||
final_value = round(running_total, 2)
|
||||
# 最终结果(基准缺失 ⇒ 不给出伪合计)
|
||||
final_value = round(running_total, 2) if base_value is not None else None
|
||||
|
||||
return {
|
||||
"indicator_type": req.indicator_type,
|
||||
"indicator_name": indicator_cfg["name"],
|
||||
"indicator_desc": indicator_cfg["description"],
|
||||
"period": req.period,
|
||||
"base_value": round(base_value, 2),
|
||||
"base_value": round(base_value, 2) if base_value is not None else None,
|
||||
"base_label": base_label,
|
||||
"base_source": base_source,
|
||||
"base_fallback": base_fallback,
|
||||
"base_missing": base_value is None,
|
||||
"final_value": final_value,
|
||||
"final_label": indicator_cfg["name"],
|
||||
"adjustment_count": sum(1 for a in adjustments if a.get("checked", False)),
|
||||
"total_adjustments": len(adjustments),
|
||||
"reconciliation_items": reconciliation_items,
|
||||
"has_real_data": net_profit != 0,
|
||||
"has_real_data": net_profit is not None,
|
||||
}
|
||||
|
||||
|
||||
@@ -908,17 +996,6 @@ def _get_adjustment_value(db: Session, adj_code: str, period: str, entity_id: in
|
||||
return abs(amount) if mapping.get("sign_inverse", False) else amount
|
||||
|
||||
|
||||
def _get_demo_block_total(block_key: str) -> float:
|
||||
"""PRD示例数据 fallback"""
|
||||
demo = {
|
||||
"operating": -567883,
|
||||
"investing": 123456,
|
||||
"financing": -98765,
|
||||
"tax": -43210,
|
||||
"discontinued": 0,
|
||||
}
|
||||
return demo.get(block_key, 0)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 追溯调整: 2026年数据按新30号准则重述 (P1)
|
||||
@@ -1363,32 +1440,6 @@ BALANCE_SHEET_SECTIONS = [
|
||||
},
|
||||
]
|
||||
|
||||
# 资产负债表示例数据(博海科技, 期末/期初, 单位: 千元)
|
||||
BS_DEMO = {
|
||||
"1001|1002|1012": {"end": 850, "begin": 780},
|
||||
"1101": {"end": 250, "begin": 220},
|
||||
"1122": {"end": 1100, "begin": 1050},
|
||||
"1123": {"end": 180, "begin": 160},
|
||||
"1131": {"end": 120, "begin": 100},
|
||||
"1221": {"end": 90, "begin": 80},
|
||||
"1405": {"end": 930, "begin": 900},
|
||||
"1511": {"end": 580, "begin": 550},
|
||||
"1501": {"end": 200, "begin": 180},
|
||||
"1601|1602": {"end": 1220, "begin": 1150},
|
||||
"1701": {"end": 130, "begin": 120},
|
||||
"2001": {"end": 1500, "begin": 1300},
|
||||
"2202": {"end": 1050, "begin": 980},
|
||||
"2203": {"end": 280, "begin": 250},
|
||||
"2211": {"end": 140, "begin": 130},
|
||||
"2221": {"end": 90, "begin": 85},
|
||||
"2241": {"end": 60, "begin": 50},
|
||||
"2501": {"end": 800, "begin": 750},
|
||||
"2502": {"end": 270, "begin": 250},
|
||||
"4001": {"end": 500, "begin": 500},
|
||||
"4002": {"end": 180, "begin": 175},
|
||||
"4103|4104": {"end": 780, "begin": 820},
|
||||
}
|
||||
|
||||
BS_CATEGORY_CN = {
|
||||
"operating": "经营类",
|
||||
"investing": "投资类",
|
||||
@@ -1438,16 +1489,28 @@ def _get_bs_amount(db: Session, codes: list, period: str, entity_id: int = 1) ->
|
||||
return round(total, 2) if has_data else None
|
||||
|
||||
|
||||
def _src_label(v: dict) -> str:
|
||||
"""Excel 数据来源标签(批① 2026-10-15:示例值已清零 ⇒ 只剩 凭证 / 无数据 / KPI)"""
|
||||
if v is None:
|
||||
return ""
|
||||
if v.get("value") is None or v.get("no_data"):
|
||||
return "无数据"
|
||||
src = v.get("source") or ""
|
||||
if "kpi" in src.lower() or src.startswith("KPI"):
|
||||
return "KPI"
|
||||
return "凭证"
|
||||
|
||||
|
||||
def _bs_line_amount(db: Session, line: dict, period: str, column: str = "end", entity_id: int = 1) -> dict:
|
||||
"""单行:凭证数据优先,否则示例数据(column: end期末 / begin期初)(账套隔离 2026-08-31)"""
|
||||
"""单行:凭证明细取数;无真实数据 ⇒ value=None + no_data=True(**不注入示例值**)
|
||||
|
||||
批① 2026-10-15(cma-reports-real-data-all):原「凭证优先,否则 BS_DEMO 示例」已摘除,
|
||||
示例值一律不得进入对外数字;`is_demo` 保留为恒 False 以兼容既有前端契约。
|
||||
"""
|
||||
real = _get_bs_amount(db, line["codes"], period, entity_id)
|
||||
if real is not None:
|
||||
return {"value": real, "is_demo": False}
|
||||
key = "|".join(c for c, _ in line["codes"])
|
||||
demo = BS_DEMO.get(key)
|
||||
if demo:
|
||||
return {"value": demo.get(column, demo["end"]), "is_demo": True}
|
||||
return {"value": None, "is_demo": True}
|
||||
return {"value": real, "is_demo": False, "no_data": False, "source": "voucher_details"}
|
||||
return {"value": None, "is_demo": False, "no_data": True, "source": "none"}
|
||||
|
||||
|
||||
# ── 资产负债表取数口径(2026-09-16 重分类落地) ──
|
||||
@@ -1532,25 +1595,31 @@ def get_balance_sheet(
|
||||
total_equity_end = total_equity_begin = 0
|
||||
all_real = has_real
|
||||
|
||||
has_no_data = False
|
||||
for sec in BALANCE_SHEET_SECTIONS:
|
||||
lines = []
|
||||
sec_end = sec_begin = 0.0
|
||||
sec_real = False
|
||||
sec_begin_real = False
|
||||
for line in sec["lines"]:
|
||||
key = "|".join(c for c, _ in line["codes"])
|
||||
if has_real:
|
||||
end = {"value": _line_value(plan, key, "end"), "is_demo": False}
|
||||
begin = {"value": _line_value(plan_prev, key, "begin"), "is_demo": False}
|
||||
_end_v = _line_value(plan, key, "end")
|
||||
_beg_v = _line_value(plan_prev, key, "begin")
|
||||
end = {"value": _end_v, "is_demo": False, "no_data": _end_v is None}
|
||||
begin = {"value": _beg_v, "is_demo": False, "no_data": _beg_v is None}
|
||||
else:
|
||||
end = _bs_line_amount(db, line, period, column="end", entity_id=entity_id)
|
||||
begin = _bs_line_amount(db, line, prev_period, column="begin", entity_id=entity_id)
|
||||
if end["is_demo"] or begin["is_demo"]:
|
||||
all_real = False
|
||||
# 批① 2026-10-15:示例注入已摘除;此处改为统计「无数据」行
|
||||
if end.get("no_data") and begin.get("no_data"):
|
||||
has_no_data = True
|
||||
if end["value"] is not None:
|
||||
sec_end += end["value"]
|
||||
sec_real = True
|
||||
if begin["value"] is not None:
|
||||
sec_begin += begin["value"]
|
||||
sec_begin_real = True
|
||||
raw_key = plan["raw"].get(key)
|
||||
diag_adj = None if raw_key is None else round(
|
||||
float(plan["after"].get(key) or 0) - float(raw_key), 2)
|
||||
@@ -1561,7 +1630,9 @@ def get_balance_sheet(
|
||||
"ns_category_label": BS_CATEGORY_CN.get(line["ns_category"], line["ns_category"]),
|
||||
"end_value": round(end["value"], 2) if end["value"] is not None else None,
|
||||
"begin_value": round(begin["value"], 2) if begin["value"] is not None else None,
|
||||
"is_demo": end["is_demo"] or begin["is_demo"],
|
||||
"is_demo": False,
|
||||
"no_data": bool(end.get("no_data")) and bool(begin.get("no_data")),
|
||||
"data_state": "voucher" if (end["value"] is not None or begin["value"] is not None) else "no_data",
|
||||
"is_reclass_line": False,
|
||||
"template_gap": False,
|
||||
"raw_value": None if raw_key is None else round(float(raw_key), 2),
|
||||
@@ -1597,18 +1668,24 @@ def get_balance_sheet(
|
||||
"key": sec["key"],
|
||||
"name": sec["name"],
|
||||
"category_label": sec["category_label"],
|
||||
"subtotal_end": round(sec_end, 2),
|
||||
"subtotal_begin": round(sec_begin, 2),
|
||||
"subtotal_end": round(sec_end, 2) if sec_real else None,
|
||||
"subtotal_begin": round(sec_begin, 2) if sec_begin_real else None,
|
||||
"lines": lines,
|
||||
"has_real_data": sec_real,
|
||||
"data_state": "voucher" if sec_real else "no_data",
|
||||
})
|
||||
|
||||
inv = plan["invariants"]
|
||||
if has_real:
|
||||
balanced = abs(total_assets_end - total_liab_end - total_equity_end) < 0.01 and \
|
||||
abs(total_assets_begin - total_liab_begin - total_equity_begin) < 0.01
|
||||
balanced_note = ("期末/期初均满足:资产 = 负债 + 所有者权益" if balanced
|
||||
else "不平:资产 ≠ 负债 + 所有者权益(差异见 totals.identity_gap;"
|
||||
"改前既有真账差异,本报表如实暴露、不掩盖)")
|
||||
else:
|
||||
balanced = abs(total_assets_end - total_liab_end - total_equity_end) < 0.01
|
||||
# 批① 2026-10-15:无真实凭证数据 ⇒ 不注入示例、勾稽不可判定 ⇒ 显式 null(禁 0==0 假平衡)
|
||||
balanced = None
|
||||
balanced_note = "无真实凭证数据(no_data)⇒ 勾稽不可判定(不再以示例值凑平)"
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
@@ -1616,6 +1693,10 @@ def get_balance_sheet(
|
||||
"basis": use_basis,
|
||||
"title": f"资产负债表 — 新30号准则({period})",
|
||||
"sections": sections,
|
||||
"data_state": "voucher" if has_real else "no_data",
|
||||
"no_data_lines": has_no_data,
|
||||
"demo_free": True,
|
||||
"source": "report_reclass 取数层(凭证明细 + 期末余额口径;本报表零示例值)",
|
||||
"totals": {
|
||||
"assets": {"end": round(total_assets_end, 2), "begin": round(total_assets_begin, 2)},
|
||||
"liabilities": {"end": round(total_liab_end, 2), "begin": round(total_liab_begin, 2)},
|
||||
@@ -1623,6 +1704,7 @@ def get_balance_sheet(
|
||||
"liab_equity": {"end": round(total_liab_end + total_equity_end, 2),
|
||||
"begin": round(total_liab_begin + total_equity_begin, 2)},
|
||||
"balanced": balanced,
|
||||
"balanced_note": balanced_note,
|
||||
"identity_gap": round(total_assets_end - total_liab_end - total_equity_end, 2),
|
||||
},
|
||||
# ── 重分类(调表不调账)──
|
||||
@@ -1777,31 +1859,70 @@ CASH_FLOW_LINES = [
|
||||
{"code": "CF16", "name": "分配股利、利润或偿付利息支付的现金", "section": "financing", "sign": -1, "kpi_code": None},
|
||||
]
|
||||
|
||||
# 现金流量表示例数据(2026-06, 单位: 千元, 与利润表/KPI口径一致)
|
||||
CF_DEMO = {
|
||||
"CF01": 5200, "CF02": 0, "CF03": 120, "CF04": -3150, "CF05": -820,
|
||||
"CF06": -360, "CF07": -140,
|
||||
"CF08": 200, "CF09": 50, "CF10": 30, "CF11": -330, "CF12": -100,
|
||||
"CF13": 0, "CF14": 500, "CF15": -200, "CF16": -150,
|
||||
}
|
||||
CF_DEMO_FX = 0 # 汇率变动对现金的影响
|
||||
CF_DEMO_BEGIN = 1200 # 期初现金及现金等价物余额
|
||||
|
||||
|
||||
def _get_cf_amount(db: Session, line: dict, period: str, entity_id: int = 1) -> dict:
|
||||
"""现金流量表行项目 — 优先KPI/凭证,否则示例数据(账套隔离 2026-08-31)"""
|
||||
"""现金流量表行项目 — KPI/凭证明细取数;无真实数据 ⇒ value=None + no_data=True
|
||||
|
||||
批① 2026-10-15:原「否则 CF_DEMO 示例数据」已摘除,示例值不进对外数字。
|
||||
"""
|
||||
# 经营净额行特殊处理:优先取 F_OP_CFLOW
|
||||
if line.get("kpi_code"):
|
||||
kpi_val = _get_kpi_val(db, line["kpi_code"], period, entity_id)
|
||||
if kpi_val is not None:
|
||||
return {"value": round(kpi_val, 2), "is_demo": False}
|
||||
return {"value": round(kpi_val, 2), "is_demo": False, "no_data": False,
|
||||
"source": "KPI %s" % line["kpi_code"]}
|
||||
real = _get_bs_amount(db, [(line["code"], line["sign"])], period, entity_id)
|
||||
if real is not None:
|
||||
return {"value": real, "is_demo": False}
|
||||
demo = CF_DEMO.get(line["code"])
|
||||
if demo is not None:
|
||||
return {"value": float(demo), "is_demo": True}
|
||||
return {"value": None, "is_demo": True}
|
||||
return {"value": real, "is_demo": False, "no_data": False, "source": "voucher_details"}
|
||||
return {"value": None, "is_demo": False, "no_data": True, "source": "none"}
|
||||
|
||||
|
||||
def _kpi_to_yuan(db: Session, kpi_code: str, value: float, entity_id: int = 1) -> float:
|
||||
"""按 `kpi_definitions.unit` 把 KPI 值折算为**元**(unit 含「万」⇒ ×10000)。
|
||||
|
||||
2026-10-15(批①):现金流量表内部一律以元为口径(凭证明细亦为元);
|
||||
原实现把 KPI(万元)与凭证(元)混排 ⇒ 单位错配,本函数收口折算。
|
||||
"""
|
||||
try:
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.kpi_code == kpi_code, KPIDefinition.entity_id == entity_id).first()
|
||||
unit = (kpi.unit if kpi else None) or ""
|
||||
except Exception:
|
||||
unit = ""
|
||||
return round(float(value) * 10000.0, 2) if "万" in unit else round(float(value), 2)
|
||||
|
||||
|
||||
def _get_official_cf(db: Session, entity_id: int, period: str) -> Optional[dict]:
|
||||
"""官方现金流量表口径(`cashflow_statements`,库内**万元**)→ 元;无该期行 ⇒ None。
|
||||
|
||||
2026-10-15(cma-reports-real-data-all 批①):现金流量表**行明细**目前未映射到科目码
|
||||
(CASH_FLOW_LINES 的 code 是 CF01… 报告行号,不是 subjects.subject_code)⇒ 行级取数
|
||||
结构性缺口(回执点名,不在本单内擅改口径);但三大活动净额 / 期初·期末现金有官方
|
||||
注入真值可用,故此处按真实来源取,并逐项标注来源与「行明细不可用」。
|
||||
"""
|
||||
try:
|
||||
from app.models import CashFlowStructureValue
|
||||
row = db.query(CashFlowStructureValue).filter(
|
||||
CashFlowStructureValue.entity_id == entity_id,
|
||||
CashFlowStructureValue.period == period).first()
|
||||
except Exception:
|
||||
return None
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
def _y(v):
|
||||
return None if v is None else round(float(v) * 10000.0, 2)
|
||||
|
||||
return {
|
||||
"period": row.period,
|
||||
"operating": _y(row.operating_net),
|
||||
"investing": _y(row.investing_net),
|
||||
"financing": _y(row.financing_net),
|
||||
"begin_cash": _y(row.begin_cash),
|
||||
"end_cash": _y(row.end_cash),
|
||||
"source": "cashflow_statements(id=%s, source=%s) 万元→元" % (row.id, row.source or ""),
|
||||
"line_detail_available": False,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/cash-flow")
|
||||
@@ -1823,6 +1944,8 @@ def get_cash_flow_statement(
|
||||
sections = []
|
||||
net_by_section = {}
|
||||
all_real = True
|
||||
has_no_data = False
|
||||
_off = _get_official_cf(db, entity_id, period)
|
||||
|
||||
for sc in section_cfg:
|
||||
lines = []
|
||||
@@ -1832,8 +1955,9 @@ def get_cash_flow_statement(
|
||||
if line["section"] != sc["key"]:
|
||||
continue
|
||||
v = _get_cf_amount(db, line, period, entity_id)
|
||||
if v["is_demo"]:
|
||||
if v.get("no_data"):
|
||||
all_real = False
|
||||
has_no_data = True
|
||||
if v["value"] is not None:
|
||||
subtotal += v["value"]
|
||||
sec_real = True
|
||||
@@ -1841,49 +1965,113 @@ def get_cash_flow_statement(
|
||||
"code": line["code"],
|
||||
"name": line["name"],
|
||||
"value": round(v["value"], 2) if v["value"] is not None else None,
|
||||
"is_demo": v["is_demo"],
|
||||
"is_demo": False,
|
||||
"no_data": bool(v.get("no_data")),
|
||||
"source": v.get("source", "none"),
|
||||
})
|
||||
net_by_section[sc["key"]] = round(subtotal, 2)
|
||||
net_by_section[sc["key"]] = round(subtotal, 2) if sec_real else None
|
||||
_official_net = None
|
||||
if not sec_real and _off and _off.get(sc["key"]) is not None:
|
||||
_official_net = _off[sc["key"]]
|
||||
net_by_section[sc["key"]] = _official_net
|
||||
sections.append({
|
||||
"key": sc["key"],
|
||||
"name": sc["name"],
|
||||
"short": sc["short"],
|
||||
"net": round(subtotal, 2),
|
||||
"net": round(subtotal, 2) if sec_real else _official_net,
|
||||
"lines": lines,
|
||||
"has_real_data": sec_real,
|
||||
"has_real_data": sec_real or (_official_net is not None),
|
||||
"data_state": ("voucher" if sec_real else
|
||||
("official_statement" if _official_net is not None else "no_data")),
|
||||
"net_source": ("voucher_details 行明细合计" if sec_real else
|
||||
("cashflow_statements 官方口径(万元→元);行明细未映射到科目码 ⇒ 保持 null"
|
||||
if _official_net is not None else "无数据(行明细未映射 + 无官方注入)")),
|
||||
"line_detail_available": bool(sec_real),
|
||||
})
|
||||
|
||||
# 经营净额行优先取 KPI F_OP_CFLOW(真实数据优先)
|
||||
# 经营净额行:① 官方口径(cashflow_statements,万元→元)优先;
|
||||
# ② 无官方行时才退回 KPI F_OP_CFLOW,并按 kpi_definitions.unit 折算为元
|
||||
# (批① 2026-10-15:原实现直接写入 KPI 原值 ⇒ 万元/元 混排;且 4dp KPI 会与
|
||||
# 2dp 官方值互踩造成勾稽假不平)
|
||||
if _off and _off.get("operating") is not None:
|
||||
sections[0]["net"] = _off["operating"]
|
||||
sections[0]["net_source"] = _off["source"] + " · operating_net"
|
||||
sections[0]["data_state"] = "official_statement"
|
||||
sections[0]["has_real_data"] = True
|
||||
net_by_section["operating"] = _off["operating"]
|
||||
else:
|
||||
op_kpi = _get_kpi_val(db, "F_OP_CFLOW", period, entity_id)
|
||||
if op_kpi is not None:
|
||||
sections[0]["net"] = round(op_kpi, 2)
|
||||
net_by_section["operating"] = round(op_kpi, 2)
|
||||
_op_yuan = _kpi_to_yuan(db, "F_OP_CFLOW", op_kpi, entity_id)
|
||||
sections[0]["net"] = _op_yuan
|
||||
sections[0]["net_source"] = "KPI F_OP_CFLOW(按 kpi_definitions.unit 折算为元)"
|
||||
sections[0]["data_state"] = "kpi"
|
||||
sections[0]["has_real_data"] = True
|
||||
net_by_section["operating"] = _op_yuan
|
||||
|
||||
# 批①:fx 无真实来源 ⇒ 显式 null(原 CF_DEMO_FX=0 注入已摘除)
|
||||
fx = _get_kpi_val(db, "F_FX_LOSS", period, entity_id)
|
||||
if fx is None:
|
||||
fx = CF_DEMO_FX
|
||||
fx_demo = True
|
||||
else:
|
||||
fx_demo = False
|
||||
|
||||
begin_cash = CF_DEMO_BEGIN
|
||||
net_increase = round(net_by_section.get("operating", 0)
|
||||
+ net_by_section.get("investing", 0)
|
||||
+ net_by_section.get("financing", 0)
|
||||
+ float(fx or 0), 2)
|
||||
end_cash = round(begin_cash + net_increase, 2)
|
||||
# 批①:期初现金取真实来源(cashflow_statements 官方口径,万元 → 元);无则 null
|
||||
begin_cash = _off.get("begin_cash") if _off else None
|
||||
official_end_cash = _off.get("end_cash") if _off else None
|
||||
if _off and begin_cash is not None:
|
||||
begin_cash_source = _off["source"] + " · begin_cash"
|
||||
else:
|
||||
begin_cash_source = "cashflow_statements 无该期期初现金(未采集)"
|
||||
if not _off:
|
||||
has_no_data = True
|
||||
|
||||
parts = [net_by_section.get("operating"), net_by_section.get("investing"),
|
||||
net_by_section.get("financing")]
|
||||
if all(p is not None for p in parts):
|
||||
net_increase = round(sum(parts) + float(fx or 0), 2)
|
||||
_by_lines = any(s.get("line_detail_available") for s in sections)
|
||||
net_increase_source = ("voucher_details 行明细合计(含 fx=%s)" % fx
|
||||
if _by_lines
|
||||
else "cashflow_statements 官方口径(三大活动净额合计;fx=%s)" % fx)
|
||||
else:
|
||||
net_increase = None
|
||||
net_increase_source = "无数据(存在净额缺失活动)"
|
||||
end_cash = (round(begin_cash + net_increase, 2)
|
||||
if (begin_cash is not None and net_increase is not None) else None)
|
||||
# 勾稽:期初 + 净增加 = 期末(官方口径给定期末时显式对拍)
|
||||
if official_end_cash is not None and end_cash is not None:
|
||||
reconciliation = {"formula": "期初现金 + 现金净增加额 = 期末现金",
|
||||
"begin_cash": begin_cash, "net_increase": net_increase,
|
||||
"end_cash_computed": end_cash, "end_cash_official": official_end_cash,
|
||||
"balanced": abs(end_cash - official_end_cash) < 0.01}
|
||||
else:
|
||||
reconciliation = {"formula": "期初现金 + 现金净增加额 = 期末现金",
|
||||
"begin_cash": begin_cash, "net_increase": net_increase,
|
||||
"end_cash_computed": end_cash, "end_cash_official": official_end_cash,
|
||||
"balanced": None,
|
||||
"note": "官方口径缺期初/净额或期末 ⇒ 勾稽不可判定(显式 null,不用示例值凑平)"}
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"title": f"现金流量表 — 新30号准则({period})",
|
||||
"sections": sections,
|
||||
"fx_effect": {"name": "四、汇率变动对现金及现金等价物的影响", "value": round(float(fx), 2), "is_demo": fx_demo},
|
||||
"fx_effect": {"name": "四、汇率变动对现金及现金等价物的影响",
|
||||
"value": round(float(fx), 2) if fx is not None else None,
|
||||
"is_demo": False,
|
||||
"no_data": fx is None,
|
||||
"source": "KPI F_FX_LOSS" if fx is not None else "无数据(未采集)"},
|
||||
"summary": {
|
||||
"net_increase": net_increase,
|
||||
"net_increase_source": net_increase_source,
|
||||
"fx_available": fx is not None,
|
||||
"begin_cash": begin_cash,
|
||||
"begin_cash_source": begin_cash_source,
|
||||
"end_cash": end_cash,
|
||||
"end_cash_official": official_end_cash,
|
||||
},
|
||||
"reconciliation": reconciliation,
|
||||
"all_items_have_data": all_real,
|
||||
"data_state": "voucher" if all_real else "partial_no_data",
|
||||
"no_data_present": has_no_data,
|
||||
"demo_free": True,
|
||||
"source": "KPI + voucher_details 真实发生额 / cashflow_statements 官方口径(按 entity 隔离;本报表零示例值)",
|
||||
}
|
||||
|
||||
|
||||
@@ -1997,7 +2185,7 @@ def export_statutory_reports(
|
||||
r = write_row(ws2, r, [sec["name"], sec.get("category_label", ""), sec["subtotal_end"], sec["subtotal_begin"], "小计"],
|
||||
section_font, section_fill)
|
||||
for line in sec.get("lines", []):
|
||||
src = "示例" if line.get("is_demo") else "凭证"
|
||||
src = _src_label(line)
|
||||
r = write_row(ws2, r, [line["name"], line.get("ns_category_label", ""),
|
||||
line["end_value"], line["begin_value"], src])
|
||||
t = bs.get("totals", {})
|
||||
@@ -2028,11 +2216,11 @@ def export_statutory_reports(
|
||||
for sec in cf.get("sections", []):
|
||||
r = write_row(ws3, r, [sec["name"], "", "", "小计"], section_font, section_fill)
|
||||
for line in sec.get("lines", []):
|
||||
src = "示例" if line.get("is_demo") else "凭证"
|
||||
src = _src_label(line)
|
||||
r = write_row(ws3, r, [line["name"], line["code"], line["value"], src])
|
||||
r = write_row(ws3, r, [f"{sec['name']}净额", "", sec["net"], ""], total_font, total_fill)
|
||||
fx = cf.get("fx_effect", {})
|
||||
r = write_row(ws3, r, [fx.get("name", ""), "", fx.get("value"), "示例" if fx.get("is_demo") else "凭证"], section_font, section_fill)
|
||||
r = write_row(ws3, r, [fx.get("name", ""), "", fx.get("value"), _src_label(fx)], section_font, section_fill)
|
||||
s = cf.get("summary", {})
|
||||
r = write_row(ws3, r, ["现金及现金等价物净增加额", "", s.get("net_increase"), ""], total_font, total_fill)
|
||||
r = write_row(ws3, r, ["加:期初现金及现金等价物余额", "", s.get("begin_cash"), ""], total_font, total_fill)
|
||||
@@ -3375,7 +3563,9 @@ def get_proforma_profit_statement(
|
||||
"period": period,
|
||||
"budget_version": _proforma_versions(found_versions),
|
||||
"requested_version": version,
|
||||
"title": f"预算版利润表 — 新30号准则({period})",
|
||||
"title": f"【预测】预算版利润表 — 新30号准则({period})",
|
||||
"is_forecast": True,
|
||||
"forecast_notice": "【预测/预算版】非实际发生数;实际数见 /reports/profit-statement、/reports/balance-sheet、/reports/cash-flow,两者不得混排",
|
||||
"blocks": blocks,
|
||||
"net_profit_actual": round(net_actual, 2),
|
||||
"net_profit_budget": round(net_budget, 2),
|
||||
@@ -3451,7 +3641,9 @@ def get_proforma_balance_sheet(
|
||||
"period": period,
|
||||
"budget_version": _proforma_versions(found_versions),
|
||||
"requested_version": version,
|
||||
"title": f"预算版资产负债表({period})",
|
||||
"title": f"【预测】预算版资产负债表({period})",
|
||||
"is_forecast": True,
|
||||
"forecast_notice": "【预测/预算版】非实际发生数;实际数见 /reports/profit-statement、/reports/balance-sheet、/reports/cash-flow,两者不得混排",
|
||||
"sections": sections,
|
||||
"budget_source_hint": "budget_plan=预算方案 / target_split=KPI目标值按月分摊 / none=无预算",
|
||||
}
|
||||
@@ -3547,7 +3739,9 @@ def get_proforma_cash_flow(
|
||||
"period": period,
|
||||
"budget_version": _proforma_versions(found_versions),
|
||||
"requested_version": version,
|
||||
"title": f"预算版现金流量表({period})",
|
||||
"title": f"【预测】预算版现金流量表({period})",
|
||||
"is_forecast": True,
|
||||
"forecast_notice": "【预测/预算版】非实际发生数;实际数见 /reports/profit-statement、/reports/balance-sheet、/reports/cash-flow,两者不得混排",
|
||||
"sections": sections,
|
||||
"summary": {
|
||||
"net_increase_actual": net_increase_actual,
|
||||
|
||||
Reference in New Issue
Block a user