# 合同 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; """ )