产品调价继承工单币种(港币不再默成美元);陆运集拼空分档只填询价重量那一档,跨境整车按已选线路写起讫。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+8
@@ -1176,6 +1176,14 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
if (!routeRows.isEmpty()) {
|
||||
QuoteTemplateExcelSupport.copyMissingWeightPrices(landSegs, rawLand);
|
||||
}
|
||||
// 销售已选线路透传:跨境整车国内/境外段按 legs 写完整起讫(多备选混在 route_rows 时不能靠同序列过滤)
|
||||
Object selectedLand = q.get("selected_land_route");
|
||||
if (selectedLand == null) {
|
||||
selectedLand = q.get("selectedLandRoute");
|
||||
}
|
||||
if (selectedLand != null) {
|
||||
out.put("selected_land_route", selectedLand);
|
||||
}
|
||||
if (!landSegs.isEmpty()) {
|
||||
out.put("landSegments", landSegs);
|
||||
out.put("route_rows", landSegs);
|
||||
|
||||
+311
-18
@@ -18,7 +18,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -2798,7 +2797,7 @@ public final class QuoteTemplateExcelSupport {
|
||||
int mappedStart = rowIdx0;
|
||||
rowIdx0 = resolveRouteDataStartRow(sheet, rowIdx0, region.getIntValue("headerRow"));
|
||||
String kind = landRouteSectionKind(sheet, region);
|
||||
JSONArray sectionRoutes = routesForLandRouteSection(routes, sheet, region, i, regions.size());
|
||||
JSONArray sectionRoutes = routesForLandRouteSection(routes, sheet, region, i, regions.size(), facts);
|
||||
if (sectionRoutes == null || sectionRoutes.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -2858,7 +2857,7 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (StringUtils.isBlank(col)) {
|
||||
continue;
|
||||
}
|
||||
String price = resolveRouteVehicleRate(rowObj, facts, vk, vehicleRates.size());
|
||||
String price = resolveRouteVehicleRate(rowObj, facts, vk, vehicleRates.size(), kind);
|
||||
if (StringUtils.isNotBlank(price) && !"-".equals(price)) {
|
||||
writeFeeCol(sheet, ri, col, price, yellow, sheet.getSheetName());
|
||||
}
|
||||
@@ -2937,6 +2936,19 @@ public final class QuoteTemplateExcelSupport {
|
||||
return firstNonBlank(rowObj.getString("amount"), rowObj.getString("salePrice"),
|
||||
rowObj.getString("costPrice"));
|
||||
}
|
||||
// 集拼矩阵副表头「最低收费」:有分档单价时只用 min_charge;分档壳无单价时回退整票价。
|
||||
if (header.contains("最低收费")) {
|
||||
String minCharge = firstNonBlank(rowObj.getString("min_charge"), rowObj.getString("minCharge"));
|
||||
if (StringUtils.isNotBlank(minCharge) && !"-".equals(minCharge)) {
|
||||
return formatTierPrice(minCharge);
|
||||
}
|
||||
JSONArray tiers = weightPricesOf(rowObj);
|
||||
if (tiers != null && !tiers.isEmpty() && !anyWeightTierHasPrice(tiers)) {
|
||||
return formatTierPrice(firstNonBlank(rowObj.getString("salePrice"),
|
||||
rowObj.getString("costPrice"), rowObj.getString("amount")));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -3092,11 +3104,19 @@ public final class QuoteTemplateExcelSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 国内段写境内城市,境外段写境外城市。两段 TMS 腿按腿序号对上。
|
||||
* 只有一对郑州→吉隆坡时:国内段只写郑州,境外段只写吉隆坡,不发明口岸。
|
||||
* 国内段 / 境外段各写对应 TMS 腿的完整起讫。
|
||||
* <p>
|
||||
* 优先用 selected_land_route.legs(销售已选线路);否则从多条备选里挑一条多腿线路。
|
||||
* 只有一对「境内→境外」且无两段腿时:国内段只写境内点、境外段只写境外点,不发明口岸。
|
||||
* WO202610120001:备选混在 route_rows 时若要求整表同 segmentSequence,会退回切片导致口岸空。
|
||||
*/
|
||||
static JSONArray routesForLandRouteSection(JSONArray routes, Sheet sheet, JSONObject region,
|
||||
int regionIndex, int regionCount) {
|
||||
return routesForLandRouteSection(routes, sheet, region, regionIndex, regionCount, null);
|
||||
}
|
||||
|
||||
static JSONArray routesForLandRouteSection(JSONArray routes, Sheet sheet, JSONObject region,
|
||||
int regionIndex, int regionCount, JSONObject facts) {
|
||||
if (routes == null || routes.isEmpty()) {
|
||||
return routes;
|
||||
}
|
||||
@@ -3107,12 +3127,15 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (StringUtils.isBlank(kind)) {
|
||||
return routes;
|
||||
}
|
||||
JSONArray byLeg = pickLandRouteLegsForSection(routes, kind);
|
||||
JSONArray preferred = preferredLandRouteLegs(facts, routes);
|
||||
JSONArray byLeg = pickLandRouteLegsForSection(preferred, kind);
|
||||
if (byLeg != null && !byLeg.isEmpty()) {
|
||||
return byLeg;
|
||||
}
|
||||
JSONObject seed = preferred != null && !preferred.isEmpty()
|
||||
? preferred.getJSONObject(0) : routes.getJSONObject(0);
|
||||
JSONArray sliced = new JSONArray();
|
||||
JSONObject one = sliceCrossBorderOd(routes.getJSONObject(0), kind);
|
||||
JSONObject one = sliceCrossBorderOd(seed, kind);
|
||||
if (one != null && (StringUtils.isNotBlank(one.getString("origin"))
|
||||
|| StringUtils.isNotBlank(one.getString("destination")))) {
|
||||
sliced.add(one);
|
||||
@@ -3120,16 +3143,91 @@ public final class QuoteTemplateExcelSupport {
|
||||
return sliced;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出单优先腿:已选线路 legs > 同 segmentSequence 且腿数最多的备选 > 原数组。
|
||||
*/
|
||||
static JSONArray preferredLandRouteLegs(JSONObject facts, JSONArray allRoutes) {
|
||||
if (facts != null) {
|
||||
Object rawSel = facts.get("selected_land_route");
|
||||
if (rawSel == null) {
|
||||
rawSel = facts.get("selectedLandRoute");
|
||||
}
|
||||
JSONObject sel = null;
|
||||
if (rawSel instanceof JSONObject) {
|
||||
sel = (JSONObject) rawSel;
|
||||
} else if (rawSel instanceof String && StringUtils.isNotBlank((String) rawSel)) {
|
||||
try {
|
||||
Object parsed = JSON.parse((String) rawSel);
|
||||
if (parsed instanceof JSONObject) {
|
||||
sel = (JSONObject) parsed;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 忽略坏 JSON
|
||||
}
|
||||
} else if (rawSel instanceof java.util.Map) {
|
||||
sel = JSON.parseObject(JSON.toJSONString(rawSel));
|
||||
}
|
||||
if (sel != null) {
|
||||
JSONArray legs = parseFeeRows(sel.get("legs"));
|
||||
if (legs != null && !legs.isEmpty()) {
|
||||
return legs;
|
||||
}
|
||||
}
|
||||
}
|
||||
JSONArray grouped = pickPrimaryLandRouteGroup(allRoutes);
|
||||
return grouped != null && !grouped.isEmpty() ? grouped : allRoutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多条备选(不同 segmentSequence)时,挑腿数最多的一条完整线路;并列取较小序号。
|
||||
*/
|
||||
static JSONArray pickPrimaryLandRouteGroup(JSONArray routes) {
|
||||
if (routes == null || routes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
java.util.Map<String, JSONArray> groups = new java.util.LinkedHashMap<>();
|
||||
for (int i = 0; i < routes.size(); i++) {
|
||||
JSONObject row = routes.getJSONObject(i);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
String seq = StringUtils.trimToEmpty(row.getString("segmentSequence"));
|
||||
if (seq.isEmpty()) {
|
||||
seq = "1";
|
||||
}
|
||||
groups.computeIfAbsent(seq, k -> new JSONArray()).add(row);
|
||||
}
|
||||
if (groups.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String bestKey = null;
|
||||
int bestLegs = -1;
|
||||
int bestSeqNum = Integer.MAX_VALUE;
|
||||
for (java.util.Map.Entry<String, JSONArray> e : groups.entrySet()) {
|
||||
int legs = e.getValue() == null ? 0 : e.getValue().size();
|
||||
int seqNum;
|
||||
try {
|
||||
seqNum = Integer.parseInt(e.getKey().trim());
|
||||
} catch (RuntimeException ex) {
|
||||
seqNum = Integer.MAX_VALUE;
|
||||
}
|
||||
if (legs > bestLegs || (legs == bestLegs && seqNum < bestSeqNum)) {
|
||||
bestLegs = legs;
|
||||
bestSeqNum = seqNum;
|
||||
bestKey = e.getKey();
|
||||
}
|
||||
}
|
||||
return bestKey == null ? null : groups.get(bestKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一条线路的多段腿:国内段取首腿完整 OD,境外段取末腿完整 OD。
|
||||
* 不足两腿时返回 null,交给 sliceCrossBorderOd。
|
||||
*/
|
||||
static JSONArray pickLandRouteLegsForSection(JSONArray routes, String kind) {
|
||||
if (routes == null || routes.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
String seq = StringUtils.trimToEmpty(routes.getJSONObject(0).getString("segmentSequence"));
|
||||
for (int i = 1; i < routes.size(); i++) {
|
||||
if (!seq.equals(StringUtils.trimToEmpty(routes.getJSONObject(i).getString("segmentSequence")))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
JSONArray ordered = new JSONArray();
|
||||
ordered.addAll(routes);
|
||||
ordered.sort((a, b) -> {
|
||||
@@ -3140,8 +3238,10 @@ public final class QuoteTemplateExcelSupport {
|
||||
JSONArray out = new JSONArray();
|
||||
if ("domestic".equals(kind)) {
|
||||
out.add(ordered.getJSONObject(0));
|
||||
} else {
|
||||
} else if ("overseas".equals(kind)) {
|
||||
out.add(ordered.getJSONObject(ordered.size() - 1));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -3483,6 +3583,14 @@ public final class QuoteTemplateExcelSupport {
|
||||
* 禁止「无车型时把同一售价灌进所有费种列」。
|
||||
*/
|
||||
static String resolveRouteVehicleRate(JSONObject rowObj, JSONObject facts, String headerKey, int headerCount) {
|
||||
return resolveRouteVehicleRate(rowObj, facts, headerKey, headerCount, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sectionKind domestic / overseas / 空;境外段要把国内标注 3T 对成 3.5T 再写价。
|
||||
*/
|
||||
static String resolveRouteVehicleRate(JSONObject rowObj, JSONObject facts, String headerKey,
|
||||
int headerCount, String sectionKind) {
|
||||
if (StringUtils.isBlank(headerKey)) {
|
||||
return "";
|
||||
}
|
||||
@@ -3517,7 +3625,20 @@ public final class QuoteTemplateExcelSupport {
|
||||
}
|
||||
// 跨境集拼是分档公斤价(2-500KG=12 …),不是把同一个 price_per_kg 灌进每一列。
|
||||
if (looksLikeKgBracketHeader(key)) {
|
||||
return matchWeightBracketPrice(rowObj, key);
|
||||
String band = matchWeightBracketPrice(rowObj, key);
|
||||
if (StringUtils.isNotBlank(band)) {
|
||||
return band;
|
||||
}
|
||||
// TMS 偶发只给分档壳(min/max)+ 整票 costPrice(计价单位:次),无档内单价。
|
||||
// 禁止灌进所有公斤列;只写入询价重量落入的那一档,避免报价单全空。
|
||||
return flatLandFeeForEmptyWeightBands(rowObj, facts, key);
|
||||
}
|
||||
if (key.contains("最低收费")) {
|
||||
return formatTierPrice(firstNonBlank(rowObj == null ? null : rowObj.getString("min_charge"),
|
||||
rowObj == null ? null : rowObj.getString("minCharge"),
|
||||
rowObj == null ? null : rowObj.getString("salePrice"),
|
||||
rowObj == null ? null : rowObj.getString("costPrice"),
|
||||
rowObj == null ? null : rowObj.getString("amount")));
|
||||
}
|
||||
if (key.toUpperCase(Locale.ROOT).contains("KG") || key.contains("公斤")) {
|
||||
return firstNonBlank(rowObj == null ? null : rowObj.getString("price_per_kg"),
|
||||
@@ -3527,16 +3648,17 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (key.contains("运输") || key.contains("运费") || key.contains("陆运")) {
|
||||
return firstNonBlank(rowObj == null ? null : rowObj.getString("salePrice"),
|
||||
rowObj == null ? null : rowObj.getString("amount"),
|
||||
rowObj == null ? null : rowObj.getString("costPrice"),
|
||||
rowObj == null ? null : rowObj.getString("land_freight"),
|
||||
facts == null ? null : facts.getString("land_freight"));
|
||||
}
|
||||
// 2) 车型列:仅当 vehicleType 对得上才写售价
|
||||
// 2) 车型列:仅当 vehicleType 对得上才写售价(含跨境国内/国外标注换算)
|
||||
String vehicleType = firstNonBlank(rowObj == null ? null : rowObj.getString("vehicleType"),
|
||||
rowObj == null ? null : rowObj.getString("vehicle_type"));
|
||||
String price = firstNonBlank(rowObj == null ? null : rowObj.getString("salePrice"),
|
||||
rowObj == null ? null : rowObj.getString("costPrice"),
|
||||
rowObj == null ? null : rowObj.getString("amount"));
|
||||
if (StringUtils.isNotBlank(vehicleType) && vehicleTypeEquals(vehicleType, key)) {
|
||||
if (StringUtils.isNotBlank(vehicleType) && vehicleTypeMatchesSection(vehicleType, key, sectionKind)) {
|
||||
return price;
|
||||
}
|
||||
// 3) 仅一列且无车型信息时,允许写入该列(整车单列模板)
|
||||
@@ -3546,6 +3668,65 @@ public final class QuoteTemplateExcelSupport {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 车型列是否对得上当前段表头。
|
||||
* <p>
|
||||
* 跨境整车:询价/TMS 常写国内标注「3T」,国外段表头是「3.5T」;
|
||||
* 境外段用国内→国外映射后再比,国内段反之。禁止把 3T 误当成所有 T 列。
|
||||
*/
|
||||
static boolean vehicleTypeMatchesSection(String vehicleType, String headerKey, String sectionKind) {
|
||||
if (vehicleTypeEquals(vehicleType, headerKey)) {
|
||||
return true;
|
||||
}
|
||||
String kind = StringUtils.trimToEmpty(sectionKind);
|
||||
if ("overseas".equals(kind)) {
|
||||
String mapped = mapDomesticVehicleToOverseas(vehicleType);
|
||||
return StringUtils.isNotBlank(mapped) && vehicleTypeEquals(mapped, headerKey);
|
||||
}
|
||||
if ("domestic".equals(kind)) {
|
||||
String mapped = mapOverseasVehicleToDomestic(vehicleType);
|
||||
return StringUtils.isNotBlank(mapped) && vehicleTypeEquals(mapped, headerKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国内整车标注 → 跨境国外段表头。WO202610080006:3T 对 3.5T。
|
||||
* 未收录的返回空,禁止瞎猜。
|
||||
*/
|
||||
static String mapDomesticVehicleToOverseas(String vehicleType) {
|
||||
String a = normalizeVehicleToken(vehicleType);
|
||||
if (a.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if ("3T".equals(a) || "3吨".equals(a)) {
|
||||
return "3.5T";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 国外段标注 → 国内段表头(单腿 TMS 偶发直接回 3.5T 时,国内段仍能写进 3T)。
|
||||
*/
|
||||
static String mapOverseasVehicleToDomestic(String vehicleType) {
|
||||
String a = normalizeVehicleToken(vehicleType);
|
||||
if (a.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if ("3.5T".equals(a) || "3.5吨".equals(a)) {
|
||||
return "3T";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String normalizeVehicleToken(String vehicleType) {
|
||||
if (StringUtils.isBlank(vehicleType)) {
|
||||
return "";
|
||||
}
|
||||
return vehicleType.replace(" ", "").toUpperCase(Locale.ROOT)
|
||||
.replace("T", "T");
|
||||
}
|
||||
|
||||
/**
|
||||
* 表头是「2-500KG」「5001KG以上」这类分档,不是「元/KG」单价列。
|
||||
*/
|
||||
@@ -3555,6 +3736,8 @@ public final class QuoteTemplateExcelSupport {
|
||||
|
||||
/**
|
||||
* 用 landSegments.weightPrices 对上分档表头。对不上就空着,禁止把一档价抄进所有列。
|
||||
* <p>
|
||||
* 档内单价优先读 price,兼容 TMS 偶发的 unitPrice/salePrice/costPrice/amount。
|
||||
*/
|
||||
static String matchWeightBracketPrice(JSONObject rowObj, String headerKey) {
|
||||
JSONArray tiers = weightPricesOf(rowObj);
|
||||
@@ -3571,12 +3754,122 @@ public final class QuoteTemplateExcelSupport {
|
||||
boolean open = tier.get("max") == null || StringUtils.isBlank(String.valueOf(tier.get("max")));
|
||||
int max = open ? -1 : intOf(tier.get("max"));
|
||||
if (min == range[0] && max == range[1]) {
|
||||
return formatTierPrice(tier.get("price"));
|
||||
return formatTierPrice(tierPriceRaw(tier));
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 分档壳有区间但没有任何档内单价时:把整票 salePrice/costPrice 写入询价重量所在那一档。
|
||||
* <p>
|
||||
* 为何:WO202610080043 跨境集拼 TMS 回 weightPrices 只有 min/max,费用是「次」18200;
|
||||
* 若所有公斤列都空着,销售会以为报价没写入。有真实档内单价时禁止走这条。
|
||||
* 副作用:只读 row/facts,不改库。
|
||||
*/
|
||||
static String flatLandFeeForEmptyWeightBands(JSONObject rowObj, JSONObject facts, String headerKey) {
|
||||
int[] range = parseKgBracket(headerKey);
|
||||
if (range == null || rowObj == null) {
|
||||
return "";
|
||||
}
|
||||
JSONArray tiers = weightPricesOf(rowObj);
|
||||
if (tiers == null || tiers.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (anyWeightTierHasPrice(tiers)) {
|
||||
return "";
|
||||
}
|
||||
String flat = firstNonBlank(rowObj.getString("salePrice"), rowObj.getString("costPrice"),
|
||||
rowObj.getString("amount"));
|
||||
if (StringUtils.isBlank(flat) || "-".equals(flat)) {
|
||||
return "";
|
||||
}
|
||||
double weight = inquiryWeightKg(rowObj, facts);
|
||||
boolean hit;
|
||||
if (weight >= 0) {
|
||||
hit = weightInKgBracket(weight, range[0], range[1]);
|
||||
} else {
|
||||
// 无询价重量:只写入 weightPrices 里的第一档,避免抄进所有列
|
||||
JSONObject first = tiers.getJSONObject(0);
|
||||
int min = first == null ? -1 : intOf(first.get("min"));
|
||||
boolean open = first == null || first.get("max") == null
|
||||
|| StringUtils.isBlank(String.valueOf(first.get("max")));
|
||||
int max = open ? -1 : intOf(first.get("max"));
|
||||
hit = min == range[0] && max == range[1];
|
||||
}
|
||||
return hit ? formatTierPrice(flat) : "";
|
||||
}
|
||||
|
||||
/** 档内是否已有任何可用单价(有则禁止整票价回填)。 */
|
||||
static boolean anyWeightTierHasPrice(JSONArray tiers) {
|
||||
if (tiers == null) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < tiers.size(); i++) {
|
||||
JSONObject tier = tiers.getJSONObject(i);
|
||||
if (tier != null && StringUtils.isNotBlank(formatTierPrice(tierPriceRaw(tier)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** TMS 档内单价字段名不统一,按常见键取第一个非空。 */
|
||||
static Object tierPriceRaw(JSONObject tier) {
|
||||
if (tier == null) {
|
||||
return null;
|
||||
}
|
||||
Object raw = tier.get("price");
|
||||
if (raw == null || StringUtils.isBlank(String.valueOf(raw)) || "null".equalsIgnoreCase(String.valueOf(raw))) {
|
||||
raw = tier.get("unitPrice");
|
||||
}
|
||||
if (raw == null || StringUtils.isBlank(String.valueOf(raw)) || "null".equalsIgnoreCase(String.valueOf(raw))) {
|
||||
raw = tier.get("salePrice");
|
||||
}
|
||||
if (raw == null || StringUtils.isBlank(String.valueOf(raw)) || "null".equalsIgnoreCase(String.valueOf(raw))) {
|
||||
raw = tier.get("costPrice");
|
||||
}
|
||||
if (raw == null || StringUtils.isBlank(String.valueOf(raw)) || "null".equalsIgnoreCase(String.valueOf(raw))) {
|
||||
raw = tier.get("amount");
|
||||
}
|
||||
if (raw == null || StringUtils.isBlank(String.valueOf(raw)) || "null".equalsIgnoreCase(String.valueOf(raw))) {
|
||||
raw = tier.get("pricePerKg");
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** 询价毛重(KG);没有或解析失败返回 -1。 */
|
||||
static double inquiryWeightKg(JSONObject rowObj, JSONObject facts) {
|
||||
String raw = firstNonBlank(
|
||||
rowObj == null ? null : rowObj.getString("weight_kg"),
|
||||
rowObj == null ? null : rowObj.getString("weightKg"),
|
||||
facts == null ? null : facts.getString("weight_kg"),
|
||||
facts == null ? null : facts.getString("weightKg"),
|
||||
facts == null ? null : facts.getString("毛重"),
|
||||
facts == null ? null : facts.getString("重量"),
|
||||
facts == null ? null : facts.getString("weight"));
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw)) {
|
||||
return -1;
|
||||
}
|
||||
String num = raw.trim().replace(",", "").replaceAll("[Kk][Gg].*$", "").trim();
|
||||
try {
|
||||
return new java.math.BigDecimal(num).doubleValue();
|
||||
} catch (RuntimeException ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重量是否落在 [min, max];max=-1 表示无上限。 */
|
||||
static boolean weightInKgBracket(double weight, int min, int max) {
|
||||
if (weight < 0 || min < 0) {
|
||||
return false;
|
||||
}
|
||||
if (weight < min) {
|
||||
return false;
|
||||
}
|
||||
return max < 0 || weight <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 route_rows 会丢掉 weightPrices。出单前从原始 landSegments 补回。
|
||||
*/
|
||||
|
||||
+195
-1
@@ -1480,6 +1480,81 @@ public class QuoteTemplateFillTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WO202610080043:TMS weightPrices 只有 min/max、整票 costPrice=18200(计价单位:次)。
|
||||
* 询价毛重 2KG 落入 2-500KG 档时写入 18200;其它公斤列保持空;有真实档内单价时不走此回退。
|
||||
*/
|
||||
@Test
|
||||
public void landGroupageEmptyWeightBandsWritesFlatFeeIntoCargoBandOnly() throws Exception {
|
||||
Path src = Files.createTempFile("quote-land-groupage-flat", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-land-groupage-flat-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("跨境集拼");
|
||||
org.apache.poi.ss.usermodel.Row matrix = row(sheet, 7);
|
||||
matrix.createCell(1).setCellValue("始发地");
|
||||
matrix.createCell(2).setCellValue("目的地");
|
||||
matrix.createCell(3).setCellValue("最低收费");
|
||||
matrix.createCell(4).setCellValue("2-500KG");
|
||||
matrix.createCell(5).setCellValue("501-1000KG");
|
||||
matrix.createCell(6).setCellValue("1001-3000KG");
|
||||
matrix.createCell(7).setCellValue("3001-5000KG");
|
||||
matrix.createCell(8).setCellValue("5001KG以上");
|
||||
row(sheet, 9);
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject route = new JSONObject(true);
|
||||
route.put("origin", "凉山-春疆货场");
|
||||
route.put("destination", "湖北省-武汉市");
|
||||
route.put("currency", "CNY");
|
||||
route.put("costPrice", "18200.00");
|
||||
route.put("amount", "18200.00");
|
||||
route.put("weightPrices", JSONArray.parseArray(
|
||||
"[{\"min\":2,\"max\":500},{\"min\":501,\"max\":1000},{\"min\":1001,\"max\":3000},"
|
||||
+ "{\"min\":3001,\"max\":5000},{\"min\":5001}]"));
|
||||
JSONArray routeRows = new JSONArray();
|
||||
routeRows.add(route);
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
mapping.put("sheet", "跨境集拼");
|
||||
JSONArray regions = new JSONArray();
|
||||
JSONObject region = new JSONObject(true);
|
||||
region.put("headerRow", 8);
|
||||
region.put("dataStartRow", 10);
|
||||
JSONObject columns = new JSONObject(true);
|
||||
columns.put("origin", "B");
|
||||
columns.put("destination", "C");
|
||||
JSONObject vehicleRates = new JSONObject(true);
|
||||
vehicleRates.put("最低收费", "D");
|
||||
vehicleRates.put("2-500KG", "E");
|
||||
vehicleRates.put("501-1000KG", "F");
|
||||
vehicleRates.put("1001-3000KG", "G");
|
||||
vehicleRates.put("3001-5000KG", "H");
|
||||
vehicleRates.put("5001KG以上", "I");
|
||||
columns.put("vehicleRates", vehicleRates);
|
||||
region.put("columns", columns);
|
||||
regions.add(region);
|
||||
mapping.put("routeRegions", regions);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("毛重", "2");
|
||||
facts.put("route_rows", routeRows);
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertEquals("凉山-春疆货场", read(sheet, 9, 1));
|
||||
assertEquals("湖北省-武汉市", read(sheet, 9, 2));
|
||||
assertEquals("18200", normalizeMoney(read(sheet, 9, 3)));
|
||||
assertEquals("18200", normalizeMoney(read(sheet, 9, 4)));
|
||||
assertEquals("", read(sheet, 9, 5));
|
||||
assertEquals("", read(sheet, 9, 6));
|
||||
assertEquals("", read(sheet, 9, 7));
|
||||
assertEquals("", read(sheet, 9, 8));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销售没填客户名称时,客户名称格保持空白,不能写成业务员。
|
||||
* 询价条带的时效写在表头下一行;公斤分档价按 weightPrices 落入各列。
|
||||
@@ -1878,6 +1953,7 @@ public class QuoteTemplateFillTest {
|
||||
|
||||
/**
|
||||
* 跨境整车分段切片后仍须带上车型与运价;尺寸副表头行不能被 4000 盖掉,运价写在下一行。
|
||||
* WO006:单腿 vehicleType=3T 时,国内写 3T 列,国外段换算成 3.5T 再写,不能留空。
|
||||
*/
|
||||
@Test
|
||||
public void landCrossBorderFtlKeepsVehiclePriceAfterOdSlice() throws Exception {
|
||||
@@ -1900,9 +1976,11 @@ public class QuoteTemplateFillTest {
|
||||
head2.createCell(2).setCellValue("目的地");
|
||||
head2.createCell(3).setCellValue("1.25T");
|
||||
head2.createCell(4).setCellValue("2.5T");
|
||||
head2.createCell(5).setCellValue("3.5T");
|
||||
org.apache.poi.ss.usermodel.Row dim2 = row(sheet, 27);
|
||||
dim2.createCell(3).setCellValue("2.8x1.5x1.6M");
|
||||
dim2.createCell(4).setCellValue("3.50x1.6x1.6M");
|
||||
dim2.createCell(5).setCellValue("4.2x1.8x1.8M");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
@@ -1933,6 +2011,7 @@ public class QuoteTemplateFillTest {
|
||||
JSONObject rates2 = new JSONObject(true);
|
||||
rates2.put("1.25T", "D");
|
||||
rates2.put("2.5T", "E");
|
||||
rates2.put("3.5T", "F");
|
||||
cols2.put("vehicleRates", rates2);
|
||||
overseas.put("columns", cols2);
|
||||
regions.add(overseas);
|
||||
@@ -1950,12 +2029,14 @@ public class QuoteTemplateFillTest {
|
||||
assertEquals("4.1*2.1*2.3M", read(sheet, 8, 3));
|
||||
assertEquals("广东省-深圳市", read(sheet, 9, 1));
|
||||
assertEquals("", read(sheet, 9, 2));
|
||||
assertEquals("4000.00", read(sheet, 9, 3));
|
||||
assertEquals("4000", normalizeMoney(read(sheet, 9, 3)));
|
||||
assertEquals("", read(sheet, 9, 4));
|
||||
assertEquals("2.8x1.5x1.6M", read(sheet, 27, 3));
|
||||
assertEquals("", read(sheet, 28, 1));
|
||||
assertEquals("谅山-春疆货场", read(sheet, 28, 2));
|
||||
assertEquals("", read(sheet, 28, 3));
|
||||
assertEquals("", read(sheet, 28, 4));
|
||||
assertEquals("4000", normalizeMoney(read(sheet, 28, 5)));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
@@ -2080,6 +2161,107 @@ public class QuoteTemplateFillTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WO202610120001:route_rows 混有多条备选(不同 segmentSequence),国内/境外段应写完整两段 OD,
|
||||
* 不能因「整表必须同序列」退回切片导致口岸列空。
|
||||
*/
|
||||
@Test
|
||||
public void landCrossBorderFtlFillsFullOdFromMultiAltRouteRows() throws Exception {
|
||||
Path src = Files.createTempFile("quote-land-cb-wo001-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-land-cb-wo001-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("跨境整车");
|
||||
row(sheet, 5).createCell(0).setCellValue("一、国内段报价");
|
||||
org.apache.poi.ss.usermodel.Row head1 = row(sheet, 7);
|
||||
head1.createCell(1).setCellValue("起始地");
|
||||
head1.createCell(2).setCellValue("目的地");
|
||||
head1.createCell(3).setCellValue("3T");
|
||||
row(sheet, 24).createCell(0).setCellValue("二、境外段报价");
|
||||
org.apache.poi.ss.usermodel.Row head2 = row(sheet, 26);
|
||||
head2.createCell(1).setCellValue("起始地");
|
||||
head2.createCell(2).setCellValue("目的地");
|
||||
head2.createCell(3).setCellValue("3.5T");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
mapping.put("sheet", "跨境整车");
|
||||
JSONArray regions = new JSONArray();
|
||||
JSONObject inland = new JSONObject(true);
|
||||
inland.put("title", "国内段报价");
|
||||
inland.put("headerRow", 8);
|
||||
inland.put("dataStartRow", 8);
|
||||
JSONObject cols1 = new JSONObject(true);
|
||||
cols1.put("origin", "B");
|
||||
cols1.put("destination", "C");
|
||||
cols1.put("3T", "D");
|
||||
inland.put("columns", cols1);
|
||||
regions.add(inland);
|
||||
JSONObject overseas = new JSONObject(true);
|
||||
overseas.put("title", "境外段报价");
|
||||
overseas.put("headerRow", 27);
|
||||
overseas.put("dataStartRow", 27);
|
||||
JSONObject cols2 = new JSONObject(true);
|
||||
cols2.put("origin", "B");
|
||||
cols2.put("destination", "C");
|
||||
cols2.put("3.5T", "D");
|
||||
overseas.put("columns", cols2);
|
||||
regions.add(overseas);
|
||||
mapping.put("routeRegions", regions);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("origin", "上海");
|
||||
facts.put("destination", "胡志明");
|
||||
JSONArray rows = new JSONArray();
|
||||
JSONObject a1 = new JSONObject(true);
|
||||
a1.put("segmentSequence", "1");
|
||||
a1.put("routeLegSequence", "1");
|
||||
a1.put("origin", "上海");
|
||||
a1.put("destination", "谅山-春疆货场");
|
||||
a1.put("vehicleType", "3T");
|
||||
a1.put("costPrice", "7500");
|
||||
a1.put("currency", "CNY");
|
||||
rows.add(a1);
|
||||
JSONObject a2 = new JSONObject(true);
|
||||
a2.put("segmentSequence", "1");
|
||||
a2.put("routeLegSequence", "2");
|
||||
a2.put("origin", "谅山-春疆货场");
|
||||
a2.put("destination", "胡志明");
|
||||
a2.put("vehicleType", "3.5T");
|
||||
a2.put("costPrice", "9600");
|
||||
a2.put("currency", "CNY");
|
||||
rows.add(a2);
|
||||
JSONObject b1 = new JSONObject(true);
|
||||
b1.put("segmentSequence", "2");
|
||||
b1.put("routeLegSequence", "1");
|
||||
b1.put("origin", "上海");
|
||||
b1.put("destination", "凭祥友谊关口岸");
|
||||
b1.put("vehicleType", "3T");
|
||||
b1.put("costPrice", "7000");
|
||||
rows.add(b1);
|
||||
JSONObject b2 = new JSONObject(true);
|
||||
b2.put("segmentSequence", "2");
|
||||
b2.put("routeLegSequence", "2");
|
||||
b2.put("origin", "凭祥友谊关口岸");
|
||||
b2.put("destination", "胡志明");
|
||||
b2.put("vehicleType", "3T");
|
||||
b2.put("costPrice", "9000");
|
||||
rows.add(b2);
|
||||
facts.put("route_rows", rows);
|
||||
facts.put("landSegments", rows);
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertEquals("上海", read(sheet, 8, 1));
|
||||
assertEquals("谅山-春疆货场", read(sheet, 8, 2));
|
||||
assertEquals("谅山-春疆货场", read(sheet, 27, 1));
|
||||
assertEquals("胡志明", read(sheet, 27, 2));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WO202610080006:跨境整车上传版表头是「时效 | 币种(错行) | 燃油 | 税率 | 含税合计」。
|
||||
* 起始地不能被费用名「运输费」盖掉;CNY 只能进币种列;燃油附加费率要写到燃油列。
|
||||
@@ -2620,6 +2802,18 @@ public class QuoteTemplateFillTest {
|
||||
return cell.getStringCellValue();
|
||||
}
|
||||
|
||||
/** 金额格可能是数字 18200.0 或文本 18200.00,统一成去尾零纯数字再比。 */
|
||||
private static String normalizeMoney(String raw) {
|
||||
if (raw == null || raw.isBlank() || "-".equals(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return new java.math.BigDecimal(raw.trim()).stripTrailingZeros().toPlainString();
|
||||
} catch (RuntimeException ex) {
|
||||
return raw.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运运价表:燃油附加费率、税率必须写到表头同列的数据行。
|
||||
* rate=0.09→9%;rateFlag 是是否含税,不写进税率格。
|
||||
|
||||
Reference in New Issue
Block a user