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:
@@ -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):
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
Reference in New Issue
Block a user