陆运多条线路按段展示 TMS 费用名和时效,选定后再写回该条成本。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -171,6 +171,129 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertNotIn("当前工单:", text)
|
||||
self.assertNotIn("贸易条款", text)
|
||||
|
||||
def test_air_group_brief_tms_total_only_once(self) -> None:
|
||||
"""群摘要 TMS 段总销售价只一行,禁止重复。"""
|
||||
tms = {
|
||||
"source": "TMS",
|
||||
"source_label": "TMS标准报价",
|
||||
"total": "183370.0",
|
||||
"currency": "CNY",
|
||||
"eta": "-",
|
||||
"valid_until": "2026-09-30",
|
||||
"fee_lines": [
|
||||
{"name": "运输费", "amount": "11.0", "currency": "CNY"},
|
||||
],
|
||||
}
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202609220018",
|
||||
facts={"起运港": "珠海", "目的港": "克拉克"},
|
||||
quote=tms,
|
||||
collab_facts={},
|
||||
has_price=True,
|
||||
tms_quote=tms,
|
||||
)
|
||||
tms_block = text.split("TMS报价:", 1)[1]
|
||||
self.assertEqual(tms_block.count("总销售价:"), 1)
|
||||
self.assertIn("总销售价:CNY 183370.0", tms_block)
|
||||
self.assertNotIn("时效", tms_block)
|
||||
self.assertIn("报价有效期:2026-09-30", tms_block)
|
||||
|
||||
def test_air_group_brief_uses_route_fees_not_air_freight_total(self) -> None:
|
||||
"""
|
||||
群摘要必须跟单聊同一套线路分项。
|
||||
主账快照若只有「空运费=总价」,仍要列出该线路运输费/提送货费/人工费。
|
||||
"""
|
||||
tms = {
|
||||
"source": "TMS",
|
||||
"source_label": "TMS标准报价",
|
||||
"total": "183370.0",
|
||||
"freight": "183370.0",
|
||||
"currency": "CNY",
|
||||
"eta": "-",
|
||||
"valid_until": "2026-09-30",
|
||||
"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},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202609220018",
|
||||
facts={"起运港": "珠海", "目的港": "克拉克", "originCode": "ZUH"},
|
||||
quote=tms,
|
||||
collab_facts={},
|
||||
has_price=True,
|
||||
tms_quote=tms,
|
||||
)
|
||||
tms_block = text.split("TMS报价:", 1)[1]
|
||||
self.assertIn("运输费:CNY 11.0", tms_block)
|
||||
self.assertIn("提送货费:CNY 6.0", tms_block)
|
||||
self.assertIn("人工费:CNY 123.0", tms_block)
|
||||
self.assertIn("总销售价:CNY 183370", tms_block)
|
||||
self.assertEqual(tms_block.count("总销售价:"), 1)
|
||||
self.assertNotIn("空运费", tms_block)
|
||||
|
||||
def test_air_group_brief_multi_route_does_not_borrow_other_air_freight(self) -> None:
|
||||
"""多条线路:只出对上起运港那条的分项,禁止套整单空运费或另一条的运输费。"""
|
||||
tms = {
|
||||
"source": "TMS",
|
||||
"total": "200040",
|
||||
"freight": "183370.00",
|
||||
"currency": "CNY",
|
||||
"valid_until": "2026-09-30",
|
||||
"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},
|
||||
],
|
||||
},
|
||||
{
|
||||
"originAirportCode": "SZX",
|
||||
"destinationAirportCode": "CRK",
|
||||
"salePrice": 200040,
|
||||
"currency": "CNY",
|
||||
"departureCharges": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202609220018",
|
||||
facts={"起运港": "深圳", "目的港": "克拉克", "originCode": "SZX"},
|
||||
quote=tms,
|
||||
collab_facts={},
|
||||
has_price=True,
|
||||
tms_quote=tms,
|
||||
)
|
||||
tms_block = text.split("TMS报价:", 1)[1]
|
||||
self.assertNotIn("空运费", tms_block)
|
||||
self.assertNotIn("运输费", tms_block)
|
||||
self.assertIn("总销售价:CNY 200040", tms_block)
|
||||
self.assertEqual(tms_block.count("总销售价:"), 1)
|
||||
|
||||
def test_work_order_only_is_activate_not_quote(self) -> None:
|
||||
from agent.routing.air_group_intent import INTENT_ACTIVATE
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from agent.schema.air_route_options import (
|
||||
needs_route_pick,
|
||||
option_fee_lines,
|
||||
parse_air_options,
|
||||
project_group_air_quote,
|
||||
project_selected_quote,
|
||||
route_codes,
|
||||
route_line,
|
||||
@@ -220,6 +221,73 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
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
|
||||
|
||||
@@ -280,7 +280,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn(copy.BTN_SKIP_COLLAB, joined)
|
||||
self.assertIn("价格明细", joined)
|
||||
self.assertIn("总销售价", joined)
|
||||
self.assertIn("时效:", joined)
|
||||
self.assertNotIn("时效:", joined)
|
||||
self.assertIn("报价有效期:", joined)
|
||||
self.assertNotIn("相同询价内容检索", joined)
|
||||
self.assertNotIn("引用历史工单号", joined)
|
||||
@@ -324,7 +324,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
)
|
||||
rows = {x["keyname"]: x["value"] for x in card["horizontal_content_list"]}
|
||||
self.assertIn("2.5", rows["货物"])
|
||||
self.assertEqual(rows["时效"], "3-5 天")
|
||||
self.assertNotIn("时效", rows)
|
||||
self.assertNotIn("sub_title_text", card)
|
||||
n = len(self.replies)
|
||||
again = self.flow.on_button(
|
||||
@@ -346,6 +346,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertTrue(str(follow.get("task_id") or "").startswith("deal-"))
|
||||
deal_rows = {r["keyname"]: r["value"] for r in follow.get("horizontal_content_list") or []}
|
||||
self.assertIn("总销售价", deal_rows)
|
||||
self.assertNotIn("时效", deal_rows)
|
||||
self.assertNotIn("历史报价", deal_rows)
|
||||
self.assertNotIn("工单状态", deal_rows)
|
||||
self.assertNotIn("来源", deal_rows)
|
||||
@@ -829,7 +830,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertEqual(copy.money_text("14669.60", ""), "14669.60")
|
||||
self.assertNotIn(copy.collab_hint("WO202609140001"), quote_text)
|
||||
sub = card["sub_title_text"]
|
||||
self.assertIn("时效:3-5 天", sub)
|
||||
self.assertNotIn("时效", sub)
|
||||
self.assertIn("报价有效期:2026-09-20", sub)
|
||||
self.assertNotIn("相同询价内容检索", sub)
|
||||
self.assertNotIn("引用历史工单号", sub)
|
||||
@@ -866,6 +867,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("空运费:USD 1250.00", text)
|
||||
self.assertIn("总销售价:CNY 142998.53", text)
|
||||
self.assertNotIn("时效", text)
|
||||
self.assertIn("报价有效期:2026-09-30", text)
|
||||
card_text = copy.quote_area_safe_text(long_quote, business_line="AIR")
|
||||
self.assertIn("总销售价:CNY 142998.53", card_text)
|
||||
self.assertIn("详见上一条明细", card_text)
|
||||
@@ -985,7 +987,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("起运地:上海", text)
|
||||
self.assertIn("体积:2.5", text)
|
||||
self.assertIn("空运费:USD 1,200", text)
|
||||
self.assertIn("时效:3-5 天", text)
|
||||
self.assertNotIn("时效", text)
|
||||
self.assertIn("报价有效期:2026-09-20", text)
|
||||
self.assertNotIn("报价日期:", text)
|
||||
self.assertNotIn("工单状态", text)
|
||||
@@ -1015,20 +1017,20 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertEqual(rows["航线"], "空运SHA→LAX")
|
||||
self.assertIn("普货", rows["货物"])
|
||||
self.assertIn("2.5", rows["货物"])
|
||||
self.assertEqual(rows["时效"], "3-5 天")
|
||||
self.assertNotIn("时效", rows)
|
||||
self.assertEqual(rows["报价有效期"], "2026-09-20")
|
||||
self.assertNotIn("报价日期", rows)
|
||||
empty_eta = copy.scheme_horizontal_rows(
|
||||
work_order_no="WO1", facts=COMPLETE_AIR, quote={"valid_until": "2026-09-20"}
|
||||
)
|
||||
self.assertEqual({x["keyname"]: x["value"] for x in empty_eta}["时效"], "-")
|
||||
self.assertNotIn("时效", {x["keyname"] for x in empty_eta})
|
||||
dashed = copy.scheme_horizontal_rows(
|
||||
work_order_no="WO1",
|
||||
facts=COMPLETE_AIR,
|
||||
quote={"eta": "-3-5 天", "valid_until": "2026-09-20"},
|
||||
)
|
||||
dashed_rows = {x["keyname"]: x["value"] for x in dashed}
|
||||
self.assertEqual(dashed_rows["时效"], "3-5 天")
|
||||
self.assertNotIn("时效", dashed_rows)
|
||||
self.assertEqual(len(card["button_list"]), 3)
|
||||
self.assertEqual(
|
||||
[x["text"] for x in card["button_list"]],
|
||||
@@ -1060,8 +1062,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("报价单已发送,请跟进成交状态", text)
|
||||
self.assertIn("咨询成交状态:请选择成交 / 未成交 / 协商中(未成交需补充原因)。", text)
|
||||
self.assertIn("总销售价:CNY 722.5", text)
|
||||
self.assertIn("时效:2天", text)
|
||||
self.assertNotIn("时效(要求/TMS)", text)
|
||||
self.assertNotIn("时效", text)
|
||||
self.assertNotIn("- / -", text)
|
||||
self.assertIn("报价有效期:2026-08-24", text)
|
||||
self.assertNotIn("历史报价", text)
|
||||
@@ -1081,7 +1082,6 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
rows,
|
||||
{
|
||||
"总销售价": "CNY 722.5",
|
||||
"时效": "2天",
|
||||
"报价有效期": "2026-08-24",
|
||||
},
|
||||
)
|
||||
@@ -1102,10 +1102,13 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertEqual(copy.canonical_button("negotiate:WO202608210017"), copy.BTN_NEGOTIATE)
|
||||
empty_rows = {
|
||||
x["keyname"]: x["value"]
|
||||
for x in copy.deal_horizontal_rows(quote={"total": "-", "valid_until": "-"})
|
||||
for x in copy.deal_horizontal_rows(
|
||||
quote={"total": "-", "valid_until": "-"},
|
||||
business_line="AIR",
|
||||
)
|
||||
}
|
||||
self.assertEqual(empty_rows["时效"], "-")
|
||||
self.assertNotEqual(empty_rows["时效"], "- / -")
|
||||
self.assertNotIn("时效", empty_rows)
|
||||
self.assertEqual(empty_rows["报价有效期"], "-")
|
||||
|
||||
def test_same_demand_cites_latest_even_if_closed(self) -> None:
|
||||
ledger = MemoryLedger()
|
||||
|
||||
@@ -74,6 +74,21 @@ class HttpLedgerParseTests(unittest.TestCase):
|
||||
self.assertEqual(body["workOrderNo"], "WO202609220001")
|
||||
self.assertEqual(body["facts"]["起运港"], "深圳")
|
||||
|
||||
def test_upsert_quote_posts_replace_tms(self) -> None:
|
||||
fake = _FakeLedgerClient()
|
||||
out = HttpLedger(client=fake).upsert_quote(
|
||||
work_order_no="WO202609220015",
|
||||
quote={"total": "17100", "currency": "CNY", "route_line": "上海-谅山-胡志明"},
|
||||
to_status="已报价",
|
||||
)
|
||||
self.assertTrue(out.get("ok"))
|
||||
self.assertEqual(len(fake.calls), 1)
|
||||
method, path, body = fake.calls[0]
|
||||
self.assertEqual(method, "POST")
|
||||
self.assertEqual(path, "/inquiry/agent/ticket/replaceTmsQuote")
|
||||
self.assertEqual(body["workOrderNo"], "WO202609220015")
|
||||
self.assertEqual(body["quote"]["total"], "17100")
|
||||
|
||||
def test_void_quote_files_posts_agent_path(self) -> None:
|
||||
fake = _FakeLedgerClient()
|
||||
out = HttpLedger(client=fake).void_quote_files(work_order_no="WO202609220001")
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
陆运线路:按 segmentSequence 成条;一段两段是一条带中转,不是两条。
|
||||
"""
|
||||
|
||||
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.schema.land_route_options import (
|
||||
group_land_routes,
|
||||
needs_route_pick,
|
||||
option_fee_lines,
|
||||
option_total,
|
||||
project_selected_route,
|
||||
route_line,
|
||||
)
|
||||
|
||||
|
||||
def _single_with_transfer() -> dict:
|
||||
return {
|
||||
"isMulti": 1,
|
||||
"landSegments": [
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "谅山",
|
||||
"costPrice": 7500,
|
||||
"currency": "CNY",
|
||||
"transitDays": 48,
|
||||
},
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "谅山",
|
||||
"destination": "胡志明",
|
||||
"costPrice": 9600,
|
||||
"currency": "CNY",
|
||||
"transitDays": 72,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _three_routes() -> dict:
|
||||
base = _single_with_transfer()
|
||||
base["isMulti"] = 2
|
||||
extra = [
|
||||
{
|
||||
"segmentSequence": 2,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "深圳",
|
||||
"costPrice": 7500,
|
||||
"currency": "CNY",
|
||||
"transitDays": 48,
|
||||
},
|
||||
{
|
||||
"segmentSequence": 2,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "深圳",
|
||||
"destination": "胡志明",
|
||||
"costPrice": 9600,
|
||||
"currency": "CNY",
|
||||
"transitDays": 72,
|
||||
},
|
||||
{
|
||||
"segmentSequence": 3,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "广西",
|
||||
"costPrice": 7500,
|
||||
"currency": "CNY",
|
||||
"transitDays": 48,
|
||||
},
|
||||
{
|
||||
"segmentSequence": 3,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "广西",
|
||||
"destination": "胡志明",
|
||||
"costPrice": 9600,
|
||||
"currency": "CNY",
|
||||
"transitDays": 72,
|
||||
},
|
||||
]
|
||||
base["landSegments"] = list(base["landSegments"]) + extra
|
||||
return base
|
||||
|
||||
|
||||
def _tms_land_fees(*pairs: tuple[int, int, str, int]) -> list[dict]:
|
||||
"""TMS feeItems 原文:费用名按回包,不编路段名。"""
|
||||
rows: list[dict] = []
|
||||
for seq, leg, name, amount in pairs:
|
||||
rows.append(
|
||||
{
|
||||
"feeName": name,
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"currency": "CNY",
|
||||
"segmentSequence": seq,
|
||||
"routeLegSequence": leg,
|
||||
"unit": "台",
|
||||
"quantity": 1,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
class LandRouteOptionsTests(unittest.TestCase):
|
||||
def test_two_legs_one_route(self) -> None:
|
||||
quote = _single_with_transfer()
|
||||
routes = group_land_routes(quote)
|
||||
self.assertEqual(len(routes), 1)
|
||||
self.assertFalse(needs_route_pick(quote))
|
||||
self.assertEqual(route_line(routes[0]), "上海-谅山-胡志明")
|
||||
fees = option_fee_lines(routes[0])
|
||||
self.assertEqual(fees[0][0], "上海 → 谅山")
|
||||
self.assertIn("7500", fees[0][1])
|
||||
self.assertEqual(fees[1][0], "谅山 → 胡志明")
|
||||
self.assertIn("17100", option_total(routes[0]))
|
||||
|
||||
def test_three_segment_sequences_need_h5(self) -> None:
|
||||
quote = _three_routes()
|
||||
routes = group_land_routes(quote)
|
||||
self.assertEqual(len(routes), 3)
|
||||
self.assertTrue(needs_route_pick(quote))
|
||||
self.assertEqual(route_line(routes[1]), "上海-深圳-胡志明")
|
||||
|
||||
def test_project_writes_n_plus_eta_not_hours(self) -> None:
|
||||
quote = {
|
||||
"isMulti": 1,
|
||||
"eta": "-",
|
||||
"landSegments": [
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "谅山",
|
||||
"costPrice": 7500,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+2",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "谅山",
|
||||
"destination": "胡志明",
|
||||
"costPrice": 9600,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
],
|
||||
}
|
||||
routes = group_land_routes(quote)
|
||||
projected = project_selected_route(quote, routes[0])
|
||||
eta = str(projected.get("eta") or "")
|
||||
self.assertIn("N+2", eta)
|
||||
self.assertIn("N+3", eta)
|
||||
self.assertIn("17100", str(projected.get("total") or ""))
|
||||
|
||||
def test_project_keeps_tms_fee_names_not_od(self) -> None:
|
||||
quote = _single_with_transfer()
|
||||
quote["fee_lines"] = _tms_land_fees(
|
||||
(1, 1, "运输费", 7500),
|
||||
(1, 2, "运输费", 9600),
|
||||
)
|
||||
routes = group_land_routes(quote)
|
||||
projected = project_selected_route(quote, routes[0])
|
||||
names = [x.get("name") for x in projected.get("fee_lines") or []]
|
||||
self.assertEqual(names, ["运输费", "运输费"])
|
||||
joined = json.dumps(projected.get("fee_lines"), ensure_ascii=False)
|
||||
self.assertNotIn("上海 →", joined)
|
||||
self.assertIn("7500", joined)
|
||||
self.assertIn("9600", joined)
|
||||
|
||||
def test_project_keeps_only_selected_legs(self) -> None:
|
||||
quote = _three_routes()
|
||||
quote["fee_lines"] = _tms_land_fees(
|
||||
(1, 1, "运输费", 7500),
|
||||
(1, 2, "运输费", 9600),
|
||||
(2, 1, "运输费", 7500),
|
||||
(2, 2, "运输费", 9600),
|
||||
(3, 1, "提货费", 500),
|
||||
(3, 2, "运输费", 9600),
|
||||
)
|
||||
routes = group_land_routes(quote)
|
||||
projected = project_selected_route(quote, routes[1])
|
||||
raw = projected.get("landSegments")
|
||||
if isinstance(raw, str):
|
||||
raw = json.loads(raw)
|
||||
seqs = {int(x.get("segmentSequence") or 0) for x in raw}
|
||||
self.assertEqual(seqs, {2})
|
||||
names = [x.get("name") for x in projected.get("fee_lines") or []]
|
||||
self.assertEqual(names, ["运输费", "运输费"])
|
||||
self.assertIn("17100", str(projected.get("total") or ""))
|
||||
self.assertNotIn("谅山", json.dumps(projected.get("fee_lines"), ensure_ascii=False))
|
||||
self.assertNotIn("提货费", json.dumps(projected.get("fee_lines"), ensure_ascii=False))
|
||||
self.assertNotIn("上海 →", json.dumps(projected.get("fee_lines"), ensure_ascii=False))
|
||||
|
||||
def test_detail_puts_tms_fee_under_each_leg(self) -> None:
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
quote = _single_with_transfer()
|
||||
quote["landSegments"][0]["origin"] = "上海"
|
||||
quote["landSegments"][0]["destination"] = "谅山Lạng Sơn-春疆货场Huu Nghi"
|
||||
quote["landSegments"][0]["transitDays"] = "N+2"
|
||||
quote["landSegments"][1]["origin"] = "谅山Lạng Sơn-春疆货场Huu Nghi"
|
||||
quote["landSegments"][1]["destination"] = "胡志明Hồ Chí Minh"
|
||||
quote["landSegments"][1]["transitDays"] = "N+3"
|
||||
quote["fee_lines"] = _tms_land_fees(
|
||||
(1, 1, "运输费", 7500.0),
|
||||
(1, 2, "运输费", 9600.0),
|
||||
)
|
||||
routes = group_land_routes(quote)
|
||||
projected = project_selected_route(quote, routes[0])
|
||||
text = copy.sea_quote_detail_text(
|
||||
work_order_no="WO202609220029",
|
||||
quote=projected,
|
||||
business_line="LAND",
|
||||
)
|
||||
self.assertIn("上海-谅山Lạng Sơn-春疆货场Huu Nghi-胡志明Hồ Chí Minh", text)
|
||||
od1 = text.find("上海 → 谅山Lạng Sơn-春疆货场Huu Nghi")
|
||||
fee1 = text.find("运输费:CNY 7500", od1)
|
||||
od2 = text.find("谅山Lạng Sơn-春疆货场Huu Nghi → 胡志明Hồ Chí Minh")
|
||||
fee2 = text.find("运输费:CNY 9600", od2)
|
||||
self.assertGreaterEqual(od1, 0)
|
||||
self.assertGreater(fee1, od1)
|
||||
self.assertGreater(od2, fee1)
|
||||
self.assertGreater(fee2, od2)
|
||||
eta1 = text.find("时效:N+2", fee1)
|
||||
eta2 = text.find("时效:N+3", fee2)
|
||||
self.assertGreater(eta1, fee1)
|
||||
self.assertLess(eta1, od2)
|
||||
self.assertGreater(eta2, fee2)
|
||||
self.assertIn("总成本价:CNY 17100", text)
|
||||
|
||||
def test_h5_lists_segment_costs(self) -> None:
|
||||
from agent.channel.h5.land_route_page import render_land_route_page
|
||||
|
||||
html = render_land_route_page(
|
||||
{"work_order_no": "WO202609220010", "quote": _three_routes()}
|
||||
)
|
||||
self.assertIn("上海-深圳-胡志明", html)
|
||||
self.assertIn("上海 → 深圳", html)
|
||||
self.assertIn("成本价合计", html)
|
||||
self.assertNotIn("userid", html.lower())
|
||||
|
||||
def test_h5_puts_tms_fee_under_each_leg(self) -> None:
|
||||
from agent.channel.h5.land_route_page import render_land_route_page
|
||||
|
||||
quote = _three_routes()
|
||||
for leg in quote["landSegments"]:
|
||||
if str(leg.get("destination") or "") == "深圳":
|
||||
leg["transitDays"] = "N+2"
|
||||
if str(leg.get("origin") or "") == "深圳":
|
||||
leg["transitDays"] = "N+3"
|
||||
quote["fee_lines"] = _tms_land_fees(
|
||||
(1, 1, "运输费", 7500),
|
||||
(1, 2, "运输费", 9600),
|
||||
(2, 1, "运输费", 7500),
|
||||
(2, 2, "运输费", 9600),
|
||||
(3, 1, "运输费", 7500),
|
||||
(3, 2, "运输费", 9600),
|
||||
)
|
||||
page = render_land_route_page(
|
||||
{"work_order_no": "WO202609220010", "quote": quote}
|
||||
)
|
||||
od = page.find("上海 → 深圳")
|
||||
fee = page.find("运输费", od)
|
||||
self.assertGreaterEqual(od, 0)
|
||||
self.assertGreater(fee, od)
|
||||
eta = page.find("时效", fee)
|
||||
self.assertGreater(eta, fee)
|
||||
self.assertIn("N+2", page)
|
||||
self.assertIn("N+3", page)
|
||||
self.assertIn("CNY 7500", page)
|
||||
self.assertIn("成本价合计", page)
|
||||
|
||||
def test_h5_and_detail_use_tms_fees_when_java_sends_json_strings(self) -> None:
|
||||
"""主账 quote 常是 JSON 串,且 fee_lines 可能没带段号。"""
|
||||
from agent.channel.h5.land_route_page import render_land_route_page
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
quote = {
|
||||
"isMulti": "2",
|
||||
"currency": "CNY",
|
||||
"landSegments": json.dumps(
|
||||
[
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "谅山Lạng Sơn-春疆货场Huu Nghi",
|
||||
"costPrice": 7500.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+2",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "谅山Lạng Sơn-春疆货场Huu Nghi",
|
||||
"destination": "胡志明Hồ Chí Minh",
|
||||
"costPrice": 9600.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 2,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "广西",
|
||||
"costPrice": 7500.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 2,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "广西",
|
||||
"destination": "胡志明Hồ Chí Minh",
|
||||
"costPrice": 12500.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
],
|
||||
ensure_ascii=False,
|
||||
),
|
||||
"fee_lines": json.dumps(
|
||||
[
|
||||
{"name": "运输费", "amount": "7500.0", "currency": "CNY"},
|
||||
{"name": "运输费", "amount": "9600.0", "currency": "CNY"},
|
||||
{"name": "运输费", "amount": "7500.0", "currency": "CNY"},
|
||||
{"name": "运输费", "amount": "12500.0", "currency": "CNY"},
|
||||
],
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
page = render_land_route_page(
|
||||
{"work_order_no": "WO202609220036", "quote": quote}
|
||||
)
|
||||
od = page.find("上海 → 谅山Lạng Sơn-春疆货场Huu Nghi")
|
||||
fee = page.find("运输费", od)
|
||||
eta = page.find("时效", fee)
|
||||
self.assertGreaterEqual(od, 0)
|
||||
self.assertGreater(fee, od)
|
||||
self.assertGreater(eta, fee)
|
||||
self.assertIn("N+2", page)
|
||||
routes = group_land_routes(quote)
|
||||
projected = project_selected_route(quote, routes[0])
|
||||
text = copy.sea_quote_detail_text(
|
||||
work_order_no="WO202609220036",
|
||||
quote=projected,
|
||||
business_line="LAND",
|
||||
)
|
||||
self.assertIn("运输费:CNY 7500", text)
|
||||
self.assertIn("时效:N+2", text)
|
||||
self.assertIn("时效:N+3", text)
|
||||
self.assertNotRegex(text, r"上海 → 谅山.+:CNY")
|
||||
|
||||
def test_recovers_transport_fee_when_fee_lines_are_od_names(self) -> None:
|
||||
"""测服 WO036 这类:fee_lines 被写成路段名,销售侧仍要看到运输费和时效。"""
|
||||
from agent.channel.h5.land_route_page import render_land_route_page
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
quote = {
|
||||
"isMulti": "2",
|
||||
"currency": "CNY",
|
||||
"route_line": "上海-谅山Lạng Sơn-春疆货场Huu Nghi-胡志明Hồ Chí Minh",
|
||||
"landSegments": [
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "谅山Lạng Sơn-春疆货场Huu Nghi",
|
||||
"costPrice": 7500.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+2",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 1,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "谅山Lạng Sơn-春疆货场Huu Nghi",
|
||||
"destination": "胡志明Hồ Chí Minh",
|
||||
"costPrice": 9600.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 2,
|
||||
"routeLegSequence": 1,
|
||||
"origin": "上海",
|
||||
"destination": "广西",
|
||||
"costPrice": 7500.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
{
|
||||
"segmentSequence": 2,
|
||||
"routeLegSequence": 2,
|
||||
"origin": "广西",
|
||||
"destination": "胡志明Hồ Chí Minh",
|
||||
"costPrice": 12500.0,
|
||||
"currency": "CNY",
|
||||
"transitDays": "N+3",
|
||||
},
|
||||
],
|
||||
"fee_lines": [
|
||||
{"name": "上海 → 谅山Lạng Sơn-春疆货场Huu Nghi", "amount": "7500.0", "currency": "CNY"},
|
||||
{"name": "谅山Lạng Sơn-春疆货场Huu Nghi → 胡志明Hồ Chí Minh", "amount": "9600.0", "currency": "CNY"},
|
||||
],
|
||||
}
|
||||
page = render_land_route_page(
|
||||
{"work_order_no": "WO202609220036", "quote": quote}
|
||||
)
|
||||
od = page.find("上海 → 谅山Lạng Sơn-春疆货场Huu Nghi")
|
||||
fee = page.find("运输费", od)
|
||||
eta = page.find("时效", fee)
|
||||
self.assertGreaterEqual(od, 0)
|
||||
self.assertGreater(fee, od)
|
||||
self.assertGreater(eta, fee)
|
||||
self.assertIn("N+2", page)
|
||||
routes = group_land_routes(quote)
|
||||
projected = project_selected_route(quote, routes[0])
|
||||
text = copy.sea_quote_detail_text(
|
||||
work_order_no="WO202609220036",
|
||||
quote=projected,
|
||||
business_line="LAND",
|
||||
)
|
||||
self.assertIn("运输费:CNY 7500", text)
|
||||
self.assertIn("时效:N+2", text)
|
||||
self.assertIn("时效:N+3", text)
|
||||
self.assertNotRegex(text, r"上海 → 谅山.+:CNY")
|
||||
|
||||
def test_single_emits_detail_multi_opens_h5(self) -> None:
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy.land_text_flow import LandTextInquiryFlow
|
||||
|
||||
def _go(flag: str) -> tuple[str, list[str]]:
|
||||
flow = LandTextInquiryFlow(MemoryLedger())
|
||||
flow._detail_before_card_gap_sec = 0
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
facts = {
|
||||
"起运港": "上海",
|
||||
"目的港": "胡志明",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
flag: "1",
|
||||
}
|
||||
flow.on_text(
|
||||
sender_id="u_land_rt",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=reply,
|
||||
injected_facts=facts,
|
||||
injected_mode="LAND",
|
||||
)
|
||||
phase = flow.on_text(sender_id="u_land_rt", text="确定", reply=reply)
|
||||
return phase, replies
|
||||
|
||||
phase, replies = _go("tms_land_single")
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
self.assertTrue(any("运输费" in x for x in replies))
|
||||
self.assertTrue(any("上海-谅山-胡志明" in x for x in replies))
|
||||
self.assertTrue(any("上海 → 谅山" in x for x in replies))
|
||||
phase, replies = _go("tms_land_multi")
|
||||
self.assertEqual(phase, "wait_land_route")
|
||||
self.assertTrue(any("条线路" in x for x in replies))
|
||||
|
||||
def test_select_then_reselect_after_adopt_voids_old_file(self) -> None:
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.land_text_flow import LandTextInquiryFlow
|
||||
|
||||
ledger = MemoryLedger()
|
||||
flow = LandTextInquiryFlow(ledger)
|
||||
flow._detail_before_card_gap_sec = 0
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
facts = {
|
||||
"起运港": "上海",
|
||||
"目的港": "胡志明",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
"tms_land_multi": "1",
|
||||
}
|
||||
flow.on_text(
|
||||
sender_id="u_land_void",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=reply,
|
||||
injected_facts=facts,
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(
|
||||
flow.on_text(sender_id="u_land_void", text="确定", reply=reply),
|
||||
"wait_land_route",
|
||||
)
|
||||
self.assertEqual(
|
||||
flow.select_land_route(sender_id="u_land_void", index=1, reply=reply),
|
||||
"wait_collab",
|
||||
)
|
||||
joined = "\n".join(replies)
|
||||
self.assertIn("上海-深圳-胡志明", joined)
|
||||
self.assertIn("运输费", joined)
|
||||
self.assertIn("上海 → 深圳", joined)
|
||||
self.assertNotIn("谅山", "\n".join(replies[-3:]))
|
||||
self.assertEqual(
|
||||
flow.on_button(sender_id="u_land_void", action=copy.BTN_SKIP_COLLAB, reply=reply),
|
||||
"wait_adopt",
|
||||
)
|
||||
self.assertEqual(
|
||||
flow.on_button(sender_id="u_land_void", action=copy.BTN_ADOPT_EXCEL, reply=reply),
|
||||
"wait_deal",
|
||||
)
|
||||
wo = flow.session_of("u_land_void").work_order_no
|
||||
self.assertTrue(ledger.get_ticket(work_order_no=wo).quote_file_ready)
|
||||
phase = flow.select_land_route(sender_id="u_land_void", index=0, reply=reply)
|
||||
self.assertEqual(phase, "wait_adopt")
|
||||
self.assertFalse(ledger.get_ticket(work_order_no=wo).quote_file_ready)
|
||||
ticket = ledger.get_ticket(work_order_no=wo)
|
||||
segs = ticket.quote.get("landSegments") or []
|
||||
seqs = {int(x.get("segmentSequence") or 0) for x in segs}
|
||||
self.assertEqual(seqs, {1})
|
||||
self.assertTrue(any("报价方案" in x or "生成Excel" in x for x in replies[-3:]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -300,8 +300,10 @@ class QuoteAdjustTests(unittest.TestCase):
|
||||
self.assertIn("总成本价:CNY 2287.65", "\n".join(copy.quote_detail_lines(sea, business_line="SEA")))
|
||||
rows = copy.deal_horizontal_rows(quote=air, business_line="AIR")
|
||||
self.assertEqual(rows[0]["keyname"], "总销售价")
|
||||
self.assertNotIn("时效", [x["keyname"] for x in rows])
|
||||
land_rows = copy.deal_horizontal_rows(quote=sea, business_line="LAND")
|
||||
self.assertEqual(land_rows[0]["keyname"], "总成本价")
|
||||
self.assertIn("时效", [x["keyname"] for x in land_rows])
|
||||
|
||||
def test_adjust_cards_use_sales_adjust_not_tms(self) -> None:
|
||||
"""调价后价格明细卡、方案卡必须写销售调价,禁止再套 TMS 标准报价。"""
|
||||
|
||||
Reference in New Issue
Block a user