feat(cma): external input ledgers for M2 capability eval (tax/hr/licence)

3 new tables + idempotent DDL migration (prod guard: refuses DB name cma without approval): tax_filing_log (P_TAX_TIMELY), hr_monthly_ledger (L_EMPLOYEE_TURNOVER), licence_ledger (P_LICENCE_OK).

Entry API /api/cma/ext-ledger: create / CSV batch import / query by entity+period / derive trigger. on_time is server-derived only; HR rows rejected when end != begin + hires - separations.

Deriver writes kpi_values only (source_type=derived, idempotent upsert); denominator 0 returns None and writes nothing; never creates or changes kpi_definitions.

Tests: 22 new pytest cases (22 passed); scratch-DB e2e battery 28 assertions PASS with zero production write.
This commit is contained in:
Hermes CI Fix
2026-09-15 18:12:05 +08:00
parent 5d0884cc3b
commit 9d9a79545a
9 changed files with 1956 additions and 1 deletions
+1
View File
@@ -7,6 +7,7 @@ from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllo
from app.models.knowledge import KnowledgeEvent, KnowledgeSummary
from app.models.driver_budget import DriverFactorTemplate, DriverFactorBudget
from app.models.product_sales import ProductSales
from app.models.ext_input_ledger import TaxFilingLog, HrMonthlyLedger, LicenceLedger
class Entity(Base):
+101
View File
@@ -0,0 +1,101 @@
"""外部输入台账 — 三张**外部输入**业务表(能力评价 M2 准入3 派生,2026-09-15)
背景:能力评价 6 项缺口值经只读实测确认**无一项**能从既有系统合法取证;其中 3 项
只能来自新的人工登记台账(PRD v1.3 §4.0)。本模块只做「建表 + 结构化字段」,
录入/派生分别见 app/api/ext_input_ledgers.py 与 app/services/ext_input_derive.py。
tax_filing_log 申报行为台账 → P_TAX_TIMELY (A4 税务申报及时率, 月频)
hr_monthly_ledger 人事月度台账 → L_EMPLOYEE_TURNOVER (B5 员工流失率, 月频)
licence_ledger 证照台账 → P_LICENCE_OK (B2 经营许可合规率, 季频)
纪律(任务 0-P0-project-20260915-cma-eval-m2-ext-input-tables):
- 本模块**只新增表**,不改既有表、不改任何 KPI 定义(kpi_definitions 属准入2/4 同表串行)
- `on_time` 是**服务端派生**字段(唯一实现 app/services/ext_input_derive.derive_on_time),
录入方手填值一律被忽略并回报 on_time_ignored
- 每表 entity_id NOT NULL + 唯一键以 entity_id 打头(多租户隔离 + 索引可用性)
"""
from sqlalchemy import Column, Integer, String, Date, DateTime, UniqueConstraint, func
from app.database import Base
# 证照年检状态枚举(PRD v1.3 §4.0 原文口径,录入时白名单校验)
ANNUAL_CHECK_STATUSES = ("未到期", "已年检", "待年检", "过期")
# 期次格式(period)
MONTH_PERIOD_RE = r"^\d{4}-(0[1-9]|1[0-2])$" # 2026-08
QUARTER_PERIOD_RE = r"^\d{4}-Q[1-4]$" # 2026-Q2
class TaxFilingLog(Base):
"""申报行为台账 — P_TAX_TIMELY 的唯一取证来源
唯一键 (entity_id, period, tax_type):同一账套同一属期同一税种只允许一条登记行。
"""
__tablename__ = "tax_filing_log"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, index=True, comment="企业ID(账套)")
period = Column(String(20), nullable=False, comment="属期 YYYY-MM(申报所属月份)")
tax_type = Column(String(50), nullable=False, comment="税种: 增值税/附加税/企业所得税/印花税…")
legal_deadline = Column(Date, nullable=True, comment="法定申报期限")
filed_date = Column(Date, nullable=True, comment="实际申报日(未申报为 NULL)")
on_time = Column(Integer, nullable=True, comment="是否按时申报 1/0(**服务端派生**,录入方不得手填)")
evidence_no = Column(String(100), nullable=True, comment="申报回执/截图编号")
registered_by = Column(String(100), 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())
__table_args__ = (
UniqueConstraint("entity_id", "period", "tax_type", name="uq_tax_filing_log_entity_period_type"),
)
class HrMonthlyLedger(Base):
"""人事月度台账 — L_EMPLOYEE_TURNOVER 的唯一取证来源
唯一键 (entity_id, period)。
硬校验:headcount_end == headcount_begin + hires - separations,
不通过时**拒绝写入并回报**(见 app/services/ext_input_derive.check_headcount_equation)。
"""
__tablename__ = "hr_monthly_ledger"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, index=True, comment="企业ID(账套)")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
headcount_begin = Column(Integer, nullable=False, comment="期初在职人数")
hires = Column(Integer, nullable=False, default=0, comment="入职人数")
separations = Column(Integer, nullable=False, default=0, comment="离职人数")
headcount_end = Column(Integer, nullable=False, comment="期末在职人数(须= 期初+入职-离职)")
registered_by = Column(String(100), 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())
__table_args__ = (
UniqueConstraint("entity_id", "period", name="uq_hr_monthly_ledger_entity_period"),
)
class LicenceLedger(Base):
"""证照台账 — P_LICENCE_OK 的唯一取证来源
唯一键 (entity_id, licence_name, licence_no)。
有效性口径(派生侧):valid_to >= 季末 且 年检状态 != '过期';
valid_to 为空 = 长期有效(仍受状态约束)。
"""
__tablename__ = "licence_ledger"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, index=True, comment="企业ID(账套)")
licence_name = Column(String(200), nullable=False, comment="证照名称")
licence_no = Column(String(100), nullable=False, comment="证照编号")
valid_from = Column(Date, nullable=True, comment="有效期起")
valid_to = Column(Date, nullable=True, comment="有效期止(空=长期有效)")
annual_check_status = Column(String(20), nullable=False, default="未到期",
comment="年检状态: 未到期/已年检/待年检/过期")
issuer = Column(String(200), nullable=True, comment="发证/主管机关")
owner_role = Column(String(100), nullable=True, comment="责任岗位(职能,不指定自然人)")
attachment_no = Column(String(100), nullable=True, comment="附件/扫描件编号")
registered_by = Column(String(100), 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())
__table_args__ = (
UniqueConstraint("entity_id", "licence_name", "licence_no", name="uq_licence_ledger_entity_name_no"),
)