108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
# 合同班次:领取表 3 → 按供应商、货主和三个日期合单 → runner 录合同 → 写表 4。
|
||
#
|
||
# 崩在 process_status=3:超过租约可再领,未过期需改回 0。
|
||
# 未捕获异常原因列只写 PAGE_FAILED。原文进审计表,会话结束后另写。
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from contextlib import nullcontext
|
||
from dataclasses import dataclass
|
||
from typing import Callable
|
||
|
||
from contract_draft import ContractDraft
|
||
from errors import DiligenceError, NodeError
|
||
from group import draft_from_group, group_by_supplier, missing_mdm_reason
|
||
from persist import KIND_DILIGENCE, KIND_FAIL, KIND_OK, ContractOutcome, build_save
|
||
from rpa_oracle import ShiftError
|
||
|
||
PAGE_FAILED = "PAGE_FAILED"
|
||
Runner = Callable[[ContractDraft], ContractOutcome]
|
||
logger = logging.getLogger("rpa_contract")
|
||
|
||
|
||
# 一个合同班次的领取、合单和成败计数。
|
||
@dataclass
|
||
class JobSummary:
|
||
claimed: int = 0
|
||
groups: int = 0
|
||
ok: int = 0
|
||
failed: int = 0
|
||
|
||
|
||
def _session(repo):
|
||
"""有 session 就借一条,没有就空上下文。页面调用必须在这个块外面。"""
|
||
session = getattr(repo, "session", None)
|
||
if session is None:
|
||
return nullcontext()
|
||
return session()
|
||
|
||
|
||
def run_batch(repo, *, runner: Runner, limit: int | None = None) -> JobSummary:
|
||
"""领取待录合同行,按供应商合单、调 runner、落表。
|
||
|
||
session 只包领取和每一组的落表。页面在连接归还之后跑。一组落完再处理下一组。
|
||
limit 交给领取层。
|
||
"""
|
||
audits: list[ShiftError] = []
|
||
with _session(repo):
|
||
claimed = repo.claim_pending(limit=limit)
|
||
summary = JobSummary(claimed=len(claimed), groups=0)
|
||
for rows in group_by_supplier(claimed):
|
||
summary.groups += 1
|
||
outcome = _run_group(rows, runner)
|
||
with _session(repo):
|
||
repo.save_group(build_save(rows, outcome))
|
||
if outcome.error_text:
|
||
supplier = rows[0].supplier_id if rows else ""
|
||
audits.append(ShiftError("合同", str(supplier), PAGE_FAILED, outcome.error_text))
|
||
if outcome.kind == KIND_OK and (outcome.contract_no or "").strip():
|
||
summary.ok += 1
|
||
else:
|
||
summary.failed += 1
|
||
repo.save_shift_errors(audits)
|
||
return summary
|
||
|
||
|
||
# 录一组同一供应商的行。缺 MDM 不调页面;未捕获异常写 PAGE_FAILED。
|
||
def _run_group(rows, runner: Runner) -> ContractOutcome:
|
||
reason = missing_mdm_reason(rows)
|
||
if reason:
|
||
return ContractOutcome(KIND_FAIL, None, reason)
|
||
try:
|
||
outcome = runner(draft_from_group(rows))
|
||
except DiligenceError:
|
||
return ContractOutcome(KIND_DILIGENCE, None, "客户未尽调")
|
||
except NodeError as exc:
|
||
return ContractOutcome(KIND_FAIL, None, str(exc))
|
||
except Exception as exc:
|
||
logger.exception("合同录入未捕获异常 supplier=%s", rows[0].supplier_id if rows else "")
|
||
return ContractOutcome(KIND_FAIL, None, PAGE_FAILED, _text(exc))
|
||
if outcome.kind == KIND_OK and not (outcome.contract_no or "").strip():
|
||
return ContractOutcome(KIND_FAIL, None, "submit 模式未读取到真实合同编号")
|
||
return outcome
|
||
|
||
|
||
# 把页面 run() 的合同号或异常收成 ContractOutcome。
|
||
#
|
||
# 尽调失败写客户未尽调;NodeError 写业务文案;其它异常写 PAGE_FAILED,原文放 error_text。
|
||
def outcome_from_run(run: Callable[[], str]) -> ContractOutcome:
|
||
try:
|
||
number = (run() or "").strip()
|
||
except DiligenceError:
|
||
return ContractOutcome(KIND_DILIGENCE, None, "客户未尽调")
|
||
except NodeError as exc:
|
||
return ContractOutcome(KIND_FAIL, None, str(exc))
|
||
except Exception as exc:
|
||
logger.exception("合同页面未捕获异常")
|
||
return ContractOutcome(KIND_FAIL, None, PAGE_FAILED, _text(exc))
|
||
if not number:
|
||
return ContractOutcome(KIND_FAIL, None, "submit 模式未读取到真实合同编号")
|
||
return ContractOutcome(KIND_OK, number, None)
|
||
|
||
|
||
# 取出异常原文。空串不当审计正文。
|
||
def _text(exc: BaseException) -> str | None:
|
||
text = str(exc).strip()
|
||
return text or None
|