落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,12 +10,18 @@ from agent.schema.contracts_loader import (
|
||||
require_formal_contract,
|
||||
resolve_contract_path,
|
||||
)
|
||||
from agent.schema.field_validate import validate_required_fields_shell
|
||||
from agent.schema.field_validate import (
|
||||
validate_required_fields,
|
||||
validate_required_fields_shell,
|
||||
)
|
||||
from agent.schema.fingerprint import inquiry_fingerprint
|
||||
|
||||
__all__ = [
|
||||
"contracts_dir",
|
||||
"load_contract",
|
||||
"require_formal_contract",
|
||||
"resolve_contract_path",
|
||||
"validate_required_fields",
|
||||
"validate_required_fields_shell",
|
||||
"inquiry_fingerprint",
|
||||
]
|
||||
|
||||
@@ -25,7 +25,8 @@ def contracts_dir() -> Path:
|
||||
here = Path(__file__).resolve()
|
||||
for parent in here.parents:
|
||||
candidate = parent / "contracts"
|
||||
if candidate.is_dir():
|
||||
# 空目录不算:发布包曾留下空 contracts,会挡住仓库根的真合同
|
||||
if candidate.is_dir() and any(candidate.glob("*.json")):
|
||||
return candidate
|
||||
# 回退:与 agent 包同级仓库根的常见布局
|
||||
return here.parents[3] / "contracts"
|
||||
|
||||
@@ -1,19 +1,265 @@
|
||||
"""
|
||||
字段合同校验入口壳。
|
||||
字段合同校验:读 inquiry-required-fields-v1,只做作用域/非空/默认值。
|
||||
|
||||
本文件职责:暴露 validate_required_fields_shell;正式规则读 contracts JSON 后再实现。
|
||||
禁止:在未 require_formal_contract 时假装已激活字段;禁止用正则替代模型语义。
|
||||
本文件职责:判断首次查价缺项;报价日期未说则填当天,不进入补问。
|
||||
口语已写 TMS 单位(件/KGS/CBM)时,空着的件数/毛重/体积可机读补上。
|
||||
禁止:正则猜港口/箱型;禁止用封闭枚举拒绝用户原文;禁止改六态。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from agent.schema.contracts_loader import load_contract, require_formal_contract
|
||||
from agent.schema.contracts_loader import require_formal_contract
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SHANGHAI = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
# 空运询价必填 8 项:销售侧名称。内部仍用合同键。
|
||||
AIR_REQUIRED_FIELDS = (
|
||||
("起运港", "起运地"),
|
||||
("目的港", "目的地"),
|
||||
("品名", "品名"),
|
||||
("件数", "件数"),
|
||||
("毛重", "重量"),
|
||||
("体积", "体积"),
|
||||
("包装方式", "包装类型"),
|
||||
("报价日期", "报价日期"),
|
||||
)
|
||||
AIR_DISPLAY = {key: label for key, label in AIR_REQUIRED_FIELDS}
|
||||
|
||||
# 口语里已经带上 TMS 文档规定的单位时,补进空字段。
|
||||
# 只认「数字+单位」,不猜港口,不把长宽高换算成立方。
|
||||
_RE_PIECES = re.compile(r"(?<![\d.])(\d+)\s*(件|pcs)\b", re.I)
|
||||
_RE_WEIGHT = re.compile(r"(?<![\d.])(\d+(?:\.\d+)?)\s*(kgs|kg|公斤|千克)\b", re.I)
|
||||
_RE_VOLUME = re.compile(
|
||||
r"(?<![\d.])(\d+(?:\.\d+)?)\s*(cbm|立方米|立方|m3|m³)\b",
|
||||
re.I,
|
||||
)
|
||||
_RE_VOLUME_FANG = re.compile(r"(?<![\d.])(\d+(?:\.\d+)?)\s*方(?!向)")
|
||||
_RE_WEIGHT_WORD = re.compile(r"(?:毛重|重量)\s*[::]?\s*(\d+(?:\.\d+)?)")
|
||||
_RE_VOLUME_WORD = re.compile(r"体积\s*[::]?\s*(\d+(?:\.\d+)?)")
|
||||
# 销售原话里的包装词:当前句出现则覆盖旧值,散货也要收下
|
||||
_PACK_WORDS = ("纸箱", "木箱", "托盘", "卡板", "散货")
|
||||
# 报价日期:9月15日 / 2026-09-15;未写年用上海当年
|
||||
_RE_DATE_YMD = re.compile(
|
||||
r"(20\d{2})\s*[-/.年]\s*(\d{1,2})\s*[-/.月]\s*(\d{1,2})\s*[日号]?"
|
||||
)
|
||||
_RE_DATE_MD = re.compile(r"(?<!\d)(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]?")
|
||||
|
||||
# 展示名 / 别名 → 内部键
|
||||
_ALIASES = {
|
||||
"起运地": "起运港",
|
||||
"目的地": "目的港",
|
||||
"包装类型": "包装方式",
|
||||
"货物数量": "货量",
|
||||
"始发站": "起运港",
|
||||
"货物品名": "品名",
|
||||
"重量(KG)": "毛重",
|
||||
"重量": "毛重",
|
||||
"体积(CBM)": "体积",
|
||||
"车型/数量": "车型数量",
|
||||
}
|
||||
|
||||
|
||||
def shanghai_today() -> str:
|
||||
"""报价日期系统默认:Asia/Shanghai 当天。"""
|
||||
from datetime import datetime
|
||||
|
||||
return datetime.now(_SHANGHAI).date().isoformat()
|
||||
|
||||
|
||||
def normalize_facts(facts: dict[str, Any] | None) -> dict[str, str]:
|
||||
"""
|
||||
把展示名/别名折成合同内部键,去掉首尾空白。
|
||||
|
||||
空值丢弃;不解释语义。
|
||||
"""
|
||||
out: dict[str, str] = {}
|
||||
for raw_key, raw_val in (facts or {}).items():
|
||||
key = str(raw_key).strip()
|
||||
key = _ALIASES.get(key, key)
|
||||
if raw_val is None:
|
||||
continue
|
||||
val = str(raw_val).strip()
|
||||
if not val:
|
||||
continue
|
||||
out[key] = val
|
||||
return out
|
||||
|
||||
|
||||
def _harvest_packaging(raw: str) -> str:
|
||||
"""当前原话里最后出现的包装词;没有则空串。"""
|
||||
last = ""
|
||||
last_pos = -1
|
||||
for word in _PACK_WORDS:
|
||||
pos = raw.rfind(word)
|
||||
if pos > last_pos:
|
||||
last_pos = pos
|
||||
last = word
|
||||
return last
|
||||
|
||||
|
||||
def _harvest_quote_date(raw: str) -> str:
|
||||
"""当前原话里的报价日期,规范成 yyyy-MM-dd;没有则空串。"""
|
||||
from datetime import datetime
|
||||
|
||||
hit = _RE_DATE_YMD.search(raw)
|
||||
if hit:
|
||||
year, month, day = int(hit.group(1)), int(hit.group(2)), int(hit.group(3))
|
||||
else:
|
||||
hit = _RE_DATE_MD.search(raw)
|
||||
if not hit:
|
||||
return ""
|
||||
year = datetime.now(_SHANGHAI).year
|
||||
month, day = int(hit.group(1)), int(hit.group(2))
|
||||
try:
|
||||
return datetime(year, month, day).date().isoformat()
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
def harvest_oral_measures(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
|
||||
"""
|
||||
从当前原话机读件数/重量/体积/包装/报价日期。
|
||||
|
||||
当前句写了的覆盖旧值:散货替换托盘,9月16日写入报价日期。
|
||||
副作用:无。不改主账。不猜港口。
|
||||
"""
|
||||
merged = dict(facts or {})
|
||||
raw = text or ""
|
||||
hit = _RE_PIECES.search(raw)
|
||||
if hit:
|
||||
merged["件数"] = f"{hit.group(1)}{hit.group(2)}"
|
||||
hit = _RE_WEIGHT.search(raw)
|
||||
if hit:
|
||||
# 原样留下用户单位:1.5KG 不要改成 1.5KGS
|
||||
merged["毛重"] = f"{hit.group(1)}{hit.group(2)}"
|
||||
else:
|
||||
hit = _RE_WEIGHT_WORD.search(raw)
|
||||
if hit:
|
||||
merged["毛重"] = hit.group(1)
|
||||
hit = _RE_VOLUME.search(raw)
|
||||
if hit:
|
||||
merged["体积"] = f"{hit.group(1)}{hit.group(2)}"
|
||||
else:
|
||||
hit = _RE_VOLUME_FANG.search(raw) or _RE_VOLUME_WORD.search(raw)
|
||||
if hit:
|
||||
merged["体积"] = hit.group(1)
|
||||
pack = _harvest_packaging(raw)
|
||||
if pack:
|
||||
merged["包装方式"] = pack
|
||||
quote_date = _harvest_quote_date(raw)
|
||||
if quote_date:
|
||||
merged["报价日期"] = quote_date
|
||||
from agent.schema.tms_air_query import harvest_named_air_route
|
||||
|
||||
return harvest_named_air_route(raw, merged)
|
||||
|
||||
|
||||
def apply_quote_date_default(facts: dict[str, str]) -> dict[str, str]:
|
||||
"""销售未提供报价日期时填当天;已有则不覆盖。"""
|
||||
merged = dict(facts)
|
||||
if not merged.get("报价日期"):
|
||||
merged["报价日期"] = shanghai_today()
|
||||
logger.info("报价日期缺省为当天 date=%s", merged["报价日期"])
|
||||
return merged
|
||||
|
||||
|
||||
def _pricing_required_keys(contract: dict[str, Any], business_line: str, land_subtype: str) -> list[str]:
|
||||
"""当前激活的首次查价内部键列表。"""
|
||||
line = (business_line or "").strip().upper()
|
||||
pricing = contract.get("pricing_required") or {}
|
||||
if line == "SEA":
|
||||
return list((pricing.get("SEA") or {}).get("always") or [])
|
||||
if line == "AIR":
|
||||
return list((pricing.get("AIR") or {}).get("always") or [])
|
||||
if line == "LAND":
|
||||
land = pricing.get("LAND") or {}
|
||||
block = land.get(land_subtype) or {}
|
||||
if not block:
|
||||
return []
|
||||
return list(block.get("always") or [])
|
||||
return []
|
||||
|
||||
|
||||
def display_name(internal_key: str, business_line: str = "AIR") -> str:
|
||||
"""补问时给销售看的名字。"""
|
||||
line = (business_line or "").upper()
|
||||
if line == "AIR":
|
||||
return AIR_DISPLAY.get(internal_key, internal_key)
|
||||
return internal_key
|
||||
|
||||
|
||||
def validate_required_fields(
|
||||
*,
|
||||
facts: dict[str, Any],
|
||||
business_line: str = "",
|
||||
land_subtype: str = "",
|
||||
require_formal: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
首次查价完整性。
|
||||
|
||||
返回:ok / implemented / missing(内部键)/ missing_display / facts(含默认报价日期)/ errors。
|
||||
报价日期不进 missing。未识别业务线时 missing 为空、need_transport_mode=True。
|
||||
"""
|
||||
_ = require_formal
|
||||
contract = require_formal_contract("inquiry-required-fields-v1")
|
||||
normalized = apply_quote_date_default(normalize_facts(facts))
|
||||
line = (business_line or "").strip().upper()
|
||||
if line not in {"SEA", "AIR", "LAND"}:
|
||||
return {
|
||||
"ok": False,
|
||||
"implemented": True,
|
||||
"need_transport_mode": True,
|
||||
"missing": [],
|
||||
"missing_display": [],
|
||||
"facts": normalized,
|
||||
"errors": [],
|
||||
"business_line": line,
|
||||
}
|
||||
if line == "LAND" and not (land_subtype or "").strip():
|
||||
return {
|
||||
"ok": False,
|
||||
"implemented": True,
|
||||
"need_transport_mode": False,
|
||||
"need_land_subtype": True,
|
||||
"missing": [],
|
||||
"missing_display": [],
|
||||
"facts": normalized,
|
||||
"errors": [],
|
||||
"business_line": line,
|
||||
}
|
||||
|
||||
required = _pricing_required_keys(contract, line, land_subtype)
|
||||
missing = [k for k in required if k != "报价日期" and not normalized.get(k)]
|
||||
missing_display = [display_name(k, line) for k in missing]
|
||||
ok = len(missing) == 0
|
||||
logger.info(
|
||||
"schema.validate line=%s land=%s ok=%s missing=%s",
|
||||
line,
|
||||
land_subtype or "-",
|
||||
ok,
|
||||
missing,
|
||||
)
|
||||
return {
|
||||
"ok": ok,
|
||||
"implemented": True,
|
||||
"need_transport_mode": False,
|
||||
"need_land_subtype": False,
|
||||
"missing": missing,
|
||||
"missing_display": missing_display,
|
||||
"facts": normalized,
|
||||
"errors": [],
|
||||
"business_line": line,
|
||||
"land_subtype": land_subtype,
|
||||
}
|
||||
|
||||
|
||||
def validate_required_fields_shell(
|
||||
*,
|
||||
@@ -21,38 +267,9 @@ def validate_required_fields_shell(
|
||||
business_line: str = "",
|
||||
require_formal: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
字段校验空壳。
|
||||
|
||||
返回结构固定:ok / implemented / missing / errors。
|
||||
implemented=False 表示规则引擎尚未接入(框架阶段预期)。
|
||||
"""
|
||||
stem = "inquiry-required-fields-v1"
|
||||
try:
|
||||
contract = (
|
||||
require_formal_contract(stem) if require_formal else load_contract(stem)
|
||||
)
|
||||
except (FileNotFoundError, RuntimeError) as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"implemented": False,
|
||||
"missing": [],
|
||||
"errors": [str(exc)],
|
||||
"business_line": business_line,
|
||||
}
|
||||
|
||||
is_ph = bool(contract.get("_meta", {}).get("is_placeholder"))
|
||||
logger.info(
|
||||
"schema.validate_shell line=%s placeholder=%s facts_keys=%s(未跑激活规则)",
|
||||
business_line or "-",
|
||||
is_ph,
|
||||
sorted(facts.keys()),
|
||||
"""兼容骨架入口:现已接入正式激活规则。"""
|
||||
return validate_required_fields(
|
||||
facts=facts,
|
||||
business_line=business_line,
|
||||
require_formal=require_formal,
|
||||
)
|
||||
return {
|
||||
"ok": False,
|
||||
"implemented": False,
|
||||
"missing": [],
|
||||
"errors": ["field_activation_rules_not_implemented"],
|
||||
"business_line": business_line,
|
||||
"contract_placeholder": is_ph,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
相同询价内容指纹。
|
||||
|
||||
本文件职责:用已确认查价字段生成可比较的键,供主账全库检索相同需求(展示用,不复用工单号)。
|
||||
禁止:用自然语言模糊相似;禁止写六态。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
|
||||
def ascii_idempotency_key(raw: str) -> str:
|
||||
"""
|
||||
企微/主账 HTTP 头必须是 ASCII。中文指纹原样塞进 X-Idempotency-Key 会直接崩,销售收不到回复。
|
||||
"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
try:
|
||||
text.encode("ascii")
|
||||
return text
|
||||
except UnicodeEncodeError:
|
||||
return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def inquiry_fingerprint(facts: dict[str, Any], *, business_line: str) -> str:
|
||||
"""
|
||||
生成相同内容键。
|
||||
|
||||
组成:业务线 + 起运 + 目的 + 品名 + 件数/货量 + 毛重 + 体积 + 报价日期。
|
||||
空运箱型不在空运必填里;海运箱型箱量若有则纳入。
|
||||
"""
|
||||
line = (business_line or "").strip().upper()
|
||||
keys = ["起运港", "目的港", "品名", "件数", "货量", "毛重", "体积", "箱型箱量", "报价日期"]
|
||||
parts = [line]
|
||||
for key in keys:
|
||||
parts.append(str(facts.get(key) or "").strip())
|
||||
return "|".join(parts)
|
||||
@@ -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