Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92bdf56a4d |
@@ -968,24 +968,12 @@ def apply_air_lock(
|
||||
)
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_lock_need_params(labels))
|
||||
return "wait_lock_params"
|
||||
# 附件/手工报价、运价库无舱位:引导纯文字报价,不算系统异常、不通知 IT。
|
||||
# 仅「航线附件报价」引导纯文字价。有 TMS / 文字手工价失败不能误进这条
|
||||
# (旧逻辑用「无匹配报价」关键字,会把 TMS 锁舱失败也拐进文字报价提示)。
|
||||
src_label = str((quote or {}).get("source_label") or (quote or {}).get("source") or "")
|
||||
is_attach_quote = "航线附件报价" in src_label
|
||||
need_text_quote = (
|
||||
err_code in {"lock_incomplete", "lock_fail"}
|
||||
and (
|
||||
"航线附件报价" in src_label
|
||||
or "附件" in src_label
|
||||
or any(
|
||||
x in reason_text
|
||||
for x in (
|
||||
"手工报价",
|
||||
"无匹配舱位",
|
||||
"无匹配报价",
|
||||
"运价库无匹配",
|
||||
"标准运价库无匹配",
|
||||
)
|
||||
)
|
||||
)
|
||||
is_attach_quote and err_code in {"lock_incomplete", "lock_fail"}
|
||||
)
|
||||
if need_text_quote:
|
||||
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)
|
||||
@@ -1562,16 +1552,18 @@ def _resolve_lock_params(
|
||||
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 ""))
|
||||
chosen = _option_row(quote, option)
|
||||
single_auto = False
|
||||
if chosen is None and not option:
|
||||
options = _air_options(quote)
|
||||
if len(options) == 1:
|
||||
chosen = options[0]
|
||||
chosen = _default_cabin_row(quote)
|
||||
if chosen is not None:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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]]:
|
||||
"""按舱位编号找报价里的那一条;对不上返回空。"""
|
||||
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()
|
||||
if no == want:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -3347,24 +3347,11 @@ def air_lock_incomplete(*, work_order_no: str, operator: str, reason: str = "")
|
||||
"""
|
||||
锁舱参数不齐(纯手工价、运价库无舱位等):业务补问,不算系统异常,不惊动 IT。
|
||||
|
||||
附件/无舱位场景改引导纯文字报价;有 TMS 舱位时仍可补 AIR-OPT 三项。
|
||||
有 TMS / 非附件报价:仍补舱位三项。附件报价的「请发纯文字」只由
|
||||
air_group_ops 在 source_label=航线附件报价 时走 air_lock_need_text_quote,
|
||||
禁止按「无匹配报价」关键字在这里拐过去(会误伤 TMS 锁舱)。
|
||||
"""
|
||||
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 = [
|
||||
f"工单{work_order_no} 还不能锁舱。",
|
||||
f"操作人:{operator}",
|
||||
|
||||
@@ -1110,8 +1110,57 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
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:
|
||||
"""缺舱位、航司、航班号时只请补,不锁舱、不报 TMS 失败、不通知 IT。"""
|
||||
"""无舱位明细时缺三项才请补,不锁舱、不报 TMS 失败、不通知 IT。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
|
||||
Reference in New Issue
Block a user