AutoRpa/rpa_yisaiqi/contract_create_node.py

224 lines
8.0 KiB
Python

"""CONTRACT_CREATE node skeleton for YiSaiQi Windows integration.
This first version proves the state-center callback path and leaves the
contract-system page automation behind a single replaceable runner function.
"""
from __future__ import annotations
import argparse
import os
import uuid
import webbrowser
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from .callback_client import StateCenterClient
from .contract_draft import ContractDraft, load_contract_draft
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
from .pdf_utils import ensure_merged_pdf
from .runtime.errors import LoginFailed as RuntimeLoginFailed
from .runtime.errors import NodeError
from .runtime.errors import PdfMergeUnsupported as RuntimePdfMergeUnsupported
from .runtime.errors import SupplierContractDisabled as RuntimeSupplierContractDisabled
from .runtime.shell import execute_node
from .yisaiqi_adapter_template import YiSaiQiBrowserAdapter
NODE_CODE = "CONTRACT_CREATE"
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
class ContractCreateError(NodeError):
pass
class LoginFailed(RuntimeLoginFailed, ContractCreateError):
pass
class SupplierContractDisabled(RuntimeSupplierContractDisabled, ContractCreateError):
pass
class PdfMergeUnsupported(RuntimePdfMergeUnsupported, ContractCreateError):
pass
@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"
open_browser: bool = False
draft_path: str | None = None
merged_pdf_output: str | None = None
ContractRunner = Callable[[ContractCreateInput], str]
def execute_contract_create(
spec: ContractCreateInput,
client: StateCenterClient,
*,
contract_runner: ContractRunner | None = None,
) -> dict[str, Any]:
runner = contract_runner or create_contract_in_contract_system
def _run() -> dict[str, Any]:
contract_no = runner(spec)
payload = build_complete_payload(spec, contract_no)
return {
"businessRefs": payload["businessRefs"],
"summary": payload["summary"],
"artifacts": payload["artifacts"],
}
return execute_node(
client,
spec.job_id,
NODE_CODE,
_run,
attempt_no=spec.attempt_no,
operator="agent",
)
def create_contract_in_contract_system(spec: ContractCreateInput) -> str:
"""Draft the contract in the contract system and return contract number."""
if spec.open_browser:
webbrowser.open(spec.contract_url)
if spec.contract_system_no:
return spec.contract_system_no
if not spec.draft_path:
raise ContractCreateError("draft_path is required for real contract drafting")
draft = load_contract_draft(spec.draft_path)
ctx = build_flow_context(spec, draft)
browser = YiSaiQiBrowserAdapter()
try:
return run_contract_create_flow(browser, ctx)
except NotImplementedError as exc:
raise ContractCreateError(
"YiSaiQiBrowserAdapter still needs to be mapped to YiSaiQi designer activities"
) from exc
def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> ContractFlowContext:
merged_pdf_path = draft.merged_pdf_path
if draft.source_type == "YYC":
output_path = spec.merged_pdf_output or str(
Path("runtime") / "contracts" / spec.job_id / "yyc-contract-merged.pdf"
)
try:
merged_pdf_path = ensure_merged_pdf(draft.platform_pdf_paths, output_path)
except (RuntimeError, ValueError) as exc:
raise PdfMergeUnsupported(str(exc)) from exc
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,
)
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,
"contractUrl": spec.contract_url,
"uploadRule": "merge_pdf_before_upload" if spec.source_type == "YYC" else "skip_contract_upload",
},
"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 parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE callback skeleton.")
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("--job-id", required=True)
parser.add_argument("--supplier-name", required=True)
parser.add_argument("--order-date", required=True)
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("--open-browser", action="store_true")
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()
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,
open_browser=args.open_browser,
draft_path=args.draft_path,
merged_pdf_output=args.merged_pdf_output,
)
client = StateCenterClient(args.state_center_url, args.token)
result = execute_contract_create(spec, client)
print(result)
return 0
if __name__ == "__main__":
raise SystemExit(main())