空运成交后可在本群继续锁舱;调价同步舱位费用桶;出单按空运段骨架落费并保留陆运车型尺寸;导出文件名乱码修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-14 15:34:24 +08:00
co-authored by Cursor
parent 92bdf56a4d
commit cc8529f569
29 changed files with 1941 additions and 276 deletions
+160 -18
View File
@@ -461,6 +461,11 @@ def effective_quote(tms: dict[str, Any] | None, overlay: dict[str, Any] | None)
_apply_amount(old, str(item.get("amount") or ""))
if item.get("currency"):
old["currency"] = item.get("currency")
# 与 merge_quote_fees 一致:盖价保留新行的单位/数量/分段,禁止旧默认「票」残留。
for key in ("unit", "unit_price", "unitPrice", "quantity", "project", "segment"):
val = item.get(key)
if val not in (None, ""):
old[key] = val
rows[idx] = old
out = dict(base)
out["fee_rows"] = rows
@@ -763,6 +768,52 @@ def _match_fee_index(name: str, fees: list[dict[str, Any]], used: set[int]) -> i
)
def _fee_air_project(fee: dict[str, Any]) -> str:
"""
空运费用落到哪一段:有 project 跟 project;否则空运费进空运段,其余进起运港。
与主账 mergeAirBucketRowsWithFlatManualFees 兜底一致,避免增值费进不了起运港。
"""
project = str(fee.get("project") or fee.get("segment") or "").strip()
if project in {"空运段", "空运"}:
return "空运段"
if project in {"起运港", "起运地"}:
return "起运港"
if project in {"目的地", "目的港"}:
return "目的地"
if project in {"其它", "其他"}:
return "其它"
name = _fee_title(fee)
if "空运" in name and "增值" not in name and "增点" not in name:
return "空运段"
return "起运港"
def _fee_as_air_charge(fee: dict[str, Any], *, quote: dict[str, Any]) -> dict[str, Any]:
"""扁平 fee_lines 转舱位 departureCharges / feeItems 会读的结构。"""
title = _fee_title(fee)
plain = _plain_amount(str(fee.get("amount") or ""))
unit_price = _plain_amount(
str(fee.get("unit_price") or fee.get("unitPrice") or fee.get("amount") or "")
)
out: dict[str, Any] = {
"chargeName": title,
"feeName": title,
"name": title,
"amount": plain or unit_price,
"unitPrice": unit_price or plain,
"currency": fee.get("currency") or quote.get("currency") or "",
"project": _fee_air_project(fee),
}
unit = str(fee.get("unit") or fee.get("unitName") or "").strip()
if unit:
out["unit"] = unit
qty = str(fee.get("quantity") or fee.get("chargeQuantity") or "").strip()
if qty:
out["quantity"] = qty
return out
def _stamp_fee_items(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
raw, was_str = _json_struct(quote.get("feeItems"))
items = [dict(x) for x in raw if isinstance(x, dict)] if isinstance(raw, list) else []
@@ -775,8 +826,20 @@ def _stamp_fee_items(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
"currency": fee.get("currency") or quote.get("currency") or "",
}
for fee in fees
if _fee_title(fee)
if _fee_title(fee) and _fee_air_project(fee) == "空运段"
]
if not items:
# 无空运段费目时仍按原逻辑铺全量,避免只有起运附加费时整表空掉
items = [
{
"name": _fee_title(fee),
"feeName": _fee_title(fee),
"amount": _plain_amount(str(fee.get("amount") or "")),
"currency": fee.get("currency") or quote.get("currency") or "",
}
for fee in fees
if _fee_title(fee)
]
else:
used: set[int] = set()
for item in items:
@@ -789,6 +852,22 @@ def _stamp_fee_items(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
item["name"] = _fee_title(fees[idx])
if not item.get("feeName"):
item["feeName"] = _fee_title(fees[idx])
# 调价新增的空运段费目也要进 feeItems,否则模板只读舱位桶时看不到
for i, fee in enumerate(fees):
if i in used or _fee_air_project(fee) != "空运段":
continue
title = _fee_title(fee)
if not title:
continue
items.append(
{
"name": title,
"feeName": title,
"amount": _plain_amount(str(fee.get("amount") or "")),
"currency": fee.get("currency") or quote.get("currency") or "",
}
)
used.add(i)
if items:
_put_struct(quote, "feeItems", items, was_str)
@@ -870,6 +949,67 @@ def _stamp_buckets(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
_store_bucket(quote, key, items)
def _stamp_air_bucket_list(
opt: dict[str, Any],
key_primary: str,
key_alt: str,
fees: list[dict[str, Any]],
used: set[int],
*,
projects: set[str],
quote: dict[str, Any],
) -> None:
"""
更新舱位某一费用桶,并把 fee_lines 里多出来的同段费用追加进去。
为何:调价/产品报价常只改扁平明细;模板读 departureCharges,不追加就会缺「增值费」行。
"""
charge_key = key_primary if key_primary in opt or key_alt not in opt else key_alt
if charge_key not in opt and key_primary not in opt and key_alt not in opt:
# 舱位从未带过该桶:只要有待追加费用就新建,否则不动
if not any(i not in used and _fee_air_project(fees[i]) in projects for i in range(len(fees))):
return
charge_key = key_primary
charges, charges_str = [], False
else:
charges, charges_str = _json_struct(opt.get(charge_key))
if not isinstance(charges, list):
charges = []
copied = [dict(x) if isinstance(x, dict) else x for x in charges]
for charge in copied:
if not isinstance(charge, dict):
continue
idx = _match_fee_index(_fee_title(charge), fees, used)
if idx < 0:
continue
used.add(idx)
plain = _plain_amount(str(fees[idx].get("amount") or ""))
_write_price(charge, plain)
# 单价/数量若调价行有值也盖上,模板按单价×数量重算时才对
fee = fees[idx]
up = _plain_amount(str(fee.get("unit_price") or fee.get("unitPrice") or ""))
if up:
charge["unitPrice"] = up
charge["unit_price"] = up
qty = str(fee.get("quantity") or "").strip()
if qty:
charge["quantity"] = qty
unit = str(fee.get("unit") or "").strip()
if unit:
charge["unit"] = unit
for i, fee in enumerate(fees):
if i in used:
continue
if _fee_air_project(fee) not in projects:
continue
title = _fee_title(fee)
if not title:
continue
copied.append(_fee_as_air_charge(fee, quote=quote))
used.add(i)
opt[charge_key] = json.dumps(copied, ensure_ascii=False) if charges_str else copied
def _stamp_air_copies(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
"""空运报价单读舱位 departureCharges 和 salePrice,不读改过的 fee_lines。"""
total_plain = _plain_amount(str(quote.get("total") or ""))
@@ -882,23 +1022,25 @@ def _stamp_air_copies(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None
for opt in options:
if not isinstance(opt, dict):
continue
charge_key = "departureCharges" if "departureCharges" in opt or "departure_charges" not in opt else "departure_charges"
if charge_key not in opt and "departureCharges" not in opt and "departure_charges" not in opt:
charge_key = ""
if charge_key:
charges, charges_str = _json_struct(opt.get(charge_key))
if isinstance(charges, list) and charges:
used: set[int] = set()
copied = [dict(x) if isinstance(x, dict) else x for x in charges]
for charge in copied:
if not isinstance(charge, dict):
continue
idx = _match_fee_index(_fee_title(charge), fees, used)
if idx < 0:
continue
used.add(idx)
_write_price(charge, _plain_amount(str(fees[idx].get("amount") or "")))
opt[charge_key] = json.dumps(copied, ensure_ascii=False) if charges_str else copied
used: set[int] = set()
_stamp_air_bucket_list(
opt,
"departureCharges",
"departure_charges",
fees,
used,
projects={"起运港", "其它"},
quote=quote,
)
_stamp_air_bucket_list(
opt,
"destinationCharges",
"destination_charges",
fees,
used,
projects={"目的地"},
quote=quote,
)
if total_plain:
if opt.get("salePrice") not in (None, ""):
opt["salePrice"] = total_plain