员工岗位可重新启用;调价认空运航线;会话存档跳号补拉;陆运报价按车型列回填
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,7 +13,6 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from agent.llm.extract_text import extract_air_collab_fields, extract_product_quote
|
||||
@@ -71,6 +70,60 @@ def _ticket_flight_date(ticket: object) -> str:
|
||||
).strip()
|
||||
|
||||
|
||||
def _quote_date_from_lock_instruction(instruction: str) -> str:
|
||||
"""
|
||||
锁舱补参句里的报价日期。
|
||||
|
||||
先认「报价日期:…」标签,再机读句末 yyyy-MM-dd(产品示例:
|
||||
AIR-OPT01,南航,航班CZ327,2026-10-18)。不改主账。
|
||||
"""
|
||||
from agent.llm.extract_text import labeled_quote_date
|
||||
from agent.schema.field_validate import _harvest_quote_date
|
||||
|
||||
labeled = labeled_quote_date(instruction or "")
|
||||
if labeled:
|
||||
return labeled
|
||||
return _harvest_quote_date(instruction or "")
|
||||
|
||||
|
||||
def _absorb_lock_quote_date(
|
||||
*,
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
instruction: str,
|
||||
work_order_no: str,
|
||||
) -> tuple[object, str, str]:
|
||||
"""
|
||||
工单报价日空、锁舱句里又写了日期时,先写入主账再锁。
|
||||
|
||||
返回:(最新 ticket, 生效报价日, 本句抽出的日期)。
|
||||
写失败不挡缺项提示;生效日仍可用本句日期做本地齐套判断。
|
||||
"""
|
||||
from agent.schema.field_validate import is_blank_quote_date
|
||||
|
||||
current = _ticket_flight_date(ticket)
|
||||
said = _quote_date_from_lock_instruction(instruction)
|
||||
if not said:
|
||||
return ticket, current, ""
|
||||
if not is_blank_quote_date(current):
|
||||
return ticket, current, said
|
||||
writer = getattr(flow.ledger, "update_facts", None)
|
||||
write_ok = False
|
||||
if callable(writer):
|
||||
try:
|
||||
out = writer(work_order_no=work_order_no, facts={"报价日期": said}) or {}
|
||||
write_ok = bool(out.get("ok", True))
|
||||
except Exception:
|
||||
logger.exception("锁舱句写报价日期失败 wo=%s", work_order_no)
|
||||
write_ok = False
|
||||
if write_ok:
|
||||
getter = getattr(flow.ledger, "get_ticket", None)
|
||||
reloaded = getter(work_order_no=work_order_no) if callable(getter) else None
|
||||
ticket2 = hydrate_ticket(flow, reloaded or ticket)
|
||||
return ticket2, _ticket_flight_date(ticket2) or said, said
|
||||
return ticket, said, said
|
||||
|
||||
|
||||
def _lock_receipt_fields(
|
||||
ticket: object,
|
||||
quote: dict[str, Any],
|
||||
@@ -451,6 +504,20 @@ def _total_matches_fee_row_sum(quote: dict[str, Any], total_text: str) -> bool:
|
||||
return abs(total_num - summed) < 0.01
|
||||
|
||||
|
||||
def _total_is_thin_vs_tms(cur_total: str, tms_total: str) -> bool:
|
||||
"""
|
||||
薄报价:明细加总明显小于 TMS 销售价(WO040:200 << 7918)才盖成销售价。
|
||||
|
||||
航线在舱位价上再加增值费/增点费时,total=明细之和会大于 salePrice(如 4966 > 3966),
|
||||
此时必须保留航线总价,禁止再盖回 TMS。
|
||||
"""
|
||||
cur = _money_plain_number(cur_total)
|
||||
tms = _money_plain_number(tms_total)
|
||||
if cur is None or tms is None:
|
||||
return False
|
||||
return cur + 0.01 < tms
|
||||
|
||||
|
||||
def _sheet_quote_with_tms_sale_total(
|
||||
quote: dict[str, Any],
|
||||
*,
|
||||
@@ -490,7 +557,10 @@ def _sheet_quote_with_tms_sale_total(
|
||||
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)
|
||||
# 仅「空/薄报价加总」才盖 TMS;航线加出增值费后 total>salePrice 必须保留。
|
||||
matches_sum = bool(cur_total) and cur_total != "-" and _total_matches_fee_row_sum(out, cur_total)
|
||||
thin_vs_tms = matches_sum and _total_is_thin_vs_tms(cur_total, tms_total)
|
||||
need_tms_total = (not cur_total) or cur_total == "-" or thin_vs_tms
|
||||
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():
|
||||
@@ -500,9 +570,13 @@ def _sheet_quote_with_tms_sale_total(
|
||||
|
||||
def resolve_air_mentions(flow: Any, ticket: object) -> tuple[str, list[str], list[str]]:
|
||||
"""
|
||||
空运群摘要必须 @ 到本单销售和航线·空运,对上海运 @ 销售/@ 产品。
|
||||
空运群摘要 @ 本单销售 + 负责本单线路的航线·空运(按后台 routes 过滤)。
|
||||
对上海运 @ 销售/@ 产品;禁止再按岗位整表广播。
|
||||
"""
|
||||
from agent.policy.deal_followup import air_staff_route_matches
|
||||
|
||||
view = ticket_view(ticket)
|
||||
facts = dict(view.get("facts") or {})
|
||||
sales_id = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
|
||||
sales_name = str(view.get("sales_name") or view.get("salesName") or "").strip()
|
||||
if not sales_name or sales_name == "销售":
|
||||
@@ -519,7 +593,25 @@ def resolve_air_mentions(flow: Any, ticket: object) -> tuple[str, list[str], lis
|
||||
rows = list(lister(role_code="air") or [])
|
||||
except Exception:
|
||||
logger.exception("列航线岗失败")
|
||||
matched_rows: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if air_staff_route_matches(routes=str(row.get("routes") or ""), facts=facts):
|
||||
matched_rows.append(row)
|
||||
# 对不上具体线路时,只保留「空运全线」,避免再 @ 全岗
|
||||
if not matched_rows:
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
toks = [
|
||||
t.strip()
|
||||
for t in str(row.get("routes") or "").replace(",", ",").replace("、", ",").split(",")
|
||||
if t.strip()
|
||||
]
|
||||
if any(t in {"空运全线", "全线"} for t in toks):
|
||||
matched_rows.append(row)
|
||||
for row in matched_rows:
|
||||
uid = str(row.get("wecomId") or row.get("wecom_id") or "").strip()
|
||||
name = str(row.get("name") or "").strip() or _staff_display_name(flow, uid)
|
||||
if not uid:
|
||||
@@ -1062,6 +1154,13 @@ def apply_air_lock(
|
||||
if blocked:
|
||||
return blocked
|
||||
no = ticket_no(ticket)
|
||||
# 补参句带日期时先写入,避免舱位三项齐了仍反复要报价日期。
|
||||
ticket, quote_date, _said_quote_date = _absorb_lock_quote_date(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
instruction=instruction,
|
||||
work_order_no=no,
|
||||
)
|
||||
name = operator_name(flow, sender_id)
|
||||
view_quote = dict(ticket_view(ticket).get("quote") or {})
|
||||
air_quote = dict((view_quote.get("segment_quotes") or {}).get("AIR") or {})
|
||||
@@ -1079,7 +1178,9 @@ def apply_air_lock(
|
||||
origin, destination, airline_label, flight_date = _lock_receipt_fields(
|
||||
ticket, quote, picked, air_code
|
||||
)
|
||||
missing = _missing_lock_labels(picked, air_code, flight)
|
||||
if not quote_date:
|
||||
quote_date = _ticket_flight_date(ticket)
|
||||
missing = _missing_lock_labels(picked, air_code, flight, quote_date=quote_date)
|
||||
if missing and _quote_ready_for_lock(quote):
|
||||
_remember_partial_lock(
|
||||
flow,
|
||||
@@ -1812,8 +1913,20 @@ def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any
|
||||
return None
|
||||
|
||||
|
||||
def _missing_lock_labels(option_no: str, airline: str, flight_no: str) -> list[str]:
|
||||
"""缺的中文名,顺序固定:舱位编号、航司、航班号。"""
|
||||
def _missing_lock_labels(
|
||||
option_no: str,
|
||||
airline: str,
|
||||
flight_no: str,
|
||||
*,
|
||||
quote_date: str = "",
|
||||
) -> list[str]:
|
||||
"""
|
||||
缺的中文名,顺序固定:舱位编号、航司、航班号、报价日期。
|
||||
|
||||
报价日期空也要进缺项,避免先只补舱位三项、再被主账单独拦一次。
|
||||
"""
|
||||
from agent.schema.field_validate import is_blank_quote_date
|
||||
|
||||
missing: list[str] = []
|
||||
if not (option_no or "").strip():
|
||||
missing.append("舱位编号")
|
||||
@@ -1821,12 +1934,18 @@ def _missing_lock_labels(option_no: str, airline: str, flight_no: str) -> list[s
|
||||
missing.append("航司")
|
||||
if not _FLIGHT_CODE_RE.match((flight_no or "").strip().upper()):
|
||||
missing.append("航班号")
|
||||
if is_blank_quote_date(quote_date):
|
||||
missing.append("报价日期")
|
||||
return missing
|
||||
|
||||
|
||||
def _labels_from_incomplete_reason(reason: str) -> list[str]:
|
||||
"""主账回了「缺少:…」时,按中文名收回缺项,丢掉英文字段名。"""
|
||||
found = [label for label in ("舱位编号", "航司", "航班号") if label in (reason or "")]
|
||||
found = [
|
||||
label
|
||||
for label in ("舱位编号", "航司", "航班号", "报价日期")
|
||||
if label in (reason or "")
|
||||
]
|
||||
return found or ["舱位编号", "航司", "航班号"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user