Files
Hermes CI Fix 94aeb14e95 feat: 预算系统6项技术改进(告警归因/实际值自动归集/真零基/派生规则/告警路径统一/现金流分类)
P1-③ 告警归因: budget_deviation_alerts+alert_type/attribution/scenario_id, 归因引擎alert_attribution.py(子KPI/科目/量价差/趋势), deviation-check统一写归因+场景, GET /deviation-alerts/{id}/attribution详情(旧告警现场组装)
P1-④ 实际值自动归集: kpi_value_sources/kpi_value_collect_logs表+CRUD+试跑+覆盖率, 采集器kpi_value_collector.py(voucher_details/进销存/cash_plans按entity+period汇总, 幂等upsert不覆盖人工), crontab每日06:30
P2-① 真零基: budget_zero_based_items逐项论证表+generate, method-comparison有论证项逐项求和is_demo=false否则fallback
P2-② 派生规则: budget_derivation_rules配置表, apply-method优先读规则rule_source=configured
P2-⑤ 告警双路径合并: deviation_engine.build_deviation_alert统一函数, 方向列表配置化kpi_alert_higher_better+alert-direction接口
P2-⑥ 现金流分类: cash_plan_classify_rules规则表+cash_plan_unclassified待分类队列, sync-cash-plans未命中进队列不静默跳过
新增: GET /kpis/{kpi_id}/values + 前端kpiApi.values(归集标签页数据源), scenario_suggestions幂等seed(init_db)
测试: test_budget_tech_improve.py 15用例, 预算相关96 passed, 全量646 passed
2026-08-28 18:03:47 +08:00

