机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -240,23 +240,221 @@ def _rows_from_list(raw: Any) -> list[tuple[str, str]]:
|
||||
return out
|
||||
|
||||
|
||||
def option_fee_lines(option: dict[str, Any] | None, quote: dict[str, Any] | None = None) -> list[tuple[str, str]]:
|
||||
"""
|
||||
该线路 TMS 回了哪些费用就列哪些。
|
||||
def _fee_item_dicts(raw: Any) -> list[dict[str, Any]]:
|
||||
"""TMS 费用数组收成字典;字符串 JSON 也认。无名称的跳过。"""
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
out: list[dict[str, Any]] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = str(
|
||||
item.get("chargeName") or item.get("feeName") or item.get("name") or item.get("label") or ""
|
||||
).strip()
|
||||
if not name:
|
||||
continue
|
||||
row = dict(item)
|
||||
row["name"] = name
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
只读本条 departureCharges / feeItems。多条线路时,整单 fee_lines 是最低价方案,
|
||||
禁止套到没回费用的线路。仅一条线路时,整单 fee_lines 才算这条的明细。
|
||||
|
||||
def _top_fee_items_for_option(
|
||||
quote: dict[str, Any] | None,
|
||||
option: dict[str, Any] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
舱位缺 feeItems 时补整单顶层(OMS 空运表价常把空运费放在 data.feeItems)。
|
||||
|
||||
顶层带 airOptionNo 时优先本舱位;对不上编号时不能退成空列表(否则只剩增点/拆板)。
|
||||
与主账 InquiryTmsQueryService.extractAirDepartureCharges 口径一致。
|
||||
"""
|
||||
items = _fee_item_dicts((quote or {}).get("feeItems") or (quote or {}).get("fee_items"))
|
||||
if not items:
|
||||
return []
|
||||
opt_no = str(
|
||||
(option or {}).get("optionNo") or (option or {}).get("option_no") or ""
|
||||
).strip()
|
||||
tagged = [
|
||||
row
|
||||
for row in items
|
||||
if str(row.get("airOptionNo") or row.get("air_option_no") or "").strip()
|
||||
]
|
||||
if opt_no and tagged:
|
||||
matched = [
|
||||
row
|
||||
for row in items
|
||||
if str(row.get("airOptionNo") or row.get("air_option_no") or "").strip() == opt_no
|
||||
]
|
||||
if matched:
|
||||
return matched
|
||||
return items
|
||||
|
||||
|
||||
def _has_air_freight_name(rows: list[dict[str, Any]]) -> bool:
|
||||
"""明细里是否已有空运段运费名。"""
|
||||
for row in rows:
|
||||
name = str(row.get("name") or "").strip()
|
||||
if name == "空运费" or name == "航空运费" or (name.endswith("运费") and "空运" in name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _only_origin_surcharges(rows: list[dict[str, Any]]) -> bool:
|
||||
"""
|
||||
是否只剩起运附加费(增点/拆板等),没有运输费类分项。
|
||||
|
||||
有运输费/提送货费时不注入总价空运费,避免和 ZUH 分项叠两套。
|
||||
"""
|
||||
if not rows:
|
||||
return False
|
||||
for row in rows:
|
||||
name = str(row.get("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
if name in {"运输费", "提送货费", "人工费", "空运费", "航空运费"}:
|
||||
return False
|
||||
if "运费" in name and "增点" not in name and "拆板" not in name:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _air_freight_from_quote_fee_lines(
|
||||
quote: dict[str, Any] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""主账 fee_lines 里已有的空运费行(带单价/单位更好)。"""
|
||||
rows = _fee_item_dicts(
|
||||
(quote or {}).get("fee_lines") or (quote or {}).get("fee_rows")
|
||||
)
|
||||
return [row for row in rows if str(row.get("name") or "").strip() in {"空运费", "航空运费"}]
|
||||
|
||||
|
||||
def _inject_air_freight_from_option(
|
||||
option: dict[str, Any] | None,
|
||||
quote: dict[str, Any] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
舱位/顶层都没有空运费时,用 salePrice 或 unitPrice×计费重补一行。
|
||||
|
||||
对齐主账 injectAirFreightFeeLineIfMissing。无可用金额则不补。
|
||||
"""
|
||||
src = dict(option or {})
|
||||
rows = _rows_from_list(src.get("departureCharges") or src.get("departure_charges"))
|
||||
rows.extend(_rows_from_list(src.get("feeItems") or src.get("fee_items")))
|
||||
q = dict(quote or {})
|
||||
unit_price = src.get("unitPrice")
|
||||
if unit_price in (None, "", "-"):
|
||||
unit_price = src.get("unit_price") or src.get("pricePerKg") or src.get("freightRate")
|
||||
sale = src.get("salePrice")
|
||||
if sale in (None, "", "-"):
|
||||
sale = src.get("sale_price") or q.get("total") or q.get("salePrice")
|
||||
cw = src.get("chargeableWeightKg")
|
||||
if cw in (None, "", "-"):
|
||||
cw = src.get("chargeable_weight_kg") or q.get("chargeableWeightKg")
|
||||
amount = ""
|
||||
price = str(unit_price or "").strip()
|
||||
cw_text = str(cw or "").strip()
|
||||
if price and cw_text:
|
||||
try:
|
||||
amount = str(
|
||||
(float(str(price).replace(",", "")) * float(cw_text.replace(",", "")))
|
||||
)
|
||||
# 去掉多余尾零:3334.0
|
||||
amount = f"{float(amount):.10g}"
|
||||
except ValueError:
|
||||
amount = ""
|
||||
if not amount:
|
||||
amount = str(sale or "").strip()
|
||||
if not amount or amount == "-":
|
||||
return None
|
||||
if (not price or price == "-") and cw_text:
|
||||
try:
|
||||
price = f"{(float(str(amount).replace(',', '')) / float(cw_text.replace(',', ''))):.10g}"
|
||||
except ValueError:
|
||||
price = amount
|
||||
ccy = str(src.get("currency") or q.get("currency") or "").strip()
|
||||
row: dict[str, Any] = {
|
||||
"name": "空运费",
|
||||
"amount": amount,
|
||||
"project": "空运段",
|
||||
}
|
||||
if ccy:
|
||||
row["currency"] = ccy
|
||||
if price and price != "-":
|
||||
row["unit_price"] = price
|
||||
row["unitPrice"] = price
|
||||
if cw_text:
|
||||
row["quantity"] = cw_text
|
||||
row["unit"] = "KG"
|
||||
return row
|
||||
|
||||
|
||||
def option_fee_dicts(
|
||||
option: dict[str, Any] | None,
|
||||
quote: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
该线路费用明细(保留单价/单位/数量),供群摘要与投影写回 fee_lines。
|
||||
|
||||
顺序与主账一致:空运段 feeItems → 起运港 departureCharges → 目的地。
|
||||
舱位 feeItems 为空时补整单顶层,避免只剩增点费/拆板费丢掉空运费(WO202610150047)。
|
||||
"""
|
||||
src = dict(option or {})
|
||||
q = dict(quote or {})
|
||||
cabin = _fee_item_dicts(src.get("feeItems") or src.get("fee_items"))
|
||||
if not cabin:
|
||||
cabin = _top_fee_items_for_option(q, src)
|
||||
dep = _fee_item_dicts(
|
||||
src.get("departureCharges") or src.get("departure_charges") or src.get("charges")
|
||||
)
|
||||
dest = _fee_item_dicts(
|
||||
src.get("destinationCharges")
|
||||
or src.get("destination_charges")
|
||||
or src.get("arrivalCharges")
|
||||
)
|
||||
rows: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for bucket in (cabin, dep, dest):
|
||||
for row in bucket:
|
||||
key = str(row.get("name") or "").strip().lower()
|
||||
if not key or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
rows.append(row)
|
||||
# 仍无空运费:先取主账 fee_lines,再按舱位总价注入(OMS 仅增点/拆板时)
|
||||
if rows and not _has_air_freight_name(rows):
|
||||
for row in _air_freight_from_quote_fee_lines(q):
|
||||
key = str(row.get("name") or "").strip().lower()
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
rows.insert(0, row)
|
||||
if not _has_air_freight_name(rows) and _only_origin_surcharges(rows):
|
||||
extra = _inject_air_freight_from_option(src, q)
|
||||
if extra:
|
||||
rows.insert(0, extra)
|
||||
if rows:
|
||||
return rows
|
||||
q = dict(quote or {})
|
||||
options = parse_air_options(q)
|
||||
if len(options) > 1:
|
||||
return []
|
||||
return _rows_from_list(q.get("fee_lines") or q.get("fee_rows") or q.get("feeItems"))
|
||||
return _fee_item_dicts(q.get("fee_lines") or q.get("fee_rows") or q.get("feeItems"))
|
||||
|
||||
|
||||
def option_fee_lines(option: dict[str, Any] | None, quote: dict[str, Any] | None = None) -> list[tuple[str, str]]:
|
||||
"""
|
||||
该线路 TMS 回了哪些费用就列哪些(名称 + 展示金额)。
|
||||
|
||||
明细字典见 option_fee_dicts:舱位缺空运段时会补整单顶层 feeItems。
|
||||
"""
|
||||
out: list[tuple[str, str]] = []
|
||||
for row in option_fee_dicts(option, quote):
|
||||
pair = _fee_row(row)
|
||||
if pair:
|
||||
out.append(pair)
|
||||
return out
|
||||
|
||||
|
||||
def option_total(option: dict[str, Any] | None, quote: dict[str, Any] | None = None) -> str:
|
||||
@@ -393,10 +591,13 @@ def apply_selected_option(facts: dict[str, str] | None, option: dict[str, Any] |
|
||||
工单还没有报价日期时,把这条的航班日期写回去,供报价单和锁舱使用。
|
||||
销售已经说了日期则不覆盖。
|
||||
"""
|
||||
from agent.schema.field_validate import is_blank_quote_date
|
||||
|
||||
merged = dict(facts or {})
|
||||
src = dict(option or {})
|
||||
flight_date = option_flight_date(src)
|
||||
if flight_date and not str(merged.get("报价日期") or "").strip():
|
||||
# 占位文案「(非必填):」当没填,必须让航班日期盖上去。
|
||||
if flight_date and is_blank_quote_date(merged.get("报价日期")):
|
||||
merged["报价日期"] = flight_date
|
||||
code = str(src.get("originAirportCode") or src.get("origin") or "").strip().upper()
|
||||
name = _place_name(code)
|
||||
@@ -417,7 +618,33 @@ def project_selected_quote(quote: dict[str, Any] | None, option: dict[str, Any]
|
||||
"""把选中线路投影成价格卡用的 quote。"""
|
||||
base = dict(quote or {})
|
||||
src = dict(option or {})
|
||||
lines = [{"name": n, "amount": a} for n, a in option_fee_lines(src, base)]
|
||||
# 保留单价/单位/数量,避免后面把「空运费=总价」误判成重复兜底删掉。
|
||||
lines: list[dict[str, Any]] = []
|
||||
for row in option_fee_dicts(src, base):
|
||||
pair = _fee_row(row)
|
||||
if not pair:
|
||||
continue
|
||||
name, amount = pair
|
||||
line = {
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"currency": str(row.get("currency") or row.get("currencyCode") or "").strip(),
|
||||
}
|
||||
unit = str(row.get("unit") or row.get("unitName") or row.get("chargeUnit") or "").strip()
|
||||
if unit:
|
||||
line["unit"] = unit
|
||||
unit_price = row.get("unitPrice")
|
||||
if unit_price in (None, ""):
|
||||
unit_price = row.get("unit_price")
|
||||
if unit_price not in (None, ""):
|
||||
line["unit_price"] = unit_price
|
||||
line["unitPrice"] = unit_price
|
||||
qty = row.get("quantity")
|
||||
if qty in (None, ""):
|
||||
qty = row.get("chargeQuantity") or row.get("charge_quantity")
|
||||
if qty not in (None, ""):
|
||||
line["quantity"] = qty
|
||||
lines.append(line)
|
||||
# 必须覆盖整单 fee_lines:空列表也要写回去,否则会残留别的线路空运费。
|
||||
base["fee_lines"] = lines
|
||||
if not lines:
|
||||
@@ -515,6 +742,24 @@ def _same_amount(left: Any, right: Any) -> bool:
|
||||
return a == b
|
||||
|
||||
|
||||
def _rated_air_freight_line(item: dict[str, Any] | None) -> bool:
|
||||
"""
|
||||
是否为带计价单位的真实空运费行(单价×计费重),而非「空运费=总价」快照兜底。
|
||||
|
||||
有 KG 单位、或有单价/数量,都按真实费率行保留。
|
||||
"""
|
||||
src = dict(item or {})
|
||||
unit = str(src.get("unit") or src.get("unitName") or src.get("chargeUnit") or "").strip().upper()
|
||||
unit = unit.replace("/", "").replace("每", "").replace(" ", "")
|
||||
if unit in {"KG", "KGS", "公斤", "千克"}:
|
||||
return True
|
||||
if src.get("unitPrice") not in (None, "", "-") or src.get("unit_price") not in (None, "", "-"):
|
||||
return True
|
||||
if src.get("quantity") not in (None, "", "-") or src.get("chargeQuantity") not in (None, "", "-"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def drop_air_freight_total_alias(quote: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""
|
||||
已经有其它费用分项时,去掉「空运费=总价」这一行兜底。
|
||||
@@ -542,7 +787,8 @@ def drop_air_freight_total_alias(quote: dict[str, Any] | None) -> dict[str, Any]
|
||||
continue
|
||||
name = str(item.get("name") or item.get("label") or item.get("feeName") or "").strip()
|
||||
amount = item.get("amount") or item.get("value")
|
||||
if name == "空运费" and _same_amount(amount, total):
|
||||
# OMS 空运费金额常等于顶层 salePrice,但带 KG 单价/数量,不是「总价兜底」假行。
|
||||
if name == "空运费" and _same_amount(amount, total) and not _rated_air_freight_line(item):
|
||||
alias.append(item)
|
||||
continue
|
||||
others.append(item)
|
||||
|
||||
Reference in New Issue
Block a user