fix(reports): profit-summary 真实科目取数重建(去 KPI 比率倒推/硬编码 0.50 + 勾稽自检 + entity 隔离)
ci/woodpecker/push/main Pipeline was canceled
ci/woodpecker/push/main Pipeline was canceled
This commit is contained in:
+31
-104
@@ -3,7 +3,7 @@ CMA管理报表中心 — 管理会计OS
|
||||
非传统财务报表,聚焦管理决策分析
|
||||
|
||||
报表:
|
||||
1. 管理利润表 — 收入→变动成本→边际贡献→固定成本→息税前利润
|
||||
1. 管理利润表 — 收入→变动成本→边际贡献→固定成本→营业利润(真实科目取数)
|
||||
2. 预算执行报告 — 各KPI预算vs实际vs差异率
|
||||
3. KPI趋势报告 — 选定KPI的历史趋势
|
||||
4. 四维度绩效评分卡 — BSC健康度雷达图
|
||||
@@ -28,6 +28,8 @@ from app.utils.dupont_basis import (
|
||||
resolve_window, stock_balance, sum_flows,
|
||||
)
|
||||
from app.utils.voucher_guard import apply_voucher_guard
|
||||
from app.utils.profit_lines import (BOTTOM_LABEL, UNIT, build_items, compute_lines,
|
||||
prev_period_of, reconcile)
|
||||
from app.services import report_reclass
|
||||
from app.deps import get_entity_id, assert_entity_slug_scope, resolve_session_slug
|
||||
import logging
|
||||
@@ -49,116 +51,41 @@ def get_profit_summary(
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""管理利润表 — 收入→变动成本→边际贡献→固定成本→息税前利润"""
|
||||
"""管理利润表 — 收入→变动成本→边际贡献→固定成本→营业利润(**真实科目取数**,按 entity 隔离)
|
||||
|
||||
2026-10-14 重建(原实现 3 处硬伤已消除):
|
||||
① 原 L74 `contribution_margin = gross_profit`(边际贡献=毛利)→ 改真账:收入 − 变动成本;
|
||||
② 原 L76 变动成本 = 营收 × 硬编码比例(五成)→ 科目口径现算,**本端点零比例常量**;
|
||||
③ 原末行标签「= 息税前利润」却装净利率算出的 net_profit → 标签=算法=「营业利润」
|
||||
(= 收入 − 变动成本 − 固定成本;未扣所得税/营业外 ⇒ 不称净利润,已扣 6603 财务费用 ⇒ 不称 EBIT)。
|
||||
|
||||
口径唯一源:`app/utils/bep_caliber.py::ENTITY_CALIBERS`(变动/固定科目构成一律从该处派生,
|
||||
本报表层不定义任何科目集);取数实现见 `app/utils/profit_lines.py`(voucher_details 真实发生额)。
|
||||
勾稽自检:响应 `reconciliation`(收入 − 变动成本 − 固定成本 = 末行;不平显式标出,禁静默不平)。
|
||||
"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
prev_period = prev_period_of(period)
|
||||
|
||||
# 从KPI数据中获取各利润要素
|
||||
def get_val(code: str):
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.kpi_code == code, KPIDefinition.entity_id == entity_id).first()
|
||||
if not kpi:
|
||||
return None
|
||||
v = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id, KPIValue.period == period
|
||||
).order_by(KPIValue.id.desc()).first()
|
||||
return v.actual_value if v else None
|
||||
|
||||
revenue = get_val("F_REVENUE")
|
||||
gross_profit_rate = get_val("F_PROFIT_RATE")
|
||||
net_profit_rate = get_val("F_NET_PROFIT_RATE")
|
||||
cost_ratio = get_val("F_COST_RATIO")
|
||||
|
||||
# 计算利润要素
|
||||
# 营收已知,用毛利率算毛利,用成本率算成本
|
||||
gross_profit = round(revenue * (gross_profit_rate / 100), 2) if revenue and gross_profit_rate else None
|
||||
total_cost = round(revenue * (cost_ratio / 100), 2) if revenue and cost_ratio else None
|
||||
net_profit = round(revenue * (net_profit_rate / 100), 2) if revenue and net_profit_rate else None
|
||||
|
||||
# 边际贡献 ≈ 毛利(简化模型)
|
||||
contribution_margin = gross_profit
|
||||
# 固定成本 ≈ 总成本 - 变动成本(假设变动成本=营收*50%)
|
||||
variable_cost = round(revenue * 0.50, 2) if revenue else None
|
||||
fixed_cost = round(total_cost - variable_cost, 2) if total_cost and variable_cost else None
|
||||
|
||||
# 找上期做环比
|
||||
prev_year, prev_month = period.split("-")
|
||||
py, pm = int(prev_year), int(prev_month)
|
||||
pm -= 1
|
||||
if pm <= 0:
|
||||
pm += 12
|
||||
py -= 1
|
||||
prev_period = f"{py}-{pm:02d}"
|
||||
|
||||
def get_prev_val(code: str):
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.kpi_code == code, KPIDefinition.entity_id == entity_id).first()
|
||||
if not kpi: return None
|
||||
v = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id, KPIValue.period == prev_period
|
||||
).order_by(KPIValue.id.desc()).first()
|
||||
return v.actual_value if v else None
|
||||
|
||||
prev_revenue = get_prev_val("F_REVENUE")
|
||||
prev_gross_profit_rate = get_prev_val("F_PROFIT_RATE")
|
||||
prev_net_profit_rate = get_prev_val("F_NET_PROFIT_RATE")
|
||||
prev_cost_ratio = get_prev_val("F_COST_RATIO")
|
||||
prev_gross_profit = round(prev_revenue * (prev_gross_profit_rate / 100), 2) if prev_revenue and prev_gross_profit_rate else None
|
||||
prev_total_cost = round(prev_revenue * (prev_cost_ratio / 100), 2) if prev_revenue and prev_cost_ratio else None
|
||||
prev_net_profit = round(prev_revenue * (prev_net_profit_rate / 100), 2) if prev_revenue and prev_net_profit_rate else None
|
||||
prev_contribution_margin = prev_gross_profit
|
||||
prev_variable_cost = round(prev_revenue * 0.50, 2) if prev_revenue else None
|
||||
prev_fixed_cost = round(prev_total_cost - prev_variable_cost, 2) if prev_total_cost and prev_variable_cost else None
|
||||
|
||||
def calc_chg(cur, prev):
|
||||
if cur is not None and prev is not None and prev != 0:
|
||||
return round((cur - prev) / prev * 100, 2)
|
||||
return None
|
||||
|
||||
items = [
|
||||
{
|
||||
"name": "营业收入",
|
||||
"value": revenue,
|
||||
"prev_value": prev_revenue,
|
||||
"change_rate": calc_chg(revenue, prev_revenue),
|
||||
"ratio": 100.0,
|
||||
},
|
||||
{
|
||||
"name": "减:变动成本",
|
||||
"value": variable_cost,
|
||||
"prev_value": prev_variable_cost,
|
||||
"change_rate": calc_chg(variable_cost, prev_variable_cost),
|
||||
"ratio": round(variable_cost / revenue * 100, 2) if variable_cost and revenue else None,
|
||||
},
|
||||
{
|
||||
"name": "= 边际贡献",
|
||||
"value": contribution_margin,
|
||||
"prev_value": prev_contribution_margin,
|
||||
"change_rate": calc_chg(contribution_margin, prev_contribution_margin),
|
||||
"ratio": round(contribution_margin / revenue * 100, 2) if contribution_margin and revenue else None,
|
||||
"is_subtotal": True,
|
||||
},
|
||||
{
|
||||
"name": "减:固定成本",
|
||||
"value": fixed_cost,
|
||||
"prev_value": prev_fixed_cost,
|
||||
"change_rate": calc_chg(fixed_cost, prev_fixed_cost),
|
||||
"ratio": round(fixed_cost / revenue * 100, 2) if fixed_cost and revenue else None,
|
||||
},
|
||||
{
|
||||
"name": "= 息税前利润",
|
||||
"value": net_profit,
|
||||
"prev_value": prev_net_profit,
|
||||
"change_rate": calc_chg(net_profit, prev_net_profit),
|
||||
"ratio": round(net_profit / revenue * 100, 2) if net_profit and revenue else None,
|
||||
"is_total": True,
|
||||
},
|
||||
]
|
||||
cur = compute_lines(db, entity_id, period, period)
|
||||
prev = compute_lines(db, entity_id, prev_period, prev_period)
|
||||
values = cur.get("values") or {}
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"prev_period": prev_period,
|
||||
"items": items,
|
||||
"entity_id": entity_id,
|
||||
"unit": UNIT,
|
||||
"bottom_label": BOTTOM_LABEL,
|
||||
"source": "voucher_details 真实凭证明细发生额(按 entity 隔离;剔结转腿 + 作废/撤单行)",
|
||||
"available": cur["available"],
|
||||
"missing": cur["missing"],
|
||||
"prev_available": prev["available"],
|
||||
"prev_missing": prev["missing"],
|
||||
"caliber": cur.get("caliber"),
|
||||
"items": build_items(cur, prev),
|
||||
"reconciliation": reconcile(values.get("revenue"), values.get("variable_cost"),
|
||||
values.get("fixed_cost"), values.get("result")),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user