Merge remote-tracking branch 'origin/lmx20260922' into feat/air-inquiry-fields-route-20260922
This commit is contained in:
+27
@@ -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<InquiryQuoteTem
|
||||
}
|
||||
}
|
||||
}
|
||||
// 空运:已落库的旧明细若只有费率,出单时按计费重补 单位/单价/数量/行金额
|
||||
if (!rows.isEmpty() && StringUtils.isNotBlank(chargeable)) {
|
||||
List<Map<String, String>> lineMaps = new ArrayList<>();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JSONObject one = rows.getJSONObject(i);
|
||||
if (one == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> 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<String, Object> enrichCtx = new LinkedHashMap<>();
|
||||
enrichCtx.put("chargeableWeightKg", chargeable);
|
||||
InquiryTmsQueryService.enrichAirWeightFeeLines(lineMaps, enrichCtx);
|
||||
JSONArray enriched = new JSONArray();
|
||||
for (Map<String, String> 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) {
|
||||
|
||||
+42
@@ -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<String, Integer> 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 才能加算;名称等仍写文本。
|
||||
*/
|
||||
|
||||
+84
-2
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空运运输费:单价 × 计费重 = 行金额。
|
||||
* <p>
|
||||
* 现象:feeItems 只给 salePrice=11(实为单价),Excel 金额列写成 11,单位/单价/数量空,
|
||||
* 预估费用 SUM 成 140,而总销售价是 11×计费重。
|
||||
* 只处理按重计的运输/空运费,提送货费、人工费等固定费不动。
|
||||
*/
|
||||
public static void enrichAirWeightFeeLines(List<Map<String, String>> lines, Map<String, Object> 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<String, String> 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<Map<String, String>> 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,
|
||||
|
||||
+5
-5
@@ -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'
|
||||
|
||||
+40
@@ -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<String, Object> freight = new HashMap<>();
|
||||
freight.put("feeName", "运输费");
|
||||
freight.put("salePrice", "11.0");
|
||||
freight.put("currency", "CNY");
|
||||
Map<String, Object> pickup = new HashMap<>();
|
||||
pickup.put("feeName", "提送货费");
|
||||
pickup.put("salePrice", "6.0");
|
||||
pickup.put("currency", "CNY");
|
||||
pickup.put("remark", "发生时收取");
|
||||
Map<String, Object> labor = new HashMap<>();
|
||||
labor.put("feeName", "人工费");
|
||||
labor.put("salePrice", "123.0");
|
||||
labor.put("currency", "CNY");
|
||||
labor.put("remark", "发生时收取");
|
||||
Map<String, Object> 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<String, Object> tms = new HashMap<>();
|
||||
tms.put("code", 1);
|
||||
tms.put("tmsData", data);
|
||||
Map<String, String> 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\""));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user