607 lines
20 KiB
Python
607 lines
20 KiB
Python
# 合同页面共用:类型、选择器、工具栏、fdId、验收报告。
|
||
#
|
||
# 各 S0x 步骤从这里取 BrowserAdapter / ContractFlowContext / _ACCEPT。
|
||
# 改字段选择器或工具栏点击,步骤文件一起变。
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from dataclasses import dataclass, field
|
||
from typing import Protocol
|
||
|
||
from accept import AcceptanceReport
|
||
|
||
|
||
# 合同页面操作口,由 ApaBrowserAdapter 实现。
|
||
class BrowserAdapter(Protocol):
|
||
# 打开指定 URL。
|
||
def open_url(self, url: str) -> None:
|
||
...
|
||
|
||
# 按选择器列表填第一个匹配到的输入框。
|
||
def fill_first(self, selectors: list[str], value: str) -> None:
|
||
...
|
||
|
||
# 点第一个匹配到的元素。
|
||
def click_first(self, selectors: list[str]) -> None:
|
||
...
|
||
|
||
# 用原生事件点第一个匹配到的元素。
|
||
def click_native(self, selectors: list[str]) -> None:
|
||
...
|
||
|
||
# 双击第一个匹配到的元素。
|
||
def double_click_first(self, selectors: list[str]) -> None:
|
||
...
|
||
|
||
# 等到页面正文出现指定文本。
|
||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
||
...
|
||
|
||
# 等到选择器列表里有一个元素出现。
|
||
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None:
|
||
...
|
||
|
||
# 选择器列表里是否已有匹配元素。
|
||
def element_exists(self, selectors: list[str]) -> bool:
|
||
...
|
||
|
||
# 切入第一个匹配到的 iframe。
|
||
def switch_to_frame(self, selectors: list[str]) -> None:
|
||
...
|
||
|
||
# 切回主文档。
|
||
def switch_to_default_content(self) -> None:
|
||
...
|
||
|
||
# 在第一个匹配到的 select 上选中指定值。
|
||
def select_first(self, selectors: list[str], value: str) -> None:
|
||
...
|
||
|
||
# 按 xform property 名选中 fancy-select 选项。
|
||
def select_xform(self, name: str, value: str) -> None:
|
||
...
|
||
|
||
# 在当前页跑一段脚本并返回结果。
|
||
def execute_script(self, script: str, *args):
|
||
...
|
||
|
||
# 把本地文件塞进第一个匹配到的 file input。
|
||
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
|
||
...
|
||
|
||
# 读第一个匹配元素的值或文本。
|
||
def read_first(self, selectors: list[str], value_name: str = "") -> str:
|
||
...
|
||
|
||
# 读当前页可见正文。
|
||
def read_body_text(self) -> str:
|
||
...
|
||
|
||
# 按名字截一张图,返回保存路径。
|
||
def screenshot(self, name: str) -> str:
|
||
...
|
||
|
||
# 有原生 alert 就点确定并返回文案。
|
||
def accept_alert(self) -> str:
|
||
...
|
||
|
||
# 关掉当前页再打开指定 URL。
|
||
def reopen_url(self, url: str) -> None:
|
||
...
|
||
|
||
|
||
# 合同货品明细一行,供选择商品弹窗和明细表回填。
|
||
@dataclass(frozen=True)
|
||
class ContractGoodsLine:
|
||
operation_code: str
|
||
goods_code: str
|
||
goods_name: str
|
||
approval_no: str
|
||
spec: str
|
||
unit: str
|
||
quantity: float
|
||
mdm_code: str = ""
|
||
unit_price: str = ""
|
||
|
||
|
||
# 一次合同起草页的入参。账密、客商、货品、提交方式都在这里。
|
||
@dataclass(frozen=True)
|
||
class ContractFlowContext:
|
||
contract_url: str
|
||
username: str
|
||
password: str
|
||
supplier_name: str
|
||
order_date: str
|
||
source_type: str
|
||
template_type: str = "FORMAT"
|
||
template_name: str = ""
|
||
customer_code: str = ""
|
||
customer_name: str = ""
|
||
contract_category: str = ""
|
||
settlement_type: str = "付款"
|
||
contract_background: str = ""
|
||
contract_nature: str = "主合同"
|
||
amount_currency: str = "人民币"
|
||
amount_type: str = "合同标的金额"
|
||
amount: str = ""
|
||
contract_title: str = ""
|
||
contract_end_date: str = ""
|
||
contract_sign_date: str = ""
|
||
contract_start_date: str = ""
|
||
is_major_contract: str = "否"
|
||
is_prepayment: str = "否"
|
||
belong_year: str = ""
|
||
payment_method: str = "账期支付"
|
||
payment_days: str = "30"
|
||
contract_status: str = "正式合同"
|
||
contains_blood_products: str = "否"
|
||
submit_mode: str = "submit"
|
||
goods_lines: list[ContractGoodsLine] = field(default_factory=list)
|
||
merged_pdf_path: str | None = None
|
||
contract_document_path: str = ""
|
||
order_no_list: list[str] = field(default_factory=list)
|
||
|
||
|
||
_ACCEPT = AcceptanceReport(flow="CONTRACT_CREATE")
|
||
|
||
|
||
# 拼合同标题:有 contract_title 用它,否则客商名+类别+订单日。
|
||
def _expected_title(ctx: ContractFlowContext) -> str:
|
||
title = (ctx.contract_title or "").strip()
|
||
if title:
|
||
return title
|
||
who = (ctx.customer_name or ctx.supplier_name or "").strip()
|
||
cate = (ctx.contract_category or "采购协议").strip()
|
||
date = (ctx.order_date or "").replace("-", "")
|
||
return f"{who}{cate}{date}"
|
||
|
||
|
||
# 合同起草页(模板选择)直达 URL,绕过 SPA 菜单/iframe 复杂性
|
||
DRAFT_PAGE_URL = (
|
||
"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"
|
||
)
|
||
|
||
# 非格式-采购协议(2025集团版)模板 ID(测试环境固定值)
|
||
# 直接跳转表单页,绕过「点模板」这一依赖前端委托事件、Selenium 点击无效的步骤
|
||
TEMPLATE_ID = "198d10b04325279b8ffeb8b4c6493b5a"
|
||
FORM_PAGE_URL = (
|
||
"http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/"
|
||
"kmAgreementApply.do?method=add"
|
||
f"&fdTemplateId={TEMPLATE_ID}"
|
||
f"&.fdTemplate={TEMPLATE_ID}"
|
||
f"&i.docTemplate={TEMPLATE_ID}"
|
||
"&s_css=default"
|
||
)
|
||
|
||
VIEW_PAGE_URL = (
|
||
"http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/"
|
||
"kmAgreementApply.do?method=view&fdId={fd_id}"
|
||
)
|
||
EDIT_PAGE_URL = (
|
||
"http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/"
|
||
"kmAgreementApply.do?method=edit&fdId={fd_id}"
|
||
)
|
||
|
||
# Landray/LUI 表单字段的真实 name
|
||
F = {
|
||
"customer_code": 'input[name="extendDataFormInfo.value(fd_customeropcode)"]',
|
||
"customer_name": 'input[name="extendDataFormInfo.value(fd_customername)"]',
|
||
"contract_category": 'input[name="extendDataFormInfo.value(fd_contract_cate_type)"]',
|
||
"settlement_type": 'select#fdSettype',
|
||
"contract_background": 'textarea[name="extendDataFormInfo.value(fd_desc_info)"]',
|
||
"nature_main": 'input[name="fdType"][value="00"]',
|
||
"amount": 'input[name="fdAccount"]',
|
||
"sign_date": 'input[name="fdSigndate"]',
|
||
"begin_date": 'input[name="fdEffedate"]',
|
||
"end_date": 'input[name="fdEnddate"]',
|
||
"is_major_contract": 'input[name="extendDataFormInfo.value(fd_is_major_contract)"]',
|
||
"is_prepayment": 'input[name="extendDataFormInfo.value(fd_advance_amout)"]',
|
||
"belong_year": 'input[name="extendDataFormInfo.value(fd_contract_year_text)"]',
|
||
"payment_method": 'input[name="extendDataFormInfo.value(fd_paytype)"]',
|
||
"payment_days": 'input[name="extendDataFormInfo.value(fd_pay_time_limit)"]',
|
||
"contract_status": 'input[name="extendDataFormInfo.value(fd_suconstatus)"]',
|
||
"contains_blood": 'input[name="extendDataFormInfo.value(fd_3d1ce3953f1162)"]',
|
||
"doc_subject": 'input[name="docSubject"]',
|
||
}
|
||
|
||
# xform fancy-select 的 property 名(select_xform 用)
|
||
X = {
|
||
"contract_category": "extendDataFormInfo.value(fd_contract_cate_type)",
|
||
"cover_template": "extendDataFormInfo.value(fd_contract_cover_type)",
|
||
"amount_type": "extendDataFormInfo.value(fd_all_amount_type)",
|
||
"is_major_contract": "extendDataFormInfo.value(fd_is_major_contract)",
|
||
"is_prepayment": "extendDataFormInfo.value(fd_advance_amout)",
|
||
"payment_method": "extendDataFormInfo.value(fd_paytype)",
|
||
"contract_status": "extendDataFormInfo.value(fd_suconstatus)",
|
||
"contains_blood": "extendDataFormInfo.value(fd_3d1ce3953f1162)",
|
||
}
|
||
|
||
_YES_NO = {
|
||
"1": "是",
|
||
"2": "否",
|
||
"Y": "是",
|
||
"N": "否",
|
||
"YES": "是",
|
||
"NO": "否",
|
||
"TRUE": "是",
|
||
"FALSE": "否",
|
||
}
|
||
_CATEGORY_ALIASES = {
|
||
"采购协议": "其他协议(采购)",
|
||
"采购": "其他协议(采购)",
|
||
}
|
||
_PAYMENT_ALIASES = {
|
||
"账期支付": "电汇",
|
||
"账期": "电汇",
|
||
"承兑": "银行承兑汇票",
|
||
}
|
||
|
||
|
||
# 把 1/2、Y/N 收成页面上的「是」「否」。
|
||
def _yes_no(value: str) -> str:
|
||
raw = (value or "").strip()
|
||
return _YES_NO.get(raw.upper(), raw)
|
||
|
||
|
||
# 把合同类别别名收成页面下拉的实际选项。
|
||
def _category_option(value: str) -> str:
|
||
raw = (value or "").strip()
|
||
return _CATEGORY_ALIASES.get(raw, raw)
|
||
|
||
|
||
# 把付款方式别名收成页面下拉的实际选项。「账期支付」对应「电汇」。
|
||
def _payment_option(value: str) -> str:
|
||
raw = (value or "").strip()
|
||
return _PAYMENT_ALIASES.get(raw, raw)
|
||
|
||
# 货品明细行的字段后缀;行索引从 0 开始(fd_detail1.0.* 为第 1 行)
|
||
GOODS_FIELD_SUFFIXES = {
|
||
"goods_code": "fd_opcode",
|
||
"goods_name": "fd_goodsname",
|
||
"goods_mdm": "fd_mdmCode",
|
||
"goods_approval": "fd_registdocno",
|
||
"goods_spec": "fd_goodstype",
|
||
"goods_unit": "fd_goodsunit",
|
||
"goods_qty": "fd_goodsqty",
|
||
"goods_price": "fd_unitprice",
|
||
}
|
||
|
||
ADD_ROW_SELECTORS = [
|
||
"text=添加行",
|
||
"text=新增明细",
|
||
"text=新增行",
|
||
"button:contains(添加行)",
|
||
]
|
||
|
||
# 货品选择窗口:点「选择商品」弹出 EopBasedataMaterial 列表,按操作码/名称搜索回填。
|
||
# lui-id-* 每次加载会变,不要写死;用 title / 按钮文本定位。
|
||
GOODS_PICKER = {
|
||
"trigger": [
|
||
'div[title="选择商品"]',
|
||
'div.lui-component.lui_widget_btn_txt',
|
||
"text=选择商品",
|
||
],
|
||
"frame": [
|
||
'iframe[src*="EopBasedataMaterial"]',
|
||
'iframe[src*="dialog_fdMaterial"]',
|
||
'iframe[src*="common-template"]',
|
||
],
|
||
"search_input": ["input.search-box-text", "input[placeholder*='关键字']"],
|
||
"search_button": [
|
||
"input.search-box-btn",
|
||
"a.search-box-btn",
|
||
"div.search-box-btn",
|
||
"span.search-box-btn",
|
||
"input[value='搜索']",
|
||
"input[value='查询']",
|
||
"[class*='search-box'] [class*='btn']",
|
||
],
|
||
"result_checkbox": ['input[name="List_Selected"]'],
|
||
}
|
||
|
||
# 客商选择窗口(点击客户名称旁「选择」按钮弹出)。若真实页面改版,仅需调整此处。
|
||
CUSTOMER_PICKER = {
|
||
"trigger": [
|
||
'div.inputselectsgl[onclick*="fdCustomerEdit"]',
|
||
'div[class*="inputselect"][onclick*="fdCustomer"]',
|
||
],
|
||
"frame": [
|
||
'iframe[src*="common-template"]',
|
||
'iframe[src*="KmRelativeMain"]',
|
||
'iframe[src*="relative"]',
|
||
],
|
||
"search_input": ["input.search-box-text"],
|
||
"search_button": ["input.search-box-btn"],
|
||
"result_checkbox": ['input[name="List_Selected"]'],
|
||
"confirm_button": ["div.btn.ok", "div.lui_toolbar_btn_def"],
|
||
}
|
||
|
||
|
||
_PORTAL_MARKERS = ("切换门户", "待办", "待阅", "合同管理", "合同起草")
|
||
|
||
|
||
# 看页面是否已进门户。看到「切换门户」或待办/待阅即视为已登录。
|
||
def _is_logged_in(browser: BrowserAdapter) -> bool:
|
||
body = browser.read_body_text() or ""
|
||
return any(marker in body for marker in _PORTAL_MARKERS)
|
||
|
||
|
||
# 取签订日。接口 3 给了用它,否则回退 order_date。
|
||
def sign_date_for(ctx: ContractFlowContext) -> str:
|
||
return (ctx.contract_sign_date or ctx.order_date or "").strip()
|
||
|
||
|
||
# 取开始日。接口 3 给了用它,否则回退 order_date。
|
||
def begin_date_for(ctx: ContractFlowContext) -> str:
|
||
return (ctx.contract_start_date or ctx.order_date or "").strip()
|
||
|
||
|
||
# 关掉挡在表单前面的「关闭/取消」弹层。
|
||
def _close_blocking_dialogs(browser: BrowserAdapter) -> None:
|
||
browser.execute_script(
|
||
"""
|
||
const labels = ['关闭', '取消'];
|
||
function clickIn(win) {
|
||
const nodes = Array.from(win.document.querySelectorAll('div, a, button, span'));
|
||
for (const label of labels) {
|
||
const hit = nodes.find((el) => (el.textContent || '').trim() === label && el.children.length === 0);
|
||
if (hit) { hit.click(); return label; }
|
||
}
|
||
for (let i = 0; i < win.frames.length; i++) {
|
||
try { const hit = clickIn(win.frames[i]); if (hit) return hit; } catch (e) {}
|
||
}
|
||
return '';
|
||
}
|
||
return clickIn(window);
|
||
"""
|
||
)
|
||
time.sleep(0.4)
|
||
|
||
|
||
# 读当前页 URL,跨 iframe 时尽量取 top.location。
|
||
def _current_url(browser: BrowserAdapter) -> str:
|
||
try:
|
||
return str(
|
||
browser.execute_script(
|
||
"try { return (window.top.location && window.top.location.href) || location.href; }"
|
||
" catch (e) { return location.href; }"
|
||
)
|
||
or ""
|
||
)
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
# 从 URL、隐藏域或 Com_Parameter 读本单 fdId。
|
||
def _read_document_id(browser: BrowserAdapter) -> str:
|
||
try:
|
||
browser.switch_to_default_content()
|
||
except Exception:
|
||
pass
|
||
value = browser.execute_script(
|
||
"""
|
||
function fromHref(href) {
|
||
const fromUrl = String(href || '').match(/[?&]fdId=([^&]+)/i);
|
||
if (fromUrl && fromUrl[1]) return decodeURIComponent(fromUrl[1]);
|
||
return '';
|
||
}
|
||
function fromDoc(win) {
|
||
try {
|
||
const id = fromHref(win.location && win.location.href);
|
||
if (id) return id;
|
||
} catch (e) {}
|
||
try {
|
||
const input = win.document.querySelector('input[name="fdId"], input[name="fdid"]');
|
||
if (input && String(input.value || '').trim()) return String(input.value).trim();
|
||
} catch (e) {}
|
||
try {
|
||
if (win.Com_Parameter && win.Com_Parameter.fdId) return String(win.Com_Parameter.fdId);
|
||
} catch (e) {}
|
||
return '';
|
||
}
|
||
function walk(win) {
|
||
const id = fromDoc(win);
|
||
if (id) return id;
|
||
for (let i = 0; i < win.frames.length; i++) {
|
||
try {
|
||
const inner = walk(win.frames[i]);
|
||
if (inner) return inner;
|
||
} catch (e) {}
|
||
}
|
||
return '';
|
||
}
|
||
return walk(window);
|
||
"""
|
||
)
|
||
return str(value or "").strip()
|
||
|
||
|
||
# 用 fdId 打开合同详情页。没有 fdId 则什么都不做。
|
||
def _open_submitted_view(browser: BrowserAdapter, fd_id: str) -> None:
|
||
doc_id = (fd_id or "").strip() or _read_document_id(browser)
|
||
if not doc_id:
|
||
print("成功页没有 fdId,无法自动打开详情", flush=True)
|
||
return
|
||
url = VIEW_PAGE_URL.format(fd_id=doc_id)
|
||
print("打开合同详情:", url, flush=True)
|
||
browser.open_url(url)
|
||
time.sleep(1.5)
|
||
|
||
|
||
# 拼 LUI 工具栏按钮的精确文本 xpath,避免 contains 点到别的按钮。
|
||
def _lui_btn_xpath(label: str) -> str:
|
||
safe = (label or "").replace("'", "")
|
||
return (
|
||
"//*[contains(@class,'lui_widget_btn_txt') and normalize-space()='%s']" % safe
|
||
)
|
||
|
||
|
||
# 点 LUI 工具栏上指定文案的按钮。脚本点不到再走 native/first。
|
||
def _click_toolbar_button(browser: BrowserAdapter, label: str) -> bool:
|
||
try:
|
||
browser.switch_to_default_content()
|
||
except Exception:
|
||
pass
|
||
clicked = browser.execute_script(_LUI_TOOLBAR_CLICK_JS, label)
|
||
print(f"toolbar {label} js={clicked}", flush=True)
|
||
if isinstance(clicked, dict) and clicked.get("ok"):
|
||
return True
|
||
selectors = [_lui_btn_xpath(label)]
|
||
try:
|
||
browser.click_native(selectors)
|
||
print(f"toolbar {label} native=ok", flush=True)
|
||
return True
|
||
except Exception as exc:
|
||
print(f"toolbar {label} native={exc}", flush=True)
|
||
try:
|
||
browser.click_first(selectors)
|
||
print(f"toolbar {label} first=ok", flush=True)
|
||
return True
|
||
except Exception as exc:
|
||
print(f"toolbar {label} first={exc}", flush=True)
|
||
return False
|
||
|
||
|
||
_LUI_TOOLBAR_FIND_JS = """
|
||
const label = String(arguments[0] || '');
|
||
function visible(el) {
|
||
const box = el.getBoundingClientRect();
|
||
return box.width > 2 && box.height > 2;
|
||
}
|
||
function textOf(el) {
|
||
return (el.textContent || '').replace(/\\s+/g, '').trim();
|
||
}
|
||
function findIn(win) {
|
||
const nodes = Array.from(win.document.querySelectorAll(
|
||
'div.lui-component.lui_widget_btn_txt, .lui_widget_btn_txt'
|
||
));
|
||
const hit = nodes.find((el) => textOf(el) === label && visible(el));
|
||
if (hit) {
|
||
return {
|
||
ok: true,
|
||
cid: hit.getAttribute('data-lui-cid') || '',
|
||
id: hit.id || '',
|
||
count: nodes.length
|
||
};
|
||
}
|
||
for (let i = 0; i < win.frames.length; i++) {
|
||
try {
|
||
const inner = findIn(win.frames[i]);
|
||
if (inner && inner.ok) return inner;
|
||
} catch (e) {}
|
||
}
|
||
return {
|
||
ok: false,
|
||
texts: nodes.map((el) => textOf(el)).filter(Boolean).slice(0, 12)
|
||
};
|
||
}
|
||
return findIn(window);
|
||
"""
|
||
|
||
|
||
_LUI_TOOLBAR_CLICK_JS = """
|
||
const label = String(arguments[0] || '');
|
||
function visible(el) {
|
||
const box = el.getBoundingClientRect();
|
||
return box.width > 2 && box.height > 2;
|
||
}
|
||
function textOf(el) {
|
||
return (el.textContent || '').replace(/\\s+/g, '').trim();
|
||
}
|
||
function fire(el, win) {
|
||
const target = el.closest('[data-lui-cid]') || el;
|
||
try { target.scrollIntoView({block: 'center', inline: 'nearest'}); } catch (e) {}
|
||
['mouseover', 'mousedown', 'mouseup', 'click'].forEach((type) => {
|
||
target.dispatchEvent(new MouseEvent(type, {
|
||
bubbles: true, cancelable: true, view: win, buttons: 1
|
||
}));
|
||
});
|
||
target.click();
|
||
}
|
||
function clickIn(win) {
|
||
const nodes = Array.from(win.document.querySelectorAll(
|
||
'div.lui-component.lui_widget_btn_txt, .lui_widget_btn_txt'
|
||
));
|
||
const hit = nodes.find((el) => textOf(el) === label && visible(el));
|
||
if (hit) {
|
||
fire(hit, win);
|
||
return {
|
||
ok: true,
|
||
via: 'btn_txt',
|
||
cid: hit.getAttribute('data-lui-cid') || '',
|
||
id: hit.id || ''
|
||
};
|
||
}
|
||
for (let i = 0; i < win.frames.length; i++) {
|
||
try {
|
||
const inner = clickIn(win.frames[i]);
|
||
if (inner && inner.ok) return inner;
|
||
} catch (e) {}
|
||
}
|
||
return {
|
||
ok: false,
|
||
label: label,
|
||
texts: nodes.map((el) => textOf(el)).filter(Boolean).slice(0, 12)
|
||
};
|
||
}
|
||
return clickIn(window);
|
||
"""
|
||
|
||
|
||
# 工具栏上是否已经出现指定文案的可见按钮。
|
||
def _lui_toolbar_button_exists(browser: BrowserAdapter, label: str) -> bool:
|
||
found = browser.execute_script(_LUI_TOOLBAR_FIND_JS, label)
|
||
ok = isinstance(found, dict) and bool(found.get("ok"))
|
||
print(f"toolbar {label} exists={found}", flush=True)
|
||
return ok
|
||
|
||
|
||
# 等到工具栏出现指定按钮。超时返回 False。
|
||
def _wait_lui_toolbar_button(
|
||
browser: BrowserAdapter, label: str, timeout_seconds: float = 10
|
||
) -> bool:
|
||
deadline = time.time() + timeout_seconds
|
||
while time.time() < deadline:
|
||
if _lui_toolbar_button_exists(browser, label):
|
||
return True
|
||
time.sleep(0.4)
|
||
return False
|
||
|
||
|
||
# 点工具栏「提交」。
|
||
def _click_top_submit(browser: BrowserAdapter) -> bool:
|
||
return _click_toolbar_button(browser, "提交")
|
||
|
||
|
||
# 判断字符串是不是正式合同号。占位符、表头、手机号不算。
|
||
def _usable_contract_no(value: str) -> bool:
|
||
text = (value or "").strip()
|
||
if not text or text in ("提交时自动生成", "自动生成", "==请选择==", "[]"):
|
||
return False
|
||
if text in ("合同标题", "合同编号", "客户编码", "客户名称", "申请时间", "合同(协议)全称"):
|
||
return False
|
||
if text.startswith("HT"):
|
||
return True
|
||
if text.isdigit() and len(text) >= 10:
|
||
return True
|
||
return any(ch.isdigit() for ch in text) and len(text) >= 8 and " " not in text
|
||
|
||
|
||
# 读第一个匹配选择器的值。元素不存在或读失败返回空字符串。
|
||
def _try_read(browser: BrowserAdapter, selectors: list[str]) -> str:
|
||
if not browser.element_exists(selectors):
|
||
return ""
|
||
try:
|
||
return (browser.read_first(selectors, "") or "").strip()
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
# 合同版本类型的验收文案:非格式合同或格式合同。
|
||
def contract_version_label(ctx: ContractFlowContext) -> str:
|
||
return "非格式合同" if ctx.template_type == "NON_FORMAT" else "格式合同"
|