产品调价继承工单币种(港币不再默成美元);陆运集拼空分档只填询价重量那一档,跨境整车按已选线路写起讫。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,14 +11,17 @@ import logging
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from agent.schema.field_validate import QUOTE_DATE_SYNONYMS
|
||||
try:
|
||||
from agent.schema.field_validate import QUOTE_DATE_SYNONYMS
|
||||
except ImportError: # 测服旧 release 未导出同义词时仍可抽产品价
|
||||
QUOTE_DATE_SYNONYMS = ()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 报价日期同义词。长的在前,避免短词截断。货好时间、提货地址不在这里。
|
||||
_QUOTE_DATE_LABEL_RE = "|".join(
|
||||
re.escape(name)
|
||||
for name in sorted(("报价日期",) + QUOTE_DATE_SYNONYMS, key=len, reverse=True)
|
||||
for name in sorted(("报价日期",) + tuple(QUOTE_DATE_SYNONYMS or ()), key=len, reverse=True)
|
||||
)
|
||||
|
||||
_MODE_WORDS = (
|
||||
@@ -829,9 +832,11 @@ def extract_air_collab_fields(
|
||||
return {k: got[k] for k in keys if str(got.get(k) or "").strip()}
|
||||
|
||||
|
||||
# 金额旁币种词:含港币;禁止只认 USD/CNY 把港币单漏成美元。
|
||||
_CCY_TOKEN = r"USD|CNY|RMB|HKD|EUR|JPY|GBP|美金|美元|人民币|港币|港元|欧元|日元|英镑"
|
||||
_RE_MONEY = re.compile(
|
||||
r"(?:USD|CNY|RMB|美金|美元|人民币)?\s*(\d+(?:\.\d+)?)\s*(?:USD|CNY|RMB|美金|美元|人民币|元)?"
|
||||
r"|(?:USD|CNY|RMB|美金|美元)\s*(\d+(?:\.\d+)?)",
|
||||
rf"(?:{_CCY_TOKEN}|元)?\s*(\d+(?:\.\d+)?)\s*(?:{_CCY_TOKEN}|元)?"
|
||||
rf"|(?:{_CCY_TOKEN})\s*(\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
# 陆运集拼:「2-500KG 单价CNY990」是分档单价,不是「燃油附加费=2」。
|
||||
@@ -839,14 +844,14 @@ _RE_WEIGHT_BAND_PRICE = re.compile(
|
||||
r"(?P<min>\d+)\s*[-~~—–到至]\s*(?P<max>\d+)\s*K\s*G"
|
||||
r"(?:\s*以上)?"
|
||||
r"\s*(?:单价)?"
|
||||
r"\s*(?:(?P<ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"\s*(?:(?P<ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<price>\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
_RE_WEIGHT_BAND_OPEN = re.compile(
|
||||
r"(?P<min>\d+)\s*K\s*G\s*以上"
|
||||
r"\s*(?:单价)?"
|
||||
r"\s*(?:(?P<ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"\s*(?:(?P<ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<price>\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
@@ -862,9 +867,9 @@ _RE_FEE_LINE = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费|[A-Za-z]{2,10})"
|
||||
r"\s*(?:改为|改成|调整为|调整成)?"
|
||||
r"\s*[::=]?"
|
||||
r"\s*(?:(?P<ccy1>USD|CNY|RMB|美金|美元|人民币)\s+)?"
|
||||
rf"\s*(?:(?P<ccy1>{_CCY_TOKEN})\s+)?"
|
||||
r"(?P<amount>\d+(?:\.\d+)?)"
|
||||
r"\s*(?P<ccy2>USD|CNY|RMB|美金|美元|人民币|元)?",
|
||||
rf"\s*(?P<ccy2>{_CCY_TOKEN}|元)?",
|
||||
re.I,
|
||||
)
|
||||
# 「文件发改为:500」漏了「费」、中间还有冒号,不能丢掉。
|
||||
@@ -873,19 +878,21 @@ _RE_FEE_CHANGE = re.compile(
|
||||
r"\s*(?:改为|改成|调整为|调整成)"
|
||||
r"\s*[::=]?"
|
||||
r"\s*(?P<amount>\d+(?:\.\d+)?)"
|
||||
r"\s*(?P<ccy>USD|CNY|RMB|美金|美元|人民币|元)?",
|
||||
rf"\s*(?P<ccy>{_CCY_TOKEN}|元)?",
|
||||
re.I,
|
||||
)
|
||||
_SKIP_FEE_NAMES = frozenset({"usd", "cny", "rmb", "美金", "美元", "人民币", "元"})
|
||||
_SKIP_FEE_NAMES = frozenset(
|
||||
{"usd", "cny", "rmb", "hkd", "eur", "jpy", "gbp", "美金", "美元", "人民币", "港币", "港元", "元"}
|
||||
)
|
||||
# 航线常写「空运费 单价CNY99,数量100,金额CNY199」。单价和金额不是同一个数,必须认金额。
|
||||
_RE_FEE_PRICED = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费)"
|
||||
r"\s*单价\s*"
|
||||
r"(?:(?P<unit_ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"(?:(?P<unit_ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<unit>\d+(?:\.\d+)?)"
|
||||
r"(?:\s*[,,、]\s*数量\s*(?P<qty>\d+(?:\.\d+)?))?"
|
||||
r"(?:\s*[,,、]\s*金额\s*"
|
||||
r"(?:(?P<amt_ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"(?:(?P<amt_ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<amount>\d+(?:\.\d+)?))?",
|
||||
re.I,
|
||||
)
|
||||
@@ -898,21 +905,40 @@ def _ascii_amount(raw: str) -> str:
|
||||
|
||||
|
||||
def _currency_of(text: str, hint: str = "") -> str:
|
||||
"""
|
||||
从正文/提示抽三字码。正文没写币种时只回传 hint,禁止默认编造 USD。
|
||||
|
||||
港币必须先于「元」判断,避免「港元」被误判成 CNY。
|
||||
WO202610090107:TMS 是 HKD,产品只写「运输费调整为1100」时必须继承 HKD。
|
||||
"""
|
||||
blob = f"{hint} {text}".upper()
|
||||
if any(x in blob for x in ("CNY", "RMB", "人民币", "元")):
|
||||
raw = f"{hint} {text}"
|
||||
if any(x in blob for x in ("HKD", "港币", "港元", "HK$")) or "港币" in raw or "港元" in raw:
|
||||
return "HKD"
|
||||
if any(x in blob for x in ("CNY", "RMB", "人民币")) or (
|
||||
"元" in raw and "港元" not in raw and "港币" not in raw
|
||||
):
|
||||
return "CNY"
|
||||
if any(x in blob for x in ("USD", "美金", "美元")):
|
||||
return "USD"
|
||||
return hint or "USD"
|
||||
if any(x in blob for x in ("EUR", "欧元")):
|
||||
return "EUR"
|
||||
if any(x in blob for x in ("JPY", "日元")):
|
||||
return "JPY"
|
||||
if any(x in blob for x in ("GBP", "英镑")):
|
||||
return "GBP"
|
||||
return str(hint or "").strip().upper()
|
||||
|
||||
|
||||
def _parse_fee_rows(text: str) -> list[dict[str, str]]:
|
||||
def _parse_fee_rows(text: str, baseline_currency: str = "") -> list[dict[str, str]]:
|
||||
"""
|
||||
按行抽费用:码头操作费100CNY、报关费改为300CNY。
|
||||
|
||||
只认带「费」的中文名或短英文费用码,避免把工单号当金额。
|
||||
句里无币种时继承 baseline_currency(通常是当前 TMS/主账币种),禁止默认 USD。
|
||||
"""
|
||||
raw = _ascii_amount(text or "")
|
||||
base_ccy = str(baseline_currency or "").strip().upper()
|
||||
rows: list[dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
@@ -925,10 +951,11 @@ def _parse_fee_rows(text: str) -> list[dict[str, str]]:
|
||||
if name in seen:
|
||||
return
|
||||
seen.add(name)
|
||||
line_ccy = _currency_of(raw, ccy or base_ccy) or base_ccy
|
||||
row = {
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"currency": _currency_of(raw, ccy),
|
||||
"currency": line_ccy,
|
||||
}
|
||||
for key, val in extra.items():
|
||||
if val:
|
||||
@@ -986,6 +1013,7 @@ def extract_product_quote(
|
||||
injected: Optional[dict[str, Any]] = None,
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
baseline_currency: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
产品文字报价:先按费用行抽,再退回合计/币别。注入优先。
|
||||
@@ -996,7 +1024,9 @@ def extract_product_quote(
|
||||
陆运集拼:「2-500KG 单价CNY990,燃油附加费率1%,税率6%」走分档单价+费率,
|
||||
禁止把公斤档下限「2」当成燃油附加费金额(WO021)。
|
||||
无费用名时按 business_line/transport_mode 默认陆运费/空运费/海运费,禁止陆运硬套海运费。
|
||||
正文未写币种时继承 baseline_currency(当前 TMS/主账币种),禁止默认 USD(WO107)。
|
||||
"""
|
||||
base_ccy = str(baseline_currency or "").strip().upper()
|
||||
if isinstance(injected, dict) and (injected.get("total") or injected.get("fee_rows")):
|
||||
out = dict(injected)
|
||||
out.setdefault("source_label", "产品报价")
|
||||
@@ -1008,14 +1038,14 @@ def extract_product_quote(
|
||||
compact = stripped.replace(" ", "").replace(" ", "")
|
||||
if re.fullmatch(r"WO\d{12}", compact, re.I):
|
||||
return {}
|
||||
band = _parse_weight_band_quote(raw)
|
||||
band = _parse_weight_band_quote(raw, baseline_currency=base_ccy)
|
||||
if band:
|
||||
return band
|
||||
# 费率句若仍落到普通费用行,会把「燃油附加费」配上句首公斤数。先剥掉再抽。
|
||||
scrubbed = _strip_rate_and_band_noise(raw)
|
||||
rows = _parse_fee_rows(scrubbed) if scrubbed.strip() else []
|
||||
rows = _parse_fee_rows(scrubbed, baseline_currency=base_ccy) if scrubbed.strip() else []
|
||||
if rows:
|
||||
ccy = rows[0].get("currency") or _currency_of(raw)
|
||||
ccy = rows[0].get("currency") or _currency_of(raw, base_ccy) or base_ccy
|
||||
money_rows = [x for x in rows if "%" not in str(x.get("amount") or "")]
|
||||
total = (money_rows[0] if money_rows else rows[0])["amount"]
|
||||
if money_rows and len({x.get("currency") for x in money_rows}) == 1:
|
||||
@@ -1040,7 +1070,7 @@ def extract_product_quote(
|
||||
amount = _ascii_amount(hit.group(1) or hit.group(2) or "")
|
||||
if not amount:
|
||||
return {}
|
||||
ccy = _currency_of(raw)
|
||||
ccy = _currency_of(raw, base_ccy) or base_ccy
|
||||
default_fee = _default_product_fee_name(business_line, transport_mode)
|
||||
# 句里已经点了费用名(空运费)时,不能退回默认海运费。
|
||||
named = re.search(
|
||||
@@ -1066,6 +1096,7 @@ def extract_product_quote(
|
||||
return out
|
||||
|
||||
|
||||
|
||||
def _format_percent_rate(raw: str) -> str:
|
||||
"""费率展示统一带 %;已有 % 保留。"""
|
||||
text = (raw or "").strip()
|
||||
@@ -1100,13 +1131,15 @@ def _strip_rate_and_band_noise(text: str) -> str:
|
||||
return cleaned
|
||||
|
||||
|
||||
def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
def _parse_weight_band_quote(text: str, baseline_currency: str = "") -> dict[str, Any]:
|
||||
"""
|
||||
陆运集拼分档单价:2-500KG 单价CNY990 + 可选燃油附加费率/税率。
|
||||
|
||||
产出 weightPrices,供主账出单按「2-500KG」表头对档;费率进顶层字段填 Excel。
|
||||
无币种时优先继承 baseline,再退回 CNY(集拼常见人民币报价)。
|
||||
"""
|
||||
raw = _ascii_amount(text or "")
|
||||
base_ccy = str(baseline_currency or "").strip().upper()
|
||||
tiers: list[dict[str, Any]] = []
|
||||
rows: list[dict[str, str]] = []
|
||||
ccy = ""
|
||||
@@ -1116,7 +1149,7 @@ def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
price = _ascii_amount(hit.group("price") or "")
|
||||
if not price:
|
||||
continue
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or "")
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or base_ccy) or base_ccy
|
||||
ccy = ccy or line_ccy
|
||||
label = f"{lo}-{hi}KG"
|
||||
tiers.append({"min": lo, "max": hi, "price": price})
|
||||
@@ -1133,7 +1166,7 @@ def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
price = _ascii_amount(hit.group("price") or "")
|
||||
if not price:
|
||||
continue
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or "")
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or base_ccy) or base_ccy
|
||||
ccy = ccy or line_ccy
|
||||
label = f"{lo}KG以上"
|
||||
tiers.append({"min": lo, "max": None, "price": price})
|
||||
@@ -1162,7 +1195,7 @@ def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
# 分档是单价,不能把多档相加冒充费用合计。
|
||||
out: dict[str, Any] = {
|
||||
"total": tiers[0]["price"],
|
||||
"currency": ccy or "CNY",
|
||||
"currency": ccy or base_ccy or "CNY",
|
||||
"source_label": "产品报价",
|
||||
"raw_text": (text or "").strip(),
|
||||
"fee_rows": rows,
|
||||
|
||||
Reference in New Issue
Block a user