空运人工价锁舱现拉补全舱位并拦清缺参;成交/报价填表与工单陆运费率展示同步收紧。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-07 23:35:24 +08:00
co-authored by Cursor
parent 419b4a1bf7
commit e197ecea10
31 changed files with 1607 additions and 150 deletions
+158 -12
View File
@@ -757,6 +757,30 @@ _RE_MONEY = re.compile(
r"|(?:USD|CNY|RMB|美金|美元)\s*(\d+(?:\.\d+)?)",
re.I,
)
# 陆运集拼:「2-500KG 单价CNY990」是分档单价,不是「燃油附加费=2」。
_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*)?"
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*)?"
r"(?P<price>\d+(?:\.\d+)?)",
re.I,
)
_RE_FUEL_SURCHARGE_RATE = re.compile(
r"燃油附加费率\s*[::=]?\s*(?P<rate>\d+(?:\.\d+)?)\s*%?",
re.I,
)
_RE_TAX_RATE = re.compile(
r"(?<![A-Za-z\u4e00-\u9fff])税率\s*[::=]?\s*(?P<rate>\d+(?:\.\d+)?)\s*%?",
re.I,
)
_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*(?:改为|改成|调整为|调整成)?"
@@ -867,6 +891,8 @@ def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = Non
认不出价格返回空 dict,调用方回「没有识别到报价」。
群里常见「码头操作费100CNY / 报关费改为300CNY」,不能只把第一笔数当成海运费。
「空运费 单价…金额…」必须保留空运费,金额用「金额」后面的数,不用单价。
陆运集拼:「2-500KG 单价CNY990,燃油附加费率1%,税率6%」走分档单价+费率,
禁止把公斤档下限「2」当成燃油附加费金额(WO021)。
"""
if isinstance(injected, dict) and (injected.get("total") or injected.get("fee_rows")):
out = dict(injected)
@@ -879,16 +905,22 @@ def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = Non
compact = stripped.replace(" ", "").replace(" ", "")
if re.fullmatch(r"WO\d{12}", compact, re.I):
return {}
rows = _parse_fee_rows(raw)
band = _parse_weight_band_quote(raw)
if band:
return band
# 费率句若仍落到普通费用行,会把「燃油附加费」配上句首公斤数。先剥掉再抽。
scrubbed = _strip_rate_and_band_noise(raw)
rows = _parse_fee_rows(scrubbed) if scrubbed.strip() else []
if rows:
ccy = rows[0].get("currency") or _currency_of(raw)
total = rows[0]["amount"]
if len({x.get("currency") for x in rows}) == 1:
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:
try:
total = str(sum(float(x["amount"]) for x in rows))
total = str(sum(float(x["amount"]) for x in money_rows))
except ValueError:
total = rows[0]["amount"]
return {
total = money_rows[0]["amount"]
out = {
"total": total,
"currency": ccy,
"source_label": "产品报价",
@@ -896,9 +928,12 @@ def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = Non
"fee_rows": rows,
"fee_lines": list(rows),
}
hit = _RE_MONEY.search(_ascii_amount(raw))
out.update(_extract_rate_fields(raw))
return out
hit = _RE_MONEY.search(_ascii_amount(scrubbed or raw))
if not hit:
return {}
rates = _extract_rate_fields(raw)
return rates if rates else {}
amount = _ascii_amount(hit.group(1) or hit.group(2) or "")
if not amount:
return {}
@@ -906,12 +941,13 @@ def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = Non
# 句里已经点了费用名(空运费)时,不能退回默认海运费。
named = re.search(
r"([\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费)",
raw,
scrubbed or raw,
)
fee_name = (named.group(1) if named else "") or "海运费"
if fee_name in {"费用"}:
fee_name = "海运费"
return {
if fee_name in {"费用", "燃油附加费"}:
# 「燃油附加费率」里的「燃油附加费」不是费用行名;无其它费名时用运费。
fee_name = "海运费" if fee_name == "费用" else "陆运费"
out = {
"total": amount,
"currency": ccy,
"source_label": "产品报价",
@@ -919,3 +955,113 @@ def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = Non
"fee_rows": [{"name": fee_name, "amount": amount, "currency": ccy}],
"fee_lines": [{"name": fee_name, "amount": amount, "currency": ccy}],
}
out.update(_extract_rate_fields(raw))
return out
def _format_percent_rate(raw: str) -> str:
"""费率展示统一带 %;已有 % 保留。"""
text = (raw or "").strip()
if not text:
return ""
if text.endswith("%"):
return text
return f"{text}%"
def _extract_rate_fields(text: str) -> dict[str, str]:
"""抽出燃油附加费率 / 税率;只返回出现过的键。"""
out: dict[str, str] = {}
fuel = _RE_FUEL_SURCHARGE_RATE.search(text or "")
if fuel:
out["fuel_surcharge_rate"] = _format_percent_rate(fuel.group("rate") or "")
out["fuelSurchargeRate"] = out["fuel_surcharge_rate"]
tax = _RE_TAX_RATE.search(text or "")
if tax:
out["tax_rate"] = _format_percent_rate(tax.group("rate") or "")
out["taxRate"] = out["tax_rate"]
return out
def _strip_rate_and_band_noise(text: str) -> str:
"""去掉分档价与费率短语,避免后续费用行/金额回退误伤。"""
cleaned = _ascii_amount(text or "")
cleaned = _RE_WEIGHT_BAND_PRICE.sub(" ", cleaned)
cleaned = _RE_WEIGHT_BAND_OPEN.sub(" ", cleaned)
cleaned = _RE_FUEL_SURCHARGE_RATE.sub(" ", cleaned)
cleaned = _RE_TAX_RATE.sub(" ", cleaned)
return cleaned
def _parse_weight_band_quote(text: str) -> dict[str, Any]:
"""
陆运集拼分档单价:2-500KG 单价CNY990 + 可选燃油附加费率/税率。
产出 weightPrices,供主账出单按「2-500KG」表头对档;费率进顶层字段填 Excel。
"""
raw = _ascii_amount(text or "")
tiers: list[dict[str, Any]] = []
rows: list[dict[str, str]] = []
ccy = ""
for hit in _RE_WEIGHT_BAND_PRICE.finditer(raw):
lo = int(hit.group("min") or 0)
hi = int(hit.group("max") or 0)
price = _ascii_amount(hit.group("price") or "")
if not price:
continue
line_ccy = _currency_of(raw, hit.group("ccy") or "")
ccy = ccy or line_ccy
label = f"{lo}-{hi}KG"
tiers.append({"min": lo, "max": hi, "price": price})
rows.append(
{
"name": label,
"amount": price,
"currency": line_ccy,
"unit_price": price,
}
)
for hit in _RE_WEIGHT_BAND_OPEN.finditer(raw):
lo = int(hit.group("min") or 0)
price = _ascii_amount(hit.group("price") or "")
if not price:
continue
line_ccy = _currency_of(raw, hit.group("ccy") or "")
ccy = ccy or line_ccy
label = f"{lo}KG以上"
tiers.append({"min": lo, "max": None, "price": price})
rows.append(
{
"name": label,
"amount": price,
"currency": line_ccy,
"unit_price": price,
}
)
if not tiers:
return {}
rates = _extract_rate_fields(raw)
# 费率行进明细方便群摘要展示;金额带 %,合计时只加数字价。
if rates.get("fuel_surcharge_rate"):
rows.append(
{
"name": "燃油附加费率",
"amount": rates["fuel_surcharge_rate"],
"currency": "",
}
)
if rates.get("tax_rate"):
rows.append({"name": "税率", "amount": rates["tax_rate"], "currency": ""})
# 分档是单价,不能把多档相加冒充费用合计。
out: dict[str, Any] = {
"total": tiers[0]["price"],
"currency": ccy or "CNY",
"source_label": "产品报价",
"raw_text": (text or "").strip(),
"fee_rows": rows,
"fee_lines": list(rows),
"weightPrices": tiers,
"price_per_kg": tiers[0]["price"],
}
out.update(rates)
return out