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"),
|
||||
|
||||
Reference in New Issue
Block a user