Files
PayrollManagementSystem/backend/app/services/tplus/source_heads.py
T
liuwenxin 108bf87225 feat(核算)!: 添加 T+ 正式核算与采集托管
补齐源快照、资料预演、正式导入及工资核算的依赖链路。
新增版本化输入、逐段 FIFO、生成锁和结构化当月调整,
并提供默认关闭的周期采集、有限重试及 Windows 托管。

原始客户资料、私有配置及首页改动不纳入本次提交。

BREAKING CHANGE: 升级前需应用新增迁移,并通过
PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。
旧单批次核算默认关闭;历史兼容需显式启用
PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
2026-11-02 09:42:53 +08:00

60 lines
2.2 KiB
Python

"""保留成功读取实际观察到的当前版本,不把历史最大ID当成当前值。"""
from sqlalchemy import inspect, select, tuple_
from app.models import TPlusSourceHead, TPlusSyncRecord
from app.services.tplus.staging import prepare_row
def observe_rows(session, batch, mapping, rows):
"""每页提交前记录所有读到的源版本,包括去重跳过的 A→B→A。"""
# V15 运行中的升级窗口继续允许原读取功能;工作区本身要求 V16。
if not inspect(session.connection()).has_table(TPlusSourceHead.__tablename__):
return
prepared = [
prepare_row(batch.batch_no, mapping.entity_type, mapping.source_table, r)
for r in rows
if r.get("id") is not None
]
wanted = [(r["source_id"], r["row_hash"]) for r in prepared]
if not wanted:
return
records = {
(r.source_id, r.row_hash): r.id
for r in session.scalars(
select(TPlusSyncRecord).where(
TPlusSyncRecord.entity_type == mapping.entity_type,
tuple_(TPlusSyncRecord.source_id, TPlusSyncRecord.row_hash).in_(wanted),
)
)
}
source = batch.source_database
if not source:
raise ValueError("源账套身份缺失,不能发布当前快照")
current = {
h.source_id: h
for h in session.scalars(
select(TPlusSourceHead).where(
TPlusSourceHead.source_database == source,
TPlusSourceHead.entity_type == mapping.entity_type,
TPlusSourceHead.source_id.in_([v[0] for v in wanted]),
)
)
}
for row in prepared:
record_id = records.get((row["source_id"], row["row_hash"]))
if record_id is None:
continue
head = current.get(row["source_id"])
if head is None:
head = TPlusSourceHead(
source_database=source,
entity_type=mapping.entity_type,
source_id=row["source_id"],
record_id=record_id,
batch_no=batch.batch_no,
)
session.add(head)
current[row["source_id"]] = head
head.record_id, head.batch_no = record_id, batch.batch_no