feat: 财务报表助手—对外法定报表(三表合一+Excel导出)
This commit is contained in:
+587
-1
@@ -601,7 +601,7 @@ def get_profit_statement(
|
||||
|
||||
|
||||
def _build_new_format_profit(db: Session, period: str) -> dict:
|
||||
"""构建新30号准则五板块利润表"""
|
||||
"""构建新30号准则五板块利润表(含附注明细)"""
|
||||
blocks = []
|
||||
total_net_profit = 0
|
||||
all_items_have_data = True
|
||||
@@ -644,6 +644,9 @@ def _build_new_format_profit(db: Session, period: str) -> dict:
|
||||
blocks.append(block_result)
|
||||
total_net_profit += block_subtotal
|
||||
|
||||
# 附注明细(对外法定报表披露要求)
|
||||
notes = _build_profit_notes(db, period, blocks, total_net_profit)
|
||||
|
||||
# 合计行:净利润 = 一二三+四+五
|
||||
return {
|
||||
"period": period,
|
||||
@@ -653,10 +656,87 @@ def _build_new_format_profit(db: Session, period: str) -> dict:
|
||||
"net_profit": round(total_net_profit, 2),
|
||||
"net_profit_name": "净利润",
|
||||
"all_items_have_data": all_items_have_data,
|
||||
"notes": notes,
|
||||
"prev_period": None, # TODO: P1追溯调整
|
||||
}
|
||||
|
||||
|
||||
def _build_profit_notes(db: Session, period: str, blocks: list, net_profit: float) -> dict:
|
||||
"""利润表附注明细 — 收入/费用/财务费用拆解 + 板块勾稽 + 关键比率"""
|
||||
def amt(code):
|
||||
return _get_subject_amount(db, code, period)
|
||||
|
||||
revenue_main = amt("6001")
|
||||
revenue_other = amt("6051")
|
||||
revenue_total = None
|
||||
if revenue_main is not None or revenue_other is not None:
|
||||
revenue_total = round((revenue_main or 0) + (revenue_other or 0), 2)
|
||||
|
||||
revenue_breakdown = [
|
||||
{"name": "主营业务收入", "code": "6001", "amount": round(revenue_main, 2) if revenue_main is not None else None},
|
||||
{"name": "其他业务收入", "code": "6051", "amount": round(revenue_other, 2) if revenue_other is not None else None},
|
||||
]
|
||||
|
||||
expense_items = [
|
||||
{"name": "营业成本", "code": "6401", "amount": amt("6401")},
|
||||
{"name": "其他业务成本", "code": "6402", "amount": amt("6402")},
|
||||
{"name": "销售费用", "code": "6601", "amount": amt("6601")},
|
||||
{"name": "管理费用", "code": "6602", "amount": amt("6602")},
|
||||
{"name": "研发费用", "code": "660204", "amount": amt("660204")},
|
||||
{"name": "经营资产减值损失", "code": "6701", "amount": amt("6701")},
|
||||
]
|
||||
expense_breakdown = [
|
||||
{"name": e["name"], "code": e["code"], "amount": round(e["amount"], 2) if e["amount"] is not None else None}
|
||||
for e in expense_items
|
||||
]
|
||||
|
||||
finance_breakdown = [
|
||||
{"name": "利息收入(投资类)", "code": "6011", "amount": amt("6011")},
|
||||
{"name": "投资收益(投资类)", "code": "6111", "amount": amt("6111")},
|
||||
{"name": "利息支出(筹资类)", "code": "660301", "amount": amt("660301")},
|
||||
{"name": "经营汇兑损益", "code": "6603", "amount": amt("6603")},
|
||||
{"name": "筹资汇兑损益", "code": "660302", "amount": amt("660302")},
|
||||
]
|
||||
finance_breakdown = [
|
||||
{"name": f["name"], "code": f["code"], "amount": round(f["amount"], 2) if f["amount"] is not None else None}
|
||||
for f in finance_breakdown
|
||||
]
|
||||
|
||||
# 板块勾稽(净利润 = 五板块之和)
|
||||
block_reconciliation = [
|
||||
{"name": b["name"], "key": b["key"], "amount": b["subtotal"], "result_name": b["subtotal_name"]}
|
||||
for b in blocks
|
||||
]
|
||||
|
||||
# 关键比率
|
||||
key_ratios = []
|
||||
if revenue_total:
|
||||
key_ratios.append({
|
||||
"name": "毛利率",
|
||||
"value": round((revenue_total - (amt("6401") or 0) - (amt("6402") or 0)) / revenue_total * 100, 2) if (amt("6401") is not None or amt("6402") is not None) else None,
|
||||
})
|
||||
key_ratios.append({
|
||||
"name": "净利率",
|
||||
"value": round(net_profit / revenue_total * 100, 2),
|
||||
})
|
||||
rd = amt("660204")
|
||||
if rd is not None:
|
||||
key_ratios.append({"name": "研发费用率", "value": round(rd / revenue_total * 100, 2)})
|
||||
else:
|
||||
key_ratios.append({"name": "毛利率", "value": None})
|
||||
key_ratios.append({"name": "净利率", "value": None})
|
||||
|
||||
return {
|
||||
"revenue_total": round(revenue_total, 2) if revenue_total is not None else None,
|
||||
"revenue_breakdown": revenue_breakdown,
|
||||
"expense_breakdown": expense_breakdown,
|
||||
"finance_breakdown": finance_breakdown,
|
||||
"block_reconciliation": block_reconciliation,
|
||||
"net_profit": round(net_profit, 2),
|
||||
"key_ratios": key_ratios,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MPM管理层指标计算器 (P2)
|
||||
# ============================================================
|
||||
@@ -1247,6 +1327,512 @@ def get_category_map(
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 对外法定报表 — 新30号准则适配 (2027)
|
||||
# 利润表(五板块+附注) / 资产负债表(新准则科目分类) / 现金流量表(三活动)
|
||||
# ============================================================
|
||||
|
||||
# 资产负债表行项目: (编码列表[(code, sign)], 名称, 板块key, 新准则分类, 是否合计行)
|
||||
# 新准则分类: operating经营 / investing投资 / financing筹资 / equity权益
|
||||
BALANCE_SHEET_SECTIONS = [
|
||||
{
|
||||
"key": "current_assets",
|
||||
"name": "流动资产",
|
||||
"category_label": "经营资产",
|
||||
"lines": [
|
||||
{"codes": [("1001", 1), ("1002", 1), ("1012", 1)], "name": "货币资金", "ns_category": "operating"},
|
||||
{"codes": [("1101", 1)], "name": "交易性金融资产", "ns_category": "investing"},
|
||||
{"codes": [("1122", 1)], "name": "应收账款", "ns_category": "operating"},
|
||||
{"codes": [("1123", 1)], "name": "预付账款", "ns_category": "operating"},
|
||||
{"codes": [("1221", 1)], "name": "其他应收款", "ns_category": "operating"},
|
||||
{"codes": [("1405", 1)], "name": "存货", "ns_category": "operating"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "non_current_assets",
|
||||
"name": "非流动资产",
|
||||
"category_label": "投资资产",
|
||||
"lines": [
|
||||
{"codes": [("1511", 1)], "name": "长期股权投资", "ns_category": "investing"},
|
||||
{"codes": [("1601", 1), ("1602", -1)], "name": "固定资产净额", "ns_category": "operating"},
|
||||
{"codes": [("1701", 1)], "name": "无形资产", "ns_category": "operating"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "current_liabilities",
|
||||
"name": "流动负债",
|
||||
"category_label": "经营负债",
|
||||
"lines": [
|
||||
{"codes": [("2001", 1)], "name": "短期借款", "ns_category": "financing"},
|
||||
{"codes": [("2202", 1)], "name": "应付账款", "ns_category": "operating"},
|
||||
{"codes": [("2203", 1)], "name": "预收账款", "ns_category": "operating"},
|
||||
{"codes": [("2211", 1)], "name": "应付职工薪酬", "ns_category": "operating"},
|
||||
{"codes": [("2221", 1)], "name": "应交税费", "ns_category": "operating"},
|
||||
{"codes": [("2241", 1)], "name": "其他应付款", "ns_category": "operating"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "non_current_liabilities",
|
||||
"name": "非流动负债",
|
||||
"category_label": "筹资负债",
|
||||
"lines": [
|
||||
{"codes": [("2501", 1)], "name": "长期借款", "ns_category": "financing"},
|
||||
{"codes": [("2502", 1)], "name": "应付债券", "ns_category": "financing"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "equity",
|
||||
"name": "所有者权益",
|
||||
"category_label": "所有者权益",
|
||||
"lines": [
|
||||
{"codes": [("4001", 1)], "name": "实收资本", "ns_category": "equity"},
|
||||
{"codes": [("4002", 1)], "name": "资本公积", "ns_category": "equity"},
|
||||
{"codes": [("4103", 1), ("4104", 1)], "name": "未分配利润", "ns_category": "equity"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# 资产负债表示例数据(博海科技, 期末/期初, 单位: 千元)
|
||||
BS_DEMO = {
|
||||
"1001|1002|1012": {"end": 850, "begin": 780},
|
||||
"1101": {"end": 250, "begin": 220},
|
||||
"1122": {"end": 1100, "begin": 1050},
|
||||
"1123": {"end": 180, "begin": 160},
|
||||
"1221": {"end": 90, "begin": 80},
|
||||
"1405": {"end": 930, "begin": 900},
|
||||
"1511": {"end": 580, "begin": 550},
|
||||
"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": 460, "begin": 540},
|
||||
}
|
||||
|
||||
BS_CATEGORY_CN = {
|
||||
"operating": "经营类",
|
||||
"investing": "投资类",
|
||||
"financing": "筹资类",
|
||||
"equity": "权益类",
|
||||
}
|
||||
|
||||
|
||||
def _prev_period_str(period: str) -> str:
|
||||
"""上一期间 YYYY-MM → YYYY-(MM-1)"""
|
||||
try:
|
||||
y, m = period.split("-")
|
||||
y, m = int(y), int(m)
|
||||
m -= 1
|
||||
if m <= 0:
|
||||
m += 12
|
||||
y -= 1
|
||||
return f"{y}-{m:02d}"
|
||||
except Exception:
|
||||
return period
|
||||
|
||||
|
||||
def _get_bs_amount(db: Session, codes: list, period: str) -> Optional[float]:
|
||||
"""资产负债表科目余额 — 优先凭证明细,无数据返回 None"""
|
||||
total = 0.0
|
||||
has_data = False
|
||||
try:
|
||||
from app.models import VoucherDetail
|
||||
for code, sign in codes:
|
||||
result = db.query(
|
||||
func.sum(VoucherDetail.debit_amount - VoucherDetail.credit_amount)
|
||||
).filter(
|
||||
VoucherDetail.subject_code == code,
|
||||
VoucherDetail.period == period,
|
||||
).scalar()
|
||||
if result is not None:
|
||||
total += float(result) * sign
|
||||
has_data = True
|
||||
except Exception:
|
||||
pass
|
||||
return round(total, 2) if has_data else None
|
||||
|
||||
|
||||
def _bs_line_amount(db: Session, line: dict, period: str, column: str = "end") -> dict:
|
||||
"""单行:凭证数据优先,否则示例数据(column: end期末 / begin期初)"""
|
||||
real = _get_bs_amount(db, line["codes"], period)
|
||||
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}
|
||||
|
||||
|
||||
@router.get("/balance-sheet")
|
||||
def get_balance_sheet(
|
||||
period: str = Query(None, description="格式 YYYY-MM"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""资产负债表 — 新30号准则科目分类(经营/投资/筹资),期末vs期初"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
prev_period = _prev_period_str(period)
|
||||
|
||||
sections = []
|
||||
total_assets_end = total_assets_begin = 0
|
||||
total_liab_end = total_liab_begin = 0
|
||||
total_equity_end = total_equity_begin = 0
|
||||
all_real = True
|
||||
|
||||
for sec in BALANCE_SHEET_SECTIONS:
|
||||
lines = []
|
||||
sec_end = sec_begin = 0.0
|
||||
sec_real = False
|
||||
for line in sec["lines"]:
|
||||
end = _bs_line_amount(db, line, period, column="end")
|
||||
begin = _bs_line_amount(db, line, prev_period, column="begin")
|
||||
if end["is_demo"] or begin["is_demo"]:
|
||||
all_real = False
|
||||
if end["value"] is not None:
|
||||
sec_end += end["value"]
|
||||
sec_real = True
|
||||
if begin["value"] is not None:
|
||||
sec_begin += begin["value"]
|
||||
lines.append({
|
||||
"name": line["name"],
|
||||
"ns_category": line["ns_category"],
|
||||
"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"],
|
||||
})
|
||||
|
||||
# 归属汇总
|
||||
if sec["key"] in ("current_assets", "non_current_assets"):
|
||||
total_assets_end += sec_end
|
||||
total_assets_begin += sec_begin
|
||||
elif sec["key"] in ("current_liabilities", "non_current_liabilities"):
|
||||
total_liab_end += sec_end
|
||||
total_liab_begin += sec_begin
|
||||
else:
|
||||
total_equity_end += sec_end
|
||||
total_equity_begin += sec_begin
|
||||
|
||||
sections.append({
|
||||
"key": sec["key"],
|
||||
"name": sec["name"],
|
||||
"category_label": sec["category_label"],
|
||||
"subtotal_end": round(sec_end, 2),
|
||||
"subtotal_begin": round(sec_begin, 2),
|
||||
"lines": lines,
|
||||
"has_real_data": sec_real,
|
||||
})
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"prev_period": prev_period,
|
||||
"title": f"资产负债表 — 新30号准则({period})",
|
||||
"sections": sections,
|
||||
"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)},
|
||||
"equity": {"end": round(total_equity_end, 2), "begin": round(total_equity_begin, 2)},
|
||||
"liab_equity": {"end": round(total_liab_end + total_equity_end, 2),
|
||||
"begin": round(total_liab_begin + total_equity_begin, 2)},
|
||||
"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,
|
||||
},
|
||||
"all_items_have_data": all_real,
|
||||
}
|
||||
|
||||
|
||||
# 现金流量表行项目: (code, 名称, 板块, 方向, kpi_code可选)
|
||||
CASH_FLOW_LINES = [
|
||||
# 经营活动
|
||||
{"code": "CF01", "name": "销售商品、提供劳务收到的现金", "section": "operating", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF02", "name": "收到的税费返还", "section": "operating", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF03", "name": "收到其他与经营活动有关的现金", "section": "operating", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF04", "name": "购买商品、接受劳务支付的现金", "section": "operating", "sign": -1, "kpi_code": None},
|
||||
{"code": "CF05", "name": "支付给职工以及为职工支付的现金", "section": "operating", "sign": -1, "kpi_code": None},
|
||||
{"code": "CF06", "name": "支付的各项税费", "section": "operating", "sign": -1, "kpi_code": None},
|
||||
{"code": "CF07", "name": "支付其他与经营活动有关的现金", "section": "operating", "sign": -1, "kpi_code": None},
|
||||
# 投资活动
|
||||
{"code": "CF08", "name": "收回投资收到的现金", "section": "investing", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF09", "name": "取得投资收益收到的现金", "section": "investing", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF10", "name": "处置固定资产、无形资产等收回的现金", "section": "investing", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF11", "name": "购建固定资产、无形资产等支付的现金", "section": "investing", "sign": -1, "kpi_code": None},
|
||||
{"code": "CF12", "name": "投资支付的现金", "section": "investing", "sign": -1, "kpi_code": None},
|
||||
# 筹资活动
|
||||
{"code": "CF13", "name": "吸收投资收到的现金", "section": "financing", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF14", "name": "取得借款收到的现金", "section": "financing", "sign": 1, "kpi_code": None},
|
||||
{"code": "CF15", "name": "偿还债务支付的现金", "section": "financing", "sign": -1, "kpi_code": None},
|
||||
{"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) -> dict:
|
||||
"""现金流量表行项目 — 优先KPI/凭证,否则示例数据"""
|
||||
# 经营净额行特殊处理:优先取 F_OP_CFLOW
|
||||
if line.get("kpi_code"):
|
||||
kpi_val = _get_kpi_val(db, line["kpi_code"], period)
|
||||
if kpi_val is not None:
|
||||
return {"value": round(kpi_val, 2), "is_demo": False}
|
||||
real = _get_bs_amount(db, [(line["code"], line["sign"])], period)
|
||||
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}
|
||||
|
||||
|
||||
@router.get("/cash-flow")
|
||||
def get_cash_flow_statement(
|
||||
period: str = Query(None, description="格式 YYYY-MM"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""现金流量表 — 经营/投资/筹资三活动(新30号准则直接法)"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
|
||||
section_cfg = [
|
||||
{"key": "operating", "name": "一、经营活动产生的现金流量", "short": "经营活动"},
|
||||
{"key": "investing", "name": "二、投资活动产生的现金流量", "short": "投资活动"},
|
||||
{"key": "financing", "name": "三、筹资活动产生的现金流量", "short": "筹资活动"},
|
||||
]
|
||||
|
||||
sections = []
|
||||
net_by_section = {}
|
||||
all_real = True
|
||||
|
||||
for sc in section_cfg:
|
||||
lines = []
|
||||
subtotal = 0.0
|
||||
sec_real = False
|
||||
for line in CASH_FLOW_LINES:
|
||||
if line["section"] != sc["key"]:
|
||||
continue
|
||||
v = _get_cf_amount(db, line, period)
|
||||
if v["is_demo"]:
|
||||
all_real = False
|
||||
if v["value"] is not None:
|
||||
subtotal += v["value"]
|
||||
sec_real = True
|
||||
lines.append({
|
||||
"code": line["code"],
|
||||
"name": line["name"],
|
||||
"value": round(v["value"], 2) if v["value"] is not None else None,
|
||||
"is_demo": v["is_demo"],
|
||||
})
|
||||
net_by_section[sc["key"]] = round(subtotal, 2)
|
||||
sections.append({
|
||||
"key": sc["key"],
|
||||
"name": sc["name"],
|
||||
"short": sc["short"],
|
||||
"net": round(subtotal, 2),
|
||||
"lines": lines,
|
||||
"has_real_data": sec_real,
|
||||
})
|
||||
|
||||
# 经营净额行优先取 KPI F_OP_CFLOW(真实数据优先)
|
||||
op_kpi = _get_kpi_val(db, "F_OP_CFLOW", period)
|
||||
if op_kpi is not None:
|
||||
sections[0]["net"] = round(op_kpi, 2)
|
||||
net_by_section["operating"] = round(op_kpi, 2)
|
||||
|
||||
fx = _get_kpi_val(db, "F_FX_LOSS", period)
|
||||
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)
|
||||
|
||||
return {
|
||||
"period": period,
|
||||
"title": f"现金流量表 — 新30号准则({period})",
|
||||
"sections": sections,
|
||||
"fx_effect": {"name": "四、汇率变动对现金及现金等价物的影响", "value": round(float(fx), 2), "is_demo": fx_demo},
|
||||
"summary": {
|
||||
"net_increase": net_increase,
|
||||
"begin_cash": begin_cash,
|
||||
"end_cash": end_cash,
|
||||
},
|
||||
"all_items_have_data": all_real,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/statutory")
|
||||
def get_statutory_reports(
|
||||
period: str = Query(None, description="格式 YYYY-MM"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""对外法定报表(新30号准则)— 利润表+资产负债表+现金流量表 组合视图"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
return {
|
||||
"period": period,
|
||||
"title": f"对外法定报表 — 新30号准则({period})",
|
||||
"profit": _build_new_format_profit(db, period),
|
||||
"balance_sheet": get_balance_sheet(period=period, db=db),
|
||||
"cash_flow": get_cash_flow_statement(period=period, db=db),
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/statutory/export")
|
||||
def export_statutory_reports(
|
||||
period: str = Query(None, description="格式 YYYY-MM"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出对外法定报表(新30号准则)— Excel 三表合一"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
data = get_statutory_reports(period=period, db=db)
|
||||
|
||||
from io import BytesIO
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
wb = Workbook()
|
||||
|
||||
head_fill = PatternFill("solid", fgColor="305496")
|
||||
head_font = Font(color="FFFFFF", bold=True, size=12)
|
||||
section_fill = PatternFill("solid", fgColor="D9E1F2")
|
||||
section_font = Font(bold=True, size=11)
|
||||
total_fill = PatternFill("solid", fgColor="FCE4D6")
|
||||
total_font = Font(bold=True, size=11)
|
||||
thin = Side(style="thin", color="BFBFBF")
|
||||
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
demo_font = Font(color="D46B08", size=9, italic=True)
|
||||
|
||||
def style_header(ws, row, ncols, title):
|
||||
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=ncols)
|
||||
c = ws.cell(row=row, column=1, value=title)
|
||||
c.font = Font(bold=True, size=14, color="305496")
|
||||
c.alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws.row_dimensions[row].height = 26
|
||||
|
||||
def write_row(ws, r, values, font=None, fill=None, use_border=True):
|
||||
for ci, v in enumerate(values, start=1):
|
||||
c = ws.cell(row=r, column=ci, value=v)
|
||||
if font:
|
||||
c.font = font
|
||||
if fill:
|
||||
c.fill = fill
|
||||
if use_border:
|
||||
c.border = border
|
||||
return r + 1
|
||||
|
||||
# ── Sheet 1: 利润表(五板块+附注) ──
|
||||
ws1 = wb.active
|
||||
ws1.title = "利润表-新30号准则"
|
||||
style_header(ws1, 1, 4, f"利润表(新30号准则五板块) {period} 单位: 元")
|
||||
r = write_row(ws1, 2, ["板块", "项目", "科目编码", "本期金额"], head_font, head_fill)
|
||||
profit = data["profit"]
|
||||
for block in profit.get("blocks", []):
|
||||
r = write_row(ws1, r, [block["name"], block.get("subtotal_name", ""), "", block["subtotal"]],
|
||||
section_font, section_fill)
|
||||
for item in block.get("items", []):
|
||||
r = write_row(ws1, r, ["", item["name"], item["code"], item["effective"]])
|
||||
r = write_row(ws1, r, ["合计", "净利润", "", profit.get("net_profit")], total_font, total_fill)
|
||||
|
||||
# 附注明细
|
||||
notes = profit.get("notes") or {}
|
||||
r += 1
|
||||
r = write_row(ws1, r, ["附注一、收入构成", "", "", ""], section_font, section_fill)
|
||||
for n in notes.get("revenue_breakdown", []):
|
||||
r = write_row(ws1, r, ["", n["name"], n["code"], n["amount"]])
|
||||
r = write_row(ws1, r, ["", "营业收入合计", "", notes.get("revenue_total")], total_font)
|
||||
r = write_row(ws1, r, ["附注二、费用构成", "", "", ""], section_font, section_fill)
|
||||
for n in notes.get("expense_breakdown", []):
|
||||
r = write_row(ws1, r, ["", n["name"], n["code"], n["amount"]])
|
||||
r = write_row(ws1, r, ["附注三、财务费用拆解", "", "", ""], section_font, section_fill)
|
||||
for n in notes.get("finance_breakdown", []):
|
||||
r = write_row(ws1, r, ["", n["name"], n["code"], n["amount"]])
|
||||
r = write_row(ws1, r, ["附注四、板块勾稽(净利润=五板块之和)", "", "", ""], section_font, section_fill)
|
||||
for n in notes.get("block_reconciliation", []):
|
||||
r = write_row(ws1, r, ["", n["name"], "", n["amount"]])
|
||||
r = write_row(ws1, r, ["", "净利润", "", notes.get("net_profit")], total_font, total_fill)
|
||||
r = write_row(ws1, r, ["附注五、关键比率", "", "", ""], section_font, section_fill)
|
||||
for n in notes.get("key_ratios", []):
|
||||
r = write_row(ws1, r, ["", n["name"], "", n["value"]])
|
||||
for col, w in zip("ABCD", [28, 40, 14, 18]):
|
||||
ws1.column_dimensions[col].width = w
|
||||
|
||||
# ── Sheet 2: 资产负债表 ──
|
||||
ws2 = wb.create_sheet("资产负债表-新30号准则")
|
||||
bs = data["balance_sheet"]
|
||||
style_header(ws2, 1, 5, f"资产负债表(新30号准则科目分类) {period} 单位: 元")
|
||||
r = write_row(ws2, 2, ["项目", "新准则分类", "期末余额", "期初余额", "数据来源"], head_font, head_fill)
|
||||
for sec in bs.get("sections", []):
|
||||
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 "凭证"
|
||||
r = write_row(ws2, r, [line["name"], line.get("ns_category_label", ""),
|
||||
line["end_value"], line["begin_value"], src])
|
||||
t = bs.get("totals", {})
|
||||
r = write_row(ws2, r, ["资产总计", "", t["assets"]["end"], t["assets"]["begin"], ""], total_font, total_fill)
|
||||
r = write_row(ws2, r, ["负债合计", "", t["liabilities"]["end"], t["liabilities"]["begin"], ""], total_font, total_fill)
|
||||
r = write_row(ws2, r, ["所有者权益合计", "", t["equity"]["end"], t["equity"]["begin"], ""], total_font, total_fill)
|
||||
r = write_row(ws2, r, ["负债和所有者权益总计", "", t["liab_equity"]["end"], t["liab_equity"]["begin"], ""], total_font, total_fill)
|
||||
r = write_row(ws2, r, ["勾稽校验(资产=负债+权益)", "", "✓ 平衡" if t.get("balanced") else "✗ 不平", "", ""], total_font, total_fill)
|
||||
for col, w in zip("ABCDE", [36, 16, 18, 18, 12]):
|
||||
ws2.column_dimensions[col].width = w
|
||||
|
||||
# ── Sheet 3: 现金流量表 ──
|
||||
ws3 = wb.create_sheet("现金流量表-新30号准则")
|
||||
cf = data["cash_flow"]
|
||||
style_header(ws3, 1, 4, f"现金流量表(新30号准则三活动) {period} 单位: 元")
|
||||
r = write_row(ws3, 2, ["项目", "行次", "本期金额", "数据来源"], head_font, head_fill)
|
||||
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 "凭证"
|
||||
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)
|
||||
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)
|
||||
r = write_row(ws3, r, ["期末现金及现金等价物余额", "", s.get("end_cash"), ""], total_font, total_fill)
|
||||
for col, w in zip("ABCD", [46, 10, 18, 12]):
|
||||
ws3.column_dimensions[col].width = w
|
||||
|
||||
buf = BytesIO()
|
||||
wb.save(buf)
|
||||
buf.seek(0)
|
||||
from urllib.parse import quote
|
||||
filename = f"对外法定报表_新30号准则_{period}.xlsx"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/dupont")
|
||||
def get_dupont_analysis(
|
||||
entity: str = Query("bohai"),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""对外法定报表适配 — 数据库迁移
|
||||
补充资产负债表科目(资产/负债/所有者权益),按新30号准则经营/投资/筹资分类打标
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from app.database import get_engine
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("migrate_statutory")
|
||||
|
||||
# (科目编码, 科目名称, 上级, 级别, 类别, 新30号准则分类)
|
||||
# 新30号准则分类: operating经营类 / investing投资类 / financing筹资类 / equity权益
|
||||
BALANCE_SHEET_SUBJECTS = [
|
||||
# ── 资产类 (category=asset) ──
|
||||
('1001', '库存现金', None, 1, 'asset', 'operating'),
|
||||
('1002', '银行存款', None, 1, 'asset', 'operating'),
|
||||
('1012', '其他货币资金', None, 1, 'asset', 'operating'),
|
||||
('1101', '交易性金融资产', None, 1, 'asset', 'investing'),
|
||||
('1122', '应收账款', None, 1, 'asset', 'operating'),
|
||||
('1123', '预付账款', None, 1, 'asset', 'operating'),
|
||||
('1131', '应收股利', None, 1, 'asset', 'investing'),
|
||||
('1221', '其他应收款', None, 1, 'asset', 'operating'),
|
||||
('1405', '库存商品', None, 1, 'asset', 'operating'),
|
||||
('1501', '持有至到期投资', None, 1, 'asset', 'investing'),
|
||||
('1511', '长期股权投资', None, 1, 'asset', 'investing'),
|
||||
('1601', '固定资产', None, 1, 'asset', 'operating'),
|
||||
('1602', '累计折旧', '1601', 2, 'asset', 'operating'),
|
||||
('1701', '无形资产', None, 1, 'asset', 'operating'),
|
||||
# ── 负债类 (category=liability) ──
|
||||
('2001', '短期借款', None, 1, 'liability', 'financing'),
|
||||
('2202', '应付账款', None, 1, 'liability', 'operating'),
|
||||
('2203', '预收账款', None, 1, 'liability', 'operating'),
|
||||
('2211', '应付职工薪酬', None, 1, 'liability', 'operating'),
|
||||
('2221', '应交税费', None, 1, 'liability', 'operating'),
|
||||
('2241', '其他应付款', None, 1, 'liability', 'operating'),
|
||||
('2501', '长期借款', None, 1, 'liability', 'financing'),
|
||||
('2502', '应付债券', None, 1, 'liability', 'financing'),
|
||||
# ── 所有者权益类 (category=equity) ──
|
||||
('4001', '实收资本', None, 1, 'equity', 'equity'),
|
||||
('4002', '资本公积', None, 1, 'equity', 'equity'),
|
||||
('4103', '本年利润', None, 1, 'equity', 'equity'),
|
||||
('4104', '利润分配', None, 1, 'equity', 'equity'),
|
||||
]
|
||||
|
||||
|
||||
def run():
|
||||
engine = get_engine()
|
||||
with engine.connect() as conn:
|
||||
inserted = 0
|
||||
for row in BALANCE_SHEET_SUBJECTS:
|
||||
res = conn.execute(text("""
|
||||
INSERT IGNORE INTO subjects (subject_code, subject_name, parent_code, level, category, new_standard_category, is_active)
|
||||
VALUES (:code, :name, :parent, :level, :cat, :ns_cat, 1)
|
||||
"""), {"code": row[0], "name": row[1], "parent": row[2],
|
||||
"level": row[3], "cat": row[4], "ns_cat": row[5]})
|
||||
inserted += res.rowcount
|
||||
conn.commit()
|
||||
logger.info(f"✅ 插入 {inserted} 条资产负债表科目(已有则跳过)")
|
||||
total = conn.execute(text("SELECT count(*) FROM subjects")).scalar()
|
||||
by_cat = conn.execute(text(
|
||||
"SELECT category, count(*) FROM subjects GROUP BY category ORDER BY category"
|
||||
)).fetchall()
|
||||
dist = {r[0]: r[1] for r in by_cat}
|
||||
logger.info(f"✅ subjects 表当前共 {total} 条,分类分布: {dist}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -360,6 +360,200 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ════ 对外法定报表:新30号准则三表合一 ════ -->
|
||||
<el-tab-pane label="📑 对外法定报表" name="statutory">
|
||||
<div v-loading="loadingStatutory">
|
||||
<div class="report-desc">对外法定报表(新30号准则2027)— 利润表五板块+附注明细 / 资产负债表新准则科目分类 / 现金流量表三活动,支持导出Excel。</div>
|
||||
|
||||
<div class="statutory-toolbar">
|
||||
<el-radio-group v-model="statutoryView" size="small">
|
||||
<el-radio-button value="profit">利润表</el-radio-button>
|
||||
<el-radio-button value="balance">资产负债表</el-radio-button>
|
||||
<el-radio-button value="cashflow">现金流量表</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" size="small" :loading="exporting" @click="exportStatutory">📥 导出Excel(三表合一)</el-button>
|
||||
<span v-if="statutoryDemoMode" class="new30-demo-hint">⚠️ 部分数据为示例数据,导入凭证/打标后显示实际数据</span>
|
||||
</div>
|
||||
|
||||
<!-- ── 利润表(五板块+附注) ── -->
|
||||
<div v-if="statutoryView === 'profit' && statutoryData.profit">
|
||||
<div class="statutory-sub-title">利润表 — 新30号准则五板块({{ statutoryData.period }})</div>
|
||||
<div class="new30-blocks">
|
||||
<div v-for="block in statutoryData.profit.blocks || []" :key="block.key" class="new30-block">
|
||||
<div class="new30-block-header" @click="block.expanded = !block.expanded">
|
||||
<span class="new30-toggle">{{ block.expanded ? '▼' : '▶' }}</span>
|
||||
<span class="new30-block-name">{{ block.name }}</span>
|
||||
<span class="new30-block-subtotal" :class="{ negative: block.subtotal < 0 }">{{ formatMoney(block.subtotal) }}</span>
|
||||
<span class="new30-block-label">{{ block.subtotal_name }}</span>
|
||||
<span v-if="!block.has_real_data" class="new30-demo-tag">示例数据</span>
|
||||
</div>
|
||||
<div v-if="block.expanded" class="new30-block-body">
|
||||
<div v-for="item in block.items" :key="item.code" class="new30-item">
|
||||
<span class="new30-item-name">{{ item.name }}</span>
|
||||
<span class="new30-item-amount" :class="{ negative: item.effective != null && item.effective < 0 }">
|
||||
{{ item.effective != null ? formatMoney(item.effective) : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="new30-net-profit" v-if="statutoryData.profit.net_profit != null">
|
||||
<div class="new30-profit-row">
|
||||
<span class="new30-profit-label">净利润:</span>
|
||||
<span class="new30-profit-value" :class="{ negative: statutoryData.profit.net_profit < 0 }">{{ formatMoney(statutoryData.profit.net_profit) }}</span>
|
||||
<span class="new30-profit-name">= 经营 + 投资 + 筹资 + 所得税 + 终止经营</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附注明细 -->
|
||||
<div v-if="statutoryData.profit.notes" class="statutory-notes">
|
||||
<div class="statutory-notes-title">附注明细</div>
|
||||
<div class="statutory-notes-grid">
|
||||
<!-- 收入构成 -->
|
||||
<div class="statutory-note-card">
|
||||
<div class="statutory-note-head">附注一、收入构成</div>
|
||||
<div v-for="n in statutoryData.profit.notes.revenue_breakdown || []" :key="n.code" class="statutory-note-row">
|
||||
<span>{{ n.name }}({{ n.code }})</span>
|
||||
<span>{{ n.amount != null ? formatMoney(n.amount) : '-' }}</span>
|
||||
</div>
|
||||
<div class="statutory-note-row total">
|
||||
<span>营业收入合计</span>
|
||||
<span>{{ statutoryData.profit.notes.revenue_total != null ? formatMoney(statutoryData.profit.notes.revenue_total) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 费用构成 -->
|
||||
<div class="statutory-note-card">
|
||||
<div class="statutory-note-head">附注二、费用构成</div>
|
||||
<div v-for="n in statutoryData.profit.notes.expense_breakdown || []" :key="n.code" class="statutory-note-row">
|
||||
<span>{{ n.name }}({{ n.code }})</span>
|
||||
<span>{{ n.amount != null ? formatMoney(n.amount) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 财务费用拆解 -->
|
||||
<div class="statutory-note-card">
|
||||
<div class="statutory-note-head">附注三、财务费用拆解</div>
|
||||
<div v-for="n in statutoryData.profit.notes.finance_breakdown || []" :key="n.code" class="statutory-note-row">
|
||||
<span>{{ n.name }}({{ n.code }})</span>
|
||||
<span>{{ n.amount != null ? formatMoney(n.amount) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 板块勾稽 + 关键比率 -->
|
||||
<div class="statutory-note-card">
|
||||
<div class="statutory-note-head">附注四、板块勾稽</div>
|
||||
<div v-for="n in statutoryData.profit.notes.block_reconciliation || []" :key="n.key" class="statutory-note-row">
|
||||
<span>{{ n.name }}</span>
|
||||
<span>{{ formatMoney(n.amount) }}</span>
|
||||
</div>
|
||||
<div class="statutory-note-row total">
|
||||
<span>净利润</span>
|
||||
<span>{{ formatMoney(statutoryData.profit.notes.net_profit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statutory-note-card">
|
||||
<div class="statutory-note-head">附注五、关键比率</div>
|
||||
<div v-for="n in statutoryData.profit.notes.key_ratios || []" :key="n.name" class="statutory-note-row">
|
||||
<span>{{ n.name }}</span>
|
||||
<span>{{ n.value != null ? n.value.toFixed(2) + '%' : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 资产负债表(新准则科目分类) ── -->
|
||||
<div v-else-if="statutoryView === 'balance' && statutoryData.balance_sheet">
|
||||
<div class="statutory-sub-title">资产负债表 — 新30号准则科目分类({{ statutoryData.period }})</div>
|
||||
<el-table :data="balanceSheetRows" border stripe size="small" style="width:100%;margin-top:12px;" :row-class-name="statutoryBsRowClass">
|
||||
<el-table-column label="项目" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ fontWeight: row.is_section ? 700 : row.is_total ? 700 : 400, paddingLeft: row.is_section ? '0' : '16px' }">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新准则分类" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.ns_category_label && !row.is_section" size="small" :type="statutoryNsTagType(row.ns_category)">{{ row.ns_category_label }}</el-tag>
|
||||
<span v-else-if="row.is_section" style="color:#999;">{{ row.category_label }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="期末余额" width="150" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ fontWeight: row.is_section || row.is_total ? 700 : 400 }">{{ row.end_value != null ? formatMoney(row.end_value) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="期初余额" width="150" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ fontWeight: row.is_section || row.is_total ? 700 : 400 }">{{ row.begin_value != null ? formatMoney(row.begin_value) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据" width="80">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.is_demo && !row.is_section" class="new30-demo-tag">示例</span>
|
||||
<span v-else-if="row.is_section" style="color:#999;font-size:12px;">小计</span>
|
||||
<span v-else style="color:#999;font-size:12px;">凭证</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="statutory-totals">
|
||||
<div class="statutory-total-card" :class="{ ok: statutoryData.balance_sheet.totals?.balanced }">
|
||||
<div class="statutory-total-label">资产总计</div>
|
||||
<div class="statutory-total-val">{{ formatMoney(statutoryData.balance_sheet.totals?.assets?.end) }}</div>
|
||||
</div>
|
||||
<div class="statutory-total-card">
|
||||
<div class="statutory-total-label">负债合计</div>
|
||||
<div class="statutory-total-val">{{ formatMoney(statutoryData.balance_sheet.totals?.liabilities?.end) }}</div>
|
||||
</div>
|
||||
<div class="statutory-total-card">
|
||||
<div class="statutory-total-label">所有者权益合计</div>
|
||||
<div class="statutory-total-val">{{ formatMoney(statutoryData.balance_sheet.totals?.equity?.end) }}</div>
|
||||
</div>
|
||||
<div class="statutory-total-card" :class="{ ok: statutoryData.balance_sheet.totals?.balanced }">
|
||||
<div class="statutory-total-label">勾稽校验(资产=负债+权益)</div>
|
||||
<div class="statutory-total-val" style="font-size:14px;">{{ statutoryData.balance_sheet.totals?.balanced ? '✅ 平衡' : '⚠️ 不平' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 现金流量表(三活动) ── -->
|
||||
<div v-else-if="statutoryView === 'cashflow' && statutoryData.cash_flow">
|
||||
<div class="statutory-sub-title">现金流量表 — 新30号准则三活动({{ statutoryData.period }})</div>
|
||||
<div v-for="sec in statutoryData.cash_flow.sections || []" :key="sec.key" class="cf-section">
|
||||
<div class="cf-section-head">
|
||||
<span class="cf-section-name">{{ sec.name }}</span>
|
||||
<span class="cf-section-net" :class="{ negative: sec.net < 0 }">{{ formatMoney(sec.net) }}</span>
|
||||
</div>
|
||||
<div v-for="line in sec.lines" :key="line.code" class="cf-line">
|
||||
<span class="cf-line-name">{{ line.name }}</span>
|
||||
<span class="cf-line-code">{{ line.code }}</span>
|
||||
<span class="cf-line-value" :class="{ negative: line.value != null && line.value < 0 }">{{ line.value != null ? formatMoney(line.value) : '-' }}</span>
|
||||
<span v-if="line.is_demo" class="new30-demo-tag">示例</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cf-section">
|
||||
<div class="cf-section-head">
|
||||
<span class="cf-section-name">{{ statutoryData.cash_flow.fx_effect?.name }}</span>
|
||||
<span class="cf-section-net">{{ formatMoney(statutoryData.cash_flow.fx_effect?.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cf-summary">
|
||||
<div class="cf-summary-row">
|
||||
<span>现金及现金等价物净增加额</span>
|
||||
<span class="cf-summary-val">{{ formatMoney(statutoryData.cash_flow.summary?.net_increase) }}</span>
|
||||
</div>
|
||||
<div class="cf-summary-row">
|
||||
<span>加:期初现金及现金等价物余额</span>
|
||||
<span class="cf-summary-val">{{ formatMoney(statutoryData.cash_flow.summary?.begin_cash) }}</span>
|
||||
</div>
|
||||
<div class="cf-summary-row total">
|
||||
<span>期末现金及现金等价物余额</span>
|
||||
<span class="cf-summary-val">{{ formatMoney(statutoryData.cash_flow.summary?.end_cash) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -471,6 +665,48 @@ function openSubjectManage() {
|
||||
window.open('/subjects', '_blank')
|
||||
}
|
||||
|
||||
// ════ 对外法定报表(新30号准则三表合一)════
|
||||
const statutoryData = ref<any>({})
|
||||
const statutoryView = ref('profit')
|
||||
const statutoryDemoMode = ref(false)
|
||||
const statutoryLoading = ref(false)
|
||||
const exporting = ref(false)
|
||||
|
||||
async function loadStatutory() {
|
||||
statutoryLoading.value = true
|
||||
try {
|
||||
const r = await api.get('/reports/statutory', { params: { period: reportPeriod.value, entity_id: getEntityId() } })
|
||||
const d = (r as any).data || {}
|
||||
statutoryData.value = d
|
||||
statutoryDemoMode.value = !d.cash_flow?.all_items_have_data
|
||||
} catch (e) {
|
||||
ElMessage.error('加载法定报表失败')
|
||||
} finally {
|
||||
statutoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function exportStatutory() {
|
||||
exporting.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('cma_token') || ''
|
||||
const url = `/api/cma/reports/statutory/export?period=${reportPeriod.value}&entity_id=${getEntityId()}`
|
||||
const resp = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } })
|
||||
if (!resp.ok) throw new Error('导出失败')
|
||||
const blob = await resp.blob()
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `对外法定报表_${reportPeriod.value}.xlsx`
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 报表2:预算执行报告 ──
|
||||
const budgetSummary = ref({ total: 0, with_budget: 0, over_budget: 0, under_budget: 0, normal: 0 })
|
||||
const budgetItems = ref<any[]>([])
|
||||
@@ -654,6 +890,7 @@ function onBudgetSort(sort: any) {
|
||||
onMounted(() => {
|
||||
loadKpiOptions()
|
||||
loadAll()
|
||||
loadStatutory()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user