AutoRpa/docs/superpowers/specs/2026-10-05-contract-job-int...

13 KiB
Raw Blame History

合同班:表 3 合单录入 + 接口 4 组包

文档属性 内容
日期 2026-10-05
状态 已锁定,本轮实施依据
范围 rpa_contract/ 从表 3 领取、按 supplierId 合单、调现有页面 runner、尽调闸、写表 4 并回写表 3;rpa_oms/app/outbound/ 接口 4 组包
对照 docs/RPA订单中心接口开发口径_20260930.md 第 4 节、第 6 节;docs/合同系统录入流程.md;Excel 4合同协议录入结果接口
不变 接口 3 收件、表 3/4 DDL、页面步骤主体(登录/起草/货品/提交/读号)、接口 2 组包形态

合同班另起进程。HTTP 收件不同步跑作业。接口 4 真实 path 仍空:只组包,不发 HTTP。


1. 要做成什么

  1. 合同班启动瞬间冻结本班可录名单:表 3 process_status 0→3。
  2. 按 supplierId 合成一份合同;头字段取该组按 fid 的第一行;货品行全收。
  3. contractEntryStatus=0 且已有 contractNo 的行不领取,留给返利班。
  4. 注入 runner 调现有合同页面流。单测不打开浏览器。
  5. 提交成功:表 4 actionCode=1、contractStatus=1、带合同号;表 3 process_status=1。同一合同下多个 omsId 回同一套号和状态。
  6. 失败(未尽调 / 页面挂):表 4 contractStatus=3 带原因;表 3 process_status=2。
  7. 接口 4 从表 4 取 push_status=0 组装 {"data":[...]},一批最多 500。不改 push_status,不 POST。

完成标准(内存替身,不连库、不开浏览器):

  • cd rpa_contract → uv run pytest tests/test_job.py::test_job_merges_same_supplier_and_writes_table4 -q
    • 同一供应商两行、两个 omsId:runner 调一次;表 4 两行同一合同号;表 3 两条 process_status=1
    • 带 contractNo 的协议重录行不领
  • cd rpa_oms → uv run pytest tests/test_contract_agreement_outbound.py::test_assembles_unpushed_rows_as_document_envelope -q

2. 不做什么

  • 不实现返利班、不打开返利中心、不回 contractStatus=2 / actionCode=2
  • 不向订单中心发 HTTP,不改 push_status
  • 不在接口 3 请求路径里录合同
  • 不按 omsId 或供应商名称合单
  • 不把下午新进行追加进上午已录合同(冻结快照自然保证)
  • 不默认合并/上传 PDF(接口 3 这条链没有平台扫描件)
  • 不改表结构、不改接口 1/2/3
  • 不改 rpa_selection/、rpa_cart/、rpa_rebate/
  • 合同班不 import rpa_oms(作业写表 4;组包在 oms 另调,对齐选品写表 2 / 接口 2 另组)
  • 不把 JSON 干跑入口 main.py --input-json 拆掉

3. 已锁定决策

项 决定
包边界 领取/合单/落表在 rpa_contract;接口 4 组包在 rpa_oms/app/outbound/contract_agreement.py
冻结 claim_pending 一次领完本班可录行,0→3;过程中新进的仍是 0,下一班再处理
可录 process_status=0 且不是(contract_entry_status=0 且 contract_no 非空)
协议重录行 永不领取,保持 process_status=0
合单键 supplier_id。头字段 = 该组 fid 升序第一行
采购员 / 计划员 只存在表 3 行上,本轮不填页面(现有 flow 无对应控件)
表 4 粒度 每个 oms_id 一行。同一 oms_id 多货品仍一行。org_code 跟该 oms_id 在表 3 的原值
表 4 fid 序列触发器生成,INSERT 不绑这一列
表 4 审计 create_user_id/update_user_id = 0;名称 = 合同班;push_status=0
客商 customerCode = 第一行 supplier_mdm_code;customerName = supplier_name
日期 页面签订日/开始日/结束日 = 第一行 contract_sign_date / contract_start_date / contract_end_date(YYYY-MM-DD)
S08 兼容 ContractFlowContext 增加可选 contract_sign_date、contract_start_date。未给时回退 order_date(JSON 干跑不破)
PDF source_type="",不走医药城合并上传
尽调 S06 之后扫页面正文含「未尽调」则失败,原因固定「客户未尽调」,不提交。无独立选择器
runner Callable[[ContractDraft], ContractOutcome]。生产包装 run_contract_create_flow;单测注入
崩溃 停在表 3 process_status=3,人工改回 0 再跑;未写表 4
组包范围 全部 push_status=0,不按 actionCode 过滤(返利以后写 actionCode=2 也走同一函数)
组包拆批 同一非空 contractNo 不拆批,算法对齐接口 2 的 omsId。contractNo 为空(失败行)按行独立,不合成一组
omsId JSON 整数
HTTP 不做。path 空

