Files
cma-management/backend/app/models/ext_input_ledger.py
T
Hermes CI Fix 9d9a79545a 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.
2026-09-15 18:12:05 +08:00

102 lines
6.0 KiB
Python
Raw 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.
"""外部输入台账 — 三张**外部输入**业务表(能力评价 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"),
)