AutoRpa/codes/rpa_yisaiqi/contract_create_node.py

355 lines
13 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
import uuid
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Any, Callable
from .apa_browser_adapter import ApaBrowserAdapter
from .callback_client import StateCenterClient
from .contract_draft import ContractDraft, contract_draft_from_dict, load_contract_draft
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
from .errors import (
ContractCreateError,
LoginFailed,
PdfMergeUnsupported,
SupplierContractDisabled,
UnknownRpaError,
)
from .pdf_utils import ensure_merged_pdf
NODE_CODE = "CONTRACT_CREATE"
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
@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,
client: StateCenterClient,
*,
contract_runner: ContractRunner | None = None,
) -> dict[str, Any]:
client.start_node(spec.job_id, NODE_CODE, attempt_no=spec.attempt_no)
runner = contract_runner or create_contract_in_contract_system
try:
contract_no = runner(spec)
if spec.submit_mode == "dry_run":
return {"status": "dry_run", "submitted": False}
if not contract_no:
raise ContractCreateError("submit 模式未读取到真实合同编号,无法 complete")
return client.complete_node(spec.job_id, NODE_CODE, build_complete_payload(spec, contract_no))
except ContractCreateError as exc:
return client.fail_node(spec.job_id, NODE_CODE, build_fail_payload(spec, exc))
except Exception as exc:
return client.fail_node(spec.job_id, NODE_CODE, build_fail_payload(spec, UnknownRpaError(str(exc))))
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)
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=draft.amount,
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,
order_no_list=draft.order_no_list,
)
def _resolve_merged_pdf(draft: ContractDraft, spec: ContractCreateInput) -> str | None:
if draft.source_type != "YYC":
return None
paths = [str(p).strip() for p in draft.platform_pdf_paths if str(p).strip()]
if not paths:
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_complete_payload(spec: ContractCreateInput, contract_no: str) -> dict[str, Any]:
return {
"attemptNo": spec.attempt_no,
"executorRunId": f"YSQ-CONTRACT-{uuid.uuid4().hex[:12]}",
"idempotencyKey": f"{spec.job_id}:{NODE_CODE}:{spec.attempt_no}:{contract_no}",
"businessRefs": {"contractSystemNo": contract_no},
"summary": {
"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,
},
"artifacts": [],
}
def build_fail_payload(spec: ContractCreateInput, exc: ContractCreateError) -> dict[str, Any]:
return {
"attemptNo": spec.attempt_no,
"errorCode": exc.error_code,
"errorSummary": str(exc),
"retryable": exc.retryable,
"routeRole": exc.route_role,
"artifacts": [],
}
def _validate_data(data: dict[str, Any], draft: ContractDraft) -> None:
missing = []
for key, label in (
("jobId", "jobId"),
("supplierName", "supplierName"),
("orderDate", "orderDate"),
("sourceType", "sourceType"),
("customerCode", "customerCode"),
):
if not _str(data.get(key)):
missing.append(label)
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:
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],
*,
state_center_url: str,
agent_token: str,
) -> dict[str, Any]:
spec = build_spec_from_data(data)
client = StateCenterClient(state_center_url, agent_token)
return execute_contract_create(spec, client)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE node.")
parser.add_argument("--state-center-url", default=os.getenv("STATE_CENTER_URL", "http://127.0.0.1:8000"))
parser.add_argument("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token"))
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()
client = StateCenterClient(args.state_center_url, args.token)
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, client)
print(json.dumps(result, ensure_ascii=False))
return 0
# 保持旧导入名可用(errors 模块为新的统一异常来源)
__all__ = [
"ContractCreateError",
"LoginFailed",
"SupplierContractDisabled",
"PdfMergeUnsupported",
"UnknownRpaError",
"ContractCreateInput",
"execute_contract_create",
"create_contract_in_contract_system",
"build_spec_from_data",
"build_flow_context",
"build_complete_payload",
"build_fail_payload",
"run_contract_create",
"load_input_data",
"NODE_CODE",
"CONTRACT_URL",
]
if __name__ == "__main__":
raise SystemExit(main())