334 lines
12 KiB
Python
334 lines
12 KiB
Python
"""CONTRACT_CREATE: 入参组装 + Selenium 起草。结果返回合同号,不回调旧状态中心。"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
from dataclasses import dataclass, replace
|
||
from pathlib import Path
|
||
from typing import Any, Callable
|
||
|
||
from apa_browser_adapter import ApaBrowserAdapter
|
||
from contract_draft import (
|
||
ContractDraft,
|
||
contract_draft_from_dict,
|
||
is_placeholder_customer_code,
|
||
load_contract_draft,
|
||
)
|
||
from contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
||
from errors import NodeError, PdfMergeUnsupported, UnknownRpaError
|
||
from pdf_utils import ensure_merged_pdf
|
||
|
||
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
|
||
|
||
|
||
class ContractCreateError(NodeError):
|
||
"""入参缺失 / 页面元素找不到 / 页面改版。"""
|
||
|
||
|
||
def default_contract_input_path() -> str:
|
||
return str(
|
||
Path(__file__).resolve().parent / "examples" / "contract_create_input_dry_run.json"
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ContractCreateInput:
|
||
job_id: str
|
||
supplier_name: str
|
||
order_date: str
|
||
source_type: str
|
||
contract_system_no: str | None = None
|
||
attempt_no: int = 1
|
||
contract_url: str = CONTRACT_URL
|
||
username: str = "test002"
|
||
password: str = "1"
|
||
submit_mode: str = "submit"
|
||
order_no_list: tuple[str, ...] = ()
|
||
template_name: str = ""
|
||
draft_path: str | None = None
|
||
merged_pdf_output: str | None = None
|
||
flow_context: ContractFlowContext | None = None
|
||
|
||
|
||
ContractRunner = Callable[[ContractCreateInput], str]
|
||
|
||
|
||
def execute_contract_create(
|
||
spec: ContractCreateInput,
|
||
*,
|
||
contract_runner: ContractRunner | None = None,
|
||
) -> dict[str, Any]:
|
||
runner = contract_runner or create_contract_in_contract_system
|
||
|
||
if spec.submit_mode == "dry_run":
|
||
runner(spec)
|
||
return {"status": "dry_run", "submitted": False}
|
||
|
||
try:
|
||
contract_no = runner(spec)
|
||
except NodeError:
|
||
raise
|
||
except Exception as exc:
|
||
raise UnknownRpaError(str(exc)) from exc
|
||
if not contract_no:
|
||
raise ContractCreateError("submit 模式未读取到真实合同编号")
|
||
return {
|
||
"status": "done",
|
||
"contractSystemNo": contract_no,
|
||
"summary": _build_summary(spec),
|
||
}
|
||
|
||
|
||
def create_contract_in_contract_system(spec: ContractCreateInput) -> str:
|
||
"""在合同系统中起草合同并返回合同编号。"""
|
||
if spec.contract_system_no:
|
||
return spec.contract_system_no
|
||
|
||
ctx = spec.flow_context
|
||
if ctx is None:
|
||
if not spec.draft_path:
|
||
raise ContractCreateError("缺少 flow_context 或 draft_path,无法起草合同")
|
||
ctx = build_flow_context(spec, load_contract_draft(spec.draft_path))
|
||
|
||
browser = ApaBrowserAdapter(url=ctx.contract_url, flow="CONTRACT_CREATE")
|
||
return run_contract_create_flow(browser, ctx)
|
||
|
||
|
||
def build_spec_from_data(data: dict[str, Any]) -> ContractCreateInput:
|
||
draft = contract_draft_from_dict(data)
|
||
_validate_data(data, draft)
|
||
|
||
spec = ContractCreateInput(
|
||
job_id=_require(data, "jobId"),
|
||
supplier_name=draft.supplier_name or _require(data, "supplierName"),
|
||
order_date=draft.order_date or _require(data, "orderDate"),
|
||
source_type=draft.source_type or _require(data, "sourceType"),
|
||
contract_system_no=_opt(data.get("contractSystemNo")),
|
||
attempt_no=int(data.get("attemptNo", 1) or 1),
|
||
contract_url=_clean(data.get("contractUrl")) or os.getenv("CONTRACT_URL", CONTRACT_URL),
|
||
username=_clean(data.get("username")) or os.getenv("CONTRACT_USERNAME", "test002"),
|
||
password=str(data.get("password") or os.getenv("CONTRACT_PASSWORD", "1")),
|
||
submit_mode=draft.submit_mode,
|
||
order_no_list=tuple(draft.order_no_list),
|
||
template_name=draft.template_name,
|
||
merged_pdf_output=_opt(data.get("mergedPdfOutput")) or os.getenv("MERGED_PDF_OUTPUT"),
|
||
)
|
||
|
||
ctx = build_flow_context(spec, draft)
|
||
return replace(spec, flow_context=ctx)
|
||
|
||
|
||
def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> ContractFlowContext:
|
||
merged_pdf_path = _resolve_merged_pdf(draft, spec)
|
||
return ContractFlowContext(
|
||
contract_url=spec.contract_url,
|
||
username=spec.username,
|
||
password=spec.password,
|
||
supplier_name=draft.supplier_name or spec.supplier_name,
|
||
order_date=draft.order_date or spec.order_date,
|
||
source_type=draft.source_type or spec.source_type,
|
||
template_type=draft.template_type,
|
||
template_name=draft.template_name,
|
||
customer_code=draft.customer_code,
|
||
customer_name=draft.customer_name,
|
||
contract_category=draft.contract_category,
|
||
settlement_type=draft.settlement_type,
|
||
contract_background=draft.contract_background,
|
||
contract_nature=draft.contract_nature,
|
||
amount_currency=draft.amount_currency,
|
||
amount_type=draft.amount_type,
|
||
amount=draft.amount,
|
||
contract_title=draft.contract_title,
|
||
contract_end_date=draft.contract_end_date,
|
||
contract_sign_date=draft.contract_sign_date,
|
||
contract_start_date=draft.contract_start_date,
|
||
is_major_contract=draft.is_major_contract,
|
||
is_prepayment=draft.is_prepayment,
|
||
belong_year=draft.belong_year,
|
||
payment_method=draft.payment_method,
|
||
payment_days=draft.payment_days,
|
||
contract_status=draft.contract_status,
|
||
contains_blood_products=draft.contains_blood_products,
|
||
submit_mode=draft.submit_mode,
|
||
goods_lines=draft.goods_lines,
|
||
merged_pdf_path=merged_pdf_path,
|
||
contract_document_path=draft.contract_document_path,
|
||
order_no_list=draft.order_no_list,
|
||
)
|
||
|
||
|
||
def _resolve_merged_pdf(draft: ContractDraft, spec: ContractCreateInput) -> str | None:
|
||
if draft.source_type != "YYC":
|
||
return None
|
||
if spec.submit_mode == "dry_run":
|
||
# dry_run 只跑到提交前,不校验也不上传 PDF
|
||
return None
|
||
|
||
paths = [str(p).strip() for p in draft.platform_pdf_paths if str(p).strip()]
|
||
if not paths:
|
||
if draft.contract_document_path:
|
||
return None
|
||
raise PdfMergeUnsupported("YYC 缺少平台合同 PDF(platformPdfPaths 为空)")
|
||
for path in paths:
|
||
if not Path(path).exists():
|
||
raise PdfMergeUnsupported(f"平台合同 PDF 不存在:{path}")
|
||
if len(paths) == 1:
|
||
return paths[0]
|
||
|
||
output_path = spec.merged_pdf_output or str(
|
||
Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf"
|
||
)
|
||
try:
|
||
return ensure_merged_pdf(paths, output_path)
|
||
except Exception as exc:
|
||
raise PdfMergeUnsupported(f"YYC 平台合同 PDF 合并失败:{exc}") from exc
|
||
|
||
|
||
def _build_summary(spec: ContractCreateInput) -> dict[str, Any]:
|
||
return {
|
||
"supplierName": spec.supplier_name,
|
||
"contractDate": spec.order_date,
|
||
"sourceType": spec.source_type,
|
||
"orderNoList": list(spec.order_no_list),
|
||
"templateName": spec.template_name,
|
||
"pdfMerged": spec.source_type == "YYC",
|
||
"submitted": True,
|
||
}
|
||
|
||
|
||
def _validate_data(data: dict[str, Any], draft: ContractDraft) -> None:
|
||
missing = []
|
||
for key in ("jobId", "supplierName", "orderDate", "sourceType", "customerCode"):
|
||
if not _str(data.get(key)):
|
||
missing.append(key)
|
||
|
||
customer_code = draft.customer_code or _str(data.get("customerCode"))
|
||
if customer_code and is_placeholder_customer_code(customer_code):
|
||
raise ContractCreateError(
|
||
f"customerCode「{customer_code}」是占位符,合同系统客商选择窗搜不到。"
|
||
"请换成测试环境真实存在的客商操作码,例如 DW0100046361(重庆亿昊药业有限公司)"
|
||
)
|
||
|
||
if draft.submit_mode not in ("dry_run", "submit"):
|
||
raise ContractCreateError(f"submitMode 非法:{draft.submit_mode}(仅支持 dry_run / submit)")
|
||
|
||
if not _str(draft.amount):
|
||
missing.append("amount")
|
||
else:
|
||
try:
|
||
float(draft.amount)
|
||
except (TypeError, ValueError):
|
||
raise ContractCreateError(f"amount 不是合法数字:{draft.amount}")
|
||
|
||
if not draft.goods_lines:
|
||
raise ContractCreateError("goodsLines 为空,至少需要一行货品明细")
|
||
for index, line in enumerate(draft.goods_lines, start=1):
|
||
if not _str(line.goods_code) or not _str(line.goods_name) or line.quantity <= 0:
|
||
raise ContractCreateError(
|
||
f"goodsLines[{index}] 缺少 goodsCode/goodsName,或 quantity<=0"
|
||
)
|
||
|
||
if missing:
|
||
raise ContractCreateError("缺少必填字段:" + ", ".join(missing))
|
||
|
||
|
||
def _require(data: dict[str, Any], key: str) -> str:
|
||
value = _str(data.get(key))
|
||
if not value:
|
||
raise ContractCreateError(f"缺少必填字段:{key}")
|
||
return value
|
||
|
||
|
||
def _opt(value: Any) -> str | None:
|
||
value = _str(value)
|
||
return value or None
|
||
|
||
|
||
def _str(value: Any) -> str:
|
||
if value is None:
|
||
return ""
|
||
return str(value).strip()
|
||
|
||
|
||
def _clean(value: Any) -> str:
|
||
value = _str(value)
|
||
if len(value) >= 2 and value[0] == "`" and value[-1] == "`":
|
||
value = value[1:-1].strip()
|
||
return value
|
||
|
||
|
||
def load_input_data(raw: str) -> dict[str, Any]:
|
||
"""解析入参:优先按 JSON 字符串,其次按 JSON 文件路径。"""
|
||
raw = (raw or "").strip()
|
||
if not raw:
|
||
fallback = default_contract_input_path()
|
||
if Path(fallback).is_file():
|
||
print("未传入 -i/--input,使用默认联调入参:", fallback, flush=True)
|
||
raw = fallback
|
||
else:
|
||
return {}
|
||
try:
|
||
obj = json.loads(raw)
|
||
if isinstance(obj, dict):
|
||
return obj
|
||
except Exception:
|
||
pass
|
||
if os.path.isfile(raw):
|
||
with open(raw, "r", encoding="utf-8") as file:
|
||
return json.load(file)
|
||
raise ContractCreateError(f"无法解析入参:既不是合法 JSON,也不是存在的文件路径:{raw}")
|
||
|
||
|
||
def run_contract_create(data: dict[str, Any]) -> dict[str, Any]:
|
||
spec = build_spec_from_data(data)
|
||
return execute_contract_create(spec)
|
||
|
||
|
||
def parse_args() -> argparse.Namespace:
|
||
parser = argparse.ArgumentParser(description="合同系统页面起草。")
|
||
parser.add_argument("--input-json", default=os.getenv("CONTRACT_INPUT_JSON"))
|
||
parser.add_argument("--job-id")
|
||
parser.add_argument("--supplier-name")
|
||
parser.add_argument("--order-date")
|
||
parser.add_argument("--source-type", choices=("YYC", "XYY", "OFFLINE"), default="YYC")
|
||
parser.add_argument("--contract-system-no", default=os.getenv("CONTRACT_SYSTEM_NO"))
|
||
parser.add_argument("--attempt-no", type=int, default=1)
|
||
parser.add_argument("--contract-url", default=os.getenv("CONTRACT_URL", CONTRACT_URL))
|
||
parser.add_argument("--username", default=os.getenv("CONTRACT_USERNAME", "test002"))
|
||
parser.add_argument("--password", default=os.getenv("CONTRACT_PASSWORD", "1"))
|
||
parser.add_argument("--draft-path", default=os.getenv("CONTRACT_DRAFT_PATH"))
|
||
parser.add_argument("--merged-pdf-output", default=os.getenv("MERGED_PDF_OUTPUT"))
|
||
return parser.parse_args()
|
||
|
||
|
||
def main() -> int:
|
||
args = parse_args()
|
||
|
||
if args.input_json:
|
||
spec = build_spec_from_data(load_input_data(args.input_json))
|
||
else:
|
||
if not (args.job_id and args.supplier_name and args.order_date):
|
||
raise SystemExit("缺少 --job-id/--supplier-name/--order-date 或 --input-json")
|
||
spec = ContractCreateInput(
|
||
job_id=args.job_id,
|
||
supplier_name=args.supplier_name,
|
||
order_date=args.order_date,
|
||
source_type=args.source_type,
|
||
contract_system_no=args.contract_system_no,
|
||
attempt_no=args.attempt_no,
|
||
contract_url=args.contract_url,
|
||
username=args.username,
|
||
password=args.password,
|
||
draft_path=args.draft_path,
|
||
merged_pdf_output=args.merged_pdf_output,
|
||
)
|
||
|
||
result = execute_contract_create(spec)
|
||
print(json.dumps(result, ensure_ascii=False))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|