224 lines
8.0 KiB
Python
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())
|