feat(oms):出入库导入按编号自动匹配名称并校验客户/物料存在性,模板列位改为动态识别
需求:①入库业务单导入模板只填物料编号+客户编号,物料名称/客户名称按编号自动匹配库中数据 ②③出库导入校验客户/物料是否已存在,不存在提示"进行维护" ④库中查询结果为空(确无数据)时报错 一、入库导入(ReservationStockInOrderImpl.parseStockInExcel): 1. 物料逐编号查库,缺失料号收集后一次性报错"物料[xx]不存在,请先维护物料信息后再导入" 2. 明细新增客户编号:去重后查库,为空报错"客户[编号]不存在,请先维护客户信息后再导入"; 查到则以该客户为单据货主并自动匹配名称;与物料归属货主不一致时拦截并提示两者编号 3. 明细回填库中物料名称;未填客户编号时维持原"货主取自物料"逻辑,兼容老模板 二、出库导入(parseExcelFile): 1. 物料/客户校验前移到落库前(原为先落库后查客户,报错会留半截数据) 2. 物料查不到收集后报错维护提示;客户查不到报错维护提示,客户ID/名称改取库中匹配值 (替换仅返回id且查不到时静默存null的queryShipperId) 3. 產品編號/客户編号漏填的行直接报错提示补全 三、模板解析改为按列头文字动态定位列(繁简体归一化),列/行顺序调整不影响解析, 识别不到时回落老模板固定列位,老模板行为不变: 1. StockInExcelListener(入库):以產品編號为锚点识别列头行,新增客戶編號列支持 2. DynamicDeliveryExcelListener(出库):左右分栏各自锚点识别;送货日期/线号/车次/ 发票张数/小计/纪录合共改按标签文字定位,行号列号不再写死 四、Mapper:ReservationStockOutOrderMapper新增queryCustomerByCode(按组织查询, USER_MEMBER_CODE或USER_SHIPPER.CUSTOMER_NC_CODE任一命中),删除queryShipperId
This commit is contained in:
+350
-167
@@ -21,20 +21,47 @@ import java.util.Map;
|
||||
/**
|
||||
* 维他奶出库Excel解析监听器(新模板版)
|
||||
* 新模板特点:货品明细与客户配送明细左右分栏展示
|
||||
* 核心功能:
|
||||
* 1. 左侧栏:第1-3行解析表头(送货日期/送货线号/车次),第5行解析纪录合共,第7行起解析货品明细
|
||||
* 2. 右侧栏:第2行解析发票张数/小计,第4行起解析客户配送明细
|
||||
* 3. 自动处理文本型数字转换(String→Integer/BigDecimal)
|
||||
* 4. 完整的异常处理和日志记录
|
||||
* 核心功能(行列位置均不写死,模板调整行/列顺序不影响解析):
|
||||
* 1. 表头标签(送货日期/送货线号/车次/发票张数/小计/纪录合共):按标签文字定位标签所在列,取右侧相邻列的值
|
||||
* 2. 货品明细:按列头文字(貨品編號/簡稱/箱/支等,繁简体归一化)定位各列,列头行之后的行按数据解析
|
||||
* 3. 客户配送明细:按列头文字(類别/發票編號/客户編號/名稱/箱/包/金額等)定位各列
|
||||
* 4. 列头行识别失败时兜底:按老模板固定布局(左栏A-D列、右栏F-L列、行号1-7)解析,保证老模板不受影响
|
||||
* 5. 自动处理文本型数字转换(String→Integer/BigDecimal)
|
||||
*/
|
||||
public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<String, Object>> {
|
||||
// 日志实例
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDeliveryExcelListener.class);
|
||||
|
||||
// 关键字定义(Excel标签匹配)
|
||||
private static final String KEYWORD_TOTAL_RECORD = "紀錄合共";
|
||||
private static final String KEYWORD_INVOICE_COUNT = "發票張數";
|
||||
private static final String KEYWORD_SUBTOTAL = "小計";
|
||||
// ---------------------- 关键字/列头别名定义(归一化后匹配,兼容简繁体) ----------------------
|
||||
// 表头标签关键字
|
||||
private static final String[] KEYWORD_DELIVERY_DATE = {"送货日期"};
|
||||
private static final String[] KEYWORD_DELIVERY_LINE = {"送货线号", "送货线路"};
|
||||
private static final String[] KEYWORD_TRAIN = {"车次"};
|
||||
private static final String[] KEYWORD_TOTAL_RECORD = {"纪录合共"};
|
||||
private static final String[] KEYWORD_INVOICE_COUNT = {"发票张数"};
|
||||
private static final String[] KEYWORD_SUBTOTAL = {"小计"};
|
||||
|
||||
// 货品明细列头别名
|
||||
private static final String[] PRODUCT_CODE_ALIASES = {"货品编号", "产品编号", "物料编号", "货品编码", "产品编码", "物料编码", "料号"};
|
||||
private static final String[] PRODUCT_NAME_ALIASES = {"简称", "货品名称", "产品名称", "物料名称", "品名", "名称"};
|
||||
private static final String[] PRODUCT_BOX_ALIASES = {"箱数", "箱"};
|
||||
private static final String[] PRODUCT_PIECE_ALIASES = {"支数", "件数", "支"};
|
||||
|
||||
// 客户配送明细列头别名
|
||||
private static final String[] CATEGORY_ALIASES = {"类别"};
|
||||
private static final String[] INVOICE_NO_ALIASES = {"发票编号", "发票号", "发票"};
|
||||
private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"};
|
||||
private static final String[] CUSTOMER_NAME_ALIASES = {"客户名称", "名称"};
|
||||
private static final String[] CUSTOMER_BOX_ALIASES = {"箱数", "箱"};
|
||||
private static final String[] CUSTOMER_PACKAGE_ALIASES = {"包数", "包"};
|
||||
private static final String[] CUSTOMER_AMOUNT_ALIASES = {"金额", "总额"};
|
||||
|
||||
// 列头行认定门槛:锚点列(编号列)必须识别到,且识别列总数达标,防止数据行误判为列头行
|
||||
private static final int HEADER_MIN_MATCH_COUNT = 2;
|
||||
// 老模板兜底:货品明细数据固定从第7行、客户配送明细数据固定从第4行开始(列头未识别到时启用固定列位)
|
||||
private static final int PRODUCT_DATA_START_ROW = 7;
|
||||
private static final int CUSTOMER_DATA_START_ROW = 4;
|
||||
|
||||
// 支持的日期格式(String转Date)
|
||||
private static final String[] SUPPORTED_DATE_FORMATS = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"};
|
||||
|
||||
@@ -43,6 +70,26 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
private final List<ProductDetail> productDetailList = new ArrayList<>();
|
||||
private final List<CustomerDeliveryDetail> customerDeliveryDetailList = new ArrayList<>();
|
||||
|
||||
// 货品明细列索引(-1表示未识别该列)
|
||||
private int productCodeCol = -1;
|
||||
private int productNameCol = -1;
|
||||
private int productBoxCol = -1;
|
||||
private int productPieceCol = -1;
|
||||
private boolean productColumnsResolved = false;
|
||||
|
||||
// 客户配送明细列索引(-1表示未识别该列)
|
||||
private int categoryCol = -1;
|
||||
private int invoiceNoCol = -1;
|
||||
private int customerCodeCol = -1;
|
||||
private int customerNameCol = -1;
|
||||
private int customerBoxCol = -1;
|
||||
private int customerPackageCol = -1;
|
||||
private int customerAmountCol = -1;
|
||||
private boolean customerColumnsResolved = false;
|
||||
|
||||
// 发票张数/小计行是否已解析
|
||||
private boolean invoiceRowParsed = false;
|
||||
|
||||
// 当前行号(1-based,与Excel行号一致)
|
||||
private int currentRowNum = 0;
|
||||
|
||||
@@ -53,10 +100,10 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
try {
|
||||
LOGGER.debug("第{}行:开始解析,所有单元格数据:{}", currentRowNum, data);
|
||||
|
||||
// 左侧区域解析(A-E列)
|
||||
// 左侧区域解析(货品相关信息)
|
||||
parseLeftSection(data);
|
||||
|
||||
// 右侧区域解析(F-M列)
|
||||
// 右侧区域解析(客户配送相关信息)
|
||||
parseRightSection(data);
|
||||
|
||||
} catch (ExcelDataConvertException e) {
|
||||
@@ -71,67 +118,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
|
||||
// ---------------------- 左侧区域解析(货品相关信息) ----------------------
|
||||
private void parseLeftSection(Map<String, Object> data) {
|
||||
switch (currentRowNum) {
|
||||
case 1:
|
||||
// 第1行:送货日期(A列=标签,B列=值)
|
||||
parseDeliveryDate(data);
|
||||
break;
|
||||
case 2:
|
||||
// 第2行:送货线号(A列=标签,B列=值)
|
||||
parseDeliveryLineNo(data);
|
||||
break;
|
||||
case 3:
|
||||
// 第3行:车次(A列=标签,B列=值)
|
||||
parseTrainNumber(data);
|
||||
break;
|
||||
case 4:
|
||||
// 第4行:货品明细标题行,无需解析
|
||||
LOGGER.debug("第{}行:货品明细标题行,跳过", currentRowNum);
|
||||
break;
|
||||
case 5:
|
||||
// 第5行:纪录合共(A列=标签,B列=总记录数,D列=总箱数,E列=总支数)
|
||||
parseTotalRecord(data);
|
||||
break;
|
||||
case 6:
|
||||
// 第6行:货品明细列标题(貨品編號/簡稱/箱/支),跳过
|
||||
LOGGER.debug("第{}行:货品明细列标题行,跳过", currentRowNum);
|
||||
break;
|
||||
default:
|
||||
// 第7行起:货品明细数据行(遇到空行自动结束)
|
||||
if (currentRowNum >= 7 && isValidProductDetailRow(data)) {
|
||||
parseProductDetail(data, currentRowNum);
|
||||
}
|
||||
break;
|
||||
// 1. 表头标签:送货日期/送货线号/车次(按标签文字定位,行位置不限)
|
||||
parseHeaderLabels(data);
|
||||
// 2. 纪录合共(按标签文字定位,行位置不限)
|
||||
parseTotalRecord(data);
|
||||
// 3. 货品明细:列布局已确定时按数据行解析
|
||||
if (productColumnsResolved) {
|
||||
if (isValidProductDetailRow(data)) {
|
||||
parseProductDetail(data, currentRowNum);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 4. 尝试识别货品明细列头行;识别成功则本行即列头行,跳过数据解析
|
||||
if (tryResolveProductColumns(data)) {
|
||||
return;
|
||||
}
|
||||
// 5. 超过老模板数据起始行仍未识别到列头行:启用固定列位兜底,从当前行开始按数据行解析
|
||||
if (currentRowNum >= PRODUCT_DATA_START_ROW) {
|
||||
applyProductFallbackColumns();
|
||||
if (isValidProductDetailRow(data)) {
|
||||
parseProductDetail(data, currentRowNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 右侧区域解析(客户配送相关信息) ----------------------
|
||||
private void parseRightSection(Map<String, Object> data) {
|
||||
switch (currentRowNum) {
|
||||
case 1:
|
||||
// 第1行:客户配送明细标题行,无需解析
|
||||
LOGGER.debug("第{}行:客户配送明细标题行,跳过", currentRowNum);
|
||||
break;
|
||||
case 2:
|
||||
// 第2行:发票张数/小计(F列=發票張數标签,G列=数值;J列=小計标签,K/L/M列=数值)
|
||||
parseInvoiceCount(data);
|
||||
break;
|
||||
case 3:
|
||||
// 第3行:客户配送明细列标题(類别/發票編號/客户編號/名稱/箱/包/金額),跳过
|
||||
LOGGER.debug("第{}行:客户配送明细列标题行,跳过", currentRowNum);
|
||||
break;
|
||||
default:
|
||||
// 第4行起:客户配送明细数据行(遇到空行自动结束)
|
||||
if (currentRowNum >= 4 && isValidCustomerDetailRow(data)) {
|
||||
parseCustomerDeliveryDetail(data, currentRowNum);
|
||||
}
|
||||
break;
|
||||
// 1. 发票张数/小计(按标签文字定位,行位置不限,解析成功后不再重复解析)
|
||||
if (!invoiceRowParsed) {
|
||||
parseInvoiceRow(data);
|
||||
}
|
||||
// 2. 客户配送明细:列布局已确定时按数据行解析
|
||||
if (customerColumnsResolved) {
|
||||
if (isValidCustomerDetailRow(data)) {
|
||||
parseCustomerDeliveryDetail(data, currentRowNum);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 3. 尝试识别客户配送明细列头行;识别成功则本行即列头行,跳过数据解析
|
||||
if (tryResolveCustomerColumns(data)) {
|
||||
return;
|
||||
}
|
||||
// 4. 超过老模板数据起始行仍未识别到列头行:启用固定列位兜底,从当前行开始按数据行解析
|
||||
if (currentRowNum >= CUSTOMER_DATA_START_ROW) {
|
||||
applyCustomerFallbackColumns();
|
||||
if (isValidCustomerDetailRow(data)) {
|
||||
parseCustomerDeliveryDetail(data, currentRowNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 解析完成回调 ----------------------
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// 小计标签始终未识别到时保持与原逻辑一致:小计相关字段置0
|
||||
if (deliveryHeader.getSubtotalAmount() == null) {
|
||||
LOGGER.warn("未找到【小计】标签,小计信息置空");
|
||||
deliveryHeader.setSubtotalBoxCount(0);
|
||||
deliveryHeader.setSubtotalPieceCount(0);
|
||||
deliveryHeader.setSubtotalAmount(BigDecimal.ZERO);
|
||||
}
|
||||
LOGGER.info("Excel解析全部完成!最终统计结果:");
|
||||
LOGGER.info("1. 表头信息:送货日期={},送货线号={},车次={},发票张数={},总记录数={},小计金额={}",
|
||||
deliveryHeader.getDeliveryDate(),
|
||||
@@ -144,47 +190,52 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
LOGGER.info("3. 客户配送明细:共{}条记录", customerDeliveryDetailList.size());
|
||||
}
|
||||
|
||||
// ---------------------- 左侧:表头信息解析 ----------------------
|
||||
private void parseDeliveryDate(Map<String, Object> data) {
|
||||
Object value = getValueSafely(data, 1); // B列
|
||||
if (value != null) {
|
||||
Date deliveryDate = convertToDate(value);
|
||||
deliveryHeader.setDeliveryDate(deliveryDate);
|
||||
LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate);
|
||||
// ---------------------- 左侧:表头信息解析(标签定位,行位置不限) ----------------------
|
||||
private void parseHeaderLabels(Map<String, Object> data) {
|
||||
// 送货日期
|
||||
if (deliveryHeader.getDeliveryDate() == null) {
|
||||
Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_DATE);
|
||||
if (keywordIndex != null) {
|
||||
Date deliveryDate = convertToDate(getValueSafely(data, keywordIndex + 1));
|
||||
if (deliveryDate != null) {
|
||||
deliveryHeader.setDeliveryDate(deliveryDate);
|
||||
LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 送货线号
|
||||
if (deliveryHeader.getDeliveryLineNo() == null || deliveryHeader.getDeliveryLineNo().trim().isEmpty()) {
|
||||
Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_LINE);
|
||||
if (keywordIndex != null) {
|
||||
Object value = getValueSafely(data, keywordIndex + 1);
|
||||
if (value != null && !value.toString().trim().isEmpty()) {
|
||||
deliveryHeader.setDeliveryLineNo(value.toString().trim());
|
||||
LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryHeader.getDeliveryLineNo());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 车次
|
||||
if (deliveryHeader.getTrainNumber() == null) {
|
||||
Integer keywordIndex = findKeywordIndex(data, KEYWORD_TRAIN);
|
||||
if (keywordIndex != null) {
|
||||
deliveryHeader.setTrainNumber(convertToInteger(getValueSafely(data, keywordIndex + 1)));
|
||||
LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, deliveryHeader.getTrainNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseDeliveryLineNo(Map<String, Object> data) {
|
||||
Object value = getValueSafely(data, 1); // B列
|
||||
if (value != null) {
|
||||
String deliveryLineNo = value.toString().trim();
|
||||
deliveryHeader.setDeliveryLineNo(deliveryLineNo);
|
||||
LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryLineNo);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseTrainNumber(Map<String, Object> data) {
|
||||
Object value = getValueSafely(data, 1); // B列
|
||||
Integer trainNumber = convertToInteger(value);
|
||||
deliveryHeader.setTrainNumber(trainNumber);
|
||||
LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, trainNumber);
|
||||
}
|
||||
|
||||
// ---------------------- 左侧:纪录合共解析 ----------------------
|
||||
// ---------------------- 左侧:纪录合共解析(标签定位,行位置不限) ----------------------
|
||||
/**
|
||||
* 第5行:纪录合共
|
||||
* 列对应关系:0=A列=紀錄合共标签,1=B列=总记录数,3=D列=总箱数,4=E列=总支数
|
||||
* 纪录合共:标签右侧第1列=总记录数,第3列=总箱数,第4列=总支数(与老模板B/D/E列偏移一致)
|
||||
*/
|
||||
private void parseTotalRecord(Map<String, Object> data) {
|
||||
Object totalRecordLabel = getValueSafely(data, 0);
|
||||
if (totalRecordLabel == null || !totalRecordLabel.toString().trim().contains(KEYWORD_TOTAL_RECORD)) {
|
||||
LOGGER.warn("第{}行:不是纪录合共行,跳过解析,第一列内容:{}", currentRowNum, totalRecordLabel);
|
||||
Integer keywordIndex = findKeywordIndex(data, KEYWORD_TOTAL_RECORD);
|
||||
if (keywordIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer totalRecordCount = convertToInteger(getValueSafely(data, 1)); // B列
|
||||
Integer totalBoxCount = convertToInteger(getValueSafely(data, 3)); // D列
|
||||
Integer totalPieceCount = convertToInteger(getValueSafely(data, 4)); // E列
|
||||
Integer totalRecordCount = convertToInteger(getValueSafely(data, keywordIndex + 1));
|
||||
Integer totalBoxCount = convertToInteger(getValueSafely(data, keywordIndex + 3));
|
||||
Integer totalPieceCount = convertToInteger(getValueSafely(data, keywordIndex + 4));
|
||||
|
||||
deliveryHeader.setTotalRecordCount(totalRecordCount);
|
||||
deliveryHeader.setTotalBoxCount(totalBoxCount);
|
||||
@@ -194,27 +245,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
currentRowNum, totalRecordCount, totalBoxCount, totalPieceCount);
|
||||
}
|
||||
|
||||
// ---------------------- 左侧:货品明细解析 ----------------------
|
||||
// ---------------------- 左侧:货品明细列头识别与数据解析 ----------------------
|
||||
/**
|
||||
* 校验货品明细行是否有效(排除表头和空行)
|
||||
* 尝试将当前行识别为货品明细列头行:以货品编号列为锚点,按列头文字(简繁归一化)匹配各列
|
||||
*/
|
||||
private boolean isValidProductDetailRow(Map<String, Object> data) {
|
||||
Object productCode = getValueSafely(data, 0); // A列
|
||||
return productCode != null
|
||||
&& !productCode.toString().trim().isEmpty()
|
||||
&& !"貨品編號".equals(productCode.toString().trim());
|
||||
private boolean tryResolveProductColumns(Map<String, Object> data) {
|
||||
int codeCol = findColumnByAliases(data, PRODUCT_CODE_ALIASES);
|
||||
if (codeCol < 0) {
|
||||
return false;
|
||||
}
|
||||
int nameCol = findColumnByAliases(data, PRODUCT_NAME_ALIASES);
|
||||
int boxCol = findColumnByAliases(data, PRODUCT_BOX_ALIASES);
|
||||
int pieceCol = findColumnByAliases(data, PRODUCT_PIECE_ALIASES);
|
||||
int matched = 1 + (nameCol >= 0 ? 1 : 0) + (boxCol >= 0 ? 1 : 0) + (pieceCol >= 0 ? 1 : 0);
|
||||
if (matched < HEADER_MIN_MATCH_COUNT) {
|
||||
return false;
|
||||
}
|
||||
productCodeCol = codeCol;
|
||||
productNameCol = nameCol;
|
||||
productBoxCol = boxCol;
|
||||
productPieceCol = pieceCol;
|
||||
productColumnsResolved = true;
|
||||
LOGGER.info("第{}行识别为货品明细列头行,列映射:货品编号={},名称={},箱={},支={}",
|
||||
currentRowNum, productCodeCol, productNameCol, productBoxCol, productPieceCol);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析货品明细(第7行起)
|
||||
* 列对应关系:0=A列=貨品編號,1=B列=簡稱,2=C列=箱,3=D列=支
|
||||
* 兜底列位:老模板货品明细固定布局(0=A列=貨品編號,1=B列=簡稱,2=C列=箱,3=D列=支)
|
||||
*/
|
||||
private void applyProductFallbackColumns() {
|
||||
productCodeCol = 0;
|
||||
productNameCol = 1;
|
||||
productBoxCol = 2;
|
||||
productPieceCol = 3;
|
||||
productColumnsResolved = true;
|
||||
LOGGER.warn("第{}行前未识别到货品明细列头行,启用老模板固定列位兜底解析", currentRowNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验货品明细行是否有效(排除列头行和空行)
|
||||
*/
|
||||
private boolean isValidProductDetailRow(Map<String, Object> data) {
|
||||
Object productCode = getValueSafely(data, productCodeCol);
|
||||
if (productCode == null || productCode.toString().trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 列头文字本身不算数据行(防止列头行被当作数据重复解析)
|
||||
String text = normalizeHeaderText(productCode);
|
||||
return !matchesAny(text, PRODUCT_CODE_ALIASES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析货品明细(列头行之后的行)
|
||||
*/
|
||||
private void parseProductDetail(Map<String, Object> data, int rowNum) {
|
||||
ProductDetail productDetail = new ProductDetail();
|
||||
productDetail.setProductCode(convertToString(getValueSafely(data, 0)));
|
||||
productDetail.setProductName(convertToString(getValueSafely(data, 1)));
|
||||
productDetail.setBoxCount(convertToInteger(getValueSafely(data, 2)));
|
||||
productDetail.setPieceCount(convertToInteger(getValueSafely(data, 3)));
|
||||
productDetail.setProductCode(convertToString(getValueSafely(data, productCodeCol)));
|
||||
productDetail.setProductName(convertToString(getValueSafely(data, productNameCol)));
|
||||
productDetail.setBoxCount(convertToInteger(getValueSafely(data, productBoxCol)));
|
||||
productDetail.setPieceCount(convertToInteger(getValueSafely(data, productPieceCol)));
|
||||
|
||||
productDetailList.add(productDetail);
|
||||
LOGGER.debug("第{}行:解析货品明细完成,货品编号={},名称={},箱数={},支数={}",
|
||||
@@ -222,67 +312,105 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
productDetail.getBoxCount(), productDetail.getPieceCount());
|
||||
}
|
||||
|
||||
// ---------------------- 右侧:发票张数与小计解析 ----------------------
|
||||
/**
|
||||
* 第2行:发票张数与小计
|
||||
* 通过关键字定位标签,再读取右侧相邻列,避免空列导致索引偏移
|
||||
*/
|
||||
private void parseInvoiceCount(Map<String, Object> data) {
|
||||
// 1. 定位并解析发票张数
|
||||
Integer invoiceCount = findValueAfterKeyword(data, KEYWORD_INVOICE_COUNT);
|
||||
if (invoiceCount != null) {
|
||||
deliveryHeader.setInvoiceCount(invoiceCount);
|
||||
// ---------------------- 右侧:发票张数与小计解析(标签定位,行位置不限) ----------------------
|
||||
private void parseInvoiceRow(Map<String, Object> data) {
|
||||
// 1. 发票张数:标签右侧相邻列
|
||||
Integer invoiceIndex = findKeywordIndex(data, KEYWORD_INVOICE_COUNT);
|
||||
if (invoiceIndex != null) {
|
||||
deliveryHeader.setInvoiceCount(convertToInteger(getValueSafely(data, invoiceIndex + 1)));
|
||||
}
|
||||
|
||||
// 2. 定位并解析小计(标签右侧第1列=箱,第2列=支,第3列=金额)
|
||||
Integer subtotalStartIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL);
|
||||
if (subtotalStartIndex != null && subtotalStartIndex >= 0) {
|
||||
Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 1));
|
||||
Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 2));
|
||||
BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalStartIndex + 3));
|
||||
// 2. 小计:标签右侧第1列=箱,第2列=支,第3列=金额
|
||||
Integer subtotalIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL);
|
||||
if (subtotalIndex != null && subtotalIndex >= 0) {
|
||||
Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalIndex + 1));
|
||||
Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalIndex + 2));
|
||||
BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalIndex + 3));
|
||||
|
||||
deliveryHeader.setSubtotalBoxCount(subtotalBoxCount);
|
||||
deliveryHeader.setSubtotalPieceCount(subtotalPieceCount);
|
||||
deliveryHeader.setSubtotalAmount(subtotalAmount);
|
||||
invoiceRowParsed = true;
|
||||
|
||||
LOGGER.debug("第{}行:解析小计完成,箱数={},支数={},金额={}",
|
||||
currentRowNum, subtotalBoxCount, subtotalPieceCount, subtotalAmount);
|
||||
} else {
|
||||
LOGGER.warn("第{}行:未找到小计标签,小计信息置空", currentRowNum);
|
||||
deliveryHeader.setSubtotalBoxCount(0);
|
||||
deliveryHeader.setSubtotalPieceCount(0);
|
||||
deliveryHeader.setSubtotalAmount(BigDecimal.ZERO);
|
||||
LOGGER.debug("第{}行:解析发票张数行完成,发票张数={},小计箱数={},支数={},金额={}",
|
||||
currentRowNum, deliveryHeader.getInvoiceCount(), subtotalBoxCount, subtotalPieceCount, subtotalAmount);
|
||||
}
|
||||
|
||||
LOGGER.debug("第{}行:解析发票张数行完成,发票张数={},小计金额={}",
|
||||
currentRowNum, deliveryHeader.getInvoiceCount(), deliveryHeader.getSubtotalAmount());
|
||||
}
|
||||
|
||||
// ---------------------- 右侧:客户配送明细解析 ----------------------
|
||||
// ---------------------- 右侧:客户配送明细列头识别与数据解析 ----------------------
|
||||
/**
|
||||
* 校验客户配送明细行是否有效(排除表头和空行)
|
||||
* 尝试将当前行识别为客户配送明细列头行:以客户编号列为锚点,按列头文字(简繁归一化)匹配各列
|
||||
*/
|
||||
private boolean tryResolveCustomerColumns(Map<String, Object> data) {
|
||||
int codeCol = findColumnByAliases(data, CUSTOMER_CODE_ALIASES);
|
||||
if (codeCol < 0) {
|
||||
return false;
|
||||
}
|
||||
int categoryColTmp = findColumnByAliases(data, CATEGORY_ALIASES);
|
||||
int invoiceNoColTmp = findColumnByAliases(data, INVOICE_NO_ALIASES);
|
||||
int nameColTmp = findColumnByAliases(data, CUSTOMER_NAME_ALIASES);
|
||||
int boxColTmp = findColumnByAliases(data, CUSTOMER_BOX_ALIASES);
|
||||
int packageColTmp = findColumnByAliases(data, CUSTOMER_PACKAGE_ALIASES);
|
||||
int amountColTmp = findColumnByAliases(data, CUSTOMER_AMOUNT_ALIASES);
|
||||
int matched = 1 + (categoryColTmp >= 0 ? 1 : 0) + (invoiceNoColTmp >= 0 ? 1 : 0) + (nameColTmp >= 0 ? 1 : 0)
|
||||
+ (boxColTmp >= 0 ? 1 : 0) + (packageColTmp >= 0 ? 1 : 0) + (amountColTmp >= 0 ? 1 : 0);
|
||||
if (matched < HEADER_MIN_MATCH_COUNT) {
|
||||
return false;
|
||||
}
|
||||
categoryCol = categoryColTmp;
|
||||
invoiceNoCol = invoiceNoColTmp;
|
||||
customerCodeCol = codeCol;
|
||||
customerNameCol = nameColTmp;
|
||||
customerBoxCol = boxColTmp;
|
||||
customerPackageCol = packageColTmp;
|
||||
customerAmountCol = amountColTmp;
|
||||
customerColumnsResolved = true;
|
||||
LOGGER.info("第{}行识别为客户配送明细列头行,列映射:类别={},发票编号={},客户编号={},名称={},箱={},包={},金额={}",
|
||||
currentRowNum, categoryCol, invoiceNoCol, customerCodeCol, customerNameCol, customerBoxCol, customerPackageCol, customerAmountCol);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底列位:老模板客户配送明细固定布局(5=F列=類别,6=G列=發票編號,7=H列=客户編號,8=I列=名稱,
|
||||
* 9=J列=箱,10=K列=包,11=L列=金額)
|
||||
*/
|
||||
private void applyCustomerFallbackColumns() {
|
||||
categoryCol = 5;
|
||||
invoiceNoCol = 6;
|
||||
customerCodeCol = 7;
|
||||
customerNameCol = 8;
|
||||
customerBoxCol = 9;
|
||||
customerPackageCol = 10;
|
||||
customerAmountCol = 11;
|
||||
customerColumnsResolved = true;
|
||||
LOGGER.warn("第{}行前未识别到客户配送明细列头行,启用老模板固定列位兜底解析", currentRowNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验客户配送明细行是否有效(排除列头行和空行)
|
||||
*/
|
||||
private boolean isValidCustomerDetailRow(Map<String, Object> data) {
|
||||
Object category = getValueSafely(data, 5); // F列
|
||||
return category != null
|
||||
&& !category.toString().trim().isEmpty()
|
||||
&& !"類别".equals(category.toString().trim());
|
||||
Object category = getValueSafely(data, categoryCol);
|
||||
if (category == null || category.toString().trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 列头文字本身不算数据行(防止列头行被当作数据重复解析)
|
||||
String text = normalizeHeaderText(category);
|
||||
return !matchesAny(text, CATEGORY_ALIASES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析客户配送明细(第4行起)
|
||||
* 列对应关系:5=F列=類别,6=G列=發票編號,7=H列=客户編號,8=I列=名稱,
|
||||
* 9=J列=箱,10=K列=包,11=L列=金額
|
||||
* 解析客户配送明细(列头行之后的行)
|
||||
*/
|
||||
private void parseCustomerDeliveryDetail(Map<String, Object> data, int rowNum) {
|
||||
CustomerDeliveryDetail customerDetail = new CustomerDeliveryDetail();
|
||||
customerDetail.setCategory(convertToString(getValueSafely(data, 5)));
|
||||
customerDetail.setInvoiceNo(convertToString(getValueSafely(data, 6)));
|
||||
customerDetail.setCustomerCode(convertToString(getValueSafely(data, 7)));
|
||||
customerDetail.setCustomerName(convertToString(getValueSafely(data, 8)));
|
||||
customerDetail.setBoxCount(convertToInteger(getValueSafely(data, 9)));
|
||||
customerDetail.setPackageCount(convertToInteger(getValueSafely(data, 10)));
|
||||
customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, 11)));
|
||||
customerDetail.setCategory(convertToString(getValueSafely(data, categoryCol)));
|
||||
customerDetail.setInvoiceNo(convertToString(getValueSafely(data, invoiceNoCol)));
|
||||
customerDetail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol)));
|
||||
customerDetail.setCustomerName(convertToString(getValueSafely(data, customerNameCol)));
|
||||
customerDetail.setBoxCount(convertToInteger(getValueSafely(data, customerBoxCol)));
|
||||
customerDetail.setPackageCount(convertToInteger(getValueSafely(data, customerPackageCol)));
|
||||
customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, customerAmountCol)));
|
||||
|
||||
customerDeliveryDetailList.add(customerDetail);
|
||||
LOGGER.debug("第{}行:解析客户配送明细完成,类别={},发票编号={},客户编号={},名称={},箱数={},包数={},金额={}",
|
||||
@@ -299,7 +427,7 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
* 此时key不是数字字符串,需跳过这些key,走位置兜底逻辑
|
||||
*/
|
||||
private Object getValueSafely(Map<String, Object> data, int columnIndex) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
if (columnIndex < 0 || data == null || data.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -333,9 +461,9 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
return "未知列";
|
||||
}
|
||||
int index = 0;
|
||||
for (String key : data.keySet()) {
|
||||
for (Object key : data.keySet()) {
|
||||
if (index == columnIndex) {
|
||||
return key;
|
||||
return String.valueOf(key);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
@@ -343,22 +471,28 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定行中查找关键字所在的列索引
|
||||
* 注意:Map的key可能是非数字字符串(如表头文本"客户配送明细"),此时跳过该key,不抛出异常
|
||||
* 在指定行中查找关键字所在的列索引(关键字归一化后包含匹配,兼容简繁体与冒号等符号)
|
||||
* 注意:Map的key可能是非数字字符串(如表头文本),此时跳过该key,不抛出异常
|
||||
*/
|
||||
private Integer findKeywordIndex(Map<String, Object> data, String keyword) {
|
||||
if (data == null || data.isEmpty() || keyword == null) {
|
||||
private Integer findKeywordIndex(Map<String, Object> data, String[] keywords) {
|
||||
if (data == null || data.isEmpty() || keywords == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object keyObj : data.keySet()) {
|
||||
Object value = data.get(keyObj);
|
||||
if (value != null && value.toString().trim().contains(keyword)) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String text = normalizeHeaderText(value);
|
||||
if (text.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (matchesAny(text, keywords)) {
|
||||
try {
|
||||
return Integer.parseInt(keyObj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
// key不是数字(可能是表头文本如"客户配送明细"),跳过,不打印警告避免日志刷屏
|
||||
LOGGER.debug("第{}行:关键字'{}'所在列key不是数字,key={},跳过",
|
||||
currentRowNum, keyword, keyObj);
|
||||
// key不是数字(可能是表头文本),跳过,不打印警告避免日志刷屏
|
||||
LOGGER.debug("第{}行:关键字所在列key不是数字,key={},跳过", currentRowNum, keyObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,15 +500,64 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找关键字所在列的右侧相邻列值,并转为Integer
|
||||
* 在指定行中按列头别名查找列索引(列头文字归一化后包含匹配,返回首个命中的列;未命中返回-1)
|
||||
*/
|
||||
private Integer findValueAfterKeyword(Map<String, Object> data, String keyword) {
|
||||
Integer keywordIndex = findKeywordIndex(data, keyword);
|
||||
if (keywordIndex == null) {
|
||||
LOGGER.warn("第{}行:未找到关键字'{}'", currentRowNum, keyword);
|
||||
return null;
|
||||
private int findColumnByAliases(Map<String, Object> data, String[] aliases) {
|
||||
if (data == null || data.isEmpty() || aliases == null) {
|
||||
return -1;
|
||||
}
|
||||
return convertToInteger(getValueSafely(data, keywordIndex + 1));
|
||||
for (Object keyObj : data.keySet()) {
|
||||
Object value = data.get(keyObj);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String text = normalizeHeaderText(value);
|
||||
if (text.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (matchesAny(text, aliases)) {
|
||||
try {
|
||||
return Integer.parseInt(keyObj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
// key不是数字(可能是表头文本),跳过
|
||||
LOGGER.debug("第{}行:列头所在列key不是数字,key={},跳过", currentRowNum, keyObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列头/标签文字归一化:去空格/冒号/括号等符号、转小写、繁体转简体(覆盖模板常见用字)
|
||||
*/
|
||||
private String normalizeHeaderText(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String s = value.toString().trim().toLowerCase();
|
||||
s = s.replace(":", "").replace(":", "")
|
||||
.replace(" ", "").replace(" ", "")
|
||||
.replace("(", "").replace(")", "")
|
||||
.replace("(", "").replace(")", "")
|
||||
.replace("/", "").replace("\\", "");
|
||||
s = s.replace('貨', '货').replace('編', '编').replace('號', '号').replace('稱', '称')
|
||||
.replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户')
|
||||
.replace('紀', '纪').replace('錄', '录').replace('張', '张').replace('計', '计')
|
||||
.replace('車', '车').replace('線', '线').replace('類', '类').replace('別', '别')
|
||||
.replace('簡', '简').replace('產', '产').replace('單', '单');
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断归一化后的文字是否命中任一别名
|
||||
*/
|
||||
private boolean matchesAny(String text, String[] aliases) {
|
||||
for (String alias : aliases) {
|
||||
if (text.contains(alias)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+206
-40
@@ -17,7 +17,11 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* 入库单Excel解析监听器
|
||||
* 功能:1. 解析第一行表头数据 2. 跳过第二行合计行、第三行列名行 3. 解析第四行开始的产品明细 4. 支持文本型数字转换
|
||||
* 功能:1. 解析第一行表头数据(入库日期、OrderNO,按标签定位,位置不敏感)
|
||||
* 2. 动态定位列头行:扫描前几行,识别出"產品編號"等已知列头文字的行即认定为列头行(行位置不敏感)
|
||||
* 3. 列按列头文字动态定位(兼容繁体列头:產品編號/客戶編號/件數/飲用日期等),列顺序调整不影响解析
|
||||
* 4. 列头无法识别时兜底:按老模板固定列位(0=编号,1=名称,2=规格,3=件数,4=饮用日期)
|
||||
* 5. 支持文本型数字转换
|
||||
*/
|
||||
public class StockInExcelListener extends AnalysisEventListener<Map<String, Object>> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StockInExcelListener.class);
|
||||
@@ -35,6 +39,29 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
"dd.MM.yyyy", "d-MMM-yyyy", "yyyyMMddHHmmss"
|
||||
};
|
||||
|
||||
// ====== 列头别名(归一化后匹配,兼容简繁体及常见叫法) ======
|
||||
private static final String[] PRODUCT_CODE_ALIASES = {"产品编号", "物料编号", "产品编码", "物料编码", "料号"};
|
||||
private static final String[] PRODUCT_NAME_ALIASES = {"产品名称", "物料名称", "商品名称", "品名"};
|
||||
private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"};
|
||||
private static final String[] DRINK_DATE_ALIASES = {"饮用日期", "饮用时间"};
|
||||
private static final String[] PIECE_COUNT_ALIASES = {"件数"};
|
||||
private static final String[] SPEC_ALIASES = {"规格", "箱含量"};
|
||||
|
||||
// 列头行至少需识别出的列数(防止把日期标签行/合计行误判为列头行)
|
||||
private static final int HEADER_MIN_MATCH_COUNT = 2;
|
||||
// 老模板列头行固定在第3行:超过该行仍未识别到列头行时,启用固定列位兜底
|
||||
private static final int FALLBACK_AFTER_ROW_NUM = 3;
|
||||
|
||||
// 动态解析出的列索引(-1表示未识别该列)
|
||||
private int productCodeCol = -1;
|
||||
private int productNameCol = -1;
|
||||
private int customerCodeCol = -1;
|
||||
private int drinkDateCol = -1;
|
||||
private int pieceCountCol = -1;
|
||||
private int specCol = -1;
|
||||
// 列布局是否已确定(列头行识别成功或已启用兜底)
|
||||
private boolean columnResolved = false;
|
||||
|
||||
// 行号标记(用于区分第一行和表格行)
|
||||
private int currentRowNum = 0;
|
||||
|
||||
@@ -48,21 +75,21 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
if (currentRowNum == 1) {
|
||||
parseHeaderData(data);
|
||||
}
|
||||
// 2. 跳过第二行:合计行(Total: xxx)
|
||||
else if (currentRowNum == 2) {
|
||||
LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum);
|
||||
}
|
||||
// 3. 跳过第三行:表格列名行(產品編號、產品名稱...)
|
||||
else if (currentRowNum == 3) {
|
||||
LOGGER.debug("第{}行是表格列名行,跳过解析", currentRowNum);
|
||||
}
|
||||
// 4. 解析第四行及以后:产品明细
|
||||
else if (currentRowNum >= 4) {
|
||||
if (isValidProductRow(data)) {
|
||||
StockInProductDetail detail = parseProductDetail(data);
|
||||
productDetailList.add(detail);
|
||||
// 2. 列布局未确定:尝试识别列头行;识别成功则本行即为列头行,跳过数据解析
|
||||
else if (!columnResolved) {
|
||||
if (!tryResolveHeaderRow(data)) {
|
||||
// 3. 超过老模板列头行位置仍未识别到列头行:启用固定列位兜底,从当前行开始按数据行解析
|
||||
if (currentRowNum > FALLBACK_AFTER_ROW_NUM) {
|
||||
applyFallbackColumns();
|
||||
parseDataRow(data);
|
||||
}
|
||||
// 否则(合计行Total等):跳过
|
||||
}
|
||||
}
|
||||
// 4. 列布局已确定:按数据行解析
|
||||
else {
|
||||
parseDataRow(data);
|
||||
}
|
||||
|
||||
} catch (ExcelDataConvertException e) {
|
||||
// Excel内置转换异常(如日期格式错误)
|
||||
@@ -79,6 +106,16 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据行解析:排除合计行/空行后解析产品明细
|
||||
*/
|
||||
private void parseDataRow(Map<String, Object> data) {
|
||||
if (isValidProductRow(data)) {
|
||||
StockInProductDetail detail = parseProductDetail(data);
|
||||
productDetailList.add(detail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析第一行表头数据
|
||||
* 模板中标签与值相邻,分别搜索"入库日期"和"OrderNO"标签后一列的值,兼容位置调整
|
||||
@@ -86,15 +123,17 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
private void parseHeaderData(Map<String, Object> data) {
|
||||
// 入库日期:查找包含"入库日期"标签的下一个值
|
||||
Object stockInDateObj = getValueAfterLabel(data, "入庫日期:");
|
||||
if (stockInDateObj == null) {
|
||||
stockInDateObj = getValueAfterLabel(data, "入库日期");
|
||||
}
|
||||
if (stockInDateObj != null) {
|
||||
Date stockInDate = convertToDate(stockInDateObj);
|
||||
stockInHeader.setStockInDate(stockInDate);
|
||||
LOGGER.debug("第1行:入库日期解析完成,值:{}", stockInDate);
|
||||
}
|
||||
|
||||
// OrderNO:优先查找"OrderNO"标签后的值(老模板布局,标签与值相邻);
|
||||
// 新模板第1行无"OrderNO"标签,单号值直接放在单元格中(如E1),
|
||||
// 此时取第1行中除标签、入库日期以外的第一个非空字符串值
|
||||
// OrderNO:优先查找"OrderNO"标签后的值(标签与值相邻);
|
||||
// 新模板第1行无"OrderNO"标签时,取第1行中除标签、入库日期以外的第一个非空字符串值
|
||||
Object orderNoObj = getValueAfterLabel(data, "OrderNO");
|
||||
if (orderNoObj == null) {
|
||||
for (Object value : data.values()) {
|
||||
@@ -103,7 +142,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
}
|
||||
String str = value.toString().trim();
|
||||
// 跳过标签单元格与已被识别为入库日期的值
|
||||
if (str.isEmpty() || str.contains("入庫日期") || str.contains("OrderNO") || str.contains("Total")) {
|
||||
if (str.isEmpty() || str.contains("入庫日期") || str.contains("入库日期") || str.contains("OrderNO") || str.contains("Total")) {
|
||||
continue;
|
||||
}
|
||||
if (value == stockInDateObj) {
|
||||
@@ -125,26 +164,113 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试将当前行识别为列头行:按归一化后的列头文字匹配已知列别名
|
||||
* 识别成功(至少识别出产品编号列且总数达标)返回true并记录各列索引
|
||||
*/
|
||||
private boolean tryResolveHeaderRow(Map<String, Object> data) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int tmpProductCodeCol = -1;
|
||||
int tmpProductNameCol = -1;
|
||||
int tmpCustomerCodeCol = -1;
|
||||
int tmpDrinkDateCol = -1;
|
||||
int tmpPieceCountCol = -1;
|
||||
int tmpSpecCol = -1;
|
||||
int matched = 0;
|
||||
|
||||
for (Object keyObj : data.keySet()) {
|
||||
Object value = data.get(keyObj);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String text = normalizeHeaderText(value);
|
||||
if (text.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int colIndex;
|
||||
try {
|
||||
colIndex = Integer.parseInt(keyObj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
// key不是数字(如表头文本),跳过
|
||||
continue;
|
||||
}
|
||||
// 按组依次匹配(先具体后一般,避免同一列头命中多组别名时归属错乱)
|
||||
if (tmpProductCodeCol < 0 && matchesAny(text, PRODUCT_CODE_ALIASES)) {
|
||||
tmpProductCodeCol = colIndex;
|
||||
matched++;
|
||||
} else if (tmpCustomerCodeCol < 0 && matchesAny(text, CUSTOMER_CODE_ALIASES)) {
|
||||
tmpCustomerCodeCol = colIndex;
|
||||
matched++;
|
||||
} else if (tmpProductNameCol < 0 && matchesAny(text, PRODUCT_NAME_ALIASES)) {
|
||||
tmpProductNameCol = colIndex;
|
||||
matched++;
|
||||
} else if (tmpDrinkDateCol < 0 && matchesAny(text, DRINK_DATE_ALIASES)) {
|
||||
tmpDrinkDateCol = colIndex;
|
||||
matched++;
|
||||
} else if (tmpPieceCountCol < 0 && matchesAny(text, PIECE_COUNT_ALIASES)) {
|
||||
tmpPieceCountCol = colIndex;
|
||||
matched++;
|
||||
} else if (tmpSpecCol < 0 && matchesAny(text, SPEC_ALIASES)) {
|
||||
tmpSpecCol = colIndex;
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
|
||||
// 产品编号列是数据解析的锚点,必须识别到;且识别总数达标才认定为列头行
|
||||
if (tmpProductCodeCol < 0 || matched < HEADER_MIN_MATCH_COUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
productCodeCol = tmpProductCodeCol;
|
||||
productNameCol = tmpProductNameCol;
|
||||
customerCodeCol = tmpCustomerCodeCol;
|
||||
drinkDateCol = tmpDrinkDateCol;
|
||||
pieceCountCol = tmpPieceCountCol;
|
||||
specCol = tmpSpecCol;
|
||||
columnResolved = true;
|
||||
LOGGER.info("第{}行识别为列头行,列映射:产品编号={},产品名称={},客户编号={},规格={},件数={},饮用日期={}",
|
||||
currentRowNum, productCodeCol, productNameCol, customerCodeCol, specCol, pieceCountCol, drinkDateCol);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底列位:老模板固定布局(0=编号,1=名称,2=规格,3=件数,4=饮用日期),无客户编号列
|
||||
*/
|
||||
private void applyFallbackColumns() {
|
||||
productCodeCol = 0;
|
||||
productNameCol = 1;
|
||||
customerCodeCol = -1;
|
||||
specCol = 2;
|
||||
pieceCountCol = 3;
|
||||
drinkDateCol = 4;
|
||||
columnResolved = true;
|
||||
LOGGER.warn("第{}行前未识别到列头行,启用老模板固定列位兜底解析", currentRowNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析产品明细数据(一行对应一个产品)
|
||||
* 新模板列顺序:0=产品编号,1=产品名称,2=规格(箱含量),3=件数,4=饮用日期
|
||||
* 各列按列头行动态定位的索引取值,未识别的列取null/默认值
|
||||
*/
|
||||
private StockInProductDetail parseProductDetail(Map<String, Object> data) {
|
||||
StockInProductDetail detail = new StockInProductDetail();
|
||||
|
||||
// 产品编号(第1列,索引0)- 支持文本型数字转换
|
||||
detail.setProductCode(convertToString(getValueSafely(data, 0)));
|
||||
// 产品名称(第2列,索引1)
|
||||
detail.setProductName(convertToString(getValueSafely(data, 1)));
|
||||
// 箱含量/规格(第3列,索引2)
|
||||
detail.setBoxCapacity(convertToInteger(getValueSafely(data, 2)));
|
||||
// 件数(第4列,索引3)
|
||||
detail.setPieceCount(convertToInteger(getValueSafely(data, 3)));
|
||||
// 饮用日期(第5列,索引4)
|
||||
detail.setDrinkDate(convertToDate(getValueSafely(data, 4)));
|
||||
// 产品编号(可不填名称,导入时按编号自动匹配库中物料名称)
|
||||
detail.setProductCode(convertToString(getValueSafely(data, productCodeCol)));
|
||||
// 产品名称(列头未识别或单元格未填时为空串,由服务端按编号自动匹配覆盖)
|
||||
detail.setProductName(convertToString(getValueSafely(data, productNameCol)));
|
||||
// 客户编号(新模板新增列,可不填;填写后由服务端校验客户存在性并自动匹配客户名称)
|
||||
detail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol)));
|
||||
// 箱含量/规格
|
||||
detail.setBoxCapacity(convertToInteger(getValueSafely(data, specCol)));
|
||||
// 件数
|
||||
detail.setPieceCount(convertToInteger(getValueSafely(data, pieceCountCol)));
|
||||
// 饮用日期
|
||||
detail.setDrinkDate(convertToDate(getValueSafely(data, drinkDateCol)));
|
||||
|
||||
LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{}",
|
||||
currentRowNum, detail.getProductCode(), detail.getProductName());
|
||||
LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{},客户编号:{}",
|
||||
currentRowNum, detail.getProductCode(), detail.getProductName(), detail.getCustomerCode());
|
||||
return detail;
|
||||
}
|
||||
|
||||
@@ -152,9 +278,6 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
* 校验产品行是否有效(排除空行和合计行)
|
||||
*/
|
||||
private boolean isValidProductRow(Map<String, Object> data) {
|
||||
// 有效条件:产品编号不为空 + 不是合计行
|
||||
Object productCodeObj = getValueSafely(data, 0);
|
||||
|
||||
// 排除合计行(整行包含"Total"字样)
|
||||
if (containsAnyValue(data, "Total")) {
|
||||
LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum);
|
||||
@@ -162,6 +285,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
}
|
||||
|
||||
// 排除空行(产品编号为空)
|
||||
Object productCodeObj = getValueSafely(data, productCodeCol);
|
||||
return productCodeObj != null && !productCodeObj.toString().trim().isEmpty();
|
||||
}
|
||||
|
||||
@@ -178,13 +302,24 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
|
||||
// ---------------------- 工具方法(类型转换+安全取值) ----------------------
|
||||
/**
|
||||
* 按列索引安全获取值(支持列索引越界处理)
|
||||
* 按列索引安全获取值
|
||||
* 优先按Map的key直接查找(适配EasyExcel以列号为key的情况,避免空单元格导致的位置偏移),
|
||||
* 找不到再按位置顺序查找(保持与原逻辑兼容)
|
||||
*/
|
||||
private Object getValueSafely(Map<String, Object> data, int columnIndex) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
if (columnIndex < 0 || data == null || data.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String strIndex = String.valueOf(columnIndex);
|
||||
// 1. 直接按key查找(key为String或Integer,跳过非数字key)
|
||||
for (Object keyObj : data.keySet()) {
|
||||
if (keyObj != null && strIndex.equals(keyObj.toString())) {
|
||||
return data.get(keyObj);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 兜底:按顺序位置查找
|
||||
int index = 0;
|
||||
for (Object value : data.values()) {
|
||||
if (index == columnIndex) {
|
||||
@@ -193,7 +328,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
index++;
|
||||
}
|
||||
|
||||
LOGGER.warn("第{}行列索引{}超出范围(当前行共{}列),返回null",
|
||||
LOGGER.warn("第{}行列索引{}超出当前行单元格数量({}列),返回null",
|
||||
currentRowNum, columnIndex, data.size());
|
||||
return null;
|
||||
}
|
||||
@@ -207,9 +342,9 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
for (String key : data.keySet()) {
|
||||
for (Object key : data.keySet()) {
|
||||
if (index == columnIndex) {
|
||||
return key;
|
||||
return String.valueOf(key);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
@@ -251,6 +386,37 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列头文字归一化:去空格/冒号/括号等符号、转小写、繁体转简体(覆盖模板常见用字)
|
||||
*/
|
||||
private String normalizeHeaderText(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String s = value.toString().trim().toLowerCase();
|
||||
s = s.replace(":", "").replace(":", "")
|
||||
.replace(" ", "").replace(" ", "")
|
||||
.replace("(", "").replace(")", "")
|
||||
.replace("(", "").replace(")", "")
|
||||
.replace("/", "").replace("\\", "");
|
||||
s = s.replace('產', '产').replace('編', '编').replace('號', '号').replace('稱', '称')
|
||||
.replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户')
|
||||
.replace('貨', '货').replace('單', '单').replace('類', '类').replace('倉', '仓');
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断归一化后的列头文字是否命中任一别名
|
||||
*/
|
||||
private boolean matchesAny(String text, String[] aliases) {
|
||||
for (String alias : aliases) {
|
||||
if (text.contains(alias)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用转换:Object转String
|
||||
*/
|
||||
@@ -347,4 +513,4 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
|
||||
public List<StockInProductDetail> getProductDetailList() {
|
||||
return productDetailList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+146
-38
@@ -172,10 +172,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
String lineCode = deliveryHeader.getDeliveryLineNo();
|
||||
Integer trainNumber = deliveryHeader.getTrainNumber();
|
||||
if (StringUtils.isBlank(lineCode)) {
|
||||
throw new Exception("Excel未识别到【送貨線號】(线路),请检查模板第2行后重新导入");
|
||||
throw new Exception("Excel未识别到【送貨線號】(线路),请检查模板表头后重新导入");
|
||||
}
|
||||
if (trainNumber == null || trainNumber <= 0) {
|
||||
throw new Exception("Excel未识别到【車次】,请检查模板第3行后重新导入");
|
||||
throw new Exception("Excel未识别到【車次】,请检查模板表头后重新导入");
|
||||
}
|
||||
AjaxResult lineResult = wmsServiceFeign.listLineManagement(lineCode);
|
||||
if (lineResult == null || lineResult.get(AjaxResult.DATA_TAG) == null) {
|
||||
@@ -259,18 +259,52 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
|
||||
List<Map<String,Object>> shipperIds = new ArrayList<>();
|
||||
Set<Long> processedShipperIds = new HashSet<>();
|
||||
// ====== 物料存在性校验(落库前):查询结果为空说明库中没有该物料,收集全部缺失料号后一次性提示维护 ======
|
||||
Map<String, MaterialBaseInfoPO> materialCache = new LinkedHashMap<>();
|
||||
List<String> missingMaterials = new ArrayList<>();
|
||||
for (ProductDetail productDetail : productDetailList) {
|
||||
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
|
||||
if (materialBaseInfoPO != null && materialBaseInfoPO.getShipperId() != null) {
|
||||
Long shipperId = materialBaseInfoPO.getShipperId();
|
||||
if (processedShipperIds.add(shipperId)) {
|
||||
Map<String, Object> shipper = new HashMap<>();
|
||||
shipper.put("shipperId", shipperId);
|
||||
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
|
||||
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
|
||||
shipperIds.add(shipper);
|
||||
}
|
||||
String productCode = productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim();
|
||||
if (productCode.isEmpty()) {
|
||||
throw new Exception("出库明细存在未填写產品編號的行,请补全后重新导入");
|
||||
}
|
||||
if (materialCache.containsKey(productCode)) {
|
||||
continue;
|
||||
}
|
||||
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId());
|
||||
if (materialBaseInfoPO == null) {
|
||||
missingMaterials.add(productCode);
|
||||
} else {
|
||||
materialCache.put(productCode, materialBaseInfoPO);
|
||||
}
|
||||
}
|
||||
if (!missingMaterials.isEmpty()) {
|
||||
throw new Exception("物料[" + String.join("、", missingMaterials) + "]不存在,请先维护物料信息后再导入");
|
||||
}
|
||||
for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) {
|
||||
if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) {
|
||||
Map<String, Object> shipper = new HashMap<>();
|
||||
shipper.put("shipperId", materialBaseInfoPO.getShipperId());
|
||||
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
|
||||
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
|
||||
shipperIds.add(shipper);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 客户存在性校验(落库前):客户编号在库中查询结果为空说明客户不存在,提示维护后再导入 ======
|
||||
Map<String, UserPo> customerCache = new HashMap<>();
|
||||
for (CustomerDeliveryDetail customerDeliveryDetail : customerDeliveryDetailList) {
|
||||
String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim();
|
||||
if (customerCode.isEmpty()) {
|
||||
throw new Exception("客户配送明细存在未填写客户编号的行,请补全后重新导入");
|
||||
}
|
||||
if (customerCache.containsKey(customerCode)) {
|
||||
continue;
|
||||
}
|
||||
UserPo customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId());
|
||||
if (customer == null || customer.getUserId() == null) {
|
||||
throw new Exception("客户[" + customerCode + "]不存在,请先维护客户信息后再导入");
|
||||
}
|
||||
customerCache.put(customerCode, customer);
|
||||
}
|
||||
|
||||
// 货品明细不为空时,校验货主信息
|
||||
@@ -330,7 +364,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
for (ProductDetail productDetail : productDetailList) {
|
||||
ReservationOutMaterialDetailDO reservationOutMaterialDetailDO = new ReservationOutMaterialDetailDO();
|
||||
reservationOutMaterialDetailDO.setOutOrderNumber(reservationStockOutOrderDO.getOutOrderNumber());
|
||||
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
|
||||
// 物料信息取自前置校验的缓存(已确认存在,避免逐行重复查库)
|
||||
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(productDetail.getProductCode());
|
||||
reservationOutMaterialDetailDO.setQuantity(new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
|
||||
if (materialBaseInfoPO != null) {
|
||||
Long materialBaseInfoId = materialBaseInfoPO.getMaterialBaseInfoId();
|
||||
@@ -398,9 +433,14 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
reservationDeliveryDetailsLink.setAmount(customerDeliveryDetail.getAmount());
|
||||
reservationDeliveryDetailsLink.setCustomerNumber(customerDeliveryDetail.getCustomerCode());
|
||||
reservationDeliveryDetailsLink.setDeliveryName(customerDeliveryDetail.getCustomerName());
|
||||
String customerCode = customerDeliveryDetail.getCustomerCode();
|
||||
Long userId = stockOutOrderMapper.queryShipperId(customerCode, loginUser.getUserPo().getOrganizationId());
|
||||
reservationDeliveryDetailsLink.setCustomerId(userId);
|
||||
String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim();
|
||||
// 客户已在前置校验通过:客户ID取库中匹配结果,名称以库中数据为准自动匹配(覆盖模板填写值;库中名称为空时保留模板值)
|
||||
UserPo customer = customerCache.get(customerCode);
|
||||
reservationDeliveryDetailsLink.setCustomerId(customer.getUserId());
|
||||
if (customer.getUserName() != null && !customer.getUserName().trim().isEmpty()) {
|
||||
customerDeliveryDetail.setCustomerName(customer.getUserName());
|
||||
reservationDeliveryDetailsLink.setDeliveryName(customer.getUserName());
|
||||
}
|
||||
reservationDeliveryDetailsLink.setBox(customerDeliveryDetail.getBoxCount());
|
||||
reservationDeliveryDetailsLink.setBag(customerDeliveryDetail.getPackageCount());
|
||||
reservationDeliveryDetailsLink.setCategory(customerDeliveryDetail.getCategory());
|
||||
@@ -462,30 +502,87 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
LOGGER.info("Excel解析成功,工作表:{},产品明细数:{}",
|
||||
targetSheetName, resultDTO.getProductDetailList().size());
|
||||
List<StockInProductDetail> productDetailList = resultDTO.getProductDetailList();
|
||||
if (productDetailList == null || productDetailList.isEmpty()) {
|
||||
throw new ServiceException("未解析到产品明细数据,请检查模板中是否填写了產品編號");
|
||||
}
|
||||
StockInHeader stockInHeader = resultDTO.getStockInHeader();
|
||||
// ====== 物料存在性校验:模板只填物料编号即可,物料名称按编号自动匹配库中数据 ======
|
||||
// 查询结果为空说明库中没有该物料,收集全部缺失料号后一次性提示维护
|
||||
Map<String, MaterialBaseInfoPO> materialCache = new LinkedHashMap<>();
|
||||
List<String> missingMaterials = new ArrayList<>();
|
||||
for (StockInProductDetail productDetail : productDetailList) {
|
||||
String productCode = productDetail.getProductCode();
|
||||
if (materialCache.containsKey(productCode)) {
|
||||
continue;
|
||||
}
|
||||
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId());
|
||||
if (materialBaseInfoPO == null) {
|
||||
missingMaterials.add(productCode);
|
||||
} else {
|
||||
materialCache.put(productCode, materialBaseInfoPO);
|
||||
}
|
||||
}
|
||||
if (!missingMaterials.isEmpty()) {
|
||||
throw new ServiceException("物料[" + String.join("、", missingMaterials) + "]不存在,请先维护物料信息后再导入");
|
||||
}
|
||||
// 明细回填库中匹配到的物料名称(自动匹配,模板中的名称列可不填/以库中为准;库中名称为空时保留模板值)
|
||||
for (StockInProductDetail productDetail : productDetailList) {
|
||||
String materialName = materialCache.get(productDetail.getProductCode()).getMaterialName();
|
||||
if (materialName != null && !materialName.trim().isEmpty()) {
|
||||
productDetail.setProductName(materialName);
|
||||
}
|
||||
}
|
||||
// ====== 物料归属货主集合(用于客户编号一致性校验,及未填客户编号时的兜底取值) ======
|
||||
List<Map<String,Object>> shipperIds = new ArrayList<>();
|
||||
Set<Long> processedShipperIds = new HashSet<>();
|
||||
for (StockInProductDetail productDetail : productDetailList) {
|
||||
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
|
||||
// 物料在当前登录组织下不存在时给出明确提示,避免后续货主取值越界导致500
|
||||
if (materialBaseInfoPO == null) {
|
||||
throw new ServiceException("产品编号[" + productDetail.getProductCode() + "]在当前组织下不存在,请核对产品编号或使用物料所属组织的账号导入");
|
||||
}
|
||||
if (materialBaseInfoPO.getShipperId() != null) {
|
||||
Long shipperId = materialBaseInfoPO.getShipperId();
|
||||
if (processedShipperIds.add(shipperId)) {
|
||||
Map<String, Object> shipper = new HashMap<>();
|
||||
shipper.put("shipperId", shipperId);
|
||||
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
|
||||
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
|
||||
shipperIds.add(shipper);
|
||||
}
|
||||
for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) {
|
||||
if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) {
|
||||
Map<String, Object> shipper = new HashMap<>();
|
||||
shipper.put("shipperId", materialBaseInfoPO.getShipperId());
|
||||
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
|
||||
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
|
||||
shipperIds.add(shipper);
|
||||
}
|
||||
}
|
||||
if (shipperIds.isEmpty()) {
|
||||
throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息");
|
||||
if (shipperIds.size() > 1) {
|
||||
StringBuilder shipperCodeStr = new StringBuilder();
|
||||
for (Map<String, Object> shipper : shipperIds) {
|
||||
if (shipperCodeStr.length() > 0) {
|
||||
shipperCodeStr.append("、");
|
||||
}
|
||||
shipperCodeStr.append(shipper.get("shipperCode"));
|
||||
}
|
||||
throw new ServiceException("导入的物料不属于同一货主(货主编号:" + shipperCodeStr + "),一个入库单只能属于一个货主");
|
||||
}
|
||||
// ====== 客户编号校验与名称自动匹配:模板只填客户编号,客户名称以库中数据为准 ======
|
||||
UserPo customer = null;
|
||||
Set<String> customerCodes = new LinkedHashSet<>();
|
||||
for (StockInProductDetail productDetail : productDetailList) {
|
||||
if (productDetail.getCustomerCode() != null && !productDetail.getCustomerCode().trim().isEmpty()) {
|
||||
customerCodes.add(productDetail.getCustomerCode().trim());
|
||||
}
|
||||
}
|
||||
if (customerCodes.size() > 1) {
|
||||
throw new ServiceException("导入明细中存在多个客户编号(" + String.join("、", customerCodes) + "),一个入库单只能属于一个客户");
|
||||
}
|
||||
if (!customerCodes.isEmpty()) {
|
||||
String customerCode = customerCodes.iterator().next();
|
||||
// 查询结果为空说明库中没有该客户,提示维护后再导入
|
||||
customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId());
|
||||
if (customer == null || customer.getUserId() == null) {
|
||||
throw new ServiceException("客户[" + customerCode + "]不存在,请先维护客户信息后再导入");
|
||||
}
|
||||
// 客户编号与物料归属货主一致性校验(不一致拦截,提示两者编号;货主编码未维护时提示货主名称)
|
||||
if (!shipperIds.isEmpty() && !customer.getUserId().equals(shipperIds.get(0).get("shipperId"))) {
|
||||
Object materialShipperCode = shipperIds.get(0).get("shipperCode");
|
||||
String materialShipperDisplay = (materialShipperCode != null && !materialShipperCode.toString().trim().isEmpty())
|
||||
? "货主编号[" + materialShipperCode + "]" : "货主[" + shipperIds.get(0).get("shipperName") + "]";
|
||||
throw new ServiceException("客户编号[" + customerCode + "]与物料归属" + materialShipperDisplay + "]不一致,请核对后再导入");
|
||||
}
|
||||
stockInHeader.setCustomerCode(customerCode);
|
||||
stockInHeader.setCustomerName(customer.getUserName());
|
||||
}
|
||||
ReservationStockInOrderDO reservationStockInOrderDO = new ReservationStockInOrderDO();
|
||||
StockInHeader stockInHeader = resultDTO.getStockInHeader();
|
||||
// 业务单号:优先使用模板自带OrderNO(不做格式规则校验),与系统已有业务单号重复时提示不允许导入;
|
||||
// 入库业务单号(in_order_number)不使用模板单号,统一由系统原本逻辑生成(见insertDomain);
|
||||
// 模板未填单号时保持系统逻辑:业务单号默认=入库业务单号
|
||||
@@ -515,9 +612,19 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
reservationStockInOrderDO.setIssueStatus(0);
|
||||
reservationStockInOrderDO.setSalesmanId(loginUser.getUserPo().getUserId().toString());
|
||||
reservationStockInOrderDO.setSalesmanName(loginUser.getUserPo().getUserName());
|
||||
reservationStockInOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
|
||||
reservationStockInOrderDO.setShipperName((String) shipperIds.get(0).get("shipperName"));
|
||||
reservationStockInOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
|
||||
// 单据货主:模板填了客户编号时以客户为准(编号/名称均为库中自动匹配值),未填时取物料归属货主
|
||||
if (customer != null) {
|
||||
reservationStockInOrderDO.setShipperId(customer.getUserId());
|
||||
reservationStockInOrderDO.setShipperName(customer.getUserName());
|
||||
reservationStockInOrderDO.setShipperCode(customer.getUserMemberCode());
|
||||
} else {
|
||||
if (shipperIds.isEmpty()) {
|
||||
throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息,或在模板中填写客户编号");
|
||||
}
|
||||
reservationStockInOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
|
||||
reservationStockInOrderDO.setShipperName((String) shipperIds.get(0).get("shipperName"));
|
||||
reservationStockInOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
|
||||
}
|
||||
reservationStockInOrderDO.setWarehouseDate(stockInHeader.getStockInDate());
|
||||
reservationStockInOrderDO.setStorageTime(stockInHeader.getStockInDate());
|
||||
reservationStockInOrderDO.setExpectTime(stockInHeader.getStockInDate());
|
||||
@@ -533,7 +640,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
|
||||
reservationInMaterialDetailDO.setCreateBy(loginUser.getUserPo().getUserId());
|
||||
reservationInMaterialDetailDO.setCreateByName(loginUser.getUserPo().getUserName());
|
||||
reservationInMaterialDetailDO.setCreateTime(new Date());
|
||||
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(stockInProductDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
|
||||
// 物料信息取自前置校验的缓存(已确认存在,避免逐行重复查库)
|
||||
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(stockInProductDetail.getProductCode());
|
||||
// 计划数量取模版"件数"列,同时件数字段落库用于明细列表回显
|
||||
reservationInMaterialDetailDO.setQuantity(new BigDecimal(stockInProductDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
|
||||
reservationInMaterialDetailDO.setPieceCount(new BigDecimal(stockInProductDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
|
||||
|
||||
+20
@@ -14,6 +14,10 @@ public class StockInHeader {
|
||||
private Date stockInDate;
|
||||
// 订单编号(Excel第一行第4列)
|
||||
private String orderNo;
|
||||
// 客户编号(明细列头动态定位,导入校验客户存在性时回填)
|
||||
private String customerCode;
|
||||
// 客户名称(按客户编号自动匹配库中名称后回填)
|
||||
private String customerName;
|
||||
|
||||
// Getter和Setter方法
|
||||
public Date getStockInDate() {
|
||||
@@ -31,4 +35,20 @@ public class StockInHeader {
|
||||
public void setOrderNo(String orderNo) {
|
||||
this.orderNo = orderNo;
|
||||
}
|
||||
|
||||
public String getCustomerCode() {
|
||||
return customerCode;
|
||||
}
|
||||
|
||||
public void setCustomerCode(String customerCode) {
|
||||
this.customerCode = customerCode;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public void setCustomerName(String customerName) {
|
||||
this.customerName = customerName;
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -9,10 +9,12 @@ import java.util.Date;
|
||||
*/
|
||||
@Data
|
||||
public class StockInProductDetail {
|
||||
// 产品编号(Excel第1列)
|
||||
// 产品编号(按列头动态定位)
|
||||
private String productCode;
|
||||
// 产品名称(Excel第2列)
|
||||
// 产品名称(按列头动态定位,可不填,导入时按产品编号自动匹配库中物料名称)
|
||||
private String productName;
|
||||
// 客户编号(按列头动态定位,可不填,导入时按客户编号自动匹配库中客户名称)
|
||||
private String customerCode;
|
||||
// 箱含量(Excel第3列)
|
||||
private Integer boxCapacity;
|
||||
// 件数(Excel第4列)
|
||||
@@ -37,6 +39,14 @@ public class StockInProductDetail {
|
||||
this.productName = productName;
|
||||
}
|
||||
|
||||
public String getCustomerCode() {
|
||||
return customerCode;
|
||||
}
|
||||
|
||||
public void setCustomerCode(String customerCode) {
|
||||
this.customerCode = customerCode;
|
||||
}
|
||||
|
||||
public Integer getBoxCapacity() {
|
||||
return boxCapacity;
|
||||
}
|
||||
|
||||
+7
-1
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationStockOutOrder.repository.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
//import com.mhd.oms.domain.reservationReservationStockOutOrder.entity.ReservationReservationStockOutOrder;
|
||||
import com.mhd.common.core.domain.po.UserPo;
|
||||
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
|
||||
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
|
||||
@@ -36,7 +37,12 @@ public interface ReservationStockOutOrderMapper extends BaseMapper<ReservationSt
|
||||
*/
|
||||
List<ReservationOutMaterialDetailPO> queryOutStockNoticePrintDetail(ReservationStockOutOrderDO stockOutOrderDO);
|
||||
|
||||
Long queryShipperId(@Param("userMemberCode") String userMemberCode,@Param("organizationId") Long organizationId);
|
||||
/**
|
||||
* 按客户编号+组织查询客户(用户表),返回客户ID/编号/名称
|
||||
* 客户编号兼容两种编码:USER.USER_MEMBER_CODE(客户编码)或 USER_SHIPPER.CUSTOMER_NC_CODE(NC客户代码/khdm),任一命中即可
|
||||
* 用于出入库导入时校验客户是否存在并自动匹配客户名称;结果为空表示库中无该客户
|
||||
*/
|
||||
UserPo queryCustomerByCode(@Param("userMemberCode") String userMemberCode, @Param("organizationId") Long organizationId);
|
||||
|
||||
int countByOrderNumberPrefix(@Param("prefix") String prefix);
|
||||
|
||||
|
||||
+10
-2
@@ -470,8 +470,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="boxPalletNo" column="box_pallet_no"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="queryShipperId" resultType="java.lang.Long">
|
||||
select max(user_id) as user_id from NGWL_TEST_USER."USER" where ORGANIZATION_ID = #{organizationId} and USER_MEMBER_CODE = #{userMemberCode} and DEL_FLAG = 1
|
||||
<!-- 按客户编号+组织查询客户(用户表):出入库导入校验客户存在性并自动匹配客户名称,结果为空表示客户不存在。
|
||||
客户编码有两处维护:USER.USER_MEMBER_CODE(客户编码)与 USER_SHIPPER.CUSTOMER_NC_CODE(NC客户代码,即单据khdm),
|
||||
模板填写的客户编号两者任一命中即可匹配到客户 -->
|
||||
<select id="queryCustomerByCode" resultType="com.mhd.common.core.domain.po.UserPo">
|
||||
select max(u.user_id) as user_id, max(u.user_name) as user_name, max(u.user_member_code) as user_member_code
|
||||
from NGWL_TEST_USER."USER" u
|
||||
left join NGWL_TEST_USER.USER_SHIPPER us
|
||||
on us.USER_ID = u.USER_ID and us.DEL_FLAG = 1
|
||||
where u.ORGANIZATION_ID = #{organizationId} and u.DEL_FLAG = 1
|
||||
and (u.USER_MEMBER_CODE = #{userMemberCode} or us.CUSTOMER_NC_CODE = #{userMemberCode})
|
||||
</select>
|
||||
|
||||
<select id="countByOrderNumberPrefix" resultType="java.lang.Integer" parameterType="java.lang.String">
|
||||
|
||||
Reference in New Issue
Block a user