329 lines
13 KiB
Python
329 lines
13 KiB
Python
"""
|
|
空运线路报价:一条直接展示,多条要选;航线带中转;费用取该线路回包。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
if ROOT not in sys.path:
|
|
sys.path.insert(0, ROOT)
|
|
|
|
from agent.ledger.memory_ledger import MemoryLedger
|
|
from agent.policy import inquiry_copy as copy
|
|
from agent.policy.air_text_flow import AirTextInquiryFlow
|
|
from agent.channel.h5.air_route_page import render_air_route_page
|
|
from agent.schema.air_route_options import (
|
|
apply_selected_option,
|
|
needs_route_pick,
|
|
option_fee_lines,
|
|
parse_air_options,
|
|
project_group_air_quote,
|
|
project_selected_quote,
|
|
route_codes,
|
|
route_line,
|
|
)
|
|
|
|
|
|
class AirRouteOptionsTests(unittest.TestCase):
|
|
def test_single_with_transfer_line(self) -> None:
|
|
quote = {
|
|
"airOptions": [
|
|
{
|
|
"originAirportCode": "CAN",
|
|
"transferAirportCode": "SHA",
|
|
"destinationAirportCode": "LAX",
|
|
"salePrice": 110,
|
|
"currency": "CNY",
|
|
"departureCharges": [
|
|
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 110}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
opts = parse_air_options(quote)
|
|
self.assertEqual(len(opts), 1)
|
|
self.assertFalse(needs_route_pick(quote))
|
|
self.assertEqual(route_line(opts[0], {}), "广州-上海-洛杉矶")
|
|
self.assertEqual(route_codes(opts[0], {}), "CAN-SHA-LAX")
|
|
fees = option_fee_lines(opts[0], quote)
|
|
self.assertEqual(fees[0][0], "运输费")
|
|
self.assertIn("110", fees[0][1])
|
|
|
|
def test_multi_needs_pick(self) -> None:
|
|
quote = {
|
|
"isMulti": "2",
|
|
"airOptions": [
|
|
{"originAirportCode": "ZUH", "transferAirportCode": "COC", "destinationAirportCode": "CRK", "salePrice": 183370},
|
|
{"originAirportCode": "ZUH", "transferAirportCode": "ABC", "destinationAirportCode": "CRK", "salePrice": 193370},
|
|
],
|
|
}
|
|
self.assertTrue(needs_route_pick(quote))
|
|
self.assertEqual(len(parse_air_options(quote)), 2)
|
|
|
|
def test_select_writes_origin(self) -> None:
|
|
option = {"originAirportCode": "SZX", "destinationAirportCode": "KUL"}
|
|
facts = apply_selected_option({"目的港": "吉隆坡"}, option)
|
|
self.assertEqual(facts["起运港"], "深圳")
|
|
self.assertEqual(facts["originCode"], "SZX")
|
|
|
|
def test_multi_quote_opens_h5_then_select_writes_origin(self) -> None:
|
|
flow = AirTextInquiryFlow(MemoryLedger())
|
|
replies: list[str] = []
|
|
extras: list[dict] = []
|
|
|
|
def reply(text: str, extra=None) -> None:
|
|
replies.append(text)
|
|
extras.append(dict(extra or {}))
|
|
|
|
facts = {
|
|
"目的港": "克拉克",
|
|
"品名": "茶叶",
|
|
"件数": "10",
|
|
"毛重": "100",
|
|
"体积": "2",
|
|
"包装方式": "托盘",
|
|
"tms_multi": "1",
|
|
}
|
|
phase = flow.on_text(
|
|
sender_id="u_multi",
|
|
text="空运",
|
|
reply=reply,
|
|
injected_facts=facts,
|
|
injected_mode="AIR",
|
|
)
|
|
self.assertEqual(phase, "wait_confirm")
|
|
phase = flow.on_text(sender_id="u_multi", text="确定", reply=reply)
|
|
self.assertEqual(phase, "wait_air_route")
|
|
self.assertTrue(any("已查到" in x and "条线路" in x for x in replies))
|
|
phase = flow.select_air_route(sender_id="u_multi", index=1, reply=reply)
|
|
self.assertEqual(phase, "wait_collab")
|
|
sess = flow.session_of("u_multi")
|
|
self.assertEqual(sess.facts.get("起运港"), "深圳")
|
|
self.assertIn("SZX-CRK", "\n".join(replies[-2:]))
|
|
|
|
def test_reselect_after_adopt_voids_old_file(self) -> None:
|
|
"""采用后再改选:旧报价单作废,回到方案卡重新采用。"""
|
|
ledger = MemoryLedger()
|
|
flow = AirTextInquiryFlow(ledger)
|
|
replies: list[str] = []
|
|
|
|
def reply(text: str, extra=None) -> None:
|
|
replies.append(text)
|
|
_ = extra
|
|
|
|
facts = {
|
|
"目的港": "克拉克",
|
|
"品名": "茶叶",
|
|
"件数": "10",
|
|
"毛重": "100",
|
|
"体积": "2",
|
|
"包装方式": "托盘",
|
|
"tms_multi": "1",
|
|
}
|
|
phase = flow.on_text(
|
|
sender_id="u_void",
|
|
text="空运",
|
|
reply=reply,
|
|
injected_facts=facts,
|
|
injected_mode="AIR",
|
|
)
|
|
self.assertEqual(phase, "wait_confirm")
|
|
self.assertEqual(
|
|
flow.on_text(sender_id="u_void", text="确定", reply=reply),
|
|
"wait_air_route",
|
|
)
|
|
self.assertEqual(flow.select_air_route(sender_id="u_void", index=0, reply=reply), "wait_collab")
|
|
self.assertEqual(
|
|
flow.on_button(sender_id="u_void", action=copy.BTN_SKIP_COLLAB, reply=reply),
|
|
"wait_adopt",
|
|
)
|
|
self.assertEqual(
|
|
flow.on_button(sender_id="u_void", action=copy.BTN_ADOPT_EXCEL, reply=reply),
|
|
"wait_deal",
|
|
)
|
|
wo = flow.session_of("u_void").work_order_no
|
|
self.assertTrue(ledger.get_ticket(work_order_no=wo).quote_file_ready)
|
|
phase = flow.select_air_route(sender_id="u_void", index=1, reply=reply)
|
|
self.assertEqual(phase, "wait_adopt")
|
|
self.assertFalse(ledger.get_ticket(work_order_no=wo).quote_file_ready)
|
|
view = ledger.get_for_agent(work_order_no=wo, sender_id="u_void")
|
|
self.assertEqual(view.get("wait_phase"), "wait_adopt")
|
|
self.assertEqual(flow.session_of("u_void").facts.get("起运港"), "深圳")
|
|
self.assertTrue(any("报价方案" in x or "生成Excel" in x for x in replies[-3:]))
|
|
|
|
def test_empty_route_fees_do_not_borrow_other_quote_lines(self) -> None:
|
|
"""TMS 该线路费用列表为空时,禁止套整单或其他线路的空运费。"""
|
|
quote = {
|
|
"total": "183370.00",
|
|
"freight": "183370.00",
|
|
"currency": "CNY",
|
|
"fee_lines": [
|
|
{
|
|
"name": "空运费",
|
|
"amount": "183370.00",
|
|
"currency": "CNY",
|
|
"unit_price": "11.00",
|
|
}
|
|
],
|
|
"airOptions": [
|
|
{
|
|
"originAirportCode": "ZUH",
|
|
"transferAirportCode": "CHG",
|
|
"destinationAirportCode": "CRK",
|
|
"salePrice": 183370,
|
|
"currency": "CNY",
|
|
"departureCharges": [
|
|
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 11},
|
|
{"chargeName": "提送货费", "currency": "CNY", "unitPrice": 6},
|
|
{"chargeName": "人工费", "currency": "CNY", "unitPrice": 123},
|
|
],
|
|
},
|
|
{
|
|
"originAirportCode": "SZX",
|
|
"transferAirportCode": "",
|
|
"destinationAirportCode": "CRK",
|
|
"salePrice": 200040,
|
|
"currency": "CNY",
|
|
"departureCharges": [],
|
|
},
|
|
],
|
|
}
|
|
opts = parse_air_options(quote)
|
|
zuh = option_fee_lines(opts[0], quote)
|
|
self.assertEqual([name for name, _ in zuh], ["运输费", "提送货费", "人工费"])
|
|
szx = option_fee_lines(opts[1], quote)
|
|
self.assertEqual(szx, [])
|
|
html = render_air_route_page(
|
|
{"work_order_no": "WO202609220009", "facts": {"目的港": "克拉克"}, "quote": quote}
|
|
)
|
|
self.assertIn("运输费", html)
|
|
self.assertIn("深圳-克拉克", html)
|
|
self.assertNotIn("空运费", html)
|
|
self.assertIn("200040", html)
|
|
self.assertIn("本线路未返回费用明细", html)
|
|
self.assertIn("直达", html)
|
|
self.assertIn("中转", html)
|
|
projected = project_selected_quote(quote, opts[1])
|
|
self.assertEqual(projected.get("fee_lines"), [])
|
|
self.assertIn("200040", str(projected.get("total") or ""))
|
|
self.assertEqual(copy.quote_fee_lines(projected), [])
|
|
detail = copy.sea_quote_detail_text(
|
|
work_order_no="WO202609220009",
|
|
quote=projected,
|
|
business_line="AIR",
|
|
)
|
|
self.assertNotIn("空运费", detail)
|
|
self.assertIn("总销售价", detail)
|
|
self.assertIn("200040", detail)
|
|
|
|
def test_group_air_quote_projects_route_fees_and_drops_total_alias(self) -> None:
|
|
"""群摘要投影:该线路分项留下,空运费=总价丢掉。"""
|
|
quote = {
|
|
"total": "183370.0",
|
|
"freight": "183370.0",
|
|
"currency": "CNY",
|
|
"fee_lines": [
|
|
{"name": "空运费", "amount": "183370.00", "currency": "CNY"},
|
|
],
|
|
"airOptions": [
|
|
{
|
|
"originAirportCode": "ZUH",
|
|
"transferAirportCode": "CHG",
|
|
"destinationAirportCode": "CRK",
|
|
"salePrice": 183370,
|
|
"currency": "CNY",
|
|
"departureCharges": [
|
|
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 11.0},
|
|
{"chargeName": "提送货费", "currency": "CNY", "unitPrice": 6.0},
|
|
{"chargeName": "人工费", "currency": "CNY", "unitPrice": 123.0},
|
|
],
|
|
}
|
|
],
|
|
}
|
|
projected = project_group_air_quote(quote, {"originCode": "ZUH"})
|
|
names = [str(x.get("name") or "") for x in (projected.get("fee_lines") or [])]
|
|
self.assertEqual(names, ["运输费", "提送货费", "人工费"])
|
|
self.assertNotIn("空运费", names)
|
|
self.assertEqual(copy.quote_fee_lines(projected)[0][0], "运输费")
|
|
|
|
def test_air_detail_text_hides_eta_keeps_valid_until(self) -> None:
|
|
"""空运没有时效;价格明细只留报价有效期。"""
|
|
text = copy.sea_quote_detail_text(
|
|
work_order_no="WO202609220013",
|
|
quote={
|
|
"source_label": "TMS标准报价",
|
|
"selected_air_option": {
|
|
"originAirportCode": "NKG",
|
|
"destinationAirportCode": "KUL",
|
|
},
|
|
"fee_lines": [
|
|
{"name": "增点费", "amount": "100.0", "currency": "CNY"},
|
|
{"name": "拆板费", "amount": "100.0", "currency": "USD"},
|
|
],
|
|
"total": "5001.0",
|
|
"currency": "CNY",
|
|
"eta": "3天",
|
|
"valid_until": "2026-09-30",
|
|
},
|
|
business_line="AIR",
|
|
)
|
|
self.assertEqual(
|
|
text,
|
|
"\n".join(
|
|
[
|
|
"TMS标准报价",
|
|
"WO202609220013价格明细",
|
|
"NKG-KUL",
|
|
"增点费:CNY 100.0",
|
|
"拆板费:USD 100.0",
|
|
"总销售价:CNY 5001.0",
|
|
"报价有效期:2026-09-30",
|
|
]
|
|
),
|
|
)
|
|
self.assertNotIn("时效", text)
|
|
|
|
def test_h5_page_lists_route_and_fees(self) -> None:
|
|
from agent.channel.h5.air_route_page import render_air_route_page
|
|
from agent.channel.h5.air_route_store import build_air_route_url
|
|
|
|
html = render_air_route_page(
|
|
{
|
|
"work_order_no": "WO202609220001",
|
|
"facts": {"目的港": "克拉克"},
|
|
"quote": {
|
|
"airOptions": [
|
|
{
|
|
"originAirportCode": "ZUH",
|
|
"transferAirportCode": "CAN",
|
|
"destinationAirportCode": "CRK",
|
|
"salePrice": 183370,
|
|
"currency": "CNY",
|
|
"departureCharges": [
|
|
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 180000}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
}
|
|
)
|
|
self.assertIn("珠海-广州-克拉克", html)
|
|
self.assertIn("运输费", html)
|
|
self.assertIn("总销售价", html)
|
|
self.assertIn("class='opt'", html)
|
|
self.assertIn("name='index'", html)
|
|
self.assertNotIn("userid", html.lower())
|
|
self.assertNotIn("sender_id", html)
|
|
url = build_air_route_url("tok-abc")
|
|
self.assertIn("/air-routes/tok-abc", url)
|
|
self.assertNotIn("userid", url)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|