陆运多条线路按段展示 TMS 费用名和时效,选定后再写回该条成本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 18:28:29 +08:00
co-authored by Cursor
parent e769eb9c1d
commit 49faa35f47
23 changed files with 2323 additions and 55 deletions
@@ -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
+16 -13
View File
@@ -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()
+15
View File
@@ -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()
+2
View File
@@ -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 标准报价。"""