落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
空运查价参数:对照 TMS V2 /quote/v2/query 是否带全。
|
||||
|
||||
本文件职责:询价卡片出现前,确认销售侧空运必填能拼出查价正文。
|
||||
三字码能投影就带;对不上不拦建单、不拦查价。
|
||||
包装:销售原文非空即收下,禁止白名单拦补问。
|
||||
禁止:猜没写过的港口;因「对不上字典」跳过 TMS 或对销售说教;改六态。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 与 tms-air-exact-seed.json 对齐,只用于把已写地名填成 TMS 要的代码
|
||||
_AIRPORTS = {
|
||||
"上海": "SHA",
|
||||
"浦东": "PVG",
|
||||
"上海浦东": "PVG",
|
||||
"虹桥": "SHA",
|
||||
"上海虹桥": "SHA",
|
||||
"洛杉矶": "LAX",
|
||||
"深圳": "SZX",
|
||||
"广州": "CAN",
|
||||
"香港": "HKG",
|
||||
"北京首都": "PEK",
|
||||
"北京大兴": "PKX",
|
||||
"珠海": "ZUH",
|
||||
"克拉克": "CRK",
|
||||
"南京": "NKG",
|
||||
"吉隆坡": "KUL",
|
||||
"北京": "PEK",
|
||||
}
|
||||
|
||||
# TMS air_packaging_type 实测只认 1/2/3。1=全部,这条航线无价;
|
||||
# 2/3 能出与现网相同的标准价。托盘走 2,纸箱/木箱走 3。
|
||||
_PACKS = {
|
||||
"全部": "1",
|
||||
"托盘": "2",
|
||||
"卡板": "2",
|
||||
"散货": "2",
|
||||
"纸箱": "3",
|
||||
"木箱": "3",
|
||||
}
|
||||
|
||||
_IATA = re.compile(r"^[A-Za-z]{3}$")
|
||||
_NUMBER = re.compile(r"(\d+(?:\.\d+)?)")
|
||||
|
||||
|
||||
def _first_number(raw: str) -> str:
|
||||
hit = _NUMBER.search((raw or "").strip())
|
||||
return hit.group(1) if hit else ""
|
||||
|
||||
|
||||
def resolve_airport_code(raw: str) -> str:
|
||||
"""销售原文是地名或三字码时,给出 TMS originCode/destinationCode。"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
if _IATA.match(text):
|
||||
return text.upper()
|
||||
return _AIRPORTS.get(text, "")
|
||||
|
||||
|
||||
def harvest_named_air_route(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
|
||||
"""
|
||||
当前原话里的「从南京飞吉隆坡 / 北京飞吉隆坡」覆盖起运地、目的地。
|
||||
|
||||
只认对照表里的地名,不猜没写过的港口。换城市必须丢掉旧三字码。
|
||||
"""
|
||||
merged = dict(facts or {})
|
||||
raw = text or ""
|
||||
names = sorted(_AIRPORTS.keys(), key=len, reverse=True)
|
||||
for origin in names:
|
||||
for dest in names:
|
||||
if origin == dest:
|
||||
continue
|
||||
token = f"{origin}飞{dest}"
|
||||
if token in raw or f"从{token}" in raw:
|
||||
merged["起运港"] = origin
|
||||
merged["目的港"] = dest
|
||||
merged.pop("originCode", None)
|
||||
merged.pop("destinationCode", None)
|
||||
return merged
|
||||
return merged
|
||||
|
||||
|
||||
def resolve_package_type(raw: str) -> str:
|
||||
"""
|
||||
只给 TMS 出站用的 packageType,不决定销售是否算已填包装。
|
||||
|
||||
认得出代码就用;认不出默认 2(不要默认 1,1=全部在现网航线常无价)。
|
||||
禁止:因不在对照表而返回空,从而再向销售补问。
|
||||
"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return "2"
|
||||
if text in _PACKS:
|
||||
return _PACKS[text]
|
||||
if text.isdigit():
|
||||
return text
|
||||
return "2"
|
||||
|
||||
|
||||
def assemble_air_query(facts: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""
|
||||
把已采集字段拼成 TMS 空运查价参数。
|
||||
|
||||
不齐则 ok=False,missing_display 给销售补问;不建单、不出卡片。
|
||||
齐了则 facts 带上 originCode/destinationCode/packageType 与纯数字件数毛重体积。
|
||||
包装只看销售是否说了,散货等原文原样保留。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
origin = str(src.get("起运港") or "").strip()
|
||||
dest = str(src.get("目的港") or "").strip()
|
||||
cargo = str(src.get("品名") or "").strip()
|
||||
pieces = _first_number(str(src.get("件数") or ""))
|
||||
weight = _first_number(str(src.get("毛重") or ""))
|
||||
volume = _first_number(str(src.get("体积") or ""))
|
||||
pack_raw = str(src.get("包装方式") or src.get("包装类型") or "").strip()
|
||||
pack_code = resolve_package_type(pack_raw)
|
||||
quote_date = str(src.get("报价日期") or "").strip()
|
||||
# 三字码必须按当前地名重算,禁止沿用上一句的 NKG/KUL
|
||||
origin_code = resolve_airport_code(origin)
|
||||
dest_code = resolve_airport_code(dest)
|
||||
|
||||
missing: list[str] = []
|
||||
missing_display: list[str] = []
|
||||
# 销售侧必填只看 8 项是否非空。三字码是出站投影,对不上不拿来再补问起运地/目的地。
|
||||
if not origin:
|
||||
missing.append("起运港")
|
||||
missing_display.append("起运地")
|
||||
if not dest:
|
||||
missing.append("目的港")
|
||||
missing_display.append("目的地")
|
||||
if not cargo:
|
||||
missing.append("品名")
|
||||
missing_display.append("品名")
|
||||
if not pieces:
|
||||
missing.append("件数")
|
||||
missing_display.append("件数")
|
||||
if not weight:
|
||||
missing.append("毛重")
|
||||
missing_display.append("重量")
|
||||
if not volume:
|
||||
missing.append("体积")
|
||||
missing_display.append("体积")
|
||||
if not pack_raw:
|
||||
missing.append("包装方式")
|
||||
missing_display.append("包装类型")
|
||||
if not quote_date:
|
||||
missing.append("报价日期")
|
||||
missing_display.append("报价日期")
|
||||
|
||||
ok = not missing
|
||||
out_facts = dict(src)
|
||||
if origin:
|
||||
out_facts["起运港"] = origin
|
||||
if origin_code:
|
||||
out_facts["originCode"] = origin_code
|
||||
else:
|
||||
out_facts.pop("originCode", None)
|
||||
if dest:
|
||||
out_facts["目的港"] = dest
|
||||
if dest_code:
|
||||
out_facts["destinationCode"] = dest_code
|
||||
else:
|
||||
out_facts.pop("destinationCode", None)
|
||||
if cargo:
|
||||
out_facts["品名"] = cargo
|
||||
# 件数/重量/体积保留销售原文(1.5KG),数字只作出站,禁止回写成 1.5 再被补成 KGS
|
||||
if str(src.get("件数") or "").strip():
|
||||
out_facts["件数"] = str(src.get("件数")).strip()
|
||||
if str(src.get("毛重") or "").strip():
|
||||
out_facts["毛重"] = str(src.get("毛重")).strip()
|
||||
if str(src.get("体积") or "").strip():
|
||||
out_facts["体积"] = str(src.get("体积")).strip()
|
||||
if pack_raw:
|
||||
out_facts["包装方式"] = pack_raw
|
||||
out_facts["packageType"] = pack_code
|
||||
if quote_date:
|
||||
out_facts["报价日期"] = quote_date
|
||||
|
||||
logger.info(
|
||||
"tms.air.assemble ok=%s origin=%s dest=%s pack=%s missing=%s",
|
||||
ok,
|
||||
origin_code or "-",
|
||||
dest_code or "-",
|
||||
pack_raw or pack_code or "-",
|
||||
missing,
|
||||
)
|
||||
return {
|
||||
"ok": ok,
|
||||
"missing": missing,
|
||||
"missing_display": missing_display,
|
||||
"facts": out_facts,
|
||||
}
|
||||
Reference in New Issue
Block a user