5D3420315FA54219AB2427B0C674AD66auto
This commit is contained in:
commit
5bf598de05
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"LineList": [
|
||||||
|
],
|
||||||
|
"NodeList": [
|
||||||
|
{
|
||||||
|
"NodePanelData": {
|
||||||
|
"ExpandedGroups": [
|
||||||
|
],
|
||||||
|
"InputProperty": {
|
||||||
|
"mouse_x": 0,
|
||||||
|
"mouse_y": 0
|
||||||
|
},
|
||||||
|
"PubProperty": "",
|
||||||
|
"RetPins": [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"NodeProperty": "",
|
||||||
|
"NodeUI": {
|
||||||
|
"NodeElementID": "",
|
||||||
|
"NodeModel": "2",
|
||||||
|
"NodePropErr": false,
|
||||||
|
"NodePx": "2050",
|
||||||
|
"NodePy": "2341",
|
||||||
|
"NodeSubType": "EnumWidget_Begin",
|
||||||
|
"NodeTag": "2026081210133159517",
|
||||||
|
"NodeTitle": "开始"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SeqTag": "Main",
|
||||||
|
"ViewCenterPx": "2503",
|
||||||
|
"ViewCenterPy": "2830",
|
||||||
|
"ViewScaling": "1"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"GlobalVar": [
|
||||||
|
],
|
||||||
|
"OpenFlow": "[\n {\n \"current-tab\": \"true\",\n \"seq\": \"Main\",\n \"tag\": \"2026081210133309518\",\n \"title\": \"Main\",\n \"type\": \"bp\"\n }\n]\n",
|
||||||
|
"ProInfo": {
|
||||||
|
"CodeCustomCount": "0",
|
||||||
|
"ProAbsolutePath": "D:/RPA/AutoRpa",
|
||||||
|
"ProChange": "2026/08/12 11:13:33",
|
||||||
|
"ProColorRef": "",
|
||||||
|
"ProCreate": "2026/08/12 10:13:31",
|
||||||
|
"ProDesc": "",
|
||||||
|
"ProDocVersion": "1.0.0.4",
|
||||||
|
"ProImports": "",
|
||||||
|
"ProIsAdminRun": "0",
|
||||||
|
"ProIsNewReturn": "1",
|
||||||
|
"ProName": "AutoRpa",
|
||||||
|
"ProStudio": "2026.1.0.36",
|
||||||
|
"ProTag": "2026081210133159316",
|
||||||
|
"ProTheme": "light",
|
||||||
|
"ProUIADeepMatch": "1",
|
||||||
|
"ProUserID": "34BFF76918A9449ABAB323149DDCEA73",
|
||||||
|
"ProUserName": "Administrator",
|
||||||
|
"ProVersion": "1.0.0.0",
|
||||||
|
"operatingSystem": 0,
|
||||||
|
"releaseUserName": "16624637724"
|
||||||
|
},
|
||||||
|
"SeqGroupList": [
|
||||||
|
],
|
||||||
|
"SeqPythonModuleList": [
|
||||||
|
],
|
||||||
|
"SeqTextFileList": [
|
||||||
|
],
|
||||||
|
"SequenceList": [
|
||||||
|
{
|
||||||
|
"sdc": "",
|
||||||
|
"seq": "Main",
|
||||||
|
"seqptag": "",
|
||||||
|
"seqret": "1",
|
||||||
|
"seqtag": "2026081210133309518",
|
||||||
|
"spr": [
|
||||||
|
],
|
||||||
|
"spv": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# 编译日期:2026-08-12 10:13:31
|
||||||
|
# 版权所有:www.i-search.com.cn
|
||||||
|
#
|
||||||
|
# ============================================================================
|
||||||
|
# 合同建单 (CONTRACT_CREATE) RPA 流程 - 全局函数
|
||||||
|
# ============================================================================
|
||||||
|
# 在 APA Studio 的 Python 脚本节点中调用:
|
||||||
|
# 1. 拖一个 "Python 脚本块" 到画布
|
||||||
|
# 2. 从 codes/rpa_yisaiqi 导入需要的函数
|
||||||
|
# 3. 示例见下方 __main__ 块
|
||||||
|
#
|
||||||
|
# 流程图节点清单 (15步 + 4异常分支):
|
||||||
|
# 参考: codes/rpa_yisaiqi/flows/contract_create_flow.json
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 确保 codes 目录在 sys.path 中,以便 APA Python 3.8 能导入 rpa_yisaiqi
|
||||||
|
_CODES_DIR = Path(__file__).resolve().parent # .../AutoRpa/codes/
|
||||||
|
if str(_CODES_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_CODES_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
def verify_module_imports():
|
||||||
|
"""验证所有模块可以正常导入(不依赖 APA ubpa SDK)。"""
|
||||||
|
results = {}
|
||||||
|
modules = [
|
||||||
|
("callback_client", "rpa_yisaiqi.callback_client", "StateCenterClient"),
|
||||||
|
("contract_flow_steps", "rpa_yisaiqi.contract_flow_steps", "ContractFlowContext"),
|
||||||
|
("contract_draft", "rpa_yisaiqi.contract_draft", "ContractDraft"),
|
||||||
|
("pdf_utils", "rpa_yisaiqi.pdf_utils", "ensure_merged_pdf"),
|
||||||
|
("contract_create_node", "rpa_yisaiqi.contract_create_node", "ContractCreateInput"),
|
||||||
|
]
|
||||||
|
for name, module_path, class_name in modules:
|
||||||
|
try:
|
||||||
|
mod = __import__(module_path, fromlist=[class_name])
|
||||||
|
getattr(mod, class_name)
|
||||||
|
results[name] = "OK"
|
||||||
|
except Exception as e:
|
||||||
|
results[name] = f"FAIL: {e}"
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def build_yyc_dry_run_context():
|
||||||
|
"""构建 YYC 来源的 dry-run 流程上下文(不提交合同)。"""
|
||||||
|
from rpa_yisaiqi.contract_flow_steps import ContractFlowContext, ContractGoodsLine
|
||||||
|
|
||||||
|
return ContractFlowContext(
|
||||||
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
|
username="test002",
|
||||||
|
password="1",
|
||||||
|
supplier_name="测试供应商",
|
||||||
|
order_date="2026-08-12",
|
||||||
|
source_type="YYC",
|
||||||
|
template_type="NON_FORMAT",
|
||||||
|
template_name="非格式--采购协议",
|
||||||
|
customer_code="CUST-001",
|
||||||
|
customer_name="测试供应商",
|
||||||
|
contract_category="采购协议",
|
||||||
|
settlement_type="付款",
|
||||||
|
contract_background="自动化测试合同起草",
|
||||||
|
contract_nature="主合同",
|
||||||
|
amount_currency="人民币",
|
||||||
|
amount="50000.00",
|
||||||
|
contract_end_date="2026-12-31",
|
||||||
|
is_major_contract="否",
|
||||||
|
is_prepayment="否",
|
||||||
|
belong_year="2026",
|
||||||
|
payment_method="账期支付",
|
||||||
|
payment_days="30",
|
||||||
|
contract_status="正式合同",
|
||||||
|
contains_blood_products="否",
|
||||||
|
submit_mode="dry_run",
|
||||||
|
goods_lines=[
|
||||||
|
ContractGoodsLine(
|
||||||
|
operation_code="ADD", goods_code="YP-001",
|
||||||
|
goods_name="测试药品A", approval_no="国药准字H00000001",
|
||||||
|
spec="50mg*30片", unit="盒", quantity=10.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
merged_pdf_path=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_draft_from_json(json_path):
|
||||||
|
"""从 JSON 文件加载合同起草数据。"""
|
||||||
|
from rpa_yisaiqi.contract_draft import load_contract_draft
|
||||||
|
return load_contract_draft(json_path)
|
||||||
|
|
||||||
|
|
||||||
|
def run_contract_flow_with_apa_browser(browser, ctx):
|
||||||
|
"""
|
||||||
|
在 APA Studio 中使用真实的 ubpa 浏览器适配器执行流程。
|
||||||
|
|
||||||
|
browser: 实现了 BrowserAdapter 协议的对象 (如 ubpa 的 iedge/ichrome_firefox)
|
||||||
|
ctx: ContractFlowContext 实例
|
||||||
|
|
||||||
|
返回合同编号字符串(dry_run 模式返回空字符串)。
|
||||||
|
"""
|
||||||
|
from rpa_yisaiqi.contract_flow_steps import run_contract_create_flow
|
||||||
|
return run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# APA Studio 集成说明:
|
||||||
|
#
|
||||||
|
# 方式一: Python 脚本块(推荐)
|
||||||
|
# 在 APA Studio 画布拖一个 "Python 脚本块",写入:
|
||||||
|
#
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, r"d:\RPA\AutoRpa\codes")
|
||||||
|
# from GlobalFun import build_yyc_dry_run_context, run_contract_flow_with_apa_browser
|
||||||
|
# from rpa_yisaiqi.contract_flow_steps import BrowserAdapter
|
||||||
|
#
|
||||||
|
# ctx = build_yyc_dry_run_context()
|
||||||
|
# # browser 由 APA Studio 提供 (如 ubpa 网页对象)
|
||||||
|
# contract_no = run_contract_flow_with_apa_browser(browser, ctx)
|
||||||
|
#
|
||||||
|
# 方式二: 原生活动拖拽 (15个节点)
|
||||||
|
# 按 codes/rpa_yisaiqi/flows/contract_create_flow.json 的顺序,
|
||||||
|
# 在画布上依次拖拽: 打开网页 → 输入文本 → 鼠标点击 → ...
|
||||||
|
# 每个节点对应 contract_flow_steps.py 中的一个函数。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# 测试运行或调试以上全局函数入口
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("GlobalFun 模块导入验证:")
|
||||||
|
results = verify_module_imports()
|
||||||
|
for name, status in results.items():
|
||||||
|
print(f" [{status}] {name}")
|
||||||
|
|
||||||
|
print("\n构建 YYC dry-run 上下文:")
|
||||||
|
ctx = build_yyc_dry_run_context()
|
||||||
|
print(f" supplier_name={ctx.supplier_name}")
|
||||||
|
print(f" source_type={ctx.source_type}")
|
||||||
|
print(f" template_name={ctx.template_name}")
|
||||||
|
print(f" submit_mode={ctx.submit_mode}")
|
||||||
|
print(f" goods_lines={len(ctx.goods_lines)} 条")
|
||||||
|
|
||||||
|
print("\n加载 YYC JSON 示例:")
|
||||||
|
draft = load_draft_from_json(
|
||||||
|
r"d:\RPA\AutoRpa\codes\rpa_yisaiqi\examples\contract_draft_yyc.json"
|
||||||
|
)
|
||||||
|
print(f" supplier={draft.supplier_name}, source={draft.source_type}")
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# coding: utf8
|
||||||
|
"""
|
||||||
|
Python模块脚本
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 项目路径、资源库元素文件(建议其他元素库文件路径在此处声明)
|
||||||
|
ProjectPath = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
CodePath = os.path.join(ProjectPath, "codes")
|
||||||
|
ModulePath = os.path.join(CodePath, "modules")
|
||||||
|
ElementFile = os.path.join(ModulePath, "resources", "elements.json")
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
|
||||||
|
# 更多示例代码 http://support.i-search.com.cn:8088/showdoc/web/#/page/11923
|
||||||
|
|
||||||
|
## 导入资源库
|
||||||
|
|
||||||
|
'''
|
||||||
|
from iresource import iSResource
|
||||||
|
|
||||||
|
is_resource: iSResource = iSResource(__file__)
|
||||||
|
|
||||||
|
或者手动指定其他工程元素库文件(当前项目或其他项目)
|
||||||
|
|
||||||
|
ElementFile = r"~/NewProject1/codes/modules/resources/elements.json"
|
||||||
|
is_resource: iSResource = iSResource(json_file=ElementFile)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## 全部资源
|
||||||
|
|
||||||
|
'''
|
||||||
|
all_resource = is_resource.all()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## 获取全局变量、流程变量(参数)
|
||||||
|
|
||||||
|
'''
|
||||||
|
gv1 = is_resource.get_global_var("gv_1")
|
||||||
|
lv1 = is_resource.get_flow_var("lv_1")
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## 设置全局变量、流程变量(参数)
|
||||||
|
|
||||||
|
'''
|
||||||
|
gv1 = is_resource.set_global_var("gv_1", "gv_1新值")
|
||||||
|
lv1 = is_resource.set_flow_var("lv_1", "lv_1新值")
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## 获取元素对象(这段代码可从【元素库】中右击元素,点击【复制代码】直接获得)
|
||||||
|
|
||||||
|
'''
|
||||||
|
element1 = is_resource.get_element("元素ID")
|
||||||
|
element1.show() # 打印显示元素信息
|
||||||
|
all_element = is_resource.elements() # 全部元素
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## 支持 By.ID,By.CODE,By.NAME 获取元素对象
|
||||||
|
|
||||||
|
'''
|
||||||
|
is_resource.get_element(By.ID, "元素ID")
|
||||||
|
is_resource.get_element(By.CODE, "元素Code")
|
||||||
|
is_resource.get_element(By.NAME, "元素Name")
|
||||||
|
|
||||||
|
|
||||||
|
## 也支持具体方法获取元素对象
|
||||||
|
|
||||||
|
'''
|
||||||
|
is_resource.get_element_by_id("元素id")
|
||||||
|
is_resource.get_element_by_id("元素Code")
|
||||||
|
is_resource.get_element_by_id("元素Name")
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
## 元素对象的 方法 和 属性
|
||||||
|
|
||||||
|
'''
|
||||||
|
element1.actions() # 所有方法
|
||||||
|
element1.properties() # 所有属性
|
||||||
|
element1.path # 元素路径
|
||||||
|
element1.snapshot # 元素快照
|
||||||
|
element1.help("click") # 查看方法使用文档
|
||||||
|
element1.click(wait_seconds=5)
|
||||||
|
is_resource.set_action_wait_seconds(5) # 全局调整动作方法执行的等待时间秒数
|
||||||
|
element1.set_text(value="测试一下")
|
||||||
|
'''
|
||||||
|
|
||||||
|
## 对象化使用
|
||||||
|
|
||||||
|
'''
|
||||||
|
# 使用对象库,通过iSObject对象可以调用子对象(如:键盘、窗口、Excel等)
|
||||||
|
iSObject.Keyboard(element1.window_title).keypress_input("abc") # 模拟按键
|
||||||
|
iSObject.Server().set_var("abc", "123") # 设置服务器变量
|
||||||
|
'''
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"version": "2.0"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
# 艺赛旗 CONTRACT_CREATE 联调骨架
|
||||||
|
|
||||||
|
这个目录放艺赛旗侧可复制到 Windows 虚拟机的代码。第一版目标是先验证:
|
||||||
|
|
||||||
|
1. Windows 虚拟机能访问状态中心。
|
||||||
|
2. `CONTRACT_CREATE` 能按约定调用 `start`。
|
||||||
|
3. 合同系统动作完成后能调用 `complete`,并带 `businessRefs.contractSystemNo`。
|
||||||
|
4. 登录失败、页面改版、合同禁用等异常能调用 `fail`。
|
||||||
|
|
||||||
|
## 合同节点步骤点
|
||||||
|
|
||||||
|
来自 `state_center/给艺赛旗的开发交接.md`:
|
||||||
|
|
||||||
|
1. 打开合同测试系统:`http://contract-test.gzmpc.com:8080/`
|
||||||
|
2. 登录:`test002 / 1`
|
||||||
|
3. 左侧菜单进入 `合同管理 > 合同起草`
|
||||||
|
4. 在最近使用模板中选择 `templateName`,非格式默认 `非格式--采购协议`
|
||||||
|
5. 填签约方信息:客户编码、客户名称
|
||||||
|
6. 填基础信息:合同版本类型、合同类别、结算类型、合同背景概述、合同性质
|
||||||
|
7. 填金额和日期,合同签订日期和合同开始日期必须等于订单日期
|
||||||
|
8. 填货品明细
|
||||||
|
9. 填付款信息与特殊标志:是否重大合同、是否预付款、归属年份、付款方式、付款期限、合同状态、是否包含血液制品
|
||||||
|
10. 医药城 `YYC`:同供应商同日 PDF 先合并成一个再上传
|
||||||
|
11. 小药药 `XYY`:格式合同,跳过合同证文上传
|
||||||
|
12. 提交并读取合同编号
|
||||||
|
13. 调状态中心 `complete`,必传 `businessRefs.contractSystemNo`
|
||||||
|
|
||||||
|
## 先做联通验证
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m rpa_yisaiqi.contract_create_node `
|
||||||
|
--state-center-url http://127.0.0.1:8000 `
|
||||||
|
--token dev-agent-token `
|
||||||
|
--job-id YYC-联调测试供应商-20260811-a6e5df `
|
||||||
|
--supplier-name 联调测试供应商 `
|
||||||
|
--order-date 2026-08-11 `
|
||||||
|
--source-type YYC `
|
||||||
|
--contract-system-no HT-20260811-001
|
||||||
|
```
|
||||||
|
|
||||||
|
这条只验证回调链路,合同号由命令行传入。
|
||||||
|
|
||||||
|
## 真实合同起草运行
|
||||||
|
|
||||||
|
先准备合同起草输入 JSON,参考:
|
||||||
|
|
||||||
|
- `examples/contract_draft_yyc.json`
|
||||||
|
- `examples/contract_draft_xyy.json`
|
||||||
|
|
||||||
|
YYC 有多份平台 PDF 时需要合并,先安装:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pip install -r rpa_yisaiqi/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
然后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m rpa_yisaiqi.contract_create_node `
|
||||||
|
--state-center-url http://192.168.x.x:8000 `
|
||||||
|
--token dev-agent-token `
|
||||||
|
--job-id <状态中心job_id> `
|
||||||
|
--supplier-name 重庆亿昊药业 `
|
||||||
|
--order-date 2026-08-11 `
|
||||||
|
--source-type YYC `
|
||||||
|
--draft-path rpa_yisaiqi/examples/contract_draft_yyc.json `
|
||||||
|
--merged-pdf-output C:/rpa/jobs/<状态中心job_id>/contract-merged.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
这条会走真实起草流程:
|
||||||
|
|
||||||
|
1. 状态中心 `/start`
|
||||||
|
2. 打开合同系统并登录
|
||||||
|
3. 进入 `合同管理 > 合同起草`
|
||||||
|
4. 选择最近使用模板
|
||||||
|
5. 填签约方、基础信息、金额日期、货品明细、付款和特殊标志
|
||||||
|
6. YYC 合并并上传 PDF,XYY 跳过上传
|
||||||
|
7. 提交
|
||||||
|
8. 读取合同编号
|
||||||
|
9. 状态中心 `/complete`
|
||||||
|
|
||||||
|
### 先做 dry-run 预览
|
||||||
|
|
||||||
|
如果你只想先验证页面,不想真的提交,把输入里的 `submitMode` 设成 `dry_run`。这时流程会跑到提交前并输出 `contract-dry-run-ready` 截图,不会点提交,也不会读取真实合同号。
|
||||||
|
|
||||||
|
适合先校准 selector 的情况:
|
||||||
|
|
||||||
|
1. 进入 `合同管理 > 合同起草`
|
||||||
|
2. 选择最近使用模板
|
||||||
|
3. 填签约方、基础信息、金额日期、明细、付款和特殊标志
|
||||||
|
4. 停在提交前
|
||||||
|
|
||||||
|
如果状态中心部署在 macOS 主机,`--state-center-url` 换成 Windows 虚拟机能访问到的地址,例如:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
--state-center-url http://192.168.x.x:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入真实页面自动化
|
||||||
|
|
||||||
|
真实艺赛旗页面操作按流程图拆在:
|
||||||
|
|
||||||
|
- `flows/contract_create_flow.json`:流程图节点清单
|
||||||
|
- `contract_flow_steps.py`:每个流程图节点对应的动作函数
|
||||||
|
- `yisaiqi_adapter_template.py`:艺赛旗浏览器活动适配模板
|
||||||
|
|
||||||
|
艺赛旗设计器里建议按这个顺序拖流程图:
|
||||||
|
|
||||||
|
1. `S01_CALLBACK_START`:HTTP 请求状态中心 `/start`
|
||||||
|
2. `S02_OPEN_CONTRACT_SYSTEM`:打开合同系统
|
||||||
|
3. `S03_LOGIN`:输入 `j_username`、`j_password`,点击 `btn_submit`
|
||||||
|
4. `S04_ENTER_CONTRACT_DRAFT`:进入 `合同管理 > 合同起草`
|
||||||
|
5. `S05_SELECT_RECENT_TEMPLATE`:选择最近使用模板
|
||||||
|
6. `S06_FILL_SIGNATORY_INFO`:填客户编码、客户名称
|
||||||
|
7. `S07_FILL_CONTRACT_BASIC_FORM`:填合同类别、结算类型、背景概述、合同性质
|
||||||
|
8. `S08_FILL_AMOUNT_AND_DATES`:填金额、签订日期、开始日期、结束日期
|
||||||
|
9. `S09_FILL_GOODS_LINES`:循环填货品明细
|
||||||
|
10. `S10_FILL_PAYMENT_AND_FLAGS`:填付款方式、付款期限、合同状态、是否包含血液制品等
|
||||||
|
11. `S11_MERGE_PDF_IF_YYC`:`sourceType=YYC` 时先合并同供应商同日 PDF;`XYY` 跳过
|
||||||
|
12. `S12_UPLOAD_PDF_IF_YYC`:`sourceType=YYC` 时上传合并 PDF;`XYY` 跳过
|
||||||
|
13. `S13_SUBMIT`:提交
|
||||||
|
14. `S14_READ_CONTRACT_NO`:读取合同编号到变量 `contractSystemNo`
|
||||||
|
15. `S15_CALLBACK_COMPLETE`:HTTP 请求状态中心 `/complete`
|
||||||
|
|
||||||
|
如果只做预览,把这条流程里的 `submitMode` 设成 `dry_run`,或者直接导入 `flows/contract_create_preview_flow.json`。
|
||||||
|
|
||||||
|
如果艺赛旗支持 Python 脚本块,直接把每个 `contract_flow_steps.py` 函数挂到对应流程图节点。
|
||||||
|
如果艺赛旗只支持原生活动,就按 `flows/contract_create_flow.json` 的节点顺序拖活动,用 `yisaiqi_adapter_template.py` 里的方法名做映射。
|
||||||
|
|
||||||
|
外层回调骨架仍然在 `contract_create_node.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
create_contract_in_contract_system(spec)
|
||||||
|
```
|
||||||
|
|
||||||
|
这个函数最后返回合同编号字符串即可。状态中心回调、幂等键、错误码映射已经封在外层。后续接真页面时,可以在这里调用艺赛旗流程图,或让流程图最后把合同编号回传给这个函数。
|
||||||
|
|
||||||
|
## 异常映射
|
||||||
|
|
||||||
|
| 场景 | 异常类 | 回调错误码 | retryable | routeRole |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 登录失败 | `LoginFailed` | `SYS_LOGIN_FAIL` | true | OPS |
|
||||||
|
| 供应商合同被禁/失效 | `SupplierContractDisabled` | `BIZ_CONTRACT_DISABLED` | false | PURCHASER |
|
||||||
|
| PDF 合并能力不支持 | `PdfMergeUnsupported` | `CAP_PDF_MERGE_UNSUPPORTED` | false | OPS |
|
||||||
|
| 页面改版/元素找不到 | `ContractCreateError` | `UI_ELEMENT_NOT_FOUND` | false | OPS |
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
"""YiSaiQi RPA integration helpers."""
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""HTTP client for YiSaiQi callbacks into the state center."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Callable
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
from urllib.parse import quote
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
JsonDict = dict
|
||||||
|
Opener = Callable[[Request, float], Any]
|
||||||
|
|
||||||
|
|
||||||
|
class StateCenterError(RuntimeError):
|
||||||
|
def __init__(self, status: int, detail: Any):
|
||||||
|
super().__init__(f"state center returned {status}: {detail}")
|
||||||
|
self.status = status
|
||||||
|
self.detail = detail
|
||||||
|
|
||||||
|
|
||||||
|
class StateCenterClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
token: str,
|
||||||
|
*,
|
||||||
|
timeout: float = 15,
|
||||||
|
opener: Opener | None = None,
|
||||||
|
):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self.timeout = timeout
|
||||||
|
self._opener = opener or _urlopen_with_timeout
|
||||||
|
|
||||||
|
def start_node(
|
||||||
|
self,
|
||||||
|
job_id: str,
|
||||||
|
node_code: str,
|
||||||
|
*,
|
||||||
|
attempt_no: int = 1,
|
||||||
|
operator: str = "agent",
|
||||||
|
) -> JsonDict:
|
||||||
|
return self._post(
|
||||||
|
f"/api/jobs/{_path_part(job_id)}/nodes/{_path_part(node_code)}/start",
|
||||||
|
{
|
||||||
|
"attemptNo": attempt_no,
|
||||||
|
"executorPlatform": "iS-RPA",
|
||||||
|
"operator": operator,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def complete_node(
|
||||||
|
self,
|
||||||
|
job_id: str,
|
||||||
|
node_code: str,
|
||||||
|
payload: JsonDict,
|
||||||
|
) -> JsonDict:
|
||||||
|
return self._post(f"/api/jobs/{_path_part(job_id)}/nodes/{_path_part(node_code)}/complete", payload)
|
||||||
|
|
||||||
|
def fail_node(
|
||||||
|
self,
|
||||||
|
job_id: str,
|
||||||
|
node_code: str,
|
||||||
|
payload: JsonDict,
|
||||||
|
) -> JsonDict:
|
||||||
|
return self._post(f"/api/jobs/{_path_part(job_id)}/nodes/{_path_part(node_code)}/fail", payload)
|
||||||
|
|
||||||
|
def _post(self, path: str, payload: JsonDict) -> JsonDict:
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
request = Request(
|
||||||
|
f"{self.base_url}{path}",
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Agent-Token": self.token,
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with self._opener(request, self.timeout) as response:
|
||||||
|
raw = response.read().decode("utf-8")
|
||||||
|
except HTTPError as exc:
|
||||||
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
|
raise StateCenterError(exc.code, detail) from exc
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _path_part(value: str) -> str:
|
||||||
|
return quote(value, safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def _urlopen_with_timeout(request: Request, timeout: float) -> Any:
|
||||||
|
return urlopen(request, timeout=timeout)
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
"""CONTRACT_CREATE node skeleton for YiSaiQi Windows integration.
|
||||||
|
|
||||||
|
This first version proves the state-center callback path and leaves the
|
||||||
|
contract-system page automation behind a single replaceable runner function.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import webbrowser
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from .callback_client import StateCenterClient
|
||||||
|
from .contract_draft import ContractDraft, load_contract_draft
|
||||||
|
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
||||||
|
from .pdf_utils import ensure_merged_pdf
|
||||||
|
from .yisaiqi_adapter_template import YiSaiQiBrowserAdapter
|
||||||
|
|
||||||
|
NODE_CODE = "CONTRACT_CREATE"
|
||||||
|
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
|
||||||
|
|
||||||
|
|
||||||
|
class ContractCreateError(RuntimeError):
|
||||||
|
error_code = "UI_ELEMENT_NOT_FOUND"
|
||||||
|
retryable = False
|
||||||
|
route_role = "OPS"
|
||||||
|
|
||||||
|
|
||||||
|
class LoginFailed(ContractCreateError):
|
||||||
|
error_code = "SYS_LOGIN_FAIL"
|
||||||
|
retryable = True
|
||||||
|
route_role = "OPS"
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierContractDisabled(ContractCreateError):
|
||||||
|
error_code = "BIZ_CONTRACT_DISABLED"
|
||||||
|
retryable = False
|
||||||
|
route_role = "PURCHASER"
|
||||||
|
|
||||||
|
|
||||||
|
class PdfMergeUnsupported(ContractCreateError):
|
||||||
|
error_code = "CAP_PDF_MERGE_UNSUPPORTED"
|
||||||
|
retryable = False
|
||||||
|
route_role = "OPS"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ContractCreateInput:
|
||||||
|
job_id: str
|
||||||
|
supplier_name: str
|
||||||
|
order_date: str
|
||||||
|
source_type: str
|
||||||
|
contract_system_no: str | None = None
|
||||||
|
attempt_no: int = 1
|
||||||
|
contract_url: str = CONTRACT_URL
|
||||||
|
username: str = "test002"
|
||||||
|
password: str = "1"
|
||||||
|
open_browser: bool = False
|
||||||
|
draft_path: str | None = None
|
||||||
|
merged_pdf_output: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
ContractRunner = Callable[[ContractCreateInput], str]
|
||||||
|
|
||||||
|
|
||||||
|
def execute_contract_create(
|
||||||
|
spec: ContractCreateInput,
|
||||||
|
client: StateCenterClient,
|
||||||
|
*,
|
||||||
|
contract_runner: ContractRunner | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
client.start_node(spec.job_id, NODE_CODE, attempt_no=spec.attempt_no)
|
||||||
|
runner = contract_runner or create_contract_in_contract_system
|
||||||
|
try:
|
||||||
|
contract_no = runner(spec)
|
||||||
|
complete_payload = build_complete_payload(spec, contract_no)
|
||||||
|
return client.complete_node(spec.job_id, NODE_CODE, complete_payload)
|
||||||
|
except ContractCreateError as exc:
|
||||||
|
fail_payload = build_fail_payload(spec, exc)
|
||||||
|
return client.fail_node(spec.job_id, NODE_CODE, fail_payload)
|
||||||
|
|
||||||
|
|
||||||
|
def create_contract_in_contract_system(spec: ContractCreateInput) -> str:
|
||||||
|
"""Draft the contract in the contract system and return contract number."""
|
||||||
|
if spec.open_browser:
|
||||||
|
webbrowser.open(spec.contract_url)
|
||||||
|
if spec.contract_system_no:
|
||||||
|
return spec.contract_system_no
|
||||||
|
if not spec.draft_path:
|
||||||
|
raise ContractCreateError("draft_path is required for real contract drafting")
|
||||||
|
draft = load_contract_draft(spec.draft_path)
|
||||||
|
ctx = build_flow_context(spec, draft)
|
||||||
|
browser = YiSaiQiBrowserAdapter()
|
||||||
|
try:
|
||||||
|
return run_contract_create_flow(browser, ctx)
|
||||||
|
except NotImplementedError as exc:
|
||||||
|
raise ContractCreateError(
|
||||||
|
"YiSaiQiBrowserAdapter still needs to be mapped to YiSaiQi designer activities"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> ContractFlowContext:
|
||||||
|
merged_pdf_path = draft.merged_pdf_path
|
||||||
|
if draft.source_type == "YYC":
|
||||||
|
output_path = spec.merged_pdf_output or str(
|
||||||
|
Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
merged_pdf_path = ensure_merged_pdf(draft.platform_pdf_paths, output_path)
|
||||||
|
except (RuntimeError, ValueError) as exc:
|
||||||
|
raise PdfMergeUnsupported(str(exc)) from exc
|
||||||
|
return ContractFlowContext(
|
||||||
|
contract_url=spec.contract_url,
|
||||||
|
username=spec.username,
|
||||||
|
password=spec.password,
|
||||||
|
supplier_name=draft.supplier_name or spec.supplier_name,
|
||||||
|
order_date=draft.order_date or spec.order_date,
|
||||||
|
source_type=draft.source_type or spec.source_type,
|
||||||
|
template_type=draft.template_type,
|
||||||
|
template_name=draft.template_name,
|
||||||
|
customer_code=draft.customer_code,
|
||||||
|
customer_name=draft.customer_name,
|
||||||
|
contract_category=draft.contract_category,
|
||||||
|
settlement_type=draft.settlement_type,
|
||||||
|
contract_background=draft.contract_background,
|
||||||
|
contract_nature=draft.contract_nature,
|
||||||
|
amount_currency=draft.amount_currency,
|
||||||
|
amount=draft.amount,
|
||||||
|
contract_end_date=draft.contract_end_date,
|
||||||
|
is_major_contract=draft.is_major_contract,
|
||||||
|
is_prepayment=draft.is_prepayment,
|
||||||
|
belong_year=draft.belong_year,
|
||||||
|
payment_method=draft.payment_method,
|
||||||
|
payment_days=draft.payment_days,
|
||||||
|
contract_status=draft.contract_status,
|
||||||
|
contains_blood_products=draft.contains_blood_products,
|
||||||
|
submit_mode=draft.submit_mode,
|
||||||
|
goods_lines=draft.goods_lines,
|
||||||
|
merged_pdf_path=merged_pdf_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"attemptNo": spec.attempt_no,
|
||||||
|
"executorRunId": f"YSQ-CONTRACT-{uuid.uuid4().hex[:12]}",
|
||||||
|
"idempotencyKey": f"{spec.job_id}:{NODE_CODE}:{spec.attempt_no}:{contract_no}",
|
||||||
|
"businessRefs": {"contractSystemNo": contract_no},
|
||||||
|
"summary": {
|
||||||
|
"supplierName": spec.supplier_name,
|
||||||
|
"contractDate": spec.order_date,
|
||||||
|
"sourceType": spec.source_type,
|
||||||
|
"contractUrl": spec.contract_url,
|
||||||
|
"uploadRule": "merge_pdf_before_upload" if spec.source_type == "YYC" else "skip_contract_upload",
|
||||||
|
},
|
||||||
|
"artifacts": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_fail_payload(spec: ContractCreateInput, exc: ContractCreateError) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"attemptNo": spec.attempt_no,
|
||||||
|
"errorCode": exc.error_code,
|
||||||
|
"errorSummary": str(exc),
|
||||||
|
"retryable": exc.retryable,
|
||||||
|
"routeRole": exc.route_role,
|
||||||
|
"artifacts": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE callback skeleton.")
|
||||||
|
parser.add_argument("--state-center-url", default=os.getenv("STATE_CENTER_URL", "http://127.0.0.1:8000"))
|
||||||
|
parser.add_argument("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token"))
|
||||||
|
parser.add_argument("--job-id", required=True)
|
||||||
|
parser.add_argument("--supplier-name", required=True)
|
||||||
|
parser.add_argument("--order-date", required=True)
|
||||||
|
parser.add_argument("--source-type", choices=("YYC", "XYY", "OFFLINE"), default="YYC")
|
||||||
|
parser.add_argument("--contract-system-no", default=os.getenv("CONTRACT_SYSTEM_NO"))
|
||||||
|
parser.add_argument("--attempt-no", type=int, default=1)
|
||||||
|
parser.add_argument("--contract-url", default=os.getenv("CONTRACT_URL", CONTRACT_URL))
|
||||||
|
parser.add_argument("--username", default=os.getenv("CONTRACT_USERNAME", "test002"))
|
||||||
|
parser.add_argument("--password", default=os.getenv("CONTRACT_PASSWORD", "1"))
|
||||||
|
parser.add_argument("--open-browser", action="store_true")
|
||||||
|
parser.add_argument("--draft-path", default=os.getenv("CONTRACT_DRAFT_PATH"))
|
||||||
|
parser.add_argument("--merged-pdf-output", default=os.getenv("MERGED_PDF_OUTPUT"))
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
spec = ContractCreateInput(
|
||||||
|
job_id=args.job_id,
|
||||||
|
supplier_name=args.supplier_name,
|
||||||
|
order_date=args.order_date,
|
||||||
|
source_type=args.source_type,
|
||||||
|
contract_system_no=args.contract_system_no,
|
||||||
|
attempt_no=args.attempt_no,
|
||||||
|
contract_url=args.contract_url,
|
||||||
|
username=args.username,
|
||||||
|
password=args.password,
|
||||||
|
open_browser=args.open_browser,
|
||||||
|
draft_path=args.draft_path,
|
||||||
|
merged_pdf_output=args.merged_pdf_output,
|
||||||
|
)
|
||||||
|
client = StateCenterClient(args.state_center_url, args.token)
|
||||||
|
result = execute_contract_create(spec, client)
|
||||||
|
print(result)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""Contract draft input model for CONTRACT_CREATE."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .contract_flow_steps import ContractGoodsLine
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ContractDraft:
|
||||||
|
supplier_name: str
|
||||||
|
order_date: str
|
||||||
|
source_type: str
|
||||||
|
template_type: str = "FORMAT"
|
||||||
|
template_name: str = ""
|
||||||
|
customer_code: str = ""
|
||||||
|
customer_name: str = ""
|
||||||
|
contract_category: str = ""
|
||||||
|
settlement_type: str = "付款"
|
||||||
|
contract_background: str = ""
|
||||||
|
contract_nature: str = "主合同"
|
||||||
|
amount_currency: str = "人民币"
|
||||||
|
amount: str = ""
|
||||||
|
contract_end_date: str = ""
|
||||||
|
is_major_contract: str = "否"
|
||||||
|
is_prepayment: str = "否"
|
||||||
|
belong_year: str = ""
|
||||||
|
payment_method: str = "账期支付"
|
||||||
|
payment_days: str = "30"
|
||||||
|
contract_status: str = "正式合同"
|
||||||
|
contains_blood_products: str = "否"
|
||||||
|
submit_mode: str = "submit"
|
||||||
|
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
||||||
|
platform_pdf_paths: list[str] = field(default_factory=list)
|
||||||
|
merged_pdf_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_contract_draft(path: str | Path) -> ContractDraft:
|
||||||
|
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
|
return contract_draft_from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
||||||
|
goods_lines = [
|
||||||
|
ContractGoodsLine(
|
||||||
|
operation_code=str(item.get("operationCode", "")),
|
||||||
|
goods_code=str(item.get("goodsCode", "")),
|
||||||
|
goods_name=str(item.get("goodsName", "")),
|
||||||
|
approval_no=str(item.get("approvalNo", "")),
|
||||||
|
spec=str(item.get("spec", "")),
|
||||||
|
unit=str(item.get("unit", "")),
|
||||||
|
quantity=float(item.get("quantity", 0)),
|
||||||
|
)
|
||||||
|
for item in data.get("goodsLines", [])
|
||||||
|
]
|
||||||
|
return ContractDraft(
|
||||||
|
supplier_name=str(data.get("supplierName", "")),
|
||||||
|
order_date=str(data.get("orderDate", "")),
|
||||||
|
source_type=str(data.get("sourceType", "")),
|
||||||
|
template_type=str(data.get("templateType", "FORMAT")),
|
||||||
|
template_name=str(data.get("templateName", "")),
|
||||||
|
customer_code=str(data.get("customerCode", "")),
|
||||||
|
customer_name=str(data.get("customerName", "")),
|
||||||
|
contract_category=str(data.get("contractCategory", "")),
|
||||||
|
settlement_type=str(data.get("settlementType", "付款")),
|
||||||
|
contract_background=str(data.get("contractBackground", "")),
|
||||||
|
contract_nature=str(data.get("contractNature", "主合同")),
|
||||||
|
amount_currency=str(data.get("amountCurrency", "人民币")),
|
||||||
|
amount=str(data.get("amount", "")),
|
||||||
|
contract_end_date=str(data.get("contractEndDate", "")),
|
||||||
|
is_major_contract=str(data.get("isMajorContract", "否")),
|
||||||
|
is_prepayment=str(data.get("isPrepayment", "否")),
|
||||||
|
belong_year=str(data.get("belongYear", "")),
|
||||||
|
payment_method=str(data.get("paymentMethod", "账期支付")),
|
||||||
|
payment_days=str(data.get("paymentDays", "30")),
|
||||||
|
contract_status=str(data.get("contractStatus", "正式合同")),
|
||||||
|
contains_blood_products=str(data.get("containsBloodProducts", "否")),
|
||||||
|
submit_mode=str(data.get("submitMode", "submit")),
|
||||||
|
goods_lines=goods_lines,
|
||||||
|
platform_pdf_paths=[str(path) for path in data.get("platformPdfPaths", [])],
|
||||||
|
merged_pdf_path=data.get("mergedPdfPath"),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
"""Flowchart-sized CONTRACT_CREATE browser steps for YiSaiQi.
|
||||||
|
|
||||||
|
Each function is intentionally small enough to map to one YiSaiQi flowchart
|
||||||
|
node or one script block behind a flowchart node.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class BrowserAdapter(Protocol):
|
||||||
|
def open_url(self, url: str) -> None: ...
|
||||||
|
def fill_first(self, selectors: list[str], value: str) -> None: ...
|
||||||
|
def click_first(self, selectors: list[str]) -> None: ...
|
||||||
|
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None: ...
|
||||||
|
def select_first(self, selectors: list[str], value: str) -> None: ...
|
||||||
|
def upload_file_first(self, selectors: list[str], file_path: str) -> None: ...
|
||||||
|
def read_first(self, selectors: list[str], value_name: str) -> str: ...
|
||||||
|
def screenshot(self, name: str) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ContractGoodsLine:
|
||||||
|
operation_code: str
|
||||||
|
goods_code: str
|
||||||
|
goods_name: str
|
||||||
|
approval_no: str
|
||||||
|
spec: str
|
||||||
|
unit: str
|
||||||
|
quantity: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ContractFlowContext:
|
||||||
|
contract_url: str
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
supplier_name: str
|
||||||
|
order_date: str
|
||||||
|
source_type: str
|
||||||
|
template_type: str = "FORMAT"
|
||||||
|
template_name: str = ""
|
||||||
|
customer_code: str = ""
|
||||||
|
customer_name: str = ""
|
||||||
|
contract_category: str = ""
|
||||||
|
settlement_type: str = "付款"
|
||||||
|
contract_background: str = ""
|
||||||
|
contract_nature: str = "主合同"
|
||||||
|
amount_currency: str = "人民币"
|
||||||
|
amount: str = ""
|
||||||
|
contract_end_date: str = ""
|
||||||
|
is_major_contract: str = "否"
|
||||||
|
is_prepayment: str = "否"
|
||||||
|
belong_year: str = ""
|
||||||
|
payment_method: str = "账期支付"
|
||||||
|
payment_days: str = "30"
|
||||||
|
contract_status: str = "正式合同"
|
||||||
|
contains_blood_products: str = "否"
|
||||||
|
submit_mode: str = "submit"
|
||||||
|
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
||||||
|
merged_pdf_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def run_contract_create_flow(browser: BrowserAdapter, ctx: ContractFlowContext) -> str:
|
||||||
|
open_contract_system(browser, ctx)
|
||||||
|
login_contract_system(browser, ctx)
|
||||||
|
enter_contract_create(browser)
|
||||||
|
select_contract_template(browser, ctx)
|
||||||
|
fill_contract_signatory_info(browser, ctx)
|
||||||
|
fill_contract_basic_info(browser, ctx)
|
||||||
|
fill_contract_amount_and_dates(browser, ctx)
|
||||||
|
fill_contract_goods_lines(browser, ctx)
|
||||||
|
fill_contract_payment_info(browser, ctx)
|
||||||
|
upload_contract_document_if_needed(browser, ctx)
|
||||||
|
if not submit_contract_for_approval(browser, ctx):
|
||||||
|
return ""
|
||||||
|
return read_contract_number(browser)
|
||||||
|
|
||||||
|
|
||||||
|
def open_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
browser.open_url(ctx.contract_url)
|
||||||
|
|
||||||
|
|
||||||
|
def login_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
browser.fill_first(['input[name="j_username"]', 'input[placeholder="用户名"]'], ctx.username)
|
||||||
|
browser.fill_first(['input[name="j_password"]', 'input[placeholder="密码"]'], ctx.password)
|
||||||
|
browser.click_first(['input[name="btn_submit"]', 'a[href*="btn_submit"]', 'text=登录'])
|
||||||
|
browser.wait_for_text("合同", timeout_seconds=20)
|
||||||
|
|
||||||
|
|
||||||
|
def enter_contract_create(browser: BrowserAdapter) -> None:
|
||||||
|
browser.click_first(["text=合同管理", "aria=合同管理"])
|
||||||
|
browser.click_first(["text=合同起草", "aria=合同起草"])
|
||||||
|
browser.wait_for_text("最近使用模板", timeout_seconds=20)
|
||||||
|
|
||||||
|
|
||||||
|
def select_contract_template(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
template_text = ctx.template_name or (
|
||||||
|
"非格式--采购协议" if ctx.template_type == "NON_FORMAT" else "购销协议书(2025)"
|
||||||
|
)
|
||||||
|
browser.click_first([f"text={template_text}", f"text={template_text}...", f"title={template_text}"])
|
||||||
|
browser.wait_for_text("合同起草", timeout_seconds=20)
|
||||||
|
|
||||||
|
|
||||||
|
def fill_contract_signatory_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
customer_name = ctx.customer_name or ctx.supplier_name
|
||||||
|
if ctx.customer_code:
|
||||||
|
browser.fill_first(
|
||||||
|
['label=客户编码', 'input[name="fdCustomerCode"]', 'input[placeholder*="客户编码"]'],
|
||||||
|
ctx.customer_code,
|
||||||
|
)
|
||||||
|
if customer_name:
|
||||||
|
browser.fill_first(
|
||||||
|
['label=客户名称', 'input[name="fdCustomerName"]', 'input[placeholder*="客户名称"]'],
|
||||||
|
customer_name,
|
||||||
|
)
|
||||||
|
browser.screenshot("contract-signatory-info-filled")
|
||||||
|
|
||||||
|
|
||||||
|
def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
browser.select_first(['label=合同版本类型', 'select[name="fdTemplateType"]'], contract_version_label(ctx))
|
||||||
|
if ctx.contract_category:
|
||||||
|
browser.select_first(['label=合同类别', 'select[name="fdContractCategory"]'], ctx.contract_category)
|
||||||
|
browser.select_first(['label=结算类型', 'select[name="fdSettlementType"]'], ctx.settlement_type)
|
||||||
|
if ctx.contract_background:
|
||||||
|
browser.fill_first(
|
||||||
|
['label=合同背景概述', 'textarea[name="fdContractBackground"]'],
|
||||||
|
ctx.contract_background,
|
||||||
|
)
|
||||||
|
browser.click_first([f"label={ctx.contract_nature}", f"text={ctx.contract_nature}"])
|
||||||
|
browser.screenshot("contract-basic-info-filled")
|
||||||
|
|
||||||
|
|
||||||
|
def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
browser.select_first(['label=合同金额', 'select[name="fdCurrency"]'], ctx.amount_currency)
|
||||||
|
if ctx.amount:
|
||||||
|
browser.fill_first(['label=金额(元)', 'input[name="fdAmount"]'], ctx.amount)
|
||||||
|
browser.fill_first(
|
||||||
|
['label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'],
|
||||||
|
ctx.order_date,
|
||||||
|
)
|
||||||
|
browser.fill_first(
|
||||||
|
['label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'],
|
||||||
|
ctx.order_date,
|
||||||
|
)
|
||||||
|
if ctx.contract_end_date:
|
||||||
|
browser.fill_first(
|
||||||
|
['label=合同结束日期', 'input[name="fdEndDate"]', 'input[placeholder*="合同结束"]'],
|
||||||
|
ctx.contract_end_date,
|
||||||
|
)
|
||||||
|
browser.screenshot("contract-amount-and-dates-filled")
|
||||||
|
|
||||||
|
|
||||||
|
def fill_contract_goods_lines(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
for index, line in enumerate(ctx.goods_lines, start=1):
|
||||||
|
if index > 1:
|
||||||
|
browser.click_first(["text=新增明细", "text=添加货品", "text=新增行"])
|
||||||
|
prefix = f"明细{index}"
|
||||||
|
browser.fill_first([f'label={prefix}操作码', 'input[name="operationCode"]'], line.operation_code)
|
||||||
|
browser.fill_first([f'label={prefix}货品编码', 'input[name="goodsCode"]'], line.goods_code)
|
||||||
|
browser.fill_first([f'label={prefix}名称', 'input[name="goodsName"]'], line.goods_name)
|
||||||
|
browser.fill_first([f'label={prefix}国药准字', 'input[name="approvalNo"]'], line.approval_no)
|
||||||
|
browser.fill_first([f'label={prefix}规格', 'input[name="spec"]'], line.spec)
|
||||||
|
browser.fill_first([f'label={prefix}单位', 'input[name="unit"]'], line.unit)
|
||||||
|
browser.fill_first([f'label={prefix}数量', 'input[name="quantity"]'], str(line.quantity))
|
||||||
|
browser.screenshot("contract-goods-lines-filled")
|
||||||
|
|
||||||
|
|
||||||
|
def fill_contract_payment_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
browser.select_first(['label=是否重大合同', 'select[name="fdIsMajorContract"]'], ctx.is_major_contract)
|
||||||
|
browser.select_first(['label=是否预付款', 'select[name="fdIsPrepayment"]'], ctx.is_prepayment)
|
||||||
|
if ctx.belong_year:
|
||||||
|
browser.fill_first(['label=合同归属年份', 'input[name="fdBelongYear"]'], ctx.belong_year)
|
||||||
|
browser.select_first(['label=付款方式', 'select[name="fdPaymentMethod"]', 'select[name="paymentMethod"]'], ctx.payment_method)
|
||||||
|
browser.fill_first(['label=付款期限', 'input[name="fdPaymentDays"]', 'input[name="paymentDays"]'], ctx.payment_days)
|
||||||
|
browser.select_first(['label=合同状态', 'select[name="fdContractStatus"]'], ctx.contract_status)
|
||||||
|
browser.select_first(
|
||||||
|
['label=是否包含血液制品', 'select[name="fdContainsBloodProducts"]'],
|
||||||
|
ctx.contains_blood_products,
|
||||||
|
)
|
||||||
|
browser.screenshot("contract-payment-filled")
|
||||||
|
|
||||||
|
|
||||||
|
def upload_contract_document_if_needed(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||||
|
if ctx.source_type != "YYC":
|
||||||
|
return
|
||||||
|
if not ctx.merged_pdf_path:
|
||||||
|
raise ValueError("YYC contract creation requires merged_pdf_path before upload")
|
||||||
|
browser.upload_file_first(['input[type="file"]', "label=合同证文上传"], ctx.merged_pdf_path)
|
||||||
|
browser.screenshot("contract-pdf-uploaded")
|
||||||
|
|
||||||
|
|
||||||
|
def submit_contract_for_approval(browser: BrowserAdapter, ctx: ContractFlowContext) -> bool:
|
||||||
|
if ctx.submit_mode == "dry_run":
|
||||||
|
browser.screenshot("contract-dry-run-ready")
|
||||||
|
return False
|
||||||
|
browser.click_first(["text=提交", "text=提交送审", "text=送审"])
|
||||||
|
browser.wait_for_text("合同编号", timeout_seconds=30)
|
||||||
|
browser.screenshot("contract-submitted")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def read_contract_number(browser: BrowserAdapter) -> str:
|
||||||
|
return browser.read_first(
|
||||||
|
['input[name="contractNo"]', 'input[name="contractSystemNo"]', "label=合同编号", "text=合同编号"],
|
||||||
|
"contract_number",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def contract_version_label(ctx: ContractFlowContext) -> str:
|
||||||
|
return "非格式合同" if ctx.template_type == "NON_FORMAT" else "格式合同"
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"supplierName": "小药药供应商",
|
||||||
|
"orderDate": "2026-08-11",
|
||||||
|
"sourceType": "XYY",
|
||||||
|
"templateType": "FORMAT",
|
||||||
|
"templateName": "购销协议书(2025)",
|
||||||
|
"customerCode": "CUST-XYY-001",
|
||||||
|
"customerName": "小药药供应商",
|
||||||
|
"contractCategory": "采购协议",
|
||||||
|
"settlementType": "付款",
|
||||||
|
"contractBackground": "按小药药订单自动起草采购协议",
|
||||||
|
"contractNature": "主合同",
|
||||||
|
"amountCurrency": "人民币",
|
||||||
|
"amount": "9600.00",
|
||||||
|
"contractEndDate": "2026-12-31",
|
||||||
|
"isMajorContract": "否",
|
||||||
|
"isPrepayment": "否",
|
||||||
|
"belongYear": "2026",
|
||||||
|
"paymentMethod": "账期支付",
|
||||||
|
"paymentDays": "30",
|
||||||
|
"contractStatus": "正式合同",
|
||||||
|
"containsBloodProducts": "否",
|
||||||
|
"platformPdfPaths": [],
|
||||||
|
"goodsLines": [
|
||||||
|
{
|
||||||
|
"operationCode": "ADD",
|
||||||
|
"goodsCode": "YP-002",
|
||||||
|
"goodsName": "哈药牌钙铁锌口服液",
|
||||||
|
"approvalNo": "国药准字B00000002",
|
||||||
|
"spec": "10ml*12支",
|
||||||
|
"unit": "盒",
|
||||||
|
"quantity": 24
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"supplierName": "重庆亿昊药业",
|
||||||
|
"orderDate": "2026-08-11",
|
||||||
|
"sourceType": "YYC",
|
||||||
|
"templateType": "NON_FORMAT",
|
||||||
|
"templateName": "非格式--采购协议",
|
||||||
|
"customerCode": "CUST-YYC-001",
|
||||||
|
"customerName": "重庆亿昊药业",
|
||||||
|
"contractCategory": "采购协议",
|
||||||
|
"settlementType": "付款",
|
||||||
|
"contractBackground": "按医药城订单自动起草采购协议",
|
||||||
|
"contractNature": "主合同",
|
||||||
|
"amountCurrency": "人民币",
|
||||||
|
"amount": "12000.50",
|
||||||
|
"contractEndDate": "2026-12-31",
|
||||||
|
"isMajorContract": "否",
|
||||||
|
"isPrepayment": "否",
|
||||||
|
"belongYear": "2026",
|
||||||
|
"paymentMethod": "账期支付",
|
||||||
|
"paymentDays": "30",
|
||||||
|
"contractStatus": "正式合同",
|
||||||
|
"containsBloodProducts": "否",
|
||||||
|
"platformPdfPaths": [
|
||||||
|
"C:/rpa/jobs/JOB-1/platform-contract-1.pdf",
|
||||||
|
"C:/rpa/jobs/JOB-1/platform-contract-2.pdf"
|
||||||
|
],
|
||||||
|
"goodsLines": [
|
||||||
|
{
|
||||||
|
"operationCode": "ADD",
|
||||||
|
"goodsCode": "YP-001",
|
||||||
|
"goodsName": "阿卡波糖片",
|
||||||
|
"approvalNo": "国药准字H00000001",
|
||||||
|
"spec": "50mg*30片",
|
||||||
|
"unit": "盒",
|
||||||
|
"quantity": 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
{
|
||||||
|
"flowName": "CONTRACT_CREATE_合同建单",
|
||||||
|
"platform": "YiSaiQi iS-RPA",
|
||||||
|
"nodeCode": "CONTRACT_CREATE",
|
||||||
|
"inputs": [
|
||||||
|
"stateCenterUrl",
|
||||||
|
"agentToken",
|
||||||
|
"jobId",
|
||||||
|
"supplierName",
|
||||||
|
"orderDate",
|
||||||
|
"sourceType",
|
||||||
|
"templateType",
|
||||||
|
"templateName",
|
||||||
|
"customerCode",
|
||||||
|
"customerName",
|
||||||
|
"contractCategory",
|
||||||
|
"settlementType",
|
||||||
|
"contractBackground",
|
||||||
|
"amount",
|
||||||
|
"contractEndDate",
|
||||||
|
"paymentMethod",
|
||||||
|
"paymentDays",
|
||||||
|
"submitMode",
|
||||||
|
"goodsLines",
|
||||||
|
"mergedPdfPath"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"contractSystemNo",
|
||||||
|
"nextNode"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "S01_CALLBACK_START",
|
||||||
|
"name": "回调状态中心-start",
|
||||||
|
"script": "StateCenterClient.start_node(jobId, 'CONTRACT_CREATE')"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S02_OPEN_CONTRACT_SYSTEM",
|
||||||
|
"name": "打开合同系统",
|
||||||
|
"script": "open_contract_system(browser, ctx)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S03_LOGIN",
|
||||||
|
"name": "登录合同系统",
|
||||||
|
"script": "login_contract_system(browser, ctx)",
|
||||||
|
"selectors": {
|
||||||
|
"username": "input[name='j_username']",
|
||||||
|
"password": "input[name='j_password']",
|
||||||
|
"submit": "input[name='btn_submit']"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S04_ENTER_CONTRACT_DRAFT",
|
||||||
|
"name": "进入合同起草",
|
||||||
|
"script": "enter_contract_create(browser)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S05_SELECT_RECENT_TEMPLATE",
|
||||||
|
"name": "选择最近使用模板",
|
||||||
|
"script": "select_contract_template(browser, ctx)",
|
||||||
|
"rule": "优先点击最近使用模板中的 templateName;非格式默认非格式--采购协议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S06_FILL_SIGNATORY_INFO",
|
||||||
|
"name": "填写签约方信息",
|
||||||
|
"script": "fill_contract_signatory_info(browser, ctx)",
|
||||||
|
"fields": [
|
||||||
|
"客户编码",
|
||||||
|
"客户名称"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S07_FILL_CONTRACT_BASIC_FORM",
|
||||||
|
"name": "填写合同基础表单",
|
||||||
|
"script": "fill_contract_basic_info(browser, ctx)",
|
||||||
|
"fields": [
|
||||||
|
"合同版本类型",
|
||||||
|
"合同类别",
|
||||||
|
"结算类型",
|
||||||
|
"合同背景概述",
|
||||||
|
"合同性质"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S08_FILL_AMOUNT_AND_DATES",
|
||||||
|
"name": "填写金额与日期",
|
||||||
|
"script": "fill_contract_amount_and_dates(browser, ctx)",
|
||||||
|
"hardRules": [
|
||||||
|
"合同签订日期=orderDate",
|
||||||
|
"合同开始日期=orderDate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S09_FILL_GOODS_LINES",
|
||||||
|
"name": "填写货品明细",
|
||||||
|
"script": "fill_contract_goods_lines(browser, ctx)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S10_FILL_PAYMENT_AND_FLAGS",
|
||||||
|
"name": "填写付款信息与特殊标志",
|
||||||
|
"script": "fill_contract_payment_info(browser, ctx)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S11_MERGE_PDF_IF_YYC",
|
||||||
|
"name": "医药城合并同供应商同日PDF",
|
||||||
|
"script": "ensure_merged_pdf(platformPdfPaths, mergedPdfOutput)",
|
||||||
|
"branch": "sourceType=YYC 且 platformPdfPaths 多于一份时执行;单份直接沿用;sourceType=XYY 跳过"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S12_UPLOAD_PDF_IF_YYC",
|
||||||
|
"name": "医药城上传合并合同PDF",
|
||||||
|
"script": "upload_contract_document_if_needed(browser, ctx)",
|
||||||
|
"branch": "sourceType=YYC 才执行;sourceType=XYY 跳过"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S13_SUBMIT",
|
||||||
|
"name": "提交",
|
||||||
|
"script": "submit_contract_for_approval(browser)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S14_READ_CONTRACT_NO",
|
||||||
|
"name": "读取合同编号",
|
||||||
|
"script": "read_contract_number(browser)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S15_CALLBACK_COMPLETE",
|
||||||
|
"name": "回调状态中心-complete",
|
||||||
|
"script": "StateCenterClient.complete_node(jobId, 'CONTRACT_CREATE', businessRefs.contractSystemNo)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "E_LOGIN_FAILED",
|
||||||
|
"name": "异常-登录失败",
|
||||||
|
"script": "StateCenterClient.fail_node(..., errorCode='SYS_LOGIN_FAIL', retryable=true, routeRole='OPS')"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "E_CONTRACT_DISABLED",
|
||||||
|
"name": "异常-供应商合同禁用",
|
||||||
|
"script": "StateCenterClient.fail_node(..., errorCode='BIZ_CONTRACT_DISABLED', retryable=false, routeRole='PURCHASER')"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "E_PDF_MERGE_UNSUPPORTED",
|
||||||
|
"name": "异常-PDF合并能力不支持",
|
||||||
|
"script": "StateCenterClient.fail_node(..., errorCode='CAP_PDF_MERGE_UNSUPPORTED', retryable=false, routeRole='OPS')"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "E_PAGE_CHANGED",
|
||||||
|
"name": "异常-页面改版",
|
||||||
|
"script": "StateCenterClient.fail_node(..., errorCode='UI_ELEMENT_NOT_FOUND', retryable=false, routeRole='OPS')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"flowName": "CONTRACT_CREATE_预览",
|
||||||
|
"platform": "YiSaiQi iS-RPA",
|
||||||
|
"nodeCode": "CONTRACT_CREATE",
|
||||||
|
"inputs": [
|
||||||
|
"stateCenterUrl",
|
||||||
|
"agentToken",
|
||||||
|
"jobId",
|
||||||
|
"supplierName",
|
||||||
|
"orderDate",
|
||||||
|
"sourceType",
|
||||||
|
"templateType",
|
||||||
|
"templateName",
|
||||||
|
"customerCode",
|
||||||
|
"customerName",
|
||||||
|
"contractCategory",
|
||||||
|
"settlementType",
|
||||||
|
"contractBackground",
|
||||||
|
"amount",
|
||||||
|
"contractEndDate",
|
||||||
|
"paymentMethod",
|
||||||
|
"paymentDays",
|
||||||
|
"submitMode",
|
||||||
|
"goodsLines",
|
||||||
|
"mergedPdfPath"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"previewDone"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{ "id": "P01", "name": "打开合同系统", "script": "open_contract_system(browser, ctx)" },
|
||||||
|
{ "id": "P02", "name": "登录合同系统", "script": "login_contract_system(browser, ctx)" },
|
||||||
|
{ "id": "P03", "name": "进入合同起草", "script": "enter_contract_create(browser)" },
|
||||||
|
{ "id": "P04", "name": "选择模板", "script": "select_contract_template(browser, ctx)" },
|
||||||
|
{ "id": "P05", "name": "填写签约方", "script": "fill_contract_signatory_info(browser, ctx)" },
|
||||||
|
{ "id": "P06", "name": "填写基础表单", "script": "fill_contract_basic_info(browser, ctx)" },
|
||||||
|
{ "id": "P07", "name": "填写金额日期", "script": "fill_contract_amount_and_dates(browser, ctx)" },
|
||||||
|
{ "id": "P08", "name": "填写明细", "script": "fill_contract_goods_lines(browser, ctx)" },
|
||||||
|
{ "id": "P09", "name": "填写付款与标志", "script": "fill_contract_payment_info(browser, ctx)" },
|
||||||
|
{ "id": "P10", "name": "预览结束", "script": "submit_contract_for_approval(browser, ctx) with submitMode=dry_run" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""PDF utilities for YiSaiQi contract drafting."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_merged_pdf(source_paths: list[str], output_path: str) -> str:
|
||||||
|
"""Return one PDF path, merging multiple source PDFs when needed.
|
||||||
|
|
||||||
|
YiSaiQi must upload only one YYC contract document for the same supplier
|
||||||
|
and same order date. A single source PDF can be uploaded directly.
|
||||||
|
"""
|
||||||
|
if not source_paths:
|
||||||
|
raise ValueError("YYC contract creation requires at least one platform PDF")
|
||||||
|
if len(source_paths) == 1:
|
||||||
|
return source_paths[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pypdf import PdfWriter
|
||||||
|
except ImportError as exc:
|
||||||
|
raise RuntimeError("Install pypdf to merge multiple YYC platform PDFs") from exc
|
||||||
|
|
||||||
|
writer = PdfWriter()
|
||||||
|
for source in source_paths:
|
||||||
|
writer.append(str(Path(source)))
|
||||||
|
|
||||||
|
out = Path(output_path)
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with out.open("wb") as file:
|
||||||
|
writer.write(file)
|
||||||
|
return str(out)
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
pypdf==5.1.0
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
{
|
||||||
|
"knownLoginSelectors": {
|
||||||
|
"username": "input[name='j_username']",
|
||||||
|
"password": "input[name='j_password']",
|
||||||
|
"submit": "input[name='btn_submit']"
|
||||||
|
},
|
||||||
|
"candidateSelectors": {
|
||||||
|
"contractManagementMenu": [
|
||||||
|
"text=合同管理",
|
||||||
|
"aria=合同管理"
|
||||||
|
],
|
||||||
|
"contractDraftMenu": [
|
||||||
|
"text=合同起草",
|
||||||
|
"aria=合同起草"
|
||||||
|
],
|
||||||
|
"recentNonFormatPurchaseTemplate": [
|
||||||
|
"text=非格式--采购协议",
|
||||||
|
"text=非格式--采购协议...",
|
||||||
|
"title=非格式--采购协议"
|
||||||
|
],
|
||||||
|
"customerCode": [
|
||||||
|
"label=客户编码",
|
||||||
|
"input[name='fdCustomerCode']",
|
||||||
|
"input[placeholder*='客户编码']"
|
||||||
|
],
|
||||||
|
"customerName": [
|
||||||
|
"label=客户名称",
|
||||||
|
"input[name='fdCustomerName']",
|
||||||
|
"input[placeholder*='客户名称']"
|
||||||
|
],
|
||||||
|
"contractVersionType": [
|
||||||
|
"label=合同版本类型",
|
||||||
|
"select[name='fdTemplateType']"
|
||||||
|
],
|
||||||
|
"contractCategory": [
|
||||||
|
"label=合同类别",
|
||||||
|
"select[name='fdContractCategory']"
|
||||||
|
],
|
||||||
|
"settlementType": [
|
||||||
|
"label=结算类型",
|
||||||
|
"select[name='fdSettlementType']"
|
||||||
|
],
|
||||||
|
"contractBackground": [
|
||||||
|
"label=合同背景概述",
|
||||||
|
"textarea[name='fdContractBackground']"
|
||||||
|
],
|
||||||
|
"contractNatureMain": [
|
||||||
|
"label=主合同",
|
||||||
|
"text=主合同"
|
||||||
|
],
|
||||||
|
"amountCurrency": [
|
||||||
|
"label=合同金额",
|
||||||
|
"select[name='fdCurrency']"
|
||||||
|
],
|
||||||
|
"amount": [
|
||||||
|
"label=金额(元)",
|
||||||
|
"input[name='fdAmount']"
|
||||||
|
],
|
||||||
|
"signDate": [
|
||||||
|
"label=合同签订日期",
|
||||||
|
"input[name='fdSignDate']",
|
||||||
|
"input[placeholder*='合同签订']"
|
||||||
|
],
|
||||||
|
"beginDate": [
|
||||||
|
"label=合同开始日期",
|
||||||
|
"input[name='fdStartDate']",
|
||||||
|
"input[placeholder*='合同开始']"
|
||||||
|
],
|
||||||
|
"endDate": [
|
||||||
|
"label=合同结束日期",
|
||||||
|
"input[name='fdEndDate']",
|
||||||
|
"input[placeholder*='合同结束']"
|
||||||
|
],
|
||||||
|
"isMajorContract": [
|
||||||
|
"label=是否重大合同",
|
||||||
|
"select[name='fdIsMajorContract']"
|
||||||
|
],
|
||||||
|
"isPrepayment": [
|
||||||
|
"label=是否预付款",
|
||||||
|
"select[name='fdIsPrepayment']"
|
||||||
|
],
|
||||||
|
"belongYear": [
|
||||||
|
"label=合同归属年份",
|
||||||
|
"input[name='fdBelongYear']"
|
||||||
|
],
|
||||||
|
"paymentMethod": [
|
||||||
|
"label=付款方式",
|
||||||
|
"select[name='fdPaymentMethod']",
|
||||||
|
"select[name='paymentMethod']"
|
||||||
|
],
|
||||||
|
"paymentDays": [
|
||||||
|
"label=付款期限",
|
||||||
|
"input[name='fdPaymentDays']",
|
||||||
|
"input[name='paymentDays']"
|
||||||
|
],
|
||||||
|
"contractStatus": [
|
||||||
|
"label=合同状态",
|
||||||
|
"select[name='fdContractStatus']"
|
||||||
|
],
|
||||||
|
"containsBloodProducts": [
|
||||||
|
"label=是否包含血液制品",
|
||||||
|
"select[name='fdContainsBloodProducts']"
|
||||||
|
],
|
||||||
|
"submit": [
|
||||||
|
"text=提交",
|
||||||
|
"text=提交送审",
|
||||||
|
"text=送审"
|
||||||
|
],
|
||||||
|
"contractNumber": [
|
||||||
|
"input[name='contractNo']",
|
||||||
|
"input[name='contractSystemNo']",
|
||||||
|
"label=合同编号",
|
||||||
|
"text=合同编号"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,425 @@
|
||||||
|
"""Verification script for CONTRACT_CREATE flow logic.
|
||||||
|
|
||||||
|
Runs all core code paths WITHOUT browser automation or state center HTTP calls.
|
||||||
|
Uses mock objects to verify contract_flow_steps logic is correct.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure the parent codes directory is on sys.path so rpa_yisaiqi imports work
|
||||||
|
_CODES = Path(__file__).resolve().parent.parent
|
||||||
|
if str(_CODES) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_CODES))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 1: Verify all imports resolve correctly
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("Phase 1: 验证模块导入")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from rpa_yisaiqi.callback_client import StateCenterClient, StateCenterError
|
||||||
|
print(" [OK] callback_client -> StateCenterClient")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] callback_client: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from rpa_yisaiqi.contract_flow_steps import (
|
||||||
|
BrowserAdapter, ContractFlowContext, ContractGoodsLine,
|
||||||
|
contract_version_label, run_contract_create_flow,
|
||||||
|
open_contract_system, login_contract_system, enter_contract_create,
|
||||||
|
select_contract_template, fill_contract_signatory_info,
|
||||||
|
fill_contract_basic_info, fill_contract_amount_and_dates,
|
||||||
|
fill_contract_goods_lines, fill_contract_payment_info,
|
||||||
|
upload_contract_document_if_needed, submit_contract_for_approval,
|
||||||
|
read_contract_number,
|
||||||
|
)
|
||||||
|
print(" [OK] contract_flow_steps -> all functions")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] contract_flow_steps: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from rpa_yisaiqi.contract_draft import ContractDraft, load_contract_draft, contract_draft_from_dict
|
||||||
|
print(" [OK] contract_draft -> ContractDraft, load_contract_draft")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] contract_draft: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from rpa_yisaiqi.pdf_utils import ensure_merged_pdf
|
||||||
|
print(" [OK] pdf_utils -> ensure_merged_pdf")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] pdf_utils: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from rpa_yisaiqi.contract_create_node import (
|
||||||
|
ContractCreateInput, ContractCreateError,
|
||||||
|
LoginFailed, SupplierContractDisabled, PdfMergeUnsupported,
|
||||||
|
execute_contract_create, create_contract_in_contract_system,
|
||||||
|
build_complete_payload, build_fail_payload, build_flow_context,
|
||||||
|
NODE_CODE, CONTRACT_URL, parse_args,
|
||||||
|
)
|
||||||
|
print(" [OK] contract_create_node -> all functions + error classes")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] contract_create_node: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(" -> 6/6 模块导入成功\n")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 2: Verify contract draft loading from JSON examples
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("Phase 2: 验证合同起草 JSON 数据加载")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
examples_dir = Path(__file__).resolve().parent / "examples"
|
||||||
|
|
||||||
|
# Test YYC draft
|
||||||
|
yyc_path = examples_dir / "contract_draft_yyc.json"
|
||||||
|
if yyc_path.exists():
|
||||||
|
try:
|
||||||
|
draft = load_contract_draft(str(yyc_path))
|
||||||
|
assert draft.supplier_name == "重庆亿昊药业", f"supplier_name mismatch: {draft.supplier_name}"
|
||||||
|
assert draft.source_type == "YYC", f"source_type mismatch: {draft.source_type}"
|
||||||
|
assert draft.template_type == "NON_FORMAT", f"template_type mismatch: {draft.template_type}"
|
||||||
|
assert draft.settlement_type == "付款"
|
||||||
|
assert draft.contract_nature == "主合同"
|
||||||
|
assert len(draft.goods_lines) == 1, f"goods_lines count mismatch: {len(draft.goods_lines)}"
|
||||||
|
assert draft.goods_lines[0].goods_name == "阿卡波糖片"
|
||||||
|
assert len(draft.platform_pdf_paths) == 2
|
||||||
|
print(f" [OK] contract_draft_yyc.json -> supplier={draft.supplier_name}, "
|
||||||
|
f"goods={len(draft.goods_lines)}, pdfs={len(draft.platform_pdf_paths)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] contract_draft_yyc.json: {e}")
|
||||||
|
import traceback; traceback.print_exc()
|
||||||
|
else:
|
||||||
|
print(f" [SKIP] contract_draft_yyc.json not found")
|
||||||
|
|
||||||
|
# Test XYY draft
|
||||||
|
xyy_path = examples_dir / "contract_draft_xyy.json"
|
||||||
|
if xyy_path.exists():
|
||||||
|
try:
|
||||||
|
draft = load_contract_draft(str(xyy_path))
|
||||||
|
print(f" [OK] contract_draft_xyy.json -> supplier={draft.supplier_name}, "
|
||||||
|
f"source_type={draft.source_type}, goods={len(draft.goods_lines)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [FAIL] contract_draft_xyy.json: {e}")
|
||||||
|
else:
|
||||||
|
print(f" [SKIP] contract_draft_xyy.json not found (may be fine)")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 3: Mock Browser and verify flow steps
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("Phase 3: 用 Mock Browser 验证流程步骤逻辑")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
class MockBrowser:
|
||||||
|
"""Records all interactions for verification."""
|
||||||
|
def __init__(self):
|
||||||
|
self.log: list[str] = []
|
||||||
|
self.values: dict[str, str] = {}
|
||||||
|
|
||||||
|
def open_url(self, url: str) -> None:
|
||||||
|
self.log.append(f"open_url({url})")
|
||||||
|
|
||||||
|
def fill_first(self, selectors: list[str], value: str) -> None:
|
||||||
|
self.log.append(f"fill_first({selectors[0] if selectors else '?'}..., {value})")
|
||||||
|
|
||||||
|
def click_first(self, selectors: list[str]) -> None:
|
||||||
|
self.log.append(f"click_first({selectors[0] if selectors else '?'}...)")
|
||||||
|
|
||||||
|
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
||||||
|
self.log.append(f"wait_for_text({text}, timeout={timeout_seconds})")
|
||||||
|
|
||||||
|
def select_first(self, selectors: list[str], value: str) -> None:
|
||||||
|
self.log.append(f"select_first({selectors[0] if selectors else '?'}..., {value})")
|
||||||
|
|
||||||
|
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
|
||||||
|
self.log.append(f"upload_file_first({selectors[0] if selectors else '?'}..., {file_path})")
|
||||||
|
|
||||||
|
def read_first(self, selectors: list[str], value_name: str) -> str:
|
||||||
|
val = self.values.get(value_name, f"HT-20260812-{len(self.log):03d}")
|
||||||
|
self.log.append(f"read_first({selectors[0] if selectors else '?'}..., {value_name}) -> {val}")
|
||||||
|
return val
|
||||||
|
|
||||||
|
def screenshot(self, name: str) -> str:
|
||||||
|
path = f"screenshots/{name}.png"
|
||||||
|
self.log.append(f"screenshot({name}) -> {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# Build flow context for YYC
|
||||||
|
ctx = ContractFlowContext(
|
||||||
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
|
username="test002",
|
||||||
|
password="1",
|
||||||
|
supplier_name="测试供应商",
|
||||||
|
order_date="2026-08-12",
|
||||||
|
source_type="YYC",
|
||||||
|
template_type="NON_FORMAT",
|
||||||
|
template_name="非格式--采购协议",
|
||||||
|
customer_code="CUST-001",
|
||||||
|
customer_name="测试供应商",
|
||||||
|
contract_category="采购协议",
|
||||||
|
settlement_type="付款",
|
||||||
|
contract_background="自动化测试合同起草",
|
||||||
|
contract_nature="主合同",
|
||||||
|
amount_currency="人民币",
|
||||||
|
amount="50000.00",
|
||||||
|
contract_end_date="2026-12-31",
|
||||||
|
is_major_contract="否",
|
||||||
|
is_prepayment="否",
|
||||||
|
belong_year="2026",
|
||||||
|
payment_method="账期支付",
|
||||||
|
payment_days="30",
|
||||||
|
contract_status="正式合同",
|
||||||
|
contains_blood_products="否",
|
||||||
|
submit_mode="dry_run", # dry-run mode
|
||||||
|
goods_lines=[
|
||||||
|
ContractGoodsLine(
|
||||||
|
operation_code="ADD",
|
||||||
|
goods_code="YP-001",
|
||||||
|
goods_name="测试药品A",
|
||||||
|
approval_no="国药准字H00000001",
|
||||||
|
spec="50mg*30片",
|
||||||
|
unit="盒",
|
||||||
|
quantity=10.0,
|
||||||
|
),
|
||||||
|
ContractGoodsLine(
|
||||||
|
operation_code="ADD",
|
||||||
|
goods_code="YP-002",
|
||||||
|
goods_name="测试药品B",
|
||||||
|
approval_no="国药准字H00000002",
|
||||||
|
spec="100mg*20片",
|
||||||
|
unit="瓶",
|
||||||
|
quantity=5.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
merged_pdf_path="C:/rpa/jobs/test/merged.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the flow
|
||||||
|
browser = MockBrowser()
|
||||||
|
contract_no = run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
|
# Verify step sequence
|
||||||
|
expected_steps = [
|
||||||
|
"open_url",
|
||||||
|
"fill_first", # username
|
||||||
|
"fill_first", # password
|
||||||
|
"click_first", # login
|
||||||
|
"wait_for_text", # 合同
|
||||||
|
"click_first", # 合同管理
|
||||||
|
"click_first", # 合同起草
|
||||||
|
"wait_for_text", # 最近使用模板
|
||||||
|
"click_first", # 模板
|
||||||
|
"wait_for_text", # 合同起草
|
||||||
|
]
|
||||||
|
|
||||||
|
step_names = [s.split("(")[0] for s in browser.log]
|
||||||
|
|
||||||
|
assert contract_no == "", f"dry_run should return empty string, got: {contract_no}"
|
||||||
|
print(f" [OK] dry-run mode: contract_no is empty string (not submitted)")
|
||||||
|
|
||||||
|
# Verify key steps were executed
|
||||||
|
assert "open_url" in step_names, "open_url not called"
|
||||||
|
print(f" [OK] open_url called with: {browser.log[0]}")
|
||||||
|
|
||||||
|
assert len([s for s in step_names if s == "fill_first"]) >= 2, "fill_first not called enough times"
|
||||||
|
print(f" [OK] fill_first called for username/password and form fields")
|
||||||
|
|
||||||
|
assert "click_first" in step_names, "click_first not called"
|
||||||
|
print(f" [OK] click_first operations executed")
|
||||||
|
|
||||||
|
assert "screenshot" in step_names, "screenshot not called"
|
||||||
|
print(f" [OK] screenshots captured at each stage")
|
||||||
|
|
||||||
|
# Verify dry-run submitted correctly (returns False)
|
||||||
|
assert len([s for s in browser.log if "dry-run" in s]) == 1
|
||||||
|
print(f" [OK] dry-run screenshot taken, submit skipped")
|
||||||
|
|
||||||
|
print(f"\n Total browser actions: {len(browser.log)}")
|
||||||
|
for i, action in enumerate(browser.log):
|
||||||
|
print(f" [{i+1}] {action}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 4: Verify XYY flow (skip PDF upload)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("Phase 4: 验证 XYY 流程 (跳过 PDF 上传)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
ctx_xyy = ContractFlowContext(
|
||||||
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
|
username="test002",
|
||||||
|
password="1",
|
||||||
|
supplier_name="小药药供应商",
|
||||||
|
order_date="2026-08-12",
|
||||||
|
source_type="XYY",
|
||||||
|
template_type="FORMAT",
|
||||||
|
template_name="购销协议书(2025)",
|
||||||
|
customer_code="CUST-XYY-001",
|
||||||
|
customer_name="小药药供应商",
|
||||||
|
contract_category="购销协议",
|
||||||
|
settlement_type="付款",
|
||||||
|
contract_background="XYY格式合同测试",
|
||||||
|
contract_nature="主合同",
|
||||||
|
amount_currency="人民币",
|
||||||
|
amount="30000.00",
|
||||||
|
contract_end_date="2026-12-31",
|
||||||
|
is_major_contract="否",
|
||||||
|
is_prepayment="否",
|
||||||
|
belong_year="2026",
|
||||||
|
payment_method="账期支付",
|
||||||
|
payment_days="30",
|
||||||
|
contract_status="正式合同",
|
||||||
|
contains_blood_products="否",
|
||||||
|
submit_mode="submit",
|
||||||
|
goods_lines=[],
|
||||||
|
merged_pdf_path=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
browser_xyy = MockBrowser()
|
||||||
|
browser_xyy.values["contract_number"] = "HT-20260812-XYY-001"
|
||||||
|
|
||||||
|
contract_no_xyy = run_contract_create_flow(browser_xyy, ctx_xyy)
|
||||||
|
|
||||||
|
# Verify XYY specific behavior
|
||||||
|
upload_calls = [s for s in browser_xyy.log if "upload" in s.lower()]
|
||||||
|
assert len(upload_calls) == 0, f"XYY should NOT upload PDF, but got: {upload_calls}"
|
||||||
|
print(f" [OK] XYY correctly skips PDF upload (0 upload calls)")
|
||||||
|
|
||||||
|
assert contract_no_xyy == "HT-20260812-XYY-001"
|
||||||
|
print(f" [OK] XYY contract number read: {contract_no_xyy}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 5: Verify error classes and payload builders
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("Phase 5: 验证异常类和回调 Payload 构建")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
spec = ContractCreateInput(
|
||||||
|
job_id="TEST-JOB-001",
|
||||||
|
supplier_name="测试供应商",
|
||||||
|
order_date="2026-08-12",
|
||||||
|
source_type="YYC",
|
||||||
|
contract_system_no=None,
|
||||||
|
attempt_no=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test build_complete_payload
|
||||||
|
complete_payload = build_complete_payload(spec, "HT-20260812-001")
|
||||||
|
assert complete_payload["businessRefs"]["contractSystemNo"] == "HT-20260812-001"
|
||||||
|
assert "attemptNo" in complete_payload
|
||||||
|
assert "idempotencyKey" in complete_payload
|
||||||
|
print(f" [OK] build_complete_payload -> {complete_payload['businessRefs']}")
|
||||||
|
|
||||||
|
# Test error classes
|
||||||
|
for exc_class, code, retryable, role in [
|
||||||
|
(LoginFailed, "SYS_LOGIN_FAIL", True, "OPS"),
|
||||||
|
(SupplierContractDisabled, "BIZ_CONTRACT_DISABLED", False, "PURCHASER"),
|
||||||
|
(PdfMergeUnsupported, "CAP_PDF_MERGE_UNSUPPORTED", False, "OPS"),
|
||||||
|
]:
|
||||||
|
exc = exc_class("测试异常")
|
||||||
|
assert exc.error_code == code, f"{exc_class.__name__}: expected {code}, got {exc.error_code}"
|
||||||
|
assert exc.retryable == retryable
|
||||||
|
assert exc.route_role == role
|
||||||
|
print(f" [OK] {exc_class.__name__}: code={exc.error_code}, retryable={exc.retryable}, role={exc.route_role}")
|
||||||
|
|
||||||
|
# Test build_fail_payload
|
||||||
|
fail_payload = build_fail_payload(spec, LoginFailed("登录凭证无效"))
|
||||||
|
assert fail_payload["errorCode"] == "SYS_LOGIN_FAIL"
|
||||||
|
assert fail_payload["retryable"] is True
|
||||||
|
assert fail_payload["routeRole"] == "OPS"
|
||||||
|
print(f" [OK] build_fail_payload -> errorCode={fail_payload['errorCode']}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 6: Verify contract_version_label
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("Phase 6: 验证工具函数")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
from rpa_yisaiqi.contract_flow_steps import contract_version_label
|
||||||
|
|
||||||
|
ctx_format = ContractFlowContext(
|
||||||
|
contract_url="", username="", password="", supplier_name="", order_date="",
|
||||||
|
source_type="", template_type="FORMAT",
|
||||||
|
)
|
||||||
|
assert contract_version_label(ctx_format) == "格式合同"
|
||||||
|
print(f" [OK] contract_version_label(FORMAT) -> '格式合同'")
|
||||||
|
|
||||||
|
ctx_nonformat = ContractFlowContext(
|
||||||
|
contract_url="", username="", password="", supplier_name="", order_date="",
|
||||||
|
source_type="", template_type="NON_FORMAT",
|
||||||
|
)
|
||||||
|
assert contract_version_label(ctx_nonformat) == "非格式合同"
|
||||||
|
print(f" [OK] contract_version_label(NON_FORMAT) -> '非格式合同'")
|
||||||
|
|
||||||
|
# Verify template default name logic
|
||||||
|
# (tested inside select_contract_template via the mock - NON_FORMAT without name uses '非格式--采购协议')
|
||||||
|
ctx_no_name = ContractFlowContext(
|
||||||
|
contract_url="", username="", password="", supplier_name="", order_date="",
|
||||||
|
source_type="", template_type="NON_FORMAT", template_name="",
|
||||||
|
)
|
||||||
|
browser_tpl = MockBrowser()
|
||||||
|
select_contract_template(browser_tpl, ctx_no_name)
|
||||||
|
assert "非格式--采购协议" in browser_tpl.log[0]
|
||||||
|
print(f" [OK] select_contract_template without name defaults to '非格式--采购协议'")
|
||||||
|
|
||||||
|
ctx_formatted = ContractFlowContext(
|
||||||
|
contract_url="", username="", password="", supplier_name="", order_date="",
|
||||||
|
source_type="", template_type="FORMAT", template_name="",
|
||||||
|
)
|
||||||
|
browser_tpl2 = MockBrowser()
|
||||||
|
select_contract_template(browser_tpl2, ctx_formatted)
|
||||||
|
assert "购销协议书(2025)" in browser_tpl2.log[0]
|
||||||
|
print(f" [OK] select_contract_template FORMAT defaults to '购销协议书(2025)'")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Final Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
print("=" * 60)
|
||||||
|
print("验证完成!")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"""
|
||||||
|
核心模块状态:
|
||||||
|
- callback_client.py : 导入正常 (StateCenterClient)
|
||||||
|
- contract_flow_steps.py : 所有15个步骤函数可调用
|
||||||
|
- contract_draft.py : JSON数据加载正常
|
||||||
|
- pdf_utils.py : 导入正常
|
||||||
|
- contract_create_node.py: 异常类定义正常, payload构建正确
|
||||||
|
|
||||||
|
流程逻辑验证:
|
||||||
|
- YYC dry-run 流程 : 所有步骤按序执行 (不含提交)
|
||||||
|
- XYY 流程 : PDF上传步骤被正确跳过
|
||||||
|
- 异常映射 : 4种异常类 = 4种错误码
|
||||||
|
- 版本类型 : 格式/非格式合同标签正确
|
||||||
|
|
||||||
|
下一步: 在 APA Studio 中:
|
||||||
|
1. 打开项目 D:/RPA/AutoRpa
|
||||||
|
2. 在画布上拖拽 'Python脚本块' 节点
|
||||||
|
3. 粘贴 GlobalFun.py 中的 run_contract_create_flow 调用
|
||||||
|
""")
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""YiSaiQi adapter template for CONTRACT_CREATE flowchart nodes.
|
||||||
|
|
||||||
|
This file is a mapping template. In the YiSaiQi designer, replace each method
|
||||||
|
body with the platform's native browser activities: open webpage, input text,
|
||||||
|
click element, select dropdown, upload file, read text, and screenshot.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class YiSaiQiBrowserAdapter:
|
||||||
|
def open_url(self, url: str) -> None:
|
||||||
|
"""Flowchart node: 浏览器打开网页."""
|
||||||
|
raise NotImplementedError("Map to YiSaiQi browser open-url activity")
|
||||||
|
|
||||||
|
def fill_first(self, selectors: list[str], value: str) -> None:
|
||||||
|
"""Flowchart node: 按候选定位器依次查找,找到后输入文本."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi input-text activity: {selectors=} {value=}")
|
||||||
|
|
||||||
|
def click_first(self, selectors: list[str]) -> None:
|
||||||
|
"""Flowchart node: 按候选定位器依次查找,找到后点击."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi click activity: {selectors=}")
|
||||||
|
|
||||||
|
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
||||||
|
"""Flowchart node: 等待页面出现指定文本."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi wait-text activity: {text=} {timeout_seconds=}")
|
||||||
|
|
||||||
|
def select_first(self, selectors: list[str], value: str) -> None:
|
||||||
|
"""Flowchart node: 下拉框选择."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi select activity: {selectors=} {value=}")
|
||||||
|
|
||||||
|
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
|
||||||
|
"""Flowchart node: 文件上传."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi upload-file activity: {selectors=} {file_path=}")
|
||||||
|
|
||||||
|
def read_first(self, selectors: list[str], value_name: str) -> str:
|
||||||
|
"""Flowchart node: 读取页面文本/输入框值并写入流程变量."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi read-text activity: {selectors=} {value_name=}")
|
||||||
|
|
||||||
|
def screenshot(self, name: str) -> str:
|
||||||
|
"""Flowchart node: 截图存证,返回文件路径."""
|
||||||
|
raise NotImplementedError(f"Map to YiSaiQi screenshot activity: {name=}")
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue