后台成本价、销售价、利润点按费用逐行展示,费用合计用报价官方总价。选完线路只关页面。报价单文件名带上模板名,空运起运地和转人工跟对应工单走。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -113,12 +113,25 @@ class AirFieldContractTests(unittest.TestCase):
|
||||
}
|
||||
)
|
||||
self.assertNotIn("工单号", text)
|
||||
self.assertIn("起运地(非必填):", text)
|
||||
self.assertNotIn("起运港(非必填)", text)
|
||||
self.assertLess(text.index("起运地(非必填):"), text.index("目的地(必填):克拉克"))
|
||||
self.assertIn("目的地(必填):克拉克", text)
|
||||
self.assertIn("起运港(非必填):", text)
|
||||
self.assertIn("货源地(非必填):", text)
|
||||
self.assertIn("贸易条款(非必填):", text)
|
||||
self.assertIn(copy.AIR_CONFIRM_TAIL, text)
|
||||
|
||||
def test_clarify_puts_origin_above_destination(self) -> None:
|
||||
text = copy.ask_clarify(
|
||||
facts={},
|
||||
missing_keys=["目的港", "品名"],
|
||||
transport_mode="空运",
|
||||
)
|
||||
self.assertIn("1.起运地(非必填):\n(如:南京)", text)
|
||||
self.assertIn("2.目的地(必填):\n(如:吉隆坡)", text)
|
||||
self.assertLess(text.index("起运地(非必填)"), text.index("目的地(必填)"))
|
||||
self.assertNotIn("起运港(非必填)", text)
|
||||
|
||||
def test_empty_origin_confirm_then_quote(self) -> None:
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
replies: list[str] = []
|
||||
|
||||
@@ -331,6 +331,7 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
self.assertIn("closeWindow", ok)
|
||||
self.assertIn("wecom-jssdk", ok)
|
||||
self.assertIn("jsapi-signature", ok)
|
||||
self.assertNotIn("window.close()", ok)
|
||||
self.assertNotIn("closeWindow", dead)
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("请确认并补充以下信息:", text)
|
||||
self.assertIn("运输方式:空运", text)
|
||||
self.assertIn("起运地:珠海", text)
|
||||
self.assertLess(text.index("起运地:珠海"), text.index("目的地:克拉克"))
|
||||
self.assertIn("目的地:克拉克", text)
|
||||
self.assertIn("品名:茶叶", text)
|
||||
self.assertIn("件数:10件", text)
|
||||
|
||||
@@ -572,5 +572,58 @@ class SeaSheetHeaderClipTests(unittest.TestCase):
|
||||
self.assertEqual(oral["facts"].get("报价日期"), "2026-09-15")
|
||||
|
||||
|
||||
# 测服 2026-10-05:空运报价单整表被抽进补问,费用和条款进了起运地、货好时间。
|
||||
AIR_QUOTE_SHEET = """
|
||||
运输方式:空运
|
||||
目的地:CRK
|
||||
品名:化妆品
|
||||
件数:11
|
||||
重量:1KG
|
||||
体 积: 100 CBM
|
||||
报价日期:46307
|
||||
货好时间:
|
||||
体 积: 100 CBM 时效要求:
|
||||
计 费 重: 10 KG
|
||||
派送地址:
|
||||
二、报价明细
|
||||
项目 费用名称 单位 币别 单价 数量 金额 备注
|
||||
空运段 空运费
|
||||
起运港 操作费
|
||||
文件费 如需多个分单另计
|
||||
*代理报关费 发生时收取
|
||||
*提货费 发生时收取
|
||||
其他 *其他杂费 发生时收取
|
||||
预估费用 0
|
||||
三、报价条件
|
||||
1、报价适用范围:一般货物(非危险品)
|
||||
贸易条款:DAP
|
||||
托运方:某某有限公司 承运方:深圳市友通达国际货运代理有限公司
|
||||
"""
|
||||
|
||||
|
||||
class AirQuoteSheetInquiryOnlyTests(unittest.TestCase):
|
||||
def test_quote_sheet_keeps_inquiry_fields_only(self) -> None:
|
||||
"""费用明细、报价条件、落款不能进补问。起运地不能收成操作费。"""
|
||||
snap = extract_inquiry_snapshot(AIR_QUOTE_SHEET, allow_b=False)
|
||||
facts = snap["facts"]
|
||||
self.assertEqual(snap["business_line"], "AIR")
|
||||
self.assertEqual(facts.get("目的港"), "CRK")
|
||||
self.assertEqual(facts.get("品名"), "化妆品")
|
||||
self.assertIn("11", str(facts.get("件数") or ""))
|
||||
self.assertIn("1", str(facts.get("毛重") or ""))
|
||||
self.assertNotIn("10", str(facts.get("毛重") or ""))
|
||||
self.assertIn("100", str(facts.get("体积") or ""))
|
||||
self.assertEqual(facts.get("报价日期"), "2026-10-12")
|
||||
self.assertNotIn("起运港", facts)
|
||||
self.assertNotIn("货好时间", facts)
|
||||
self.assertNotIn("贸易条款", facts)
|
||||
blob = "\n".join(facts.values())
|
||||
self.assertNotIn("操作费", blob)
|
||||
self.assertNotIn("报价明细", blob)
|
||||
self.assertNotIn("报价条件", blob)
|
||||
self.assertNotIn("空舱", blob)
|
||||
self.assertNotIn("友通达", blob)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -345,5 +345,231 @@ class HandoffFlowOfferTests(unittest.TestCase):
|
||||
self.assertIn(copy.BTN_PULL_COLLAB, bodies)
|
||||
|
||||
|
||||
class NamedTicketHandoffTests(unittest.TestCase):
|
||||
"""点名工单号转人工时,只看那一张单,不看私聊里另一张已建群的单。"""
|
||||
|
||||
def _flow(self):
|
||||
from agent.policy.land_text_flow import LandTextInquiryFlow
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
|
||||
return LandTextInquiryFlow(MemoryLedger())
|
||||
|
||||
def test_deal_followup_not_blocked_by_other_group(self) -> None:
|
||||
flow = self._flow()
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="s1",
|
||||
thread_id="s1",
|
||||
phase="wait_deal",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040002",
|
||||
status="已报价",
|
||||
quote={"total": "CNY 800", "quoteFileIssued": True},
|
||||
)
|
||||
)
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="s1",
|
||||
thread_id="s1",
|
||||
phase="collab_group",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040004",
|
||||
collab_chat_id="wr_other",
|
||||
status="询价中",
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.offer_handoff(
|
||||
sender_id="s1", text="WO202610040002转人工", reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "handoff_offer")
|
||||
self.assertIn("运输方式:陆运", replies[-1])
|
||||
self.assertIn("是否拉产品人员进群协同", replies[-1])
|
||||
self.assertNotIn("原有空运群", replies[-1])
|
||||
self.assertNotEqual(replies[-1], copy.HANDOFF_ALREADY_COLLAB)
|
||||
named = flow.session_by_ticket("s1", "WO202610040002")
|
||||
self.assertEqual(named.phase, "handoff_offer")
|
||||
self.assertFalse((named.collab_chat_id or "").strip())
|
||||
|
||||
def test_named_ticket_with_group_still_blocked(self) -> None:
|
||||
flow = self._flow()
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="s1",
|
||||
thread_id="s1",
|
||||
phase="collab_group",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040002",
|
||||
collab_chat_id="wr_real",
|
||||
status="询价中",
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.offer_handoff(
|
||||
sender_id="s1", text="WO202610040002转人工", reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "handoff_already")
|
||||
self.assertEqual(replies[-1], copy.HANDOFF_ALREADY_COLLAB)
|
||||
|
||||
def test_ledger_deal_ticket_without_group_offers_card(self) -> None:
|
||||
from agent.ledger.memory_ledger import MemoryTicket
|
||||
|
||||
flow = self._flow()
|
||||
no = "WO202610040012"
|
||||
flow.ledger._tickets[no] = MemoryTicket(
|
||||
work_order_no=no,
|
||||
sales_wecom_id="s1",
|
||||
business_line="LAND",
|
||||
facts={"运输方式": "陆运"},
|
||||
fingerprint="land-deal",
|
||||
status="已报价",
|
||||
quote={"total": "CNY 800", "quoteFileIssued": True},
|
||||
quote_file_ready=True,
|
||||
)
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="s1",
|
||||
thread_id="s1",
|
||||
phase="collab_group",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040004",
|
||||
collab_chat_id="wr_other",
|
||||
status="询价中",
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.offer_handoff(sender_id="s1", text=f"{no}转人工", reply=reply)
|
||||
self.assertEqual(phase, "handoff_offer")
|
||||
self.assertIn("是否拉产品人员进群协同", "\n".join(replies))
|
||||
self.assertNotIn(copy.HANDOFF_ALREADY_COLLAB, replies)
|
||||
|
||||
def test_air_deal_ticket_ignores_other_land_group(self) -> None:
|
||||
"""空运成交跟进没有协同群时,不能被另一张陆运已拉群的单挡成「已拉产品进群」。"""
|
||||
from agent.ledger.memory_ledger import MemoryTicket
|
||||
|
||||
flow = self._flow()
|
||||
no = "WO202610040002"
|
||||
flow.ledger._tickets[no] = MemoryTicket(
|
||||
work_order_no=no,
|
||||
sales_wecom_id="s1",
|
||||
business_line="AIR",
|
||||
facts={"运输方式": "空运", "目的港": "CRK"},
|
||||
fingerprint="air-deal",
|
||||
status="已报价",
|
||||
quote={"total": "CNY 800", "quoteFileIssued": True},
|
||||
quote_file_ready=True,
|
||||
)
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="s1",
|
||||
thread_id="s1",
|
||||
phase="collab_group",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040004",
|
||||
collab_chat_id="wr_other",
|
||||
status="询价中",
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.offer_handoff(sender_id="s1", text=f"{no}转人工", reply=reply)
|
||||
joined = "\n".join(replies)
|
||||
self.assertEqual(phase, "handoff_offer")
|
||||
self.assertIn("原有空运群", joined)
|
||||
self.assertNotIn("是否拉产品人员进群协同", joined)
|
||||
self.assertNotIn(copy.HANDOFF_ALREADY_COLLAB, joined)
|
||||
|
||||
def test_ledger_mode_wins_over_stale_bookmark(self) -> None:
|
||||
"""主账是空运时,进程里标成陆运的旧书签也不能改成陆运拉群卡。"""
|
||||
from agent.ledger.memory_ledger import MemoryTicket
|
||||
|
||||
flow = self._flow()
|
||||
no = "WO202610040002"
|
||||
flow.ledger._tickets[no] = MemoryTicket(
|
||||
work_order_no=no,
|
||||
sales_wecom_id="s1",
|
||||
business_line="AIR",
|
||||
facts={},
|
||||
fingerprint="air-wins",
|
||||
status="已报价",
|
||||
quote={"total": "CNY 800", "quoteFileIssued": True},
|
||||
quote_file_ready=True,
|
||||
)
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="s1",
|
||||
thread_id="s1",
|
||||
phase="wait_deal",
|
||||
business_line="LAND",
|
||||
work_order_no=no,
|
||||
status="已报价",
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.offer_handoff(sender_id="s1", text=f"{no}转人工", reply=reply)
|
||||
joined = "\n".join(replies)
|
||||
self.assertEqual(phase, "handoff_offer")
|
||||
self.assertIn("原有空运群", joined)
|
||||
self.assertNotIn("运输方式:陆运", joined)
|
||||
|
||||
def test_sea_deal_ticket_uses_sea_card(self) -> None:
|
||||
from agent.ledger.memory_ledger import MemoryTicket
|
||||
|
||||
flow = self._flow()
|
||||
no = "WO202610040013"
|
||||
flow.ledger._tickets[no] = MemoryTicket(
|
||||
work_order_no=no,
|
||||
sales_wecom_id="s1",
|
||||
business_line="SEA",
|
||||
facts={"运输方式": "海运"},
|
||||
fingerprint="sea-deal",
|
||||
status="已报价",
|
||||
quote={"total": "USD 100", "quoteFileIssued": True},
|
||||
quote_file_ready=True,
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.offer_handoff(sender_id="s1", text=f"{no}转人工", reply=reply)
|
||||
joined = "\n".join(replies)
|
||||
self.assertEqual(phase, "handoff_offer")
|
||||
self.assertIn("运输方式:海运", joined)
|
||||
self.assertIn("是否拉产品人员进群协同", joined)
|
||||
self.assertNotIn("原有空运群", joined)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -711,5 +711,69 @@ class LandSkipAfterRestartTests(unittest.TestCase):
|
||||
self.assertIn(f"{wo}报价方案", joined)
|
||||
|
||||
|
||||
class LandContinuePullCardTests(unittest.TestCase):
|
||||
def test_tms_miss_resume_shows_pull_card(self) -> None:
|
||||
"""陆运没查到价时继续工单,要再出「是否拉产品进群」卡,不能出空运无价卡。"""
|
||||
flow = LandTextInquiryFlow(MemoryLedger())
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="land-miss",
|
||||
thread_id="land-miss",
|
||||
phase="tms_miss",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040004",
|
||||
status="询价中",
|
||||
allowed=(copy.BTN_PULL_COLLAB,),
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
extras: list[dict] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
extras.append(dict(extra) if extra else {})
|
||||
return ""
|
||||
|
||||
phase = flow.resume_historical(
|
||||
sender_id="land-miss", work_order_no="WO202610040004", reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "tms_miss")
|
||||
joined = "\n".join(replies)
|
||||
self.assertIn("是否需要拉内部产品人员进群", joined)
|
||||
self.assertIn(copy.BTN_PULL_COLLAB, joined)
|
||||
buttons = (extras[-1].get("template_card") or {}).get("button_list") or []
|
||||
self.assertTrue(any(copy.BTN_PULL_COLLAB == b.get("text") for b in buttons))
|
||||
|
||||
def test_priced_resume_shows_pull_card(self) -> None:
|
||||
"""陆运已有价、还在问要不要拉群时,继续工单要出带拉群按钮的价格卡。"""
|
||||
flow = LandTextInquiryFlow(MemoryLedger())
|
||||
flow._save(
|
||||
FlowSession(
|
||||
sender_id="land-hit",
|
||||
thread_id="land-hit",
|
||||
phase="wait_collab",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202610040004",
|
||||
status="已报价",
|
||||
quote={"total": "CNY 800", "source_label": "TMS 标准报价"},
|
||||
allowed=(copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB),
|
||||
)
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
return ""
|
||||
|
||||
phase = flow.resume_historical(
|
||||
sender_id="land-hit", work_order_no="WO202610040004", reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
joined = "\n".join(replies)
|
||||
self.assertIn("是否拉产品人员进群协同", joined)
|
||||
self.assertIn(copy.BTN_PULL_COLLAB, joined)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""系统报价单对外文件名:工单号接后台模板名,并带文件后缀。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.quote_templates.filename import filename_from_render, quote_sheet_filename
|
||||
|
||||
|
||||
class QuoteSheetFilenameTests(unittest.TestCase):
|
||||
def test_joins_work_order_and_backend_template_name(self) -> None:
|
||||
self.assertEqual(
|
||||
quote_sheet_filename("WO202609100001", "陆运国内运输拼车报价单"),
|
||||
"WO202609100001陆运国内运输拼车报价单.xlsx",
|
||||
)
|
||||
|
||||
def test_drops_suffix_on_template_name(self) -> None:
|
||||
self.assertEqual(
|
||||
quote_sheet_filename("WO202609100001", "海运报价单模板.xlsx"),
|
||||
"WO202609100001海运报价单模板.xlsx",
|
||||
)
|
||||
|
||||
def test_pdf_uses_pdf_suffix(self) -> None:
|
||||
self.assertEqual(
|
||||
quote_sheet_filename("WO202609100001", "默认空运", suffix=".pdf"),
|
||||
"WO202609100001默认空运.pdf",
|
||||
)
|
||||
|
||||
def test_render_prefers_template_name_over_old_file_name(self) -> None:
|
||||
shown = filename_from_render(
|
||||
"WO202609100001",
|
||||
{
|
||||
"templateName": "国内运输拼车报价单",
|
||||
"fileName": "WO202609100001_quote.xlsx",
|
||||
},
|
||||
)
|
||||
self.assertEqual(shown, "WO202609100001国内运输拼车报价单.xlsx")
|
||||
|
||||
def test_blank_template_keeps_xlsx_suffix(self) -> None:
|
||||
shown = filename_from_render(
|
||||
"WO202609100001",
|
||||
{"fileName": "WO202609100001_quote.xlsx"},
|
||||
)
|
||||
self.assertEqual(shown, "WO202609100001_quote.xlsx")
|
||||
@@ -2404,7 +2404,8 @@ class SeaGroupOpsTests(unittest.TestCase):
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.status, "已报价")
|
||||
self.assertTrue(ticket.attachments)
|
||||
self.assertIn("quote", str(ticket.attachments[0].get("file_name") or "").lower())
|
||||
saved_name = str(ticket.attachments[0].get("file_name") or "")
|
||||
self.assertEqual(saved_name, f"{wo}海运默认.xlsx")
|
||||
texts = [x.get("content") or "" for x in self.groups.sent if x.get("kind") == "text"]
|
||||
self.assertIn(f"工单{wo}的1个附件已保存到后台工单", texts)
|
||||
self.assertTrue(any("成交" in t for t in texts))
|
||||
|
||||
Reference in New Issue
Block a user