查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.5 KiB
Python
201 lines
6.5 KiB
Python
"""
|
||
空运查价参数:对照 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,
|
||
}
|