员工岗位可重新启用;调价认空运航线;会话存档跳号补拉;陆运报价按车型列回填
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,9 +23,15 @@ _PERCENT_UP = re.compile(
|
||||
r"(上调|上涨|上升|上浮|提高|提升|加|涨|升)\s*(\d+(?:\.\d+)?)\s*%"
|
||||
)
|
||||
_PERCENT_DOWN = re.compile(r"(下调|下降|降低|减|降)\s*(\d+(?:\.\d+)?)\s*%")
|
||||
_TARGET = re.compile(
|
||||
r"(提升到|减少到|上调到|下调到|加到|减到|调到|改为|改成)\s*(\d+(?:\.\d+)?)"
|
||||
# 「增加到/调整到/涨到 110%」= 改到原价的 110%,不是上调 110%。必须先于绝对金额与相对上调。
|
||||
_TO_VERBS = (
|
||||
"增加到|提高到|提升到|减少到|上调到|下调到|调整到|上浮到|上涨到|上升到|"
|
||||
"涨到|升到|加到|减到|调到|增至|变为|变成|改为|改成|调整为|调整成"
|
||||
)
|
||||
_PERCENT_TO = re.compile(rf"({_TO_VERBS})\s*(\d+(?:\.\d+)?)\s*%")
|
||||
# (?!\d) 防止「增加到110%」回退成「加到11」;(?!\s*%) 把带百分号的留给 _PERCENT_TO。
|
||||
_TARGET = re.compile(rf"({_TO_VERBS})\s*(\d+(?:\.\d+)?)(?!\d)(?!\s*%)")
|
||||
_DAO_PERCENT = re.compile(rf"(?:{_TO_VERBS})\s*(\d+(?:\.\d+)?)\s*%")
|
||||
_SPLIT = re.compile(r"[,,;;。!!??\n]|和(?=\S)")
|
||||
_FEE_TOKEN = re.compile(r"([\u4e00-\u9fffA-Za-z0-9]{1,16}费)")
|
||||
_TOTAL_WORDS = ("费用合计", "合计", "总价", "汇总价", "总销售价", "总成本价")
|
||||
@@ -432,9 +438,74 @@ def row_currency(row: dict[str, Any] | None, fallback: str = "") -> str:
|
||||
return prefix or (fallback or "").strip().upper().replace("RMB", "CNY")
|
||||
|
||||
|
||||
def _parse_qty(raw: Any) -> Optional[float]:
|
||||
"""数量/计费重转浮点;读不了返回 None。"""
|
||||
if raw in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return float(str(raw).replace(",", "").strip())
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _row_or_quote_qty(row: dict[str, Any], quote: dict[str, Any] | None = None) -> Optional[float]:
|
||||
"""
|
||||
费用行数量;行上没有时用工单/舱位计费重兜底。
|
||||
|
||||
空运调价常只有 unitPrice+amount,quantity 在 chargeableWeightKg。
|
||||
"""
|
||||
qty = _parse_qty(row.get("quantity"))
|
||||
if qty is not None:
|
||||
return qty
|
||||
qty = _parse_qty(row.get("chargeQuantity"))
|
||||
if qty is not None:
|
||||
return qty
|
||||
src = dict(quote or {})
|
||||
for key in ("chargeableWeightKg", "chargeable_weight_kg", "chargeableWeight"):
|
||||
qty = _parse_qty(src.get(key))
|
||||
if qty is not None:
|
||||
return qty
|
||||
opt = src.get("selected_air_option") or src.get("selectedAirOption")
|
||||
if isinstance(opt, str) and opt.strip():
|
||||
try:
|
||||
opt = json.loads(opt)
|
||||
except json.JSONDecodeError:
|
||||
opt = None
|
||||
if isinstance(opt, dict):
|
||||
for key in ("chargeableWeightKg", "chargeable_weight_kg", "chargeableWeight"):
|
||||
qty = _parse_qty(opt.get(key))
|
||||
if qty is not None:
|
||||
return qty
|
||||
return None
|
||||
|
||||
|
||||
def _apply_amount(row: dict[str, Any], amount: str) -> None:
|
||||
"""
|
||||
写入行金额;有数量时单价=金额÷数量,禁止把行合计原样塞进单价。
|
||||
|
||||
空运 KG:调价后金额是单价×计费重,展示读 unit_price;无数量(按票等)才单价=金额。
|
||||
"""
|
||||
row["amount"] = amount
|
||||
row["unit_price"] = amount
|
||||
qty_raw = row.get("quantity")
|
||||
if qty_raw in (None, ""):
|
||||
qty_raw = row.get("chargeQuantity")
|
||||
qty_num: Optional[float] = None
|
||||
if qty_raw not in (None, ""):
|
||||
try:
|
||||
qty_num = float(str(qty_raw).replace(",", "").strip())
|
||||
except ValueError:
|
||||
qty_num = None
|
||||
amt_num, _ = parse_money(amount)
|
||||
if qty_num is not None and abs(qty_num) > 1e-9 and amt_num is not None:
|
||||
# 有计费数量:单价从行金额反推,避免 3385.8 被当成「每KG」
|
||||
unit_plain = format_money(amt_num / qty_num, sample=str(row.get("unit_price") or row.get("unitPrice") or ""))
|
||||
unit_plain = _plain_amount(unit_plain)
|
||||
row["unit_price"] = unit_plain
|
||||
row["unitPrice"] = unit_plain
|
||||
else:
|
||||
row["unit_price"] = amount
|
||||
if row.get("unitPrice") not in (None, ""):
|
||||
row["unitPrice"] = amount
|
||||
for key in ("salePrice", "price", "value"):
|
||||
if row.get(key) not in (None, ""):
|
||||
row[key] = amount
|
||||
@@ -518,7 +589,12 @@ def text_has_total_only_adjust(text: str) -> bool:
|
||||
「总销售价改为180」仍是销售调价。点了「空运费」「报关费」则不是。不打模型。
|
||||
"""
|
||||
raw = text or ""
|
||||
if not (_TARGET.search(raw) or _PERCENT_UP.search(raw) or _PERCENT_DOWN.search(raw)):
|
||||
if not (
|
||||
_TARGET.search(raw)
|
||||
or _PERCENT_TO.search(raw)
|
||||
or _PERCENT_UP.search(raw)
|
||||
or _PERCENT_DOWN.search(raw)
|
||||
):
|
||||
return False
|
||||
if not any(word in raw for word in _TOTAL_WORDS) and "报价" not in raw:
|
||||
return False
|
||||
@@ -546,27 +622,30 @@ def extract_adjust(
|
||||
本地回退只给无网/模型失败用,不是正式识别。
|
||||
allow_model=False 时禁止打模型。
|
||||
"""
|
||||
raw = (text or "").strip()
|
||||
if injected is not None:
|
||||
if isinstance(injected, AdjustExtract):
|
||||
return injected
|
||||
return _coerce_dao_percent(raw, injected)
|
||||
items = []
|
||||
for raw in injected.get("items") or []:
|
||||
if not isinstance(raw, dict):
|
||||
for row in injected.get("items") or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
items.append(
|
||||
AdjustItem(
|
||||
fee_name=str(raw.get("fee_name") or "").strip(),
|
||||
kind=str(raw.get("kind") or "percent"),
|
||||
direction=str(raw.get("direction") or "up"),
|
||||
value=float(raw.get("value") or 0),
|
||||
fee_name=str(row.get("fee_name") or "").strip(),
|
||||
kind=str(row.get("kind") or "percent"),
|
||||
direction=str(row.get("direction") or "up"),
|
||||
value=float(row.get("value") or 0),
|
||||
)
|
||||
)
|
||||
return AdjustExtract(
|
||||
is_adjust=bool(injected.get("is_adjust")),
|
||||
items=items,
|
||||
wants_total=bool(injected.get("wants_total")),
|
||||
return _coerce_dao_percent(
|
||||
raw,
|
||||
AdjustExtract(
|
||||
is_adjust=bool(injected.get("is_adjust")),
|
||||
items=items,
|
||||
wants_total=bool(injected.get("wants_total")),
|
||||
),
|
||||
)
|
||||
raw = (text or "").strip()
|
||||
if not raw:
|
||||
return AdjustExtract(is_adjust=False)
|
||||
names = [str(x).strip() for x in (fee_names or []) if str(x).strip()]
|
||||
@@ -617,11 +696,16 @@ def extract_adjust(
|
||||
items=merged,
|
||||
wants_total=bool(modeled.wants_total),
|
||||
)
|
||||
return modeled
|
||||
return _coerce_dao_percent(raw, modeled)
|
||||
except Exception:
|
||||
logger.exception("调价模型抽取失败,回退本地")
|
||||
total_hit = any(w in raw for w in _TOTAL_WORDS)
|
||||
has_op = bool(_PERCENT_UP.search(raw) or _PERCENT_DOWN.search(raw) or _TARGET.search(raw))
|
||||
has_op = bool(
|
||||
_PERCENT_UP.search(raw)
|
||||
or _PERCENT_DOWN.search(raw)
|
||||
or _PERCENT_TO.search(raw)
|
||||
or _TARGET.search(raw)
|
||||
)
|
||||
fee_in_text = [n for n in names_sorted if n and n in raw]
|
||||
if total_hit and has_op and not fee_in_text:
|
||||
op = _item_from_chunk(raw, "")
|
||||
@@ -634,7 +718,9 @@ def extract_adjust(
|
||||
item = _item_from_chunk(raw, "")
|
||||
if item:
|
||||
items.append(item)
|
||||
return AdjustExtract(is_adjust=bool(items or has_op), items=items)
|
||||
return _coerce_dao_percent(
|
||||
raw, AdjustExtract(is_adjust=bool(items or has_op), items=items)
|
||||
)
|
||||
|
||||
|
||||
def _local_fee_items(raw: str, names_sorted: list[str]) -> list[AdjustItem]:
|
||||
@@ -664,10 +750,40 @@ def _local_fee_items(raw: str, names_sorted: list[str]) -> list[AdjustItem]:
|
||||
return items
|
||||
|
||||
|
||||
def _coerce_dao_percent(text: str, extract: AdjustExtract) -> AdjustExtract:
|
||||
"""
|
||||
「增加到110%」纠正为 percent/set。
|
||||
|
||||
模型常抽成 percent/up;本地旧正则会抽成绝对金额 target。两种都不能当上调。
|
||||
"""
|
||||
hit = _DAO_PERCENT.search(text or "")
|
||||
if not hit or not extract.items:
|
||||
return extract
|
||||
val = float(hit.group(1))
|
||||
fixed: list[AdjustItem] = []
|
||||
changed = False
|
||||
for item in extract.items:
|
||||
same_num = abs(float(item.value) - val) < 1e-9
|
||||
if item.kind == "percent" and item.direction in {"up", "down"} and same_num:
|
||||
fixed.append(AdjustItem(item.fee_name, "percent", "set", val))
|
||||
changed = True
|
||||
elif item.kind == "target" and item.direction == "set" and same_num:
|
||||
fixed.append(AdjustItem(item.fee_name, "percent", "set", val))
|
||||
changed = True
|
||||
else:
|
||||
fixed.append(item)
|
||||
if not changed:
|
||||
return extract
|
||||
return AdjustExtract(is_adjust=extract.is_adjust, items=fixed, wants_total=extract.wants_total)
|
||||
|
||||
|
||||
def _item_from_chunk(chunk: str, fee_name: str) -> Optional[AdjustItem]:
|
||||
to_pct = _PERCENT_TO.search(chunk)
|
||||
up = _PERCENT_UP.search(chunk)
|
||||
down = _PERCENT_DOWN.search(chunk)
|
||||
target = _TARGET.search(chunk)
|
||||
if to_pct:
|
||||
return AdjustItem(fee_name=fee_name, kind="percent", direction="set", value=float(to_pct.group(2)))
|
||||
if target:
|
||||
return AdjustItem(fee_name=fee_name, kind="target", direction="set", value=float(target.group(2)))
|
||||
if up:
|
||||
@@ -1167,6 +1283,10 @@ def _apply_total_only(
|
||||
if item.kind == "target":
|
||||
new_amt = float(item.value)
|
||||
how = f"改到 {item.value:g}"
|
||||
elif item.kind == "percent" and item.direction == "set":
|
||||
# 「费用合计增加到110%」= 改到原合计的 110%,不是上调 110%
|
||||
new_amt = old_total * (float(item.value) / 100.0)
|
||||
how = f"调到 {item.value:g}%"
|
||||
elif item.direction == "down":
|
||||
new_amt = old_total * (1 - float(item.value) / 100.0)
|
||||
how = f"下调 {item.value:g}%"
|
||||
@@ -1311,15 +1431,45 @@ def apply_adjust(
|
||||
if old_amt is None:
|
||||
continue
|
||||
line_ccy = row_currency(row, header) or prefix
|
||||
if item.kind == "target":
|
||||
old_up, _ = parse_money(row.get("unit_price") or row.get("unitPrice"))
|
||||
qty_num = _row_or_quote_qty(row, src)
|
||||
# 有单价且有计费重:比例调价按「单价」缩放,再算行金额=单价×数量。
|
||||
# 否则会把行合计×110% 后整笔写进单价(15→42633.53)。
|
||||
scale_by_unit = (
|
||||
item.kind == "percent"
|
||||
and old_up is not None
|
||||
and qty_num is not None
|
||||
and abs(qty_num) > 1e-9
|
||||
and abs(old_up - old_amt) > 1e-6
|
||||
)
|
||||
if scale_by_unit:
|
||||
if item.direction == "set":
|
||||
new_up = old_up * (float(item.value) / 100.0)
|
||||
how = f"调到 {item.value:g}%"
|
||||
elif item.direction == "down":
|
||||
new_up = old_up * (1 - float(item.value) / 100.0)
|
||||
how = f"下调 {item.value:g}%"
|
||||
else:
|
||||
new_up = old_up * (1 + float(item.value) / 100.0)
|
||||
how = f"上调 {item.value:g}%"
|
||||
new_amt = new_up * qty_num
|
||||
if row.get("quantity") in (None, ""):
|
||||
row["quantity"] = f"{qty_num:.10g}"
|
||||
elif item.kind == "target":
|
||||
new_amt = float(item.value)
|
||||
how = f"改到 {item.value:g}"
|
||||
elif item.kind == "percent" and item.direction == "set":
|
||||
# 「运输费增加到110%」= 原价 × 110%,不是上调 110%(×210%)
|
||||
new_amt = old_amt * (float(item.value) / 100.0)
|
||||
how = f"调到 {item.value:g}%"
|
||||
elif item.direction == "down":
|
||||
new_amt = old_amt * (1 - float(item.value) / 100.0)
|
||||
how = f"下调 {item.value:g}%"
|
||||
else:
|
||||
new_amt = old_amt * (1 + float(item.value) / 100.0)
|
||||
how = f"上调 {item.value:g}%"
|
||||
if qty_num is not None and row.get("quantity") in (None, ""):
|
||||
row["quantity"] = f"{qty_num:.10g}"
|
||||
_apply_amount(row, format_money(new_amt, sample=str(row.get("amount") or ""), currency=line_ccy or prefix))
|
||||
rows[idx] = row
|
||||
if not any_applied:
|
||||
|
||||
Reference in New Issue
Block a user