4. 领取与合单

4.1 行模型

PurchaseContractRow(表 3 已有列,camelCase 不进作业):

fid, oms_id, supplier_id, supplier_name, contract_sign_date, contract_start_date, contract_end_date, payment_term, goods_id, goods_name, reg_no, spec, unit, qty, unit_price, purchaser_id, purchaser_name, planner_id, planner_name, contract_entry_status, contract_no, process_status, org_code, goods_mdm_code, supplier_mdm_code

4.2 SQL 语义

WHERE process_status = 0
  AND NOT (NVL(contract_entry_status, -1) = 0
           AND contract_no IS NOT NULL
           AND LENGTH(TRIM(contract_no)) > 0)
ORDER BY fid

领取:对命中行 UPDATE ... SET process_status = 3, process_time = SYSTIMESTAMP, update_time = SYSTIMESTAMP WHERE fid = :fid AND process_status = 0。rowcount=1 才算领到。可选 limit 仅供单测。

contract_no 空白串视为空。

4.3 分组

对已领取列表按 fid 升序扫描,OrderedDict 按 supplier_id 归组。组内顺序保持 fid 升序。每个 supplier_id 一份合同。

4.4 草稿映射

第一行 → 头:

草稿 来源
supplier_name / customer_name supplier_name
customer_code supplier_mdm_code
order_date contract_sign_date(标题用)
contract_sign_date / contract_start_date / contract_end_date 对应三列,格式 YYYY-MM-DD
payment_days str(payment_term)
is_prepayment 否
payment_method 账期支付
contract_category 采购协议
source_type ""
amount sum(qty * unit_price),四位小数字符串
belong_year 签订日年份

每一行 → ContractGoodsLine:

字段 来源
operation_code goods_id(弹窗实际搜 goods_code)
goods_code goods_id
goods_name goods_name
approval_no reg_no
spec / unit 同名
quantity qty
mdm_code goods_mdm_code
unit_price 四位小数字符串

第一行 supplier_mdm_code 为空:整组业务失败,原因「缺少供应商MDM码」,不调 runner。


5. 页面改动(只两处)

现有 run_contract_create_flow 继续串 S02–S13。本轮只加:

  1. S08 日期(fill_contract_amount_and_dates):签订日 = contract_sign_date or order_date;开始日 = contract_start_date or order_date;结束日仍用 contract_end_date。
  2. 尽调闸(S06 之后、S07 之前):check_customer_diligence 读页面正文。含「未尽调」则抛 DiligenceError(errors.py,NodeError 子类,文案「客户未尽调」)。生产 runner 让它冒泡到 job。

不改货品弹窗搜索、不改读号策略、不改 JSON 干跑提交前停住的行为。


6. 班次结果

@dataclass(frozen=True)
class ContractOutcome:
    kind: str          # "ok" | "diligence" | "fail"
    contract_no: str | None
    reason: str | None
kind 表 3 process_status / process_result 表 4
ok 且有合同号 1 / 成功说明 action_code=1,contract_status=1,contract_no,失败原因空
diligence 2 / 客户未尽调 action_code=1,contract_status=3,contract_fail_reason=客户未尽调,contract_no 空
fail 或其他异常 2 / 原因(截到 1000) contract_status=3,contract_fail_reason 截到 500,contract_no 空
runner 返回 ok 但合同号空 同 fail,「submit 模式未读取到真实合同编号」 同 fail

同一组所有表 3 fid 写同一套处理状态。表 4 按组内去重后的 oms_id(首次出现顺序)各插一行,合同号和状态相同;agreement_* 全空;org_code 取该 oms_id 对应表 3 行。

一次 save_group:先插表 4,再改表 3。中途崩溃停在 3、表 4 可能部分写入——Oracle 同一连接一事务提交。内存替身同样先写 4 再改 3,一次调用内完成。

