266 lines
13 KiB
Markdown
266 lines
13 KiB
Markdown
# 合同班:表 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` 升序第一行 |
|
||
| 表 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. 班次结果
|
||
|
||
```python
|
||
@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`。
|
||
|
||
```python
|
||
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。
|