247 lines
11 KiB
Python
247 lines
11 KiB
Python
"""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 "格式合同"
|