打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
563 lines
24 KiB
Python
563 lines
24 KiB
Python
"""固定真实源快照和人工事实,组装隔离的 FIFO 核算预演。"""
|
||
|
||
from calendar import monthrange
|
||
from collections import Counter
|
||
from datetime import date
|
||
from decimal import InvalidOperation
|
||
|
||
from sqlalchemy import select
|
||
|
||
from app.core.exceptions import BadRequestError
|
||
from app.models import Customer, Product, TPlusSourceLink, TPlusSyncRecord, TPlusWorkLine
|
||
from app.services import param_service
|
||
from app.services.rate_rules import personal_values
|
||
from app.services.tplus.fifo import START, amount_text, money, preview_group
|
||
from app.services.tplus.snapshots import source_records, source_statement
|
||
from app.services.tplus.workbench import RULE_VERSION, WorkbenchError, digest, get_run, latest_facts
|
||
|
||
|
||
def _parameters(session, days):
|
||
values = {}
|
||
for day in sorted(days):
|
||
snap = param_service.resolve_snapshot(session, date.fromisoformat(day))
|
||
values[day] = {
|
||
"parameter_snapshot_hash": digest([
|
||
(code, dimension, value, str(snap._effective.get((code, dimension))))
|
||
for code, dimension, value in sorted(snap.iter_items())
|
||
]),
|
||
"start": snap.get("salesperson.rate.start", role_code="salesperson"),
|
||
"slope": snap.get("salesperson.rate.slope", role_code="salesperson"),
|
||
"floor": snap.get("salesperson.rate.floor", role_code="salesperson"),
|
||
"boundary": snap.integer("salesperson.days.boundary", role_code="salesperson"),
|
||
"enterprise": snap.get("enterprise.rate"),
|
||
"enterprise_by_category": {
|
||
category: snap.get("enterprise.rate", product_category=category)
|
||
for category in ("municipal_water", "rail_transit")
|
||
},
|
||
"info": snap.get("info.rate", role_code="info_officer"),
|
||
"personal": {
|
||
category: personal_values(snap, category)
|
||
for category in ("municipal_water", "rail_transit")
|
||
},
|
||
"thresholds": {
|
||
category: snap.get("product.threshold", product_category=category)
|
||
for category in ("municipal_water", "rail_transit")
|
||
},
|
||
}
|
||
return values
|
||
|
||
|
||
def _group_records(session, source, entity, ids):
|
||
rows = source_records(session, source, entity, ids)
|
||
if {r.source_id for r in rows} != set(ids):
|
||
raise WorkbenchError("GROUP_SOURCE_MISSING")
|
||
return rows
|
||
|
||
|
||
def _guidance(facts, product_id, line_id, month):
|
||
candidates = [
|
||
f.data_json
|
||
for (kind, _), f in facts.items()
|
||
if kind == "GUIDANCE"
|
||
and f.data_json["product_id"] == product_id
|
||
and f.data_json["month"] == month
|
||
and (f.data_json["basis"] == "UNIT" or f.data_json.get("shipment_line_id") == line_id)
|
||
]
|
||
return candidates[0] if len(candidates) == 1 else None
|
||
|
||
|
||
def _shipments(session, run, key, group, facts):
|
||
from app.services.tplus.corrections import verified_correction
|
||
headers = _group_records(session, run.source_database, "shipments", group["shipment_ids"])
|
||
details = source_records(
|
||
session, run.source_database, "shipment_items", parent_ids=group["shipment_ids"]
|
||
)
|
||
heads = {r.source_id: r for r in headers}
|
||
product_ids = {str(r.payload_json.get("idinventory")) for r in details}
|
||
product_targets = dict(
|
||
session.execute(
|
||
select(TPlusSourceLink.source_id, Product)
|
||
.join(Product, Product.id == TPlusSourceLink.target_id)
|
||
.where(
|
||
TPlusSourceLink.source_database == run.source_database,
|
||
TPlusSourceLink.entity_type == "products",
|
||
TPlusSourceLink.source_id.in_(product_ids),
|
||
)
|
||
).all()
|
||
)
|
||
customer_target = session.scalar(
|
||
select(Customer)
|
||
.join(TPlusSourceLink, Customer.id == TPlusSourceLink.target_id)
|
||
.where(
|
||
TPlusSourceLink.source_database == run.source_database,
|
||
TPlusSourceLink.entity_type == "customers",
|
||
TPlusSourceLink.source_id == group["customer_id"],
|
||
)
|
||
)
|
||
rows, snapshots = [], headers + details
|
||
for record in details:
|
||
raw = record.payload_json
|
||
head_record = heads.get(str(raw.get("source_parent_id")))
|
||
if head_record is None:
|
||
continue
|
||
head = head_record.payload_json
|
||
if str(head.get("idcustomer")) != group["customer_id"]:
|
||
raise WorkbenchError("GROUP_CUSTOMER_MISMATCH")
|
||
supplement = facts.get(("SHIPMENT", record.source_id))
|
||
supplement = supplement.data_json if supplement else {}
|
||
correction = verified_correction(session, run.source_database, facts, "SHIPMENT", head_record)
|
||
product_id = str(raw.get("idinventory"))
|
||
product = facts.get(("PRODUCT", product_id))
|
||
customer = facts.get(("CUSTOMER", group["customer_id"]))
|
||
pending = []
|
||
if (
|
||
not supplement.get("sales_amount_excludes_freight")
|
||
and supplement.get("freight_amount") is None
|
||
):
|
||
pending.append("FREIGHT_PENDING")
|
||
if not supplement.get("business_key"):
|
||
pending.append("SHIPPING_OWNERSHIP_PENDING")
|
||
# SA_SaleDelivery.isCancel 是 VoucherSettleState(如428未结清、180已结清),
|
||
# 并非作废标志;不能将非零编码当作红冲或作废。
|
||
if head.get("voucherState") != 189:
|
||
pending.append("SHIPMENT_STATE_PENDING")
|
||
if head.get("isCounteractDelivery") and not correction.get("exclude_performance"):
|
||
pending.append("NEGATIVE_EVENT_REVIEW_REQUIRED")
|
||
day = supplement.get("date_override") or str(head["voucherdate"])[:10]
|
||
managers = [
|
||
f.data_json
|
||
for (kind, _), f in facts.items()
|
||
if kind == "MANAGER"
|
||
and f.data_json["business_key"] == supplement.get("business_key")
|
||
and f.data_json["effective_date"] <= day
|
||
]
|
||
managers.sort(key=lambda v: v["effective_date"])
|
||
for (kind, _), fact in facts.items():
|
||
if (
|
||
kind == "HANDOVER"
|
||
and fact.data_json["business_key"] == supplement.get("business_key")
|
||
and fact.data_json["mode"] == "ACTIVE"
|
||
and not fact.data_json["shipping_ownership_confirmed"]
|
||
):
|
||
pending.append("HANDOVER_SHIPMENTS_PENDING")
|
||
if raw.get("taxAmount") is None:
|
||
pending.append("TAX_AMOUNT_PENDING")
|
||
gross_amount = money(raw.get("taxAmount") or "0")
|
||
freight = (
|
||
money(supplement["freight_amount"])
|
||
if supplement.get("freight_amount") is not None
|
||
else money("0")
|
||
)
|
||
target_product = product_targets.get(product_id)
|
||
rows.append(
|
||
{
|
||
"source_id": head_record.source_id,
|
||
"line_id": record.source_id,
|
||
"source_no": head_record.source_no,
|
||
"date": day,
|
||
"group_key": key,
|
||
"business_key": supplement.get("business_key"),
|
||
"personnel_id": supplement.get("personnel_id"),
|
||
"amount": str(gross_amount - freight),
|
||
"gross_tax_amount": str(gross_amount),
|
||
"freight_amount": str(freight)
|
||
if supplement.get("freight_amount") is not None
|
||
else None,
|
||
"product_id": product_id,
|
||
"quantity": str(raw.get("quantity") or "0"),
|
||
"category": target_product.product_category
|
||
if target_product
|
||
else product.data_json["category"]
|
||
if product
|
||
else None,
|
||
"unit": target_product.unit
|
||
if target_product
|
||
else product.data_json["unit"]
|
||
if product
|
||
else None,
|
||
"customer_type": customer_target.customer_type
|
||
if customer_target
|
||
else customer.data_json["customer_type"]
|
||
if customer
|
||
else None,
|
||
"classification_evidence": {
|
||
"product_target_id": target_product.id if target_product else None,
|
||
"customer_target_id": customer_target.id if customer_target else None,
|
||
},
|
||
"manager_id": managers[-1]["manager_id"] if managers else None,
|
||
"performance_excluded": bool(correction.get("exclude_performance")),
|
||
"correction_pair_key": correction.get("pair_key"),
|
||
"exclusion_reason": "CORRECTION_EXCLUDED" if correction else None,
|
||
"pending": pending,
|
||
}
|
||
)
|
||
covered = {r["source_id"] for r in rows}
|
||
if covered != set(group["shipment_ids"]):
|
||
raise WorkbenchError("SHIPMENT_DETAILS_MISSING")
|
||
return rows, snapshots
|
||
|
||
|
||
def _receipts(session, run, key, group, facts):
|
||
from app.services.tplus.corrections import verified_correction
|
||
records = _group_records(session, run.source_database, "receipts", group["receipt_ids"])
|
||
rows = []
|
||
for record in records:
|
||
raw = record.payload_json
|
||
correction = verified_correction(session, run.source_database, facts, "RECEIPT", record)
|
||
redirect_target = correction.get("redirect_business_key")
|
||
if str(raw.get("idpartner")) != group["customer_id"] and redirect_target != key:
|
||
raise WorkbenchError("GROUP_CUSTOMER_MISMATCH")
|
||
fact = facts.get(("RECEIPT", record.source_id))
|
||
confirmed, voucher_evidence, voucher_pending = _voucher_evidence(raw, fact)
|
||
pending = []
|
||
pending.extend(voucher_pending)
|
||
if raw.get("isReceiveFlag") != 1:
|
||
pending.append("NOT_RECEIPT")
|
||
# 凭证关系与人工核实冲突时不能强行按人工勾选放行。
|
||
if pending:
|
||
confirmed = False
|
||
if record.source_id in group.get("excluded_receipt_ids", []):
|
||
continue
|
||
if raw.get("voucherstate") != 189:
|
||
confirmed = False
|
||
exclusion = None
|
||
if raw.get("isReceiveFlag") == 0:
|
||
exclusion = "NOT_RECEIPT"
|
||
elif (
|
||
voucher_evidence.get("basis") == "GL_SrcVoucherInfo/GL_Doc"
|
||
and voucher_evidence.get("count") == 0
|
||
and not voucher_pending
|
||
):
|
||
exclusion = "NOT_GENERATED_VOUCHER"
|
||
if redirect_target and redirect_target != key:
|
||
exclusion = "REDIRECTED_RECEIPT"
|
||
rows.append(
|
||
{
|
||
"source_id": record.source_id,
|
||
"source_no": record.source_no,
|
||
"date": str(raw["voucherDate"])[:10],
|
||
"group_key": key,
|
||
"amount": str(raw["amount"]),
|
||
"voucher_confirmed": confirmed,
|
||
"exclusion": exclusion,
|
||
"voucher_evidence": voucher_evidence,
|
||
"accounting_voucher_id": raw.get("accounting_voucher_id"),
|
||
"accounting_voucher_code": raw.get("accounting_voucher_code"),
|
||
"pending": pending,
|
||
"redirect_business_key": redirect_target,
|
||
"redirected_from_customer_id": str(raw.get("idpartner"))
|
||
if redirect_target == key and str(raw.get("idpartner")) != key
|
||
else None,
|
||
}
|
||
)
|
||
return rows, records
|
||
|
||
|
||
def _voucher_evidence(raw, fact):
|
||
"""以真实总账关系核实生成资格;旧快照只接受有依据的人工核实。"""
|
||
count, valid = raw.get("accounting_voucher_count"), raw.get("valid_accounting_voucher_count")
|
||
if count is None or valid is None:
|
||
return (
|
||
bool(fact and fact.data_json["voucher_confirmed"]),
|
||
{
|
||
"basis": "MANUAL" if fact else "UNKNOWN",
|
||
"reference": fact.data_json["voucher_reference"] if fact else None,
|
||
},
|
||
[],
|
||
)
|
||
count, valid = int(count), int(valid)
|
||
if count < 0 or valid < 0 or valid > count:
|
||
raise ValueError("凭证关联数量异常")
|
||
generated = count > 0 and valid == count and raw.get("accounting_voucher_id") is not None
|
||
pending = ["ACCOUNTING_VOUCHER_INVALID"] if count > 0 and not generated else []
|
||
if fact and fact.data_json["voucher_confirmed"] != generated:
|
||
pending.append("VOUCHER_EVIDENCE_CONFLICT")
|
||
return (
|
||
generated and not pending,
|
||
{
|
||
"basis": "GL_SrcVoucherInfo/GL_Doc",
|
||
"count": count,
|
||
"valid_count": valid,
|
||
"voucher_id": raw.get("accounting_voucher_id"),
|
||
"voucher_code": raw.get("accounting_voucher_code"),
|
||
"reference": fact.data_json["voucher_reference"] if fact else None,
|
||
},
|
||
pending,
|
||
)
|
||
|
||
|
||
def _costed_shipments(shipments, receipts, facts, group):
|
||
"""回款月定价时成本按每笔回款取值,交给 FIFO 逐段计算。"""
|
||
basis = "SHIPMENT"
|
||
for ship in shipments:
|
||
if basis is None:
|
||
ship["pending"].append("GUIDANCE_MONTH_BASIS_PENDING")
|
||
continue
|
||
months = {r["date"][:7] for r in receipts} if basis == "RECEIPT" else {ship["date"][:7]}
|
||
costs = {}
|
||
for month in months:
|
||
guide = _guidance(facts, ship["product_id"], ship["line_id"], month)
|
||
if guide is None or guide["unit"] != ship["unit"]:
|
||
continue
|
||
ship["guidance_finalized"] = bool(guide.get("is_finalized"))
|
||
cost = money(guide["amount"])
|
||
if guide["basis"] == "UNIT":
|
||
if money(ship["quantity"]) <= 0:
|
||
ship["pending"].append("QUANTITY_PENDING")
|
||
continue
|
||
cost *= money(ship["quantity"])
|
||
costs[month] = amount_text(cost)
|
||
ship["cost_by_month"] = costs
|
||
ship["cost_month_basis"] = basis
|
||
ship["cost_total"] = costs.get(ship["date"][:7]) if basis == "SHIPMENT" else None
|
||
|
||
|
||
def execute_preview(session, run, *, save_lines=True, customer_scope=False):
|
||
"""同一次事务固化所有事实、来源指纹、参数和结果,不写正式台账。"""
|
||
if run.input_json.get("replay_no"):
|
||
return _replay(session, run)
|
||
facts = latest_facts(session, run.source_database)
|
||
month = run.input_json["month"]
|
||
year, mon = map(int, month.split("-"))
|
||
until = date(year, mon, monthrange(year, mon)[1])
|
||
selected = []
|
||
overlapping = set()
|
||
for key in run.input_json["group_keys"]:
|
||
fact = facts.get(("GROUP", key))
|
||
if fact is None:
|
||
selected.append((key, {}))
|
||
continue
|
||
group = dict(fact.data_json)
|
||
if customer_scope:
|
||
peers = [
|
||
f.data_json
|
||
for (kind, _), f in facts.items()
|
||
if kind == "GROUP" and f.data_json["customer_id"] == group["customer_id"]
|
||
]
|
||
|
||
def policy(g):
|
||
return (
|
||
money(g["opening_balance"]) if g.get("opening_balance") is not None else None,
|
||
tuple(sorted(g.get("excluded_receipt_ids", []))),
|
||
bool(g.get("opening_confirmed")),
|
||
bool(g.get("historical_settlement_confirmed")),
|
||
)
|
||
|
||
if len({policy(g) for g in peers}) != 1:
|
||
selected.append((key, {"customer_scope_conflict": True}))
|
||
continue
|
||
# 2026-10-30 E7:同一发货客户名头统一平账,不再按合同/人工业务键拆组。
|
||
group["scope_confirmed"] = True
|
||
for entity, field, day_field, target_field in (
|
||
("shipments", "idcustomer", "voucherdate", "shipment_ids"),
|
||
("receipts", "idpartner", "voucherDate", "receipt_ids"),
|
||
):
|
||
statement = source_statement(session, run.source_database, entity).where(
|
||
TPlusSyncRecord.payload_json[field].as_integer() == int(group["customer_id"])
|
||
)
|
||
source_ids = [
|
||
r.source_id
|
||
for r in session.scalars(statement)
|
||
if START.isoformat()
|
||
<= str(r.payload_json.get(day_field, ""))[:10]
|
||
<= until.isoformat()
|
||
]
|
||
if entity == "receipts":
|
||
source_ids.extend(
|
||
f.fact_key.removeprefix("RECEIPT:")
|
||
for (kind, _), f in facts.items()
|
||
if kind == "CORRECTION"
|
||
and f.data_json.get("entity") == "RECEIPT"
|
||
and f.data_json.get("redirect_business_key") == key
|
||
and f.fact_key.removeprefix("RECEIPT:") not in source_ids
|
||
)
|
||
group[target_field] = sorted(source_ids, key=lambda value: int(value))
|
||
else:
|
||
redirected = [
|
||
f.fact_key.removeprefix("RECEIPT:")
|
||
for (kind, _), f in facts.items()
|
||
if kind == "CORRECTION" and f.data_json.get("entity") == "RECEIPT"
|
||
and f.data_json.get("redirect_business_key") == key
|
||
]
|
||
group["receipt_ids"] = sorted(set(group["receipt_ids"] + redirected), key=int)
|
||
|
||
def effective_receipts(value, group_key):
|
||
from app.services.tplus.corrections import correction_fact
|
||
|
||
return {
|
||
rid for rid in value["receipt_ids"]
|
||
if not (f := correction_fact(facts, "RECEIPT", rid))
|
||
or f.data_json.get("redirect_business_key") == group_key
|
||
}
|
||
# 也检查未选组,避免同一单据在另一组被解释为独立资金。
|
||
for (kind, other_key), other in facts.items():
|
||
if (
|
||
kind == "GROUP"
|
||
and other_key != key
|
||
and not customer_scope
|
||
and (
|
||
set(group["shipment_ids"]) & set(other.data_json["shipment_ids"])
|
||
or effective_receipts(group, key) & effective_receipts(other.data_json, other_key)
|
||
)
|
||
):
|
||
overlapping.add(key)
|
||
selected.append((key, group))
|
||
frozen = {
|
||
"rule_version": RULE_VERSION,
|
||
"month": month,
|
||
"source": [],
|
||
"facts": [
|
||
{
|
||
"id": f.id,
|
||
"kind": f.kind,
|
||
"key": f.fact_key,
|
||
"version": f.version,
|
||
"data": f.data_json,
|
||
"evidence": f.evidence,
|
||
}
|
||
for f in facts.values()
|
||
],
|
||
"parameters": {},
|
||
"calculation_inputs": {},
|
||
}
|
||
count, summaries = 0, {}
|
||
person_facts = {key: f.data_json for (kind, key), f in facts.items() if kind == "PERSONNEL"}
|
||
handovers = [f.data_json for (kind, _), f in facts.items() if kind == "HANDOVER"]
|
||
for key, data in selected:
|
||
group = {**data, "key": key}
|
||
if data.get("customer_scope_conflict"):
|
||
result = {"status": "PENDING", "reasons": ["CUSTOMER_SCOPE_CONFLICT"], "lines": []}
|
||
elif key in overlapping:
|
||
result = {"status": "PENDING", "reasons": ["OVERLAPPING_GROUPS"], "lines": []}
|
||
elif not data:
|
||
result = {"status": "PENDING", "reasons": ["GROUP_FACT_REQUIRED"], "lines": []}
|
||
elif (
|
||
not group.get("scope_confirmed")
|
||
or not group.get("opening_confirmed")
|
||
or not group.get("historical_settlement_confirmed")
|
||
):
|
||
result = preview_group(group, [], [], {}, {}, until)
|
||
else:
|
||
try:
|
||
ships, ship_refs = _shipments(session, run, key, group, facts)
|
||
cash, cash_refs = _receipts(session, run, key, group, facts)
|
||
frozen["source"].extend(
|
||
(r.entity_type, r.source_id, r.id, r.row_hash) for r in ship_refs + cash_refs
|
||
)
|
||
_costed_shipments(ships, cash, facts, group)
|
||
# max(发货日,回款日) 必为其中一个日期,无需构造单据笛卡尔积。
|
||
days = {
|
||
row["date"]
|
||
for row in [*ships, *cash]
|
||
if START.isoformat() <= row["date"] <= until.isoformat()
|
||
}
|
||
params = _parameters(session, days)
|
||
# 已确认的原修改方案和到账日口径不再依赖重复人工勾选。
|
||
params["august_confirmed"] = True
|
||
params["prepayment_parameter_basis"] = "RECEIPT"
|
||
frozen["parameters"][key] = params
|
||
frozen["calculation_inputs"][key] = {
|
||
"group": group,
|
||
"shipments": ships,
|
||
"receipts": cash,
|
||
"facts": {"personnel": person_facts, "handovers": handovers},
|
||
"params": params,
|
||
}
|
||
result = preview_group(
|
||
group,
|
||
ships,
|
||
cash,
|
||
{"personnel": person_facts, "handovers": handovers},
|
||
params,
|
||
until,
|
||
)
|
||
except WorkbenchError as exc:
|
||
frozen["calculation_inputs"].pop(key, None)
|
||
result = {"status": "PENDING", "reasons": [str(exc)], "lines": []}
|
||
except BadRequestError:
|
||
frozen["calculation_inputs"].pop(key, None)
|
||
result = {"status": "PENDING", "reasons": ["PARAMETERS_PENDING"], "lines": []}
|
||
except (ValueError, KeyError, InvalidOperation, TypeError):
|
||
frozen["calculation_inputs"].pop(key, None)
|
||
result = {"status": "PENDING", "reasons": ["DATA_VALUE_INVALID"], "lines": []}
|
||
if key not in frozen["calculation_inputs"]:
|
||
frozen["calculation_inputs"][key] = {"pending": result}
|
||
summaries[key] = {k: v for k, v in result.items() if k != "lines"}
|
||
if save_lines:
|
||
count = _save_group(session, run, key, result, count)
|
||
run.snapshot_json, run.snapshot_hash = frozen, digest(frozen)
|
||
run.summary_json = {
|
||
"groups": summaries,
|
||
"total_lines": count,
|
||
"rule_version": RULE_VERSION,
|
||
"month": month,
|
||
"formal_payroll_written": False,
|
||
}
|
||
states = Counter(v["status"] for v in summaries.values())
|
||
run.status = "PARTIAL" if states["PENDING"] or states["PARTIAL"] else "SUCCEEDED"
|
||
|
||
|
||
def _save_group(session, run, key, result, count):
|
||
lines = result["lines"] or (
|
||
[{"status": "PENDING", "reasons": result["reasons"], "performance": None}]
|
||
if result["status"] == "PENDING"
|
||
else []
|
||
)
|
||
for line in lines:
|
||
count += 1
|
||
session.add(
|
||
TPlusWorkLine(
|
||
run_no=run.run_no,
|
||
line_no=count,
|
||
entity_type="group",
|
||
source_id=key,
|
||
action=line["status"],
|
||
reason=",".join(line.get("reasons", []))[:128] or None,
|
||
data_json={**line, "group_key": key, "formal_payroll_written": False},
|
||
)
|
||
)
|
||
return count
|
||
|
||
|
||
def _replay(session, run):
|
||
"""只重算原运行冻结输入;不读取当前事实、指导价、参数或源数据。"""
|
||
import copy
|
||
|
||
previous = get_run(session, run.input_json["replay_no"], run.source_database)
|
||
frozen = copy.deepcopy(previous.snapshot_json or {})
|
||
if (
|
||
previous.kind != "CALC_PREVIEW"
|
||
or frozen.get("rule_version") not in ("2026-10-30.1", RULE_VERSION)
|
||
or not frozen.get("calculation_inputs")
|
||
):
|
||
raise WorkbenchError("REPLAY_UNAVAILABLE")
|
||
year, month = map(int, frozen["month"].split("-"))
|
||
until = date(year, month, monthrange(year, month)[1])
|
||
summaries, count = {}, 0
|
||
for key, values in frozen["calculation_inputs"].items():
|
||
result = values.get("pending")
|
||
if result is None:
|
||
result = preview_group(
|
||
values["group"],
|
||
values["shipments"],
|
||
values["receipts"],
|
||
values["facts"],
|
||
values["params"],
|
||
until,
|
||
)
|
||
summaries[key] = {k: v for k, v in result.items() if k != "lines"}
|
||
count = _save_group(session, run, key, result, count)
|
||
run.snapshot_json, run.snapshot_hash = frozen, digest(frozen)
|
||
run.summary_json = {
|
||
"groups": summaries,
|
||
"total_lines": count,
|
||
"rule_version": RULE_VERSION,
|
||
"month": frozen["month"],
|
||
"formal_payroll_written": False,
|
||
}
|
||
run.status = (
|
||
"PARTIAL"
|
||
if any(v["status"] in ("PENDING", "PARTIAL") for v in summaries.values())
|
||
else "SUCCEEDED"
|
||
)
|