914 lines
55 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""管理会计OS 数据模型"""
from sqlalchemy import Column, Integer, String, Text, Float, DateTime, ForeignKey, Boolean, JSON, func, UniqueConstraint, Numeric, Date
from app.database import Base
from app.models.budget_plan import BudgetPlan
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
from app.models.knowledge import KnowledgeEvent, KnowledgeSummary
from app.models.driver_budget import DriverFactorTemplate, DriverFactorBudget
from app.models.product_sales import ProductSales
class Entity(Base):
"""企业实体"""
__tablename__ = "entities"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, comment="企业全称")
short_name = Column(String(50), comment="企业简称")
industry = Column(String(50), comment="行业")
status = Column(String(20), default="active", comment="active/inactive/demo")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class User(Base):
"""用户"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(128), nullable=False)
name = Column(String(100), nullable=False)
role = Column(String(20), default="finance") # ceo / finance / business / it
phone = Column(String(20), nullable=True)
created_at = Column(DateTime, server_default=func.now())
class UserEntity(Base):
"""用户-企业授权(账套模式多对多)— 新表必须带entity_id(开发规范)"""
__tablename__ = "user_entities"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True, comment="用户ID")
entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, index=True, comment="企业ID(账套)")
granted_by = Column(Integer, nullable=True, comment="授权人")
created_at = Column(DateTime, server_default=func.now())
__table_args__ = (UniqueConstraint("user_id", "entity_id", name="uq_user_entity"),)
class StrategicMap(Base):
"""战略地图"""
__tablename__ = "strategic_maps"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (多租户隔离 2026-08-25)")
title = Column(String(200), nullable=False, comment="地图名称")
version = Column(String(20), default="v1.0", comment="版本号")
status = Column(String(20), default="draft", comment="draft/published")
dimensions = Column(JSON, nullable=True, comment="四维度和目标列表")
canvas_data = Column(JSON, nullable=True, comment="画布连线数据")
created_by = Column(Integer, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class KPIDefinition(Base):
"""KPI字典"""
__tablename__ = "kpi_definitions"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID")
map_id = Column(Integer, ForeignKey("strategic_maps.id"), nullable=True, comment="关联战略地图")
kpi_code = Column(String(50), unique=True, nullable=False, comment="KPI编码")
kpi_name = Column(String(200), nullable=False, comment="KPI名称")
dimension = Column(String(50), comment="所属维度: finance/customer/process/learning")
objective = Column(String(200), comment="关联战略目标")
formula = Column(Text, nullable=True, comment="计算公式")
formula_desc = Column(String(500), nullable=True, comment="公式说明")
data_source_type = Column(String(20), default="manual", comment="erp/business/excel/manual")
data_source_config = Column(JSON, nullable=True, comment="数据源配置")
data_source = Column(String(500), default="待补充", comment="数据来源")
data_owner = Column(String(100), default="待指定", comment="数据责任人")
frequency = Column(String(20), default="monthly", comment="daily/weekly/monthly/quarterly/yearly")
unit = Column(String(50), default="%", comment="单位")
target_value = Column(Float, nullable=True, comment="目标值(兼容旧字段)")
target_monthly = Column(Float, nullable=True, comment="月度目标值")
target_quarterly = Column(Float, nullable=True, comment="季度目标值")
target_yearly = Column(Float, nullable=True, comment="年度目标值")
target_calc_type = Column(String(20), nullable=True, comment="指标类型: accumulate累计(月×3=季,×12=年) / ratio比率(季/年沿用基准,可手调)")
threshold_green = Column(String(100), nullable=True, comment="绿灯阈值")
threshold_yellow = Column(String(100), nullable=True, comment="黄灯阈值")
threshold_red = Column(String(100), nullable=True, comment="红灯阈值")
category = Column(String(50), nullable=True, comment="BSC二级类别: revenue_growth/profitability/cost_control/asset_efficiency/cash_risk/customer_scale/customer_concentration/customer_satisfaction/supply_chain/delivery_quality/talent_pipeline/employee_engagement/innovation")
important_flag = Column(Integer, default=0, comment="是否重要数据(1=是, 数据分类分级 2026-08-26)")
data_level = Column(String(20), nullable=True, comment="数据分级: core核心/important重要/general一般")
data_category = Column(String(50), nullable=True, comment="行业参考分类(自定义,如金融/医疗/工业)")
responsible_dept = Column(String(200), nullable=True, comment="负责部门")
responsible_user = Column(String(100), nullable=True, comment="负责人")
kpi_level = Column(String(20), default="operational", comment="strategic/operational")
status = Column(String(20), default="active")
bot_source = Column(String(50), nullable=True, comment="Bot标识: finance-bot/ops-bot等")
epic = Column(String(50), default="Epic2", comment="所属Epic")
created_by = Column(Integer, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class KPIValue(Base):
"""KPI实际值"""
__tablename__ = "kpi_values"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=None, comment="企业ID (P2多租户隔离 2026-08-23, 按kpi_id回填)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False)
period = Column(String(20), nullable=False, comment="期间 2026-05")
actual_value = Column(Float, nullable=True, comment="实际值")
source_type = Column(String(20), default="manual", comment="erp/excel/manual")
source_batch = Column(String(100), nullable=True, comment="导入批次号")
data_status = Column(String(20), default="pending", comment="pending/verified/error")
calculated_at = Column(DateTime, server_default=func.now())
remark = Column(String(500), nullable=True)
class DataSourceConfig(Base):
"""数据源配置"""
__tablename__ = "data_source_config"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(200), nullable=False, comment="数据源名称")
source_type = Column(String(20), nullable=False, comment="erp/business/excel")
api_endpoint = Column(String(500), nullable=True, comment="API地址")
api_key = Column(String(200), nullable=True, comment="API Key")
query_sql = Column(Text, nullable=True, comment="SQL查询语句")
sync_type = Column(String(20), default="realtime", comment="realtime/batch")
status = Column(String(20), default="active")
last_sync_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now())
class KPIAlert(Base):
"""预警记录"""
__tablename__ = "kpi_alerts"
id = Column(Integer, primary_key=True, index=True)
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False)
kpi_value_id = Column(Integer, ForeignKey("kpi_values.id"), nullable=True)
alert_level = Column(String(20), default="yellow", comment="green/yellow/red")
alert_message = Column(String(500), nullable=False)
alert_type = Column(String(30), default="actual", comment="actual/forecast — 实际值超限/预测值超限")
suggestion = Column(Text, nullable=True, comment="情景建议")
action_plan_linked_id = Column(Integer, nullable=True, comment="关联的改善计划ID")
status = Column(String(20), default="pending", comment="pending/processing/resolved")
assignee = Column(String(100), nullable=True, comment="处理人")
resolution = Column(Text, nullable=True, comment="处理结果")
resolved_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now())
class OperationLog(Base):
"""操作日志"""
__tablename__ = "operation_logs"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, nullable=True)
action = Column(String(50), nullable=False, comment="create/update/delete/calculate/import")
target_type = Column(String(50), nullable=False, comment="kpi/map/alert/source")
target_id = Column(Integer, nullable=True)
detail = Column(JSON, nullable=True)
created_at = Column(DateTime, server_default=func.now())
class NotificationChannel(Base):
"""通知渠道配置"""
__tablename__ = "notification_channels"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, comment="渠道名称")
channel_type = Column(String(30), nullable=False, comment="wecom/mail/sms")
config = Column(JSON, nullable=True, comment="渠道配置")
enabled = Column(Boolean, default=True)
created_at = Column(DateTime, server_default=func.now())
class NotificationLog(Base):
"""通知发送日志"""
__tablename__ = "notification_logs"
id = Column(Integer, primary_key=True, index=True)
alert_id = Column(Integer, ForeignKey("kpi_alerts.id"), nullable=True)
channel = Column(String(30), nullable=False, comment="wecom/mail")
recipient = Column(String(200), nullable=True, comment="收件人")
title = Column(String(200), nullable=True)
content = Column(Text, nullable=True)
status = Column(String(20), default="pending", comment="pending/sent/failed")
error_msg = Column(String(500), nullable=True)
sent_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now())
class RolePermission(Base):
"""角色权限配置(单条记录,key-value"""
__tablename__ = "role_permissions"
id = Column(Integer, primary_key=True, index=True)
key = Column(String(50), unique=True, nullable=False, comment="配置键: route_permissions / action_permissions")
value = Column(JSON, nullable=False, comment="配置值")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class Objective(Base):
"""OKR目标"""
__tablename__ = "objectives"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
title = Column(String(200), nullable=False, comment="目标标题")
description = Column(Text, nullable=True, comment="目标描述")
dimension = Column(String(50), nullable=True, comment="关联维度: finance/customer/process/learning")
strategic_map_id = Column(Integer, ForeignKey("strategic_maps.id"), nullable=True, comment="关联战略地图")
quarter = Column(String(20), nullable=False, comment="季度: 2026Q3")
owner = Column(String(100), nullable=True, comment="负责人")
status = Column(String(20), default="active", comment="active/completed/cancelled")
progress = Column(Integer, default=0, comment="整体进度 0-100")
confidence = Column(Integer, default=5, comment="信心指数 1-10")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class ActionPlan(Base):
"""改善行动计划"""
__tablename__ = "action_plans"
id = Column(Integer, primary_key=True, index=True)
alert_id = Column(Integer, ForeignKey("kpi_alerts.id"), nullable=True, comment="关联预警")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=True, comment="关联OKR目标")
kr_id = Column(Integer, ForeignKey("krs.id"), nullable=True, comment="关联KR (行动挂KR 2026-08-27)")
title = Column(String(200), nullable=False, comment="计划标题")
description = Column(Text, nullable=True, comment="详细描述")
assignee = Column(String(100), nullable=True, comment="负责人")
priority = Column(String(20), default="medium", comment="high/medium/low")
due_date = Column(DateTime, nullable=True, comment="截止日期")
status = Column(String(20), default="pending", comment="pending/in_progress/completed/cancelled")
progress = Column(Integer, default=0, comment="完成进度 0-100")
result = Column(Text, nullable=True, comment="改善结果")
monthly_milestones = Column(JSON, nullable=True, comment="月度里程碑: [{\"month\":\"2026-07\",\"label\":\"...\",\"status\":\"completed\"}]")
auto_verify_rule = Column(JSON, nullable=True, comment="自动验证规则: {\"condition\": \"value > target\"}")
verify_result = Column(String(20), nullable=True, comment="验证结果: pass/fail/pending")
verify_log = Column(JSON, nullable=True, comment="验证历史日志")
verify_status = Column(String(20), default="pending", comment="验证状态: pending/passed/failed/retrying/escalated")
verify_attempts = Column(Integer, default=0, comment="验证尝试次数")
verified_at = Column(DateTime, nullable=True, comment="验证完成时间")
kpi_current_before = Column(Float, nullable=True, comment="执行前KPI值")
kpi_current_after = Column(Float, nullable=True, comment="执行后KPI值")
created_by = Column(String(100), nullable=True, comment="创建人")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class OrgNode(Base):
"""组织节点: 集团→事业部→区域→部门→班组 5级"""
__tablename__ = "org_nodes"
id = Column(Integer, primary_key=True, index=True)
parent_id = Column(Integer, ForeignKey("org_nodes.id"), nullable=True, comment="父节点ID")
name = Column(String(100), nullable=False, comment="节点名称")
code = Column(String(50), unique=True, nullable=True, comment="编码")
level = Column(Integer, nullable=False, comment="1=集团 2=事业部 3=区域 4=部门 5=班组")
sort_order = Column(Integer, default=0, comment="排序")
enabled = Column(Integer, default=1, comment="1启用 0禁用")
path = Column(String(500), nullable=True, comment="路径")
remark = Column(String(200), nullable=True, comment="备注")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class StrategicMapVersion(Base):
"""战略地图版本快照"""
__tablename__ = "strategic_map_versions"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (OpenCode审查#9 2026-08-26)")
map_id = Column(Integer, ForeignKey("strategic_maps.id", ondelete="CASCADE"), nullable=False, comment="关联地图")
version = Column(String(20), nullable=False, comment="版本号 v1.0 v1.1 ...")
dimensions = Column(JSON, nullable=False, comment="维度数据快照")
canvas_data = Column(JSON, nullable=False, comment="画布数据快照")
comment = Column(String(500), nullable=True, comment="说明")
created_by = Column(Integer, nullable=True)
created_at = Column(DateTime, server_default=func.now())
class ReviewRecord(Base):
"""复盘记录 — 战略回顾会/运营复盘会结论持久化(大PDCA Act闭环 2026-08-27"""
__tablename__ = "review_records"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (多租户隔离 2026-08-27)")
map_id = Column(Integer, ForeignKey("strategic_maps.id", ondelete="CASCADE"), nullable=False, comment="关联战略地图")
review_type = Column(String(20), default="quarterly", comment="monthly/quarterly 月度运营复盘/季度战略回顾")
review_date = Column(Date, nullable=False, comment="复盘日期")
summary = Column(Text, nullable=False, comment="复盘结论(为什么没达成/怎么调整)")
adjustments = Column(JSON, nullable=True, comment="调整项JSON [{type,target,action}]")
next_priorities = Column(JSON, nullable=True, comment="下阶段优先级 [str,...]")
created_by = Column(Integer, nullable=True, comment="创建人用户ID")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class MapObjective(Base):
"""战略地图目标: 每个维度下的具体目标"""
__tablename__ = "map_objectives"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (多租户隔离 2026-08-25)")
map_id = Column(Integer, ForeignKey("strategic_maps.id", ondelete="CASCADE"), nullable=False, comment="关联地图")
dimension_key = Column(String(50), nullable=False, comment="所属维度: finance/customer/process/learning")
name = Column(String(200), nullable=False, comment="目标名称")
description = Column(Text, nullable=True, comment="描述")
icon = Column(String(50), default="target", comment="图标标识")
kpis = Column(JSON, nullable=True, comment="关联KPI编码数组")
sort_order = Column(Integer, default=0, comment="排序")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class KPICausality(Base):
"""KPI因果链 — 记录KPI间的因果关系"""
__tablename__ = "kpi_causality"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (多租户隔离 2026-08-27)")
source_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="源KPI(因)")
target_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="目标KPI(果)")
strength = Column(Float, default=0.5, comment="影响强度 0~1")
lag_months = Column(Integer, default=1, comment="滞后期(月)")
formula = Column(String(500), nullable=True, comment="影响公式描述")
direction = Column(String(10), default="positive", comment="positive/negative 正向/负向影响")
source_type = Column(String(20), default="manual", comment="建链来源 AI_suggested/manual/imported (2026-08-27 验证机制)")
verify_status = Column(String(20), default="pending", comment="验证状态 pending/data_verified/human_verified/disputed (2026-08-27)")
verified_at = Column(DateTime, nullable=True, comment="验证时间")
verified_by = Column(String(50), nullable=True, comment="验证人/AI/脚本")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class MpmResult(Base):
"""MPM财务Bot分析结果记录"""
__tablename__ = "mpm_results"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, comment="企业实体ID")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
source = Column(String(50), default="finance-bot", comment="来源Bot标识")
raw_data = Column(JSON, nullable=False, comment="完整的MPM计算结果")
created_at = Column(DateTime, server_default=func.now())
class KpiForecastLog(Base):
"""KPI预测历史 — 预测偏差告警数据源 (2026-08-25 升级2a)"""
__tablename__ = "kpi_forecast_log"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID")
kpi_id = Column(Integer, nullable=False, comment="KPI ID")
kpi_code = Column(String(50), nullable=False, comment="KPI编码")
period = Column(String(20), nullable=False, comment="预测期间")
forecast_value = Column(Float, nullable=True, comment="预测值")
model = Column(String(30), default="linear", comment="预测模型")
confidence = Column(String(10), nullable=True, comment="置信度")
trend = Column(String(10), nullable=True, comment="趋势")
created_at = Column(DateTime, server_default=func.now())
class BotBridgeConfig(Base):
"""Bot桥接鉴权配置"""
__tablename__ = "bot_bridge_config"
id = Column(Integer, primary_key=True, index=True)
bot_name = Column(String(50), unique=True, nullable=False, comment="Bot名称")
token = Column(String(64), nullable=False, comment="鉴权Token")
is_active = Column(Boolean, default=True, comment="是否激活")
created_at = Column(DateTime, server_default=func.now())
class KpiDataQualityLog(Base):
"""数据质量监控日志"""
__tablename__ = "kpi_data_quality_log"
id = Column(Integer, primary_key=True, index=True)
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
check_type = Column(String(30), nullable=False, comment="abnormal_change/flat_data/missing_data/value_outlier")
severity = Column(String(20), default="warning", comment="info/warning/critical")
detail = Column(JSON, nullable=True, comment="检测详情")
suggestion = Column(String(500), nullable=True, comment="建议操作")
status = Column(String(20), default="open", comment="open/resolved/ignored")
resolved_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now())
class BiReportTemplate(Base):
"""BI报表模板"""
__tablename__ = "bi_report_templates"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
name = Column(String(200), nullable=False, comment="模板名称")
report_type = Column(String(50), nullable=False, comment="overview/trend/comparison/topn/causality")
config = Column(JSON, nullable=False, comment="报表配置")
is_system = Column(Integer, default=0, comment="系统预置模板")
created_by = Column(Integer, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BiReport(Base):
"""用户保存的BI报表"""
__tablename__ = "bi_reports"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
template_id = Column(Integer, ForeignKey("bi_report_templates.id"), nullable=True)
name = Column(String(200), nullable=False, comment="报表名称")
config = Column(JSON, nullable=False, comment="报表配置(行/列/值)")
chart_type = Column(String(50), default="auto", comment="图表类型")
is_shared = Column(Integer, default=0, comment="是否分享")
created_by = Column(Integer, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BscLayerConfig(Base):
"""BSC四层配置 — 不同企业的权重配置"""
__tablename__ = "bsc_layer_config"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, comment="企业ID")
layer = Column(String(20), nullable=False, comment="financial/customer/process/learning")
weight = Column(Float, nullable=False, comment="该层权重(%")
kpi_count_min = Column(Integer, default=2, comment="最少KPI数")
kpi_count_max = Column(Integer, default=5, comment="最多KPI数")
class KPIHierarchy(Base):
"""KPI层级关系 — 公司→部门→个人三级分解"""
__tablename__ = "kpi_hierarchy"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), default=1, comment="企业ID")
parent_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="上级KPI")
child_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="下级KPI")
level = Column(Integer, default=1, comment="1=公司级 2=部门级 3=个人级")
weight = Column(Float, default=1.0, comment="下级对上级的贡献权重(%)")
child_name = Column(String(200), nullable=True, comment="下级节点名称(个人或部门名)")
created_at = Column(DateTime, server_default=func.now())
# 兼容性: P2开发新增的模板API需要的模型
# KPI模板(独立表)
class KPITemplate(Base):
__tablename__ = "kpi_templates"
id = Column(Integer, primary_key=True, index=True)
kpi_code = Column(String(50), unique=True, nullable=False)
kpi_name = Column(String(200), nullable=False)
dimension = Column(String(50))
category = Column(String(50))
formula = Column(Text)
formula_desc = Column(String(500))
unit = Column(String(50))
target_value = Column(Float)
description = Column(Text)
is_system = Column(Integer, default=0)
usage_count = Column(Integer, default=0)
created_by = Column(Integer)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class OKRTemplate(Base):
"""OKR模板库"""
__tablename__ = "okr_templates"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, comment="O名称")
description = Column(Text, comment="O描述")
dimension = Column(String(20), nullable=False, comment="finance/customer/process/learning")
layer = Column(String(20), default="level1", comment="level1/level2/level3")
industry_tag = Column(String(50), default="general", comment="行业标签")
preset_krs = Column(JSON, nullable=False, comment="预设关键结果列表")
source = Column(String(20), default="system", comment="system/user/industry_pack")
owner = Column(String(50), default="CMA标准库", comment="模板负责人(治理P2: 可追踪)")
use_count = Column(Integer, default=0, comment="使用次数")
sort_order = Column(Integer, default=0)
is_active = Column(Integer, default=1)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class CashForecast(Base):
"""现金流预测 — 每日未来30天预测"""
__tablename__ = "cash_forecasts"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, comment="企业ID")
forecast_date = Column(DateTime, nullable=False, comment="预测日期(每天一条)")
predicted_cash = Column(Float, nullable=True, comment="预测现金余额")
lower_bound = Column(Float, nullable=True, comment="置信区间下界")
upper_bound = Column(Float, nullable=True, comment="置信区间上界")
alert_status = Column(String(20), default="green", comment="green/yellow/red")
created_at = Column(DateTime, server_default=func.now())
class CashPlan(Base):
"""收付款计划 — 资金管理智能体(唯一应收载体:含回款登记、负责人、数据来源)"""
__tablename__ = "cash_plans"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), default=1, comment="企业ID")
plan_type = Column(String(10), nullable=False, comment="receive收/pay付")
related_kpi_id = Column(Integer, nullable=True, comment="关联KPI(预算联动 2026-08-27)")
budget_plan_id = Column(Integer, nullable=True, comment="来源预算计划ID")
amount = Column(Float, nullable=False, comment="金额(万元)")
plan_date = Column(DateTime, nullable=False, comment="计划日期(应收即到期日)")
counterparty = Column(String(200), nullable=True, comment="关联客户/供应商")
description = Column(String(500), nullable=True, comment="说明")
status = Column(String(20), default="pending", comment="pending/completed/cancelled")
owner = Column(String(100), nullable=True, comment="负责人/业务员(应收催收责任人)")
source = Column(String(50), default="manual", comment="数据来源: manual/bohai_ar/receivables_migrate")
paid_amount = Column(Float, default=0, comment="已回款金额(万元)")
completed_at = Column(DateTime, nullable=True, comment="完成时间")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class SystemConfig(Base):
"""系统配置 — key-value存储"""
__tablename__ = "system_configs"
id = Column(Integer, primary_key=True, index=True)
config_key = Column(String(100), unique=True, nullable=False, comment="配置键")
config_value = Column(String(500), nullable=True, comment="配置值")
description = Column(String(500), nullable=True, comment="配置说明")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BudgetDeviationAlert(Base):
"""预算偏差预警记录"""
__tablename__ = "budget_deviation_alerts"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
budget_value = Column(Float, nullable=True, comment="预算值")
actual_value = Column(Float, nullable=True, comment="实际值")
deviation_rate = Column(Float, nullable=True, comment="偏差率 %")
deviation_value = Column(Float, nullable=True, comment="偏差绝对值")
alert_level = Column(String(20), default="warning", comment="warning/critical")
status = Column(String(20), default="open", comment="open/resolved/ignored")
suggestion = Column(String(500), nullable=True, comment="处理建议")
alert_type = Column(String(30), nullable=True, comment="归因场景: cost_high/revenue_drop/cash_low/cash_critical (2026-08-28 告警归因P1-③)")
attribution = Column(JSON, nullable=True, comment="归因JSON: 子KPI拆解+科目拆解+量价差+趋势 (2026-08-28)")
scenario_id = Column(Integer, nullable=True, comment="FK scenario_suggestions.id 场景建议 (2026-08-28)")
created_at = Column(DateTime, server_default=func.now())
class ReportHistory(Base):
"""自动生成的经营分析报告记录"""
__tablename__ = "report_history"
id = Column(Integer, primary_key=True, index=True)
report_type = Column(String(20), nullable=False, comment="weekly/monthly/special")
period = Column(String(20), nullable=False, comment="期间: 2026-W30 / 2026-07 / 2026-Q2")
title = Column(String(200), nullable=False, comment="报告标题")
markdown_content = Column(Text, nullable=True, comment="Markdown格式报告(用于微信推送)")
json_content = Column(JSON, nullable=True, comment="JSON结构化数据(写入CMA系统)")
status = Column(String(20), default="generated", comment="generated/pushed/failed")
trigger_type = Column(String(20), default="manual", comment="manual/scheduled/event")
alert_ref = Column(String(50), nullable=True, comment="事件触发时的预警引用")
created_at = Column(DateTime, server_default=func.now())
class AnalysisResult(Base):
"""财务Bot分析结论 — 带置信度评分"""
__tablename__ = "analysis_results"
id = Column(Integer, primary_key=True, index=True)
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
conclusion = Column(String(1000), nullable=False, comment="分析结论")
confidence = Column(Integer, nullable=False, comment="置信度 0-100")
data_source = Column(String(500), nullable=True, comment="数据来源")
calculation_logic = Column(String(1000), nullable=True, comment="计算逻辑")
comparable_benchmark = Column(String(500), nullable=True, comment="可比基准")
limitations = Column(String(1000), nullable=True, comment="局限说明")
has_actual = Column(Integer, default=0, comment="有实际值")
has_target = Column(Integer, default=0, comment="有目标值")
has_trend = Column(Integer, default=0, comment="有历史趋势")
has_review = Column(Integer, default=0, comment="有人工复核")
kpi_code = Column(String(50), nullable=True, comment="关联KPI编码")
kpi_name = Column(String(200), nullable=True, comment="关联KPI名称")
created_at = Column(DateTime, server_default=func.now())
class ForecastAccuracy(Base):
"""预测准确率 — 上期预测 vs 本期实际"""
__tablename__ = "forecast_accuracy"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, comment="企业ID")
period = Column(String(20), nullable=False, comment="期间 2026-07")
forecast_value = Column(Float, nullable=True, comment="预测值")
actual_value = Column(Float, nullable=True, comment="实际值")
mae = Column(Float, nullable=True, comment="绝对误差")
mape = Column(Float, nullable=True, comment="百分比误差")
created_at = Column(DateTime, server_default=func.now())
class ScenarioSuggestion(Base):
"""情景建议模板 — 根据不同预警类型自动生成建议"""
__tablename__ = "scenario_suggestions"
id = Column(Integer, primary_key=True, index=True)
alert_type = Column(String(30), nullable=False, comment="预警类型: cash_low/cash_critical/cost_high/revenue_drop")
title = Column(String(200), nullable=False, comment="建议标题")
description = Column(Text, nullable=True, comment="详细建议")
action_template = Column(Text, nullable=True, comment="改善行动模板")
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")
important_flag = Column(Integer, default=0, comment="是否重要数据(1=是, 数据分类分级 2026-08-26)")
data_level = Column(String(20), nullable=True, comment="数据分级: core核心/important重要/general一般")
data_category = Column(String(50), nullable=True, comment="行业参考分类(自定义,如金融/医疗/工业)")
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号准则分类 (网银流水导入 2026-08-28)"""
__tablename__ = "voucher_details"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, nullable=False, comment="企业ID (多租户隔离)")
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="摘要")
carry_forward = Column(Integer, default=0, nullable=False, comment="结转行标记(1=结转行不参与现金流)")
new_standard_category = Column(String(20), nullable=True, comment="新30号准则分类")
period = Column(String(20), nullable=True, comment="期间 YYYY-MM")
batch = Column(String(100), nullable=True, comment="导入批次号")
created_at = Column(DateTime, server_default=func.now())
class ImportLog(Base):
"""数据导入日志 — 网银流水/Excel导入批次记录 (2026-08-28)"""
__tablename__ = "import_logs"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, nullable=False, comment="企业ID (多租户隔离)")
filename = Column(String(500), nullable=False, comment="文件名")
batch = Column(String(100), nullable=False, comment="批次号")
total_rows = Column(Integer, nullable=True, comment="总行数")
success_rows = Column(Integer, nullable=True, comment="成功行数")
failed_rows = Column(Integer, nullable=True, comment="失败行数")
errors = Column(JSON, nullable=True, comment="失败详情 [{row, field, reason}]")
period = Column(String(20), nullable=True, comment="导入期间")
import_type = Column(String(20), nullable=True, comment="导入类型: vouchers/kpi")
created_by = Column(String(100), nullable=True, comment="导入人")
created_at = Column(DateTime, server_default=func.now())
# ============================================================
# 费用审核智能体 (2026-08)
# ============================================================
class ExpenseRule(Base):
"""费用规则 — 自动校验报销单的标准"""
__tablename__ = "expense_rules"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
rule_name = Column(String(100), nullable=False, comment="规则名称")
dimension = Column(String(20), nullable=False, comment="维度: department/person/expense_type")
dimension_value = Column(String(100), nullable=True, comment="维度值: 部门名/人员名/费用类型(空=全局)")
expense_type = Column(String(30), nullable=False, comment="费用类型: entertainment/travel/office/management")
limit_type = Column(String(20), nullable=False, comment="限额类型: single/monthly/yearly 单笔/月度累计/年度累计")
limit_amount = Column(Float, nullable=False, comment="限额金额")
cycle = Column(String(20), default="monthly", comment="周期: single/monthly/yearly")
status = Column(String(20), default="active", comment="active/inactive")
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 ExpenseReimbursement(Base):
"""费用报销单 — 提交后自动校验规则,超限自动打回"""
__tablename__ = "expense_reimbursements"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
reimb_no = Column(String(50), unique=True, nullable=False, comment="报销单号")
applicant = Column(String(100), nullable=False, comment="申请人")
department = Column(String(100), nullable=True, comment="部门")
expense_type = Column(String(30), nullable=False, comment="费用类型: entertainment/travel/office/management")
title = Column(String(200), nullable=False, comment="事由/摘要")
amount = Column(Float, nullable=False, comment="报销金额")
expense_date = Column(DateTime, nullable=True, comment="费用发生日期")
attachment = Column(String(500), nullable=True, comment="附件文件名")
status = Column(String(20), default="pending", comment="pending待审批/approved已通过/rejected已拒绝/returned已打回")
check_result = Column(String(20), default="pass", comment="自动校验结果: pass/fail")
check_reason = Column(String(1000), nullable=True, comment="超限原因")
check_detail = Column(JSON, nullable=True, comment="校验明细: [{rule_name, limit, actual, passed}]")
checked_at = Column(DateTime, nullable=True, comment="自动校验时间")
approver = Column(String(100), nullable=True, comment="审批人")
approve_comment = Column(String(500), nullable=True, comment="审批意见")
approved_at = Column(DateTime, nullable=True, comment="审批时间")
created_by = Column(String(100), nullable=True, comment="提交人")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# ============================================================
# 税务合规智能体 (2026-08)
# ① 税负监控 ② 发票校验 ③ 社保比对
# ============================================================
class TaxRecord(Base):
"""税务记录 — 税负监控:应纳税额/实缴额/税负率 vs 行业基准"""
__tablename__ = "tax_records"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
tax_type = Column(String(30), nullable=False, comment="税种: vat增值税/income所得税/surtax附加税")
tax_payable = Column(Float, default=0, comment="应纳税额")
tax_paid = Column(Float, default=0, comment="实缴税额")
tax_rate = Column(Float, nullable=True, comment="适用税率 %")
income = Column(Float, default=0, comment="当期收入/计税收入(税负率分母)")
tax_burden_rate = Column(Float, nullable=True, comment="税负率 % = 实缴税额/收入×100")
burden_status = Column(String(20), default="normal", comment="normal正常/warning超基准±20%内/alert超基准±20%")
warning_msg = Column(String(500), nullable=True, 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 InvoiceCheck(Base):
"""发票校验 — 录入后按规则自动校验:号码格式/金额匹配报销单/供应商匹配合同"""
__tablename__ = "invoice_check"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID")
invoice_no = Column(String(50), nullable=False, comment="发票号码")
amount = Column(Float, nullable=False, comment="发票金额")
invoice_type = Column(String(30), default="vat", comment="发票类型: vat专用/vat普通/electronic电子/other其他")
invoice_date = Column(DateTime, nullable=True, comment="开票日期")
supplier = Column(String(200), nullable=True, comment="供应商名称")
reimb_no = Column(String(50), nullable=True, comment="关联报销单号")
contract_no = Column(String(50), nullable=True, comment="关联合同编号")
check_status = Column(String(20), default="pending", comment="校验状态: pending待校验/valid通过/invalid异常/warning提醒")
check_result = Column(JSON, nullable=True, comment="校验明细: [{rule, passed, message}]")
check_reason = Column(String(1000), nullable=True, comment="异常原因汇总")
checked_at = Column(DateTime, nullable=True, comment="校验时间")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class SocialSecurity(Base):
"""社保缴费记录 — 社保比对:基数与工资匹配/单位缴纳比例/漏缴提醒"""
__tablename__ = "social_security"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID")
employee = Column(String(100), nullable=False, comment="人员姓名")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
base_amount = Column(Float, default=0, comment="缴费基数")
salary = Column(Float, nullable=True, comment="申报工资")
company_amount = Column(Float, default=0, comment="单位缴纳金额")
personal_amount = Column(Float, default=0, comment="个人缴纳金额")
company_rate = Column(Float, nullable=True, comment="单位缴纳比例 % (养老16%+医疗8%+失业0.5%≈24.5%)")
check_status = Column(String(20), default="normal", comment="normal正常/warning基数或比例异常/alert漏缴")
warning_msg = Column(String(500), nullable=True, 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())
# ============================================================
# 本体三支柱: 科目↔KPI↔OKR 三层互联 (2026-08-19)
# 追溯链: 目标(O) → 指标(KPI) → 科目(数据)
# ============================================================
class KPISubjectMap(Base):
"""科目↔KPI映射 — 指标计算依赖的底层会计科目"""
__tablename__ = "kpi_subject_map"
id = Column(Integer, primary_key=True, index=True)
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="KPI ID")
subject_code = Column(String(20), nullable=False, comment="科目编码")
calc_type = Column(String(20), default="sum", comment="sum/avg/ratio/other")
weight = Column(Numeric(5, 2), default=1.00, comment="权重(负=扣减项)")
remark = Column(String(200), nullable=True, comment="备注")
__table_args__ = (UniqueConstraint("kpi_id", "subject_code", name="uk_kpi_subject"),)
class ObjectiveKPI(Base):
"""KPI↔O支撑 — 目标由哪些KPI度量"""
__tablename__ = "objective_kpi"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=False, comment="OKR目标ID")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="KPI ID")
weight = Column(Numeric(5, 2), default=1.00, comment="支撑权重")
__table_args__ = (UniqueConstraint("objective_id", "kpi_id", name="uk_obj_kpi"),)
class KR(Base):
"""关键结果KR — OKR完整化 (O→KR→KPI)"""
__tablename__ = "krs"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=False, comment="OKR目标ID")
title = Column(String(200), nullable=False, comment="KR标题")
metric_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=True, comment="度量KPI ID")
operator = Column(String(5), default=">=", nullable=False, comment="方向符号: >=/<=/>/</= (KR完整修复 2026-08-27)")
tolerance = Column(Numeric(10, 4), nullable=True, comment="容差(仅=时使用)")
weight = Column(Numeric(5, 2), nullable=True, comment="KR权重%")
target_value = Column(Numeric(15, 2), nullable=True, comment="目标值")
current_value = Column(Numeric(15, 2), nullable=True, comment="当前值")
progress = Column(Integer, default=0, comment="完成进度 0-100")
status = Column(String(20), default="pending", comment="pending/in_progress/completed/cancelled/achieved")
due_date = Column(Date, nullable=True, comment="截止日期")
monthly_milestones = Column(JSON, nullable=True, comment="月度里程碑: [{\"month\":\"2026-07\",\"label\":\"...\",\"status\":\"completed\"}]")
sort_order = Column(Integer, default=0, comment="排序")
created_at = Column(DateTime, server_default=func.now())
# ============================================================
# 预算系统技术改进 (2026-08-28 yanxue-budget-tech-improve)
# ① kpi_value_sources/kpi_value_collect_logs: 实际值自动归集 P1-④
# ② budget_zero_based_items: 真零基逐项论证 P2-①
# ③ budget_derivation_rules: 派生规则可配置 P2-②
# ④ cash_plan_classify_rules/cash_plan_unclassified: 现金流分类规则 P2-⑥
# ============================================================
class KPIValueSource(Base):
"""KPI实际值取数映射 — 自动归集源头 (P1-④ 2026-08-28)"""
__tablename__ = "kpi_value_sources"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="目标KPI")
source_table = Column(String(50), nullable=False, comment="源头表: voucher_details/product_inventory/product_inventory_detail/cash_plans")
source_field = Column(String(50), nullable=False, comment="金额字段: credit_amount/debit_amount/amount/qty")
aggregate = Column(String(10), default="sum", comment="sum/avg/count/max/min")
filter_rule = Column(JSON, nullable=True, comment="过滤: {\"subject_code\":\"6601\",\"direction\":\"credit\"}")
period_field = Column(String(50), default="period", comment="期间字段: period/voucher_date")
unit_conversion = Column(Float, default=1, comment="单位倍率(元→万元/10000)")
status = Column(String(20), default="active", comment="active/inactive")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
__table_args__ = (UniqueConstraint("entity_id", "kpi_id", "source_table", name="uk_source"),)
class KPIValueCollectLog(Base):
"""实际值采集日志 — 每次自动归集记录 (P1-④ 2026-08-28)"""
__tablename__ = "kpi_value_collect_logs"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID")
kpi_id = Column(Integer, nullable=False, comment="KPI ID")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
source_table = Column(String(50), nullable=False, comment="源头表")
collected_value = Column(Float, nullable=True, comment="采集到的值")
status = Column(String(20), default="success", comment="success/failed")
message = Column(String(500), nullable=True, comment="说明/错误信息")
collected_at = Column(DateTime, server_default=func.now())
class BudgetZeroBasedItem(Base):
"""零基预算逐项论证项 (P2-① 2026-08-28)"""
__tablename__ = "budget_zero_based_items"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
item_name = Column(String(200), nullable=False, comment="费用科目名")
item_category = Column(String(20), default="discretionary", comment="fixed/variable/discretionary")
base_value = Column(Float, nullable=False, comment="基准值(上年/上月实际)")
justification = Column(Text, nullable=True, comment="逐项论证理由(为何保留/削减/取消)")
proposed_value = Column(Float, nullable=False, comment="论证后金额")
status = Column(String(20), default="draft", comment="draft/approved")
created_by = Column(String(100), nullable=True, comment="创建人")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BudgetDerivationRule(Base):
"""KPI派生规则 — apply-method 可配置派生 (P2-② 2026-08-28)"""
__tablename__ = "budget_derivation_rules"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="目标KPI")
rule_type = Column(String(30), nullable=False, comment="incremental/percentage_of/formula")
base_kpi_id = Column(Integer, nullable=True, comment="来源KPI(percentage_of用)")
params = Column(JSON, nullable=True, comment="{\"rate\":0.02,\"field\":\"net_profit\"}")
formula_text = Column(String(500), nullable=True, comment="可读公式说明")
status = Column(String(20), default="active", comment="active/inactive")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
__table_args__ = (UniqueConstraint("entity_id", "kpi_id", "rule_type", name="uk_rule"),)
class CashPlanClassifyRule(Base):
"""现金流收付分类规则 — KPI→receive/pay 可维护 (P2-⑥ 2026-08-28)"""
__tablename__ = "cash_plan_classify_rules"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, nullable=True, comment="精确匹配KPI,优先")
kpi_code_pattern = Column(String(200), nullable=True, comment="关键词/编码模式匹配,兜底")
plan_type = Column(String(10), nullable=False, comment="receive/pay")
priority = Column(Integer, default=10, comment="匹配顺序,小优先")
status = Column(String(20), default="active", comment="active/inactive")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class CashPlanUnclassified(Base):
"""现金流待分类KPI队列 — 无法判别的KPI不静默跳过 (P2-⑥ 2026-08-28)"""
__tablename__ = "cash_plan_unclassified"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, nullable=False, comment="KPI ID")
kpi_name = Column(String(200), nullable=True, comment="KPI名称")
period = Column(String(20), nullable=True, comment="期间")
budget_value = Column(Float, nullable=True, comment="预算值")
reason = Column(String(200), nullable=True, comment="无法分类原因")
status = Column(String(20), default="pending", comment="pending/classified/ignored")
created_at = Column(DateTime, server_default=func.now())
resolved_at = Column(DateTime, nullable=True)