生产 runner:由 ContractDraft 填 ContractFlowContext(含三个日期),调 run_contract_create_flow,读到号 → kind=ok;捕获 DiligenceError → kind=diligence;其他 NodeError / 异常 → kind=fail。


7. 接口 4 组包

对标 app/outbound/cart_settlement.py。

load_contract_agreement_envelope(repo, limit=500) -> {"data": [...]}

repo.list_unpushed():WHERE push_status = 0 ORDER BY fid。

每条 camelCase:

fid, omsId, actionCode, contractStatus, contractFailReason,
contractNo, agreementStatus, agreementFailReason,
agreementId, agreementDtlId, orgCode
  • fid / omsId / actionCode / 状态码:JSON 整数(库值 None 则字段为 null)
  • 字符串字段原样,空为 null
  • 不改 push_status,不发 HTTP

拆批:按 contract_no 分组(仅非空)。空 contract_no 每行单独一组。一组放不进当前批且批已有内容则停;批为空则整组放入(可超过 500,对齐接口 2)。


8. 文件落点

rpa_contract/(作业扁平,与选品同款):

文件 职责
group.py 可录判定、按 supplier_id 分组、草稿映射、金额四位小数
job.py claim_pending → 分组 → runner → save_group
repo.py MemoryContractRepo + OracleContractRepo:领取、插表 4、回写表 3
persist.py 由 ContractOutcome + 组行生成表 4 dict、表 3 状态
db.py 拷选品连接模式;空缺复用 rpa_oms/.env;用户名戳 合同班
errors.py 新增 DiligenceError
contract_flow_steps.py S08 日期回退;S06 后尽调扫描
run_batch.py 班次入口:from_env + 生产 runner。保留现有 main.py JSON 干跑
pyproject.toml 加 oracledb
tests/test_job.py 点名合单/跳过重录/未尽调
tests/test_group.py 头字段取第一行、金额合计、空白 contract_no 仍可领

rpa_oms/:

文件 职责
app/outbound/contract_agreement.py load_ / build_contract_agreement_envelope
app/outbound/memory_repo.py 增 MemoryContractAgreementRepo.list_unpushed
app/outbound/oracle_repo.py 增 OracleContractAgreementRepo(或同文件新类)只读未推行
tests/test_contract_agreement_outbound.py 点名组包
examples/contract_agreement_batch.json 与 Excel 请求示例同形(omsId 用整数)
README.md 接口 4:组包已做,path 空,不发 HTTP

rpa_contract/README.md:合单/尽调/写表 4 本轮已接;接口 4 组包在 oms,本班不推。


9. 验收用例(点名)

9.1 test_job_merges_same_supplier_and_writes_table4

两行同一 supplier_id,oms_id 分别为 2026100501、2026100502,process_status=0。第三行 contract_entry_status=0 且 contract_no="HT-OLD"。runner 记调用次数并返回 kind=ok, contract_no="HT20261005001"。

断言:

  • runner 调用 1 次;草稿两行货品;客商编码为第一行 MDM
  • 表 4 两行:同一 contract_no,action_code=1,contract_status=1,push_status=0,oms_id 各一
  • 前两行表 3 process_status=1;第三行仍为 0(未领)

9.2 test_job_marks_diligence_fail

一组一行。runner 返回 kind=diligence, reason="客户未尽调"。

断言:表 3 process_status=2,process_result 含「客户未尽调」;表 4 contract_status=3,contract_no 空,contract_fail_reason="客户未尽调"。

9.3 test_assembles_unpushed_rows_as_document_envelope

内存两行 push_status=0:一行 action_code=1, contract_status=1, contract_no="HT1";一行 action_code=2, agreement_status=1。另有一行 push_status=1 不进包。

断言信封 data 两条,camelCase 与第 7 节字段一致,omsId 为 int,已推行不出现。

另测(对齐接口 2 _take_complete_groups):同一 contract_no 三行、limit=2、当前批为空 → 整组三条都放入(允许超过 limit)。两个不同 contract_no 各一行、limit=1 → 只出第一组。


10. 风险与边界

  • 尽调靠正文扫描,页面若换文案会漏拦。本轮不补选择器。
  • 货品弹窗仍按 goods_id / 品名搜,不改成 MDM。
  • 表 4 没有表 3 fid 列,回执关联键是表 4 自己的 fid 和 omsId。
  • 真库领取/落表在 Windows 上做,本轮验收不连库。
  • 接口 4 path、Token、是否带 orgSecret 仍按口径第 6 节后置,本轮不发明 URL。