Files
inquiry_robot/inquiry-agent/agent/schema/tms_land_query.py
T

176 lines
6.0 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.
"""
陆运查价参数:对照 TMS 出站是否带全。
本文件职责:核对卡通过后、建单查价前,确认当前运输类型的货物字段能拼出查价正文。
带去:运输类型、线路类别、始发/目的/品名/报价日期,以及该类型的体积重量或车型/数量;通关口岸有值才带。
出站类型/线路译成 TMS 用的值,线路键名 routeCategory。工单与卡片仍用销售中文。
中港整车、中港零担/集拼出站类型都只写「中港」。中港整车不带车型/数量。
禁止:把运输分类放进出站;直连 TMS;改六态。
线程:纯函数。
"""
from __future__ import annotations
import logging
import re
from typing import Any
from agent.schema.field_validate import apply_quote_date_default, normalize_facts
logger = logging.getLogger(__name__)
LCL_TYPES = frozenset({"国内运输拼车", "跨境集拼"})
FTL_TYPES = frozenset({"国内运输整车", "跨境整车", "中港零担/集拼"})
HK_FTL_TYPE = "中港整车"
CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港整车", "中港零担/集拼"})
_TYPE_ENUM = {
"国内运输拼车": "DOMESTIC_LTL",
"国内运输整车": "DOMESTIC_FTL",
"跨境集拼": "CROSS_BORDER_LTL",
"跨境整车": "CROSS_BORDER_FTL",
"中港整车": "中港",
"中港零担/集拼": "中港",
}
_ROUTE_ENUM = {
"国内长途/零担": "DOMESTIC_LONG_LTL",
"国内城配/拖车/打包": "DOMESTIC_CITY_DIST",
"中港/中亚/中欧": "CN_HK_CA_EU",
"东南亚": "SOUTHEAST_ASIA",
}
_NUMBER = re.compile(r"(\d+(?:\.\d+)?)")
_VEHICLE_SPLIT = re.compile(r"\s*[/xX×**]\s*")
_TAIL_QTY = re.compile(r"^(.*?)[\s,,]+(\d+)\s*(台|辆)?$")
def tms_land_type_code(raw: str) -> str:
"""销售中文运输类型 → 查价 landTransportType。中港两类都是「中港」。对不上空串。"""
text = (raw or "").strip()
if text in _TYPE_ENUM:
return _TYPE_ENUM[text]
if text in set(_TYPE_ENUM.values()):
return text
return ""
def tms_land_route_code(raw: str) -> str:
"""销售中文线路类别 → TMS 6.1.1 routeCategory。对不上空串。"""
text = (raw or "").strip()
if text in _ROUTE_ENUM:
return _ROUTE_ENUM[text]
if text in set(_ROUTE_ENUM.values()):
return text
return ""
def _first_number(raw: str) -> str:
hit = _NUMBER.search((raw or "").strip())
return hit.group(1) if hit else ""
def split_vehicle_qty(raw: str) -> tuple[str, str]:
"""
把「车型/数量」拆成出站车型与台数。
例:9米6/1、9米6*2、9米6 1台。拆不出台数时台数默认 1,不回头补问。
不修改入参。
"""
text = (raw or "").strip()
if not text:
return "", ""
parts = _VEHICLE_SPLIT.split(text, maxsplit=1)
if len(parts) == 2 and parts[0].strip() and parts[1].strip():
return parts[0].strip(), _first_number(parts[1]) or parts[1].strip()
hit = _TAIL_QTY.match(text)
if hit and hit.group(1).strip():
return hit.group(1).strip(), hit.group(2)
return text, "1"
def assemble_land_query(facts: dict[str, Any] | None) -> dict[str, Any]:
"""
把已采集陆运字段拼成 TMS 查价参数。
不齐则 ok=False,missing 给销售补问。齐了也不带分类。
改了运输类型后,outbound 只带当前类型需要的键。
"""
src = apply_quote_date_default(
normalize_facts({str(k): v for k, v in dict(facts or {}).items() if v is not None})
)
raw = {str(k): str(v).strip() for k, v in dict(facts or {}).items() if v is not None}
land_type = (raw.get("运输类型") or src.get("运输类型") or "").strip()
route = (raw.get("线路类别") or src.get("线路类别") or "").strip()
origin = (src.get("起运港") or "").strip()
dest = (src.get("目的港") or "").strip()
cargo = (src.get("品名") or "").strip()
quote_date = (src.get("报价日期") or "").strip()
customs = (raw.get("通关口岸") or src.get("通关口岸") or "").strip()
missing: list[str] = []
if not land_type:
missing.append("运输类型")
if not route:
missing.append("线路类别")
if not origin:
missing.append("起运港")
if not dest:
missing.append("目的港")
if not cargo:
missing.append("品名")
outbound: dict[str, Any] = {
"transportMode": "LAND",
"landTransportType": tms_land_type_code(land_type),
"routeCategory": tms_land_route_code(route),
"origin": origin,
"destination": dest,
"cargoName": cargo,
"quoteDate": quote_date,
}
if land_type in LCL_TYPES:
vol = _first_number(src.get("体积") or "")
wgt = _first_number(src.get("毛重") or "")
if not vol:
missing.append("体积")
else:
outbound["volumeCbm"] = vol
if not wgt:
missing.append("毛重")
else:
outbound["weightKg"] = wgt
elif land_type == HK_FTL_TYPE:
pass
elif land_type in FTL_TYPES:
combo = (
src.get("车型/数量")
or raw.get("车型/数量")
or src.get("车型")
or raw.get("车型")
or ""
).strip()
if not combo:
missing.append("车型/数量")
else:
vehicle, qty = split_vehicle_qty(combo)
leftover_qty = (src.get("数量") or raw.get("数量") or "").strip()
if leftover_qty and leftover_qty not in combo:
qty = leftover_qty
outbound["vehicleType"] = vehicle
outbound["quantity"] = qty or "1"
elif land_type:
missing.append("运输类型")
if land_type in CUSTOMS_TYPES and customs:
outbound["customsPort"] = customs
ok = len(missing) == 0
logger.info("tms.land.assemble ok=%s missing=%s type=%s", ok, missing, land_type or "-")
return {
"ok": ok,
"missing": missing,
"facts": src,
"outbound": outbound,
}