"""Flowchart-sized CONTRACT_CREATE browser steps for YiSaiQi. Each function is intentionally small enough to map to one YiSaiQi flowchart node or one script block behind a flowchart node. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Protocol class BrowserAdapter(Protocol): 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 wait_for_text(self, text: str, timeout_seconds: int = 15) -> None: ... def select_first(self, selectors: list[str], value: str) -> None: ... def upload_file_first(self, selectors: list[str], file_path: str) -> None: ... def read_first(self, selectors: list[str], value_name: str) -> str: ... def screenshot(self, name: str) -> str: ... @dataclass(frozen=True) class ContractGoodsLine: operation_code: str goods_code: str goods_name: str approval_no: str spec: str unit: str quantity: float @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: str = "" contract_end_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 def run_contract_create_flow(browser: BrowserAdapter, ctx: ContractFlowContext) -> str: open_contract_system(browser, ctx) login_contract_system(browser, ctx) enter_contract_create(browser) select_contract_template(browser, ctx) fill_contract_signatory_info(browser, ctx) fill_contract_basic_info(browser, ctx) fill_contract_amount_and_dates(browser, ctx) fill_contract_goods_lines(browser, ctx) fill_contract_payment_info(browser, ctx) upload_contract_document_if_needed(browser, ctx) if not submit_contract_for_approval(browser, ctx): return "" return read_contract_number(browser) # 合同起草页(模板选择)直达 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" ) # 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)"]', "goods_code": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_opcode)"]', "goods_name": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsname)"]', "goods_mdm": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_mdmCode)"]', "goods_approval": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_registdocno)"]', "goods_spec": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodstype)"]', "goods_unit": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsunit)"]', "goods_qty": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)"]', "goods_price": 'input[name="extendDataFormInfo.value(fd_detail1.0.fd_unitprice)"]', } def open_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: browser.open_url(ctx.contract_url) def login_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: browser.fill_first(['input[name="j_username"]', 'input[placeholder="用户名"]'], ctx.username) browser.fill_first(['input[name="j_password"]', 'input[placeholder="密码"]'], ctx.password) browser.click_first(['a[class*="login_button"]', 'a:contains("登录")', 'input[name="btn_submit"]']) browser.wait_for_text("合同", timeout_seconds=20) def enter_contract_create(browser: BrowserAdapter) -> None: # 登录后直接导航到起草表单页,跳过模板选择(前端委托事件 Selenium 点击无效) browser.open_url(FORM_PAGE_URL) browser.wait_for_text("客户编码", timeout_seconds=20) def select_contract_template(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: # 已通过 URL 直接指定模板,本步骤仅兜底:若尚未进入表单则再次导航 if "客户编码" not in browser.read_body_text(): browser.open_url(FORM_PAGE_URL) browser.wait_for_text("客户编码", timeout_seconds=20) def fill_contract_signatory_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: customer_name = ctx.customer_name or ctx.supplier_name if ctx.customer_code: browser.fill_first([F["customer_code"]], ctx.customer_code) if customer_name: browser.fill_first([F["customer_name"]], customer_name) browser.screenshot("contract-signatory-info-filled") def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: if ctx.contract_category: browser.fill_first([F["contract_category"]], ctx.contract_category) if ctx.settlement_type: browser.select_first([F["settlement_type"]], 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"]']) browser.screenshot("contract-basic-info-filled") def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: if ctx.amount: browser.fill_first([F["amount"]], ctx.amount) browser.fill_first([F["sign_date"]], ctx.order_date) browser.fill_first([F["begin_date"]], ctx.order_date) if ctx.contract_end_date: browser.fill_first([F["end_date"]], ctx.contract_end_date) browser.screenshot("contract-amount-and-dates-filled") def fill_contract_goods_lines(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: for index, line in enumerate(ctx.goods_lines, start=1): if index > 1: browser.click_first(["text=添加行", "text=新增明细", "text=新增行"]) browser.fill_first([F["goods_code"]], line.goods_code) browser.fill_first([F["goods_name"]], line.goods_name) if line.approval_no: browser.fill_first([F["goods_approval"]], line.approval_no) if line.spec: browser.fill_first([F["goods_spec"]], line.spec) if line.unit: browser.fill_first([F["goods_unit"]], line.unit) browser.fill_first([F["goods_qty"]], str(line.quantity)) browser.screenshot("contract-goods-lines-filled") def fill_contract_payment_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: browser.fill_first([F["is_major_contract"]], ctx.is_major_contract) browser.fill_first([F["is_prepayment"]], ctx.is_prepayment) if ctx.belong_year: browser.fill_first([F["belong_year"]], ctx.belong_year) if ctx.payment_method: browser.fill_first([F["payment_method"]], ctx.payment_method) if ctx.payment_days: browser.fill_first([F["payment_days"]], ctx.payment_days) if ctx.contract_status: browser.fill_first([F["contract_status"]], ctx.contract_status) if ctx.contains_blood_products: browser.fill_first([F["contains_blood"]], ctx.contains_blood_products) browser.screenshot("contract-payment-filled") def upload_contract_document_if_needed(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: if ctx.source_type != "YYC": return if not ctx.merged_pdf_path: raise ValueError("YYC contract creation requires merged_pdf_path before upload") browser.upload_file_first(['input[type="file"]', "label=合同证文上传"], ctx.merged_pdf_path) browser.screenshot("contract-pdf-uploaded") def submit_contract_for_approval(browser: BrowserAdapter, ctx: ContractFlowContext) -> bool: if ctx.submit_mode == "dry_run": browser.screenshot("contract-dry-run-ready") return False browser.click_first(["text=提交", "text=提交送审", "text=送审"]) browser.wait_for_text("合同编号", timeout_seconds=30) browser.screenshot("contract-submitted") return True def read_contract_number(browser: BrowserAdapter) -> str: return browser.read_first( ['input[name="contractNo"]', 'input[name="contractSystemNo"]', "label=合同编号", "text=合同编号"], "contract_number", ) def contract_version_label(ctx: ContractFlowContext) -> str: return "非格式合同" if ctx.template_type == "NON_FORMAT" else "格式合同"