213 lines
9.2 KiB
Python
213 lines
9.2 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)
|
||
|
||
|
||
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(['input[name="btn_submit"]', 'a[href*="btn_submit"]', 'text=登录'])
|
||
browser.wait_for_text("合同", timeout_seconds=20)
|
||
|
||
|
||
def enter_contract_create(browser: BrowserAdapter) -> None:
|
||
browser.click_first(["text=合同管理", "aria=合同管理"])
|
||
browser.click_first(["text=合同起草", "aria=合同起草"])
|
||
browser.wait_for_text("最近使用模板", timeout_seconds=20)
|
||
|
||
|
||
def select_contract_template(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||
template_text = ctx.template_name or (
|
||
"非格式--采购协议" if ctx.template_type == "NON_FORMAT" else "购销协议书(2025)"
|
||
)
|
||
browser.click_first([f"text={template_text}", f"text={template_text}...", f"title={template_text}"])
|
||
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(
|
||
['label=客户编码', 'input[name="fdCustomerCode"]', 'input[placeholder*="客户编码"]'],
|
||
ctx.customer_code,
|
||
)
|
||
if customer_name:
|
||
browser.fill_first(
|
||
['label=客户名称', 'input[name="fdCustomerName"]', 'input[placeholder*="客户名称"]'],
|
||
customer_name,
|
||
)
|
||
browser.screenshot("contract-signatory-info-filled")
|
||
|
||
|
||
def fill_contract_basic_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||
browser.select_first(['label=合同版本类型', 'select[name="fdTemplateType"]'], contract_version_label(ctx))
|
||
if ctx.contract_category:
|
||
browser.select_first(['label=合同类别', 'select[name="fdContractCategory"]'], ctx.contract_category)
|
||
browser.select_first(['label=结算类型', 'select[name="fdSettlementType"]'], ctx.settlement_type)
|
||
if ctx.contract_background:
|
||
browser.fill_first(
|
||
['label=合同背景概述', 'textarea[name="fdContractBackground"]'],
|
||
ctx.contract_background,
|
||
)
|
||
browser.click_first([f"label={ctx.contract_nature}", f"text={ctx.contract_nature}"])
|
||
browser.screenshot("contract-basic-info-filled")
|
||
|
||
|
||
def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||
browser.select_first(['label=合同金额', 'select[name="fdCurrency"]'], ctx.amount_currency)
|
||
if ctx.amount:
|
||
browser.fill_first(['label=金额(元)', 'input[name="fdAmount"]'], ctx.amount)
|
||
browser.fill_first(
|
||
['label=合同签订日期', 'input[name="fdSignDate"]', 'input[placeholder*="合同签订"]'],
|
||
ctx.order_date,
|
||
)
|
||
browser.fill_first(
|
||
['label=合同开始日期', 'input[name="fdStartDate"]', 'input[placeholder*="合同开始"]'],
|
||
ctx.order_date,
|
||
)
|
||
if ctx.contract_end_date:
|
||
browser.fill_first(
|
||
['label=合同结束日期', 'input[name="fdEndDate"]', 'input[placeholder*="合同结束"]'],
|
||
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=新增行"])
|
||
prefix = f"明细{index}"
|
||
browser.fill_first([f'label={prefix}操作码', 'input[name="operationCode"]'], line.operation_code)
|
||
browser.fill_first([f'label={prefix}货品编码', 'input[name="goodsCode"]'], line.goods_code)
|
||
browser.fill_first([f'label={prefix}名称', 'input[name="goodsName"]'], line.goods_name)
|
||
browser.fill_first([f'label={prefix}国药准字', 'input[name="approvalNo"]'], line.approval_no)
|
||
browser.fill_first([f'label={prefix}规格', 'input[name="spec"]'], line.spec)
|
||
browser.fill_first([f'label={prefix}单位', 'input[name="unit"]'], line.unit)
|
||
browser.fill_first([f'label={prefix}数量', 'input[name="quantity"]'], str(line.quantity))
|
||
browser.screenshot("contract-goods-lines-filled")
|
||
|
||
|
||
def fill_contract_payment_info(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||
browser.select_first(['label=是否重大合同', 'select[name="fdIsMajorContract"]'], ctx.is_major_contract)
|
||
browser.select_first(['label=是否预付款', 'select[name="fdIsPrepayment"]'], ctx.is_prepayment)
|
||
if ctx.belong_year:
|
||
browser.fill_first(['label=合同归属年份', 'input[name="fdBelongYear"]'], ctx.belong_year)
|
||
browser.select_first(['label=付款方式', 'select[name="fdPaymentMethod"]', 'select[name="paymentMethod"]'], ctx.payment_method)
|
||
browser.fill_first(['label=付款期限', 'input[name="fdPaymentDays"]', 'input[name="paymentDays"]'], ctx.payment_days)
|
||
browser.select_first(['label=合同状态', 'select[name="fdContractStatus"]'], ctx.contract_status)
|
||
browser.select_first(
|
||
['label=是否包含血液制品', 'select[name="fdContainsBloodProducts"]'],
|
||
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 "格式合同"
|