完善空运锁舱与海运报价单单选回填,并增强模板预览与协同字段盖写。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,7 +20,7 @@ _CLOSED = frozenset({"已成交", "未成交", "已关闭", "转人工"})
|
||||
_QUOTED = frozenset({"已报价", "协商中"})
|
||||
|
||||
_PERCENT_UP = re.compile(
|
||||
r"(上调|上涨|上升|上浮|提高|加|涨|升)\s*(\d+(?:\.\d+)?)\s*%"
|
||||
r"(上调|上涨|上升|上浮|提高|提升|加|涨|升)\s*(\d+(?:\.\d+)?)\s*%"
|
||||
)
|
||||
_PERCENT_DOWN = re.compile(r"(下调|下降|降低|减|降)\s*(\d+(?:\.\d+)?)\s*%")
|
||||
_TARGET = re.compile(
|
||||
@@ -531,6 +531,7 @@ def extract_adjust(
|
||||
if not raw:
|
||||
return AdjustExtract(is_adjust=False)
|
||||
names = [str(x).strip() for x in (fee_names or []) if str(x).strip()]
|
||||
names_sorted = sorted(names, key=len, reverse=True)
|
||||
use_model = allow_model
|
||||
if use_model is None:
|
||||
try:
|
||||
@@ -558,11 +559,28 @@ def extract_adjust(
|
||||
op = _item_from_chunk(raw, "")
|
||||
if op:
|
||||
modeled.items = [op]
|
||||
# 模型常只抽出「费用合计」:本地再扫点名小计/费目,避免「始发港费用小计提升10%」被丢掉
|
||||
local_items = _local_fee_items(raw, names_sorted)
|
||||
if local_items:
|
||||
seen = {
|
||||
str(x.fee_name or "").strip()
|
||||
for x in (modeled.items or [])
|
||||
if str(x.fee_name or "").strip()
|
||||
}
|
||||
merged = list(modeled.items or [])
|
||||
for it in local_items:
|
||||
key = str(it.fee_name or "").strip()
|
||||
if key and key not in seen:
|
||||
merged.append(it)
|
||||
seen.add(key)
|
||||
modeled = AdjustExtract(
|
||||
is_adjust=True,
|
||||
items=merged,
|
||||
wants_total=bool(modeled.wants_total),
|
||||
)
|
||||
return modeled
|
||||
except Exception:
|
||||
logger.exception("调价模型抽取失败,回退本地")
|
||||
names_sorted = sorted(names, key=len, reverse=True)
|
||||
blob = raw.replace(" ", "")
|
||||
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))
|
||||
fee_in_text = [n for n in names_sorted if n and n in raw]
|
||||
@@ -571,6 +589,17 @@ def extract_adjust(
|
||||
return AdjustExtract(is_adjust=True, wants_total=True, items=[op] if op else [])
|
||||
if _QUOTE_WORD in raw and has_op and not fee_in_text and not total_hit:
|
||||
return AdjustExtract(is_adjust=True, wants_total=False)
|
||||
items = _local_fee_items(raw, names_sorted)
|
||||
if not items and has_op:
|
||||
# 说了费用口语但和名单不完全一致:整句再扫一次
|
||||
item = _item_from_chunk(raw, "")
|
||||
if item:
|
||||
items.append(item)
|
||||
return AdjustExtract(is_adjust=bool(items or has_op), items=items)
|
||||
|
||||
|
||||
def _local_fee_items(raw: str, names_sorted: list[str]) -> list[AdjustItem]:
|
||||
"""按分句把点名费目/小计抽成调价项。提升/上调等口令都认。"""
|
||||
items: list[AdjustItem] = []
|
||||
for part in _SPLIT.split(raw) or [raw]:
|
||||
chunk = (part or "").strip()
|
||||
@@ -585,16 +614,15 @@ def extract_adjust(
|
||||
found = _FEE_TOKEN.findall(chunk)
|
||||
spoken = found[-1] if found else ""
|
||||
if not spoken:
|
||||
continue
|
||||
# 「费用合计提升10%」:名单里有费用合计时上面已命中;否则用合计词
|
||||
if any(w in chunk for w in _TOTAL_WORDS):
|
||||
spoken = "费用合计"
|
||||
else:
|
||||
continue
|
||||
item = _item_from_chunk(chunk, spoken)
|
||||
if item:
|
||||
items.append(item)
|
||||
if not items and has_op:
|
||||
# 说了费用口语但和名单不完全一致:整句再扫一次
|
||||
item = _item_from_chunk(raw, "")
|
||||
if item:
|
||||
items.append(item)
|
||||
return AdjustExtract(is_adjust=bool(items or has_op), items=items)
|
||||
return items
|
||||
|
||||
|
||||
def _item_from_chunk(chunk: str, fee_name: str) -> Optional[AdjustItem]:
|
||||
@@ -763,6 +791,42 @@ def _apply_total_only(
|
||||
)
|
||||
|
||||
|
||||
def _fold_total_into_items(
|
||||
extract: AdjustExtract,
|
||||
*,
|
||||
business_line: str = "",
|
||||
quote: dict[str, Any] | None = None,
|
||||
) -> AdjustExtract:
|
||||
"""
|
||||
合计与费目小计写在同一条指令里时:取消 wants_total 独占,把合计并进 items。
|
||||
|
||||
为何:wants_total 只改 total,海运「始发港费用小计」不会动,Excel 汇总仍是旧数。
|
||||
"""
|
||||
if not extract.wants_total:
|
||||
return extract
|
||||
named = [x for x in (extract.items or []) if str(x.fee_name or "").strip()]
|
||||
if not named:
|
||||
return extract
|
||||
label = total_price_label(business_line, quote=quote)
|
||||
has_total = any(fee_name_same(str(x.fee_name or ""), label) for x in named)
|
||||
items = list(named)
|
||||
if not has_total:
|
||||
seed = named[0]
|
||||
for raw in extract.items or []:
|
||||
if not str(raw.fee_name or "").strip():
|
||||
seed = raw
|
||||
break
|
||||
items.append(
|
||||
AdjustItem(
|
||||
fee_name=label,
|
||||
kind=seed.kind,
|
||||
direction=seed.direction,
|
||||
value=seed.value,
|
||||
)
|
||||
)
|
||||
return AdjustExtract(is_adjust=True, wants_total=False, items=items)
|
||||
|
||||
|
||||
def apply_adjust(
|
||||
quote: dict[str, Any] | None,
|
||||
extract: AdjustExtract,
|
||||
@@ -782,6 +846,8 @@ def apply_adjust(
|
||||
rows = adjustable_rows(src)
|
||||
if not extract.is_adjust:
|
||||
return ApplyResult(ok=False, code="not_adjust", quote=src)
|
||||
# 同时说了「始发港费用小计」和「费用合计」时:不能只走合计通道,否则小计不改、Excel 汇总也不动。
|
||||
extract = _fold_total_into_items(extract, business_line=business_line, quote=src)
|
||||
header = str(src.get("currency") or "").strip()
|
||||
total_ccy = row_currency({"currency": header, "amount": src.get("total")}, header)
|
||||
currencies = {row_currency(r, header) for r in rows if row_currency(r, header)}
|
||||
@@ -798,6 +864,7 @@ def apply_adjust(
|
||||
currency_hint = bool(multi)
|
||||
total_amt, _ = parse_money(src.get("total"))
|
||||
new_total = total_amt
|
||||
total_touched = False
|
||||
single_same_ccy = _single_line_same_currency(
|
||||
[row for row in rows if not row.get("_subtotal_key") and not row.get("_is_total")],
|
||||
total_ccy or header,
|
||||
@@ -845,8 +912,10 @@ def apply_adjust(
|
||||
bucket = str(row.get("_bucket") or "")
|
||||
if row.get("_is_total"):
|
||||
subtotal_only["__total__"] = new_amt
|
||||
total_touched = True
|
||||
continue
|
||||
if row.get("_subtotal_key"):
|
||||
# 只改小计数字写回 fee_total_*;明细是否另调看有没有点名费目
|
||||
subtotal_only[bucket] = new_amt - old_amt
|
||||
continue
|
||||
if bucket:
|
||||
@@ -861,10 +930,15 @@ def apply_adjust(
|
||||
|
||||
if (not multi) and new_total is not None:
|
||||
if "__total__" in subtotal_only:
|
||||
# 合计是整单改数;小计差额不再叠进合计,避免「合计×1.1 + 小计差额」重复
|
||||
new_total = subtotal_only.pop("__total__")
|
||||
for bucket, delta in subtotal_only.items():
|
||||
if bucket not in touched_buckets:
|
||||
new_total = new_total + delta
|
||||
else:
|
||||
for bucket, delta in subtotal_only.items():
|
||||
if bucket not in touched_buckets:
|
||||
new_total = new_total + delta
|
||||
elif total_touched and "__total__" in subtotal_only:
|
||||
# 多币种:点名「费用合计」仍改合计数字,不做汇率换算
|
||||
new_total = subtotal_only["__total__"]
|
||||
|
||||
out = dict(src)
|
||||
visible = []
|
||||
@@ -876,7 +950,7 @@ def apply_adjust(
|
||||
out["fee_rows"] = visible
|
||||
out["fee_lines"] = visible
|
||||
_write_adjusted_buckets(out, rows)
|
||||
if applied and (not multi) and new_total is not None and new_total != total_amt:
|
||||
if applied and new_total is not None and new_total != total_amt and ((not multi) or total_touched):
|
||||
out["total"] = format_money(new_total, sample=str(src.get("total") or ""), currency=total_ccy)
|
||||
if not applied:
|
||||
code = "ambiguous" if ambiguous else "none_matched"
|
||||
|
||||
Reference in New Issue
Block a user