diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryQuoteTemplateServiceImpl.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryQuoteTemplateServiceImpl.java index 990241e..80c56d6 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryQuoteTemplateServiceImpl.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryQuoteTemplateServiceImpl.java @@ -21,6 +21,7 @@ import org.jeecg.modules.inquiry.template.QuoteTemplateExcelSupport; import org.jeecg.modules.inquiry.template.QuoteTemplateMatcher; import org.jeecg.modules.inquiry.template.QuoteTemplateObjectStore; import org.jeecg.modules.inquiry.template.QuoteTemplateSnapshotRenderer; +import org.jeecg.modules.inquiry.tms.InquiryTmsQueryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -885,6 +886,32 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl> lineMaps = new ArrayList<>(); + for (int i = 0; i < rows.size(); i++) { + JSONObject one = rows.getJSONObject(i); + if (one == null) { + continue; + } + Map m = new LinkedHashMap<>(); + for (String k : one.keySet()) { + Object v = one.get(k); + if (v != null) { + m.put(k, String.valueOf(v)); + } + } + lineMaps.add(m); + } + Map enrichCtx = new LinkedHashMap<>(); + enrichCtx.put("chargeableWeightKg", chargeable); + InquiryTmsQueryService.enrichAirWeightFeeLines(lineMaps, enrichCtx); + JSONArray enriched = new JSONArray(); + for (Map m : lineMaps) { + enriched.add(new JSONObject(m)); + } + rows = enriched; + } for (int i = 0; i < rows.size(); i++) { JSONObject one = rows.getJSONObject(i); if (one == null) { diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/template/QuoteTemplateExcelSupport.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/template/QuoteTemplateExcelSupport.java index cb4e064..91a9d5e 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/template/QuoteTemplateExcelSupport.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/template/QuoteTemplateExcelSupport.java @@ -51,6 +51,9 @@ public final class QuoteTemplateExcelSupport { FILL_BY_LABEL.put("有效期至", "valid_until"); FILL_BY_LABEL.put("费用总计", "total_amount"); FILL_BY_LABEL.put("报价合计", "total_amount"); + FILL_BY_LABEL.put("预估费用", "total_amount"); + FILL_BY_LABEL.put("预估总价", "total_amount"); + FILL_BY_LABEL.put("总销售价", "total_amount"); FILL_BY_LABEL.put("RFQ No", "quote_no"); FILL_BY_LABEL.put("报价单编号", "quote_no"); FILL_BY_LABEL.put("报价编号", "quote_no"); @@ -592,6 +595,9 @@ public final class QuoteTemplateExcelSupport { // 模板「预估总价」是 SUM。POI 写完数字不会重算,缓存仍是 0。 // 企微/WPS 预览常不重算,销售会看到汇总为 0。这里把公式缓存刷成当前值。 refreshFormulas(wb); + // 刷公式后:预估费用若 SUM 了「发生时收取」固定费,会与 TMS 总销售价不一致。 + // 有 total_amount 时再按标签写回,以总销售价为准。 + writeFactsByLabel(wb, facts, yellow); // 转 PDF 前把纸张改成 A4 居中,避免顶部裁字、整页偏左。 applyQuotePrintLayout(wb); Files.createDirectories(targetXlsx.getParent()); @@ -740,6 +746,8 @@ public final class QuoteTemplateExcelSupport { sheet = wb.getSheetAt(0); } int startRow = resolveFeeDataStartRow(sheet, region, anchorRef.row); + // 映射缺单位/单价/数量列时,按表头补齐,否则明细有值也写不进 Excel + columns = mergeFeeColumnsFromHeader(sheet, startRow, columns); // 数据起点整行已标黄:本段是模板固定占位(如陆运「项目二」),整段跳过 if (rowLooksYellowFixed(sheet, startRow)) { log.info("费用区起点为黄底固定区,跳过赋值 region={} startRow={}", region.getString("id"), startRow); @@ -1446,6 +1454,40 @@ public final class QuoteTemplateExcelSupport { applyRemarkWrap(sheet, row, cell, value); } + /** + * AI mapping 常只标 name/amount/currency;空运表头有「单位/单价/数量」时现场补列字母。 + * 不覆盖 mapping 已有列,避免改错已对齐的金额列。 + */ + static JSONObject mergeFeeColumnsFromHeader(Sheet sheet, int dataStartRow, JSONObject mapped) { + JSONObject out = mapped == null ? new JSONObject(true) : new JSONObject(mapped); + if (sheet == null) { + return out; + } + boolean needUnit = StringUtils.isBlank(out.getString("unit")); + boolean needPrice = StringUtils.isBlank(out.getString("unit_price")); + boolean needQty = StringUtils.isBlank(out.getString("quantity")); + if (!needUnit && !needPrice && !needQty) { + return out; + } + Map header = findFeeTableColumnsAbove(sheet, Math.max(dataStartRow, 0)); + if (header == null || header.isEmpty()) { + header = findFeeTableColumns(sheet); + } + if (header == null || header.isEmpty()) { + return out; + } + if (needUnit && header.get("unit") != null) { + out.put("unit", CellReference.convertNumToColString(header.get("unit"))); + } + if (needPrice && header.get("unit_price") != null) { + out.put("unit_price", CellReference.convertNumToColString(header.get("unit_price"))); + } + if (needQty && header.get("quantity") != null) { + out.put("quantity", CellReference.convertNumToColString(header.get("quantity"))); + } + return out; + } + /** * 金额/数量/单价写成数字,SUM 才能加算;名称等仍写文本。 */ 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 c3ea94d..5895ebe 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 @@ -536,9 +536,81 @@ public class InquiryTmsQueryService { addFeeLine(lines, "目的地杂费", firstText(data.get("destFee"), data.get("destinationFee")), headerCurrency, null); addFeeLine(lines, "其他附加费", firstText(other, data.get("otherFee"), data.get("sundryFee")), headerCurrency, null); } + // 空运按计费重计价:TMS 常把单价写在 salePrice,单位/数量/单价缺省;补齐后再算行金额 + enrichAirWeightFeeLines(lines, data); return JSON.toJSONString(lines); } + /** + * 空运运输费:单价 × 计费重 = 行金额。 + *

+ * 现象:feeItems 只给 salePrice=11(实为单价),Excel 金额列写成 11,单位/单价/数量空, + * 预估费用 SUM 成 140,而总销售价是 11×计费重。 + * 只处理按重计的运输/空运费,提送货费、人工费等固定费不动。 + */ + public static void enrichAirWeightFeeLines(List> lines, Map data) { + if (lines == null || lines.isEmpty() || data == null) { + return; + } + String chargeable = firstText(data.get("chargeableWeightKg"), data.get("chargeable_weight_kg")); + if (StringUtils.isBlank(chargeable) || "-".equals(chargeable)) { + return; + } + for (Map line : lines) { + if (line == null || !isAirWeightFeeName(line.get("name"))) { + continue; + } + String unitPrice = firstText(line.get("unit_price"), line.get("unitPrice")); + String qty = firstText(line.get("quantity")); + String amount = firstText(line.get("amount")); + String unit = firstText(line.get("unit")); + if (StringUtils.isBlank(qty)) { + line.put("quantity", chargeable); + qty = chargeable; + } + if (StringUtils.isBlank(unit)) { + line.put("unit", "KG"); + } + // 有数量、无单价:当前 amount 当作单价(TMS 把费率塞进 salePrice/amount) + if (StringUtils.isBlank(unitPrice) && StringUtils.isNotBlank(amount)) { + line.put("unit_price", amount); + unitPrice = amount; + } + // 单价与金额相同(或金额空):按 单价×数量 重算行金额 + if (StringUtils.isNotBlank(unitPrice) && StringUtils.isNotBlank(qty) + && (StringUtils.isBlank(amount) || amount.equals(unitPrice))) { + String product = multiplyDecimal(unitPrice, qty); + if (StringUtils.isNotBlank(product)) { + line.put("amount", product); + } + } + } + } + + /** 空运按计费重的费目:运输费 / 空运费;不含提送货、人工、报关等固定费。 */ + public static boolean isAirWeightFeeName(String name) { + String n = StringUtils.trimToEmpty(name); + if (n.isEmpty()) { + return false; + } + if (n.contains("提") || n.contains("送") || n.contains("人工") || n.contains("报关") + || n.contains("操作") || n.contains("文件") || n.contains("杂费")) { + return false; + } + return n.contains("空运") || n.contains("运输费") || "运费".equals(n); + } + + static String multiplyDecimal(String left, String right) { + try { + java.math.BigDecimal a = new java.math.BigDecimal(left.trim().replace(",", "")); + java.math.BigDecimal b = new java.math.BigDecimal(right.trim().replace(",", "")); + java.math.BigDecimal product = a.multiply(b); + return product.stripTrailingZeros().toPlainString(); + } catch (Exception e) { + return ""; + } + } + /** * 海运三段费用桶单独落 quote,供模板 feeRegions 按 tmsSource 分区写,禁止三段共用扁平 fee_lines。 */ @@ -589,8 +661,18 @@ public class InquiryTmsQueryService { private static void appendFeeLineFromRow(List> lines, Map row, String headerCurrency) { String name = firstText(row.get("feeName"), row.get("name"), row.get("chargeName"), row.get("itemName"), row.get("feeType")); - String amount = firstText(row.get("salePrice"), row.get("amount"), row.get("price"), - row.get("feeAmount"), row.get("totalPrice")); + String unitPrice = firstText(row.get("unitPrice"), row.get("unit_price")); + // 行金额优先认 amount/totalPrice;salePrice 与单价相同时当作费率,不当行金额 + String amount = firstText(row.get("amount"), row.get("totalPrice"), row.get("feeAmount"), + row.get("price")); + String sale = firstText(row.get("salePrice")); + if (StringUtils.isBlank(amount)) { + if (StringUtils.isNotBlank(sale) && StringUtils.isNotBlank(unitPrice) && sale.equals(unitPrice)) { + amount = sale; + } else if (StringUtils.isNotBlank(sale)) { + amount = sale; + } + } String lineCurrency = TmsQuoteCurrency.fromFeeRow(row); addFeeLine(lines, name, amount, StringUtils.isNotBlank(lineCurrency) ? lineCurrency : headerCurrency, diff --git a/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_land_class_v2.sql b/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_land_class_v2.sql index 217ecfa..aa5f99f 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_land_class_v2.sql +++ b/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_land_class_v2.sql @@ -25,10 +25,10 @@ DELETE FROM `inq_response_timeout` 'rt100000000000000000000000000019' ); --- 2) 保留的 4 项:分类与线路解耦,route_category 置空;文案对齐产品确认 +-- 2) 保留的 4 项:用 UNHEX 写入 UTF-8,避免客户端字符集把中文写成问号 UPDATE `inq_response_timeout` SET `route_category`='', - `transport_type`='普件(东南亚、中港常用线路/常规车型)', + `transport_type`=CONVERT(UNHEX('e699aee4bbb628e4b89ce58d97e4ba9ae38081e4b8ade6b8afe5b8b8e794a8e7babfe8b7af2fe5b8b8e8a784e8bda6e59e8b29') USING utf8mb4), `update_by`='system', `update_time`=NOW() WHERE `id`='rt100000000000000000000000000007' @@ -36,7 +36,7 @@ UPDATE `inq_response_timeout` UPDATE `inq_response_timeout` SET `route_category`='', - `transport_type`='普件(东南亚、中亚、中欧涉及国内+国外段)', + `transport_type`=CONVERT(UNHEX('e699aee4bbb628e4b89ce58d97e4ba9ae38081e4b8ade4ba9ae38081e4b8ade6aca7e6b689e58f8ae59bbde586852be59bbde5a496e6aeb529') USING utf8mb4), `update_by`='system', `update_time`=NOW() WHERE `id`='rt100000000000000000000000000008' @@ -44,7 +44,7 @@ UPDATE `inq_response_timeout` UPDATE `inq_response_timeout` SET `route_category`='', - `transport_type`='普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)', + `transport_type`=CONVERT(UNHEX('e699aee4bbb628e4b89ce58d97e4ba9ae38081e4b8ade4ba9ae38081e4b8ade6aca7e6b689e58f8ae5a283e5a496e6aeb5e6b885e6b4bee7ad89e789b9e6ae8ae8a681e6b18229') USING utf8mb4), `update_by`='system', `update_time`=NOW() WHERE `id`='rt100000000000000000000000000009' @@ -52,7 +52,7 @@ UPDATE `inq_response_timeout` UPDATE `inq_response_timeout` SET `route_category`='', - `transport_type`='急件', + `transport_type`=CONVERT(UNHEX('e680a5e4bbb6') USING utf8mb4), `update_by`='system', `update_time`=NOW() WHERE `id`='rt100000000000000000000000000010' 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 4cbda92..6efc7f2 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 @@ -220,4 +220,44 @@ public class TmsQuoteCurrencyTest { assertEquals("470", quote.get("fee_total_podCostItems")); assertTrue(quote.get("fee_rows_oceanCostItems").contains("575")); } + + @Test + public void toAgentQuoteAirRateTimesChargeableWeight() { + // TMS 把运费单价塞进 salePrice,数量/单价空;应用计费重算出行金额 + Map freight = new HashMap<>(); + freight.put("feeName", "运输费"); + freight.put("salePrice", "11.0"); + freight.put("currency", "CNY"); + Map pickup = new HashMap<>(); + pickup.put("feeName", "提送货费"); + pickup.put("salePrice", "6.0"); + pickup.put("currency", "CNY"); + pickup.put("remark", "发生时收取"); + Map labor = new HashMap<>(); + labor.put("feeName", "人工费"); + labor.put("salePrice", "123.0"); + labor.put("currency", "CNY"); + labor.put("remark", "发生时收取"); + Map data = new HashMap<>(); + data.put("quoteFound", true); + data.put("totalSalePrice", "183370.0"); + data.put("currency", "CNY"); + data.put("chargeableWeightKg", "16670.0000"); + data.put("feeItems", java.util.Arrays.asList(freight, pickup, labor)); + Map tms = new HashMap<>(); + tms.put("code", 1); + tms.put("tmsData", data); + Map quote = InquiryTmsQueryService.toAgentQuote(tms); + assertEquals("183370.0", quote.get("total")); + assertEquals("16670.0000", quote.get("chargeableWeightKg")); + assertTrue(quote.get("fee_lines").contains("\"name\":\"运输费\"")); + assertTrue(quote.get("fee_lines").contains("\"unit_price\":\"11.0\"")); + assertTrue(quote.get("fee_lines").contains("\"quantity\":\"16670.0000\"")); + assertTrue(quote.get("fee_lines").contains("\"unit\":\"KG\"")); + assertTrue(quote.get("fee_lines").contains("\"amount\":\"183370\"")); + // 固定费保持原金额,不乘计费重 + assertTrue(quote.get("fee_lines").contains("\"name\":\"提送货费\"")); + assertTrue(quote.get("fee_lines").contains("\"amount\":\"6.0\"")); + assertTrue(quote.get("fee_lines").contains("\"amount\":\"123.0\"")); + } }