空运已有 TMS 选定舱位时直接锁舱,不再追问三项;纯文字报价提示只留给附件报价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -968,24 +968,12 @@ def apply_air_lock(
|
|||||||
)
|
)
|
||||||
send_group_text(group_client_of(flow), chat_id, copy.air_lock_need_params(labels))
|
send_group_text(group_client_of(flow), chat_id, copy.air_lock_need_params(labels))
|
||||||
return "wait_lock_params"
|
return "wait_lock_params"
|
||||||
# 附件/手工报价、运价库无舱位:引导纯文字报价,不算系统异常、不通知 IT。
|
# 仅「航线附件报价」引导纯文字价。有 TMS / 文字手工价失败不能误进这条
|
||||||
|
# (旧逻辑用「无匹配报价」关键字,会把 TMS 锁舱失败也拐进文字报价提示)。
|
||||||
src_label = str((quote or {}).get("source_label") or (quote or {}).get("source") or "")
|
src_label = str((quote or {}).get("source_label") or (quote or {}).get("source") or "")
|
||||||
|
is_attach_quote = "航线附件报价" in src_label
|
||||||
need_text_quote = (
|
need_text_quote = (
|
||||||
err_code in {"lock_incomplete", "lock_fail"}
|
is_attach_quote and err_code in {"lock_incomplete", "lock_fail"}
|
||||||
and (
|
|
||||||
"航线附件报价" in src_label
|
|
||||||
or "附件" in src_label
|
|
||||||
or any(
|
|
||||||
x in reason_text
|
|
||||||
for x in (
|
|
||||||
"手工报价",
|
|
||||||
"无匹配舱位",
|
|
||||||
"无匹配报价",
|
|
||||||
"运价库无匹配",
|
|
||||||
"标准运价库无匹配",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if need_text_quote:
|
if need_text_quote:
|
||||||
send_group_text(
|
send_group_text(
|
||||||
@@ -1555,6 +1543,8 @@ def _resolve_lock_params(
|
|||||||
拼出锁舱三项。
|
拼出锁舱三项。
|
||||||
|
|
||||||
这一句里写的优先;没写的用上一句暂存的;再没有才用报价上已有的舱位。
|
这一句里写的优先;没写的用上一句暂存的;再没有才用报价上已有的舱位。
|
||||||
|
TMS 已有舱位明细时(selected_air_option / airOptions):句中未点名也自动带出,
|
||||||
|
禁止再追问舱位编号/航司/航班号(WO014:航线确认TMS 后直接「锁舱」仍被补问)。
|
||||||
航司写成南航、东方航空这类名字时归一成二字码;归一不了也算已经补充,不再追问。
|
航司写成南航、东方航空这类名字时归一成二字码;归一不了也算已经补充,不再追问。
|
||||||
"""
|
"""
|
||||||
said_opt, said_air, said_flight = lock_params_from_text(instruction)
|
said_opt, said_air, said_flight = lock_params_from_text(instruction)
|
||||||
@@ -1562,16 +1552,18 @@ def _resolve_lock_params(
|
|||||||
air = _normalize_airline(airline or said_air or str(facts.get(_PENDING_LOCK_AIRLINE) or ""))
|
air = _normalize_airline(airline or said_air or str(facts.get(_PENDING_LOCK_AIRLINE) or ""))
|
||||||
flight = _normalize_flight(flight_no or said_flight or str(facts.get(_PENDING_LOCK_FLIGHT) or ""))
|
flight = _normalize_flight(flight_no or said_flight or str(facts.get(_PENDING_LOCK_FLIGHT) or ""))
|
||||||
chosen = _option_row(quote, option)
|
chosen = _option_row(quote, option)
|
||||||
single_auto = False
|
|
||||||
if chosen is None and not option:
|
if chosen is None and not option:
|
||||||
options = _air_options(quote)
|
chosen = _default_cabin_row(quote)
|
||||||
if len(options) == 1:
|
if chosen is not None:
|
||||||
chosen = options[0]
|
|
||||||
option = str(chosen.get("optionNo") or chosen.get("option_no") or "").strip()
|
option = str(chosen.get("optionNo") or chosen.get("option_no") or "").strip()
|
||||||
single_auto = True
|
|
||||||
if not _airline_ready(air) and chosen is not None:
|
if not _airline_ready(air) and chosen is not None:
|
||||||
air = _normalize_airline(
|
air = _normalize_airline(
|
||||||
str(chosen.get("airlineCode") or chosen.get("airline") or "")
|
str(
|
||||||
|
chosen.get("airlineCode")
|
||||||
|
or chosen.get("airline")
|
||||||
|
or chosen.get("airlineName")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if not flight and chosen is not None:
|
if not flight and chosen is not None:
|
||||||
flight = _normalize_flight(str(chosen.get("flightNo") or chosen.get("flight_no") or ""))
|
flight = _normalize_flight(str(chosen.get("flightNo") or chosen.get("flight_no") or ""))
|
||||||
@@ -1584,6 +1576,51 @@ def _resolve_lock_params(
|
|||||||
return option, air, flight
|
return option, air, flight
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_cabin_row(quote: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||||
|
"""报价上已选定的舱位(JSON 字符串或 dict)。"""
|
||||||
|
selected = (quote or {}).get("selected_air_option") or (quote or {}).get("selectedAirOption")
|
||||||
|
if isinstance(selected, str) and selected.strip().startswith("{"):
|
||||||
|
try:
|
||||||
|
selected = json.loads(selected)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
selected = None
|
||||||
|
if isinstance(selected, dict) and (
|
||||||
|
selected.get("optionNo") or selected.get("option_no") or selected.get("flightNo")
|
||||||
|
):
|
||||||
|
return selected
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _default_cabin_row(quote: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
句中未点名舱位时的默认行。
|
||||||
|
|
||||||
|
优先 selected_air_option;否则单条 airOptions;TMS 多条时取第一条,
|
||||||
|
避免「已有 TMS 舱位仍追问三项」(WO014)。
|
||||||
|
"""
|
||||||
|
selected = _selected_cabin_row(quote)
|
||||||
|
if selected is not None:
|
||||||
|
return selected
|
||||||
|
options = _air_options(quote)
|
||||||
|
if len(options) == 1:
|
||||||
|
return options[0]
|
||||||
|
if len(options) > 1 and _quote_has_tms_cabin(quote):
|
||||||
|
return options[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _quote_has_tms_cabin(quote: dict[str, Any]) -> bool:
|
||||||
|
"""本单报价里已有可引用的 TMS 舱位明细。"""
|
||||||
|
src = dict(quote or {})
|
||||||
|
qid = str(src.get("quoteId") or src.get("quote_id") or "").strip()
|
||||||
|
if qid and not qid.upper().startswith("QT-"):
|
||||||
|
return bool(_air_options(src) or _selected_cabin_row(src))
|
||||||
|
label = str(src.get("source_label") or src.get("source") or "").upper()
|
||||||
|
if "TMS" in label:
|
||||||
|
return bool(_air_options(src) or _selected_cabin_row(src))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any]]:
|
def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any]]:
|
||||||
"""按舱位编号找报价里的那一条;对不上返回空。"""
|
"""按舱位编号找报价里的那一条;对不上返回空。"""
|
||||||
want = (option_no or "").strip().upper()
|
want = (option_no or "").strip().upper()
|
||||||
@@ -1593,6 +1630,11 @@ def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any
|
|||||||
no = str(row.get("optionNo") or row.get("option_no") or "").strip().upper()
|
no = str(row.get("optionNo") or row.get("option_no") or "").strip().upper()
|
||||||
if no == want:
|
if no == want:
|
||||||
return row
|
return row
|
||||||
|
selected = _selected_cabin_row(quote)
|
||||||
|
if selected is not None:
|
||||||
|
no = str(selected.get("optionNo") or selected.get("option_no") or "").strip().upper()
|
||||||
|
if no == want:
|
||||||
|
return selected
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3347,24 +3347,11 @@ def air_lock_incomplete(*, work_order_no: str, operator: str, reason: str = "")
|
|||||||
"""
|
"""
|
||||||
锁舱参数不齐(纯手工价、运价库无舱位等):业务补问,不算系统异常,不惊动 IT。
|
锁舱参数不齐(纯手工价、运价库无舱位等):业务补问,不算系统异常,不惊动 IT。
|
||||||
|
|
||||||
附件/无舱位场景改引导纯文字报价;有 TMS 舱位时仍可补 AIR-OPT 三项。
|
有 TMS / 非附件报价:仍补舱位三项。附件报价的「请发纯文字」只由
|
||||||
|
air_group_ops 在 source_label=航线附件报价 时走 air_lock_need_text_quote,
|
||||||
|
禁止按「无匹配报价」关键字在这里拐过去(会误伤 TMS 锁舱)。
|
||||||
"""
|
"""
|
||||||
why = (reason or "").strip()
|
why = (reason or "").strip()
|
||||||
blob = why
|
|
||||||
if any(
|
|
||||||
x in blob
|
|
||||||
for x in (
|
|
||||||
"手工报价",
|
|
||||||
"附件",
|
|
||||||
"无匹配舱位",
|
|
||||||
"无匹配报价",
|
|
||||||
"运价库无匹配",
|
|
||||||
"标准运价库无匹配",
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return air_lock_need_text_quote(
|
|
||||||
work_order_no=work_order_no, operator=operator, reason=why
|
|
||||||
)
|
|
||||||
lines = [
|
lines = [
|
||||||
f"工单{work_order_no} 还不能锁舱。",
|
f"工单{work_order_no} 还不能锁舱。",
|
||||||
f"操作人:{operator}",
|
f"操作人:{operator}",
|
||||||
|
|||||||
@@ -1110,8 +1110,57 @@ class AirGroupCollabTests(unittest.TestCase):
|
|||||||
self.assertEqual(phase, "lock_ok")
|
self.assertEqual(phase, "lock_ok")
|
||||||
self.assertTrue(any("执行成功" in t for t in self._texts()))
|
self.assertTrue(any("执行成功" in t for t in self._texts()))
|
||||||
|
|
||||||
|
def test_tms_quote_with_selected_cabin_locks_without_asking_params(self) -> None:
|
||||||
|
"""TMS 已有 selected_air_option:直接锁,不追问舱位/航司/航班(WO014)。"""
|
||||||
|
wo = self._create_air(
|
||||||
|
quote={
|
||||||
|
"total": "183370",
|
||||||
|
"source": "TMS",
|
||||||
|
"source_label": "航线确认TMS",
|
||||||
|
"quoteId": "AIR-2094721692181016578-2094721692701110273",
|
||||||
|
"selected_air_option": {
|
||||||
|
"optionNo": "AIR-OPT01",
|
||||||
|
"airlineCode": "JG",
|
||||||
|
"flightNo": "JG2719",
|
||||||
|
},
|
||||||
|
"airOptions": [
|
||||||
|
{
|
||||||
|
"optionNo": "AIR-OPT01",
|
||||||
|
"airlineCode": "JG",
|
||||||
|
"flightNo": "JG2719",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionNo": "AIR-OPT02",
|
||||||
|
"airlineCode": "RW",
|
||||||
|
"flightNo": "RW633",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
status="已报价",
|
||||||
|
)
|
||||||
|
calls: list[dict] = []
|
||||||
|
orig = self.ledger.lock_cabin
|
||||||
|
|
||||||
|
def spy(**kwargs):
|
||||||
|
calls.append(kwargs)
|
||||||
|
return orig(**kwargs)
|
||||||
|
|
||||||
|
self.ledger.lock_cabin = spy # type: ignore
|
||||||
|
phase = handle_air_group(
|
||||||
|
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_tms_auto", mid="tmsauto1"),
|
||||||
|
flow=self.flow,
|
||||||
|
ledger=self.ledger,
|
||||||
|
injected_intent="air_lock",
|
||||||
|
)
|
||||||
|
self.assertEqual(phase, "lock_ok")
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertEqual(calls[0].get("option_no"), "AIR-OPT01")
|
||||||
|
self.assertEqual(calls[0].get("airline"), "JG")
|
||||||
|
self.assertEqual(calls[0].get("flight_no"), "JG2719")
|
||||||
|
self.assertFalse(any("锁舱参数不完整" in t for t in self._texts()))
|
||||||
|
|
||||||
def test_lock_missing_params_asks_and_does_not_call_tms(self) -> None:
|
def test_lock_missing_params_asks_and_does_not_call_tms(self) -> None:
|
||||||
"""缺舱位、航司、航班号时只请补,不锁舱、不报 TMS 失败、不通知 IT。"""
|
"""无舱位明细时缺三项才请补,不锁舱、不报 TMS 失败、不通知 IT。"""
|
||||||
wo = self._create_air(
|
wo = self._create_air(
|
||||||
quote={
|
quote={
|
||||||
"total": "CNY 1800",
|
"total": "CNY 1800",
|
||||||
|
|||||||
Reference in New Issue
Block a user