完善空运锁舱与海运报价单单选回填,并增强模板预览与协同字段盖写。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -226,6 +226,8 @@ def send_air_brief_and_sheet(
|
||||
空运群再发一版摘要 + 匹配到的空运模板。对齐海运 send_brief_and_sheet。
|
||||
|
||||
必须先发出工单信息,模板等摘要发出后再发,禁止后台线程抢先发文件。
|
||||
填表报价必须带上 TMS 总销售价:群票上的 latest quote 常只有增点费/拆板费明细,
|
||||
若直接拿去出单,预估费用会落成明细加总(如 200)而不是 salePrice(如 7918.25)。
|
||||
"""
|
||||
ticket = hydrate_ticket(flow, ticket)
|
||||
view = ticket_view(ticket)
|
||||
@@ -247,6 +249,8 @@ def send_air_brief_and_sheet(
|
||||
if not manual and copy.quote_is_product(override or view_q):
|
||||
manual = override or view_q
|
||||
quote = dict(override or view_q or tms or manual)
|
||||
# 出单专用:把 TMS salePrice 盖回薄报价,避免预估费用=明细 SUM。
|
||||
sheet_quote = _sheet_quote_with_tms_sale_total(quote, tms=tms, facts=facts)
|
||||
sales_name, airline_names, mention_ids = resolve_air_mentions(flow, view)
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
@@ -264,10 +268,133 @@ def send_air_brief_and_sheet(
|
||||
),
|
||||
mention_userids=mention_ids,
|
||||
)
|
||||
_send_air_activate_sheet(flow=flow, ticket=view, chat_id=room, quote=quote)
|
||||
_send_air_activate_sheet(flow=flow, ticket=view, chat_id=room, quote=sheet_quote)
|
||||
return "brief_and_sheet"
|
||||
|
||||
|
||||
def _air_usable_sale_total(quote: Optional[dict[str, Any]]) -> str:
|
||||
"""
|
||||
空运出单可用的总销售价:顶层 total / salePrice,或选定/首条线路 salePrice。
|
||||
「-」与空串不算。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
for key in ("total", "total_amount", "salePrice", "totalSalePrice"):
|
||||
val = str(src.get(key) or "").strip()
|
||||
if val and val != "-":
|
||||
return val
|
||||
selected = src.get("selected_air_option") or src.get("selectedAirOption")
|
||||
if isinstance(selected, dict):
|
||||
val = str(selected.get("salePrice") or selected.get("sale_price") or "").strip()
|
||||
if val and val != "-":
|
||||
return val
|
||||
raw_opts = src.get("airOptions") or src.get("air_options") or []
|
||||
if isinstance(raw_opts, list) and raw_opts and isinstance(raw_opts[0], dict):
|
||||
val = str(raw_opts[0].get("salePrice") or raw_opts[0].get("sale_price") or "").strip()
|
||||
if val and val != "-":
|
||||
return val
|
||||
return ""
|
||||
|
||||
|
||||
def _money_plain_number(text: str) -> Optional[float]:
|
||||
"""从「CNY 7918.25」类文本抽出数字;解不开返回 None。"""
|
||||
raw = str(text or "").strip()
|
||||
if not raw or raw == "-":
|
||||
return None
|
||||
digits = "".join(ch if (ch.isdigit() or ch in ".-") else " " for ch in raw).split()
|
||||
if not digits:
|
||||
return None
|
||||
try:
|
||||
return float(digits[-1].replace(",", ""))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _total_matches_fee_row_sum(quote: dict[str, Any], total_text: str) -> bool:
|
||||
"""
|
||||
顶层 total 是否只是明细金额相加(如增点费100+拆板费100=200)。
|
||||
是则说明缺了 TMS 总销售价,不能当真预估费用。
|
||||
"""
|
||||
total_num = _money_plain_number(total_text)
|
||||
if total_num is None:
|
||||
return False
|
||||
rows = quote.get("fee_rows") or quote.get("fee_lines") or quote.get("feeItems") or []
|
||||
if isinstance(rows, str) and rows.strip():
|
||||
try:
|
||||
rows = json.loads(rows)
|
||||
except json.JSONDecodeError:
|
||||
rows = []
|
||||
if not isinstance(rows, list) or not rows:
|
||||
return False
|
||||
summed = 0.0
|
||||
hit = 0
|
||||
for item in rows:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
amt = _money_plain_number(
|
||||
str(
|
||||
item.get("amount")
|
||||
or item.get("value")
|
||||
or item.get("salePrice")
|
||||
or item.get("price")
|
||||
or ""
|
||||
)
|
||||
)
|
||||
if amt is None:
|
||||
continue
|
||||
summed += amt
|
||||
hit += 1
|
||||
if hit <= 0:
|
||||
return False
|
||||
return abs(total_num - summed) < 0.01
|
||||
|
||||
|
||||
def _sheet_quote_with_tms_sale_total(
|
||||
quote: dict[str, Any],
|
||||
*,
|
||||
tms: Optional[dict[str, Any]] = None,
|
||||
facts: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
群填表用的报价包:保留产品/航线改过的明细,但预估费用必须能落到 TMS 总销售价。
|
||||
|
||||
协同群 latest quote 常只有 departureCharges 投影出的附加费行,total 空或被加成 200;
|
||||
销售 1V1 会话里带了 salePrice,所以同一工单两边会不一致。这里出单前对齐。
|
||||
"""
|
||||
from agent.schema.air_route_options import project_group_air_quote
|
||||
|
||||
out = dict(quote or {})
|
||||
source = dict(tms or {}) or dict(out)
|
||||
if not source:
|
||||
return out
|
||||
projected = project_group_air_quote(source, dict(facts or {}))
|
||||
tms_total = _air_usable_sale_total(projected)
|
||||
cur_total = _air_usable_sale_total(out)
|
||||
# 补齐线路对象 / 计费重,便于主账从 airOptions.salePrice 再兜底一次。
|
||||
for key in (
|
||||
"airOptions",
|
||||
"air_options",
|
||||
"selected_air_option",
|
||||
"selectedAirOption",
|
||||
"chargeableWeightKg",
|
||||
"chargeable_weight_kg",
|
||||
"currency",
|
||||
"valid_until",
|
||||
):
|
||||
cur = out.get(key)
|
||||
empty = cur in (None, "", "-", [], {})
|
||||
proj_val = projected.get(key)
|
||||
if empty and proj_val not in (None, "", "-", [], {}):
|
||||
out[key] = proj_val
|
||||
if not tms_total:
|
||||
return out
|
||||
need_tms_total = (not cur_total) or cur_total == "-" or _total_matches_fee_row_sum(out, cur_total)
|
||||
if need_tms_total:
|
||||
out["total"] = str(projected.get("total") or tms_total).strip()
|
||||
if projected.get("currency") and not str(out.get("currency") or "").strip():
|
||||
out["currency"] = projected.get("currency")
|
||||
return out
|
||||
|
||||
|
||||
def resolve_air_mentions(flow: Any, ticket: object) -> tuple[str, list[str], list[str]]:
|
||||
"""
|
||||
空运群摘要必须 @ 到本单销售和航线·空运,对上海运 @ 销售/@ 产品。
|
||||
@@ -581,16 +708,21 @@ def apply_air_confirm_tms(*, flow: Any, ticket: object, sender_id: str, chat_id:
|
||||
quote["source_label"] = "航线确认TMS"
|
||||
no = ticket_no(ticket)
|
||||
_clear_air_pending(flow, no, sender_id)
|
||||
saved = flow.ledger.upsert_product_quote(work_order_no=no, quote=quote, to_status="已报价") or {}
|
||||
# 确认 TMS 出单:同样用总销售价,避免只落附加费明细时预估费用=200。
|
||||
sheet_quote = _sheet_quote_with_tms_sale_total(
|
||||
quote, tms=quote, facts=dict(view.get("facts") or {})
|
||||
)
|
||||
saved = flow.ledger.upsert_product_quote(work_order_no=no, quote=sheet_quote, to_status="已报价") or {}
|
||||
if not saved.get("ok"):
|
||||
send_group_text(group_client_of(flow), chat_id, "主账写入报价失败。")
|
||||
return "ledger_fail"
|
||||
view["collab_chat_id"] = chat_id
|
||||
view["quote"] = sheet_quote
|
||||
_persist_quote_sheet(
|
||||
flow=flow,
|
||||
ticket=view,
|
||||
sess=None,
|
||||
quote=quote,
|
||||
quote=sheet_quote,
|
||||
sender_id=sender_id,
|
||||
business_line="AIR",
|
||||
)
|
||||
@@ -658,6 +790,7 @@ def apply_air_lock(
|
||||
chat_id: str,
|
||||
instruction: str,
|
||||
option_no: str = "",
|
||||
airline: str = "",
|
||||
) -> str:
|
||||
if not is_airline(flow, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_airline_only_cabin())
|
||||
@@ -705,6 +838,7 @@ def apply_air_lock(
|
||||
sender_id=sender_id,
|
||||
operator_name=name,
|
||||
instruction_text=instruction,
|
||||
airline=(airline or "").strip(),
|
||||
)
|
||||
or {},
|
||||
is_ok=lambda x: bool((x or {}).get("ok")),
|
||||
|
||||
@@ -2239,13 +2239,14 @@ def adjust_only_sales() -> str:
|
||||
|
||||
|
||||
def adjust_group_body(quote: dict[str, Any] | None, business_line: str = "") -> str:
|
||||
"""群里调后全量明细,利润点不写给销售。"""
|
||||
"""
|
||||
群里调后全量明细,利润点不写给销售。
|
||||
|
||||
海运必须带三段标题与小计(始发港/海运/目的港),禁止只摊平 fee_lines 把小计弄丢。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
header = str(src.get("currency") or "")
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header)
|
||||
lines.append(f"{_total_label(src, business_line)}:{total}")
|
||||
return "调价后价格:\n" + "\n".join(lines)
|
||||
text = quote_area_fee_text(src, business_line=business_line)
|
||||
return "调价后价格:\n" + text
|
||||
|
||||
|
||||
def adopt_excel_ack() -> str:
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
|
||||
主账填表只认英文码,货好时间 / 海关编码 / 含油电磁常空着。
|
||||
本文件只改 Excel 字节,不打主账、不占回调线程。
|
||||
|
||||
注意:openpyxl 保存会弄坏海运顶部 Form Control(drawing/VML/sheet1 锚点)。
|
||||
因此只用来「读」标签位置;真正写入走 zip 内定点改格,不动控件骨架。
|
||||
无待写字段、或目标格已是目标值时原样返回。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
@@ -30,6 +35,18 @@ _COLLAB_LABEL_MARKERS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("包装方式", ("包装方式", "包装类型")),
|
||||
)
|
||||
|
||||
# 旧路径兜底:若仍误走 openpyxl 保存,至少盖回这些部件
|
||||
_PRESERVE_ZIP_PARTS = (
|
||||
"xl/drawings/drawing1.xml",
|
||||
"xl/drawings/vmlDrawing1.vml",
|
||||
"xl/styles.xml",
|
||||
"xl/ctrlProps/ctrlProp1.xml",
|
||||
"xl/ctrlProps/ctrlProp2.xml",
|
||||
"xl/worksheets/sheet1.xml",
|
||||
"xl/worksheets/_rels/sheet1.xml.rels",
|
||||
"[Content_Types].xml",
|
||||
)
|
||||
|
||||
|
||||
def _fact_for_markers(facts: Mapping[str, Any], keys: tuple[str, ...]) -> str:
|
||||
for key in keys:
|
||||
@@ -39,6 +56,102 @@ def _fact_for_markers(facts: Mapping[str, Any], keys: tuple[str, ...]) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _restore_sea_draw_parts(original: bytes, rewritten: bytes) -> bytes:
|
||||
"""把原件里的 drawing/VML/ctrl/styles/sheet1 控件骨架盖回 openpyxl 重写后的 xlsx。"""
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(original)) as zin:
|
||||
keep = {
|
||||
name: zin.read(name)
|
||||
for name in _PRESERVE_ZIP_PARTS
|
||||
if name in zin.namelist()
|
||||
}
|
||||
if not keep:
|
||||
return rewritten
|
||||
out = io.BytesIO()
|
||||
with zipfile.ZipFile(io.BytesIO(rewritten)) as zin, zipfile.ZipFile(
|
||||
out, "w", compression=zipfile.ZIP_DEFLATED
|
||||
) as zout:
|
||||
for info in zin.infolist():
|
||||
data = keep.get(info.filename) or zin.read(info.filename)
|
||||
zout.writestr(info, data)
|
||||
for name, data in keep.items():
|
||||
if name not in zin.namelist():
|
||||
zout.writestr(name, data)
|
||||
return out.getvalue()
|
||||
except Exception:
|
||||
logger.exception("overlay 盖回海运 drawing 失败,退回 openpyxl 结果")
|
||||
return rewritten
|
||||
|
||||
|
||||
def _col_letter(col: int) -> str:
|
||||
"""1-based 列号 → Excel 列字母。"""
|
||||
n = int(col)
|
||||
letters: list[str] = []
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
letters.append(chr(65 + rem))
|
||||
return "".join(reversed(letters)) or "A"
|
||||
|
||||
|
||||
def _xml_escape(text: str) -> str:
|
||||
return (
|
||||
str(text)
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace('"', """)
|
||||
)
|
||||
|
||||
|
||||
def _patch_cells_keep_structure(data: bytes, cells: list[tuple[int, int, str]]) -> bytes:
|
||||
"""
|
||||
只改 sheet1 里指定格的字符串,保留 AlternateContent / 控件关系。
|
||||
|
||||
禁止整表经 openpyxl 重存:否则拉群协同 Excel 顶部 Port/其它单选会空白,
|
||||
而「直接生成」不经本函数所以看起来正常(WO031 vs WO033)。
|
||||
"""
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
if not cells:
|
||||
return data
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as zin:
|
||||
if "xl/worksheets/sheet1.xml" not in zin.namelist():
|
||||
return data
|
||||
sheet = zin.read("xl/worksheets/sheet1.xml").decode("utf-8", "replace")
|
||||
others = {name: zin.read(name) for name in zin.namelist() if name != "xl/worksheets/sheet1.xml"}
|
||||
|
||||
for row, col, val in cells:
|
||||
ref = f"{_col_letter(col)}{int(row)}"
|
||||
cell_xml = (
|
||||
f'<c r="{ref}" t="inlineStr"><is><t>{_xml_escape(val)}</t></is></c>'
|
||||
)
|
||||
pat = re.compile(rf'<c\b[^>]*\br="{re.escape(ref)}"[^>]*(?:/>|>[\s\S]*?</c>)')
|
||||
if pat.search(sheet):
|
||||
sheet = pat.sub(cell_xml, sheet, count=1)
|
||||
continue
|
||||
row_pat = re.compile(rf'(<row\b[^>]*\br="{int(row)}"[^>]*>)([\s\S]*?)(</row>)')
|
||||
m = row_pat.search(sheet)
|
||||
if m:
|
||||
sheet = sheet[: m.start(3)] + cell_xml + sheet[m.start(3) :]
|
||||
continue
|
||||
sheet = sheet.replace(
|
||||
"</sheetData>",
|
||||
f'<row r="{int(row)}">{cell_xml}</row></sheetData>',
|
||||
1,
|
||||
)
|
||||
|
||||
out = io.BytesIO()
|
||||
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
|
||||
for name, blob in others.items():
|
||||
zout.writestr(name, blob)
|
||||
zout.writestr("xl/worksheets/sheet1.xml", sheet.encode("utf-8"))
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def overlay_labeled_facts(
|
||||
*,
|
||||
facts: Mapping[str, Any],
|
||||
@@ -48,7 +161,8 @@ def overlay_labeled_facts(
|
||||
"""
|
||||
按模板标签再写协同格。失败则原样交回。
|
||||
|
||||
副作用:可能写临时 xlsx。
|
||||
副作用:可能写临时 xlsx。无待写字段、或格子已是目标值时不改字节,
|
||||
避免毁掉海运顶部运输分类单选。
|
||||
"""
|
||||
import base64
|
||||
import tempfile
|
||||
@@ -91,20 +205,30 @@ def overlay_labeled_facts(
|
||||
if not val:
|
||||
continue
|
||||
dest = _label_value_cell(ws, cell)
|
||||
# 主账已填过同值(如贸易条款):再写会经 openpyxl 毁掉单选
|
||||
if str(dest.value or "").strip() == val:
|
||||
used_rows.add(cell.row)
|
||||
break
|
||||
pending.append((dest.row, dest.column, val))
|
||||
used_rows.add(cell.row)
|
||||
break
|
||||
for r, c, val in pending:
|
||||
ws.cell(row=r, column=c, value=val)
|
||||
written += 1
|
||||
wb.save(tmp)
|
||||
wb.close()
|
||||
if not pending:
|
||||
# 无增量可写:原样返回,禁止重存
|
||||
logger.info("overlay 协同字段 written=0 keep_original")
|
||||
return {
|
||||
"filepath": filepath or str(tmp),
|
||||
"file_b64": file_b64 or base64.b64encode(raw).decode("ascii"),
|
||||
}
|
||||
patched = _patch_cells_keep_structure(raw, pending)
|
||||
tmp.write_bytes(patched)
|
||||
written = len(pending)
|
||||
except Exception:
|
||||
logger.exception("overlay 协同字段失败")
|
||||
return {"filepath": filepath, "file_b64": file_b64}
|
||||
|
||||
out_b64 = base64.b64encode(tmp.read_bytes()).decode("ascii")
|
||||
logger.info("overlay 协同字段 written=%s", written)
|
||||
logger.info("overlay 协同字段 written=%s keep_structure=1", written)
|
||||
return {"filepath": str(tmp), "file_b64": out_b64}
|
||||
|
||||
|
||||
|
||||
@@ -401,17 +401,21 @@ def hydrate_ticket(flow: Any, ticket: object) -> object:
|
||||
)
|
||||
)
|
||||
quote = dict(view.get("quote") or {})
|
||||
has_quote = bool(quote.get("total") or _quote_fee_rows(quote))
|
||||
# total="-" 在 Python 里也是 truthy,不能当成已有总价;只有附加费明细也不算完整。
|
||||
has_sale_total = _quote_has_usable_sale_total(quote)
|
||||
has_fee_rows = bool(_quote_fee_rows(quote))
|
||||
has_quote = has_sale_total or has_fee_rows
|
||||
has_status = bool(ticket_status(ticket))
|
||||
has_tms = bool(dict(view.get("tms_quote") or view.get("tmsQuote") or {}))
|
||||
# 调价后 latest quote 仍有价,但不能因此跳过补 TMS 原价。
|
||||
if has_facts and has_quote and has_status and has_tms:
|
||||
return ticket
|
||||
# 仅有 fee_rows、没有可用总价时仍要往下补全,否则空运出单预估费用会落成明细 SUM。
|
||||
if has_facts and has_sale_total and has_status and has_tms:
|
||||
return _soft_fill_quote_sale_fields(ticket)
|
||||
no = ticket_no(ticket)
|
||||
sales = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
|
||||
book = getattr(flow, "ledger", None)
|
||||
if book is None or not no:
|
||||
return ticket
|
||||
return _soft_fill_quote_sale_fields(ticket)
|
||||
full: Any = None
|
||||
getter = getattr(book, "get_for_agent", None)
|
||||
if callable(getter) and sales:
|
||||
@@ -423,13 +427,33 @@ def hydrate_ticket(flow: Any, ticket: object) -> object:
|
||||
if callable(getter):
|
||||
full = getter(work_order_no=no)
|
||||
if full is None:
|
||||
return ticket
|
||||
return _soft_fill_quote_sale_fields(ticket)
|
||||
merged = dict(view)
|
||||
full_view = ticket_view(full)
|
||||
if (not has_facts) and full_view.get("facts"):
|
||||
merged["facts"] = dict(full_view.get("facts") or {})
|
||||
if (not has_quote) and full_view.get("quote"):
|
||||
merged["quote"] = dict(full_view.get("quote") or {})
|
||||
if (not has_sale_total) and full_view.get("quote"):
|
||||
# 薄报价保留已有明细,补上完整快照里的总价/线路。
|
||||
rich = dict(full_view.get("quote") or {})
|
||||
cur = dict(quote)
|
||||
for key in (
|
||||
"total",
|
||||
"salePrice",
|
||||
"totalSalePrice",
|
||||
"airOptions",
|
||||
"air_options",
|
||||
"selected_air_option",
|
||||
"selectedAirOption",
|
||||
"chargeableWeightKg",
|
||||
"chargeable_weight_kg",
|
||||
"currency",
|
||||
"valid_until",
|
||||
):
|
||||
if cur.get(key) in (None, "", "-", [], {}) and rich.get(key) not in (None, "", "-", [], {}):
|
||||
cur[key] = rich[key]
|
||||
if not has_fee_rows and (rich.get("fee_lines") or rich.get("fee_rows")):
|
||||
cur["fee_lines"] = rich.get("fee_lines") or rich.get("fee_rows")
|
||||
merged["quote"] = cur
|
||||
if full_view.get("tms_quote") or full_view.get("tmsQuote"):
|
||||
merged["tms_quote"] = dict(full_view.get("tms_quote") or full_view.get("tmsQuote") or {})
|
||||
if (not has_status) and full_view.get("status"):
|
||||
@@ -449,6 +473,66 @@ def hydrate_ticket(flow: Any, ticket: object) -> object:
|
||||
):
|
||||
if full_view.get(key) and not merged.get(key):
|
||||
merged[key] = full_view.get(key)
|
||||
return _soft_fill_quote_sale_fields(merged)
|
||||
|
||||
|
||||
def _quote_has_usable_sale_total(quote: dict[str, Any] | None) -> bool:
|
||||
"""
|
||||
是否已有可用于出单预估费用的总价。
|
||||
total="-" 不算;仅有 fee_rows 也不算。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
total = str(src.get("total") or src.get("total_amount") or src.get("salePrice") or "").strip()
|
||||
if total and total != "-":
|
||||
return True
|
||||
selected = src.get("selected_air_option") or src.get("selectedAirOption")
|
||||
if isinstance(selected, dict):
|
||||
sale = str(selected.get("salePrice") or selected.get("sale_price") or "").strip()
|
||||
if sale and sale != "-":
|
||||
return True
|
||||
opts = src.get("airOptions") or src.get("air_options") or []
|
||||
if isinstance(opts, list) and opts and isinstance(opts[0], dict):
|
||||
sale = str(opts[0].get("salePrice") or opts[0].get("sale_price") or "").strip()
|
||||
if sale and sale != "-":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _soft_fill_quote_sale_fields(ticket: object) -> object:
|
||||
"""
|
||||
从 tms_quote 补齐当前 quote 缺失的总销售价 / 线路对象。
|
||||
群 findByCollabChat 常只有薄 latest quote,出单前必须能落到 salePrice。
|
||||
"""
|
||||
view = ticket_view(ticket)
|
||||
cur = dict(view.get("quote") or {})
|
||||
rich = dict(view.get("tms_quote") or view.get("tmsQuote") or {})
|
||||
if not rich:
|
||||
return ticket if isinstance(ticket, dict) else view
|
||||
changed = False
|
||||
for key in (
|
||||
"total",
|
||||
"salePrice",
|
||||
"totalSalePrice",
|
||||
"airOptions",
|
||||
"air_options",
|
||||
"selected_air_option",
|
||||
"selectedAirOption",
|
||||
"chargeableWeightKg",
|
||||
"chargeable_weight_kg",
|
||||
"currency",
|
||||
"valid_until",
|
||||
):
|
||||
if cur.get(key) in (None, "", "-", [], {}) and rich.get(key) not in (None, "", "-", [], {}):
|
||||
cur[key] = rich[key]
|
||||
changed = True
|
||||
if not changed:
|
||||
return ticket if isinstance(ticket, dict) else view
|
||||
if isinstance(ticket, dict):
|
||||
out = dict(ticket)
|
||||
out["quote"] = cur
|
||||
return out
|
||||
merged = dict(view)
|
||||
merged["quote"] = cur
|
||||
return merged
|
||||
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
from agent.schema.sea_options import drop_invented_door_trade_terms, harvest_sea_class
|
||||
|
||||
facts = harvest_sea_class(text, facts)
|
||||
# 旧陆运/补问草稿可能把「门到门」塞进贸易条款;原话没写条款则丢掉。
|
||||
# 贸易条款标签行原样优先(Port to Port 等);再丢掉无标签的冒充「门到门」
|
||||
facts = drop_invented_door_trade_terms(text, facts)
|
||||
check = validate_required_fields(facts=facts, business_line="SEA")
|
||||
facts = dict(check["facts"])
|
||||
|
||||
Reference in New Issue
Block a user