commit 5bf598de058673cd21d1cb0a9d9c4aa82d130be9 Author: 34BFF76918A9449ABAB323149DDCEA73 Date: Wed Aug 12 11:13:33 2026 +0800 5D3420315FA54219AB2427B0C674AD66auto diff --git a/Main.seq b/Main.seq new file mode 100644 index 0000000..1b75b42 --- /dev/null +++ b/Main.seq @@ -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" +} diff --git a/Project.pro b/Project.pro new file mode 100644 index 0000000..16fcd71 --- /dev/null +++ b/Project.pro @@ -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": [ + ] + } + ] +} diff --git a/codes/GlobalFun.py b/codes/GlobalFun.py new file mode 100644 index 0000000..1fd5a5b --- /dev/null +++ b/codes/GlobalFun.py @@ -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}") diff --git a/codes/modules/__init__.py b/codes/modules/__init__.py new file mode 100644 index 0000000..1319a5a --- /dev/null +++ b/codes/modules/__init__.py @@ -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") diff --git a/codes/modules/readme.txt b/codes/modules/readme.txt new file mode 100644 index 0000000..5e27597 --- /dev/null +++ b/codes/modules/readme.txt @@ -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") # 设置服务器变量 +''' \ No newline at end of file diff --git a/codes/modules/resources/elements.json b/codes/modules/resources/elements.json new file mode 100644 index 0000000..22d243d --- /dev/null +++ b/codes/modules/resources/elements.json @@ -0,0 +1,3 @@ +{ + "version": "2.0" +} diff --git a/codes/rpa_yisaiqi/README.md b/codes/rpa_yisaiqi/README.md new file mode 100644 index 0000000..ba091d4 --- /dev/null +++ b/codes/rpa_yisaiqi/README.md @@ -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 | diff --git a/codes/rpa_yisaiqi/__init__.py b/codes/rpa_yisaiqi/__init__.py new file mode 100644 index 0000000..a13d19b --- /dev/null +++ b/codes/rpa_yisaiqi/__init__.py @@ -0,0 +1,2 @@ +"""YiSaiQi RPA integration helpers.""" + diff --git a/codes/rpa_yisaiqi/__pycache__/__init__.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..5f669bb Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/__init__.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..822817f Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/__init__.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-313.pyc new file mode 100644 index 0000000..2b4237e Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc new file mode 100644 index 0000000..303215a Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/callback_client.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-313.pyc new file mode 100644 index 0000000..808d6b5 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc new file mode 100644 index 0000000..2fd64e4 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/contract_create_node.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-313.pyc new file mode 100644 index 0000000..46c8972 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc new file mode 100644 index 0000000..557d9ee Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/contract_draft.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-313.pyc new file mode 100644 index 0000000..b241643 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc new file mode 100644 index 0000000..3955441 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/contract_flow_steps.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-313.pyc new file mode 100644 index 0000000..da67695 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc new file mode 100644 index 0000000..ad75b51 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/pdf_utils.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-313.pyc b/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-313.pyc new file mode 100644 index 0000000..da312fd Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-313.pyc differ diff --git a/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc b/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc new file mode 100644 index 0000000..e9a8ba2 Binary files /dev/null and b/codes/rpa_yisaiqi/__pycache__/yisaiqi_adapter_template.cpython-38.pyc differ diff --git a/codes/rpa_yisaiqi/callback_client.py b/codes/rpa_yisaiqi/callback_client.py new file mode 100644 index 0000000..41479f1 --- /dev/null +++ b/codes/rpa_yisaiqi/callback_client.py @@ -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) diff --git a/codes/rpa_yisaiqi/contract_create_node.py b/codes/rpa_yisaiqi/contract_create_node.py new file mode 100644 index 0000000..9a1e2b9 --- /dev/null +++ b/codes/rpa_yisaiqi/contract_create_node.py @@ -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()) diff --git a/codes/rpa_yisaiqi/contract_draft.py b/codes/rpa_yisaiqi/contract_draft.py new file mode 100644 index 0000000..82c972c --- /dev/null +++ b/codes/rpa_yisaiqi/contract_draft.py @@ -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"), + ) diff --git a/codes/rpa_yisaiqi/contract_flow_steps.py b/codes/rpa_yisaiqi/contract_flow_steps.py new file mode 100644 index 0000000..bc0b0cf --- /dev/null +++ b/codes/rpa_yisaiqi/contract_flow_steps.py @@ -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 "格式合同" diff --git a/codes/rpa_yisaiqi/examples/contract_draft_xyy.json b/codes/rpa_yisaiqi/examples/contract_draft_xyy.json new file mode 100644 index 0000000..bfc3103 --- /dev/null +++ b/codes/rpa_yisaiqi/examples/contract_draft_xyy.json @@ -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 + } + ] +} diff --git a/codes/rpa_yisaiqi/examples/contract_draft_yyc.json b/codes/rpa_yisaiqi/examples/contract_draft_yyc.json new file mode 100644 index 0000000..72a6262 --- /dev/null +++ b/codes/rpa_yisaiqi/examples/contract_draft_yyc.json @@ -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 + } + ] +} diff --git a/codes/rpa_yisaiqi/flows/contract_create_flow.json b/codes/rpa_yisaiqi/flows/contract_create_flow.json new file mode 100644 index 0000000..d3fa6be --- /dev/null +++ b/codes/rpa_yisaiqi/flows/contract_create_flow.json @@ -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')" + } + ] +} diff --git a/codes/rpa_yisaiqi/flows/contract_create_preview_flow.json b/codes/rpa_yisaiqi/flows/contract_create_preview_flow.json new file mode 100644 index 0000000..72906b3 --- /dev/null +++ b/codes/rpa_yisaiqi/flows/contract_create_preview_flow.json @@ -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" } + ] +} diff --git a/codes/rpa_yisaiqi/pdf_utils.py b/codes/rpa_yisaiqi/pdf_utils.py new file mode 100644 index 0000000..272a28e --- /dev/null +++ b/codes/rpa_yisaiqi/pdf_utils.py @@ -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) + diff --git a/codes/rpa_yisaiqi/requirements.txt b/codes/rpa_yisaiqi/requirements.txt new file mode 100644 index 0000000..5629a7f --- /dev/null +++ b/codes/rpa_yisaiqi/requirements.txt @@ -0,0 +1 @@ +pypdf==5.1.0 diff --git a/codes/rpa_yisaiqi/selectors/contract_create_selectors.json b/codes/rpa_yisaiqi/selectors/contract_create_selectors.json new file mode 100644 index 0000000..e1c8bb6 --- /dev/null +++ b/codes/rpa_yisaiqi/selectors/contract_create_selectors.json @@ -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=合同编号" + ] + } +} diff --git a/codes/rpa_yisaiqi/verify_flow.py b/codes/rpa_yisaiqi/verify_flow.py new file mode 100644 index 0000000..a26c116 --- /dev/null +++ b/codes/rpa_yisaiqi/verify_flow.py @@ -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 调用 +""") diff --git a/codes/rpa_yisaiqi/yisaiqi_adapter_template.py b/codes/rpa_yisaiqi/yisaiqi_adapter_template.py new file mode 100644 index 0000000..47f0468 --- /dev/null +++ b/codes/rpa_yisaiqi/yisaiqi_adapter_template.py @@ -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=}") + diff --git a/project.html b/project.html new file mode 100644 index 0000000..a192331 --- /dev/null +++ b/project.html @@ -0,0 +1,273 @@ + + + + +流程组件展示 + + + +
+
I-Search 流程组件展示图
+
+ + +
+ +
+ + +
+
+
+ +
+
+
+ + + + + \ No newline at end of file