海运补问保留贸易条款;陆运多车型价表按列回填,车型码不进起始地
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+39
-1
@@ -1374,9 +1374,10 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
JSONArray routeRows = QuoteTemplateExcelSupport.parseFeeRows(q.get("route_rows"));
|
||||
JSONArray rawLand = QuoteTemplateExcelSupport.parseFeeRows(q.get("landSegments"));
|
||||
JSONArray landSegs = routeRows.isEmpty() ? rawLand : routeRows;
|
||||
// 已落库的 route_rows 是规范化扁平行,分档公斤价还在原始 landSegments 上。
|
||||
// 已落库的 route_rows 是规范化扁平行,分档公斤价 / 多车型价表还在原始 landSegments 上。
|
||||
if (!routeRows.isEmpty()) {
|
||||
QuoteTemplateExcelSupport.copyMissingWeightPrices(landSegs, rawLand);
|
||||
QuoteTemplateExcelSupport.copyMissingVehicleRates(landSegs, rawLand);
|
||||
}
|
||||
// 销售已选线路透传:跨境整车国内/境外段按 legs 写完整起讫(多备选混在 route_rows 时不能靠同序列过滤)
|
||||
Object selectedLand = q.get("selected_land_route");
|
||||
@@ -1389,6 +1390,8 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
if (!landSegs.isEmpty()) {
|
||||
// 产品文字价/分档壳偶发无 vehicleType:用询价「车型/数量」补上,否则整车矩阵车型列空、金额只进合计。
|
||||
enrichLandRouteVehicleFromInquiry(landSegs, out, src);
|
||||
// 产品多车型挂段时 OD 偶发空:用询价 facts 顶层 origin/destination 回填(境外段目的地不能空)。
|
||||
fillBlankLandRouteOdFromInquiry(landSegs, out, src);
|
||||
out.put("landSegments", landSegs);
|
||||
out.put("route_rows", landSegs);
|
||||
// 首段时效/燃油/税率抬到顶层,供侧栏与标签回填(WO006 燃油列不能空着有数却写不上)
|
||||
@@ -1522,6 +1525,41 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 线路行缺起运/目的时,用询价顶层 OD / 提货收货地址回填。
|
||||
* <p>
|
||||
* 为何:产品多车型价挂段时智能体偶发只带上提货地址、目的港键名对不上,境外段目的地空着。
|
||||
* 副作用:只改传入行;不写库。
|
||||
*/
|
||||
private static void fillBlankLandRouteOdFromInquiry(JSONArray landSegs, JSONObject out,
|
||||
Map<String, Object> src) {
|
||||
if (landSegs == null || landSegs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String origin = firstNonBlankLocal(
|
||||
out == null ? null : out.getString("origin"),
|
||||
first(src, "起运港", "起运地", "始发站", "始发地", "提货地址", "origin", "pol"));
|
||||
String dest = firstNonBlankLocal(
|
||||
out == null ? null : out.getString("destination"),
|
||||
out == null ? null : out.getString("pod"),
|
||||
first(src, "目的港", "目的地", "收货地址", "送货地址", "destination", "pod"));
|
||||
if (StringUtils.isBlank(origin) && StringUtils.isBlank(dest)) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < landSegs.size(); i++) {
|
||||
JSONObject seg = landSegs.getJSONObject(i);
|
||||
if (seg == null) {
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isBlank(seg.getString("origin")) && StringUtils.isNotBlank(origin)) {
|
||||
seg.put("origin", origin);
|
||||
}
|
||||
if (StringUtils.isBlank(seg.getString("destination")) && StringUtils.isNotBlank(dest)) {
|
||||
seg.put("destination", dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 线路行缺 vehicleType 时,从询价「车型/数量」拆出车型补上。
|
||||
* <p>
|
||||
|
||||
+164
@@ -4209,6 +4209,11 @@ public final class QuoteTemplateExcelSupport {
|
||||
rowObj == null ? null : rowObj.getString("land_freight"),
|
||||
facts == null ? null : facts.getString("land_freight"));
|
||||
}
|
||||
// 1.5) 产品多车型价表:一行挂多个车型价,按表头键对列(5T/8T 同价拆键后分别命中)
|
||||
String fromMap = matchVehicleRateFromMap(rowObj, key, sectionKind);
|
||||
if (StringUtils.isNotBlank(fromMap) && !"-".equals(fromMap)) {
|
||||
return fromMap;
|
||||
}
|
||||
// 2) 车型列:仅当 vehicleType 对得上才写售价(含跨境国内/国外标注换算)
|
||||
String vehicleType = firstNonBlank(rowObj == null ? null : rowObj.getString("vehicleType"),
|
||||
rowObj == null ? null : rowObj.getString("vehicle_type"));
|
||||
@@ -4227,6 +4232,67 @@ public final class QuoteTemplateExcelSupport {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品文字多车型价:landSegments.vehicleRates(或 vehicle_rates)按表头车型匹配金额。
|
||||
* <p>
|
||||
* 为何:群报「5T/8T:13000 10T:14000」会拆成多键同价;不能只用单行 vehicleType+salePrice,
|
||||
* 否则只能填一列。对不上的列空着,禁止把第一档价灌进所有车型列。
|
||||
*/
|
||||
static String matchVehicleRateFromMap(JSONObject rowObj, String headerKey, String sectionKind) {
|
||||
JSONObject rates = vehicleRatesOf(rowObj);
|
||||
if (rates == null || rates.isEmpty() || StringUtils.isBlank(headerKey)) {
|
||||
return "";
|
||||
}
|
||||
for (String vk : rates.keySet()) {
|
||||
if (StringUtils.isBlank(vk)) {
|
||||
continue;
|
||||
}
|
||||
if (!vehicleTypeMatchesSection(vk, headerKey, sectionKind)) {
|
||||
continue;
|
||||
}
|
||||
String amount = StringUtils.trimToEmpty(rates.getString(vk));
|
||||
if (StringUtils.isNotBlank(amount) && !"-".equals(amount)) {
|
||||
return formatTierPrice(amount);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取行上多车型价表;兼容 camel / snake。值为对象 {5T:13000,...}。
|
||||
*/
|
||||
static JSONObject vehicleRatesOf(JSONObject row) {
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
Object raw = row.get("vehicleRates");
|
||||
if (raw == null) {
|
||||
raw = row.get("vehicle_rates");
|
||||
}
|
||||
if (raw instanceof JSONObject) {
|
||||
return (JSONObject) raw;
|
||||
}
|
||||
if (raw instanceof String) {
|
||||
String text = StringUtils.trimToEmpty((String) raw);
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Object parsed = JSON.parse(text);
|
||||
if (parsed instanceof JSONObject) {
|
||||
return (JSONObject) parsed;
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (raw instanceof Map) {
|
||||
Object converted = JSON.toJSON(raw);
|
||||
return converted instanceof JSONObject ? (JSONObject) converted : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 车型列是否对得上当前段表头。
|
||||
* <p>
|
||||
@@ -4673,6 +4739,64 @@ public final class QuoteTemplateExcelSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 route_rows 会丢掉 vehicleRates。出单前从原始 landSegments 补回。
|
||||
*/
|
||||
public static void copyMissingVehicleRates(JSONArray rows, JSONArray rawLand) {
|
||||
if (rows == null || rawLand == null || rawLand.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JSONObject row = rows.getJSONObject(i);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
JSONObject existing = vehicleRatesOf(row);
|
||||
if (existing != null && !existing.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
JSONObject src = i < rawLand.size() ? rawLand.getJSONObject(i) : null;
|
||||
if (vehicleRatesOf(src) == null) {
|
||||
src = findLandSegmentByVehicleRates(rawLand, row);
|
||||
}
|
||||
JSONObject rates = vehicleRatesOf(src);
|
||||
if (rates != null && !rates.isEmpty()) {
|
||||
row.put("vehicleRates", rates);
|
||||
row.put("vehicle_rates", rates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject findLandSegmentByVehicleRates(JSONArray rawLand, JSONObject row) {
|
||||
if (rawLand == null) {
|
||||
return null;
|
||||
}
|
||||
String origin = StringUtils.trimToEmpty(firstNonBlank(
|
||||
row == null ? null : row.getString("origin"),
|
||||
row == null ? null : row.getString("originCode")));
|
||||
String dest = StringUtils.trimToEmpty(firstNonBlank(
|
||||
row == null ? null : row.getString("destination"),
|
||||
row == null ? null : row.getString("destinationCode")));
|
||||
for (int i = 0; i < rawLand.size(); i++) {
|
||||
JSONObject one = rawLand.getJSONObject(i);
|
||||
if (one == null || vehicleRatesOf(one) == null) {
|
||||
continue;
|
||||
}
|
||||
String o = StringUtils.trimToEmpty(firstNonBlank(one.getString("origin"), one.getString("originCode")));
|
||||
String d = StringUtils.trimToEmpty(firstNonBlank(one.getString("destination"), one.getString("destinationCode")));
|
||||
if (origin.equals(o) && dest.equals(d)) {
|
||||
return one;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < rawLand.size(); i++) {
|
||||
JSONObject one = rawLand.getJSONObject(i);
|
||||
if (vehicleRatesOf(one) != null) {
|
||||
return one;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JSONObject findLandSegmentByOriginDest(JSONArray rawLand, JSONObject row) {
|
||||
if (rawLand == null || row == null) {
|
||||
return null;
|
||||
@@ -5250,12 +5374,16 @@ public final class QuoteTemplateExcelSupport {
|
||||
/**
|
||||
* 费用名,不是城市。写进「起始地/目的地」会把深圳盖成运输费。
|
||||
* 只拦常见运价费目,不拦「春疆货场」这类地名。
|
||||
* 车型码(5T/40HQ)也不是地名:产品多车型 fee_rows 误写进起始地列时要拦。
|
||||
*/
|
||||
static boolean looksLikeFeeNameNotPlace(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
String t = value.trim().replaceAll("\\s+", "");
|
||||
if (looksLikeVehicleTypeToken(t)) {
|
||||
return true;
|
||||
}
|
||||
if ("运输费".equals(t) || "提货费".equals(t) || "送货费".equals(t) || "派送费".equals(t)
|
||||
|| "陆运费".equals(t) || "干线费".equals(t) || "空运费".equals(t) || "海运费".equals(t)
|
||||
|| "运费".equals(t) || "燃油附加费".equals(t) || "操作费".equals(t) || "文件费".equals(t)
|
||||
@@ -5267,6 +5395,33 @@ public final class QuoteTemplateExcelSupport {
|
||||
&& !t.contains("站") && !t.contains("仓");
|
||||
}
|
||||
|
||||
/**
|
||||
* 车型口语/表头码:5T、10T、40HQ、45HQ、3.5T、53尺 等。
|
||||
* 产品多车型价表键与矩阵列头同形;禁止当费目名找格、当城市写进起始地。
|
||||
*/
|
||||
static boolean looksLikeVehicleTypeToken(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
String t = normalizeVehicleCompareToken(value);
|
||||
if (t.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (t.matches("^\\d+(\\.\\d+)?T$")) {
|
||||
return true;
|
||||
}
|
||||
if (t.matches("^\\d+(HQ|GP|RH)$")) {
|
||||
return true;
|
||||
}
|
||||
if (t.matches("^\\d+尺$")) {
|
||||
return true;
|
||||
}
|
||||
if (t.contains("平板") || t.contains("厢车") || t.contains("冷柜")) {
|
||||
return t.length() <= 12;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这些列头不是币种:写 CNY 会把覆盖区域的城市、或燃油附加费率的空格盖掉。
|
||||
* 表头是「币种 / 币别」,或上面没有列头时,仍允许写币种。
|
||||
@@ -7530,6 +7685,10 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (StringUtils.isBlank(name) || StringUtils.isBlank(amount) || "-".equals(amount)) {
|
||||
continue;
|
||||
}
|
||||
// 多车型价键(5T/40HQ)禁止当费目名:会命中矩阵列头,把车型写进国内段起始地。
|
||||
if (looksLikeVehicleTypeToken(name)) {
|
||||
continue;
|
||||
}
|
||||
Cell nameCell = findCellByFeeName(sheet, name, usedRows);
|
||||
if (nameCell == null) {
|
||||
nameCell = findCellByLandFeeAlias(sheet, name, usedRows);
|
||||
@@ -7954,6 +8113,11 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (StringUtils.isBlank(text) || isFeeTableHeader(text) || isSectionTitle(sheet, cell)) {
|
||||
continue;
|
||||
}
|
||||
// 跨境整车矩阵列头「10T/40HQ」不是费用名称格
|
||||
if (looksLikeVehicleTypeToken(text) || isLandRouteMatrixHeaderCell(text)
|
||||
|| looksLikeVehicleDimensionText(text)) {
|
||||
continue;
|
||||
}
|
||||
if (feeNameMatches(text, want)) {
|
||||
return cell;
|
||||
}
|
||||
|
||||
+79
@@ -3411,6 +3411,85 @@ public class QuoteTemplateFillTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品多车型价表:一行 vehicleRates 按表头写入多列;禁止无匹配时把第一档灌进所有列。
|
||||
*/
|
||||
@Test
|
||||
public void landProductMultiVehicleRatesFillColumns() throws Exception {
|
||||
Path src = Files.createTempFile("quote-land-mv-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-land-mv-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("跨境整车");
|
||||
Row header = row(sheet, 8);
|
||||
header.createCell(1).setCellValue("起始地");
|
||||
header.createCell(2).setCellValue("目的地");
|
||||
header.createCell(3).setCellValue("5T");
|
||||
header.createCell(4).setCellValue("8T");
|
||||
header.createCell(5).setCellValue("10T");
|
||||
header.createCell(6).setCellValue("40HQ");
|
||||
header.createCell(7).setCellValue("45HQ");
|
||||
header.createCell(8).setCellValue("备注");
|
||||
row(sheet, 9);
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
mapping.put("sheet", "跨境整车");
|
||||
JSONArray regions = new JSONArray();
|
||||
JSONObject region = new JSONObject(true);
|
||||
region.put("title", "项目一");
|
||||
region.put("headerRow", 9);
|
||||
region.put("dataStartRow", 10);
|
||||
JSONObject cols = new JSONObject(true);
|
||||
cols.put("origin", "B");
|
||||
cols.put("destination", "C");
|
||||
cols.put("remark", "I");
|
||||
JSONObject rates = new JSONObject(true);
|
||||
rates.put("5T", "D");
|
||||
rates.put("8T", "E");
|
||||
rates.put("10T", "F");
|
||||
rates.put("40HQ", "G");
|
||||
rates.put("45HQ", "H");
|
||||
cols.put("vehicleRates", rates);
|
||||
region.put("columns", cols);
|
||||
regions.add(region);
|
||||
mapping.put("routeRegions", regions);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("origin", "太原富士康");
|
||||
facts.put("destination", "越南北寧市南山工業區");
|
||||
facts.put("currency", "CNY");
|
||||
facts.put("route_rows",
|
||||
"[{\"origin\":\"太原富士康\",\"destination\":\"越南北寧市南山工業區\","
|
||||
+ "\"currency\":\"CNY\",\"remark\":\"含0点税\","
|
||||
+ "\"vehicleRates\":{\"5T\":\"13000\",\"8T\":\"13000\",\"10T\":\"14000\","
|
||||
+ "\"40HQ\":\"19500\",\"45HQ\":\"19500\"}}]");
|
||||
assertEquals("13000", QuoteTemplateExcelSupport.resolveRouteVehicleRate(
|
||||
JSON.parseObject("{\"vehicleRates\":{\"5T\":\"13000\",\"10T\":\"14000\"}}"),
|
||||
facts, "5T", 5, ""));
|
||||
assertEquals("14000", QuoteTemplateExcelSupport.resolveRouteVehicleRate(
|
||||
JSON.parseObject("{\"vehicleRates\":{\"5T\":\"13000\",\"10T\":\"14000\"}}"),
|
||||
facts, "10T", 5, ""));
|
||||
assertEquals("", QuoteTemplateExcelSupport.resolveRouteVehicleRate(
|
||||
JSON.parseObject("{\"vehicleRates\":{\"5T\":\"13000\"}}"),
|
||||
facts, "40HQ", 5, ""));
|
||||
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("13000", normalizeMoney(read(sheet, 9, 3)));
|
||||
assertEquals("13000", normalizeMoney(read(sheet, 9, 4)));
|
||||
assertEquals("14000", normalizeMoney(read(sheet, 9, 5)));
|
||||
assertEquals("19500", normalizeMoney(read(sheet, 9, 6)));
|
||||
assertEquals("19500", normalizeMoney(read(sheet, 9, 7)));
|
||||
assertEquals("含0点税", read(sheet, 9, 8));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中港:TMS 别名经国内映射后再对等中港表头;跨境:吨位列与 3T 族境外 3.5T;禁止 5T 误命中 3.5T。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user