AutoRpa/rpa_contract/contract_create_node.py

332 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
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())