查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
6.5 KiB
Python
185 lines
6.5 KiB
Python
"""
|
||
主账 HTTP 账本:建单 / 查价 / 报价 / 六态只打 8180。
|
||
|
||
本文件职责:把 MemoryLedger 同名方法接到 /inquiry/agent/**;查价走真 TMS(主账出站)。
|
||
禁止:直连 TMS / MySQL;把 Jeecg 包装失败当成有价。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any, Optional
|
||
|
||
from agent.ledger import LedgerClient
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _unwrap(data: Any) -> dict[str, Any]:
|
||
"""解开 Jeecg Result:{success, result} → result。"""
|
||
if not isinstance(data, dict):
|
||
return {}
|
||
if "result" in data and isinstance(data.get("result"), dict):
|
||
inner = data["result"]
|
||
if data.get("success") is False:
|
||
return {"ok": False, "error": str(data.get("message") or "ledger_error"), **inner}
|
||
return inner
|
||
return data
|
||
|
||
|
||
class HttpLedger:
|
||
"""
|
||
生产主账客户端。
|
||
|
||
超时随 LedgerClient;查价在主账线程打 TMS,本类只转发分类。
|
||
"""
|
||
|
||
def __init__(self, client: LedgerClient | None = None) -> None:
|
||
self._client = client or LedgerClient.from_settings()
|
||
|
||
def _post(self, path: str, body: dict[str, Any], *, idem: str = "") -> dict[str, Any]:
|
||
resp = self._client.request_json("POST", path, json_body=body, idempotency_key=idem)
|
||
if not resp.ok:
|
||
logger.warning("http_ledger fail path=%s status=%s err=%s", path, resp.status_code, resp.error)
|
||
return {"ok": False, "error": resp.error or f"http_{resp.status_code}"}
|
||
return _unwrap(resp.data)
|
||
|
||
def find_similar_open(
|
||
self,
|
||
*,
|
||
facts: dict[str, str],
|
||
business_line: str,
|
||
exclude_work_order_no: str = "",
|
||
) -> dict[str, Any]:
|
||
"""全库相同需求检索;exclude_work_order_no 排除刚建的本单。"""
|
||
out = self._post(
|
||
"/inquiry/agent/ticket/findSimilar",
|
||
{
|
||
"salesWecomId": "",
|
||
"businessLine": business_line,
|
||
"facts": facts,
|
||
"excludeWorkOrderNo": exclude_work_order_no,
|
||
},
|
||
)
|
||
if "found" not in out:
|
||
return {"found": False, "first_or_same": "first", "work_order_no": "", "status": ""}
|
||
return out
|
||
|
||
def create_ticket(
|
||
self,
|
||
*,
|
||
sender_id: str,
|
||
business_line: str,
|
||
facts: dict[str, str],
|
||
idempotency_key: str = "",
|
||
) -> dict[str, Any]:
|
||
out = self._post(
|
||
"/inquiry/agent/ticket/create",
|
||
{
|
||
"salesWecomId": sender_id,
|
||
"businessLine": business_line,
|
||
"facts": facts,
|
||
"idempotencyKey": idempotency_key,
|
||
},
|
||
idem=idempotency_key,
|
||
)
|
||
if not out.get("ok") and not out.get("work_order_no"):
|
||
return {"ok": False, "created": False, "work_order_no": "", "status": "", "first_or_same": "first"}
|
||
out.setdefault("ok", True)
|
||
if out.get("similar_work_order_no"):
|
||
out.setdefault("first_or_same", "same")
|
||
else:
|
||
out.setdefault("first_or_same", "first")
|
||
return out
|
||
|
||
def get_for_agent(self, *, work_order_no: str, sender_id: str) -> dict[str, Any]:
|
||
"""读主账工单快照,供续办回到当时节点。"""
|
||
out = self._post(
|
||
"/inquiry/agent/ticket/get",
|
||
{"work_order_no": work_order_no, "salesWecomId": sender_id},
|
||
)
|
||
if not out.get("found") and not out.get("ok"):
|
||
return {"ok": False, "found": False, "owned": False, "work_order_no": work_order_no}
|
||
return out
|
||
|
||
def query_tms(self, *, work_order_no: str, facts: dict[str, str]) -> dict[str, Any]:
|
||
out = self._post(
|
||
"/inquiry/agent/tms/query",
|
||
{"work_order_no": work_order_no, "facts": facts},
|
||
idem=f"tms:{work_order_no}",
|
||
)
|
||
if "classification" not in out and not out.get("ok"):
|
||
return {
|
||
"ok": False,
|
||
"classification": "TMS_TECH_FAILURE",
|
||
"has_price": False,
|
||
"quote": {},
|
||
}
|
||
out.setdefault("quote", {})
|
||
return out
|
||
|
||
def upsert_quote(
|
||
self,
|
||
*,
|
||
work_order_no: str,
|
||
quote: dict[str, Any],
|
||
to_status: str = "",
|
||
) -> dict[str, Any]:
|
||
# 有价时主账 query 已写快照并迁「已报价」;此处只做幂等确认,避免二次假写入。
|
||
_ = (work_order_no, quote, to_status)
|
||
return {"ok": True, "quote_version": 1, "status": to_status or "已报价"}
|
||
|
||
def render_quote(
|
||
self,
|
||
*,
|
||
work_order_no: str,
|
||
business_line: str,
|
||
facts: dict[str, Any],
|
||
quote: Optional[dict[str, Any]] = None,
|
||
format: str = "xlsx",
|
||
sales_wecom_id: str = "",
|
||
) -> dict[str, Any]:
|
||
"""
|
||
打主账按后台报价模板填 Excel。
|
||
|
||
失败(无关键词且无默认)原样带回销售话术。Java 不转 PDF。
|
||
"""
|
||
from agent.quote_templates.match import FAIL_MESSAGE
|
||
|
||
# 填表+回文件可能超过默认 10s,单独放宽,仍禁止无限等。
|
||
prev_timeout = self._client.timeout_seconds
|
||
self._client.timeout_seconds = max(float(prev_timeout or 10), 45.0)
|
||
self._client.close()
|
||
try:
|
||
out = self._post(
|
||
"/inquiry/agent/quote/render",
|
||
{
|
||
"workOrderNo": work_order_no,
|
||
"businessLine": business_line,
|
||
"facts": facts,
|
||
"quote": quote or {},
|
||
"salesWecomId": sales_wecom_id,
|
||
},
|
||
idem=f"quote-render:{work_order_no}:{format}",
|
||
)
|
||
finally:
|
||
self._client.timeout_seconds = prev_timeout
|
||
self._client.close()
|
||
if not out.get("ok"):
|
||
return {
|
||
"ok": False,
|
||
"error": out.get("error") or "NO_TEMPLATE",
|
||
"message": out.get("message") or FAIL_MESSAGE,
|
||
}
|
||
out.setdefault("needsPdf", str(format).lower() == "pdf")
|
||
return out
|
||
|
||
def transition(self, *, work_order_no: str, to_status: str) -> dict[str, Any]:
|
||
out = self._post(
|
||
"/inquiry/agent/ticket/transition",
|
||
{"workOrderNo": work_order_no, "toStatus": to_status},
|
||
)
|
||
if not out.get("ok") and out.get("status"):
|
||
out["ok"] = True
|
||
return out if out.get("ok") else {"ok": False, "error": out.get("error") or "transition_failed"}
|