Compare commits
No commits in common. "63eec4033428ee2e115f63e19549d45242cdbe93" and "4060d7b45d0d795da35f306e83f1b50ee1f0b96e" have entirely different histories.
63eec40334
...
4060d7b45d
|
|
@ -21,12 +21,6 @@ state_center_web/dist/
|
||||||
|
|
||||||
# Local runtime / agent
|
# Local runtime / agent
|
||||||
.runtime/
|
.runtime/
|
||||||
rpa_yisaiqi/screenshots/
|
|
||||||
rpa-chrome-profile/
|
|
||||||
**/chrome-profile/
|
|
||||||
.tmp/
|
|
||||||
.grok/
|
|
||||||
.grokignore
|
|
||||||
.claude/
|
.claude/
|
||||||
.superpowers/
|
.superpowers/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,20 +57,10 @@ python -m rpa_yisaiqi.contract_create_node `
|
||||||
|
|
||||||
## 真实合同起草运行
|
## 真实合同起草运行
|
||||||
|
|
||||||
客商必须用合同测试系统里**真实存在**的操作码。`CUST-YYC-001` 这类占位符会在选客商时搜不到,入参校验会直接拒绝。
|
先准备合同起草输入 JSON,参考:
|
||||||
|
|
||||||
测试环境当前可用:
|
- `examples/contract_draft_yyc.json`
|
||||||
|
- `examples/contract_draft_xyy.json`
|
||||||
| 字段 | 值 |
|
|
||||||
| --- | --- |
|
|
||||||
| customerCode | `DW0100046361` |
|
|
||||||
| customerName | `重庆亿昊药业有限公司` |
|
|
||||||
|
|
||||||
联调入参用:
|
|
||||||
|
|
||||||
- `examples/contract_create_input_dry_run.json`:真客商 + `dry_run`,先校准页面
|
|
||||||
- `examples/contract_draft_yyc.json`:医药城,同上客商
|
|
||||||
- `examples/contract_draft_xyy.json`:小药药分支(不上传 PDF),客商仍用合同系统里能搜到的同一条
|
|
||||||
|
|
||||||
YYC 有多份平台 PDF 时需要合并,先安装:
|
YYC 有多份平台 PDF 时需要合并,先安装:
|
||||||
|
|
||||||
|
|
@ -85,7 +75,7 @@ python -m rpa_yisaiqi.contract_create_node `
|
||||||
--state-center-url http://192.168.x.x:8000 `
|
--state-center-url http://192.168.x.x:8000 `
|
||||||
--token dev-agent-token `
|
--token dev-agent-token `
|
||||||
--job-id <状态中心job_id> `
|
--job-id <状态中心job_id> `
|
||||||
--supplier-name 重庆亿昊药业有限公司 `
|
--supplier-name 重庆亿昊药业 `
|
||||||
--order-date 2026-08-11 `
|
--order-date 2026-08-11 `
|
||||||
--source-type YYC `
|
--source-type YYC `
|
||||||
--draft-path rpa_yisaiqi/examples/contract_draft_yyc.json `
|
--draft-path rpa_yisaiqi/examples/contract_draft_yyc.json `
|
||||||
|
|
@ -121,27 +111,6 @@ python -m rpa_yisaiqi.contract_create_node `
|
||||||
--state-center-url http://192.168.x.x:8000
|
--state-center-url http://192.168.x.x:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
## 艺赛旗画布怎么看
|
|
||||||
|
|
||||||
Studio 里「插入代码块」标题改不了,会一直显示「插入代码块#1」。步骤名写在节点**备注**里,画布上出现在代码块下方,例如 `S06 填写签约方`。
|
|
||||||
|
|
||||||
流程按四段换行,不要再排成一条长龙:
|
|
||||||
|
|
||||||
```
|
|
||||||
准备 开始 → S01 初始化 → S02 打开系统 → S03 登录
|
|
||||||
进入起草 S04 进入起草 → S05 选择模板
|
|
||||||
填写表单 S06 签约方 → S07 基础信息 → S08 金额日期 → S09 货品 → S10 付款
|
|
||||||
提交回写 S11 上传PDF → S12 提交 → S13 读合同号 → S14 回调
|
|
||||||
```
|
|
||||||
|
|
||||||
改步骤后先关掉 Studio,再生成画布:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python rpa_yisaiqi/build_multinode_seq.py
|
|
||||||
```
|
|
||||||
|
|
||||||
然后重新打开 `rpa_yisaiqi/studio` 项目并编译。
|
|
||||||
|
|
||||||
## 接入真实页面自动化
|
## 接入真实页面自动化
|
||||||
|
|
||||||
真实艺赛旗页面操作按流程图拆在:
|
真实艺赛旗页面操作按流程图拆在:
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
"""多节点流程图(Main.seq)的跨节点共享状态容器。
|
|
||||||
|
|
||||||
艺赛旗编译 Main.seq 后,各代码节点的 PBCode 会在同一 Python 进程中执行。
|
|
||||||
本模块以「模块级全局变量」承载 browser / ctx / client / spec 等对象,
|
|
||||||
避免依赖艺赛旗对局部变量的拼接方式,让每个节点都能可靠地取到上一步的产物。
|
|
||||||
"""
|
|
||||||
|
|
||||||
spec = None
|
|
||||||
client = None
|
|
||||||
ctx = None
|
|
||||||
browser = None
|
|
||||||
submitted = False
|
|
||||||
contract_no = ""
|
|
||||||
fd_id = ""
|
|
||||||
is_dry_run = False
|
|
||||||
completed = False
|
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
"""
|
|
||||||
APA (艺赛旗) Selenium Chrome BrowserAdapter 实现
|
|
||||||
通过 Selenium WebDriver 直接操控 Chrome,不依赖 APA 浏览器模块
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def _is_inside_repo(path: Path, repo_root: Path) -> bool:
|
|
||||||
try:
|
|
||||||
path.resolve().relative_to(repo_root.resolve())
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_chrome_profile_base(requested: str | None = None, *, repo_root: Path | None = None) -> Path:
|
|
||||||
"""Chrome user-data dir must stay outside the repo so list_dir never walks 3GB."""
|
|
||||||
root = (repo_root or REPO_ROOT).resolve()
|
|
||||||
outside = (root.parent / "rpa-chrome-profile").resolve()
|
|
||||||
raw = os.getenv("RPA_CHROME_PROFILE") if requested is None else requested
|
|
||||||
if raw:
|
|
||||||
candidate = Path(raw).expanduser().resolve()
|
|
||||||
if _is_inside_repo(candidate, root):
|
|
||||||
print(f"RPA_CHROME_PROFILE 指向仓库内 {candidate},已改用 {outside}", flush=True)
|
|
||||||
return outside
|
|
||||||
return candidate
|
|
||||||
return outside
|
|
||||||
|
|
||||||
|
|
||||||
class ApaBrowserAdapter:
|
|
||||||
"""Selenium Chrome 浏览器适配器,实现 BrowserAdapter 协议"""
|
|
||||||
|
|
||||||
def __init__(self, url: str = "", *, flow: str = "default"):
|
|
||||||
self._url = url
|
|
||||||
self._flow = flow
|
|
||||||
self._driver = None
|
|
||||||
self._is_open = False
|
|
||||||
|
|
||||||
def _get_driver(self):
|
|
||||||
"""创建或获取 Chrome WebDriver"""
|
|
||||||
if self._driver is not None:
|
|
||||||
return self._driver
|
|
||||||
|
|
||||||
from selenium import webdriver
|
|
||||||
from selenium.webdriver.chrome.service import Service
|
|
||||||
from selenium.webdriver.chrome.options import Options
|
|
||||||
|
|
||||||
profile_base = resolve_chrome_profile_base()
|
|
||||||
if _is_inside_repo(profile_base, REPO_ROOT):
|
|
||||||
raise RuntimeError(f"拒绝在仓库内创建 Chrome 用户目录: {profile_base}")
|
|
||||||
# 每次进程单独目录:上次 detach 留下的 Chrome 会锁住同一 profile
|
|
||||||
profile_dir = profile_base / str(os.getpid())
|
|
||||||
os.makedirs(profile_dir, exist_ok=True)
|
|
||||||
profile_dir = str(profile_dir)
|
|
||||||
|
|
||||||
options = Options()
|
|
||||||
options.page_load_strategy = "eager"
|
|
||||||
# 不自动关闭浏览器,方便调试
|
|
||||||
options.add_experimental_option("detach", True)
|
|
||||||
# 避免被检测为自动化
|
|
||||||
options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"])
|
|
||||||
options.add_experimental_option("useAutomationExtension", False)
|
|
||||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
|
||||||
options.add_argument("--no-first-run")
|
|
||||||
options.add_argument("--no-default-browser-check")
|
|
||||||
options.add_argument("--disable-gpu")
|
|
||||||
options.add_argument("--disable-extensions")
|
|
||||||
options.add_argument("--disable-popup-blocking")
|
|
||||||
options.add_argument("--start-maximized")
|
|
||||||
# 独立目录,避免和日常 Chrome 抢用户数据目录导致卡住
|
|
||||||
options.add_argument("--user-data-dir=" + profile_dir)
|
|
||||||
|
|
||||||
chromedriver = os.getenv("CHROMEDRIVER_PATH")
|
|
||||||
if chromedriver and os.path.exists(chromedriver):
|
|
||||||
service = Service(chromedriver)
|
|
||||||
else:
|
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
|
||||||
service = Service(ChromeDriverManager().install())
|
|
||||||
self._driver = webdriver.Chrome(service=service, options=options)
|
|
||||||
self._driver.set_page_load_timeout(45)
|
|
||||||
self._driver.set_script_timeout(30)
|
|
||||||
return self._driver
|
|
||||||
|
|
||||||
def _ensure_open(self):
|
|
||||||
if self._driver is None:
|
|
||||||
driver = self._get_driver()
|
|
||||||
if self._url:
|
|
||||||
driver.get(self._url)
|
|
||||||
self._is_open = True
|
|
||||||
|
|
||||||
# ---- 内部查找方法 ----
|
|
||||||
|
|
||||||
def _find(self, selector: str, timeout: int = 5):
|
|
||||||
"""
|
|
||||||
智能查找元素:支持 CSS、XPath、text=、label=、aria=、title=、:contains()
|
|
||||||
"""
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from selenium.common.exceptions import NoSuchElementException
|
|
||||||
import re
|
|
||||||
|
|
||||||
# 1. text=xxx → 查找包含对应文本的元素
|
|
||||||
if selector.startswith("text="):
|
|
||||||
text_val = selector[5:].strip('"').strip("'")
|
|
||||||
xpath = f'//*[contains(text(),"{text_val}")]'
|
|
||||||
return self._driver.find_element(By.XPATH, xpath)
|
|
||||||
|
|
||||||
# 2. label=xxx → 找到对应标签的输入框
|
|
||||||
if selector.startswith("label="):
|
|
||||||
text_val = selector[6:].strip('"').strip("'")
|
|
||||||
xpath = f'//label[contains(text(),"{text_val}")]/following-sibling::*[1][self::input or self::select or self::textarea]'
|
|
||||||
try:
|
|
||||||
return self._driver.find_element(By.XPATH, xpath)
|
|
||||||
except NoSuchElementException:
|
|
||||||
# Fallback: find the label's "for" attribute
|
|
||||||
try:
|
|
||||||
for_id = self._driver.find_element(
|
|
||||||
By.XPATH, f'//label[contains(text(),"{text_val}")]'
|
|
||||||
).get_attribute("for")
|
|
||||||
if for_id:
|
|
||||||
return self._driver.find_element(By.ID, for_id)
|
|
||||||
except NoSuchElementException:
|
|
||||||
pass
|
|
||||||
raise
|
|
||||||
|
|
||||||
# 3. aria=xxx → aria-label 属性
|
|
||||||
if selector.startswith("aria="):
|
|
||||||
text_val = selector[5:].strip('"').strip("'")
|
|
||||||
return self._driver.find_element(By.CSS_SELECTOR, f'[aria-label="{text_val}"]')
|
|
||||||
|
|
||||||
# 4. title=xxx → title 属性
|
|
||||||
if selector.startswith("title="):
|
|
||||||
text_val = selector[6:].strip('"').strip("'")
|
|
||||||
return self._driver.find_element(By.CSS_SELECTOR, f'[title="{text_val}"]')
|
|
||||||
|
|
||||||
# 5. :contains() → 转 XPath(支持 button:contains(文本) / a:contains("文本"))
|
|
||||||
if ":contains(" in selector:
|
|
||||||
m = re.match(r'(.+?):contains\(["\']?(.+?)["\']?\)', selector)
|
|
||||||
if m:
|
|
||||||
tag_part, text_val = m.groups()
|
|
||||||
xpath = f'//{tag_part}[contains(.,"{text_val}")]'
|
|
||||||
return self._driver.find_element(By.XPATH, xpath)
|
|
||||||
|
|
||||||
# 6. 显式 XPath(以 / 或 // 开头)优先按 XPath 查找
|
|
||||||
if selector.startswith("//") or selector.startswith("/"):
|
|
||||||
return self._driver.find_element(By.XPATH, selector)
|
|
||||||
|
|
||||||
# 7. 标准 CSS selector
|
|
||||||
return self._driver.find_element(By.CSS_SELECTOR, selector)
|
|
||||||
|
|
||||||
# ---- BrowserAdapter 协议实现 ----
|
|
||||||
|
|
||||||
def open_url(self, url: str) -> None:
|
|
||||||
self._url = url
|
|
||||||
driver = self._get_driver()
|
|
||||||
try:
|
|
||||||
driver.get(url)
|
|
||||||
except Exception as exc:
|
|
||||||
# eager 仍可能因长轮询资源超时;页面文档已出来就继续
|
|
||||||
current = ""
|
|
||||||
try:
|
|
||||||
current = driver.current_url or ""
|
|
||||||
except Exception:
|
|
||||||
current = ""
|
|
||||||
if not current or current in ("data:,", "about:blank"):
|
|
||||||
raise RuntimeError(f"打开页面失败:{url} ({exc})") from exc
|
|
||||||
self._is_open = True
|
|
||||||
|
|
||||||
def _set_value_via_js(self, el, value: str) -> None:
|
|
||||||
"""通过原生 value setter + 事件注入设置值,兼容 LUI/React 受控组件"""
|
|
||||||
self._driver.execute_script(
|
|
||||||
"""
|
|
||||||
var el = arguments[0];
|
|
||||||
var value = arguments[1];
|
|
||||||
var proto = el.tagName === 'TEXTAREA'
|
|
||||||
? window.HTMLTextAreaElement.prototype
|
|
||||||
: window.HTMLInputElement.prototype;
|
|
||||||
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
||||||
setter.call(el, value);
|
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
||||||
""",
|
|
||||||
el,
|
|
||||||
str(value),
|
|
||||||
)
|
|
||||||
|
|
||||||
def fill_first(self, selectors: list, value: str) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
# 先尝试原生输入;LUI 受控组件用 JS 注入兜底
|
|
||||||
try:
|
|
||||||
el.clear()
|
|
||||||
el.send_keys(str(value))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if el.get_attribute("value") != str(value):
|
|
||||||
self._set_value_via_js(el, str(value))
|
|
||||||
time.sleep(0.3)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"fill_first failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def click_native(self, selectors: list) -> None:
|
|
||||||
"""真实鼠标点击,给只监听 mousedown 的 LUI 页签/工具栏用。"""
|
|
||||||
self._ensure_open()
|
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
self._driver.execute_script(
|
|
||||||
"arguments[0].scrollIntoView({block:'center', inline:'center'});",
|
|
||||||
el,
|
|
||||||
)
|
|
||||||
time.sleep(0.2)
|
|
||||||
ActionChains(self._driver).move_to_element(el).pause(0.15).click().perform()
|
|
||||||
time.sleep(0.4)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"click_native failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def click_first(self, selectors: list) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
# LUI/委托事件用 JS 点击最可靠
|
|
||||||
self._driver.execute_script("arguments[0].click()", el)
|
|
||||||
time.sleep(0.5)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"click_first failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def double_click_first(self, selectors: list) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
ActionChains(self._driver).double_click(el).perform()
|
|
||||||
time.sleep(0.5)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"double_click_first failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
try:
|
|
||||||
WebDriverWait(self._driver, timeout_seconds).until(
|
|
||||||
EC.text_to_be_present_in_element((By.TAG_NAME, "body"), text)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise TimeoutError(f"wait_for_text '{text}' timed out: {e}")
|
|
||||||
|
|
||||||
def wait_for_element(self, selectors: list, timeout_seconds: int = 15) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
deadline = time.time() + timeout_seconds
|
|
||||||
last_error = None
|
|
||||||
while time.time() < deadline:
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
self._find(sel, timeout=1)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
time.sleep(0.5)
|
|
||||||
raise TimeoutError(f"wait_for_element timed out: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def element_exists(self, selectors: list) -> bool:
|
|
||||||
self._ensure_open()
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
self._find(sel, timeout=1)
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return False
|
|
||||||
|
|
||||||
def switch_to_frame(self, selectors: list) -> None:
|
|
||||||
"""切换到匹配到的 iframe。优先可见弹窗,避免切到上次留下的隐藏 iframe。"""
|
|
||||||
self._ensure_open()
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
|
|
||||||
last_error = None
|
|
||||||
found = []
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
if sel.startswith("//") or sel.startswith("/"):
|
|
||||||
found.extend(self._driver.find_elements(By.XPATH, sel))
|
|
||||||
else:
|
|
||||||
found.extend(self._driver.find_elements(By.CSS_SELECTOR, sel))
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
visible = []
|
|
||||||
hidden = []
|
|
||||||
for el in found:
|
|
||||||
try:
|
|
||||||
(visible if el.is_displayed() else hidden).append(el)
|
|
||||||
except Exception:
|
|
||||||
hidden.append(el)
|
|
||||||
for el in visible + hidden:
|
|
||||||
try:
|
|
||||||
self._driver.switch_to.default_content()
|
|
||||||
self._driver.switch_to.frame(el)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
raise RuntimeError(f"switch_to_frame failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def switch_to_default_content(self) -> None:
|
|
||||||
"""切回主文档(顶层 frame)。"""
|
|
||||||
self._ensure_open()
|
|
||||||
self._driver.switch_to.default_content()
|
|
||||||
|
|
||||||
def execute_script(self, script: str, *args):
|
|
||||||
self._ensure_open()
|
|
||||||
return self._driver.execute_script(script, *args)
|
|
||||||
|
|
||||||
def select_xform(self, name: str, value: str) -> None:
|
|
||||||
"""选中 Landray xform fancy-select(.fs-wrap / .fs-option)。"""
|
|
||||||
self._ensure_open()
|
|
||||||
result = self._driver.execute_script(
|
|
||||||
"""
|
|
||||||
const name = arguments[0];
|
|
||||||
const wanted = String(arguments[1] || '').trim();
|
|
||||||
const flag = document.querySelector('xformflag[property="' + name + '"]');
|
|
||||||
if (!flag) {
|
|
||||||
return {ok: false, error: 'flag not found: ' + name};
|
|
||||||
}
|
|
||||||
const norm = (s) => String(s || '').replace(/\\s+/g, '').trim();
|
|
||||||
const wantedN = norm(wanted);
|
|
||||||
const opts = Array.from(flag.querySelectorAll('.fs-option'));
|
|
||||||
let target = opts.find((o) => {
|
|
||||||
const text = norm((o.querySelector('.fs-option-label') || o).textContent);
|
|
||||||
const val = norm(o.getAttribute('data-value'));
|
|
||||||
return text === wantedN || val === wantedN;
|
|
||||||
});
|
|
||||||
if (!target && wantedN.length >= 2) {
|
|
||||||
target = opts.find((o) => {
|
|
||||||
const text = norm((o.querySelector('.fs-option-label') || o).textContent);
|
|
||||||
return text.indexOf(wantedN) >= 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!target) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: 'option not found: ' + wanted,
|
|
||||||
available: opts.map((o) => ((o.querySelector('.fs-option-label') || o).textContent || '').trim()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const labelWrap = flag.querySelector('.fs-label-wrap');
|
|
||||||
if (labelWrap) { labelWrap.click(); }
|
|
||||||
target.click();
|
|
||||||
const input = flag.querySelector('input[name="' + name + '"]');
|
|
||||||
const selected = target.getAttribute('data-value') || '';
|
|
||||||
if (input) {
|
|
||||||
const proto = window.HTMLInputElement.prototype;
|
|
||||||
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
||||||
setter.call(input, selected);
|
|
||||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
if (typeof __xformDispatch === 'function') {
|
|
||||||
try { __xformDispatch(selected, input); } catch (e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
value: selected,
|
|
||||||
label: ((flag.querySelector('.fs-label') || {}).textContent || '').trim(),
|
|
||||||
};
|
|
||||||
""",
|
|
||||||
name,
|
|
||||||
str(value),
|
|
||||||
)
|
|
||||||
if not result or not result.get("ok"):
|
|
||||||
raise RuntimeError(f"select_xform failed: {name}={value} {result}")
|
|
||||||
time.sleep(0.3)
|
|
||||||
|
|
||||||
def select_first(self, selectors: list, value: str) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
from selenium.webdriver.support.ui import Select
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
# 原生 <select>
|
|
||||||
try:
|
|
||||||
Select(el).select_by_visible_text(value)
|
|
||||||
time.sleep(0.3)
|
|
||||||
if self._selected_text_matches(el, value):
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# 兜底:JS 定位 option 并设置选中
|
|
||||||
self._select_via_js(el, value)
|
|
||||||
time.sleep(0.3)
|
|
||||||
if self._selected_text_matches(el, value):
|
|
||||||
return
|
|
||||||
raise RuntimeError(f"select_first 未选中目标项: {value}")
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"select_first failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def _selected_text_matches(self, el, value: str) -> bool:
|
|
||||||
try:
|
|
||||||
script = (
|
|
||||||
"var el = arguments[0];"
|
|
||||||
"if (el.selectedIndex != null) {"
|
|
||||||
" var o = el.options[el.selectedIndex];"
|
|
||||||
" return o ? (o.textContent || o.text || '').trim() : '';"
|
|
||||||
"}"
|
|
||||||
"return (el.value || '').trim();"
|
|
||||||
)
|
|
||||||
selected = self._driver.execute_script(script, el)
|
|
||||||
return (selected or "").strip() == str(value).strip()
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _select_via_js(self, el, value: str) -> None:
|
|
||||||
self._driver.execute_script(
|
|
||||||
"""
|
|
||||||
var el = arguments[0];
|
|
||||||
var value = arguments[1];
|
|
||||||
var options = el.options || [];
|
|
||||||
for (var i = 0; i < options.length; i++) {
|
|
||||||
var t = (options[i].textContent || options[i].text || '').trim();
|
|
||||||
if (t === value || options[i].value === value) {
|
|
||||||
el.selectedIndex = i;
|
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
el,
|
|
||||||
str(value),
|
|
||||||
)
|
|
||||||
|
|
||||||
def upload_file_first(self, selectors: list, file_path: str) -> None:
|
|
||||||
self._ensure_open()
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
el.send_keys(file_path)
|
|
||||||
time.sleep(1)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"upload_file_first failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def read_first(self, selectors: list, value_name: str = "") -> str:
|
|
||||||
self._ensure_open()
|
|
||||||
last_error = None
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
|
||||||
el = self._find(sel)
|
|
||||||
result = el.text or el.get_attribute("value") or ""
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
last_error = e
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"read_first failed: {selectors}, last: {last_error}")
|
|
||||||
|
|
||||||
def read_body_text(self) -> str:
|
|
||||||
self._ensure_open()
|
|
||||||
try:
|
|
||||||
return self._driver.find_element("tag name", "body").text
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def screenshot(self, name: str) -> str:
|
|
||||||
from .runtime.accept import artifact_dir
|
|
||||||
|
|
||||||
save_dir = artifact_dir(self._flow)
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
path = save_dir / f"{name}.png"
|
|
||||||
if self._driver:
|
|
||||||
self._driver.save_screenshot(str(path))
|
|
||||||
return str(path)
|
|
||||||
|
|
@ -1,358 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
"""
|
|
||||||
构建合同建单 Main.seq —— 按业务段换行,画布上能直接读出步骤。
|
|
||||||
|
|
||||||
艺赛旗「插入代码块」的标题固定为「插入代码块#1」,「业务流程描述」节点
|
|
||||||
打开后再保存标题会被清空。画布上真正能看见的是代码块的备注(NOTE)。
|
|
||||||
因此本脚本:
|
|
||||||
1. 不再插入空的业务流程描述节点
|
|
||||||
2. 把「S06 填写签约方」写进 NOTE,作为画布副标题
|
|
||||||
3. 按 准备 / 进入起草 / 填写表单 / 提交回写 四段换行
|
|
||||||
|
|
||||||
关闭 Studio 后运行本脚本,再重新打开项目编译。
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
|
||||||
OUT = HERE / "studio" / "Main.seq"
|
|
||||||
|
|
||||||
COL_W = 280
|
|
||||||
ROW_H = 200
|
|
||||||
ORIGIN_X = 360
|
|
||||||
ORIGIN_Y = 220
|
|
||||||
BEGIN_TAG = "202608141000000001"
|
|
||||||
|
|
||||||
|
|
||||||
def b64_apa(val):
|
|
||||||
# 艺赛旗 EP_CONTROL / EP_Gen 字段值末尾带换行,缺失会导致字段解析失败
|
|
||||||
return json.dumps({"val": base64.b64encode(str(val).encode()).decode()}, indent=4) + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def b64_raw(val):
|
|
||||||
return base64.b64encode(str(val).encode()).decode()
|
|
||||||
|
|
||||||
|
|
||||||
STEP_INIT = '''import sys, os, json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
_REPO_ROOT = next(
|
|
||||||
p for p in Path(__file__).resolve().parents
|
|
||||||
if (p / "rpa_yisaiqi" / "__init__.py").exists()
|
|
||||||
)
|
|
||||||
if str(_REPO_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_REPO_ROOT))
|
|
||||||
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data, build_spec_from_data
|
|
||||||
from rpa_yisaiqi.callback_client import StateCenterClient
|
|
||||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
|
|
||||||
|
|
||||||
|
|
||||||
def _first(*values):
|
|
||||||
for value in values:
|
|
||||||
if value is not None and str(value).strip():
|
|
||||||
return str(value).strip()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
raw = (self.input_arg or "").strip()
|
|
||||||
if not raw:
|
|
||||||
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
|
|
||||||
if not raw:
|
|
||||||
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
|
|
||||||
print("未传入 -i/--input,使用默认联调入参:", raw)
|
|
||||||
if not raw:
|
|
||||||
raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径")
|
|
||||||
|
|
||||||
data = load_input_data(raw)
|
|
||||||
state_center_url = _first(
|
|
||||||
data.get("stateCenterUrl"), data.get("stateCenterURL"),
|
|
||||||
os.getenv("STATE_CENTER_URL"), "http://192.168.105.69:8000",
|
|
||||||
)
|
|
||||||
agent_token = _first(data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token")
|
|
||||||
|
|
||||||
_st.spec = build_spec_from_data(data)
|
|
||||||
_st.client = StateCenterClient(state_center_url, agent_token)
|
|
||||||
_st.ctx = _st.spec.flow_context
|
|
||||||
_st.browser = ApaBrowserAdapter(url=_st.ctx.contract_url, flow="CONTRACT_CREATE")
|
|
||||||
_st.is_dry_run = (_st.spec.submit_mode == "dry_run")
|
|
||||||
|
|
||||||
if not _st.is_dry_run:
|
|
||||||
print("回调状态中心 start", _st.spec.job_id)
|
|
||||||
_st.client.start_node(_st.spec.job_id, "CONTRACT_CREATE")'''
|
|
||||||
|
|
||||||
STEP_OPEN = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import open_contract_system
|
|
||||||
|
|
||||||
open_contract_system(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_LOGIN = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import login_contract_system
|
|
||||||
|
|
||||||
login_contract_system(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_ENTER = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import enter_contract_create
|
|
||||||
|
|
||||||
enter_contract_create(_st.browser)'''
|
|
||||||
|
|
||||||
STEP_TEMPLATE = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import select_contract_template
|
|
||||||
|
|
||||||
select_contract_template(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_SIGNATORY = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_signatory_info
|
|
||||||
|
|
||||||
fill_contract_signatory_info(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_BASIC = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info
|
|
||||||
|
|
||||||
fill_contract_basic_info(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_AMOUNT = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_amount_and_dates
|
|
||||||
|
|
||||||
fill_contract_amount_and_dates(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_GOODS = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_goods_lines
|
|
||||||
|
|
||||||
fill_contract_goods_lines(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_PAYMENT = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_payment_info
|
|
||||||
|
|
||||||
fill_contract_payment_info(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_UPLOAD = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import upload_contract_document_if_needed
|
|
||||||
|
|
||||||
upload_contract_document_if_needed(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_SUBMIT = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import submit_contract_for_approval
|
|
||||||
|
|
||||||
_st.submitted = submit_contract_for_approval(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_READ_NO = '''import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
|
||||||
|
|
||||||
if _st.submitted:
|
|
||||||
_st.contract_no = read_contract_number(_st.browser, _st.ctx)'''
|
|
||||||
|
|
||||||
STEP_COMPLETE = '''import json
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
|
|
||||||
if not _st.is_dry_run and _st.contract_no:
|
|
||||||
print("回调状态中心 complete", _st.spec.job_id, _st.contract_no)
|
|
||||||
_st.client.complete_node(_st.spec.job_id, "CONTRACT_CREATE", {
|
|
||||||
"businessRefs": {"contractSystemNo": _st.contract_no},
|
|
||||||
})
|
|
||||||
print(json.dumps({"status": "done", "contractSystemNo": _st.contract_no}, ensure_ascii=False))'''
|
|
||||||
|
|
||||||
|
|
||||||
# 每段一行。NOTE 会显示在画布「插入代码块#1」下方,务必写短步骤名。
|
|
||||||
PHASES = [
|
|
||||||
(
|
|
||||||
"准备",
|
|
||||||
[
|
|
||||||
("S01 初始化并回调 start", STEP_INIT),
|
|
||||||
("S02 打开合同系统", STEP_OPEN),
|
|
||||||
("S03 登录合同系统", STEP_LOGIN),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"进入起草",
|
|
||||||
[
|
|
||||||
("S04 进入合同起草", STEP_ENTER),
|
|
||||||
("S05 选择模板", STEP_TEMPLATE),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"填写表单",
|
|
||||||
[
|
|
||||||
("S06 填写签约方", STEP_SIGNATORY),
|
|
||||||
("S07 填写基础信息", STEP_BASIC),
|
|
||||||
("S08 填写金额日期", STEP_AMOUNT),
|
|
||||||
("S09 填写货品明细", STEP_GOODS),
|
|
||||||
("S10 填写付款信息", STEP_PAYMENT),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"提交回写",
|
|
||||||
[
|
|
||||||
("S11 上传合同PDF", STEP_UPLOAD),
|
|
||||||
("S12 提交送审", STEP_SUBMIT),
|
|
||||||
("S13 读取合同编号", STEP_READ_NO),
|
|
||||||
("S14 回调 complete", STEP_COMPLETE),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def code_tag(i: int) -> str:
|
|
||||||
return f"202608143{i:02d}0000000"
|
|
||||||
|
|
||||||
|
|
||||||
def line_tag(i: int) -> str:
|
|
||||||
return f"202608149{i:02d}0000000"
|
|
||||||
|
|
||||||
|
|
||||||
def _iter_steps():
|
|
||||||
index = 1
|
|
||||||
for phase_name, items in PHASES:
|
|
||||||
for title, code in items:
|
|
||||||
yield index, phase_name, title, code
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
|
|
||||||
def build_begin_node(px: int, py: int) -> dict:
|
|
||||||
return {
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [],
|
|
||||||
"InputProperty": {"mouse_x": 0, "mouse_y": 0},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [],
|
|
||||||
},
|
|
||||||
"NodeProperty": "",
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": False,
|
|
||||||
"NodePx": str(px),
|
|
||||||
"NodePy": str(py),
|
|
||||||
"NodeSubType": "EnumWidget_Begin",
|
|
||||||
"NodeTag": BEGIN_TAG,
|
|
||||||
"NodeTitle": "开始",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_code_node(tag: str, title: str, code: str, px: int, py: int) -> dict:
|
|
||||||
body = f'print("[{title}]")\n' + code
|
|
||||||
return {
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [],
|
|
||||||
"InputProperty": {"mouse_x": 0, "mouse_y": 0},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [],
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": json.dumps(
|
|
||||||
{"PImport": "", "PBCode": b64_raw(body)}, indent=4
|
|
||||||
) + "\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": b64_apa("0"),
|
|
||||||
"EP_Gen_BEFOREDELAY": b64_apa("0"),
|
|
||||||
"EP_Gen_NOTE": b64_apa(title),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": False,
|
|
||||||
"NodePx": str(px),
|
|
||||||
"NodePy": str(py),
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": tag,
|
|
||||||
"NodeTitle": title,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_nodes():
|
|
||||||
nodes = []
|
|
||||||
placed = [] # (tag, x, y)
|
|
||||||
|
|
||||||
begin_x, begin_y = ORIGIN_X, ORIGIN_Y
|
|
||||||
nodes.append(build_begin_node(begin_x, begin_y))
|
|
||||||
placed.append((BEGIN_TAG, begin_x, begin_y))
|
|
||||||
|
|
||||||
# 第一段从「开始」右侧接着排;后续每段单独一行
|
|
||||||
col = 1
|
|
||||||
row = 0
|
|
||||||
current_phase = None
|
|
||||||
for index, phase_name, title, code in _iter_steps():
|
|
||||||
if current_phase is None:
|
|
||||||
current_phase = phase_name
|
|
||||||
elif phase_name != current_phase:
|
|
||||||
current_phase = phase_name
|
|
||||||
row += 1
|
|
||||||
col = 0
|
|
||||||
x = ORIGIN_X + col * COL_W
|
|
||||||
y = ORIGIN_Y + row * ROW_H
|
|
||||||
tag = code_tag(index)
|
|
||||||
nodes.append(build_code_node(tag, title, code, x, y))
|
|
||||||
placed.append((tag, x, y))
|
|
||||||
col += 1
|
|
||||||
|
|
||||||
return nodes, placed
|
|
||||||
|
|
||||||
|
|
||||||
def build_lines(placed: list) -> list:
|
|
||||||
lines = []
|
|
||||||
for k in range(len(placed) - 1):
|
|
||||||
prev_tag, prev_x, prev_y = placed[k]
|
|
||||||
next_tag, next_x, next_y = placed[k + 1]
|
|
||||||
same_row = prev_y == next_y
|
|
||||||
if same_row:
|
|
||||||
start_x, start_y = prev_x + 150, prev_y + 40
|
|
||||||
end_x, end_y = next_x - 4, next_y + 40
|
|
||||||
else:
|
|
||||||
start_x, start_y = prev_x + 70, prev_y + 90
|
|
||||||
end_x, end_y = next_x + 70, next_y - 8
|
|
||||||
lines.append({
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": f"{next_tag}/EPin_Flow_In",
|
|
||||||
"Px": str(end_x),
|
|
||||||
"Py": str(end_y),
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {"Px": "", "Py": ""},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": f"{prev_tag}/EPin_Flow_Out",
|
|
||||||
"Px": str(start_x),
|
|
||||||
"Py": str(start_y),
|
|
||||||
},
|
|
||||||
"LineTag": line_tag(k + 1),
|
|
||||||
"LineType": 0,
|
|
||||||
})
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
nodes, placed = build_nodes()
|
|
||||||
xs = [x for _, x, _ in placed]
|
|
||||||
ys = [y for _, _, y in placed]
|
|
||||||
main_seq = {
|
|
||||||
"LineList": build_lines(placed),
|
|
||||||
"NodeList": nodes,
|
|
||||||
"SeqTag": "Main",
|
|
||||||
"ViewCenterPx": str((min(xs) + max(xs)) // 2),
|
|
||||||
"ViewCenterPy": str((min(ys) + max(ys)) // 2),
|
|
||||||
"ViewScaling": "0.7",
|
|
||||||
}
|
|
||||||
OUT.write_text(json.dumps(main_seq, ensure_ascii=False, indent=4), encoding="utf-8")
|
|
||||||
print(f"多节点 Main.seq 已生成:{OUT}")
|
|
||||||
print(f"节点数:{len(nodes)}(1 开始 + {len(placed) - 1} 代码块,已去掉空业务标识)")
|
|
||||||
print(f"连线数:{len(main_seq['LineList'])}")
|
|
||||||
current_phase = None
|
|
||||||
for index, phase_name, title, _ in _iter_steps():
|
|
||||||
if phase_name != current_phase:
|
|
||||||
current_phase = phase_name
|
|
||||||
print(f" [{phase_name}]")
|
|
||||||
print(f" {title}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable
|
||||||
|
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
JsonDict = Dict[str, Any]
|
JsonDict = dict[str, Any]
|
||||||
Opener = Callable[[Request, float], Any]
|
Opener = Callable[[Request, float], Any]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,47 @@
|
||||||
"""CONTRACT_CREATE node: input building + state-center callback (runtime.shell) + Selenium drafting."""
|
"""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
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import uuid
|
||||||
from dataclasses import dataclass, replace
|
import webbrowser
|
||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from .apa_browser_adapter import ApaBrowserAdapter
|
|
||||||
from .callback_client import StateCenterClient
|
from .callback_client import StateCenterClient
|
||||||
from .contract_draft import (
|
from .contract_draft import ContractDraft, load_contract_draft
|
||||||
ContractDraft,
|
|
||||||
contract_draft_from_dict,
|
|
||||||
is_placeholder_customer_code,
|
|
||||||
load_contract_draft,
|
|
||||||
)
|
|
||||||
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
||||||
from .pdf_utils import ensure_merged_pdf
|
from .pdf_utils import ensure_merged_pdf
|
||||||
from .runtime.errors import NodeError, PdfMergeUnsupported, UnknownRpaError
|
from .runtime.errors import LoginFailed as RuntimeLoginFailed
|
||||||
|
from .runtime.errors import NodeError
|
||||||
|
from .runtime.errors import PdfMergeUnsupported as RuntimePdfMergeUnsupported
|
||||||
|
from .runtime.errors import SupplierContractDisabled as RuntimeSupplierContractDisabled
|
||||||
from .runtime.shell import execute_node
|
from .runtime.shell import execute_node
|
||||||
|
from .yisaiqi_adapter_template import YiSaiQiBrowserAdapter
|
||||||
|
|
||||||
NODE_CODE = "CONTRACT_CREATE"
|
NODE_CODE = "CONTRACT_CREATE"
|
||||||
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
|
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
|
||||||
|
|
||||||
|
|
||||||
class ContractCreateError(NodeError):
|
class ContractCreateError(NodeError):
|
||||||
"""入参缺失 / 页面元素找不到 / 页面改版,均映射 UI_ELEMENT_NOT_FOUND。"""
|
pass
|
||||||
|
|
||||||
def __init__(self, message: str = "") -> None:
|
|
||||||
# Studio 编译出来的旧 S01 在 input_arg 为空时会直接 raise。
|
|
||||||
# 这里拦下来改走默认联调 JSON,避免每次改 Main.py 都被覆盖。
|
|
||||||
if _should_bootstrap_default_input(message):
|
|
||||||
_bootstrap_default_input_and_exit()
|
|
||||||
super().__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
def default_contract_input_path() -> str:
|
class LoginFailed(RuntimeLoginFailed, ContractCreateError):
|
||||||
return str(
|
pass
|
||||||
Path(__file__).resolve().parent / "examples" / "contract_create_input_dry_run.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _should_bootstrap_default_input(message: str) -> bool:
|
class SupplierContractDisabled(RuntimeSupplierContractDisabled, ContractCreateError):
|
||||||
if "缺少业务入参" not in str(message):
|
pass
|
||||||
return False
|
|
||||||
script = os.path.basename(str(sys.argv[0] if sys.argv else "")).lower()
|
|
||||||
return script == "main.py"
|
|
||||||
|
|
||||||
|
|
||||||
def _bootstrap_default_input_and_exit() -> None:
|
class PdfMergeUnsupported(RuntimePdfMergeUnsupported, ContractCreateError):
|
||||||
path = default_contract_input_path()
|
pass
|
||||||
if not Path(path).is_file():
|
|
||||||
return
|
|
||||||
print("未传入 -i/--input,使用默认联调入参:", path, flush=True)
|
|
||||||
codes_dir = Path(__file__).resolve().parent / "studio" / "codes"
|
|
||||||
if str(codes_dir) not in sys.path:
|
|
||||||
sys.path.insert(0, str(codes_dir))
|
|
||||||
from run_contract import main
|
|
||||||
|
|
||||||
raise SystemExit(main())
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -74,12 +55,9 @@ class ContractCreateInput:
|
||||||
contract_url: str = CONTRACT_URL
|
contract_url: str = CONTRACT_URL
|
||||||
username: str = "test002"
|
username: str = "test002"
|
||||||
password: str = "1"
|
password: str = "1"
|
||||||
submit_mode: str = "submit"
|
open_browser: bool = False
|
||||||
order_no_list: tuple[str, ...] = ()
|
|
||||||
template_name: str = ""
|
|
||||||
draft_path: str | None = None
|
draft_path: str | None = None
|
||||||
merged_pdf_output: str | None = None
|
merged_pdf_output: str | None = None
|
||||||
flow_context: ContractFlowContext | None = None
|
|
||||||
|
|
||||||
|
|
||||||
ContractRunner = Callable[[ContractCreateInput], str]
|
ContractRunner = Callable[[ContractCreateInput], str]
|
||||||
|
|
@ -93,24 +71,13 @@ def execute_contract_create(
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
runner = contract_runner or create_contract_in_contract_system
|
runner = contract_runner or create_contract_in_contract_system
|
||||||
|
|
||||||
# dry_run:只跑到提交前,不回调状态中心(不 start/complete/fail)
|
|
||||||
if spec.submit_mode == "dry_run":
|
|
||||||
runner(spec)
|
|
||||||
return {"status": "dry_run", "submitted": False}
|
|
||||||
|
|
||||||
def _run() -> dict[str, Any]:
|
def _run() -> dict[str, Any]:
|
||||||
try:
|
|
||||||
contract_no = runner(spec)
|
contract_no = runner(spec)
|
||||||
except NodeError:
|
payload = build_complete_payload(spec, contract_no)
|
||||||
raise
|
|
||||||
except Exception as exc:
|
|
||||||
raise UnknownRpaError(str(exc)) from exc
|
|
||||||
if not contract_no:
|
|
||||||
raise ContractCreateError("submit 模式未读取到真实合同编号,无法 complete")
|
|
||||||
return {
|
return {
|
||||||
"businessRefs": {"contractSystemNo": contract_no},
|
"businessRefs": payload["businessRefs"],
|
||||||
"summary": _build_summary(spec),
|
"summary": payload["summary"],
|
||||||
"artifacts": [],
|
"artifacts": payload["artifacts"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return execute_node(
|
return execute_node(
|
||||||
|
|
@ -124,46 +91,34 @@ def execute_contract_create(
|
||||||
|
|
||||||
|
|
||||||
def create_contract_in_contract_system(spec: ContractCreateInput) -> str:
|
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:
|
if spec.contract_system_no:
|
||||||
return spec.contract_system_no
|
return spec.contract_system_no
|
||||||
|
|
||||||
ctx = spec.flow_context
|
|
||||||
if ctx is None:
|
|
||||||
if not spec.draft_path:
|
if not spec.draft_path:
|
||||||
raise ContractCreateError("缺少 flow_context 或 draft_path,无法起草合同")
|
raise ContractCreateError("draft_path is required for real contract drafting")
|
||||||
ctx = build_flow_context(spec, load_contract_draft(spec.draft_path))
|
draft = load_contract_draft(spec.draft_path)
|
||||||
|
|
||||||
browser = ApaBrowserAdapter(url=ctx.contract_url, flow="CONTRACT_CREATE")
|
|
||||||
return run_contract_create_flow(browser, ctx)
|
|
||||||
|
|
||||||
|
|
||||||
def build_spec_from_data(data: dict[str, Any]) -> ContractCreateInput:
|
|
||||||
draft = contract_draft_from_dict(data)
|
|
||||||
_validate_data(data, draft)
|
|
||||||
|
|
||||||
spec = ContractCreateInput(
|
|
||||||
job_id=_require(data, "jobId"),
|
|
||||||
supplier_name=draft.supplier_name or _require(data, "supplierName"),
|
|
||||||
order_date=draft.order_date or _require(data, "orderDate"),
|
|
||||||
source_type=draft.source_type or _require(data, "sourceType"),
|
|
||||||
contract_system_no=_opt(data.get("contractSystemNo")),
|
|
||||||
attempt_no=int(data.get("attemptNo", 1) or 1),
|
|
||||||
contract_url=_clean(data.get("contractUrl")) or os.getenv("CONTRACT_URL", CONTRACT_URL),
|
|
||||||
username=_clean(data.get("username")) or os.getenv("CONTRACT_USERNAME", "test002"),
|
|
||||||
password=str(data.get("password") or os.getenv("CONTRACT_PASSWORD", "1")),
|
|
||||||
submit_mode=draft.submit_mode,
|
|
||||||
order_no_list=tuple(draft.order_no_list),
|
|
||||||
template_name=draft.template_name,
|
|
||||||
merged_pdf_output=_opt(data.get("mergedPdfOutput")) or os.getenv("MERGED_PDF_OUTPUT"),
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx = build_flow_context(spec, draft)
|
ctx = build_flow_context(spec, draft)
|
||||||
return replace(spec, flow_context=ctx)
|
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:
|
def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> ContractFlowContext:
|
||||||
merged_pdf_path = _resolve_merged_pdf(draft, spec)
|
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(
|
return ContractFlowContext(
|
||||||
contract_url=spec.contract_url,
|
contract_url=spec.contract_url,
|
||||||
username=spec.username,
|
username=spec.username,
|
||||||
|
|
@ -180,9 +135,7 @@ def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> Contr
|
||||||
contract_background=draft.contract_background,
|
contract_background=draft.contract_background,
|
||||||
contract_nature=draft.contract_nature,
|
contract_nature=draft.contract_nature,
|
||||||
amount_currency=draft.amount_currency,
|
amount_currency=draft.amount_currency,
|
||||||
amount_type=draft.amount_type,
|
|
||||||
amount=draft.amount,
|
amount=draft.amount,
|
||||||
contract_title=draft.contract_title,
|
|
||||||
contract_end_date=draft.contract_end_date,
|
contract_end_date=draft.contract_end_date,
|
||||||
is_major_contract=draft.is_major_contract,
|
is_major_contract=draft.is_major_contract,
|
||||||
is_prepayment=draft.is_prepayment,
|
is_prepayment=draft.is_prepayment,
|
||||||
|
|
@ -194,158 +147,51 @@ def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> Contr
|
||||||
submit_mode=draft.submit_mode,
|
submit_mode=draft.submit_mode,
|
||||||
goods_lines=draft.goods_lines,
|
goods_lines=draft.goods_lines,
|
||||||
merged_pdf_path=merged_pdf_path,
|
merged_pdf_path=merged_pdf_path,
|
||||||
contract_document_path=draft.contract_document_path,
|
|
||||||
order_no_list=draft.order_no_list,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_merged_pdf(draft: ContractDraft, spec: ContractCreateInput) -> str | None:
|
def build_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[str, Any]:
|
||||||
if draft.source_type != "YYC":
|
|
||||||
return None
|
|
||||||
if spec.submit_mode == "dry_run":
|
|
||||||
# dry_run 只跑到提交前,不校验也不上传 PDF
|
|
||||||
return None
|
|
||||||
|
|
||||||
paths = [str(p).strip() for p in draft.platform_pdf_paths if str(p).strip()]
|
|
||||||
if not paths:
|
|
||||||
if draft.contract_document_path:
|
|
||||||
return None
|
|
||||||
raise PdfMergeUnsupported("YYC 缺少平台合同 PDF(platformPdfPaths 为空)")
|
|
||||||
for path in paths:
|
|
||||||
if not Path(path).exists():
|
|
||||||
raise PdfMergeUnsupported(f"平台合同 PDF 不存在:{path}")
|
|
||||||
if len(paths) == 1:
|
|
||||||
return paths[0]
|
|
||||||
|
|
||||||
output_path = spec.merged_pdf_output or str(
|
|
||||||
Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return ensure_merged_pdf(paths, output_path)
|
|
||||||
except Exception as exc:
|
|
||||||
raise PdfMergeUnsupported(f"YYC 平台合同 PDF 合并失败:{exc}") from exc
|
|
||||||
|
|
||||||
|
|
||||||
def _build_summary(spec: ContractCreateInput) -> dict[str, Any]:
|
|
||||||
return {
|
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,
|
"supplierName": spec.supplier_name,
|
||||||
"contractDate": spec.order_date,
|
"contractDate": spec.order_date,
|
||||||
"sourceType": spec.source_type,
|
"sourceType": spec.source_type,
|
||||||
"orderNoList": list(spec.order_no_list),
|
"contractUrl": spec.contract_url,
|
||||||
"templateName": spec.template_name,
|
"uploadRule": "merge_pdf_before_upload" if spec.source_type == "YYC" else "skip_contract_upload",
|
||||||
"pdfMerged": spec.source_type == "YYC",
|
},
|
||||||
"submitted": True,
|
"artifacts": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _validate_data(data: dict[str, Any], draft: ContractDraft) -> None:
|
def build_fail_payload(spec: ContractCreateInput, exc: ContractCreateError) -> dict[str, Any]:
|
||||||
missing = []
|
return {
|
||||||
for key in ("jobId", "supplierName", "orderDate", "sourceType", "customerCode"):
|
"attemptNo": spec.attempt_no,
|
||||||
if not _str(data.get(key)):
|
"errorCode": exc.error_code,
|
||||||
missing.append(key)
|
"errorSummary": str(exc),
|
||||||
|
"retryable": exc.retryable,
|
||||||
customer_code = draft.customer_code or _str(data.get("customerCode"))
|
"routeRole": exc.route_role,
|
||||||
if customer_code and is_placeholder_customer_code(customer_code):
|
"artifacts": [],
|
||||||
raise ContractCreateError(
|
}
|
||||||
f"customerCode「{customer_code}」是占位符,合同系统客商选择窗搜不到。"
|
|
||||||
"请换成测试环境真实存在的客商操作码,例如 DW0100046361(重庆亿昊药业有限公司)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if draft.submit_mode not in ("dry_run", "submit"):
|
|
||||||
raise ContractCreateError(f"submitMode 非法:{draft.submit_mode}(仅支持 dry_run / submit)")
|
|
||||||
|
|
||||||
if not _str(draft.amount):
|
|
||||||
missing.append("amount")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
float(draft.amount)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise ContractCreateError(f"amount 不是合法数字:{draft.amount}")
|
|
||||||
|
|
||||||
if not draft.goods_lines:
|
|
||||||
raise ContractCreateError("goodsLines 为空,至少需要一行货品明细")
|
|
||||||
for index, line in enumerate(draft.goods_lines, start=1):
|
|
||||||
if not _str(line.goods_code) or not _str(line.goods_name) or line.quantity <= 0:
|
|
||||||
raise ContractCreateError(
|
|
||||||
f"goodsLines[{index}] 缺少 goodsCode/goodsName,或 quantity<=0"
|
|
||||||
)
|
|
||||||
|
|
||||||
if missing:
|
|
||||||
raise ContractCreateError("缺少必填字段:" + ", ".join(missing))
|
|
||||||
|
|
||||||
|
|
||||||
def _require(data: dict[str, Any], key: str) -> str:
|
|
||||||
value = _str(data.get(key))
|
|
||||||
if not value:
|
|
||||||
raise ContractCreateError(f"缺少必填字段:{key}")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _opt(value: Any) -> str | None:
|
|
||||||
value = _str(value)
|
|
||||||
return value or None
|
|
||||||
|
|
||||||
|
|
||||||
def _str(value: Any) -> str:
|
|
||||||
if value is None:
|
|
||||||
return ""
|
|
||||||
return str(value).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _clean(value: Any) -> str:
|
|
||||||
value = _str(value)
|
|
||||||
if len(value) >= 2 and value[0] == "`" and value[-1] == "`":
|
|
||||||
value = value[1:-1].strip()
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def load_input_data(raw: str) -> dict[str, Any]:
|
|
||||||
"""解析入参:优先按 JSON 字符串,其次按 JSON 文件路径。"""
|
|
||||||
raw = (raw or "").strip()
|
|
||||||
if not raw:
|
|
||||||
fallback = default_contract_input_path()
|
|
||||||
if Path(fallback).is_file():
|
|
||||||
print("未传入 -i/--input,使用默认联调入参:", fallback, flush=True)
|
|
||||||
raw = fallback
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
try:
|
|
||||||
obj = json.loads(raw)
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
return obj
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if os.path.isfile(raw):
|
|
||||||
with open(raw, "r", encoding="utf-8") as file:
|
|
||||||
return json.load(file)
|
|
||||||
raise ContractCreateError(f"无法解析入参:既不是合法 JSON,也不是存在的文件路径:{raw}")
|
|
||||||
|
|
||||||
|
|
||||||
def run_contract_create(
|
|
||||||
data: dict[str, Any],
|
|
||||||
*,
|
|
||||||
state_center_url: str,
|
|
||||||
agent_token: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
spec = build_spec_from_data(data)
|
|
||||||
client = StateCenterClient(state_center_url, agent_token)
|
|
||||||
return execute_contract_create(spec, client)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE node.")
|
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("--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("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token"))
|
||||||
parser.add_argument("--input-json", default=os.getenv("CONTRACT_INPUT_JSON"))
|
parser.add_argument("--job-id", required=True)
|
||||||
parser.add_argument("--job-id")
|
parser.add_argument("--supplier-name", required=True)
|
||||||
parser.add_argument("--supplier-name")
|
parser.add_argument("--order-date", required=True)
|
||||||
parser.add_argument("--order-date")
|
|
||||||
parser.add_argument("--source-type", choices=("YYC", "XYY", "OFFLINE"), default="YYC")
|
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("--contract-system-no", default=os.getenv("CONTRACT_SYSTEM_NO"))
|
||||||
parser.add_argument("--attempt-no", type=int, default=1)
|
parser.add_argument("--attempt-no", type=int, default=1)
|
||||||
parser.add_argument("--contract-url", default=os.getenv("CONTRACT_URL", CONTRACT_URL))
|
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("--username", default=os.getenv("CONTRACT_USERNAME", "test002"))
|
||||||
parser.add_argument("--password", default=os.getenv("CONTRACT_PASSWORD", "1"))
|
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("--draft-path", default=os.getenv("CONTRACT_DRAFT_PATH"))
|
||||||
parser.add_argument("--merged-pdf-output", default=os.getenv("MERGED_PDF_OUTPUT"))
|
parser.add_argument("--merged-pdf-output", default=os.getenv("MERGED_PDF_OUTPUT"))
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
@ -353,13 +199,6 @@ def parse_args() -> argparse.Namespace:
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
client = StateCenterClient(args.state_center_url, args.token)
|
|
||||||
|
|
||||||
if args.input_json:
|
|
||||||
spec = build_spec_from_data(load_input_data(args.input_json))
|
|
||||||
else:
|
|
||||||
if not (args.job_id and args.supplier_name and args.order_date):
|
|
||||||
raise SystemExit("缺少 --job-id/--supplier-name/--order-date 或 --input-json")
|
|
||||||
spec = ContractCreateInput(
|
spec = ContractCreateInput(
|
||||||
job_id=args.job_id,
|
job_id=args.job_id,
|
||||||
supplier_name=args.supplier_name,
|
supplier_name=args.supplier_name,
|
||||||
|
|
@ -370,12 +209,13 @@ def main() -> int:
|
||||||
contract_url=args.contract_url,
|
contract_url=args.contract_url,
|
||||||
username=args.username,
|
username=args.username,
|
||||||
password=args.password,
|
password=args.password,
|
||||||
|
open_browser=args.open_browser,
|
||||||
draft_path=args.draft_path,
|
draft_path=args.draft_path,
|
||||||
merged_pdf_output=args.merged_pdf_output,
|
merged_pdf_output=args.merged_pdf_output,
|
||||||
)
|
)
|
||||||
|
client = StateCenterClient(args.state_center_url, args.token)
|
||||||
result = execute_contract_create(spec, client)
|
result = execute_contract_create(spec, client)
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
print(result)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,6 @@ from typing import Any
|
||||||
|
|
||||||
from .contract_flow_steps import ContractGoodsLine
|
from .contract_flow_steps import ContractGoodsLine
|
||||||
|
|
||||||
_PLACEHOLDER_CUSTOMER_PREFIXES = ("CUST-", "TEST-", "MOCK-")
|
|
||||||
|
|
||||||
|
|
||||||
def is_placeholder_customer_code(code: str) -> bool:
|
|
||||||
"""CUST-001 / TEST-xxx 这类占位符在合同系统里搜不到,不能拿去点真页面。"""
|
|
||||||
value = (code or "").strip().upper()
|
|
||||||
return value.startswith(_PLACEHOLDER_CUSTOMER_PREFIXES)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ContractDraft:
|
class ContractDraft:
|
||||||
|
|
@ -31,9 +23,7 @@ class ContractDraft:
|
||||||
contract_background: str = ""
|
contract_background: str = ""
|
||||||
contract_nature: str = "主合同"
|
contract_nature: str = "主合同"
|
||||||
amount_currency: str = "人民币"
|
amount_currency: str = "人民币"
|
||||||
amount_type: str = "合同标的金额"
|
|
||||||
amount: str = ""
|
amount: str = ""
|
||||||
contract_title: str = ""
|
|
||||||
contract_end_date: str = ""
|
contract_end_date: str = ""
|
||||||
is_major_contract: str = "否"
|
is_major_contract: str = "否"
|
||||||
is_prepayment: str = "否"
|
is_prepayment: str = "否"
|
||||||
|
|
@ -46,8 +36,6 @@ class ContractDraft:
|
||||||
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
||||||
platform_pdf_paths: list[str] = field(default_factory=list)
|
platform_pdf_paths: list[str] = field(default_factory=list)
|
||||||
merged_pdf_path: str | None = None
|
merged_pdf_path: str | None = None
|
||||||
contract_document_path: str = ""
|
|
||||||
order_no_list: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
def load_contract_draft(path: str | Path) -> ContractDraft:
|
def load_contract_draft(path: str | Path) -> ContractDraft:
|
||||||
|
|
@ -64,9 +52,7 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
||||||
approval_no=str(item.get("approvalNo", "")),
|
approval_no=str(item.get("approvalNo", "")),
|
||||||
spec=str(item.get("spec", "")),
|
spec=str(item.get("spec", "")),
|
||||||
unit=str(item.get("unit", "")),
|
unit=str(item.get("unit", "")),
|
||||||
quantity=float(item.get("quantity", 0) or 0),
|
quantity=float(item.get("quantity", 0)),
|
||||||
mdm_code=str(item.get("mdmCode", "")),
|
|
||||||
unit_price=str(item.get("unitPrice", "")),
|
|
||||||
)
|
)
|
||||||
for item in data.get("goodsLines", [])
|
for item in data.get("goodsLines", [])
|
||||||
]
|
]
|
||||||
|
|
@ -83,9 +69,7 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
||||||
contract_background=str(data.get("contractBackground", "")),
|
contract_background=str(data.get("contractBackground", "")),
|
||||||
contract_nature=str(data.get("contractNature", "主合同")),
|
contract_nature=str(data.get("contractNature", "主合同")),
|
||||||
amount_currency=str(data.get("amountCurrency", "人民币")),
|
amount_currency=str(data.get("amountCurrency", "人民币")),
|
||||||
amount_type=str(data.get("amountType", "合同标的金额")),
|
|
||||||
amount=str(data.get("amount", "")),
|
amount=str(data.get("amount", "")),
|
||||||
contract_title=str(data.get("contractTitle") or data.get("docSubject") or ""),
|
|
||||||
contract_end_date=str(data.get("contractEndDate", "")),
|
contract_end_date=str(data.get("contractEndDate", "")),
|
||||||
is_major_contract=str(data.get("isMajorContract", "否")),
|
is_major_contract=str(data.get("isMajorContract", "否")),
|
||||||
is_prepayment=str(data.get("isPrepayment", "否")),
|
is_prepayment=str(data.get("isPrepayment", "否")),
|
||||||
|
|
@ -98,8 +82,4 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
||||||
goods_lines=goods_lines,
|
goods_lines=goods_lines,
|
||||||
platform_pdf_paths=[str(path) for path in data.get("platformPdfPaths", [])],
|
platform_pdf_paths=[str(path) for path in data.get("platformPdfPaths", [])],
|
||||||
merged_pdf_path=data.get("mergedPdfPath"),
|
merged_pdf_path=data.get("mergedPdfPath"),
|
||||||
contract_document_path=str(
|
|
||||||
data.get("contractDocumentPath") or data.get("contractBodyPath") or ""
|
|
||||||
),
|
|
||||||
order_no_list=[str(x) for x in data.get("orderNoList", [])],
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"jobId": "YYC-重庆亿昊药业-2fa0-20260812-16abff",
|
|
||||||
"supplierName": "重庆亿昊药业有限公司",
|
|
||||||
"orderDate": "2026-08-12",
|
|
||||||
"sourceType": "YYC",
|
|
||||||
"templateType": "NON_FORMAT",
|
|
||||||
"templateName": "非格式--采购协议",
|
|
||||||
"customerCode": "DW0100046361",
|
|
||||||
"customerName": "重庆亿昊药业有限公司",
|
|
||||||
"contractCategory": "其他协议(采购)",
|
|
||||||
"settlementType": "付款",
|
|
||||||
"contractBackground": "自动化测试合同起草",
|
|
||||||
"contractNature": "主合同",
|
|
||||||
"contractTitle": "test",
|
|
||||||
"amountCurrency": "人民币",
|
|
||||||
"amountType": "合同标的金额",
|
|
||||||
"amount": "50000.00",
|
|
||||||
"contractEndDate": "2026-12-31",
|
|
||||||
"isMajorContract": "否",
|
|
||||||
"isPrepayment": "否",
|
|
||||||
"belongYear": "2026",
|
|
||||||
"paymentMethod": "电汇",
|
|
||||||
"paymentDays": "30",
|
|
||||||
"contractStatus": "正式合同",
|
|
||||||
"containsBloodProducts": "否",
|
|
||||||
"submitMode": "dry_run",
|
|
||||||
"orderNoList": [],
|
|
||||||
"platformPdfPaths": [],
|
|
||||||
"goodsLines": [
|
|
||||||
{
|
|
||||||
"operationCode": "ADD",
|
|
||||||
"goodsCode": "FQX0100118047",
|
|
||||||
"goodsName": "盐酸托烷司琼注射液",
|
|
||||||
"approvalNo": "苏20160372",
|
|
||||||
"spec": "5ml:5mg*5支",
|
|
||||||
"unit": "盒",
|
|
||||||
"quantity": 10,
|
|
||||||
"mdmCode": "",
|
|
||||||
"unitPrice": "50.00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateCenterUrl": "http://127.0.0.1:8000",
|
|
||||||
"agentToken": "dev-agent-token"
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"supplierName": "重庆亿昊药业有限公司",
|
"supplierName": "小药药供应商",
|
||||||
"orderDate": "2026-08-11",
|
"orderDate": "2026-08-11",
|
||||||
"sourceType": "XYY",
|
"sourceType": "XYY",
|
||||||
"templateType": "FORMAT",
|
"templateType": "FORMAT",
|
||||||
"templateName": "购销协议书(2025)",
|
"templateName": "购销协议书(2025)",
|
||||||
"customerCode": "DW0100046361",
|
"customerCode": "CUST-XYY-001",
|
||||||
"customerName": "重庆亿昊药业有限公司",
|
"customerName": "小药药供应商",
|
||||||
"contractCategory": "采购协议",
|
"contractCategory": "采购协议",
|
||||||
"settlementType": "付款",
|
"settlementType": "付款",
|
||||||
"contractBackground": "按小药药订单自动起草采购协议",
|
"contractBackground": "按小药药订单自动起草采购协议",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"supplierName": "重庆亿昊药业有限公司",
|
"supplierName": "重庆亿昊药业",
|
||||||
"orderDate": "2026-08-11",
|
"orderDate": "2026-08-11",
|
||||||
"sourceType": "YYC",
|
"sourceType": "YYC",
|
||||||
"templateType": "NON_FORMAT",
|
"templateType": "NON_FORMAT",
|
||||||
"templateName": "非格式--采购协议",
|
"templateName": "非格式--采购协议",
|
||||||
"customerCode": "DW0100046361",
|
"customerCode": "CUST-YYC-001",
|
||||||
"customerName": "重庆亿昊药业有限公司",
|
"customerName": "重庆亿昊药业",
|
||||||
"contractCategory": "采购协议",
|
"contractCategory": "采购协议",
|
||||||
"settlementType": "付款",
|
"settlementType": "付款",
|
||||||
"contractBackground": "按医药城订单自动起草采购协议",
|
"contractBackground": "按医药城订单自动起草采购协议",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
pypdf==5.1.0
|
pypdf==5.1.0
|
||||||
selenium>=4.15.0
|
|
||||||
webdriver-manager>=4.0.0
|
|
||||||
requests>=2.31.0
|
|
||||||
|
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
"""Shared ACCEPT report for every RPA flow (any platform / page).
|
|
||||||
|
|
||||||
New flows (ORDER_FETCH, REBATE_*, M5, ...) must use this instead of
|
|
||||||
reading screenshots. Screenshots stay on disk for humans; this text is
|
|
||||||
the agent oracle.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Protocol
|
|
||||||
|
|
||||||
from .errors import NodeError
|
|
||||||
|
|
||||||
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
SCREENSHOT_ROOT = PACKAGE_ROOT / "screenshots"
|
|
||||||
|
|
||||||
|
|
||||||
class HasScreenshot(Protocol):
|
|
||||||
def screenshot(self, name: str) -> str: ...
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AcceptLine:
|
|
||||||
step: str
|
|
||||||
title: str
|
|
||||||
ok: bool
|
|
||||||
facts: dict[str, str]
|
|
||||||
mismatches: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
def safe_flow_name(flow: str) -> str:
|
|
||||||
text = (flow or "default").strip() or "default"
|
|
||||||
return "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in text)
|
|
||||||
|
|
||||||
|
|
||||||
def artifact_dir(flow: str) -> Path:
|
|
||||||
return SCREENSHOT_ROOT / safe_flow_name(flow)
|
|
||||||
|
|
||||||
|
|
||||||
def evidence_shot(browser: HasScreenshot, name: str, *, failed: bool = False) -> str:
|
|
||||||
"""Save a screenshot only on failure. Set RPA_SCREENSHOT=always to keep every frame."""
|
|
||||||
mode = (os.environ.get("RPA_SCREENSHOT") or "fail").strip().lower()
|
|
||||||
if not failed and mode != "always":
|
|
||||||
return ""
|
|
||||||
return browser.screenshot(name)
|
|
||||||
|
|
||||||
|
|
||||||
def accept_log_path(flow: str = "default") -> Path:
|
|
||||||
override = os.environ.get("RPA_ACCEPT_LOG")
|
|
||||||
if override:
|
|
||||||
return Path(override)
|
|
||||||
return artifact_dir(flow) / "ACCEPT.txt"
|
|
||||||
|
|
||||||
|
|
||||||
def field_match(actual: str, expected: str) -> bool:
|
|
||||||
actual_text = (actual or "").strip()
|
|
||||||
expected_text = (expected or "").strip()
|
|
||||||
if not expected_text:
|
|
||||||
return True
|
|
||||||
if actual_text == expected_text:
|
|
||||||
return True
|
|
||||||
compact_a = actual_text.replace(" ", "").replace(",", "")
|
|
||||||
compact_e = expected_text.replace(" ", "").replace(",", "")
|
|
||||||
if compact_a == compact_e or compact_a.replace("-", "") == compact_e.replace("-", ""):
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
return float(compact_a) == float(compact_e)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
yes = {"是", "1", "Y", "YES", "TRUE"}
|
|
||||||
no = {"否", "2", "0", "N", "NO", "FALSE"}
|
|
||||||
if compact_a.upper() in yes and compact_e.upper() in yes:
|
|
||||||
return True
|
|
||||||
if compact_a.upper() in no and compact_e.upper() in no:
|
|
||||||
return True
|
|
||||||
return compact_e in compact_a or compact_a in compact_e
|
|
||||||
|
|
||||||
|
|
||||||
def format_accept(line: AcceptLine) -> str:
|
|
||||||
status = "ok" if line.ok else "FAIL"
|
|
||||||
facts = " ".join(f"{key}={value}" for key, value in line.facts.items())
|
|
||||||
extra = f" {' | '.join(line.mismatches)}" if line.mismatches else ""
|
|
||||||
return f"ACCEPT {line.step} {status} {line.title} {facts}{extra}".rstrip()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AcceptanceReport:
|
|
||||||
"""DOM / page-state checklist. One instance per flow run."""
|
|
||||||
|
|
||||||
flow: str = "default"
|
|
||||||
lines: list[AcceptLine] = field(default_factory=list)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self.lines = []
|
|
||||||
|
|
||||||
def check(
|
|
||||||
self,
|
|
||||||
step: str,
|
|
||||||
title: str,
|
|
||||||
pairs: list[tuple[str, object, object]],
|
|
||||||
*,
|
|
||||||
browser: HasScreenshot | None = None,
|
|
||||||
screenshot_on_fail: str = "",
|
|
||||||
) -> None:
|
|
||||||
facts: dict[str, str] = {}
|
|
||||||
mismatches: list[str] = []
|
|
||||||
for name, actual, expected in pairs:
|
|
||||||
actual_text = "" if actual is None else str(actual).strip()
|
|
||||||
expected_text = "" if expected is None else str(expected).strip()
|
|
||||||
facts[name] = actual_text or "(empty)"
|
|
||||||
if expected_text and not field_match(actual_text, expected_text):
|
|
||||||
mismatches.append(f"{name} expected={expected_text!r} actual={actual_text!r}")
|
|
||||||
ok = not mismatches
|
|
||||||
self.lines.append(AcceptLine(step, title, ok, facts, mismatches))
|
|
||||||
print(format_accept(self.lines[-1]), flush=True)
|
|
||||||
self.dump()
|
|
||||||
if ok:
|
|
||||||
return
|
|
||||||
if browser is not None and screenshot_on_fail:
|
|
||||||
evidence_shot(browser, screenshot_on_fail, failed=True)
|
|
||||||
raise NodeError(f"ACCEPT {step} FAIL " + "; ".join(mismatches))
|
|
||||||
|
|
||||||
def note(
|
|
||||||
self,
|
|
||||||
step: str,
|
|
||||||
title: str,
|
|
||||||
facts: dict[str, object],
|
|
||||||
*,
|
|
||||||
ok: bool = True,
|
|
||||||
raise_error: bool = True,
|
|
||||||
) -> None:
|
|
||||||
rendered = {key: ("" if value is None else str(value).strip()) or "(empty)" for key, value in facts.items()}
|
|
||||||
self.lines.append(AcceptLine(step, title, ok, rendered, [] if ok else ["failed"]))
|
|
||||||
print(format_accept(self.lines[-1]), flush=True)
|
|
||||||
self.dump()
|
|
||||||
if not ok and raise_error:
|
|
||||||
raise NodeError(f"ACCEPT {step} FAIL {title} " + " ".join(f"{k}={v}" for k, v in rendered.items()))
|
|
||||||
|
|
||||||
def dump(self) -> None:
|
|
||||||
path = accept_log_path(self.flow)
|
|
||||||
try:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(self.summary_text(), encoding="utf-8")
|
|
||||||
except OSError as exc:
|
|
||||||
print(f"ACCEPT log write skipped: {exc}", flush=True)
|
|
||||||
|
|
||||||
def summary_text(self) -> str:
|
|
||||||
passed = sum(1 for line in self.lines if line.ok)
|
|
||||||
rows = [f"ACCEPT SUMMARY {self.flow} {passed}/{len(self.lines)} ok"]
|
|
||||||
rows.extend(format_accept(line) for line in self.lines)
|
|
||||||
return "\n".join(rows) + "\n"
|
|
||||||
|
|
@ -23,9 +23,3 @@ class PdfMergeUnsupported(NodeError):
|
||||||
error_code = "CAP_PDF_MERGE_UNSUPPORTED"
|
error_code = "CAP_PDF_MERGE_UNSUPPORTED"
|
||||||
retryable = False
|
retryable = False
|
||||||
route_role = "OPS"
|
route_role = "OPS"
|
||||||
|
|
||||||
|
|
||||||
class UnknownRpaError(NodeError):
|
|
||||||
error_code = "RPA_UNKNOWN_ERROR"
|
|
||||||
retryable = True
|
|
||||||
route_role = "OPS"
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
"""Generic start → runner → complete/fail shell."""
|
"""Generic start → runner → complete/fail shell."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, Optional
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from ..callback_client import StateCenterClient
|
from ..callback_client import StateCenterClient
|
||||||
from .errors import NodeError
|
from .errors import NodeError
|
||||||
from .payloads import build_complete_payload, build_fail_payload
|
from .payloads import build_complete_payload, build_fail_payload
|
||||||
|
|
||||||
Runner = Callable[[], Optional[Dict[str, Any]]]
|
Runner = Callable[[], dict[str, Any] | None]
|
||||||
|
|
||||||
|
|
||||||
def execute_node(
|
def execute_node(
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,116 @@
|
||||||
{
|
{
|
||||||
"login": {
|
"knownLoginSelectors": {
|
||||||
"username": "input[name='j_username']",
|
"username": "input[name='j_username']",
|
||||||
"password": "input[name='j_password']",
|
"password": "input[name='j_password']",
|
||||||
"submit": "input[name='btn_submit']",
|
"submit": "input[name='btn_submit']"
|
||||||
"submitLink": "text=登录"
|
|
||||||
},
|
},
|
||||||
"navigation": {
|
"candidateSelectors": {
|
||||||
"draftPage": "http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/draft/draft.jsp?isSimpleCategory=false&mainModelName=com.landray.kmss.km.agreement.model.KmAgreementApply&modelName=com.landray.kmss.km.agreement.model.KmAgreementTmpl",
|
"contractManagementMenu": [
|
||||||
"templatePrefix": "非格式-采购协议"
|
"text=合同管理",
|
||||||
},
|
"aria=合同管理"
|
||||||
"form": {
|
],
|
||||||
"customerCode": "input[name='extendDataFormInfo.value(fd_customeropcode)']",
|
"contractDraftMenu": [
|
||||||
"customerName": "input[name='extendDataFormInfo.value(fd_customername)']",
|
"text=合同起草",
|
||||||
"contractCategory": "xform:extendDataFormInfo.value(fd_contract_cate_type)",
|
"aria=合同起草"
|
||||||
"coverTemplate": "xform:extendDataFormInfo.value(fd_contract_cover_type)",
|
],
|
||||||
"settlementType": "select#fdSettype",
|
"recentNonFormatPurchaseTemplate": [
|
||||||
"contractBackground": "textarea[name='extendDataFormInfo.value(fd_desc_info)']",
|
"text=非格式--采购协议",
|
||||||
"contractNatureMain": "input[name='fdType'][value='00']",
|
"text=非格式--采购协议...",
|
||||||
"contractNatureSub": "input[name='fdType'][value='01']",
|
"title=非格式--采购协议"
|
||||||
"docSubject": "input[name='docSubject']",
|
],
|
||||||
"amountType": "xform:extendDataFormInfo.value(fd_all_amount_type)",
|
"customerCode": [
|
||||||
"amount": "input[name='fdAccount']",
|
"label=客户编码",
|
||||||
"signDate": "input[name='fdSigndate']",
|
"input[name='fdCustomerCode']",
|
||||||
"beginDate": "input[name='fdEffedate']",
|
"input[placeholder*='客户编码']"
|
||||||
"endDate": "input[name='fdEnddate']",
|
],
|
||||||
"isMajorContract": "input[name='extendDataFormInfo.value(fd_is_major_contract)']",
|
"customerName": [
|
||||||
"isPrepayment": "input[name='extendDataFormInfo.value(fd_advance_amout)']",
|
"label=客户名称",
|
||||||
"belongYear": "input[name='extendDataFormInfo.value(fd_contract_year_text)']",
|
"input[name='fdCustomerName']",
|
||||||
"paymentMethod": "input[name='extendDataFormInfo.value(fd_paytype)']",
|
"input[placeholder*='客户名称']"
|
||||||
"paymentDays": "input[name='extendDataFormInfo.value(fd_pay_time_limit)']",
|
],
|
||||||
"contractStatus": "input[name='extendDataFormInfo.value(fd_suconstatus)']",
|
"contractVersionType": [
|
||||||
"containsBloodProducts": "input[name='extendDataFormInfo.value(fd_3d1ce3953f1162)']",
|
"label=合同版本类型",
|
||||||
"selectGoodsButton": "div[title='选择商品']",
|
"select[name='fdTemplateType']"
|
||||||
"goodsDialogFrame": "iframe[src*='EopBasedataMaterial']",
|
],
|
||||||
"goodsOperationCode": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_opcode)']",
|
"contractCategory": [
|
||||||
"goodsCode": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_opcode)']",
|
"label=合同类别",
|
||||||
"goodsName": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodsname)']",
|
"select[name='fdContractCategory']"
|
||||||
"goodsMdmCode": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_mdmCode)']",
|
],
|
||||||
"goodsApprovalNo": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_registdocno)']",
|
"settlementType": [
|
||||||
"goodsSpec": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodstype)']",
|
"label=结算类型",
|
||||||
"goodsUnit": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodsunit)']",
|
"select[name='fdSettlementType']"
|
||||||
"goodsQuantity": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)']",
|
],
|
||||||
"goodsUnitPrice": "input[name='extendDataFormInfo.value(fd_detail1.0.fd_unitprice)']",
|
"contractBackground": [
|
||||||
"submit": "text=提交"
|
"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=合同编号"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,790 +0,0 @@
|
||||||
{
|
|
||||||
"LineList": [
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143010000000/EPin_Flow_In",
|
|
||||||
"Px": "697",
|
|
||||||
"Py": "314.931"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608141000000001/EPin_Flow_Out",
|
|
||||||
"Px": "547",
|
|
||||||
"Py": "296"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149010000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143020000000/EPin_Flow_In",
|
|
||||||
"Px": "977",
|
|
||||||
"Py": "314.931"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143010000000/EPin_Flow_Out",
|
|
||||||
"Px": "827",
|
|
||||||
"Py": "314.931"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149020000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143030000000/EPin_Flow_In",
|
|
||||||
"Px": "1257",
|
|
||||||
"Py": "320"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143020000000/EPin_Flow_Out",
|
|
||||||
"Px": "1107",
|
|
||||||
"Py": "314.931"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149030000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143040000000/EPin_Flow_In",
|
|
||||||
"Px": "417",
|
|
||||||
"Py": "520"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143030000000/EPin_Flow_Out",
|
|
||||||
"Px": "1387",
|
|
||||||
"Py": "320"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149040000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143050000000/EPin_Flow_In",
|
|
||||||
"Px": "697",
|
|
||||||
"Py": "520"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143040000000/EPin_Flow_Out",
|
|
||||||
"Px": "547",
|
|
||||||
"Py": "520"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149050000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143060000000/EPin_Flow_In",
|
|
||||||
"Px": "417",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143050000000/EPin_Flow_Out",
|
|
||||||
"Px": "827",
|
|
||||||
"Py": "520"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149060000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143070000000/EPin_Flow_In",
|
|
||||||
"Px": "697",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143060000000/EPin_Flow_Out",
|
|
||||||
"Px": "547",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149070000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143080000000/EPin_Flow_In",
|
|
||||||
"Px": "977",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143070000000/EPin_Flow_Out",
|
|
||||||
"Px": "827",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149080000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143090000000/EPin_Flow_In",
|
|
||||||
"Px": "1257",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143080000000/EPin_Flow_Out",
|
|
||||||
"Px": "1107",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149090000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143100000000/EPin_Flow_In",
|
|
||||||
"Px": "1537",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143090000000/EPin_Flow_Out",
|
|
||||||
"Px": "1387",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149100000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143110000000/EPin_Flow_In",
|
|
||||||
"Px": "417",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143100000000/EPin_Flow_Out",
|
|
||||||
"Px": "1667",
|
|
||||||
"Py": "720"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149110000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143120000000/EPin_Flow_In",
|
|
||||||
"Px": "697",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143110000000/EPin_Flow_Out",
|
|
||||||
"Px": "547",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149120000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143130000000/EPin_Flow_In",
|
|
||||||
"Px": "977",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143120000000/EPin_Flow_Out",
|
|
||||||
"Px": "827",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149130000000",
|
|
||||||
"LineType": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LineColor": "255, 255, 255",
|
|
||||||
"LineCommented": 0,
|
|
||||||
"LineEnd": {
|
|
||||||
"Pidx": "202608143140000000/EPin_Flow_In",
|
|
||||||
"Px": "1257",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineNote": "",
|
|
||||||
"LineSplit": {
|
|
||||||
"Px": "",
|
|
||||||
"Py": ""
|
|
||||||
},
|
|
||||||
"LineStart": {
|
|
||||||
"Pidx": "202608143130000000/EPin_Flow_Out",
|
|
||||||
"Px": "1107",
|
|
||||||
"Py": "920"
|
|
||||||
},
|
|
||||||
"LineTag": "202608149140000000",
|
|
||||||
"LineType": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"NodeList": [
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": "",
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "420",
|
|
||||||
"NodePy": "280",
|
|
||||||
"NodeSubType": "EnumWidget_Begin",
|
|
||||||
"NodeTag": "202608141000000001",
|
|
||||||
"NodeTitle": "开始"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PBCode\": \"cHJpbnQoIltTMDEg5Yid5aeL5YyW5bm25Zue6LCDIHN0YXJ0XSIpCmltcG9ydCBzeXMsIG9zLCBqc29uCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKX1JFUE9fUk9PVCA9IG5leHQoCiAgICBwIGZvciBwIGluIFBhdGgoX19maWxlX18pLnJlc29sdmUoKS5wYXJlbnRzCiAgICBpZiAocCAvICJycGFfeWlzYWlxaSIgLyAiX19pbml0X18ucHkiKS5leGlzdHMoKQopCmlmIHN0cihfUkVQT19ST09UKSBub3QgaW4gc3lzLnBhdGg6CiAgICBzeXMucGF0aC5pbnNlcnQoMCwgc3RyKF9SRVBPX1JPT1QpKQoKaW1wb3J0IHJwYV95aXNhaXFpLl9mbG93X3N0YXRlIGFzIF9zdApmcm9tIHJwYV95aXNhaXFpLmNvbnRyYWN0X2NyZWF0ZV9ub2RlIGltcG9ydCBDb250cmFjdENyZWF0ZUVycm9yLCBsb2FkX2lucHV0X2RhdGEsIGJ1aWxkX3NwZWNfZnJvbV9kYXRhCmZyb20gcnBhX3lpc2FpcWkuY2FsbGJhY2tfY2xpZW50IGltcG9ydCBTdGF0ZUNlbnRlckNsaWVudApmcm9tIHJwYV95aXNhaXFpLmFwYV9icm93c2VyX2FkYXB0ZXIgaW1wb3J0IEFwYUJyb3dzZXJBZGFwdGVyCgoKZGVmIF9maXJzdCgqdmFsdWVzKToKICAgIGZvciB2YWx1ZSBpbiB2YWx1ZXM6CiAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmUgYW5kIHN0cih2YWx1ZSkuc3RyaXAoKToKICAgICAgICAgICAgcmV0dXJuIHN0cih2YWx1ZSkuc3RyaXAoKQogICAgcmV0dXJuICIiCgoKcmF3ID0gKHNlbGYuaW5wdXRfYXJnIG9yICIiKS5zdHJpcCgpCmlmIG5vdCByYXc6CiAgICByYXcgPSBvcy5nZXRlbnYoIkNPTlRSQUNUX0lOUFVUX0pTT04iLCAiIikuc3RyaXAoKQppZiBub3QgcmF3OgogICAgcmF3ID0gc3RyKF9SRVBPX1JPT1QgLyAicnBhX3lpc2FpcWkiIC8gImV4YW1wbGVzIiAvICJjb250cmFjdF9jcmVhdGVfaW5wdXRfZHJ5X3J1bi5qc29uIikKICAgIHByaW50KCLmnKrkvKDlhaUgLWkvLS1pbnB1dO+8jOS9v+eUqOm7mOiupOiBlOiwg+WFpeWPgu+8miIsIHJhdykKaWYgbm90IHJhdzoKICAgIHJhaXNlIENvbnRyYWN0Q3JlYXRlRXJyb3IoIue8uuWwkeS4muWKoeWFpeWPgu+8muivt+mAmui/hyAtaS8tLWlucHV0IOS8oOWFpSBKU09OIOWtl+espuS4suaIluaWh+S7tui3r+W+hCIpCgpkYXRhID0gbG9hZF9pbnB1dF9kYXRhKHJhdykKc3RhdGVfY2VudGVyX3VybCA9IF9maXJzdCgKICAgIGRhdGEuZ2V0KCJzdGF0ZUNlbnRlclVybCIpLCBkYXRhLmdldCgic3RhdGVDZW50ZXJVUkwiKSwKICAgIG9zLmdldGVudigiU1RBVEVfQ0VOVEVSX1VSTCIpLCAiaHR0cDovLzEyNy4wLjAuMTo4MDAwIiwKKQphZ2VudF90b2tlbiA9IF9maXJzdChkYXRhLmdldCgiYWdlbnRUb2tlbiIpLCBvcy5nZXRlbnYoIkFHRU5UX1RPS0VOIiksICJkZXYtYWdlbnQtdG9rZW4iKQoKX3N0LnNwZWMgPSBidWlsZF9zcGVjX2Zyb21fZGF0YShkYXRhKQpfc3QuY2xpZW50ID0gU3RhdGVDZW50ZXJDbGllbnQoc3RhdGVfY2VudGVyX3VybCwgYWdlbnRfdG9rZW4pCl9zdC5jdHggPSBfc3Quc3BlYy5mbG93X2NvbnRleHQKX3N0LmJyb3dzZXIgPSBBcGFCcm93c2VyQWRhcHRlcih1cmw9X3N0LmN0eC5jb250cmFjdF91cmwpCl9zdC5pc19kcnlfcnVuID0gKF9zdC5zcGVjLnN1Ym1pdF9tb2RlID09ICJkcnlfcnVuIikKCmlmIG5vdCBfc3QuaXNfZHJ5X3J1biBhbmQgb3MuZ2V0ZW52KCJSUEFfQ0FMTEJBQ0tfU1RBVEVfQ0VOVEVSIikgPT0gIjEiOgogICAgX3N0LmNsaWVudC5zdGFydF9ub2RlKF9zdC5zcGVjLmpvYl9pZCwgIkNPTlRSQUNUX0NSRUFURSIp\",\n \"PImport\": \"\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzAxIOWIneWni+WMluW5tuWbnuiwgyBzdGFydA==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "700",
|
|
||||||
"NodePy": "280",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143010000000",
|
|
||||||
"NodeTitle": "插入代码块#1-13"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PBCode\": \"cHJpbnQoIltTMDIg5omT5byA5ZCI5ZCM57O757ufXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBvcGVuX2NvbnRyYWN0X3N5c3RlbQoKb3Blbl9jb250cmFjdF9zeXN0ZW0oX3N0LmJyb3dzZXIsIF9zdC5jdHgp\",\n \"PImport\": \"\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzAyIOaJk+W8gOWQiOWQjOezu+e7nw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "980",
|
|
||||||
"NodePy": "280",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143020000000",
|
|
||||||
"NodeTitle": "插入代码块#1-12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDMg55m75b2V5ZCI5ZCM57O757ufXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBsb2dpbl9jb250cmFjdF9zeXN0ZW0KCmxvZ2luX2NvbnRyYWN0X3N5c3RlbShfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzAzIOeZu+W9leWQiOWQjOezu+e7nw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "1260",
|
|
||||||
"NodePy": "280",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143030000000",
|
|
||||||
"NodeTitle": "插入代码块#1-11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDQg6L+b5YWl5ZCI5ZCM6LW36I2JXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBlbnRlcl9jb250cmFjdF9jcmVhdGUKCmVudGVyX2NvbnRyYWN0X2NyZWF0ZShfc3QuYnJvd3Nlcik=\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA0IOi/m+WFpeWQiOWQjOi1t+iNiQ==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "420",
|
|
||||||
"NodePy": "480",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143040000000",
|
|
||||||
"NodeTitle": "插入代码块#1-10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDUg6YCJ5oup5qih5p2/XSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBzZWxlY3RfY29udHJhY3RfdGVtcGxhdGUKCnNlbGVjdF9jb250cmFjdF90ZW1wbGF0ZShfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA1IOmAieaLqeaooeadvw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "700",
|
|
||||||
"NodePy": "480",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143050000000",
|
|
||||||
"NodeTitle": "插入代码块#1-9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDYg5aGr5YaZ562+57qm5pa5XSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X3NpZ25hdG9yeV9pbmZvCgpmaWxsX2NvbnRyYWN0X3NpZ25hdG9yeV9pbmZvKF9zdC5icm93c2VyLCBfc3QuY3R4KQ==\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA2IOWhq+WGmeetvue6puaWuQ==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "420",
|
|
||||||
"NodePy": "680",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143060000000",
|
|
||||||
"NodeTitle": "插入代码块#1-8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDcg5aGr5YaZ5Z+656GA5L+h5oGvXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X2Jhc2ljX2luZm8KCmZpbGxfY29udHJhY3RfYmFzaWNfaW5mbyhfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA3IOWhq+WGmeWfuuehgOS/oeaBrw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "700",
|
|
||||||
"NodePy": "680",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143070000000",
|
|
||||||
"NodeTitle": "插入代码块#1-7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDgg5aGr5YaZ6YeR6aKd5pel5pyfXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X2Ftb3VudF9hbmRfZGF0ZXMKCmZpbGxfY29udHJhY3RfYW1vdW50X2FuZF9kYXRlcyhfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA4IOWhq+WGmemHkemineaXpeacnw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "980",
|
|
||||||
"NodePy": "680",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143080000000",
|
|
||||||
"NodeTitle": "插入代码块#1-6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMDkg5aGr5YaZ6LSn5ZOB5piO57uGXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X2dvb2RzX2xpbmVzCgpmaWxsX2NvbnRyYWN0X2dvb2RzX2xpbmVzKF9zdC5icm93c2VyLCBfc3QuY3R4KQ==\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzA5IOWhq+WGmei0p+WTgeaYjue7hg==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "1260",
|
|
||||||
"NodePy": "680",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143090000000",
|
|
||||||
"NodeTitle": "插入代码块#1-5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTAg5aGr5YaZ5LuY5qy+5L+h5oGvXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBmaWxsX2NvbnRyYWN0X3BheW1lbnRfaW5mbwoKZmlsbF9jb250cmFjdF9wYXltZW50X2luZm8oX3N0LmJyb3dzZXIsIF9zdC5jdHgp\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzEwIOWhq+WGmeS7mOasvuS/oeaBrw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "1540",
|
|
||||||
"NodePy": "680",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143100000000",
|
|
||||||
"NodeTitle": "插入代码块#1-4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTEg5LiK5Lyg5ZCI5ZCMUERGXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCB1cGxvYWRfY29udHJhY3RfZG9jdW1lbnRfaWZfbmVlZGVkCgp1cGxvYWRfY29udHJhY3RfZG9jdW1lbnRfaWZfbmVlZGVkKF9zdC5icm93c2VyLCBfc3QuY3R4KQ==\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzExIOS4iuS8oOWQiOWQjFBERg==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "420",
|
|
||||||
"NodePy": "880",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143110000000",
|
|
||||||
"NodeTitle": "插入代码块#1-3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTIg5o+Q5Lqk6YCB5a6hXSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCBzdWJtaXRfY29udHJhY3RfZm9yX2FwcHJvdmFsCgpfc3Quc3VibWl0dGVkID0gc3VibWl0X2NvbnRyYWN0X2Zvcl9hcHByb3ZhbChfc3QuYnJvd3NlciwgX3N0LmN0eCk=\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzEyIOaPkOS6pOmAgeWuoQ==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "700",
|
|
||||||
"NodePy": "880",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143120000000",
|
|
||||||
"NodeTitle": "插入代码块#1-2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTMg6K+75Y+W5ZCI5ZCM57yW5Y+3XSIpCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKZnJvbSBycGFfeWlzYWlxaS5jb250cmFjdF9mbG93X3N0ZXBzIGltcG9ydCByZWFkX2NvbnRyYWN0X251bWJlcgoKaWYgX3N0LnN1Ym1pdHRlZDoKICAgIF9zdC5jb250cmFjdF9ubyA9IHJlYWRfY29udHJhY3RfbnVtYmVyKF9zdC5icm93c2VyKQ==\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzEzIOivu+WPluWQiOWQjOe8luWPtw==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "980",
|
|
||||||
"NodePy": "880",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143130000000",
|
|
||||||
"NodeTitle": "插入代码块#1-1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NodePanelData": {
|
|
||||||
"ExpandedGroups": [
|
|
||||||
],
|
|
||||||
"InputProperty": {
|
|
||||||
"mouse_x": 0,
|
|
||||||
"mouse_y": 0
|
|
||||||
},
|
|
||||||
"PubProperty": "",
|
|
||||||
"RetPins": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"NodeProperty": {
|
|
||||||
"EP_CONTROL": {
|
|
||||||
"EP_CONTROL_IMPORT": "{\n \"PImport\": \"\",\n \"PBCode\": \"cHJpbnQoIltTMTQg5Zue6LCDIGNvbXBsZXRlXSIpCmltcG9ydCBqc29uCmltcG9ydCBycGFfeWlzYWlxaS5fZmxvd19zdGF0ZSBhcyBfc3QKCmlmIG5vdCBfc3QuaXNfZHJ5X3J1biBhbmQgX3N0LmNvbnRyYWN0X25vIGFuZCBvcy5nZXRlbnYoIlJQQV9DQUxMQkFDS19TVEFURV9DRU5URVIiKSA9PSAiMSI6CiAgICBfc3QuY2xpZW50LmNvbXBsZXRlX25vZGUoX3N0LnNwZWMuam9iX2lkLCAiQ09OVFJBQ1RfQ1JFQVRFIiwgewogICAgICAgICJidXNpbmVzc1JlZnMiOiB7ImNvbnRyYWN0U3lzdGVtTm8iOiBfc3QuY29udHJhY3Rfbm99LAogICAgfSkKcHJpbnQoanNvbi5kdW1wcyh7InN0YXR1cyI6ICJkb25lIiwgImNvbnRyYWN0U3lzdGVtTm8iOiBfc3QuY29udHJhY3Rfbm99LCBlbnN1cmVfYXNjaWk9RmFsc2UpKQ==\"\n}\n"
|
|
||||||
},
|
|
||||||
"EP_Gen": {
|
|
||||||
"EP_Gen_AFTERDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_BEFOREDELAY": "{\n \"val\": \"MA==\"\n}\n",
|
|
||||||
"EP_Gen_NOTE": "{\n \"val\": \"UzE0IOWbnuiwgyBjb21wbGV0ZQ==\"\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodeUI": {
|
|
||||||
"NodeElementID": "",
|
|
||||||
"NodeModel": "2",
|
|
||||||
"NodePropErr": false,
|
|
||||||
"NodePx": "1260",
|
|
||||||
"NodePy": "880",
|
|
||||||
"NodeSubType": "EnumWidget_Code_Custom",
|
|
||||||
"NodeTag": "202608143140000000",
|
|
||||||
"NodeTitle": "插入代码块#1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"SeqTag": "Main",
|
|
||||||
"ViewCenterPx": "1357.02",
|
|
||||||
"ViewCenterPy": "547.412",
|
|
||||||
"ViewScaling": "0.564474"
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
{
|
|
||||||
"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:/work/AutoRpa/rpa_yisaiqi/studio",
|
|
||||||
"ProChange": "2026/08/18 13:12:33",
|
|
||||||
"ProColorRef": "4",
|
|
||||||
"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": "qm01"
|
|
||||||
},
|
|
||||||
"SeqGroupList": [
|
|
||||||
],
|
|
||||||
"SeqPythonModuleList": [
|
|
||||||
],
|
|
||||||
"SeqTextFileList": [
|
|
||||||
],
|
|
||||||
"SequenceList": [
|
|
||||||
{
|
|
||||||
"sdc": "",
|
|
||||||
"seq": "Main",
|
|
||||||
"seqptag": "",
|
|
||||||
"seqret": "1",
|
|
||||||
"seqtag": "2026081210133309518",
|
|
||||||
"spr": [
|
|
||||||
],
|
|
||||||
"spv": [
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
# 编译日期:2026-08-12 10:13:31
|
|
||||||
# 版权所有:www.i-search.com.cn
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 测试运行或调试以上全局函数入口
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pass
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
# 编译日期:2026-08-17 16:28:48
|
|
||||||
# 版权所有:www.i-search.com.cn
|
|
||||||
import ubpa.init_input as iinput
|
|
||||||
from ubpa.base_util import StdOutHook, ExceptionHandler
|
|
||||||
import time
|
|
||||||
import pdb
|
|
||||||
from ubpa.ilog import ILog
|
|
||||||
import getopt
|
|
||||||
from sys import argv
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import datetime as is_datetime
|
|
||||||
import pandas
|
|
||||||
from ubpa.base_img import *
|
|
||||||
import ubpa.iplatform as iplatform
|
|
||||||
|
|
||||||
class AutoRpa:
|
|
||||||
|
|
||||||
def __init__(self,**kwargs):
|
|
||||||
self.__logger = ILog(__file__)
|
|
||||||
self.path = set_img_res_path(__file__)
|
|
||||||
self.robot_no = ''
|
|
||||||
self.proc_no = ''
|
|
||||||
self.job_no = ''
|
|
||||||
self.input_arg = ''
|
|
||||||
if('robot_no' in kwargs.keys()):
|
|
||||||
self.robot_no = kwargs['robot_no']
|
|
||||||
if('proc_no' in kwargs.keys()):
|
|
||||||
self.proc_no = kwargs['proc_no']
|
|
||||||
if('job_no' in kwargs.keys()):
|
|
||||||
self.job_no = kwargs['job_no']
|
|
||||||
ILog.JOB_NO, ILog.OLD_STDOUT = self.job_no, sys.stdout
|
|
||||||
sys.stdout = StdOutHook(self.job_no, sys.stdout)
|
|
||||||
ExceptionHandler.JOB_NO, ExceptionHandler.OLD_STDERR = self.job_no, sys.stderr
|
|
||||||
sys.excepthook = ExceptionHandler.handle_exception
|
|
||||||
if('input_arg' in kwargs.keys()):
|
|
||||||
self.input_arg = kwargs['input_arg']
|
|
||||||
if(len(self.input_arg) <= 0):
|
|
||||||
self.input_arg = iinput.load_init(__file__)
|
|
||||||
if self.input_arg is None:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def Main(self):
|
|
||||||
#插入代码块#1-13
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143010000000,Title:插入代码块#1-13,Note:S01 初始化并回调 start')
|
|
||||||
print("[S01 初始化并回调 start]")
|
|
||||||
import sys, os, json
|
|
||||||
from pathlib import Path
|
|
||||||
_REPO_ROOT = next(
|
|
||||||
p for p in Path(__file__).resolve().parents
|
|
||||||
if (p / "rpa_yisaiqi" / "__init__.py").exists()
|
|
||||||
)
|
|
||||||
if str(_REPO_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_REPO_ROOT))
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data, build_spec_from_data
|
|
||||||
from rpa_yisaiqi.callback_client import StateCenterClient
|
|
||||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
|
|
||||||
def _first(*values):
|
|
||||||
for value in values:
|
|
||||||
if value is not None and str(value).strip():
|
|
||||||
return str(value).strip()
|
|
||||||
return ""
|
|
||||||
raw = (self.input_arg or "").strip()
|
|
||||||
if not raw:
|
|
||||||
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
|
|
||||||
if not raw:
|
|
||||||
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
|
|
||||||
print("未传入 -i/--input,使用默认联调入参:", raw)
|
|
||||||
if not raw:
|
|
||||||
raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径")
|
|
||||||
data = load_input_data(raw)
|
|
||||||
state_center_url = _first(
|
|
||||||
data.get("stateCenterUrl"), data.get("stateCenterURL"),
|
|
||||||
os.getenv("STATE_CENTER_URL"), "http://127.0.0.1:8000",
|
|
||||||
)
|
|
||||||
agent_token = _first(data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token")
|
|
||||||
_st.spec = build_spec_from_data(data)
|
|
||||||
_st.client = StateCenterClient(state_center_url, agent_token)
|
|
||||||
_st.ctx = _st.spec.flow_context
|
|
||||||
_st.browser = ApaBrowserAdapter(url=_st.ctx.contract_url)
|
|
||||||
_st.is_dry_run = (_st.spec.submit_mode == "dry_run")
|
|
||||||
if not _st.is_dry_run and os.getenv("RPA_CALLBACK_STATE_CENTER") == "1":
|
|
||||||
_st.client.start_node(_st.spec.job_id, "CONTRACT_CREATE")
|
|
||||||
#插入代码块#1-12
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143020000000,Title:插入代码块#1-12,Note:S02 打开合同系统')
|
|
||||||
print("[S02 打开合同系统]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import open_contract_system
|
|
||||||
open_contract_system(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-11
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143030000000,Title:插入代码块#1-11,Note:S03 登录合同系统')
|
|
||||||
print("[S03 登录合同系统]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import login_contract_system
|
|
||||||
login_contract_system(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-10
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143040000000,Title:插入代码块#1-10,Note:S04 进入合同起草')
|
|
||||||
print("[S04 进入合同起草]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import enter_contract_create
|
|
||||||
enter_contract_create(_st.browser)
|
|
||||||
#插入代码块#1-9
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143050000000,Title:插入代码块#1-9,Note:S05 选择模板')
|
|
||||||
print("[S05 选择模板]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import select_contract_template
|
|
||||||
select_contract_template(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-8
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143060000000,Title:插入代码块#1-8,Note:S06 填写签约方')
|
|
||||||
print("[S06 填写签约方]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_signatory_info
|
|
||||||
fill_contract_signatory_info(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-7
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143070000000,Title:插入代码块#1-7,Note:S07 填写基础信息')
|
|
||||||
print("[S07 填写基础信息]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info
|
|
||||||
fill_contract_basic_info(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-6
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143080000000,Title:插入代码块#1-6,Note:S08 填写金额日期')
|
|
||||||
print("[S08 填写金额日期]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_amount_and_dates
|
|
||||||
fill_contract_amount_and_dates(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-5
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143090000000,Title:插入代码块#1-5,Note:S09 填写货品明细')
|
|
||||||
print("[S09 填写货品明细]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_goods_lines
|
|
||||||
fill_contract_goods_lines(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-4
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143100000000,Title:插入代码块#1-4,Note:S10 填写付款信息')
|
|
||||||
print("[S10 填写付款信息]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import fill_contract_payment_info
|
|
||||||
fill_contract_payment_info(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-3
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143110000000,Title:插入代码块#1-3,Note:S11 上传合同PDF')
|
|
||||||
print("[S11 上传合同PDF]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import upload_contract_document_if_needed
|
|
||||||
upload_contract_document_if_needed(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-2
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143120000000,Title:插入代码块#1-2,Note:S12 提交送审')
|
|
||||||
print("[S12 提交送审]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import submit_contract_for_approval
|
|
||||||
_st.submitted = submit_contract_for_approval(_st.browser, _st.ctx)
|
|
||||||
#插入代码块#1-1
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143130000000,Title:插入代码块#1-1,Note:S13 读取合同编号')
|
|
||||||
print("[S13 读取合同编号]")
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
|
||||||
if _st.submitted:
|
|
||||||
_st.contract_no = read_contract_number(_st.browser)
|
|
||||||
#插入代码块#1
|
|
||||||
self.__logger.dlogs(job_no=self.job_no,logmsg='Flow:Main,StepNodeTag:202608143140000000,Title:插入代码块#1,Note:S14 回调 complete')
|
|
||||||
print("[S14 回调 complete]")
|
|
||||||
import json
|
|
||||||
import rpa_yisaiqi._flow_state as _st
|
|
||||||
if not _st.is_dry_run and _st.contract_no and os.getenv("RPA_CALLBACK_STATE_CENTER") == "1":
|
|
||||||
_st.client.complete_node(_st.spec.job_id, "CONTRACT_CREATE", {
|
|
||||||
"businessRefs": {"contractSystemNo": _st.contract_no},
|
|
||||||
})
|
|
||||||
print(json.dumps({"status": "done", "contractSystemNo": _st.contract_no}, ensure_ascii=False))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
ILog.begin_init()
|
|
||||||
robot_no = ''
|
|
||||||
proc_no = ''
|
|
||||||
job_no = ''
|
|
||||||
input_arg = ''
|
|
||||||
params = {}
|
|
||||||
fn = "Main"
|
|
||||||
flow_params={'Main':[]}
|
|
||||||
try:
|
|
||||||
argv = sys.argv[1:]
|
|
||||||
opts, args = getopt.getopt(argv,"hr:p:j:i:c:",["robot = ","proc = ","job = ","input = ","params = "])
|
|
||||||
except getopt.GetoptError:
|
|
||||||
print ('robot.py -r <robot> -p <proc> -j <job>')
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == '-h':
|
|
||||||
print ('robot.py -r <robot> -p <proc> -j <job>')
|
|
||||||
elif opt in ("-r", "--robot"):
|
|
||||||
robot_no = arg
|
|
||||||
elif opt in ("-p", "--proc"):
|
|
||||||
proc_no = arg
|
|
||||||
elif opt in ("-j", "--job"):
|
|
||||||
job_no = arg
|
|
||||||
elif opt in ("-i", "--input"):
|
|
||||||
input_arg = arg
|
|
||||||
elif opt in ("-c", "--params"):
|
|
||||||
cParam: dict = iinput.parseConnectorParam(arg)
|
|
||||||
params = cParam.pop("Params", params)
|
|
||||||
fn = cParam.pop("FlowName", fn)
|
|
||||||
pro = AutoRpa(robot_no=robot_no,proc_no=proc_no,job_no=job_no,input_arg=input_arg)
|
|
||||||
iplatform.Return(MainResult=getattr(pro, fn)(**params), job_no=job_no, fn=fn, flow_return_params=flow_params.get(fn,[]))
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# 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")
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
|
|
||||||
# 更多示例代码 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") # 设置服务器变量
|
|
||||||
'''
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"version": "2.0"
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
"""合同起草真实入口。Studio 编译不会改这个文件。"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _repo_root() -> Path:
|
|
||||||
for parent in Path(__file__).resolve().parents:
|
|
||||||
if (parent / "rpa_yisaiqi" / "__init__.py").exists():
|
|
||||||
return parent
|
|
||||||
raise RuntimeError("找不到仓库根目录(缺少 rpa_yisaiqi/__init__.py)")
|
|
||||||
|
|
||||||
|
|
||||||
_REPO_ROOT = _repo_root()
|
|
||||||
if str(_REPO_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_REPO_ROOT))
|
|
||||||
|
|
||||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter # noqa: E402
|
|
||||||
from rpa_yisaiqi.callback_client import StateCenterClient # noqa: E402
|
|
||||||
from rpa_yisaiqi.contract_create_node import ( # noqa: E402
|
|
||||||
ContractCreateError,
|
|
||||||
build_spec_from_data,
|
|
||||||
load_input_data,
|
|
||||||
)
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import ( # noqa: E402
|
|
||||||
enter_contract_create,
|
|
||||||
fill_contract_amount_and_dates,
|
|
||||||
fill_contract_basic_info,
|
|
||||||
fill_contract_goods_lines,
|
|
||||||
fill_contract_payment_info,
|
|
||||||
fill_contract_signatory_info,
|
|
||||||
login_contract_system,
|
|
||||||
open_contract_system,
|
|
||||||
read_contract_number,
|
|
||||||
select_contract_template,
|
|
||||||
submit_contract_for_approval,
|
|
||||||
upload_contract_document_if_needed,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _first(*values) -> str:
|
|
||||||
for value in values:
|
|
||||||
if value is not None and str(value).strip():
|
|
||||||
return str(value).strip()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_input_arg(argv: list[str]) -> str:
|
|
||||||
i = 0
|
|
||||||
while i < len(argv):
|
|
||||||
arg = argv[i]
|
|
||||||
if arg in ("-i", "--input", "--input-json") and i + 1 < len(argv):
|
|
||||||
return argv[i + 1]
|
|
||||||
if arg.startswith("--input=") or arg.startswith("--input-json="):
|
|
||||||
return arg.split("=", 1)[1]
|
|
||||||
i += 1
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
print("合同起草流程启动", flush=True)
|
|
||||||
|
|
||||||
raw = _parse_input_arg(sys.argv[1:])
|
|
||||||
if not raw:
|
|
||||||
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
|
|
||||||
if not raw:
|
|
||||||
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
|
|
||||||
print("未传入 -i/--input,使用默认联调入参:", raw, flush=True)
|
|
||||||
|
|
||||||
print("[S01 初始化]", flush=True)
|
|
||||||
data = load_input_data(raw)
|
|
||||||
state_center_url = _first(
|
|
||||||
data.get("stateCenterUrl"),
|
|
||||||
data.get("stateCenterURL"),
|
|
||||||
os.getenv("STATE_CENTER_URL"),
|
|
||||||
"http://192.168.105.69:8000",
|
|
||||||
)
|
|
||||||
agent_token = _first(data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token")
|
|
||||||
spec = build_spec_from_data(data)
|
|
||||||
ctx = spec.flow_context
|
|
||||||
if ctx is None:
|
|
||||||
raise ContractCreateError("初始化失败:flow_context 为空")
|
|
||||||
client = StateCenterClient(state_center_url, agent_token)
|
|
||||||
browser = ApaBrowserAdapter(url=ctx.contract_url, flow="CONTRACT_CREATE")
|
|
||||||
is_dry_run = spec.submit_mode == "dry_run"
|
|
||||||
print(" jobId=", spec.job_id, " submitMode=", spec.submit_mode, flush=True)
|
|
||||||
print(" contractDocumentPath=", ctx.contract_document_path or ctx.merged_pdf_path, flush=True)
|
|
||||||
|
|
||||||
if not is_dry_run:
|
|
||||||
print("回调状态中心 start", spec.job_id, flush=True)
|
|
||||||
client.start_node(spec.job_id, "CONTRACT_CREATE")
|
|
||||||
|
|
||||||
print("[S02 打开合同系统]", flush=True)
|
|
||||||
open_contract_system(browser, ctx)
|
|
||||||
|
|
||||||
print("[S03 登录合同系统]", flush=True)
|
|
||||||
login_contract_system(browser, ctx)
|
|
||||||
|
|
||||||
print("[S04 进入合同起草]", flush=True)
|
|
||||||
enter_contract_create(browser)
|
|
||||||
|
|
||||||
print("[S05 选择模板]", flush=True)
|
|
||||||
select_contract_template(browser, ctx)
|
|
||||||
|
|
||||||
print("[S06 填写签约方]", flush=True)
|
|
||||||
fill_contract_signatory_info(browser, ctx)
|
|
||||||
|
|
||||||
print("[S07 填写基础信息]", flush=True)
|
|
||||||
fill_contract_basic_info(browser, ctx)
|
|
||||||
|
|
||||||
print("[S08 填写金额日期]", flush=True)
|
|
||||||
fill_contract_amount_and_dates(browser, ctx)
|
|
||||||
|
|
||||||
print("[S09 填写货品明细]", flush=True)
|
|
||||||
fill_contract_goods_lines(browser, ctx)
|
|
||||||
|
|
||||||
print("[S10 填写付款信息]", flush=True)
|
|
||||||
fill_contract_payment_info(browser, ctx)
|
|
||||||
|
|
||||||
print("[S11 上传合同PDF]", flush=True)
|
|
||||||
upload_contract_document_if_needed(browser, ctx)
|
|
||||||
|
|
||||||
print("[S12 提交送审]", flush=True)
|
|
||||||
submitted = submit_contract_for_approval(browser, ctx)
|
|
||||||
|
|
||||||
contract_no = ""
|
|
||||||
if submitted:
|
|
||||||
print("[S13 读取合同编号]", flush=True)
|
|
||||||
contract_no = read_contract_number(browser, ctx)
|
|
||||||
|
|
||||||
print("[S14 结束]", flush=True)
|
|
||||||
if not is_dry_run and contract_no:
|
|
||||||
print("回调状态中心 complete", spec.job_id, contract_no, flush=True)
|
|
||||||
client.complete_node(
|
|
||||||
spec.job_id,
|
|
||||||
"CONTRACT_CREATE",
|
|
||||||
{"businessRefs": {"contractSystemNo": contract_no}},
|
|
||||||
)
|
|
||||||
print(json.dumps({"status": "done", "contractSystemNo": contract_no}, ensure_ascii=False), flush=True)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
"""APA 启动 Main.py 前会加载本文件。Studio 重新编译不会改这里。
|
|
||||||
|
|
||||||
Studio 点运行只带 -p,不带 -i。这里自动补上默认联调 JSON,
|
|
||||||
避免 S01 因 input_arg 为空直接报「缺少业务入参」。
|
|
||||||
账号过期把 Main.py 覆盖成空注释时,改走 run_contract.py。
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def _codes_dir() -> str:
|
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def _default_input_path() -> str:
|
|
||||||
repo = os.path.abspath(os.path.join(_codes_dir(), "..", "..", ".."))
|
|
||||||
return os.path.join(repo, "rpa_yisaiqi", "examples", "contract_create_input_dry_run.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _is_apa_main() -> bool:
|
|
||||||
if not sys.argv:
|
|
||||||
return False
|
|
||||||
return os.path.basename(str(sys.argv[0])).lower() == "main.py"
|
|
||||||
|
|
||||||
|
|
||||||
def _argv_has_input(argv: list) -> bool:
|
|
||||||
for i, arg in enumerate(argv):
|
|
||||||
if arg in ("-i", "--input") and i + 1 < len(argv) and str(argv[i + 1]).strip():
|
|
||||||
return True
|
|
||||||
if str(arg).startswith("--input=") and str(arg).split("=", 1)[1].strip():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _main_is_expired_stub(path: str) -> bool:
|
|
||||||
try:
|
|
||||||
text = open(path, encoding="utf-8").read()
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
return "账号过期" in text
|
|
||||||
|
|
||||||
|
|
||||||
# Windows 虚拟机只回调 Mac 状态中心,不在本机起库。
|
|
||||||
os.environ.setdefault("STATE_CENTER_URL", "http://192.168.105.69:8000")
|
|
||||||
os.environ.setdefault("AGENT_TOKEN", "dev-agent-token")
|
|
||||||
os.environ.setdefault("RPA_CALLBACK_STATE_CENTER", "1")
|
|
||||||
|
|
||||||
if _is_apa_main():
|
|
||||||
if _main_is_expired_stub(sys.argv[0]):
|
|
||||||
print("检测到 Main.py 被艺赛旗覆盖(账号过期),改走独立入口", flush=True)
|
|
||||||
if _codes_dir() not in sys.path:
|
|
||||||
sys.path.insert(0, _codes_dir())
|
|
||||||
from run_contract import main
|
|
||||||
|
|
||||||
raise SystemExit(main())
|
|
||||||
|
|
||||||
if not _argv_has_input(sys.argv):
|
|
||||||
default_input = _default_input_path()
|
|
||||||
if os.path.isfile(default_input):
|
|
||||||
sys.argv.extend(["-i", default_input])
|
|
||||||
print("未传入 -i,自动使用默认联调入参:", default_input, flush=True)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
[]
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,33 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from rpa_yisaiqi.apa_browser_adapter import REPO_ROOT, resolve_chrome_profile_base
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_chrome_profile_is_outside_repo():
|
|
||||||
profile = resolve_chrome_profile_base(requested="")
|
|
||||||
assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve()
|
|
||||||
assert REPO_ROOT.resolve() not in profile.parents
|
|
||||||
assert profile != REPO_ROOT.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
def test_env_path_inside_repo_is_rejected(tmp_path, monkeypatch):
|
|
||||||
inside = REPO_ROOT / "rpa_yisaiqi" / "runtime" / "chrome-profile"
|
|
||||||
monkeypatch.delenv("RPA_CHROME_PROFILE", raising=False)
|
|
||||||
profile = resolve_chrome_profile_base(requested=str(inside))
|
|
||||||
assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve()
|
|
||||||
assert not _is_relative_to(profile, REPO_ROOT)
|
|
||||||
|
|
||||||
|
|
||||||
def test_env_path_outside_repo_is_kept(tmp_path):
|
|
||||||
outside = tmp_path / "custom-chrome"
|
|
||||||
outside.mkdir()
|
|
||||||
profile = resolve_chrome_profile_base(requested=str(outside))
|
|
||||||
assert profile == outside.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
def _is_relative_to(path: Path, root: Path) -> bool:
|
|
||||||
try:
|
|
||||||
path.resolve().relative_to(root.resolve())
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
from rpa_yisaiqi.runtime.accept import (
|
|
||||||
AcceptanceReport,
|
|
||||||
accept_log_path,
|
|
||||||
artifact_dir,
|
|
||||||
evidence_shot,
|
|
||||||
field_match,
|
|
||||||
)
|
|
||||||
from rpa_yisaiqi.runtime.errors import NodeError
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def test_each_flow_writes_its_own_accept_file(tmp_path, monkeypatch):
|
|
||||||
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"rpa_yisaiqi.runtime.accept.SCREENSHOT_ROOT",
|
|
||||||
tmp_path,
|
|
||||||
)
|
|
||||||
report = AcceptanceReport(flow="ORDER_FETCH")
|
|
||||||
report.note("S01", "登录", {"user": "u1"}, ok=True)
|
|
||||||
path = accept_log_path("ORDER_FETCH")
|
|
||||||
assert path == tmp_path / "ORDER_FETCH" / "ACCEPT.txt"
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
assert "ACCEPT SUMMARY ORDER_FETCH 1/1 ok" in text
|
|
||||||
assert "ACCEPT S01 ok 登录 user=u1" in text
|
|
||||||
|
|
||||||
|
|
||||||
def test_artifact_dir_is_per_flow():
|
|
||||||
assert artifact_dir("REBATE_HEADER").name == "REBATE_HEADER"
|
|
||||||
assert artifact_dir("M5_CREATE_ORDER").name == "M5_CREATE_ORDER"
|
|
||||||
assert artifact_dir("CONTRACT_CREATE") != artifact_dir("ORDER_FETCH")
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_fails_on_mismatch_without_reading_images():
|
|
||||||
report = AcceptanceReport(flow="REBATE_HEADER")
|
|
||||||
with pytest.raises(NodeError, match="ACCEPT S02 FAIL"):
|
|
||||||
report.check("S02", "总单号", [("rebateOrderNo", "RB-1", "RB-2")])
|
|
||||||
|
|
||||||
|
|
||||||
def test_evidence_shot_skips_success_and_keeps_failure():
|
|
||||||
class Fake:
|
|
||||||
def __init__(self):
|
|
||||||
self.names = []
|
|
||||||
|
|
||||||
def screenshot(self, name):
|
|
||||||
self.names.append(name)
|
|
||||||
return name
|
|
||||||
|
|
||||||
browser = Fake()
|
|
||||||
assert evidence_shot(browser, "ok") == ""
|
|
||||||
assert browser.names == []
|
|
||||||
assert evidence_shot(browser, "bad", failed=True) == "bad"
|
|
||||||
assert browser.names == ["bad"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_field_match_accepts_numeric_and_yes_no():
|
|
||||||
assert field_match("12.0", "12")
|
|
||||||
assert field_match("是", "1")
|
|
||||||
assert field_match("2026-08-11", "20260811")
|
|
||||||
assert not field_match("重庆其他", "重庆亿昊药业有限公司")
|
|
||||||
|
|
@ -37,6 +37,6 @@ class YiSaiQiBrowserAdapter:
|
||||||
raise NotImplementedError(f"Map to YiSaiQi read-text activity: {selectors=} {value_name=}")
|
raise NotImplementedError(f"Map to YiSaiQi read-text activity: {selectors=} {value_name=}")
|
||||||
|
|
||||||
def screenshot(self, name: str) -> str:
|
def screenshot(self, name: str) -> str:
|
||||||
"""Human evidence only. Acceptance is ACCEPT text from runtime.accept, not this image."""
|
"""Flowchart node: 截图存证,返回文件路径."""
|
||||||
raise NotImplementedError(f"Map to YiSaiQi screenshot activity: {name=}")
|
raise NotImplementedError(f"Map to YiSaiQi screenshot activity: {name=}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,13 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
|
|
||||||
from rpa_yisaiqi.contract_create_node import (
|
from rpa_yisaiqi.contract_create_node import (
|
||||||
ContractCreateError,
|
ContractCreateError,
|
||||||
ContractCreateInput,
|
ContractCreateInput,
|
||||||
PdfMergeUnsupported,
|
PdfMergeUnsupported,
|
||||||
build_spec_from_data,
|
|
||||||
create_contract_in_contract_system,
|
create_contract_in_contract_system,
|
||||||
)
|
)
|
||||||
from rpa_yisaiqi.contract_draft import is_placeholder_customer_code, load_contract_draft
|
from rpa_yisaiqi.contract_draft import load_contract_draft
|
||||||
from rpa_yisaiqi.pdf_utils import ensure_merged_pdf
|
from rpa_yisaiqi.pdf_utils import ensure_merged_pdf
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,33 +77,6 @@ def test_load_contract_draft_maps_full_contract_inputs(tmp_path):
|
||||||
assert draft.goods_lines[0].goods_code == "YP-001"
|
assert draft.goods_lines[0].goods_code == "YP-001"
|
||||||
|
|
||||||
|
|
||||||
def test_placeholder_customer_code_is_rejected_before_opening_browser():
|
|
||||||
assert is_placeholder_customer_code("CUST-YYC-001")
|
|
||||||
assert is_placeholder_customer_code("test-001")
|
|
||||||
assert not is_placeholder_customer_code("DW0100046361")
|
|
||||||
|
|
||||||
with pytest.raises(ContractCreateError, match="占位符"):
|
|
||||||
build_spec_from_data(
|
|
||||||
{
|
|
||||||
"jobId": "JOB-1",
|
|
||||||
"supplierName": "重庆亿昊药业有限公司",
|
|
||||||
"orderDate": "2026-08-11",
|
|
||||||
"sourceType": "XYY",
|
|
||||||
"customerCode": "CUST-YYC-001",
|
|
||||||
"customerName": "重庆亿昊药业有限公司",
|
|
||||||
"amount": "100",
|
|
||||||
"submitMode": "dry_run",
|
|
||||||
"goodsLines": [
|
|
||||||
{
|
|
||||||
"goodsCode": "YP-001",
|
|
||||||
"goodsName": "阿卡波糖片",
|
|
||||||
"quantity": 1,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_yyc_requires_pdf_inputs_before_contract_drafting(tmp_path):
|
def test_yyc_requires_pdf_inputs_before_contract_drafting(tmp_path):
|
||||||
draft_path = tmp_path / "draft.json"
|
draft_path = tmp_path / "draft.json"
|
||||||
draft_path.write_text(
|
draft_path.write_text(
|
||||||
|
|
@ -140,16 +111,14 @@ def test_ensure_merged_pdf_uses_single_pdf_without_merge(tmp_path):
|
||||||
assert ensure_merged_pdf([str(source)], str(tmp_path / "merged.pdf")) == str(source)
|
assert ensure_merged_pdf([str(source)], str(tmp_path / "merged.pdf")) == str(source)
|
||||||
|
|
||||||
|
|
||||||
def test_create_contract_uses_apa_browser_adapter(tmp_path, monkeypatch):
|
def test_real_drafting_requires_yisaiqi_adapter_mapping(tmp_path):
|
||||||
draft_path = tmp_path / "draft.json"
|
draft_path = tmp_path / "draft.json"
|
||||||
draft_path.write_text(
|
draft_path.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"supplierName": "重庆亿昊药业有限公司",
|
"supplierName": "小药药供应商",
|
||||||
"orderDate": "2026-08-11",
|
"orderDate": "2026-08-11",
|
||||||
"sourceType": "XYY",
|
"sourceType": "XYY",
|
||||||
"customerCode": "DW0100046361",
|
|
||||||
"customerName": "重庆亿昊药业有限公司",
|
|
||||||
"goodsLines": [],
|
"goodsLines": [],
|
||||||
"platformPdfPaths": [],
|
"platformPdfPaths": [],
|
||||||
},
|
},
|
||||||
|
|
@ -159,22 +128,11 @@ def test_create_contract_uses_apa_browser_adapter(tmp_path, monkeypatch):
|
||||||
)
|
)
|
||||||
spec = ContractCreateInput(
|
spec = ContractCreateInput(
|
||||||
job_id="JOB-1",
|
job_id="JOB-1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="小药药供应商",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="XYY",
|
source_type="XYY",
|
||||||
draft_path=str(draft_path),
|
draft_path=str(draft_path),
|
||||||
)
|
)
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_run(browser, ctx):
|
with pytest.raises(ContractCreateError, match="YiSaiQiBrowserAdapter"):
|
||||||
captured["browser"] = browser
|
create_contract_in_contract_system(spec)
|
||||||
captured["ctx"] = ctx
|
|
||||||
return "HT-1"
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"rpa_yisaiqi.contract_create_node.run_contract_create_flow",
|
|
||||||
fake_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert create_contract_in_contract_system(spec) == "HT-1"
|
|
||||||
assert isinstance(captured["browser"], ApaBrowserAdapter)
|
|
||||||
|
|
|
||||||
|
|
@ -1,269 +1,110 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import (
|
from rpa_yisaiqi.contract_flow_steps import (
|
||||||
ContractFlowContext,
|
ContractFlowContext,
|
||||||
ContractGoodsLine,
|
ContractGoodsLine,
|
||||||
fill_contract_signatory_info,
|
|
||||||
run_contract_create_flow,
|
run_contract_create_flow,
|
||||||
)
|
)
|
||||||
from rpa_yisaiqi.runtime.errors import SupplierContractDisabled
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def no_sleep(monkeypatch):
|
|
||||||
monkeypatch.setattr("rpa_yisaiqi.contract_flow_steps.time.sleep", lambda _s: None)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeBrowser:
|
class FakeBrowser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calls = []
|
self.calls = []
|
||||||
self.body_text = ""
|
|
||||||
self.xform_labels = {}
|
|
||||||
self.filled = {}
|
|
||||||
self.read_values = {
|
self.read_values = {
|
||||||
"contract_number": "HT-20260811-REAL-001",
|
"contract_number": "HT-20260811-REAL-001",
|
||||||
}
|
}
|
||||||
|
|
||||||
def open_url(self, url):
|
def open_url(self, url):
|
||||||
self.calls.append(("open_url", url))
|
self.calls.append(("open_url", url))
|
||||||
if "kmAgreementApply" in url or "draft.jsp" in url:
|
|
||||||
self.body_text = "客户编码 客户名称 合同类别"
|
|
||||||
|
|
||||||
def fill_first(self, selectors, value):
|
def fill_first(self, selectors, value):
|
||||||
self.calls.append(("fill_first", tuple(selectors), value))
|
self.calls.append(("fill_first", tuple(selectors), value))
|
||||||
for selector in selectors:
|
|
||||||
self.filled[selector] = value
|
|
||||||
|
|
||||||
def click_native(self, selectors):
|
|
||||||
self.calls.append(("click_native", tuple(selectors)))
|
|
||||||
|
|
||||||
def click_first(self, selectors):
|
def click_first(self, selectors):
|
||||||
self.calls.append(("click_first", tuple(selectors)))
|
self.calls.append(("click_first", tuple(selectors)))
|
||||||
joined = " ".join(str(item) for item in selectors)
|
|
||||||
if "login" in joined or "登录" in joined:
|
|
||||||
self.body_text = "切换门户 待办 待阅 合同管理 合同起草"
|
|
||||||
|
|
||||||
def wait_for_text(self, text, timeout_seconds=15):
|
def wait_for_text(self, text, timeout_seconds=15):
|
||||||
self.calls.append(("wait_for_text", text, timeout_seconds))
|
self.calls.append(("wait_for_text", text, timeout_seconds))
|
||||||
if text and text not in self.body_text:
|
|
||||||
self.body_text = f"{self.body_text} {text}".strip()
|
|
||||||
|
|
||||||
def wait_for_element(self, selectors, timeout_seconds=15):
|
|
||||||
self.calls.append(("wait_for_element", tuple(selectors), timeout_seconds))
|
|
||||||
|
|
||||||
def element_exists(self, selectors):
|
|
||||||
self.calls.append(("element_exists", tuple(selectors)))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def switch_to_frame(self, selectors):
|
|
||||||
self.calls.append(("switch_to_frame", tuple(selectors)))
|
|
||||||
|
|
||||||
def switch_to_default_content(self):
|
|
||||||
self.calls.append(("switch_to_default_content",))
|
|
||||||
|
|
||||||
def select_first(self, selectors, value):
|
def select_first(self, selectors, value):
|
||||||
self.calls.append(("select_first", tuple(selectors), value))
|
self.calls.append(("select_first", tuple(selectors), value))
|
||||||
for selector in selectors:
|
|
||||||
self.filled[selector] = value
|
|
||||||
|
|
||||||
def select_xform(self, name, value):
|
|
||||||
self.calls.append(("select_xform", name, value))
|
|
||||||
self.xform_labels[name] = value
|
|
||||||
|
|
||||||
def execute_script(self, script, *args):
|
|
||||||
self.calls.append(("execute_script", script[:48], args))
|
|
||||||
if "hasTextInFrames" in script:
|
|
||||||
return True
|
|
||||||
if "latestByApplyTime" in script and "headerIdx" in script:
|
|
||||||
return "001132026081734940"
|
|
||||||
if "wantedTitle" in script and "headerIdx" in script:
|
|
||||||
return "001132026081734940"
|
|
||||||
if "fd_opcode" in script and "return i" in script:
|
|
||||||
return 0
|
|
||||||
if "List_Selected" in script and "innerText" in script:
|
|
||||||
token = str(args[0]) if args else ""
|
|
||||||
if token.startswith("DW"):
|
|
||||||
self.filled['input[name="extendDataFormInfo.value(fd_customeropcode)"]'] = token
|
|
||||||
elif token:
|
|
||||||
self.filled['input[name="extendDataFormInfo.value(fd_detail1.0.fd_opcode)"]'] = token
|
|
||||||
if "box.click" in script:
|
|
||||||
return {"ok": True}
|
|
||||||
return True
|
|
||||||
if "marked" in script and "fd_detail1" in script:
|
|
||||||
return []
|
|
||||||
if "return location.href" in script:
|
|
||||||
return "http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/kmAgreementApply.do?method=view"
|
|
||||||
if "Com_Parameter" in script and "fdId" in script:
|
|
||||||
return "FD-TEST-001"
|
|
||||||
if "添加合同" in script and ("pdf" in script or "wantedStem" in script):
|
|
||||||
if any(call[0] == "upload_file_first" for call in self.calls):
|
|
||||||
return {"ok": True, "via": "name"}
|
|
||||||
return {"ok": False, "via": "empty"}
|
|
||||||
if "提交" in script or "合同正文" in script or "确定" in script:
|
|
||||||
return {"ok": True}
|
|
||||||
if "fs-label" in script and args:
|
|
||||||
return self.xform_labels.get(args[0], "")
|
|
||||||
if "data-value" in script:
|
|
||||||
return "模板A"
|
|
||||||
return []
|
|
||||||
|
|
||||||
def upload_file_first(self, selectors, file_path):
|
def upload_file_first(self, selectors, file_path):
|
||||||
self.calls.append(("upload_file_first", tuple(selectors), file_path))
|
self.calls.append(("upload_file_first", tuple(selectors), file_path))
|
||||||
|
|
||||||
def read_first(self, selectors, value_name):
|
def read_first(self, selectors, value_name):
|
||||||
self.calls.append(("read_first", tuple(selectors), value_name))
|
self.calls.append(("read_first", tuple(selectors), value_name))
|
||||||
if value_name in self.read_values:
|
|
||||||
return self.read_values[value_name]
|
return self.read_values[value_name]
|
||||||
for selector in selectors:
|
|
||||||
if selector in self.filled:
|
|
||||||
return self.filled[selector]
|
|
||||||
joined = " ".join(str(item) for item in selectors)
|
|
||||||
if "fd_customername" in joined:
|
|
||||||
return self.read_values.get("customer_name", "重庆亿昊药业有限公司")
|
|
||||||
if "fd_customeropcode" in joined:
|
|
||||||
return self.read_values.get("customer_code", self.filled.get(
|
|
||||||
'input[name="extendDataFormInfo.value(fd_customeropcode)"]', ""
|
|
||||||
))
|
|
||||||
if "fdSettype" in joined:
|
|
||||||
return self.read_values.get("settlement_type", "付款")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def read_body_text(self):
|
|
||||||
self.calls.append(("read_body_text",))
|
|
||||||
return self.body_text
|
|
||||||
|
|
||||||
def screenshot(self, name):
|
def screenshot(self, name):
|
||||||
self.calls.append(("screenshot", name))
|
self.calls.append(("screenshot", name))
|
||||||
return f"/tmp/{name}.png"
|
return f"/tmp/{name}.png"
|
||||||
|
|
||||||
|
|
||||||
def test_signatory_rejects_placeholder_customer_before_opening_picker():
|
|
||||||
browser = FakeBrowser()
|
|
||||||
ctx = ContractFlowContext(
|
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
||||||
username="test002",
|
|
||||||
password="1",
|
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
|
||||||
order_date="2026-08-11",
|
|
||||||
source_type="YYC",
|
|
||||||
customer_code="CUST-YYC-001",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(SupplierContractDisabled, match="占位符"):
|
|
||||||
fill_contract_signatory_info(browser, ctx)
|
|
||||||
|
|
||||||
assert browser.calls == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_contract_flow_logs_in_with_real_contract_login_selectors():
|
def test_contract_flow_logs_in_with_real_contract_login_selectors():
|
||||||
browser = FakeBrowser()
|
browser = FakeBrowser()
|
||||||
ctx = ContractFlowContext(
|
ctx = ContractFlowContext(
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
username="test002",
|
username="test002",
|
||||||
password="1",
|
password="1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="合同节点联调供应商",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="XYY",
|
source_type="XYY",
|
||||||
template_type="FORMAT",
|
template_type="FORMAT",
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
goods_lines=[],
|
goods_lines=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
run_contract_create_flow(browser, ctx)
|
run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
assert ("open_url", "http://contract-test.gzmpc.com:8080/") in browser.calls
|
assert browser.calls[:5] == [
|
||||||
assert ("fill_first", ('input[name="j_username"]', 'input[placeholder="用户名"]'), "test002") in browser.calls
|
("open_url", "http://contract-test.gzmpc.com:8080/"),
|
||||||
assert ("fill_first", ('input[name="j_password"]', 'input[placeholder="密码"]'), "1") in browser.calls
|
("fill_first", ('input[name="j_username"]', 'input[placeholder="用户名"]'), "test002"),
|
||||||
assert (
|
("fill_first", ('input[name="j_password"]', 'input[placeholder="密码"]'), "1"),
|
||||||
"click_first",
|
("click_first", ('input[name="btn_submit"]', 'a[href*="btn_submit"]', 'text=登录')),
|
||||||
('a[class*="login_button"]', 'a:contains("登录")', 'input[name="btn_submit"]'),
|
("wait_for_text", "合同", 20),
|
||||||
) in browser.calls
|
]
|
||||||
assert not [call for call in browser.calls if call[0] == "screenshot"]
|
|
||||||
|
|
||||||
|
|
||||||
def _touch_pdf(path):
|
def test_contract_flow_uses_order_date_and_uploads_yyc_merged_pdf():
|
||||||
path.write_bytes(b"%PDF-1.4\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n")
|
|
||||||
return str(path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_contract_flow_uses_order_date_and_uploads_yyc_merged_pdf(tmp_path):
|
|
||||||
browser = FakeBrowser()
|
browser = FakeBrowser()
|
||||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
||||||
pdf_path = _touch_pdf(tmp_path / "contract-merged.pdf")
|
|
||||||
ctx = ContractFlowContext(
|
ctx = ContractFlowContext(
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
username="test002",
|
username="test002",
|
||||||
password="1",
|
password="1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="合同节点联调供应商",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="YYC",
|
source_type="YYC",
|
||||||
template_type="FORMAT",
|
template_type="FORMAT",
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
goods_lines=[
|
goods_lines=[
|
||||||
ContractGoodsLine(
|
ContractGoodsLine(
|
||||||
operation_code="ADD",
|
operation_code="ADD",
|
||||||
goods_code="FQX0100118047",
|
goods_code="YP-001",
|
||||||
goods_name="盐酸托烷司琼注射液",
|
goods_name="阿卡波糖片",
|
||||||
approval_no="苏20160372",
|
approval_no="国药准字H00000001",
|
||||||
spec="5ml:5mg*5支",
|
spec="50mg*30片",
|
||||||
unit="盒",
|
unit="盒",
|
||||||
quantity=12,
|
quantity=12,
|
||||||
unit_price="50.00",
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
merged_pdf_path=pdf_path,
|
merged_pdf_path="C:/rpa/jobs/JOB-1/contract-merged.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
contract_no = run_contract_create_flow(browser, ctx)
|
contract_no = run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
assert contract_no == "001132026081734940"
|
assert contract_no == "HT-20260811-REAL-001"
|
||||||
assert ("fill_first", ('input[name="fdSigndate"]',), "2026-08-11") in browser.calls
|
assert ("fill_first", ('label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'), "2026-08-11") in browser.calls
|
||||||
assert ("fill_first", ('input[name="fdEffedate"]',), "2026-08-11") in browser.calls
|
assert ("fill_first", ('label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'), "2026-08-11") in browser.calls
|
||||||
assert (
|
assert ("upload_file_first", ('input[type="file"]', 'label=合同证文上传'), "C:/rpa/jobs/JOB-1/contract-merged.pdf") in browser.calls
|
||||||
"click_first",
|
|
||||||
('div[title="选择商品"]', "div.lui-component.lui_widget_btn_txt", "text=选择商品"),
|
|
||||||
) in browser.calls
|
|
||||||
assert (
|
|
||||||
"switch_to_frame",
|
|
||||||
(
|
|
||||||
'iframe[src*="EopBasedataMaterial"]',
|
|
||||||
'iframe[src*="dialog_fdMaterial"]',
|
|
||||||
'iframe[src*="common-template"]',
|
|
||||||
),
|
|
||||||
) in browser.calls
|
|
||||||
assert ("fill_first", ("input.search-box-text", "input[placeholder*='关键字']"), "FQX0100118047") in browser.calls
|
|
||||||
assert (
|
|
||||||
"fill_first",
|
|
||||||
('input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)"]',),
|
|
||||||
"12",
|
|
||||||
) in browser.calls
|
|
||||||
assert (
|
|
||||||
"fill_first",
|
|
||||||
('input[name="extendDataFormInfo.value(fd_detail1.0.fd_unitprice)"]',),
|
|
||||||
"50.00",
|
|
||||||
) in browser.calls
|
|
||||||
assert any(
|
|
||||||
call[0] == "upload_file_first" and call[-1] == pdf_path
|
|
||||||
for call in browser.calls
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_contract_flow_skips_pdf_upload_for_xyy():
|
def test_contract_flow_skips_pdf_upload_for_xyy():
|
||||||
browser = FakeBrowser()
|
browser = FakeBrowser()
|
||||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
||||||
ctx = ContractFlowContext(
|
ctx = ContractFlowContext(
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
username="test002",
|
username="test002",
|
||||||
password="1",
|
password="1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="小药药供应商",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="XYY",
|
source_type="XYY",
|
||||||
template_type="FORMAT",
|
template_type="FORMAT",
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
goods_lines=[],
|
goods_lines=[],
|
||||||
merged_pdf_path="C:/should-not-upload.pdf",
|
merged_pdf_path="C:/should-not-upload.pdf",
|
||||||
)
|
)
|
||||||
|
|
@ -273,46 +114,45 @@ def test_contract_flow_skips_pdf_upload_for_xyy():
|
||||||
assert not [call for call in browser.calls if call[0] == "upload_file_first"]
|
assert not [call for call in browser.calls if call[0] == "upload_file_first"]
|
||||||
|
|
||||||
|
|
||||||
def test_contract_flow_enters_draft_form_by_template_url(tmp_path):
|
def test_contract_flow_enters_real_draft_menu_and_recent_template():
|
||||||
from rpa_yisaiqi.contract_flow_steps import FORM_PAGE_URL
|
|
||||||
|
|
||||||
browser = FakeBrowser()
|
browser = FakeBrowser()
|
||||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
||||||
ctx = ContractFlowContext(
|
ctx = ContractFlowContext(
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
username="test002",
|
username="test002",
|
||||||
password="1",
|
password="1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="重庆亿昊药业",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="YYC",
|
source_type="YYC",
|
||||||
template_type="NON_FORMAT",
|
template_type="NON_FORMAT",
|
||||||
template_name="非格式--采购协议",
|
template_name="非格式--采购协议",
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
goods_lines=[],
|
goods_lines=[],
|
||||||
merged_pdf_path=_touch_pdf(tmp_path / "contract-merged.pdf"),
|
merged_pdf_path="C:/rpa/jobs/JOB-1/contract-merged.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
run_contract_create_flow(browser, ctx)
|
run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
assert ("open_url", FORM_PAGE_URL) in browser.calls
|
assert ("click_first", ("text=合同管理", "aria=合同管理")) in browser.calls
|
||||||
assert ("wait_for_text", "客户编码", 20) in browser.calls
|
assert ("click_first", ("text=合同起草", "aria=合同起草")) in browser.calls
|
||||||
|
assert ("wait_for_text", "最近使用模板", 20) in browser.calls
|
||||||
|
assert (
|
||||||
|
"click_first",
|
||||||
|
("text=非格式--采购协议", "text=非格式--采购协议...", "title=非格式--采购协议"),
|
||||||
|
) in browser.calls
|
||||||
|
|
||||||
|
|
||||||
def test_contract_flow_fills_real_contract_draft_form_fields_from_screenshot():
|
def test_contract_flow_fills_real_contract_draft_form_fields_from_screenshot():
|
||||||
browser = FakeBrowser()
|
browser = FakeBrowser()
|
||||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
||||||
ctx = ContractFlowContext(
|
ctx = ContractFlowContext(
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
username="test002",
|
username="test002",
|
||||||
password="1",
|
password="1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="重庆亿昊药业",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="XYY",
|
source_type="XYY",
|
||||||
template_type="NON_FORMAT",
|
template_type="NON_FORMAT",
|
||||||
template_name="非格式--采购协议",
|
template_name="非格式--采购协议",
|
||||||
customer_code="DW0100046361",
|
customer_code="CUST-001",
|
||||||
customer_name="重庆亿昊药业有限公司",
|
customer_name="重庆亿昊药业",
|
||||||
contract_category="采购协议",
|
contract_category="采购协议",
|
||||||
settlement_type="付款",
|
settlement_type="付款",
|
||||||
contract_background="按订单自动起草采购协议",
|
contract_background="按订单自动起草采购协议",
|
||||||
|
|
@ -332,53 +172,37 @@ def test_contract_flow_fills_real_contract_draft_form_fields_from_screenshot():
|
||||||
|
|
||||||
run_contract_create_flow(browser, ctx)
|
run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
assert (
|
assert ("fill_first", ('label=客户编码', 'input[name="fdCustomerCode"]', 'input[placeholder*="客户编码"]'), "CUST-001") in browser.calls
|
||||||
"select_xform",
|
assert ("fill_first", ('label=客户名称', 'input[name="fdCustomerName"]', 'input[placeholder*="客户名称"]'), "重庆亿昊药业") in browser.calls
|
||||||
"extendDataFormInfo.value(fd_contract_cate_type)",
|
assert ("select_first", ('label=合同类别', 'select[name="fdContractCategory"]'), "采购协议") in browser.calls
|
||||||
"其他协议(采购)",
|
assert ("select_first", ('label=结算类型', 'select[name="fdSettlementType"]'), "付款") in browser.calls
|
||||||
) in browser.calls
|
assert ("fill_first", ('label=合同背景概述', 'textarea[name="fdContractBackground"]'), "按订单自动起草采购协议") in browser.calls
|
||||||
assert ("select_first", ('select#fdSettype',), "付款") in browser.calls
|
assert ("click_first", ("label=主合同", "text=主合同")) in browser.calls
|
||||||
assert (
|
assert ("select_first", ('label=合同金额', 'select[name="fdCurrency"]'), "人民币") in browser.calls
|
||||||
"fill_first",
|
assert ("fill_first", ('label=金额(元)', 'input[name="fdAmount"]'), "12000.50") in browser.calls
|
||||||
('textarea[name="extendDataFormInfo.value(fd_desc_info)"]',),
|
assert ("fill_first", ('label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'), "2026-08-11") in browser.calls
|
||||||
"按订单自动起草采购协议",
|
assert ("fill_first", ('label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'), "2026-08-11") in browser.calls
|
||||||
) in browser.calls
|
assert ("fill_first", ('label=合同结束日期', 'input[name="fdEndDate"]', 'input[placeholder*="合同结束"]'), "2026-12-31") in browser.calls
|
||||||
assert ("select_xform", "extendDataFormInfo.value(fd_all_amount_type)", "合同标的金额") in browser.calls
|
assert ("select_first", ('label=是否重大合同', 'select[name="fdIsMajorContract"]'), "否") in browser.calls
|
||||||
assert ("fill_first", ('input[name="fdAccount"]',), "12000.50") in browser.calls
|
assert ("select_first", ('label=是否预付款', 'select[name="fdIsPrepayment"]'), "否") in browser.calls
|
||||||
assert ("fill_first", ('input[name="fdSigndate"]',), "2026-08-11") in browser.calls
|
assert ("fill_first", ('label=合同归属年份', 'input[name="fdBelongYear"]'), "2026") in browser.calls
|
||||||
assert ("fill_first", ('input[name="fdEffedate"]',), "2026-08-11") in browser.calls
|
assert ("select_first", ('label=付款方式', 'select[name="fdPaymentMethod"]', 'select[name="paymentMethod"]'), "账期支付") in browser.calls
|
||||||
assert ("fill_first", ('input[name="fdEnddate"]',), "2026-12-31") in browser.calls
|
assert ("fill_first", ('label=付款期限', 'input[name="fdPaymentDays"]', 'input[name="paymentDays"]'), "30") in browser.calls
|
||||||
assert ("select_xform", "extendDataFormInfo.value(fd_is_major_contract)", "否") in browser.calls
|
assert ("select_first", ('label=合同状态', 'select[name="fdContractStatus"]'), "正式合同") in browser.calls
|
||||||
assert ("select_xform", "extendDataFormInfo.value(fd_advance_amout)", "否") in browser.calls
|
assert ("select_first", ('label=是否包含血液制品', 'select[name="fdContainsBloodProducts"]'), "否") in browser.calls
|
||||||
assert (
|
|
||||||
"fill_first",
|
|
||||||
('input[name="extendDataFormInfo.value(fd_contract_year_text)"]',),
|
|
||||||
"2026",
|
|
||||||
) in browser.calls
|
|
||||||
assert ("select_xform", "extendDataFormInfo.value(fd_paytype)", "电汇") in browser.calls
|
|
||||||
assert (
|
|
||||||
"fill_first",
|
|
||||||
('input[name="extendDataFormInfo.value(fd_pay_time_limit)"]',),
|
|
||||||
"30",
|
|
||||||
) in browser.calls
|
|
||||||
assert ("select_xform", "extendDataFormInfo.value(fd_suconstatus)", "正式合同") in browser.calls
|
|
||||||
assert ("select_xform", "extendDataFormInfo.value(fd_3d1ce3953f1162)", "否") in browser.calls
|
|
||||||
|
|
||||||
|
|
||||||
def test_contract_flow_dry_run_skips_submit_but_still_reads_page_state():
|
def test_contract_flow_dry_run_skips_submit_but_still_reads_page_state():
|
||||||
browser = FakeBrowser()
|
browser = FakeBrowser()
|
||||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
||||||
ctx = ContractFlowContext(
|
ctx = ContractFlowContext(
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||||
username="test002",
|
username="test002",
|
||||||
password="1",
|
password="1",
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
supplier_name="重庆亿昊药业",
|
||||||
order_date="2026-08-11",
|
order_date="2026-08-11",
|
||||||
source_type="XYY",
|
source_type="XYY",
|
||||||
template_type="NON_FORMAT",
|
template_type="NON_FORMAT",
|
||||||
template_name="非格式--采购协议",
|
template_name="非格式--采购协议",
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
submit_mode="dry_run",
|
submit_mode="dry_run",
|
||||||
goods_lines=[],
|
goods_lines=[],
|
||||||
)
|
)
|
||||||
|
|
@ -386,105 +210,6 @@ def test_contract_flow_dry_run_skips_submit_but_still_reads_page_state():
|
||||||
contract_no = run_contract_create_flow(browser, ctx)
|
contract_no = run_contract_create_flow(browser, ctx)
|
||||||
|
|
||||||
assert not [call for call in browser.calls if call[0] == "click_first" and "text=提交" in call[1]]
|
assert not [call for call in browser.calls if call[0] == "click_first" and "text=提交" in call[1]]
|
||||||
assert not [call for call in browser.calls if call[0] == "screenshot"]
|
assert ("screenshot", "contract-dry-run-ready") in browser.calls
|
||||||
assert contract_no == ""
|
assert contract_no == ""
|
||||||
|
assert not [call for call in browser.calls if call[0] == "read_first"]
|
||||||
|
|
||||||
def test_contract_flow_writes_text_accept_report_instead_of_requiring_screenshots(tmp_path, monkeypatch):
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import _ACCEPT, accept_log_path
|
|
||||||
|
|
||||||
monkeypatch.setenv("RPA_ACCEPT_LOG", str(tmp_path / "ACCEPT.txt"))
|
|
||||||
browser = FakeBrowser()
|
|
||||||
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
||||||
ctx = ContractFlowContext(
|
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
||||||
username="test002",
|
|
||||||
password="1",
|
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
|
||||||
order_date="2026-08-11",
|
|
||||||
source_type="XYY",
|
|
||||||
template_type="NON_FORMAT",
|
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
submit_mode="dry_run",
|
|
||||||
goods_lines=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
run_contract_create_flow(browser, ctx)
|
|
||||||
|
|
||||||
text = accept_log_path("CONTRACT_CREATE").read_text(encoding="utf-8")
|
|
||||||
assert "ACCEPT SUMMARY" in text
|
|
||||||
assert "ACCEPT S03 ok" in text
|
|
||||||
assert "ACCEPT S06 ok" in text
|
|
||||||
assert "customerName=重庆亿昊药业有限公司" in text
|
|
||||||
assert "ACCEPT S12 ok" in text
|
|
||||||
assert all(line.ok for line in _ACCEPT.lines)
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_contract_number_falls_back_to_my_contracts_list():
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
|
||||||
|
|
||||||
browser = FakeBrowser()
|
|
||||||
browser.read_values.pop("contract_number", None)
|
|
||||||
ctx = ContractFlowContext(
|
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
||||||
username="test002",
|
|
||||||
password="1",
|
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
|
||||||
order_date="2026-08-11",
|
|
||||||
source_type="XYY",
|
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
contract_title="test1",
|
|
||||||
goods_lines=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
contract_no = read_contract_number(browser, ctx)
|
|
||||||
|
|
||||||
assert contract_no == "001132026081734940"
|
|
||||||
assert ("open_url", "http://contract-test.gzmpc.com:8080/") in browser.calls
|
|
||||||
assert ("click_first", ("text=我的合同",)) in browser.calls
|
|
||||||
assert ("click_first", ("text=我起草的",)) in browser.calls
|
|
||||||
assert ("click_first", ("text=申请时间",)) in browser.calls
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_contract_number_goes_to_my_list_even_without_ctx_arg():
|
|
||||||
from rpa_yisaiqi import _flow_state as state
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
|
||||||
|
|
||||||
browser = FakeBrowser()
|
|
||||||
browser.read_values.pop("contract_number", None)
|
|
||||||
old_ctx = state.ctx
|
|
||||||
state.ctx = ContractFlowContext(
|
|
||||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
||||||
username="test002",
|
|
||||||
password="1",
|
|
||||||
supplier_name="重庆亿昊药业有限公司",
|
|
||||||
order_date="2026-08-11",
|
|
||||||
source_type="XYY",
|
|
||||||
customer_code="DW0100046361",
|
|
||||||
customer_name="重庆亿昊药业有限公司",
|
|
||||||
contract_title="test1",
|
|
||||||
goods_lines=[],
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
contract_no = read_contract_number(browser)
|
|
||||||
finally:
|
|
||||||
state.ctx = old_ctx
|
|
||||||
|
|
||||||
assert contract_no == "001132026081734940"
|
|
||||||
assert ("click_first", ("text=我的合同",)) in browser.calls
|
|
||||||
assert ("click_first", ("text=申请时间",)) in browser.calls
|
|
||||||
|
|
||||||
|
|
||||||
def test_accept_check_fails_when_page_value_mismatches():
|
|
||||||
from rpa_yisaiqi.contract_flow_steps import AcceptanceReport
|
|
||||||
from rpa_yisaiqi.runtime.errors import NodeError
|
|
||||||
|
|
||||||
report = AcceptanceReport()
|
|
||||||
with pytest.raises(NodeError, match="ACCEPT S06 FAIL"):
|
|
||||||
report.check(
|
|
||||||
"S06",
|
|
||||||
"签约方",
|
|
||||||
[("customerName", "其他公司", "重庆亿昊药业有限公司")],
|
|
||||||
)
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue