机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-19 09:41:59 +08:00
co-authored by Cursor
parent 277cdc1c28
commit 2b528758b4
119 changed files with 6760 additions and 3516 deletions
+258 -12
View File
@@ -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)