diff --git a/inquiry-agent/agent/policy/inquiry_copy.py b/inquiry-agent/agent/policy/inquiry_copy.py index 6b54973..78a1017 100644 --- a/inquiry-agent/agent/policy/inquiry_copy.py +++ b/inquiry-agent/agent/policy/inquiry_copy.py @@ -776,6 +776,59 @@ def quote_fee_lines(quote: dict[str, Any] | None) -> list[tuple[str, str]]: return lines +_SEA_FEE_BUCKETS = ( + ("polCostItems", "始发港费用", "始发港费用小计"), + ("oceanCostItems", "海运费", "海运费小计"), + ("podCostItems", "目的港费用", "目的港费用小计"), +) + + +def _parse_fee_bucket(quote: dict[str, Any], key: str) -> list[dict[str, Any]]: + """读 polCostItems / fee_rows_polCostItems,字符串 JSON 也认。""" + raw = quote.get(key) or quote.get("fee_rows_" + key) + if isinstance(raw, str) and raw.strip(): + try: + raw = json.loads(raw) + except json.JSONDecodeError: + return [] + if not isinstance(raw, list): + return [] + return [row for row in raw if isinstance(row, dict)] + + +def sea_bucket_fee_lines(quote: dict[str, Any] | None) -> list[str]: + """ + 海运三段正文。有任一段桶才用;否则返回空列表,调用方回退扁平 fee_lines。 + + 费目按该条币种金额原样;小计用 fee_total_* + 表头结算币种。本地不折算。 + """ + src = dict(quote or {}) + header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip() + sections: list[str] = [] + any_bucket = False + for key, title, subtotal_label in _SEA_FEE_BUCKETS: + rows = _parse_fee_bucket(src, key) + subtotal = str(src.get("fee_total_" + key) or "").strip() + if not rows and not subtotal: + continue + any_bucket = True + sections.append(title) + for row in rows: + name = str(row.get("name") or row.get("label") or row.get("feeName") or "").strip() + amount = str(row.get("amount") or row.get("value") or "").strip() + line_ccy = str(row.get("currency") or row.get("currencyCode") or header_ccy).strip() + if name and amount and amount != "-": + sections.append(f"{name}:{money_text(amount, line_ccy)}") + if subtotal and subtotal != "-": + sections.append(f"{subtotal_label}:{money_text(subtotal, header_ccy)}") + sections.append("") + if not any_bucket: + return [] + while sections and sections[-1] == "": + sections.pop() + return sections + + def quote_area_fee_text( quote: dict[str, Any] | None, business_line: str = "", @@ -789,7 +842,11 @@ def quote_area_fee_text( src = dict(quote or {}) header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip() total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy) - fee_lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)] + bucket_lines = sea_bucket_fee_lines(src) + if bucket_lines: + fee_lines = list(bucket_lines) + else: + fee_lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)] fee_lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}") return "\n".join(fee_lines) @@ -920,7 +977,14 @@ def quote_detail_lines( src = dict(quote or {}) header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip() total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy) - lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)] + bucket_lines = sea_bucket_fee_lines(src) if (business_line or "").upper() == "SEA" or ( + transport_mode or "" + ).strip() == "海运" else [] + if bucket_lines: + lines = list(bucket_lines) + else: + lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)] + lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}") lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}") lines.append(f"时效:{tms_eta_display(src)}") lines.append(f"报价有效期:{tms_valid_until_display(src)}") diff --git a/inquiry-agent/tests/test_sea_text_inquiry.py b/inquiry-agent/tests/test_sea_text_inquiry.py index 9b3aebb..6924faf 100644 --- a/inquiry-agent/tests/test_sea_text_inquiry.py +++ b/inquiry-agent/tests/test_sea_text_inquiry.py @@ -382,6 +382,33 @@ class SeaTmsCardTests(unittest.TestCase): self.assertNotIn("运输分类", ready["tms_facts"]) self.assertEqual(ready["facts"]["运输分类"], "Port to Port") + def test_sea_quote_text_uses_three_buckets(self) -> None: + quote = { + "total": "6952.77", + "currency": "CNY", + "source_label": "TMS 标准报价", + "polCostItems": [ + {"name": "操作费", "amount": "1020", "currency": "CNY"}, + {"name": "舱单", "amount": "50", "currency": "CNY"}, + ], + "oceanCostItems": [{"name": "海运费", "amount": "575", "currency": "USD"}], + "podCostItems": [ + {"name": "文件费", "amount": "450", "currency": "CNY"}, + {"name": "设备费", "amount": "30", "currency": "CNY"}, + ], + "fee_total_polCostItems": "1070", + "fee_total_oceanCostItems": "4038", + "fee_total_podCostItems": "470", + } + text = copy.sea_quote_detail_text(work_order_no="WO202609200002", quote=quote) + self.assertIn("始发港费用\n操作费:CNY 1020", text) + self.assertIn("始发港费用小计:CNY 1070", text) + self.assertIn("海运费:USD 575", text) + self.assertIn("海运费小计:CNY 4038", text) + self.assertIn("目的港费用小计:CNY 470", text) + self.assertIn("总成本价:CNY 6952.77", text) + self.assertNotIn("时效", text) + def test_long_fee_lines_go_to_text_not_clipped_card(self) -> None: """海运多项费用:完整明细必须在文本里,卡片引用条不得半截。""" long_quote = { diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/tms/InquiryTmsQueryService.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/tms/InquiryTmsQueryService.java index 1390012..cc3cc58 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/tms/InquiryTmsQueryService.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/tms/InquiryTmsQueryService.java @@ -557,6 +557,10 @@ public class InquiryTmsQueryService { appendFeeLineFromRow(buckets.get(bucket), (Map) one, headerCurrency); } } + String bucketTotal = firstText(row.get("totalCostPrice"), row.get("total_cost_price")); + if (StringUtils.isNotBlank(bucketTotal)) { + quote.put("fee_total_" + bucket, bucketTotal); + } } } for (Map.Entry>> e : buckets.entrySet()) { diff --git a/inquiry-api/jeecg-module-inquiry/src/test/java/org/jeecg/modules/inquiry/tms/TmsQuoteCurrencyTest.java b/inquiry-api/jeecg-module-inquiry/src/test/java/org/jeecg/modules/inquiry/tms/TmsQuoteCurrencyTest.java index 19f1c84..4cbda92 100644 --- a/inquiry-api/jeecg-module-inquiry/src/test/java/org/jeecg/modules/inquiry/tms/TmsQuoteCurrencyTest.java +++ b/inquiry-api/jeecg-module-inquiry/src/test/java/org/jeecg/modules/inquiry/tms/TmsQuoteCurrencyTest.java @@ -182,4 +182,42 @@ public class TmsQuoteCurrencyTest { assertTrue(quote.get("fee_lines").contains("运输费")); assertTrue(quote.get("fee_lines").contains("2800")); } + + @Test + public void toAgentQuoteKeepsSeaBucketTotals() { + Map pol = new HashMap<>(); + pol.put("feeName", "操作费"); + pol.put("amount", 1020); + pol.put("currency", "CNY"); + Map polWrap = new java.util.LinkedHashMap<>(); + polWrap.put("polCostItems", Collections.singletonList(pol)); + polWrap.put("totalCostPrice", 1070); + Map ocean = new HashMap<>(); + ocean.put("feeName", "海运费"); + ocean.put("amount", 575); + ocean.put("currency", "USD"); + Map oceanWrap = new java.util.LinkedHashMap<>(); + oceanWrap.put("oceanCostItems", Collections.singletonList(ocean)); + oceanWrap.put("totalCostPrice", 4038); + Map pod = new HashMap<>(); + pod.put("feeName", "文件费"); + pod.put("amount", 450); + pod.put("currency", "CNY"); + Map podWrap = new java.util.LinkedHashMap<>(); + podWrap.put("podCostItems", Collections.singletonList(pod)); + podWrap.put("totalCostPrice", 470); + Map data = new HashMap<>(); + data.put("quoteFound", true); + data.put("totalCostPrice", 6952.77); + data.put("currency", "CNY"); + data.put("feeItems", java.util.Arrays.asList(polWrap, oceanWrap, podWrap)); + Map tms = new HashMap<>(); + tms.put("code", 1); + tms.put("tmsData", data); + Map quote = InquiryTmsQueryService.toAgentQuote(tms); + assertEquals("1070", quote.get("fee_total_polCostItems")); + assertEquals("4038", quote.get("fee_total_oceanCostItems")); + assertEquals("470", quote.get("fee_total_podCostItems")); + assertTrue(quote.get("fee_rows_oceanCostItems").contains("575")); + } }