空运成交后可在本群继续锁舱;调价同步舱位费用桶;出单按空运段骨架落费并保留陆运车型尺寸;导出文件名乱码修复。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
空运协同群业务:激活当前工单、补含电含磁、航线报价/附件、锁舱/释放、成交跟进。
|
||||
空运协同群业务:激活当前工单、补含电含磁、航线报价/附件、成交后再锁舱、成交后仍可取消舱位。
|
||||
|
||||
本文件职责:群里副作用(主账 + BOT 回话)。海运应用群仍走 sea_group_ops。
|
||||
禁止:自动建群/拉人/解散;拿当前工单去猜锁舱工单号。
|
||||
@@ -39,6 +39,7 @@ from agent.policy.sea_group_ops import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TERMINAL = {"已成交", "未成交", "已关闭", "转人工"}
|
||||
OPEN_ACTIVATE = {"询价中", "已报价", "协商中"}
|
||||
AIRLINE_ROLE = "air"
|
||||
@@ -535,12 +536,13 @@ def resolve_cabin_ticket(
|
||||
chat_id: str,
|
||||
work_order_no: str,
|
||||
allow_terminal: bool,
|
||||
for_lock: bool = False,
|
||||
) -> tuple[Optional[object], str]:
|
||||
"""
|
||||
锁舱/释放按句中工单号取单,不猜当前单。
|
||||
|
||||
未结案:激活并切到本群。已在别的群:occupied,不锁不切。
|
||||
已成交/未成交:不能再激活,但释放仍要读到这张单(成交拦住、未成交放舱)。
|
||||
已成交可以锁、也可以取消已锁舱位,不再激活。未成交不能锁,但已有锁仍可取消。
|
||||
"""
|
||||
book = flow.ledger
|
||||
no = (work_order_no or "").strip()
|
||||
@@ -561,13 +563,18 @@ def resolve_cabin_ticket(
|
||||
send_group_text(client, chat_id, copy.air_ticket_occupied(no))
|
||||
return None, "occupied"
|
||||
if is_terminal(ticket):
|
||||
st = ticket_status(ticket)
|
||||
# 锁舱只在已成交之后办。未成交要能读到单,才能回「不能锁」,而不是「不能再激活」。
|
||||
if for_lock and st in {"已成交", "未成交"}:
|
||||
# 已成交不再走普通激活,但要把本群记下,下一句补舱位不用再带工单号。
|
||||
if st == "已成交":
|
||||
_bind_deal_cabin_room(flow, chat_id=chat_id, work_order_no=no)
|
||||
return ticket, "deal_loaded" if st == "已成交" else "lost_loaded"
|
||||
if allow_terminal:
|
||||
return ticket, "terminal_loaded"
|
||||
if ticket_status(ticket) == "转人工":
|
||||
if st == "转人工":
|
||||
return None, "handoff_ignore"
|
||||
send_group_text(
|
||||
client, chat_id, copy.air_ticket_closed(no, ticket_status(ticket))
|
||||
)
|
||||
send_group_text(client, chat_id, copy.air_ticket_closed(no, st))
|
||||
return None, "ticket_closed"
|
||||
return activate_ticket(flow=flow, chat_id=chat_id, work_order_no=no, speak=False)
|
||||
|
||||
@@ -869,9 +876,91 @@ def apply_air_deal(
|
||||
followup_hours=result.get("negotiate_hours"),
|
||||
),
|
||||
)
|
||||
# 共用回执仍是「已记录为已成交」。催锁只加在空运群,海运陆运不走这里。
|
||||
if outcome == "已成交" and phase == "done":
|
||||
prompt_airline_to_lock(flow=flow, ticket=ticket, chat_id=chat_id, work_order_no=no)
|
||||
return phase
|
||||
|
||||
|
||||
def _bind_deal_cabin_room(flow: Any, *, chat_id: str, work_order_no: str) -> None:
|
||||
"""已成交锁舱时把工单留在本群。普通激活仍拒绝已成交。失败只记日志,不挡这一句锁舱。"""
|
||||
book = getattr(flow, "ledger", None)
|
||||
activator = getattr(book, "activate_air_group", None) if book is not None else None
|
||||
if not callable(activator):
|
||||
return
|
||||
try:
|
||||
activator(chat_id=chat_id, work_order_no=work_order_no, allow_deal=True)
|
||||
except TypeError:
|
||||
logger.warning("主账激活不接受已成交绑群 wo=%s", work_order_no)
|
||||
except Exception:
|
||||
logger.exception("已成交锁舱绑群失败 wo=%s", work_order_no)
|
||||
|
||||
|
||||
def prompt_airline_to_lock(
|
||||
*,
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
chat_id: str,
|
||||
work_order_no: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
空运单记成已成交后,在协同群点名航线锁舱。
|
||||
|
||||
只 @ 岗位为航线·空运的人,不改成交回执。没有群、或多段/海运/陆运,不发。
|
||||
群消息在调用方线程发出(群回调或私聊成交后的同一轮)。
|
||||
"""
|
||||
view = ticket_view(ticket)
|
||||
no = (work_order_no or ticket_no(ticket)).strip()
|
||||
room = (chat_id or str(view.get("collab_chat_id") or view.get("collabChatId") or "")).strip()
|
||||
line = str(view.get("business_line") or view.get("businessLine") or "AIR").strip().upper()
|
||||
if not no or not room or line != "AIR":
|
||||
return
|
||||
_sales_name, airline_names, mention_ids = resolve_air_mentions(flow, ticket)
|
||||
sales_id = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
|
||||
air_ids = [uid for uid in mention_ids if uid and uid != sales_id]
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
room,
|
||||
copy.air_lock_ask_airline(work_order_no=no, airline_names=airline_names),
|
||||
mention_userids=air_ids,
|
||||
)
|
||||
|
||||
|
||||
def _block_lock_until_deal(flow: Any, ticket: object, chat_id: str) -> str:
|
||||
"""
|
||||
未成交的空运单不许锁。已成交返回空串,调用方继续锁。
|
||||
|
||||
询价中/已报价:点名本单销售。协商中、未成交、已关闭各说各的。
|
||||
转人工沿用静默。不打 TMS。
|
||||
"""
|
||||
st = ticket_status(ticket)
|
||||
if st == "已成交":
|
||||
return ""
|
||||
no = ticket_no(ticket)
|
||||
client = group_client_of(flow)
|
||||
if st == "未成交":
|
||||
send_group_text(client, chat_id, copy.air_lock_blocked_lost(no))
|
||||
return "lock_lost"
|
||||
if st == "协商中":
|
||||
send_group_text(client, chat_id, copy.air_lock_blocked_negotiating(no))
|
||||
return "lock_negotiating"
|
||||
if st == "转人工":
|
||||
return "handoff_ignore"
|
||||
if st == "已关闭":
|
||||
send_group_text(client, chat_id, copy.air_ticket_closed(no, st))
|
||||
return "ticket_closed"
|
||||
sales_name, _airline_names, _mention_ids = resolve_air_mentions(flow, ticket)
|
||||
view = ticket_view(ticket)
|
||||
sales_id = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
|
||||
send_group_text(
|
||||
client,
|
||||
chat_id,
|
||||
copy.air_lock_need_deal(sales_name=sales_name),
|
||||
mention_userids=[sales_id] if sales_id else None,
|
||||
)
|
||||
return "lock_need_deal"
|
||||
|
||||
|
||||
def apply_air_lock(
|
||||
*,
|
||||
flow: Any,
|
||||
@@ -884,15 +973,19 @@ def apply_air_lock(
|
||||
flight_no: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
航线锁舱。
|
||||
航线锁舱。只有工单已是「已成交」才打主账。
|
||||
|
||||
还没回复成交:点名销售先确认。协商中、未成交各用一句,不催销售改口。
|
||||
舱位编号、航司、航班号没齐时只请补全,不调 TMS 锁舱,不标系统异常。
|
||||
三项齐了(话里写的优先,报价里已有的可以补上)才打主账锁舱。
|
||||
上次锁舱失败标过暂停的,航线再锁仍要打 TMS。
|
||||
上次锁舱失败标过暂停的,航线再锁仍要打 TMS。锁成功不再推成交跟进。
|
||||
"""
|
||||
if not is_airline(flow, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_airline_only_cabin())
|
||||
return "not_airline"
|
||||
blocked = _block_lock_until_deal(flow, ticket, chat_id)
|
||||
if blocked:
|
||||
return blocked
|
||||
no = ticket_no(ticket)
|
||||
name = operator_name(flow, sender_id)
|
||||
view_quote = dict(ticket_view(ticket).get("quote") or {})
|
||||
@@ -950,12 +1043,15 @@ def apply_air_lock(
|
||||
is_ok=lambda x: bool((x or {}).get("ok")),
|
||||
)
|
||||
if not out.get("ok"):
|
||||
if str(out.get("error") or "") == "no_quote":
|
||||
err_code = str(out.get("error") or "")
|
||||
if err_code == "no_quote":
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_lock_need_quote(no))
|
||||
return "no_quote"
|
||||
# 主账再拦一遍成交状态:只回业务句,不当 TMS 故障。
|
||||
if err_code in {"lock_need_deal", "lock_negotiating", "lock_lost", "ticket_closed"}:
|
||||
return _block_lock_until_deal(flow, ticket, chat_id) or err_code
|
||||
# 主账在出站前发现三项不齐:只请补,不当 TMS 故障,不通知 IT。
|
||||
reason_text = str(out.get("failReason") or "")
|
||||
err_code = str(out.get("error") or "")
|
||||
if err_code == "lock_params_incomplete" or "锁舱参数不完整" in reason_text:
|
||||
labels = missing or _labels_from_incomplete_reason(reason_text)
|
||||
_remember_partial_lock(
|
||||
@@ -1065,9 +1161,6 @@ def apply_air_lock(
|
||||
|
||||
if ticket_is_paused(ticket):
|
||||
clear_system_exception(ledger=flow.ledger, work_order_no=no)
|
||||
view = ticket_view(ticket)
|
||||
view["collab_chat_id"] = chat_id
|
||||
emit_group_deal(flow=flow, ticket=view, quote=dict(view.get("quote") or {}))
|
||||
return "lock_ok"
|
||||
|
||||
def _release_should_push_deal(ticket: object) -> bool:
|
||||
@@ -1091,11 +1184,8 @@ def apply_air_release(
|
||||
if not is_airline(flow, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_airline_only_cabin())
|
||||
return "not_airline"
|
||||
# 释放同理:暂停单也允许再打 TMS,回话用释放失败原因而不是接口异常兜底。
|
||||
# 释放同理:暂停单、已成交单也允许再打 TMS。还没锁过的仍拒绝。
|
||||
no = ticket_no(ticket)
|
||||
if ticket_status(ticket) == "已成交":
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_release_deal_blocked(no))
|
||||
return "release_deal_blocked"
|
||||
facts = dict(ticket_view(ticket).get("collab_facts") or {})
|
||||
lock_id = str(facts.get(_LOCK_ID) or "").strip()
|
||||
name = operator_name(flow, sender_id)
|
||||
|
||||
Reference in New Issue
Block a user