Files
inquiry_robot/inquiry-agent/tests/test_extract_oral.py
T
jillion886andCursor 43e633f359 修好多段含空运的群跟单、锁舱话术、相册报价通道,以及附件上传成功后电脑端关不掉 H5。
多段激活后无需再带工单号;含空运群回复走机器人;复制补问清单时空货好时间不再吃成下一行编号。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-08 15:23:06 +08:00

653 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
口述空运抽字段:解析 DeepSeek B JSON,不打外网。
"""
from __future__ import annotations
import os
import sys
import unittest
from types import SimpleNamespace
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
os.environ.setdefault("LEDGER_BACKEND", "memory")
from agent.llm.extract_text import (
detect_transport_mode,
extract_inquiry_snapshot,
parse_labeled_facts,
)
from agent.schema.field_validate import normalize_facts
from agent.llm.mode_extract_fields import invoke_shell, parse_extract_payload
from agent.policy.air_text_flow import AirTextInquiryFlow
from agent.ledger.memory_ledger import MemoryLedger
ORAL = "上海到洛杉矶空运,3件 180kg,体积2.5,普货纸箱"
B_JSON = {
"transport_mode": "AIR",
"fields": {
"起运港": "上海",
"目的港": "洛杉矶",
"品名": "普货",
"件数": "3",
"毛重": "180",
"体积": "2.5",
"包装方式": "纸箱",
"报价日期": "",
},
}
class OralExtractTests(unittest.TestCase):
def test_parse_b_json(self) -> None:
parsed = parse_extract_payload(B_JSON)
self.assertTrue(parsed["ok"])
self.assertEqual(parsed["business_line"], "AIR")
self.assertEqual(parsed["facts"]["起运港"], "上海")
self.assertNotIn("报价日期", parsed["facts"])
def test_invoke_shell_with_json(self) -> None:
out = invoke_shell(payload={"assistant_json": B_JSON})
self.assertTrue(out["ok"])
self.assertEqual(out["facts"]["目的港"], "洛杉矶")
def test_invoke_shell_without_json_keeps_legacy(self) -> None:
out = invoke_shell(payload={"a": 1})
self.assertEqual(out.get("error"), "extract_fields_shell_not_implemented")
def test_city_pair_without_mode_does_not_default_air(self) -> None:
"""没写运输方式时,模型猜的空运不能当成已选。港口和日期仍留下。"""
text = "查一下10月9日郑州-吉隆坡的报价"
guessed = parse_extract_payload(
{
"transport_mode": "AIR",
"fields": {
"起运港": "郑州",
"目的港": "吉隆坡",
"报价日期": "2026-10-09",
},
}
)
snap = extract_inquiry_snapshot(text, b_result=guessed)
self.assertEqual(snap["business_line"], "")
self.assertEqual(snap["facts"].get("起运港"), "郑州")
self.assertEqual(snap["facts"].get("目的港"), "吉隆坡")
self.assertEqual(snap["facts"].get("报价日期"), "2026-10-09")
flow = AirTextInquiryFlow(MemoryLedger())
replies: list[str] = []
phase = flow.on_text(
sender_id="u-mode",
text=text,
reply=lambda body, extra=None: replies.append(body),
b_result=guessed,
allow_b=False,
)
self.assertEqual(phase, "need_mode")
self.assertTrue(any("告知我询价的运输方式" in x for x in replies))
self.assertFalse(any(x.startswith("运输方式:空运") or "运输方式:空运" in x for x in replies))
def test_oral_snapshot_uses_b_not_regex(self) -> None:
snap = extract_inquiry_snapshot(ORAL, b_result=parse_extract_payload(B_JSON))
self.assertEqual(snap["business_line"], "AIR")
self.assertEqual(snap["facts"]["件数"], "3件")
self.assertEqual(snap["source"], "deepseek_b")
def test_oral_without_b_does_not_guess_ports(self) -> None:
snap = extract_inquiry_snapshot(ORAL)
self.assertEqual(snap["business_line"], "AIR")
self.assertFalse(snap["facts"].get("起运港"))
def test_bare_dap_is_trade_term(self) -> None:
"""句尾单独的 DAP 记成贸易条款,不依赖模型有没有这个字段。"""
snap = extract_inquiry_snapshot("查一下空运,出货日期9月30日广州到香港的普货价格,DAP")
self.assertEqual(snap["business_line"], "AIR")
self.assertEqual(snap["facts"].get("贸易条款"), "DAP")
def test_trade_term_example_line_is_not_filled(self) -> None:
snap = extract_inquiry_snapshot("贸易条款(非必填):\n(如:DDP、DAP、CIF、CIP)")
self.assertFalse(snap["facts"].get("贸易条款"))
labeled = extract_inquiry_snapshot("贸易条款:CIF\n(如:DDP、DAP、CIF、CIP)")
self.assertEqual(labeled["facts"].get("贸易条款"), "CIF")
def test_labeled_customs_port(self) -> None:
self.assertEqual(parse_labeled_facts("通关口岸:皇岗").get("通关口岸"), "皇岗")
snap = extract_inquiry_snapshot("通关口岸(非必填):皇岗")
self.assertEqual(snap["facts"].get("通关口岸"), "皇岗")
snap2 = extract_inquiry_snapshot("通关口岸皇岗")
self.assertEqual(snap2["facts"].get("通关口岸"), "皇岗")
def test_b_keeps_land_customs_port(self) -> None:
parsed = parse_extract_payload(
{
"transport_mode": "LAND",
"fields": {"起运港": "广州", "通关口岸": "皇岗"},
}
)
self.assertEqual(parsed["facts"].get("通关口岸"), "皇岗")
def test_assemble_air_query_zhuhai_clark(self) -> None:
from agent.schema.tms_air_query import assemble_air_query
out = assemble_air_query(
{
"起运港": "珠海",
"目的港": "克拉克",
"品名": "茶叶",
"件数": "10件",
"毛重": "100KGS",
"体积": "2CBM",
"包装方式": "托盘",
"报价日期": "2026-09-12",
}
)
self.assertTrue(out["ok"])
self.assertEqual(out["facts"]["originCode"], "ZUH")
self.assertEqual(out["facts"]["destinationCode"], "CRK")
self.assertEqual(out["facts"]["packageType"], "托盘")
self.assertEqual(out["facts"]["件数"], "10件")
bulk = assemble_air_query(
{
"起运港": "珠海",
"目的港": "克拉克",
"品名": "茶叶",
"件数": "10",
"毛重": "100",
"体积": "2",
"包装方式": "散货",
"报价日期": "2026-09-12",
}
)
self.assertTrue(bulk["ok"])
self.assertEqual(bulk["facts"]["包装方式"], "散货")
self.assertNotIn("包装方式", bulk["missing"])
def test_place_stops_before_cargo_and_price(self) -> None:
"""目的地只留地名。后面的品名、包装、的价格不能粘在地名上。"""
from agent.schema.field_validate import validate_required_fields
check = validate_required_fields(
facts={
"目的港": "吉隆坡,普货,散货的价格",
"品名": "普货",
"包装方式": "散货",
"报价日期": "2026-09-29",
},
business_line="AIR",
)
self.assertEqual(check["facts"]["目的港"], "吉隆坡")
self.assertEqual(check["facts"]["品名"], "普货")
self.assertEqual(check["facts"]["包装方式"], "散货")
bare = validate_required_fields(
facts={"目的港": "洛杉矶,茶叶,纸箱的价格"},
business_line="AIR",
)
self.assertEqual(bare["facts"]["目的港"], "洛杉矶")
self.assertEqual(bare["facts"]["品名"], "茶叶")
self.assertEqual(bare["facts"]["包装方式"], "纸箱")
def test_fly_route_overwrites_stale_nkg_kul(self) -> None:
from agent.schema.field_validate import harvest_oral_measures
from agent.schema.tms_air_query import assemble_air_query
from agent.policy import inquiry_copy as copy
first = harvest_oral_measures(
"从南京飞吉隆坡,9月14日茶叶,托盘,4.2cbm,1.5KG",
{},
)
self.assertEqual(first["起运港"], "南京")
self.assertEqual(first["目的港"], "吉隆坡")
assembled = assemble_air_query(
{**first, "originCode": "NKG", "destinationCode": "KUL", "件数": "10"}
)
card = copy.inquiry_card(work_order_no="WO1", facts=assembled["facts"])
self.assertIn("起运地:南京", card)
self.assertIn("目的地:吉隆坡", card)
self.assertNotIn("起运地:NKG", card)
self.assertNotIn("目的地:KUL", card)
second = harvest_oral_measures(
"北京飞吉隆坡,9月14日茶叶,4.2cbm,1.5KG",
{**assembled["facts"]},
)
again = assemble_air_query(second)
self.assertEqual(again["facts"]["起运港"], "北京")
self.assertEqual(again["facts"]["originCode"], "PEK")
self.assertEqual(again["facts"]["destinationCode"], "KUL")
shown = copy.inquiry_card(work_order_no="WO2", facts=again["facts"])
self.assertIn("起运地:北京", shown)
self.assertNotIn("起运地:NKG", shown)
self.assertNotIn("起运地:南京", shown)
def test_iata_pair_kept_not_rewritten_to_city(self) -> None:
from agent.schema.field_validate import harvest_oral_measures
from agent.schema.tms_air_query import assemble_air_query
from agent.policy import inquiry_copy as copy
oral = "空运 SZX-CGK 发光二级管组件 11P 2400KG 20cbm"
facts = harvest_oral_measures(
oral,
{"起运港": "深圳", "目的港": "雅加达", "品名": "发光二级管组件"},
)
self.assertEqual(facts["起运港"], "SZX")
self.assertEqual(facts["目的港"], "CGK")
assembled = assemble_air_query(
{
**facts,
"件数": "11P",
"毛重": "2400KG",
"体积": "20cbm",
"包装方式": "托盘",
"报价日期": "2026-09-18",
}
)
self.assertEqual(assembled["facts"]["originCode"], "SZX")
self.assertEqual(assembled["facts"]["destinationCode"], "CGK")
card = copy.inquiry_card(work_order_no="WO1", facts=assembled["facts"])
self.assertIn("起运地:SZX", card)
self.assertIn("目的地:CGK", card)
self.assertNotIn("深圳", card)
self.assertNotIn("雅加达", card)
def test_currency_pair_is_not_air_route(self) -> None:
from agent.schema.field_validate import harvest_oral_measures
facts = harvest_oral_measures(
"空运 货值 USD-CNY 茶叶",
{"起运港": "上海", "目的港": "洛杉矶"},
)
self.assertEqual(facts["起运港"], "上海")
self.assertEqual(facts["目的港"], "洛杉矶")
def test_oral_pack_and_quote_date_from_current_sentence(self) -> None:
from agent.schema.field_validate import harvest_oral_measures
first = harvest_oral_measures(
"南京飞吉隆坡,9月15日茶叶,托盘,4.2cbm,1.5KG",
{},
)
self.assertEqual(first["包装方式"], "托盘")
self.assertEqual(first["报价日期"], "2026-09-15")
self.assertEqual(first["毛重"], "1.5KG")
self.assertEqual(first["体积"], "4.2cbm")
second = harvest_oral_measures(
"京飞吉隆坡,9月16日茶叶,散货,5.2cbm,2.5KG",
first,
)
self.assertEqual(second["包装方式"], "散货")
self.assertEqual(second["报价日期"], "2026-09-16")
self.assertEqual(second["毛重"], "2.5KG")
self.assertEqual(second["体积"], "5.2cbm")
def test_clarify_replaces_pallet_with_bulk_and_keeps_quote_date(self) -> None:
replies: list[str] = []
def reply(text: str, extra=None) -> None:
replies.append(text)
_ = extra
flow = AirTextInquiryFlow(MemoryLedger())
flow.on_text(
sender_id="u_date",
text="南京飞吉隆坡,9月15日茶叶,托盘,4.2cbm,1.5KG",
reply=reply,
injected_facts={
"起运港": "南京",
"目的港": "吉隆坡",
"品名": "茶叶",
"包装方式": "托盘",
},
injected_mode="AIR",
)
self.assertEqual(flow.session_of("u_date").facts["包装方式"], "托盘")
self.assertEqual(flow.session_of("u_date").facts["报价日期"], "2026-09-15")
flow.on_text(
sender_id="u_date",
text="京飞吉隆坡,9月16日茶叶,散货,5.2cbm,2.5KG",
reply=reply,
injected_facts={
"起运港": "南京",
"目的港": "吉隆坡",
"品名": "茶叶",
},
injected_mode="AIR",
)
sess = flow.session_of("u_date")
self.assertEqual(sess.facts["包装方式"], "散货")
self.assertEqual(sess.facts["报价日期"], "2026-09-16")
last = replies[-1]
self.assertIn("包装类型:散货", last)
self.assertIn("报价日期:2026-09-16", last)
def test_weight_keeps_user_kg_not_kgs(self) -> None:
from agent.policy import inquiry_copy as copy
from agent.schema.field_validate import harvest_oral_measures
facts = harvest_oral_measures("从南京飞吉隆坡,茶叶,1.5KG,4.2cbm,托盘", {})
self.assertEqual(facts["毛重"], "1.5KG")
shown = copy.inquiry_card(
work_order_no="WO1",
facts={**facts, "起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶", "件数": "10"},
)
self.assertIn("重量(KG):1.5KG", shown)
self.assertNotIn("1.5KGS", shown)
def test_dimension_text_is_volume_without_multiplying(self) -> None:
from agent.schema.field_validate import harvest_oral_measures
facts = harvest_oral_measures("茶叶,1.1*2.1*2.2,11kg,21件", {})
self.assertEqual(facts["体积"], "1.1*2.1*2.2")
self.assertEqual(facts["毛重"], "11kg")
self.assertEqual(facts["件数"], "21件")
def test_oral_units_fill_weight_volume_without_b(self) -> None:
snap = extract_inquiry_snapshot(
"珠海到克拉克空运,茶叶 10件 100KGS 2CBM 托盘"
)
self.assertEqual(snap["facts"]["件数"], "10件")
self.assertEqual(snap["facts"]["毛重"], "100KGS")
self.assertEqual(snap["facts"]["体积"], "2CBM")
self.assertFalse(snap["facts"].get("起运港"))
def test_labeled_weight_alias(self) -> None:
from agent.llm.extract_text import parse_labeled_facts
from agent.schema.field_validate import normalize_facts
facts = parse_labeled_facts("重量(KG):180\n体积(CBM):2.5")
norm = normalize_facts(facts)
self.assertEqual(norm["毛重"], "180")
self.assertEqual(norm["体积"], "2.5")
def test_extract_prompt_keeps_iata(self) -> None:
from agent.llm.mode_extract_fields import build_extract_messages
blob = str(build_extract_messages("空运 SZX-CGK"))
self.assertIn("SZX-CGK", blob)
self.assertIn("禁止翻译成城市名", blob)
def test_chat_fn_fixture_drives_flow(self) -> None:
def chat_fn(**kwargs):
return SimpleNamespace(
ok=True,
content="",
error="",
tool_calls=[
{
"function": {
"name": "extract_inquiry_fields",
"arguments": __import__("json").dumps(B_JSON, ensure_ascii=False),
}
}
],
)
replies: list[str] = []
def reply(text: str, extra=None) -> None:
replies.append(text)
_ = extra
flow = AirTextInquiryFlow(MemoryLedger())
phase = flow.on_text(
sender_id="oral1",
text=ORAL,
reply=reply,
chat_fn=chat_fn,
)
if phase == "wait_confirm":
phase = flow.on_text(sender_id="oral1", text="确定", reply=reply)
self.assertEqual(phase, "wait_collab")
joined = "\n".join(replies)
self.assertIn("工单号:", joined)
self.assertIn("起运地:上海", joined)
self.assertIn("目的地:洛杉矶", joined)
self.assertNotIn("【询价卡片】", joined)
def test_mode_index_maps_land_sea_air(self) -> None:
self.assertEqual(detect_transport_mode("1"), "LAND")
self.assertEqual(detect_transport_mode("2"), "SEA")
self.assertEqual(detect_transport_mode("3"), "AIR")
self.assertEqual(detect_transport_mode("1.陆运"), "LAND")
self.assertEqual(detect_transport_mode("空运"), "AIR")
self.assertEqual(detect_transport_mode("10件"), "")
self.assertEqual(detect_transport_mode("1.件数(必填):10"), "")
class ClarifyPasteTests(unittest.TestCase):
def test_paste_numbered_required_lines_and_skip_examples(self) -> None:
"""复制补问清单:只收冒号后的值,示例行和空项都不算填了。"""
text = """
1.件数(必填):10
(如:10)
2.重量(KG)(必填):10
(如:100)
3.体积(CBM)(必填):10
(如:2)
4.包装类型(必填):
(如:托盘、散货)
"""
facts = normalize_facts(parse_labeled_facts(text))
self.assertEqual(facts.get("件数"), "10")
self.assertEqual(facts.get("毛重"), "10")
self.assertEqual(facts.get("体积"), "10")
self.assertNotIn("包装方式", facts)
self.assertNotEqual(facts.get("包装方式"), "托盘、散货")
def test_blank_cargo_ready_does_not_take_next_item_number(self) -> None:
"""货好时间空着时,下一行「4.提货地址」的编号不能写进货好时间。示例行也不算填了。"""
text = """
1.包装类型(必填):托盘
(如:托盘、散货)
2.货源地(非必填):
(如:东莞)
3.货好时间(非必填):
(如:随时可提)
4.提货地址(非必填):
(如:东莞仓库)
5.收货地址(非必填):
(如:马尼拉)
6.贸易条款(非必填):
(如:DDP、DAP、CIF、CIP)
"""
facts = extract_inquiry_snapshot(text, allow_b=False)["facts"]
self.assertEqual(facts.get("包装方式") or facts.get("包装类型"), "托盘")
self.assertNotIn("货好时间", facts)
self.assertNotIn("提货地址", facts)
self.assertNotIn("货源地", facts)
self.assertNotIn("贸易条款", facts)
class ImageOptionalClipTests(unittest.TestCase):
def test_numbered_extra_lines_do_not_enter_fields(self) -> None:
oral = """
运输方式:空运
目的地:印尼巴淡
件数:1PLT
重量:55.8KGS
体积:1.23CBM
包装类型:托盘
报价日期:2026-09-23
起运地:深圳坪山保税仓
货好时间:今天货好
4. 希望送到目的地工厂时间: 尽快
5.
提货地址:深圳市坪山区龙田街道大工业区绿荫北路7号601、
602、603 (保税仓库)
6. 送货地址: PT SAT NUSAPERSADA TBK----JL. PELITA VI NO.99
"""
snap = extract_inquiry_snapshot(oral, allow_b=False)
facts = snap["facts"]
self.assertEqual(facts.get("货好时间"), "今天货好")
self.assertIn("坪山区", facts.get("提货地址") or "")
self.assertNotIn("希望送到", facts.get("货好时间") or "")
self.assertNotIn("希望送到", facts.get("提货地址") or "")
self.assertNotIn("送货地址", facts.get("提货地址") or "")
self.assertIn("PT SAT", facts.get("收货地址") or "")
from agent.policy import inquiry_copy as copy
card = copy.ask_clarify(
facts=facts,
missing_keys=["品名"],
transport_mode="空运",
)
self.assertNotIn("希望送到", card)
self.assertNotIn("4. 希望送到", card)
self.assertIn("收货地址", card)
# 测服手工海运报价单:表头和格子在同一行,还带 POL/POD/Terms。
SEA_SHEET_ORAL = """
起运港/POL(有指定起运港口的必填) 纽约港
*品名/Commodity: 机器设备 目的港/POD(有指定目的港口的必填) 巴生西港
*货物数量/Quantity (或提供装箱单/Packing List) 18
整柜或拼柜:FCL
箱型箱量:40HQ*1
贸易条款/Terms: CIF 要求运输时效/Requested Transit Time
运输分类:其他
报价日期:2026-09-07
"""
class SeaSheetHeaderClipTests(unittest.TestCase):
def test_quote_sheet_headers_do_not_stick_to_values(self) -> None:
"""报价单表头不能进补问:起运港只要纽约港,贸易条款只要 CIF。"""
snap = extract_inquiry_snapshot(SEA_SHEET_ORAL, allow_b=False)
facts = snap["facts"]
self.assertEqual(facts.get("起运港"), "纽约港")
self.assertEqual(facts.get("目的港"), "巴生西港")
self.assertEqual(facts.get("品名"), "机器设备")
self.assertEqual(str(facts.get("货量") or facts.get("货物数量") or ""), "18")
self.assertEqual(facts.get("整柜或拼柜"), "FCL")
self.assertEqual(facts.get("箱型箱量"), "40HQ*1")
self.assertEqual(facts.get("贸易条款"), "CIF")
self.assertNotIn("必填", facts.get("起运港") or "")
self.assertNotIn("Terms", facts.get("贸易条款") or "")
self.assertNotIn("Commodity", facts.get("品名") or "")
self.assertNotIn("时效", facts.get("贸易条款") or "")
from agent.policy import inquiry_copy as copy
card = "\n".join(
copy.inquiry_field_lines(
work_order_no="",
facts=facts,
transport_mode="海运",
)
)
self.assertIn("纽约港", card)
self.assertNotIn("口的必填", card)
self.assertNotIn("Commodity", card)
self.assertNotIn("Requested Transit", card)
def test_next_column_header_is_not_the_value(self) -> None:
"""
手工海运单同一行是「字段、格子、下一列表头」。
目的港只要巴生西港,不能收成费用汇总/POD Local Charges。
"""
oral = """
起运港/POL(有指定起运港口的必填) 纽约港 目的港/POD(有指定目的港口的必填) 巴生西港 费用汇总/POD Local Charges
*品名/Commodity: 机器设备 *货物数量/Quantity (或提供装箱单/Packing List) 18 提货地址/Place of Receipt
整柜或拼柜:FCL 箱型箱量:40HQ*1 送货地址/Place of Delivery
贸易条款/Terms: CIF 要求货物抵达时间/Requested Arrival Time
运输分类:其他
报价日期:2026-09-07
"""
facts = extract_inquiry_snapshot(oral, allow_b=False)["facts"]
self.assertEqual(facts.get("起运港"), "纽约港")
self.assertEqual(facts.get("目的港"), "巴生西港")
self.assertEqual(facts.get("品名"), "机器设备")
self.assertEqual(str(facts.get("货量") or facts.get("货物数量") or ""), "18")
self.assertEqual(facts.get("整柜或拼柜"), "FCL")
self.assertEqual(facts.get("箱型箱量"), "40HQ*1")
self.assertEqual(facts.get("贸易条款"), "CIF")
self.assertNotIn("费用汇总", facts.get("目的港") or "")
self.assertNotIn("提货地址", facts.get("货量") or facts.get("货物数量") or "")
self.assertNotIn("送货地址", facts.get("整柜或拼柜") or "")
self.assertNotIn("抵达", facts.get("箱型箱量") or "")
def test_quote_date_synonyms_for_sea_land_air(self) -> None:
"""询价/走货/发货/出货/提货的日期或时间,海运陆运空运都记成报价日期。"""
from agent.schema.field_validate import QUOTE_DATE_SYNONYMS, normalize_facts
modes = (("海运", "SEA"), ("陆运", "LAND"), ("空运", "AIR"))
for label in QUOTE_DATE_SYNONYMS:
self.assertEqual(normalize_facts({label: "2026-09-23"})["报价日期"], "2026-09-23")
for word, mode in modes:
snap = extract_inquiry_snapshot(f"{word}\n{label}:2026-09-23", allow_b=False)
self.assertEqual(snap["business_line"], mode, label)
self.assertEqual(snap["facts"].get("报价日期"), "2026-09-23", label)
self.assertNotIn(label, snap["facts"])
def test_quote_date_synonym_does_not_steal_ready_or_pickup(self) -> None:
"""货好时间、提货地址仍是原字段;同义词日期不被前面的日期抢走。"""
snap = extract_inquiry_snapshot(
"海运\n货好时间:9月20日\n提货地址:上海仓库\n询价日期:2026-09-15",
allow_b=False,
)
facts = snap["facts"]
self.assertEqual(facts.get("报价日期"), "2026-09-15")
self.assertEqual(facts.get("货好时间"), "9月20日")
self.assertEqual(facts.get("提货地址"), "上海仓库")
oral = extract_inquiry_snapshot("空运 发货日期9月15日", allow_b=False)
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()