空运人工价锁舱现拉补全舱位并拦清缺参;成交/报价填表与工单陆运费率展示同步收紧。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user