AutoRpa/rpa_contract/contract_form.py

295 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 合同 S06–S08、S10:签约方、尽调、基础信息、金额日期、付款。
#
# 客商必须是合同系统真实操作码。正文含「未尽调」则本单失败,不提交。
from __future__ import annotations
import time
from accept import evidence_shot
from errors import DiligenceError, NodeError, SupplierContractDisabled
from contract_common import (
CUSTOMER_PICKER,
F,
X,
BrowserAdapter,
ContractFlowContext,
_ACCEPT,
_category_option,
_expected_title,
_payment_option,
_try_read,
_yes_no,
begin_date_for,
sign_date_for,
)
# 填签约方(S06):按客商操作码在弹窗里选客商并校验名称。
def fill_contract_signatory_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
expected_name = (ctx.customer_name or ctx.supplier_name).strip()
if not ctx.customer_code:
raise SupplierContractDisabled("客商操作码(customerCode)为空,无法加载客商")
# 点击客户名称字段旁的「选择」按钮打开客商选择窗口,按编码搜索并带回客商
browser.click_first(CUSTOMER_PICKER["trigger"])
_pick_customer_from_dialog(browser, ctx.customer_code)
loaded_name = _wait_customer_name(browser, timeout=8)
if not loaded_name:
evidence_shot(browser, "contract-customer-load-failed", failed=True)
raise SupplierContractDisabled(
f"客商加载失败,请确认输入的客商操作码是否有误:{ctx.customer_code}"
)
if expected_name and loaded_name != expected_name:
evidence_shot(browser, "contract-customer-name-mismatch", failed=True)
raise SupplierContractDisabled(
f"客商名称校验不通过:期望「{expected_name}」,实际「{loaded_name}」"
)
evidence_shot(browser, "contract-signatory-info-filled")
loaded_code = _try_read(browser, [F["customer_code"]])
_ACCEPT.check(
"S06",
"签约方",
[
("customerName", loaded_name, expected_name),
("customerCode", loaded_code, ctx.customer_code),
],
browser=browser,
screenshot_on_fail="contract-customer-name-mismatch",
)
# 等客户名称回填。加载失败文案出现时返回空字符串。
def _wait_customer_name(browser: BrowserAdapter, timeout: int) -> str:
deadline = time.time() + timeout
while time.time() < deadline:
body = browser.read_body_text()
if "自动加载客商失败" in body or "客商操作码是否有误" in body:
return ""
name = _try_read(browser, [F["customer_name"]])
if name and name not in ("==请选择==", "请选择"):
return name
time.sleep(0.5)
return ""
# 在客商选择 iframe 里按操作码搜并勾中对应行。
def _pick_customer_from_dialog(browser: BrowserAdapter, code: str) -> None:
# 客商选择窗口是一个 iframe 弹窗:切入后搜索编码 → 按编码点对应行 → 切回主文档。
browser.wait_for_element(CUSTOMER_PICKER["frame"], timeout_seconds=10)
browser.switch_to_frame(CUSTOMER_PICKER["frame"])
try:
browser.fill_first(CUSTOMER_PICKER["search_input"], code)
browser.click_first(CUSTOMER_PICKER["search_button"])
time.sleep(1.5)
browser.wait_for_element(CUSTOMER_PICKER["result_checkbox"], timeout_seconds=10)
picked = browser.execute_script(
"""
const code = String(arguments[0] || '');
const rows = Array.from(document.querySelectorAll('tr'));
for (const row of rows) {
const text = (row.innerText || '');
if (text.indexOf(code) < 0) continue;
const box = row.querySelector('input[name="List_Selected"]');
if (box) { box.click(); return {ok: true, text: text.replace(/\\s+/g, ' ').slice(0, 80)}; }
}
return {ok: false};
""",
code,
)
if not (isinstance(picked, dict) and picked.get("ok")):
browser.click_first(CUSTOMER_PICKER["result_checkbox"])
time.sleep(1)
finally:
browser.switch_to_default_content()
# 填基础信息(S07):类别、封面模板、结算、背景、标题。
def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
if ctx.contract_category:
browser.select_xform(X["contract_category"], _category_option(ctx.contract_category))
_select_cover_template_if_needed(browser)
if ctx.settlement_type:
_select_settlement_type(browser, ctx.settlement_type)
if ctx.contract_background:
browser.fill_first([F["contract_background"]], ctx.contract_background)
# 合同性质默认主合同(页面已勾选),仅在需要从合同时切换
if ctx.contract_nature and ctx.contract_nature != "主合同":
browser.click_first(['input[name="fdType"][value="01"]'])
_fill_contract_title(browser, ctx)
evidence_shot(browser, "contract-basic-info-filled")
_ACCEPT.check(
"S07",
"基础信息",
[
("category", _read_xform_label(browser, X["contract_category"]), _category_option(ctx.contract_category) if ctx.contract_category else ""),
("settlement", _try_read(browser, [F["settlement_type"]]), ctx.settlement_type),
("background", _try_read(browser, [F["contract_background"]]), ctx.contract_background),
("title", _try_read(browser, [F["doc_subject"]]), _expected_title(ctx)),
],
browser=browser,
screenshot_on_fail="contract-basic-info-filled",
)
# 把合同标题写入 docSubject。空则按客商+类别+订单日拼。
def _fill_contract_title(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
title = (ctx.contract_title or "").strip()
if not title:
who = (ctx.customer_name or ctx.supplier_name or "").strip()
cate = (ctx.contract_category or "采购协议").strip()
date = (ctx.order_date or "").replace("-", "")
title = f"{who}{cate}{date}"
if title:
browser.fill_first([F["doc_subject"]], title)
# 封面模板未选时优先选「否」,避免原合同申请编号变必填。
def _select_cover_template_if_needed(browser: BrowserAdapter) -> None:
# 常用模板/已审核版本:优先选「否」,避免连带「原合同申请编号」变必填
current = _read_xform_label(browser, X["cover_template"])
if current and current not in ("==请选择==", "请选择"):
return
picked = browser.execute_script(
"""
const name = arguments[0];
const flag = document.querySelector('xformflag[property="' + name + '"]');
if (!flag) return '';
const opts = Array.from(flag.querySelectorAll('.fs-option')).map((o) => ({
value: (o.getAttribute('data-value') || '').trim(),
text: ((o.querySelector('.fs-option-label') || o).textContent || '').trim(),
})).filter((o) => o.value || (o.text && o.text.indexOf('请选择') < 0));
const preferNo = opts.find((o) => o.text === '否' || o.value === '2' || o.value === '0');
const hit = preferNo || opts[0];
return hit ? (hit.text || hit.value) : '';
""",
X["cover_template"],
)
if picked:
browser.select_xform(X["cover_template"], picked)
# 读 xform fancy-select 当前可见标签。读不到返回空字符串。
def _read_xform_label(browser: BrowserAdapter, name: str) -> str:
try:
return (
browser.execute_script(
"""
const flag = document.querySelector('xformflag[property="' + arguments[0] + '"]');
return flag ? ((flag.querySelector('.fs-label') || {}).textContent || '').trim() : '';
""",
name,
)
or ""
).strip()
except Exception:
return ""
# 选结算类型。原生 select 未中则按可见文本点 LUI 下拉。
def _select_settlement_type(browser: BrowserAdapter, value: str) -> None:
browser.select_first([F["settlement_type"]], value)
selected = _try_read(browser, [F["settlement_type"]])
if not selected or selected in ("==请选择==", "请选择"):
# LUI 自定义下拉兜底:点击展开后按可见文本选择
browser.click_first([F["settlement_type"]])
browser.click_first([f"//option[contains(text(),'{value}')]", f"text={value}"])
selected = _try_read(browser, [F["settlement_type"]])
if not selected or selected in ("==请选择==", "请选择"):
raise NodeError(f"结算方式下拉未选中,仍停留在「==请选择==」,期望「{value}」")
# 尽调闸门。页面正文含「未尽调」则失败,不提交。
def check_customer_diligence(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
text = browser.read_body_text() or ""
if "未尽调" in text:
raise DiligenceError("客户未尽调")
# 填金额与签订日、开始日、结束日(S08)。
def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
amount_type = (ctx.amount_type or "合同标的金额").strip()
if amount_type:
browser.select_xform(X["amount_type"], amount_type)
if ctx.amount:
browser.fill_first([F["amount"]], ctx.amount)
sign_date = sign_date_for(ctx)
begin_date = begin_date_for(ctx)
browser.fill_first([F["sign_date"]], sign_date)
browser.fill_first([F["begin_date"]], begin_date)
if ctx.contract_end_date:
browser.fill_first([F["end_date"]], ctx.contract_end_date)
evidence_shot(browser, "contract-amount-and-dates-filled")
_ACCEPT.check(
"S08",
"金额日期",
[
("amount", _try_read(browser, [F["amount"]]), ctx.amount),
("signDate", _try_read(browser, [F["sign_date"]]), sign_date),
("beginDate", _try_read(browser, [F["begin_date"]]), begin_date),
("endDate", _try_read(browser, [F["end_date"]]), ctx.contract_end_date),
],
browser=browser,
screenshot_on_fail="contract-amount-and-dates-filled",
)
# 填付款与标志(S10)。这期一律账期,不勾预付款。
def fill_contract_payment_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
browser.select_xform(X["is_major_contract"], _yes_no(ctx.is_major_contract))
browser.select_xform(X["is_prepayment"], _yes_no(ctx.is_prepayment))
if ctx.belong_year:
browser.fill_first([F["belong_year"]], ctx.belong_year)
if ctx.payment_method:
browser.select_xform(X["payment_method"], _payment_option(ctx.payment_method))
if ctx.payment_days:
browser.fill_first([F["payment_days"]], ctx.payment_days)
if ctx.contract_status:
browser.select_xform(X["contract_status"], ctx.contract_status)
if ctx.contains_blood_products:
browser.select_xform(X["contains_blood"], _yes_no(ctx.contains_blood_products))
_fill_concession_defaults(browser)
evidence_shot(browser, "contract-payment-filled")
_ACCEPT.check(
"S10",
"付款信息",
[
("major", _read_xform_label(browser, X["is_major_contract"]), _yes_no(ctx.is_major_contract)),
("prepay", _read_xform_label(browser, X["is_prepayment"]), _yes_no(ctx.is_prepayment)),
("year", _try_read(browser, [F["belong_year"]]), ctx.belong_year),
("payMethod", _read_xform_label(browser, X["payment_method"]), _payment_option(ctx.payment_method) if ctx.payment_method else ""),
("payDays", _try_read(browser, [F["payment_days"]]), ctx.payment_days),
("status", _read_xform_label(browser, X["contract_status"]), ctx.contract_status),
],
browser=browser,
screenshot_on_fail="contract-payment-filled",
)
# 让步情形等是/否单选未填时默认点「否」,避免挡住提交。
def _fill_concession_defaults(browser: BrowserAdapter) -> None:
# 让步情形等是/否单选未填会挡住提交;默认点「否」
browser.execute_script(
"""
const clicked = [];
document.querySelectorAll('tr').forEach((tr) => {
const radios = Array.from(tr.querySelectorAll('input[type="radio"]'));
if (radios.length < 2) return;
const labeled = radios.map((r) => ({
el: r,
text: ((r.closest('label') || r.parentElement || {}).textContent || '')
.replace(/\\s+/g, '').trim(),
}));
if (!labeled.some((x) => x.text === '是') || !labeled.some((x) => x.text === '否')) return;
if (radios.some((r) => r.checked)) return;
const no = labeled.find((x) => x.text === '否');
if (no) { no.el.click(); clicked.push(radios[0].name); }
});
return clicked;
"""
)