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