295 lines
13 KiB
Python
295 lines
13 KiB
Python
# 合同 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;
|
||
"""
|
||
)
|