陆运选定线路后,私聊明细补报价有效期,群摘要只用已选线路并不再重复标题。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -45,6 +45,8 @@ class FlowSession:
|
|||||||
history_work_order_no: str = ""
|
history_work_order_no: str = ""
|
||||||
quote: dict[str, Any] = field(default_factory=dict)
|
quote: dict[str, Any] = field(default_factory=dict)
|
||||||
tms_quote: dict[str, Any] = field(default_factory=dict)
|
tms_quote: dict[str, Any] = field(default_factory=dict)
|
||||||
|
# 陆运 H5 重选要用的整包线路。选定后 tms_quote 只留已选那条,供群摘要使用。
|
||||||
|
land_route_quote: dict[str, Any] = field(default_factory=dict)
|
||||||
status: str = ""
|
status: str = ""
|
||||||
allowed: tuple[str, ...] = ()
|
allowed: tuple[str, ...] = ()
|
||||||
immutable_text: str = ""
|
immutable_text: str = ""
|
||||||
@@ -88,6 +90,7 @@ def _session_from_payload(raw: dict[str, Any]) -> FlowSession:
|
|||||||
history_work_order_no=str(raw.get("history_work_order_no") or ""),
|
history_work_order_no=str(raw.get("history_work_order_no") or ""),
|
||||||
quote=dict(raw.get("quote") or {}),
|
quote=dict(raw.get("quote") or {}),
|
||||||
tms_quote=dict(raw.get("tms_quote") or raw.get("tmsQuote") or {}),
|
tms_quote=dict(raw.get("tms_quote") or raw.get("tmsQuote") or {}),
|
||||||
|
land_route_quote=dict(raw.get("land_route_quote") or raw.get("landRouteQuote") or {}),
|
||||||
status=str(raw.get("status") or ""),
|
status=str(raw.get("status") or ""),
|
||||||
allowed=tuple(str(x) for x in (raw.get("allowed") or ())),
|
allowed=tuple(str(x) for x in (raw.get("allowed") or ())),
|
||||||
immutable_text=str(raw.get("immutable_text") or ""),
|
immutable_text=str(raw.get("immutable_text") or ""),
|
||||||
@@ -228,6 +231,7 @@ class AirTextInquiryFlow:
|
|||||||
"status": sess.status,
|
"status": sess.status,
|
||||||
"quote": dict(sess.quote or {}),
|
"quote": dict(sess.quote or {}),
|
||||||
"tms_quote": dict(sess.tms_quote or {}),
|
"tms_quote": dict(sess.tms_quote or {}),
|
||||||
|
"land_route_quote": dict(sess.land_route_quote or {}),
|
||||||
"allowed": sess.allowed,
|
"allowed": sess.allowed,
|
||||||
"invited_userids": sess.invited_userids,
|
"invited_userids": sess.invited_userids,
|
||||||
"invited_sales_name": sess.invited_sales_name,
|
"invited_sales_name": sess.invited_sales_name,
|
||||||
|
|||||||
@@ -1005,7 +1005,8 @@ def sea_quote_detail_text(
|
|||||||
"""
|
"""
|
||||||
完整价格明细文本:来源、工单号、费用行、总价。海运/陆运/空运共用。
|
完整价格明细文本:来源、工单号、费用行、总价。海运/陆运/空运共用。
|
||||||
|
|
||||||
陆运分段时效跟在各段费用后面,用 TMS 原文。海运时效、报价有效期仍只在卡片上。
|
陆运分段时效跟在各段费用后面,用 TMS 原文;报价有效期写在总成本价下面。
|
||||||
|
海运时效、报价有效期仍只在按钮卡副标题。
|
||||||
空运:业务确认没有时效,总销售价下面只留报价有效期。
|
空运:业务确认没有时效,总销售价下面只留报价有效期。
|
||||||
不受卡片引用条字数限制。first_or_same 只兼容调用方。
|
不受卡片引用条字数限制。first_or_same 只兼容调用方。
|
||||||
"""
|
"""
|
||||||
@@ -1025,7 +1026,8 @@ def sea_quote_detail_text(
|
|||||||
if land_line:
|
if land_line:
|
||||||
lines.append(land_line)
|
lines.append(land_line)
|
||||||
lines.append(quote_area_fee_text(src, business_line=business_line or "SEA"))
|
lines.append(quote_area_fee_text(src, business_line=business_line or "SEA"))
|
||||||
if line == "AIR":
|
# 空运、陆运的价格明细正文都要带有效期。海运仍放在按钮卡副标题,避免和原口径打架。
|
||||||
|
if line in {"AIR", "LAND"}:
|
||||||
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -1698,6 +1700,40 @@ def resolve_tms_quote(sess: Any = None, ticket: Any = None) -> dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _land_quote_is_selected(quote: dict[str, Any] | None) -> bool:
|
||||||
|
"""已收成一条陆运线路:有选定段,或只剩一条且写了线路名。"""
|
||||||
|
src = dict(quote or {})
|
||||||
|
selected = src.get("selected_land_route")
|
||||||
|
if isinstance(selected, dict) and list(selected.get("legs") or []):
|
||||||
|
return True
|
||||||
|
if not str(src.get("route_line") or "").strip():
|
||||||
|
return False
|
||||||
|
from agent.schema.land_route_options import needs_route_pick
|
||||||
|
|
||||||
|
return not needs_route_pick(src)
|
||||||
|
|
||||||
|
|
||||||
|
def land_group_tms_quote(
|
||||||
|
tms: dict[str, Any] | None,
|
||||||
|
current: dict[str, Any] | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
群摘要陆运 TMS 段用已选线路,不用还没选的整包。
|
||||||
|
|
||||||
|
选定前书签里的 TMS 原价含全部备选费用,总成本价和时效都还没汇总。
|
||||||
|
当前价已是选定投影时用当前价。当前价已是人工报价或调价时,仍用传入的 TMS 价。
|
||||||
|
"""
|
||||||
|
raw = dict(tms or {})
|
||||||
|
cur = dict(current or {})
|
||||||
|
if not quote_is_tms(cur) or not _land_quote_is_selected(cur):
|
||||||
|
return raw
|
||||||
|
from agent.schema.land_route_options import needs_route_pick
|
||||||
|
|
||||||
|
if _land_quote_is_selected(raw) and not needs_route_pick(raw):
|
||||||
|
return raw
|
||||||
|
return cur
|
||||||
|
|
||||||
|
|
||||||
def sea_group_brief(
|
def sea_group_brief(
|
||||||
*,
|
*,
|
||||||
work_order_no: str,
|
work_order_no: str,
|
||||||
@@ -1741,10 +1777,26 @@ def sea_group_brief(
|
|||||||
product_quote=product_quote,
|
product_quote=product_quote,
|
||||||
has_price=has_price,
|
has_price=has_price,
|
||||||
)
|
)
|
||||||
|
if mode == "陆运":
|
||||||
|
# 拉群时书签仍可能是整包。当前价已选定时换成那一条,和私聊明细对齐。
|
||||||
|
tms = land_group_tms_quote(tms, quote)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("TMS报价:")
|
lines.append("TMS报价:")
|
||||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||||
lines.extend(quote_detail_lines(tms, transport_mode=mode))
|
if mode == "陆运":
|
||||||
|
# 与私聊价格明细同一套:线路名、分段费用、各段时效、总成本价、有效期。
|
||||||
|
# 不用 quote_detail_lines,那会再补一行合并时效,也会在未选定时摊开全部费用。
|
||||||
|
# 群里已有「TMS报价:」,不要再叠「TMS标准报价」和「工单号价格明细」。
|
||||||
|
detail = sea_quote_detail_text(
|
||||||
|
work_order_no=no,
|
||||||
|
quote=tms,
|
||||||
|
business_line="LAND",
|
||||||
|
).splitlines()
|
||||||
|
source = str(tms.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||||
|
title = f"{no}价格明细"
|
||||||
|
lines.extend(ln for ln in detail if ln.strip() not in {source, title, "TMS标准报价"})
|
||||||
|
else:
|
||||||
|
lines.extend(quote_detail_lines(tms, transport_mode=mode))
|
||||||
lines.append(group_tms_confirm_hint())
|
lines.append(group_tms_confirm_hint())
|
||||||
else:
|
else:
|
||||||
lines.append("TMS 暂无匹配报价")
|
lines.append("TMS 暂无匹配报价")
|
||||||
|
|||||||
@@ -418,7 +418,12 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
|||||||
self._save(sess)
|
self._save(sess)
|
||||||
return "ledger_fail"
|
return "ledger_fail"
|
||||||
sess.quote = quote
|
sess.quote = quote
|
||||||
copy.remember_tms_quote(sess, raw_quote)
|
# 整包留给 H5 重选。已只有一条时,群摘要用的 TMS 价必须是投影后的那条。
|
||||||
|
sess.land_route_quote = dict(raw_quote)
|
||||||
|
if needs_route_pick(raw_quote):
|
||||||
|
copy.remember_tms_quote(sess, raw_quote)
|
||||||
|
else:
|
||||||
|
copy.remember_tms_quote(sess, quote)
|
||||||
sess.status = "已报价"
|
sess.status = "已报价"
|
||||||
if needs_route_pick(raw_quote):
|
if needs_route_pick(raw_quote):
|
||||||
return self._offer_land_routes(sess=sess, work_order_no=no, quote=raw_quote, reply=reply)
|
return self._offer_land_routes(sess=sess, work_order_no=no, quote=raw_quote, reply=reply)
|
||||||
@@ -485,7 +490,9 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
|||||||
return "route_expired"
|
return "route_expired"
|
||||||
from agent.schema.land_route_options import group_land_routes, project_selected_route
|
from agent.schema.land_route_options import group_land_routes, project_selected_route
|
||||||
|
|
||||||
raw_quote = dict(sess.tms_quote or sess.quote or {})
|
stored = dict(getattr(sess, "land_route_quote", None) or {})
|
||||||
|
# 选定后 tms_quote 已收成一条,重选必须回到整包,否则第二条线路会丢。
|
||||||
|
raw_quote = stored if group_land_routes(stored) else dict(sess.tms_quote or sess.quote or {})
|
||||||
routes = group_land_routes(raw_quote)
|
routes = group_land_routes(raw_quote)
|
||||||
if index < 0 or index >= len(routes):
|
if index < 0 or index >= len(routes):
|
||||||
reply("没有这条线路,请重新打开页面选择。")
|
reply("没有这条线路,请重新打开页面选择。")
|
||||||
@@ -502,6 +509,9 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
|||||||
|
|
||||||
clear_quote_file_ready(sess.work_order_no)
|
clear_quote_file_ready(sess.work_order_no)
|
||||||
self._ledger.upsert_quote(work_order_no=sess.work_order_no, quote=quote, to_status="已报价")
|
self._ledger.upsert_quote(work_order_no=sess.work_order_no, quote=quote, to_status="已报价")
|
||||||
|
if not stored:
|
||||||
|
sess.land_route_quote = dict(raw_quote)
|
||||||
|
copy.remember_tms_quote(sess, quote)
|
||||||
sess.quote = quote
|
sess.quote = quote
|
||||||
sess.status = "已报价"
|
sess.status = "已报价"
|
||||||
sess.wait_version += 1
|
sess.wait_version += 1
|
||||||
|
|||||||
@@ -486,10 +486,16 @@ def send_brief_and_sheet(
|
|||||||
or "SEA"
|
or "SEA"
|
||||||
).strip().upper()
|
).strip().upper()
|
||||||
mode = "陆运" if line == "LAND" else "海运"
|
mode = "陆运" if line == "LAND" else "海运"
|
||||||
|
# 陆运要把当前生效价一并带上:选定投影和未选整包不是同一份,摘要里要能换成已选那条。
|
||||||
|
brief_quote = dict(tms or {})
|
||||||
|
if mode == "陆运":
|
||||||
|
current = dict(override or sess_q or view_q or {})
|
||||||
|
if current:
|
||||||
|
brief_quote = current
|
||||||
brief = copy.sea_group_brief(
|
brief = copy.sea_group_brief(
|
||||||
work_order_no=no,
|
work_order_no=no,
|
||||||
facts=facts,
|
facts=facts,
|
||||||
quote=tms or None,
|
quote=brief_quote or None,
|
||||||
collab_facts=collab,
|
collab_facts=collab,
|
||||||
sales_name=sales_name,
|
sales_name=sales_name,
|
||||||
product_names=products,
|
product_names=products,
|
||||||
|
|||||||
@@ -240,6 +240,116 @@ class LandRouteOptionsTests(unittest.TestCase):
|
|||||||
self.assertLess(eta1, od2)
|
self.assertLess(eta1, od2)
|
||||||
self.assertGreater(eta2, fee2)
|
self.assertGreater(eta2, fee2)
|
||||||
self.assertIn("总成本价:CNY 17100", text)
|
self.assertIn("总成本价:CNY 17100", text)
|
||||||
|
valid = text.find("报价有效期:", text.find("总成本价:"))
|
||||||
|
self.assertGreater(valid, text.find("总成本价:"))
|
||||||
|
|
||||||
|
def test_land_detail_and_group_use_selected_route_valid_until(self) -> None:
|
||||||
|
"""选定广西这条后:私聊明细和群 TMS 段同一套正文,不摊开没选的线路。"""
|
||||||
|
from agent.policy import inquiry_copy as copy
|
||||||
|
|
||||||
|
raw = {
|
||||||
|
"source": "TMS",
|
||||||
|
"source_label": "TMS标准报价",
|
||||||
|
"isMulti": "2",
|
||||||
|
"currency": "CNY",
|
||||||
|
"valid_until": "2026-10-31",
|
||||||
|
"landSegments": [
|
||||||
|
{
|
||||||
|
"segmentSequence": 1,
|
||||||
|
"routeLegSequence": 1,
|
||||||
|
"origin": "上海",
|
||||||
|
"destination": "谅山",
|
||||||
|
"costPrice": 7500.0,
|
||||||
|
"currency": "CNY",
|
||||||
|
"transitDays": "N+2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segmentSequence": 1,
|
||||||
|
"routeLegSequence": 2,
|
||||||
|
"origin": "谅山",
|
||||||
|
"destination": "胡志明",
|
||||||
|
"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": "胡志明",
|
||||||
|
"costPrice": 12500.0,
|
||||||
|
"currency": "CNY",
|
||||||
|
"transitDays": "N+3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"fee_lines": [
|
||||||
|
{"name": "运输费", "amount": "7500.00", "currency": "CNY"},
|
||||||
|
{"name": "运输费", "amount": "9600.00", "currency": "CNY"},
|
||||||
|
{"name": "运输费", "amount": "7500.00", "currency": "CNY"},
|
||||||
|
{"name": "运输费", "amount": "12500.00", "currency": "CNY"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
routes = group_land_routes(raw)
|
||||||
|
projected = project_selected_route(raw, routes[1])
|
||||||
|
detail = copy.sea_quote_detail_text(
|
||||||
|
work_order_no="WO202609230003",
|
||||||
|
quote=projected,
|
||||||
|
business_line="LAND",
|
||||||
|
)
|
||||||
|
self.assertIn("TMS标准报价", detail)
|
||||||
|
self.assertIn("WO202609230003价格明细", detail)
|
||||||
|
self.assertIn("上海-广西壮族自治区-胡志明", detail)
|
||||||
|
self.assertIn("上海 → 广西壮族自治区", detail)
|
||||||
|
self.assertIn("运输费:CNY 7500.00", detail)
|
||||||
|
self.assertIn("广西壮族自治区 → 胡志明", detail)
|
||||||
|
self.assertIn("运输费:CNY 12500.00", detail)
|
||||||
|
self.assertIn("总成本价:CNY 20000", detail)
|
||||||
|
self.assertIn("报价有效期:2026-10-31", detail)
|
||||||
|
self.assertGreater(detail.find("报价有效期:"), detail.find("总成本价:"))
|
||||||
|
self.assertNotIn("9600", detail)
|
||||||
|
self.assertNotIn("时效:N+3 / N+3", detail)
|
||||||
|
brief = copy.sea_group_brief(
|
||||||
|
work_order_no="WO202609230003",
|
||||||
|
facts={
|
||||||
|
"运输类型": "国内运输拼车",
|
||||||
|
"线路类别": "国内长途/零担",
|
||||||
|
"运输分类": "急件",
|
||||||
|
"起运港": "上海",
|
||||||
|
"目的港": "胡志明",
|
||||||
|
"品名": "衣服",
|
||||||
|
"体积": "1",
|
||||||
|
"毛重": "1",
|
||||||
|
"报价日期": "2026-09-23",
|
||||||
|
},
|
||||||
|
quote=projected,
|
||||||
|
collab_facts={},
|
||||||
|
sales_name="销",
|
||||||
|
product_names=["产"],
|
||||||
|
has_price=True,
|
||||||
|
tms_quote=raw,
|
||||||
|
transport_mode="陆运",
|
||||||
|
)
|
||||||
|
tms_block = brief.split("TMS报价:", 1)[1].split("有TMS报价时", 1)[0]
|
||||||
|
self.assertIn("上海-广西壮族自治区-胡志明", tms_block)
|
||||||
|
self.assertIn("上海 → 广西壮族自治区", tms_block)
|
||||||
|
self.assertIn("运输费:CNY 12500.00", tms_block)
|
||||||
|
self.assertIn("总成本价:CNY 20000", tms_block)
|
||||||
|
self.assertIn("报价有效期:2026-10-31", tms_block)
|
||||||
|
self.assertNotIn("TMS标准报价", tms_block)
|
||||||
|
self.assertNotIn("价格明细", tms_block)
|
||||||
|
self.assertNotIn("9600", tms_block)
|
||||||
|
self.assertNotIn("总成本价:-", tms_block)
|
||||||
|
self.assertNotIn("时效:-", tms_block)
|
||||||
|
self.assertNotIn("时效:N+3 / N+3", tms_block)
|
||||||
|
|
||||||
def test_h5_lists_segment_costs(self) -> None:
|
def test_h5_lists_segment_costs(self) -> None:
|
||||||
from agent.channel.h5.land_route_page import render_land_route_page
|
from agent.channel.h5.land_route_page import render_land_route_page
|
||||||
|
|||||||
Reference in New Issue
Block a user