13 KiB
合同班:表 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. 要做成什么
- 合同班启动瞬间冻结本班可录名单:表 3
process_status0→3。 - 按
supplierId合成一份合同;头字段取该组按fid的第一行;货品行全收。 contractEntryStatus=0且已有contractNo的行不领取,留给返利班。- 注入 runner 调现有合同页面流。单测不打开浏览器。
- 提交成功:表 4
actionCode=1、contractStatus=1、带合同号;表 3process_status=1。同一合同下多个omsId回同一套号和状态。 - 失败(未尽调 / 页面挂):表 4
contractStatus=3带原因;表 3process_status=2。 - 接口 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 升序第一行 |
| 表 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 干跑不破) |
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。本轮只加:
- S08 日期(
fill_contract_amount_and_dates):签订日 =contract_sign_date or order_date;开始日 =contract_start_date or order_date;结束日仍用contract_end_date。 - 尽调闸(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,已推行不出现。
另测:同一 contract_no 三行、limit=2 时三条都不进当前批(批为空则整组放入的规则:此处批空 + 组大小 3 > 2 → 整组放入,与接口 2 相同)。再测两个不同 contract_no、limit=1:只出第一组。
10. 风险与边界
- 尽调靠正文扫描,页面若换文案会漏拦。本轮不补选择器。
- 货品弹窗仍按
goods_id/ 品名搜,不改成 MDM。 - 表 4 没有表 3
fid列,回执关联键是表 4 自己的fid和omsId。 - 真库领取/落表在 Windows 上做,本轮验收不连库。
- 接口 4 path、Token、是否带
orgSecret仍按口径第 6 节后置,本轮不发明 URL。