feat: 新30号准则P0 — 利润表五板块重构+费用分类打标
This commit is contained in:
+273
-2
@@ -15,7 +15,7 @@ from typing import Optional
|
||||
from datetime import datetime, date
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_role, require_auth
|
||||
from app.models import KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, User
|
||||
from app.models import KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, User, Subject
|
||||
from app.utils.deviation_engine import calc_period_deviation, calc_period_diff
|
||||
import logging
|
||||
|
||||
@@ -423,9 +423,280 @@ def _build_scorecard_from_kpis(db: Session, period: str) -> dict:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 杜邦分析 (CMA P2)
|
||||
# 利润表: 新30号准则五板块结构 (2027)
|
||||
# ============================================================
|
||||
|
||||
# 科目编码 → 新30号准则板块映射(PRD第118-145行)
|
||||
NEW_STANDARD_MAP = {
|
||||
# 经营类
|
||||
"6001": "operating", # 主营业务收入
|
||||
"6051": "operating", # 其他业务收入
|
||||
"6401": "operating", # 主营业务成本
|
||||
"6402": "operating", # 其他业务成本
|
||||
"6601": "operating", # 销售费用
|
||||
"6602": "operating", # 管理费用
|
||||
"660204": "operating_rd", # 研发费用(从管理费剥离)
|
||||
"6603": "operating_fx", # 经营汇兑损益
|
||||
"6701": "operating", # 经营资产减值损失
|
||||
|
||||
# 投资类
|
||||
"6011": "investing", # 利息收入(银行存款)
|
||||
"6111": "investing", # 投资收益
|
||||
"611101": "investing", # 股权投资
|
||||
"670101": "investing", # 投资类资产减值
|
||||
|
||||
# 筹资类
|
||||
"660301": "financing", # 利息支出(借款)
|
||||
"660302": "financing_fx", # 筹资汇兑损益
|
||||
|
||||
# 所得税
|
||||
"6801": "tax", # 所得税费用
|
||||
|
||||
# 终止经营
|
||||
"6901": "discontinued", # 终止经营损益
|
||||
}
|
||||
|
||||
# 板块 → 展示信息
|
||||
BLOCK_INFO = {
|
||||
"operating": {
|
||||
"name": "一、经营类损益",
|
||||
"short_name": "经营类",
|
||||
"items": [
|
||||
{"code": "6001", "name": "营业收入", "sign": 1},
|
||||
{"code": "6051", "name": "其他业务收入", "sign": 1},
|
||||
{"code": "6401", "name": "减:营业成本", "sign": -1},
|
||||
{"code": "6402", "name": "减:其他业务成本", "sign": -1},
|
||||
{"code": "6601", "name": "减:销售费用", "sign": -1},
|
||||
{"code": "6602", "name": "减:管理费用", "sign": -1},
|
||||
{"code": "660204", "name": "减:研发费用", "sign": -1},
|
||||
{"code": "6603", "name": "经营汇兑损益", "sign": 1},
|
||||
{"code": "6701", "name": "减:经营资产减值损失", "sign": -1},
|
||||
],
|
||||
"result_key": "operating_profit",
|
||||
"result_name": "经营利润",
|
||||
},
|
||||
"investing": {
|
||||
"name": "二、投资类损益",
|
||||
"short_name": "投资类",
|
||||
"items": [
|
||||
{"code": "6011", "name": "利息收入", "sign": 1},
|
||||
{"code": "6111", "name": "投资收益", "sign": 1},
|
||||
{"code": "670101", "name": "减:投资类资产减值", "sign": -1},
|
||||
],
|
||||
"result_key": "investing_profit",
|
||||
"result_name": "投资净收益",
|
||||
},
|
||||
"financing": {
|
||||
"name": "三、筹资类损益",
|
||||
"short_name": "筹资类",
|
||||
"items": [
|
||||
{"code": "660301", "name": "减:利息支出", "sign": -1},
|
||||
{"code": "660302", "name": "筹资汇兑损益", "sign": 1},
|
||||
],
|
||||
"result_key": "financing_profit",
|
||||
"result_name": "筹资费用净额",
|
||||
},
|
||||
"tax": {
|
||||
"name": "四、所得税费用",
|
||||
"short_name": "所得税",
|
||||
"items": [
|
||||
{"code": "6801", "name": "减:所得税费用", "sign": -1},
|
||||
],
|
||||
"result_key": "tax_profit",
|
||||
"result_name": "所得税费用",
|
||||
},
|
||||
"discontinued": {
|
||||
"name": "五、终止经营损益",
|
||||
"short_name": "终止经营",
|
||||
"items": [
|
||||
{"code": "6901", "name": "终止经营损益", "sign": 1},
|
||||
],
|
||||
"result_key": "discontinued_profit",
|
||||
"result_name": "终止经营损益",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _get_subject_amount(db: Session, code: str, period: str) -> Optional[float]:
|
||||
"""从 subjects + kpi_values 获取科目金额数据"""
|
||||
# 尝试从KPI数据获取(KPI编码与科目编码映射)
|
||||
kpi_code_map = {
|
||||
"6001": "F_REVENUE",
|
||||
"6051": "F_REVENUE_OTHER",
|
||||
"6401": "F_COST",
|
||||
"6402": "F_COST_OTHER",
|
||||
"6601": "F_SELLING_EXP",
|
||||
"6602": "F_ADMIN_EXP",
|
||||
"660204": "F_RD_EXP",
|
||||
"6603": "F_FINANCE_EXP",
|
||||
"6701": "F_IMPAIRMENT_LOSS",
|
||||
"6011": "F_INTEREST_INCOME",
|
||||
"6111": "F_INVEST_INCOME",
|
||||
"611101": "F_INVEST_INCOME",
|
||||
"660301": "F_INTEREST_EXP",
|
||||
"660302": "F_FX_LOSS",
|
||||
"6801": "F_TAX_EXP",
|
||||
"6901": "F_DISCONTINUED",
|
||||
}
|
||||
|
||||
# 1. 优先从 kpi_values 取
|
||||
if code in kpi_code_map:
|
||||
kpi_code = kpi_code_map[code]
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == kpi_code).first()
|
||||
if kpi:
|
||||
v = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id,
|
||||
KPIValue.period == period
|
||||
).order_by(KPIValue.id.desc()).first()
|
||||
if v and v.actual_value is not None:
|
||||
return float(v.actual_value)
|
||||
|
||||
# 2. 从 subjects + voucher_details 取(如果存在)
|
||||
try:
|
||||
from app.models import VoucherDetail
|
||||
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:
|
||||
return float(result)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/profit-statement")
|
||||
def get_profit_statement(
|
||||
period: str = Query(None, description="格式 YYYY-MM"),
|
||||
format: str = Query("old", description="old/new"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""利润表 — 支持旧格式和新30号准则五板块格式"""
|
||||
if period is None:
|
||||
period = datetime.now().strftime("%Y-%m")
|
||||
|
||||
if format == "old":
|
||||
# 旧30号准则格式(保留兼容)
|
||||
return get_profit_summary(period=period, db=db)
|
||||
|
||||
# === 新30号准则:五板块结构 ===
|
||||
blocks = []
|
||||
total_net_profit = 0
|
||||
all_items_have_data = True
|
||||
|
||||
for block_key in ["operating", "investing", "financing", "tax", "discontinued"]:
|
||||
block_cfg = BLOCK_INFO[block_key]
|
||||
items = []
|
||||
block_subtotal = 0
|
||||
block_has_data = False
|
||||
|
||||
for item_cfg in block_cfg["items"]:
|
||||
amount = _get_subject_amount(db, item_cfg["code"], period)
|
||||
if amount is not None:
|
||||
effective = amount * item_cfg["sign"]
|
||||
block_subtotal += effective
|
||||
block_has_data = True
|
||||
items.append({
|
||||
"code": item_cfg["code"],
|
||||
"name": item_cfg["name"],
|
||||
"amount": round(amount, 2) if amount is not None else None,
|
||||
"sign": item_cfg["sign"],
|
||||
"effective": round(amount * item_cfg["sign"], 2) if amount is not None else None,
|
||||
})
|
||||
|
||||
# Fallback: 使用PRD示例数据
|
||||
if not block_has_data:
|
||||
all_items_have_data = False
|
||||
block_subtotal = _get_demo_block_total(block_key)
|
||||
|
||||
block_result = {
|
||||
"key": block_key,
|
||||
"name": block_cfg["name"],
|
||||
"short_name": block_cfg["short_name"],
|
||||
"subtotal": round(block_subtotal, 2),
|
||||
"subtotal_name": block_cfg["result_name"],
|
||||
"items": items,
|
||||
"expanded": True,
|
||||
"has_real_data": block_has_data,
|
||||
}
|
||||
blocks.append(block_result)
|
||||
total_net_profit += block_subtotal
|
||||
|
||||
# 合计行:净利润 = 一二三+四+五
|
||||
return {
|
||||
"period": period,
|
||||
"format": "new",
|
||||
"title": f"利润表 — 新30号准则({period})",
|
||||
"blocks": blocks,
|
||||
"net_profit": round(total_net_profit, 2),
|
||||
"net_profit_name": "净利润",
|
||||
"all_items_have_data": all_items_have_data,
|
||||
"prev_period": None, # TODO: P1追溯调整
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@router.get("/category-map")
|
||||
def get_category_map(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""返回科目→新30号准则板块映射"""
|
||||
subjects_data = db.query(Subject).filter(Subject.is_active == 1).order_by(Subject.subject_code).all()
|
||||
|
||||
map_list = []
|
||||
for s in subjects_data:
|
||||
if s.new_standard_category:
|
||||
map_list.append({
|
||||
"subject_code": s.subject_code,
|
||||
"subject_name": s.subject_name,
|
||||
"category": s.new_standard_category,
|
||||
})
|
||||
|
||||
# 如果没有数据库数据,返回硬编码映射
|
||||
if not map_list:
|
||||
# 从 NEW_STANDARD_MAP 反向构造
|
||||
all_subjects = db.query(Subject).filter(Subject.is_active == 1).all()
|
||||
subj_map = {s.subject_code: s.subject_name for s in all_subjects}
|
||||
for code, cat in NEW_STANDARD_MAP.items():
|
||||
map_list.append({
|
||||
"subject_code": code,
|
||||
"subject_name": subj_map.get(code, code),
|
||||
"category": cat,
|
||||
})
|
||||
|
||||
# 按板块分组
|
||||
grouped = {"operating": [], "operating_rd": [], "operating_fx": [],
|
||||
"investing": [], "financing": [], "financing_fx": [],
|
||||
"tax": [], "discontinued": []}
|
||||
for m in map_list:
|
||||
cat = m["category"]
|
||||
if cat in grouped:
|
||||
grouped[cat].append(m)
|
||||
else:
|
||||
grouped.setdefault(cat, []).append(m)
|
||||
|
||||
return {
|
||||
"mapping": NEW_STANDARD_MAP,
|
||||
"subjects": map_list,
|
||||
"grouped": grouped,
|
||||
"total": len(map_list),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dupont")
|
||||
def get_dupont_analysis(
|
||||
entity: str = Query("bohai"),
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""会计科目管理 — 新30号准则适配"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from typing import Optional, List
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_role
|
||||
from app.models import Subject
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("cma.subjects")
|
||||
|
||||
router = APIRouter(prefix="/api/cma/subjects", tags=["会计科目"],
|
||||
dependencies=[Depends(require_role("ceo", "finance", "business"))],
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_subjects(
|
||||
category: Optional[str] = Query(None, description="新30号准则分类过滤"),
|
||||
keyword: Optional[str] = Query(None, description="科目名称/编码搜索"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""科目列表 — 支持新30号准则分类筛选"""
|
||||
query = db.query(Subject).filter(Subject.is_active == 1)
|
||||
|
||||
if category:
|
||||
query = query.filter(Subject.new_standard_category == category)
|
||||
|
||||
if keyword:
|
||||
like = f"%{keyword}%"
|
||||
query = query.filter(
|
||||
Subject.subject_name.like(like) | Subject.subject_code.like(like)
|
||||
)
|
||||
|
||||
subjects = query.order_by(Subject.subject_code).all()
|
||||
|
||||
return {
|
||||
"total": len(subjects),
|
||||
"data": [
|
||||
{
|
||||
"id": s.id,
|
||||
"subject_code": s.subject_code,
|
||||
"subject_name": s.subject_name,
|
||||
"parent_code": s.parent_code,
|
||||
"level": s.level,
|
||||
"category": s.category,
|
||||
"new_standard_category": s.new_standard_category,
|
||||
"is_active": s.is_active,
|
||||
"remark": s.remark,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
}
|
||||
for s in subjects
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{subject_id}")
|
||||
def update_subject_category(
|
||||
subject_id: int,
|
||||
new_standard_category: str = Query(..., description="operating/investing/financing/tax/discontinued"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新单个科目的新30号准则分类"""
|
||||
subject = db.query(Subject).filter(Subject.id == subject_id).first()
|
||||
if not subject:
|
||||
raise HTTPException(status_code=404, detail="科目不存在")
|
||||
|
||||
valid = {"operating", "operating_rd", "operating_fx", "investing",
|
||||
"financing", "financing_fx", "tax", "discontinued"}
|
||||
if new_standard_category not in valid:
|
||||
raise HTTPException(status_code=400, detail=f"无效的分类: {new_standard_category}")
|
||||
|
||||
subject.new_standard_category = new_standard_category
|
||||
db.commit()
|
||||
return {"message": "更新成功", "subject_id": subject_id, "new_standard_category": new_standard_category}
|
||||
|
||||
|
||||
@router.put("/batch/category")
|
||||
def batch_update_category(
|
||||
ids: List[int] = Query(..., description="科目ID列表"),
|
||||
new_standard_category: str = Query(..., description="operating/investing/financing/tax/discontinued"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""批量更新科目新30号准则分类"""
|
||||
valid = {"operating", "operating_rd", "operating_fx", "investing",
|
||||
"financing", "financing_fx", "tax", "discontinued"}
|
||||
if new_standard_category not in valid:
|
||||
raise HTTPException(status_code=400, detail=f"无效的分类: {new_standard_category}")
|
||||
|
||||
updated = db.query(Subject).filter(
|
||||
Subject.id.in_(ids), Subject.is_active == 1
|
||||
).update({"new_standard_category": new_standard_category}, synchronize_session=False)
|
||||
db.commit()
|
||||
return {"message": f"批量更新成功", "updated_count": updated}
|
||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from dotenv import load_dotenv
|
||||
from app.database import init_db
|
||||
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates
|
||||
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects
|
||||
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
||||
from scripts.erp_sync import run_sync as run_erp_sync
|
||||
from app.auth_middleware import require_auth
|
||||
@@ -66,6 +66,7 @@ app.include_router(entities.router)
|
||||
app.include_router(bsc_layers.router)
|
||||
app.include_router(okr.router)
|
||||
app.include_router(okr_templates.router)
|
||||
app.include_router(subjects.router)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
|
||||
@@ -404,3 +404,39 @@ class ScenarioSuggestion(Base):
|
||||
priority = Column(String(20), default="medium", comment="high/medium/low")
|
||||
sort_order = Column(Integer, default=0, comment="排序")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 新30号准则模型 (2027)
|
||||
# ============================================================
|
||||
|
||||
class Subject(Base):
|
||||
"""会计科目 — 新30号准则分类"""
|
||||
__tablename__ = "subjects"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
subject_code = Column(String(20), nullable=False, unique=True, comment="科目编码")
|
||||
subject_name = Column(String(200), nullable=False, comment="科目名称")
|
||||
parent_code = Column(String(20), nullable=True, comment="上级科目编码")
|
||||
level = Column(Integer, default=1, comment="科目级别 1-4")
|
||||
category = Column(String(50), nullable=True, comment="科目类别")
|
||||
new_standard_category = Column(String(20), nullable=True, comment="新30号准则分类: operating/investing/financing/tax/discontinued")
|
||||
is_active = Column(Integer, default=1, comment="是否启用")
|
||||
remark = Column(String(500), nullable=True, comment="备注")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class VoucherDetail(Base):
|
||||
"""凭证明细 — 新30号准则分类"""
|
||||
__tablename__ = "voucher_details"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
voucher_no = Column(String(50), nullable=False, comment="凭证编号")
|
||||
voucher_date = Column(DateTime, nullable=False, comment="凭证日期")
|
||||
subject_code = Column(String(20), nullable=False, comment="科目编码")
|
||||
subject_name = Column(String(200), nullable=True, comment="科目名称")
|
||||
debit_amount = Column(Float, default=0, comment="借方金额")
|
||||
credit_amount = Column(Float, default=0, comment="贷方金额")
|
||||
summary = Column(String(500), nullable=True, comment="摘要")
|
||||
new_standard_category = Column(String(20), nullable=True, comment="新30号准则分类")
|
||||
period = Column(String(20), nullable=True, comment="期间 YYYY-MM")
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""新30号准则适配 — 数据库迁移
|
||||
创建 subjects 和 voucher_details 表,添加 new_standard_category 字段
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from app.database import get_engine, get_session_local
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("migrate_new30")
|
||||
|
||||
def run():
|
||||
engine = get_engine()
|
||||
with engine.connect() as conn:
|
||||
# 1. 创建 subjects 表(会计科目表)
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS subjects (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
subject_code VARCHAR(20) NOT NULL COMMENT '科目编码',
|
||||
subject_name VARCHAR(200) NOT NULL COMMENT '科目名称',
|
||||
parent_code VARCHAR(20) DEFAULT NULL COMMENT '上级科目编码',
|
||||
level INT DEFAULT 1 COMMENT '科目级别 1-4',
|
||||
category VARCHAR(50) DEFAULT NULL COMMENT '科目类别: asset/liability/equity/revenue/expense/profit_loss',
|
||||
new_standard_category VARCHAR(20) DEFAULT NULL COMMENT '新30号准则分类: operating/investing/financing/tax/discontinued',
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT '是否启用',
|
||||
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_subject_code (subject_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会计科目表'
|
||||
"""))
|
||||
logger.info("✅ 创建 subjects 表")
|
||||
|
||||
# 2. 创建 voucher_details 表(凭证明细表)
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS voucher_details (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
voucher_no VARCHAR(50) NOT NULL COMMENT '凭证编号',
|
||||
voucher_date DATE NOT NULL COMMENT '凭证日期',
|
||||
subject_code VARCHAR(20) NOT NULL COMMENT '科目编码',
|
||||
subject_name VARCHAR(200) DEFAULT NULL COMMENT '科目名称',
|
||||
debit_amount DECIMAL(18,2) DEFAULT 0 COMMENT '借方金额',
|
||||
credit_amount DECIMAL(18,2) DEFAULT 0 COMMENT '贷方金额',
|
||||
summary VARCHAR(500) DEFAULT NULL COMMENT '摘要',
|
||||
new_standard_category VARCHAR(20) DEFAULT NULL COMMENT '新30号准则分类: operating/investing/financing/tax/discontinued',
|
||||
period VARCHAR(20) DEFAULT NULL COMMENT '期间 YYYY-MM',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_voucher_no (voucher_no),
|
||||
INDEX idx_period (period),
|
||||
INDEX idx_subject_code (subject_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证明细表'
|
||||
"""))
|
||||
logger.info("✅ 创建 voucher_details 表")
|
||||
|
||||
# 3. 插入初始科目数据(新30号准则相关科目)
|
||||
seed_subjects = [
|
||||
('6001', '主营业务收入', None, 1, 'revenue', 'operating'),
|
||||
('6051', '其他业务收入', None, 1, 'revenue', 'operating'),
|
||||
('6401', '主营业务成本', None, 1, 'expense', 'operating'),
|
||||
('6402', '其他业务成本', None, 1, 'expense', 'operating'),
|
||||
('6601', '销售费用', None, 1, 'expense', 'operating'),
|
||||
('6602', '管理费用', None, 1, 'expense', 'operating'),
|
||||
('660204', '研发费用', '6602', 2, 'expense', 'operating_rd'),
|
||||
('6603', '财务费用', None, 1, 'expense', 'operating_fx'),
|
||||
('660301', '利息支出(借款)', '6603', 2, 'expense', 'financing'),
|
||||
('660302', '筹资汇兑损益', '6603', 2, 'expense', 'financing_fx'),
|
||||
('6011', '利息收入(银行存款)', None, 1, 'revenue', 'investing'),
|
||||
('6111', '投资收益', None, 1, 'revenue', 'investing'),
|
||||
('611101', '股权投资', '6111', 2, 'revenue', 'investing'),
|
||||
('6701', '资产减值损失', None, 1, 'expense', 'operating'),
|
||||
('670101', '投资类资产减值', '6701', 2, 'expense', 'investing'),
|
||||
('6801', '所得税费用', None, 1, 'expense', 'tax'),
|
||||
('6901', '终止经营损益', None, 1, 'profit_loss', 'discontinued'),
|
||||
]
|
||||
for row in seed_subjects:
|
||||
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]})
|
||||
logger.info(f"✅ 插入 {len(seed_subjects)} 条初始科目数据")
|
||||
|
||||
conn.commit()
|
||||
logger.info("🎉 新30号准则数据库迁移完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -36,6 +36,7 @@ const routes = [
|
||||
{ path: 'data-quality', name: 'DataQuality', component: () => import('@/views/DataQuality.vue'), meta: { title: '数据质量', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } },
|
||||
{ path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'subjects', name: 'SubjectManage', component: () => import('@/views/SubjectManage.vue'), meta: { title: '科目打标', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
/>
|
||||
<el-button text size="small" :loading="loading" @click="loadAll">刷新</el-button>
|
||||
<el-button type="primary" size="small" @click="openDupont">📊 杜邦分析</el-button>
|
||||
<el-button type="success" size="small" @click="openSubjectManage">📋 科目打标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 四个标签页 -->
|
||||
<!-- 标签页 -->
|
||||
<el-tabs v-model="activeTab" type="border-card" class="report-tabs">
|
||||
|
||||
<!-- ════ 报表1:管理利润表 ════ -->
|
||||
@@ -61,6 +62,55 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ════ 新30号准则利润表 ════ -->
|
||||
<el-tab-pane label="📋 新30号准则" name="new30">
|
||||
<div v-loading="loadingNew30">
|
||||
<div class="report-desc">基于新30号准则的五板块利润表列报(2027版)。经营/投资/筹资分类列示,财务费用拆解。</div>
|
||||
|
||||
<div class="new30-blocks">
|
||||
<div v-for="block in new30Blocks" :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 }">
|
||||
{{ block.subtotal != null ? 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>
|
||||
<span v-if="item.effective != null && block.subtotal != null && block.subtotal !== 0" class="new30-item-ratio">
|
||||
{{ (Math.abs(item.effective) / Math.abs(block.subtotal) * 100).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="new30-net-profit">
|
||||
<div class="new30-profit-row">
|
||||
<span class="new30-profit-label">合计:</span>
|
||||
<span class="new30-profit-value" :class="{ negative: new30NetProfit < 0 }">
|
||||
{{ formatMoney(new30NetProfit) }}
|
||||
</span>
|
||||
<span class="new30-profit-name">净利润</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="new30-footer">
|
||||
<el-button size="small" :type="new30Format === 'new' ? 'primary' : ''" @click="switchNew30Format('new')">新准则</el-button>
|
||||
<el-button size="small" :type="new30Format === 'old' ? 'primary' : ''" @click="switchNew30Format('old')">旧准则对比</el-button>
|
||||
<el-button size="small" @click="exportNew30">导出</el-button>
|
||||
<span v-if="new30DemoMode" class="new30-demo-hint">⚠️ 当前显示示例数据,科目打标后显示实际数据</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ════ 报表2:预算执行报告 ════ -->
|
||||
<el-tab-pane label="📋 预算执行报告" name="budget">
|
||||
<div v-loading="loading">
|
||||
@@ -129,19 +179,14 @@
|
||||
<el-option label="30期" :value="30" />
|
||||
</el-select>
|
||||
</div>
|
||||
<!-- 趋势图表 -->
|
||||
<div v-if="trendData.length > 0" class="trend-grid">
|
||||
<el-card v-for="t in trendData" :key="t.kpi_id" class="trend-card-2">
|
||||
<template #header>
|
||||
<div class="trend-hd">
|
||||
<span class="trend-name">{{ t.kpi_name }}</span>
|
||||
<span class="trend-unit">{{ t.unit }}</span>
|
||||
<span class="trend-stat">
|
||||
均值 {{ t.avg }} | 最高 {{ t.max }} | 最低 {{ t.min }}
|
||||
</span>
|
||||
<span class="trend-dir" :class="t.trend_dir">
|
||||
{{ t.trend_dir === 'up' ? '↑ 上升' : t.trend_dir === 'down' ? '↓ 下降' : '→ 平稳' }}
|
||||
</span>
|
||||
<span class="trend-stat">均值 {{ t.avg }} | 最高 {{ t.max }} | 最低 {{ t.min }}</span>
|
||||
<span class="trend-dir" :class="t.trend_dir">{{ t.trend_dir === 'up' ? '↑ 上升' : t.trend_dir === 'down' ? '↓ 下降' : '→ 平稳' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div :ref="el => setTrendChartRef(t.kpi_id, el)" style="height:180px;"></div>
|
||||
@@ -156,17 +201,13 @@
|
||||
<div v-loading="loading">
|
||||
<div class="report-desc">基于BSC框架的四维度绩效评分,从已发布战略地图构建。</div>
|
||||
<div v-if="bscData.overall_score != null" style="margin-top:12px;">
|
||||
<!-- 总分 -->
|
||||
<div class="bsc-overall">
|
||||
<div class="bsc-score-ring" :style="{ borderColor: bscScoreColor(bscData.overall_score) }">
|
||||
<span class="bsc-score-val">{{ bscData.overall_score }}</span>
|
||||
<span class="bsc-score-label">综合评分</span>
|
||||
</div>
|
||||
<div class="bsc-map-info" v-if="bscData.map_title">
|
||||
基于:{{ bscData.map_title }} | 周期:{{ bscData.period }}
|
||||
</div>
|
||||
<div class="bsc-map-info" v-if="bscData.map_title">基于:{{ bscData.map_title }} | 周期:{{ bscData.period }}</div>
|
||||
</div>
|
||||
<!-- 四维度 -->
|
||||
<div class="bsc-dims">
|
||||
<div v-for="dim in bscData.dimensions" :key="dim.key" class="bsc-dim-card">
|
||||
<div class="bsc-dim-head" :style="{ borderLeft: '4px solid ' + dim.color }">
|
||||
@@ -233,6 +274,58 @@ function profitSummary(param: any) {
|
||||
return sums
|
||||
}
|
||||
|
||||
// ── 新30号准则利润表 ──
|
||||
const loadingNew30 = ref(false)
|
||||
const new30Blocks = ref<any[]>([])
|
||||
const new30NetProfit = ref(0)
|
||||
const new30Format = ref('new')
|
||||
const new30DemoMode = ref(false)
|
||||
|
||||
async function loadNew30() {
|
||||
loadingNew30.value = true
|
||||
try {
|
||||
const r = await api.get('/reports/profit-statement', {
|
||||
params: { period: reportPeriod.value, format: 'new' }
|
||||
})
|
||||
const d = (r as any).data || {}
|
||||
new30Blocks.value = d.blocks || []
|
||||
new30NetProfit.value = d.net_profit ?? 0
|
||||
new30DemoMode.value = !d.all_items_have_data
|
||||
} catch (e) {
|
||||
new30Blocks.value = []
|
||||
new30NetProfit.value = 0
|
||||
ElMessage.error('加载新30号准则利润表失败')
|
||||
} finally {
|
||||
loadingNew30.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatMoney(val: number): string {
|
||||
if (val == null) return '-'
|
||||
const abs = Math.abs(val)
|
||||
let formatted = '¥ ' + abs.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
if (val < 0) formatted = '-' + formatted
|
||||
return formatted
|
||||
}
|
||||
|
||||
function switchNew30Format(fmt: string) {
|
||||
new30Format.value = fmt
|
||||
if (fmt === 'new') {
|
||||
loadNew30()
|
||||
} else {
|
||||
// 切换到旧准则tab
|
||||
activeTab.value = 'profit'
|
||||
}
|
||||
}
|
||||
|
||||
function exportNew30() {
|
||||
ElMessage.success('导出功能将在P2实现')
|
||||
}
|
||||
|
||||
function openSubjectManage() {
|
||||
window.open('/subjects', '_blank')
|
||||
}
|
||||
|
||||
// ── 报表2:预算执行报告 ──
|
||||
const budgetSummary = ref({ total: 0, with_budget: 0, over_budget: 0, under_budget: 0, normal: 0 })
|
||||
const budgetItems = ref<any[]>([])
|
||||
@@ -324,7 +417,7 @@ async function loadKpiOptions() {
|
||||
|
||||
function loadAll() {
|
||||
loading.value = true
|
||||
Promise.all([loadProfit(), loadBudget(), loadTrends(), loadBsc()]).finally(() => { loading.value = false })
|
||||
Promise.all([loadProfit(), loadNew30(), loadBudget(), loadTrends(), loadBsc()]).finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
function openDupont() {
|
||||
@@ -347,7 +440,6 @@ function renderTrendCharts() {
|
||||
areaStyle: { opacity: 0.1 }, symbol: 'circle', symbolSize: 4,
|
||||
name: '实际值',
|
||||
}]
|
||||
// 目标线
|
||||
if (t.target_value != null) {
|
||||
series.push({
|
||||
type: 'line', data: Array(dates.length).fill(t.target_value),
|
||||
@@ -445,4 +537,31 @@ onMounted(() => {
|
||||
.bsc-kpi-score { font-weight: 600; color: #333; }
|
||||
.bsc-kpi-more { font-size: 11px; color: #409eff; text-align: center; padding: 2px; }
|
||||
.bsc-obj-empty { font-size: 11px; color: #ccc; text-align: center; padding: 4px; }
|
||||
|
||||
/* 新30号准则样式 */
|
||||
.new30-blocks { margin-top: 12px; }
|
||||
.new30-block { background: #fff; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); border: 1px solid #ebeef5; overflow: hidden; }
|
||||
.new30-block-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; cursor: pointer; background: #f9fafc; border-bottom: 1px solid #ebeef5; transition: background 0.2s; }
|
||||
.new30-block-header:hover { background: #f0f2f5; }
|
||||
.new30-toggle { font-size: 10px; color: #999; width: 16px; }
|
||||
.new30-block-name { font-weight: 600; font-size: 14px; flex: 1; color: #1a1a2e; }
|
||||
.new30-block-subtotal { font-size: 16px; font-weight: 700; color: #1a1a2e; min-width: 120px; text-align: right; }
|
||||
.new30-block-subtotal.negative { color: #f56c6c; }
|
||||
.new30-block-label { font-size: 11px; color: #999; width: 80px; }
|
||||
.new30-demo-tag { font-size: 10px; background: #fff7e6; color: #d46b08; padding: 1px 6px; border-radius: 3px; }
|
||||
.new30-block-body { padding: 6px 14px 10px 38px; }
|
||||
.new30-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; border-bottom: 1px dashed #f0f0f0; }
|
||||
.new30-item:last-child { border-bottom: none; }
|
||||
.new30-item-name { flex: 1; font-size: 13px; color: #555; }
|
||||
.new30-item-amount { font-size: 13px; font-weight: 500; min-width: 120px; text-align: right; color: #333; }
|
||||
.new30-item-amount.negative { color: #f56c6c; }
|
||||
.new30-item-ratio { font-size: 11px; color: #bbb; width: 60px; text-align: right; }
|
||||
.new30-net-profit { margin-top: 16px; padding: 12px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; color: #fff; }
|
||||
.new30-profit-row { display: flex; align-items: center; gap: 12px; }
|
||||
.new30-profit-label { font-size: 14px; opacity: 0.9; }
|
||||
.new30-profit-value { font-size: 24px; font-weight: 700; flex: 1; }
|
||||
.new30-profit-value.negative { color: #ffccc7; }
|
||||
.new30-profit-name { font-size: 13px; opacity: 0.8; }
|
||||
.new30-footer { margin-top: 12px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.new30-demo-hint { font-size: 11px; color: #d46b08; margin-left: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="subject-page">
|
||||
<div class="subject-header">
|
||||
<h3 class="page-title">📋 会计科目管理 — 新30号准则打标</h3>
|
||||
<div class="header-right">
|
||||
<el-button size="small" @click="goBack">← 返回报表</el-button>
|
||||
<el-button type="primary" size="small" :loading="loading" @click="loadSubjects">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 过滤 -->
|
||||
<div class="filter-row">
|
||||
<el-input v-model="keyword" placeholder="搜索科目名称/编码" size="small" style="width:200px;" clearable @change="loadSubjects" />
|
||||
<el-select v-model="filterCategory" placeholder="新准则分类" clearable size="small" style="width:160px;" @change="loadSubjects">
|
||||
<el-option label="经营类" value="operating" />
|
||||
<el-option label="研发费用" value="operating_rd" />
|
||||
<el-option label="经营汇兑" value="operating_fx" />
|
||||
<el-option label="投资类" value="investing" />
|
||||
<el-option label="筹资类" value="financing" />
|
||||
<el-option label="筹资汇兑" value="financing_fx" />
|
||||
<el-option label="所得税" value="tax" />
|
||||
<el-option label="终止经营" value="discontinued" />
|
||||
</el-select>
|
||||
<span class="filter-info">共 {{ totalSubjects }} 个科目</span>
|
||||
<span style="margin-left:auto;">
|
||||
<el-button size="small" type="success" :disabled="selectedIds.length === 0" @click="showBatchDialog">
|
||||
批量打标 ({{ selectedIds.length }})
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 科目表格 -->
|
||||
<el-table
|
||||
:data="subjects"
|
||||
border stripe size="small"
|
||||
style="width:100%;margin-top:8px;"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="40" />
|
||||
<el-table-column prop="subject_code" label="科目编码" width="100" />
|
||||
<el-table-column prop="subject_name" label="科目名称" min-width="160" />
|
||||
<el-table-column prop="parent_code" label="上级编码" width="80" />
|
||||
<el-table-column prop="level" label="级别" width="50" />
|
||||
<el-table-column label="新30号准则分类" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.new_standard_category"
|
||||
size="small"
|
||||
placeholder="选择分类"
|
||||
style="width:150px;"
|
||||
@change="val => updateCategory(row, val)"
|
||||
>
|
||||
<el-option label="未分类" value="" />
|
||||
<el-option label="经营类" value="operating" />
|
||||
<el-option label="研发费用(经营类)" value="operating_rd" />
|
||||
<el-option label="经营汇兑损益" value="operating_fx" />
|
||||
<el-option label="投资类" value="investing" />
|
||||
<el-option label="筹资类" value="financing" />
|
||||
<el-option label="筹资汇兑损益" value="financing_fx" />
|
||||
<el-option label="所得税" value="tax" />
|
||||
<el-option label="终止经营" value="discontinued" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category" label="科目类别" width="80" />
|
||||
<el-table-column label="分类标签" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.new_standard_category" :type="tagType(row.new_standard_category)" size="small">
|
||||
{{ catLabel(row.new_standard_category) }}
|
||||
</el-tag>
|
||||
<span v-else style="color:#ccc;">未分类</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 批量打标对话框 -->
|
||||
<el-dialog v-model="batchDialogVisible" title="批量设置新30号准则分类" width="400px">
|
||||
<div class="batch-dialog-body">
|
||||
<p>已选 <strong>{{ selectedIds.length }}</strong> 个科目</p>
|
||||
<el-select v-model="batchCategory" placeholder="选择分类" size="small" style="width:100%;">
|
||||
<el-option label="经营类" value="operating" />
|
||||
<el-option label="研发费用(经营类)" value="operating_rd" />
|
||||
<el-option label="经营汇兑损益" value="operating_fx" />
|
||||
<el-option label="投资类" value="investing" />
|
||||
<el-option label="筹资类" value="financing" />
|
||||
<el-option label="筹资汇兑损益" value="financing_fx" />
|
||||
<el-option label="所得税" value="tax" />
|
||||
<el-option label="终止经营" value="discontinued" />
|
||||
</el-select>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="batchDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" size="small" :loading="batchLoading" @click="doBatchUpdate">确认打标</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
|
||||
api.interceptors.request.use((config: any) => {
|
||||
const token = localStorage.getItem('cma_token')
|
||||
if (token) config.headers.Authorization = `'Bearer ' + token`
|
||||
return config
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const subjects = ref<any[]>([])
|
||||
const totalSubjects = ref(0)
|
||||
const keyword = ref('')
|
||||
const filterCategory = ref('')
|
||||
const selectedIds = ref<number[]>([])
|
||||
|
||||
// 批量打标
|
||||
const batchDialogVisible = ref(false)
|
||||
const batchCategory = ref('')
|
||||
const batchLoading = ref(false)
|
||||
|
||||
async function loadSubjects() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (filterCategory.value) params.category = filterCategory.value
|
||||
if (keyword.value) params.keyword = keyword.value
|
||||
const r = await api.get('/subjects', { params })
|
||||
const d = (r as any).data || {}
|
||||
subjects.value = d.data || []
|
||||
totalSubjects.value = d.total || 0
|
||||
} catch (e) {
|
||||
ElMessage.error('加载科目列表失败')
|
||||
subjects.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCategory(row: any, val: string) {
|
||||
if (!val) {
|
||||
// 清空分类
|
||||
row.new_standard_category = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.put(`/subjects/${row.id}`, null, {
|
||||
params: { new_standard_category: val }
|
||||
})
|
||||
ElMessage.success(`${row.subject_name} 分类更新成功`)
|
||||
} catch (e) {
|
||||
ElMessage.error('更新失败')
|
||||
// 回滚
|
||||
row.new_standard_category = row._old_category || ''
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: any[]) {
|
||||
selectedIds.value = rows.map((r: any) => r.id)
|
||||
}
|
||||
|
||||
function showBatchDialog() {
|
||||
if (selectedIds.value.length === 0) {
|
||||
ElMessage.warning('请先选择科目')
|
||||
return
|
||||
}
|
||||
batchCategory.value = ''
|
||||
batchDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function doBatchUpdate() {
|
||||
if (!batchCategory.value) {
|
||||
ElMessage.warning('请选择分类')
|
||||
return
|
||||
}
|
||||
batchLoading.value = true
|
||||
try {
|
||||
await api.put('/subjects/batch/category', null, {
|
||||
params: {
|
||||
ids: selectedIds.value,
|
||||
new_standard_category: batchCategory.value,
|
||||
}
|
||||
})
|
||||
ElMessage.success(`批量更新 ${selectedIds.value.length} 个科目成功`)
|
||||
batchDialogVisible.value = false
|
||||
await loadSubjects()
|
||||
} catch (e) {
|
||||
ElMessage.error('批量更新失败')
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function catLabel(cat: string): string {
|
||||
const map: Record<string, string> = {
|
||||
operating: '经营类', operating_rd: '研发费用', operating_fx: '经营汇兑',
|
||||
investing: '投资类', financing: '筹资类', financing_fx: '筹资汇兑',
|
||||
tax: '所得税', discontinued: '终止经营',
|
||||
}
|
||||
return map[cat] || cat
|
||||
}
|
||||
|
||||
function tagType(cat: string): string {
|
||||
const map: Record<string, string> = {
|
||||
operating: '', operating_rd: 'warning', operating_fx: 'info',
|
||||
investing: 'success', financing: 'danger', financing_fx: 'danger',
|
||||
tax: 'info', discontinued: 'warning',
|
||||
}
|
||||
return map[cat] || ''
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSubjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.subject-page { max-width: 1200px; margin: 0 auto; padding: 16px; }
|
||||
.subject-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.header-right { display: flex; gap: 8px; align-items: center; }
|
||||
.page-title { font-size: 18px; font-weight: 600; color: #1a1a2e; margin: 0; }
|
||||
.filter-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.filter-info { font-size: 12px; color: #999; }
|
||||
.batch-dialog-body { display: flex; flex-direction: column; gap: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user