fix(wms):出库委托单导入出仓日期丢失、升列与单价回填修复

- HeaderListener:parseDate升级为parseDateSmart,兼容Excel日期序列号(20000~60000≈1954~2064年,
  POI DateUtil.getJavaDate转换)及yyyy/M/d、yyyy.M.d、yyyy年M月d日等多格式文本;
  出仓日期/完成时间/下单日期三处调用点同步切换;解析失败记入dateParseErrors不再静默
- ExcelParseService:导入校验汇总dateParseErrors,日期单元格有值但格式不识别时阻断导入并
  明确提示(为空仍放行,与原行为一致);单价回填从料号分支移至料号/SKU/名称三分支统一回填块,
  补齐SKU、商品名称匹配场景
- ItemListener:升列新增normalizeNumeric归一化,去千分位逗号、空值统一存null、纯数字去尾零,
  落库值恒为"纯数字或null",前端合计不再产生NaN/拼接
This commit is contained in:
rcx
2026-08-21 14:56:12 +08:00
parent 27f3fceb6e
commit 3aec384ba0
3 changed files with 71 additions and 24 deletions
@@ -167,6 +167,10 @@ public class ExcelParseService {
stockOutOrder.setCheckTaskDistribution(2);
//校验必填字段
StringBuilder errorMsg = check(stockOutOrder);
// 日期单元格有值但解析失败:与物料一致性错误同等对待,阻断导入并明确提示,杜绝静默丢日期
for (String dateError : headerListener.getDateParseErrors()) {
errorMsg.append(dateError).append("; ");
}
// 物料一致性校验错误与必填校验错误合并,任一有错都不落库
if (materialConsistencyErrors.length() > 0) {
errorMsg.insert(0, materialConsistencyErrors);
@@ -453,8 +457,6 @@ public class ExcelParseService {
if (materialList != null && !materialList.isEmpty()) {
materialBaseInfoPO = materialList.get(0);
materialBaseInfoId = materialBaseInfoPO.getMaterialBaseInfoId();
BigDecimal unitPrice = materialBaseInfoPO.getUnitPrice();
outMaterialDetailDO.setUnitPrice(unitPrice);
// ==== 新增:料号与规格型号/SKU码一致性校验(档案有值就必须一致,Excel留空视为不一致;档案为空跳过)====
String excelSpec = StringUtils.trimToEmpty(outMaterialDetailDO.getSpecificationModel());
if (StringUtils.isNotBlank(materialBaseInfoPO.getSpecificationModel())
@@ -516,6 +518,8 @@ public class ExcelParseService {
// 设置物料基础信息ID,并从物料基础信息中回填料号、物料编码、物料名称等信息
if (materialBaseInfoId != null && materialBaseInfoPO != null) {
outMaterialDetailDO.setMaterialBaseInfoId(materialBaseInfoId);
// 单价统一从物料档案回填(原逻辑仅料号匹配时回填,SKU/商品名称匹配时漏填,导致编辑页部分行单价为空)
outMaterialDetailDO.setUnitPrice(materialBaseInfoPO.getUnitPrice());
// 如果Excel中没有料号,从物料基础信息中回填
if (StringUtils.isBlank(outMaterialDetailDO.getMaterialNo())) {
outMaterialDetailDO.setMaterialNo(materialBaseInfoPO.getMaterialCode());
@@ -17,6 +17,9 @@ import java.util.List;
import java.util.Map;
import java.util.Collections;
import java.util.ArrayList;
import org.apache.poi.ss.usermodel.DateUtil; // Excel序列号转日期,POI已随EasyExcel引入
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* 解析表头信息的监听器(处理前11行)
@@ -25,6 +28,8 @@ import java.util.ArrayList;
public class HeaderListener extends AnalysisEventListener<Map<Integer, Object>> {
private final StockOutOrderDO stockOutOrder = new StockOutOrderDO();
private int rowNum = 0;
/** 日期单元格有值但格式无法识别的收集器(类上已有@Getter,自动生成getDateParseErrors() */
private final List<String> dateParseErrors = new ArrayList<>();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
@@ -55,10 +60,9 @@ public class HeaderListener extends AnalysisEventListener<Map<Integer, Object>>
stockOutOrder.setDriverSign(getStringValue(rowData[16]));
break;
case 6: // 第7行:出仓日期、fax、运输方式、申报地关区
LocalDate localDate = parseDate(getStringValue(rowData[2]));
if (localDate != null) {
Date date = Date.from(localDate.atStartOfDay(ZoneId.of("UTC")).toInstant());
stockOutOrder.setOutboundDate(date);
Date outboundDate = parseDateSmart(getStringValue(rowData[2]), "出仓日期");
if (outboundDate != null) {
stockOutOrder.setOutboundDate(outboundDate);
}
stockOutOrder.setFax(getStringValue(rowData[5]));
stockOutOrder.setTransportModeName(getStringValue(rowData[12]));
@@ -68,10 +72,9 @@ public class HeaderListener extends AnalysisEventListener<Map<Integer, Object>>
stockOutOrder.setManufacturer(getStringValue(rowData[2]));
stockOutOrder.setCustomerName(getStringValue(rowData[5]));
stockOutOrder.setCustomsDeclarerInfo(getStringValue(rowData[12]));
LocalDate CompleteTime = parseDate(getStringValue(rowData[16]));
if (CompleteTime != null) {
Date date = Date.from(CompleteTime.atStartOfDay(ZoneId.of("UTC")).toInstant());
stockOutOrder.setCompleteTime(date);
Date completeTime = parseDateSmart(getStringValue(rowData[16]), "完成时间");
if (completeTime != null) {
stockOutOrder.setCompleteTime(completeTime);
}
break;
case 8: // 第9行:监管方式、送货地址、海关是否查验货物、进出境关别
@@ -82,10 +85,9 @@ public class HeaderListener extends AnalysisEventListener<Map<Integer, Object>>
stockOutOrder.setEntryExitCustoms(getStringValue(rowData[16]));
break;
case 9: // 第10行:下单日期、起运国
LocalDate orderDate = parseDate(getStringValue(rowData[2]));
Date orderDate = parseDateSmart(getStringValue(rowData[2]), "下单日期");
if (orderDate != null) {
Date date = Date.from(orderDate.atStartOfDay(ZoneId.of("UTC")).toInstant());
stockOutOrder.setOrderDate(date);
stockOutOrder.setOrderDate(orderDate);
}
stockOutOrder.setDepartureArrivalCountry(getStringValue(rowData[5]));
break;
@@ -160,19 +162,41 @@ public class HeaderListener extends AnalysisEventListener<Map<Integer, Object>>
}
/**
* 解析日期
* 智能解析日期:兼容 Excel 日期单元格序列号 + 常见文本格式。
* 解析失败记入 dateParseErrors,由 ExcelParseService 汇总返回给用户,不再静默丢失。
* @param raw 单元格原始字符串
* @param fieldLabel 字段中文名(用于错误提示)
*/
private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isEmpty()) {
return null;
private Date parseDateSmart(String raw, String fieldLabel) {
String s = raw == null ? "" : raw.trim();
if (s.isEmpty()) {
return null; // 单元格为空:保持原行为,不算错误
}
try {
if (dateStr.contains(" ")) {
dateStr = dateStr.split(" ")[0];
// 1) Excel 日期序列号:NoConvertConverter 会把真日期单元格转成纯数字字符串(如 46234 = 2026-08-20
// 区间 20000~60000 对应 1954~2064 年,避免把普通数字误判成日期
if (s.matches("^\\d{1,5}(\\.\\d+)?$")) {
try {
double serial = Double.parseDouble(s);
if (serial >= 20000 && serial <= 60000) {
return DateUtil.getJavaDate(serial);
}
} catch (NumberFormatException ignore) {
}
return LocalDate.parse(dateStr, dateFormatter);
} catch (Exception e) {
return null;
}
// 2) 常见文本格式逐个尝试
String[] patterns = {
"yyyy-MM-dd", "yyyy/M/d", "yyyy.M.d", "yyyy年M月d日",
"yyyy-MM-dd HH:mm:ss", "yyyy/M/d H:mm:ss", "yyyy-MM-dd H:mm"
};
for (String p : patterns) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(p);
sdf.setLenient(false);
return sdf.parse(s);
} catch (ParseException ignore) {
}
}
dateParseErrors.add(fieldLabel + "" + s + "」格式无法识别,请改为日期格式或 2026-08-20 / 2026/8/20 / 2026年8月20日 等格式");
return null;
}
}
@@ -57,7 +57,7 @@ public class ItemListener extends AnalysisEventListener<Map<Integer, Object>> {
item.setInvoiceNo(getStringValue(rowData[5]));
item.setDeclareQuantity(parseBigDecimal(rowData[6]));
item.setDeclareUnit(getStringValue(rowData[7]));
item.setLiter(getStringValue(rowData[8]));
item.setLiter(normalizeNumeric(getStringValue(rowData[8])));
item.setCaseCount(parseBigDecimal(rowData[9]));
item.setPieceCount(parseBigDecimal(rowData[10]));
item.setSize(getStringValue(rowData[11]));
@@ -285,6 +285,25 @@ public class ItemListener extends AnalysisEventListener<Map<Integer, Object>> {
}
}
/**
* 升列归一化:去千分位逗号、去空白;空值统一存 null(避免前端合计时对空串求和);
* 纯数字归一为无多余零的字符串;非数字内容原样保留。
*/
private String normalizeNumeric(String raw) {
if (raw == null) {
return null;
}
String s = raw.trim().replace(",", "");
if (s.isEmpty()) {
return null;
}
try {
return new BigDecimal(s).stripTrailingZeros().toPlainString();
} catch (NumberFormatException e) {
return s;
}
}
public static boolean isSequenceNumber(String str) {
if (str == null || str.trim().isEmpty()) {