@@ -776,6 +776,59 @@ def quote_fee_lines(quote: dict[str, Any] | None) -> list[tuple[str, str]]:
|
|||||||
return lines
|
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(
|
def quote_area_fee_text(
|
||||||
quote: dict[str, Any] | None,
|
quote: dict[str, Any] | None,
|
||||||
business_line: str = "",
|
business_line: str = "",
|
||||||
@@ -789,6 +842,10 @@ def quote_area_fee_text(
|
|||||||
src = dict(quote or {})
|
src = dict(quote or {})
|
||||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||||
|
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 = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||||
fee_lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
fee_lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||||
return "\n".join(fee_lines)
|
return "\n".join(fee_lines)
|
||||||
@@ -920,8 +977,15 @@ def quote_detail_lines(
|
|||||||
src = dict(quote or {})
|
src = dict(quote or {})
|
||||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||||
|
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 = [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"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||||
lines.append(f"时效:{tms_eta_display(src)}")
|
lines.append(f"时效:{tms_eta_display(src)}")
|
||||||
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -382,6 +382,33 @@ class SeaTmsCardTests(unittest.TestCase):
|
|||||||
self.assertNotIn("运输分类", ready["tms_facts"])
|
self.assertNotIn("运输分类", ready["tms_facts"])
|
||||||
self.assertEqual(ready["facts"]["运输分类"], "Port to Port")
|
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:
|
def test_long_fee_lines_go_to_text_not_clipped_card(self) -> None:
|
||||||
"""海运多项费用:完整明细必须在文本里,卡片引用条不得半截。"""
|
"""海运多项费用:完整明细必须在文本里,卡片引用条不得半截。"""
|
||||||
long_quote = {
|
long_quote = {
|
||||||
|
|||||||
+4
@@ -557,6 +557,10 @@ public class InquiryTmsQueryService {
|
|||||||
appendFeeLineFromRow(buckets.get(bucket), (Map<?, ?>) one, headerCurrency);
|
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<String, List<Map<String, String>>> e : buckets.entrySet()) {
|
for (Map.Entry<String, List<Map<String, String>>> e : buckets.entrySet()) {
|
||||||
|
|||||||
+38
@@ -182,4 +182,42 @@ public class TmsQuoteCurrencyTest {
|
|||||||
assertTrue(quote.get("fee_lines").contains("运输费"));
|
assertTrue(quote.get("fee_lines").contains("运输费"));
|
||||||
assertTrue(quote.get("fee_lines").contains("2800"));
|
assertTrue(quote.get("fee_lines").contains("2800"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void toAgentQuoteKeepsSeaBucketTotals() {
|
||||||
|
Map<String, Object> pol = new HashMap<>();
|
||||||
|
pol.put("feeName", "操作费");
|
||||||
|
pol.put("amount", 1020);
|
||||||
|
pol.put("currency", "CNY");
|
||||||
|
Map<String, Object> polWrap = new java.util.LinkedHashMap<>();
|
||||||
|
polWrap.put("polCostItems", Collections.singletonList(pol));
|
||||||
|
polWrap.put("totalCostPrice", 1070);
|
||||||
|
Map<String, Object> ocean = new HashMap<>();
|
||||||
|
ocean.put("feeName", "海运费");
|
||||||
|
ocean.put("amount", 575);
|
||||||
|
ocean.put("currency", "USD");
|
||||||
|
Map<String, Object> oceanWrap = new java.util.LinkedHashMap<>();
|
||||||
|
oceanWrap.put("oceanCostItems", Collections.singletonList(ocean));
|
||||||
|
oceanWrap.put("totalCostPrice", 4038);
|
||||||
|
Map<String, Object> pod = new HashMap<>();
|
||||||
|
pod.put("feeName", "文件费");
|
||||||
|
pod.put("amount", 450);
|
||||||
|
pod.put("currency", "CNY");
|
||||||
|
Map<String, Object> podWrap = new java.util.LinkedHashMap<>();
|
||||||
|
podWrap.put("podCostItems", Collections.singletonList(pod));
|
||||||
|
podWrap.put("totalCostPrice", 470);
|
||||||
|
Map<String, Object> 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<String, Object> tms = new HashMap<>();
|
||||||
|
tms.put("code", 1);
|
||||||
|
tms.put("tmsData", data);
|
||||||
|
Map<String, String> 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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user