diff --git a/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java b/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java index bcdfacb14..0b6ed4003 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java @@ -11,6 +11,7 @@ import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO; import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO; import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO; import com.mhd.oms.domain.outEntrustOrder.service.OutEntrustOrderDomainService; +import com.mhd.oms.domain.outEntrustOrder.repository.listener.OutEntrustExcelParseService; import com.mhd.system.api.SystemServiceFeign; import com.mhd.system.api.WmsServiceFeign; import com.mhd.system.api.domain.NoticeMaterialDetailDTO; @@ -20,8 +21,10 @@ import com.mhd.system.api.model.LoginUser; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; @@ -52,6 +55,9 @@ public class OutEntrustOrderApplicationService { @Resource private WmsServiceFeign wmsServiceFeign; + @Resource + private OutEntrustExcelParseService outEntrustExcelParseService; + /** * 分页查询出仓委托单列表(不做默认状态过滤,显示全部数据) */ @@ -108,6 +114,45 @@ public class OutEntrustOrderApplicationService { return outEntrustOrderDomainService.insert(outEntrustOrderDO); } + /** + * 导入出仓委托单(模板与WMS出库管理导入一致:前11行表头+第12行起明细) + * 解析校验通过后复用新增链路落库:生成CKWT委托单号,状态待审核/未下发 + * + * @return 校验错误信息;空串表示导入成功 + */ + public StringBuilder importOutEntrustOrder(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) throws IOException { + //解析+校验(客户解析/物料匹配/字典转换/必填项) + OutEntrustExcelParseService.ParseResult parseResult = + outEntrustExcelParseService.parseOutEntrustOrderExcel(file, warehouseName, warehouseCode, warehouseId); + if (parseResult.getErrorMsg().length() > 0) { + return parseResult.getErrorMsg(); + } + OutEntrustOrderDO outEntrustOrderDO = parseResult.getOrder(); + //审计信息(与WMS出库导入一致,导入人=当前登录人) + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser != null) { + String userRealName; + if (loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null + && !loginUser.getUserPo().getUserName().trim().isEmpty()) { + userRealName = loginUser.getUserPo().getUserName(); + } else if (loginUser.getRealname() != null && !loginUser.getRealname().trim().isEmpty()) { + userRealName = loginUser.getRealname(); + } else { + userRealName = loginUser.getUsername(); + } + Date now = new Date(); + outEntrustOrderDO.setCreateBy(loginUser.getUserid()); + outEntrustOrderDO.setCreateByName(userRealName); + outEntrustOrderDO.setCreateTime(now); + outEntrustOrderDO.setUpdateBy(loginUser.getUserid()); + outEntrustOrderDO.setUpdateByName(userRealName); + outEntrustOrderDO.setUpdateTime(now); + } + //复用新增链路:必填校验→默认普通出库→仓库校验→组织信息→汇总申报数量→生成CKWT委托单号(待审核/未下发) + insert(outEntrustOrderDO); + return new StringBuilder(); + } + /** * 修改出仓委托单(仅待审核/已驳回且未下发可修改;已驳回单编辑后状态重置为待审核) */ diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/NoConvertConverter.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/NoConvertConverter.java new file mode 100644 index 000000000..07d44ae51 --- /dev/null +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/NoConvertConverter.java @@ -0,0 +1,58 @@ +package com.mhd.oms.domain.outEntrustOrder.repository.listener; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * 自定义转换器,不进行任何类型转换,直接返回原始值 + * 覆盖EasyExcel默认的StringNumberConverter,避免数字格式化错误 + * + * @author rcx + * @date 2026-09-14 + */ +public class NoConvertConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + // 关键:返回NUMBER类型,覆盖StringNumberConverter + return CellDataTypeEnum.NUMBER; + } + + @Override + public String convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 不进行任何格式化,直接返回原始值的字符串表示 + if (cellData == null) { + return ""; + } + + // 根据单元格类型获取原始值 + try { + CellDataTypeEnum type = cellData.getType(); + if (type == CellDataTypeEnum.STRING) { + return cellData.getStringValue() != null ? cellData.getStringValue() : ""; + } else if (type == CellDataTypeEnum.NUMBER) { + // 直接转换数字为字符串,不进行格式化 + return cellData.getNumberValue() != null ? cellData.getNumberValue().toString() : ""; + } else if (type == CellDataTypeEnum.BOOLEAN) { + return cellData.getBooleanValue() != null ? cellData.getBooleanValue().toString() : ""; + } else if (type == CellDataTypeEnum.EMPTY) { + return ""; + } else { + // 其他类型尝试获取字符串值 + return cellData.getStringValue() != null ? cellData.getStringValue() : ""; + } + } catch (Exception e) { + // 如果获取失败,返回空字符串 + return ""; + } + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustExcelParseService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustExcelParseService.java new file mode 100644 index 000000000..38cfa81ad --- /dev/null +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustExcelParseService.java @@ -0,0 +1,481 @@ +package com.mhd.oms.domain.outEntrustOrder.repository.listener; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.read.metadata.ReadSheet; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.mhd.common.core.domain.po.SysDictDataVo; +import com.mhd.common.core.domain.po.UserPo; +import com.mhd.common.core.exception.ServiceException; +import com.mhd.common.core.utils.StringUtils; +import com.mhd.common.core.web.domain.AjaxResult; +import com.mhd.common.security.utils.SecurityUtils; +import com.mhd.oms.domain.outEntrustOrder.repository.mapper.OutEntrustOrderMapper; +import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO; +import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO; +import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO; +import com.mhd.system.api.SystemServiceFeign; +import com.mhd.system.api.UserServiceFeign; +import com.mhd.system.api.model.LoginUser; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 出仓委托单Excel解析服务 + * 模板与WMS出库管理导入一致:前11行为表头,第12行起为明细 + * + * @author rcx + * @date 2026-09-14 + */ +@Slf4j +@Service +public class OutEntrustExcelParseService { + + @Resource + private SystemServiceFeign systemServiceFeign; + + @Resource + private UserServiceFeign userServiceFeign; + + @Autowired + private OutEntrustOrderMapper outEntrustOrderMapper; + + /** + * 解析结果:委托单对象 + 校验错误信息(空串表示通过) + */ + @Getter + public static class ParseResult { + private final OutEntrustOrderDO order; + private final StringBuilder errorMsg; + + public ParseResult(OutEntrustOrderDO order, StringBuilder errorMsg) { + this.order = order; + this.errorMsg = errorMsg; + } + } + + /** + * 解析出仓委托单Excel(不落库,落库由应用服务走新增链路) + * + * @param file 上传的Excel文件 + * @param warehouseName 仓库名称(导入弹窗选择) + * @param warehouseCode 仓库编码 + * @param warehouseId 仓库ID + * @return 解析结果:委托单对象 + 校验错误信息;错误信息非空时不应落库 + */ + public ParseResult parseOutEntrustOrderExcel(MultipartFile file, String warehouseName, String warehouseCode, Long warehouseId) throws IOException { + + OutEntrustHeaderListener headerListener = new OutEntrustHeaderListener(); + + ReadSheet headerSheet = EasyExcel.readSheet(0) + .headRowNumber(0) + .build(); + + ExcelReader headerReader = EasyExcel.read(file.getInputStream()) + .autoCloseStream(false) + .registerReadListener(headerListener) + .registerConverter(new NoConvertConverter()) + .headRowNumber(0) + .build(); + + // 通过监听器控制只读取前11行 + headerReader.read(headerSheet); + + InputStream itemInputStream = file.getInputStream(); + OutEntrustItemListener itemListener = new OutEntrustItemListener(); + + // 商品明细需要跳过前11行,从第12行开始读取 + ExcelReader itemReader = EasyExcel.read(itemInputStream) + .autoCloseStream(false) + .registerReadListener(itemListener) + .registerConverter(new NoConvertConverter()) + .headRowNumber(12) + .build(); + + ReadSheet itemSheet = EasyExcel.readSheet(0).build(); + itemReader.read(itemSheet); + + itemReader.finish(); + List itemList = itemListener.getItemList(); + OutEntrustOrderDO outEntrustOrder = headerListener.getOutEntrustOrder(); + + // 提前获取货主ID,用于查询物料基础信息时过滤 + String customerName = outEntrustOrder.getShipperName(); + UserPo shipperUser = resolveCustomerShipperUserPoForImport(customerName); + Long shipperId = shipperUser != null ? shipperUser.getUserId() : null; + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long topOrganizationId = loginUser != null && loginUser.getUserPo() != null + ? loginUser.getUserPo().getTopOrganizationId() : null; + initList(itemList, shipperId, topOrganizationId); + outEntrustOrder.setMaterialDetailList(itemList); + outEntrustOrder.setWarehouseName(warehouseName); + outEntrustOrder.setWarehouseCode(warehouseCode); + outEntrustOrder.setWarehouseId(warehouseId); + + //业务员默认当前登录人 + if (loginUser != null) { + outEntrustOrder.setSalesmanId(String.valueOf(loginUser.getUserid())); + outEntrustOrder.setSalesmanName(loginUser.getUserPo() != null ? loginUser.getUserPo().getUserName() : loginUser.getUsername()); + } + //货主信息回填(名称/编号以用户中心为准) + if (shipperUser != null) { + outEntrustOrder.setShipperId(shipperUser.getUserId()); + outEntrustOrder.setShipperCode(shipperUser.getUserMemberCode()); + outEntrustOrder.setShipperName(shipperUser.getUserName()); + } + initDict(outEntrustOrder); + //校验必填字段 + StringBuilder errorMsg = check(outEntrustOrder); + // 日期单元格有值但解析失败:与物料一致性错误同等对待,阻断导入并明确提示,杜绝静默丢日期 + for (String dateError : headerListener.getDateParseErrors()) { + errorMsg.append(dateError).append("; "); + } + return new ParseResult(outEntrustOrder, errorMsg); + } + + /** + * 校验导入明细是否已在当前货主下维护物料基础信息 + */ + private StringBuilder validateShipperMaterialDetails(OutEntrustOrderDO outEntrustOrder) { + StringBuilder sb = new StringBuilder(); + List details = outEntrustOrder.getMaterialDetailList(); + if (details == null || details.isEmpty()) { + return sb; + } + Long shipperId = outEntrustOrder.getShipperId(); + for (OutEntrustOrderDetailDO row : details) { + if (row.getMaterialBaseInfoId() != null) { + continue; + } + boolean hasIdentifier = StringUtils.isNotBlank(row.getMaterialNo()) + || StringUtils.isNotBlank(row.getSkucode()) + || StringUtils.isNotBlank(row.getCommodityName()); + if (!hasIdentifier) { + continue; + } + String desc = buildImportMaterialRowDesc(row); + if (ObjectUtil.isEmpty(shipperId)) { + sb.append("请先确认客户(货主)信息;无法校验以下物料是否已在货主下维护:").append(desc).append("; "); + } else { + sb.append("当前货主下未维护以下物料:").append(desc).append("; "); + } + } + return sb; + } + + /** + * 提示文案只强调 Excel 导入列「商品名称」;商品名称为空时用导入料号/SKU 辅助说明。 + */ + private String buildImportMaterialRowDesc(OutEntrustOrderDetailDO row) { + String commodityName = StringUtils.trimToEmpty(row.getCommodityName()); + if (StringUtils.isNotBlank(commodityName)) { + return "商品名称「" + commodityName + "」"; + } + String materialNo = StringUtils.trimToEmpty(row.getMaterialNo()); + String sku = StringUtils.trimToEmpty(row.getSkucode()); + StringBuilder b = new StringBuilder("导入商品名称为空"); + if (StringUtils.isNotBlank(materialNo)) { + b.append(",料号「").append(materialNo).append("」"); + } + if (StringUtils.isNotBlank(sku)) { + b.append(",SKU「").append(sku).append("」"); + } + return b.toString(); + } + + private StringBuilder check(OutEntrustOrderDO outEntrustOrder) { + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append(validateShipperMaterialDetails(outEntrustOrder)); + if (outEntrustOrder.getMaterialDetailList() == null || outEntrustOrder.getMaterialDetailList().isEmpty()) { + errorMsg.append("物料明细列表不能为空或匹配不上物料信息; "); + } + if (StringUtils.isEmpty(outEntrustOrder.getToReceiver())) errorMsg.append("To不能为空; "); + if (StringUtils.isEmpty(outEntrustOrder.getCarrier())) errorMsg.append("承运商不能为空; "); + if (StringUtils.isEmpty(outEntrustOrder.getTransportModeCode())) errorMsg.append("运输方式不能为空; "); + if (StringUtils.isEmpty(outEntrustOrder.getSupervisionModeCode())) errorMsg.append("监督方式不能为空; "); + if (StringUtils.isEmpty(outEntrustOrder.getDepartureArrivalCountry())) errorMsg.append("出发/到达国家不能为空; "); + if (StringUtils.isEmpty(outEntrustOrder.getContactPerson())) errorMsg.append("联系人不能为空; "); + if (ObjectUtil.isEmpty(outEntrustOrder.getShipperId())) errorMsg.append("客户名称不能为空或匹配不上客户信息; "); + if (StringUtils.isEmpty(outEntrustOrder.getDeclareCustoms())) errorMsg.append("申报地关区不能为空; "); + if (StringUtils.isEmpty(outEntrustOrder.getEntryExitCustoms())) errorMsg.append("进出境关别不能为空; "); + return errorMsg; + } + + /** + * @description 初始化物料明细列表,查询物料基础信息并设置物料基础信息ID(与WMS出库管理导入逻辑一致:料号 > SKU码 > 商品名称) + * @param itemList 出仓委托单明细列表 + * @param shipperId 货主ID(用于过滤物料基础信息,确保查询到的是该货主绑定的物料) + * @param topOrganizationId 一级组织ID(用于过滤物料基础信息) + */ + private void initList(List itemList, Long shipperId, Long topOrganizationId) { + for (OutEntrustOrderDetailDO detail : itemList) { + // 未识别货主时不查物料,避免匹配到其他货主的物料基础信息 + if (shipperId == null) { + continue; + } + + // 根据明细表中的字段查询物料基础信息(优先:料号 > 商品SKU码 > 商品名称) + MaterialBaseInfoPO materialBaseInfoPO = null; + + // 优先使用料号查询(materialNo对应materialCode) + String materialNo = detail.getMaterialNo(); + if (StringUtils.isNotBlank(materialNo)) { + List materialList = outEntrustOrderMapper.getMaterialByMaterialCode(materialNo, shipperId, topOrganizationId); + if (materialList != null && !materialList.isEmpty()) { + materialBaseInfoPO = materialList.get(0); + // ==== 料号与规格型号/SKU码一致性校验:任何一项不符直接报对应错误 ==== + // 双方(trim后)必须完全一致:Excel留空而档案有值、或Excel填了值而档案为空,均视为不一致报错 + String excelSpec = StringUtils.trimToEmpty(detail.getSpecificationModel()); + String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel()); + if (!excelSpec.equals(dbSpec)) { + throw new ServiceException(String.format("导入失败:料号「%s」规格型号与物料档案不一致(Excel:%s / 档案:%s)", + materialNo, excelSpec.isEmpty() ? "空" : excelSpec, dbSpec.isEmpty() ? "空" : dbSpec)); + } + String excelSku = StringUtils.trimToEmpty(detail.getSkucode()); + String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode()); + if (!excelSku.equals(dbSku)) { + throw new ServiceException(String.format("导入失败:料号「%s」SKU码与物料档案不一致(Excel:%s / 档案:%s)", + materialNo, excelSku.isEmpty() ? "空" : excelSku, dbSku.isEmpty() ? "空" : dbSku)); + } + } else { + // 料号已填写但在该货主下查不到 → 直接报错(与WMS出库导入一致),不再被SKU码/商品名称静默兜底 + throw new ServiceException(String.format("导入失败:料号「%s」在当前客户(货主)下不存在,请核对料号或在物料管理中维护", materialNo)); + } + } + + // 如果料号查询不到,使用商品SKU码查询(skucode对应barCode) + if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getSkucode())) { + List materialList = outEntrustOrderMapper.getMaterialByBarCode(detail.getSkucode(), shipperId, topOrganizationId); + if (materialList != null && !materialList.isEmpty()) { + materialBaseInfoPO = materialList.get(0); + } + } + + // 如果料号和SKU码都查询不到,使用商品名称查询(commodityName对应materialName) + if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getCommodityName())) { + List materialList = outEntrustOrderMapper.getMaterialByMaterialName(detail.getCommodityName(), shipperId, topOrganizationId); + if (materialList != null && !materialList.isEmpty()) { + materialBaseInfoPO = materialList.get(0); + } + } + + // 设置物料基础信息ID,并从物料基础信息中回填料号、物料编码、物料名称等信息 + if (materialBaseInfoPO != null) { + detail.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId()); + // 单价统一从物料档案回填 + detail.setUnitPrice(materialBaseInfoPO.getUnitPrice()); + // 如果Excel中没有料号,从物料基础信息中回填 + if (StringUtils.isBlank(detail.getMaterialNo())) { + detail.setMaterialNo(materialBaseInfoPO.getMaterialCode()); + } + // 回填物料编码 + if (StringUtils.isBlank(detail.getMaterialCode())) { + detail.setMaterialCode(materialBaseInfoPO.getMaterialCode()); + } + // 回填商品编号 + if (StringUtils.isBlank(detail.getCommodityNo())) { + detail.setCommodityNo(materialBaseInfoPO.getMaterialCode()); + } + // 回填物料名称(如果Excel中的商品名称为空,使用物料基础信息中的名称) + if (StringUtils.isBlank(detail.getCommodityName())) { + detail.setCommodityName(materialBaseInfoPO.getMaterialName()); + } + // 回填物料名称到materialName字段 + detail.setMaterialName(materialBaseInfoPO.getMaterialName()); + // 回填条形码 + detail.setBarCode(materialBaseInfoPO.getBarCode()); + // 如果Excel中没有SKU码,从物料基础信息中回填 + if (StringUtils.isBlank(detail.getSkucode())) { + detail.setSkucode(materialBaseInfoPO.getBarCode()); + } + } + // 单位:优先使用Excel中的"单位"字段,如果为空则使用"申报单位",如果还为空则从物料主数据获取(与WMS出库导入处理一致) + String unit = StringUtils.isNotBlank(detail.getUnit()) ? detail.getUnit() : null; + if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(detail.getDeclareUnit())) { + unit = detail.getDeclareUnit(); + } + if (StringUtils.isBlank(unit) && materialBaseInfoPO != null && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) { + unit = materialBaseInfoPO.getUnitName(); + } + detail.setUnit(unit); + } + } + + private void initDict(OutEntrustOrderDO outEntrustOrder) { + if (ObjectUtil.isNotNull(outEntrustOrder.getContainerNoName())) { + AjaxResult containerNo = systemServiceFeign.selectListByDictType("cabinetNo"); + if (ObjectUtil.isNotNull(containerNo) && "200".equals(String.valueOf(containerNo.get("code")))) { + List containerNoList = JSON.parseArray(JSONObject.toJSONString(containerNo.get("data")), SysDictDataVo.class); + for (SysDictDataVo sysDictDataVo : containerNoList) { + if (outEntrustOrder.getContainerNoName().equals(sysDictDataVo.getDictLabel())) { + outEntrustOrder.setContainerNo(sysDictDataVo.getDictValue()); + break; + } + } + } + } + + if (ObjectUtil.isNotNull(outEntrustOrder.getTransportModeName())) { + AjaxResult transportMode = systemServiceFeign.selectListByDictType("transportMode"); + if (ObjectUtil.isNotNull(transportMode) && "200".equals(String.valueOf(transportMode.get("code")))) { + List transportModeList = JSON.parseArray(JSONObject.toJSONString(transportMode.get("data")), SysDictDataVo.class); + for (SysDictDataVo sysDictDataVo : transportModeList) { + if (outEntrustOrder.getTransportModeName().equals(sysDictDataVo.getDictLabel())) { + outEntrustOrder.setTransportModeCode(sysDictDataVo.getDictValue()); + break; + } + } + } + } + + if (ObjectUtil.isNotNull(outEntrustOrder.getSupervisionModeName())) { + AjaxResult supervisionMode = systemServiceFeign.selectListByDictType("controlType"); + if (ObjectUtil.isNotNull(supervisionMode) && "200".equals(String.valueOf(supervisionMode.get("code")))) { + List supervisionModeList = JSON.parseArray(JSONObject.toJSONString(supervisionMode.get("data")), SysDictDataVo.class); + for (SysDictDataVo sysDictDataVo : supervisionModeList) { + if (outEntrustOrder.getSupervisionModeName().equals(sysDictDataVo.getDictLabel())) { + outEntrustOrder.setSupervisionModeCode(sysDictDataVo.getDictValue()); + break; + } + } + } + } + } + + /** + * 导入出库单:解析出的货主用户是否属于当前登录用户同一组织(organizationId)。 + * 仅当登录用户带有 organizationId 时启用过滤;否则保持与旧行为一致。 + */ + private boolean shipperMatchesImporterOrganization(UserPo userPo, Long loginOrganizationId) { + if (loginOrganizationId == null) { + return true; + } + if (userPo == null) { + return false; + } + Long oid = userPo.getOrganizationId(); + return oid != null && loginOrganizationId.equals(oid); + } + + /** + * 按客户名称或数字ID解析货主用户,逻辑与WMS出库管理导入一致:getInfoByName → 货主列表兜底,均做 organizationId 校验。 + */ + private UserPo resolveCustomerShipperUserPoForImport(String customerName) { + if (StringUtils.isEmpty(customerName)) { + return null; + } + Long loginOrganizationId = null; + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser != null && loginUser.getUserPo() != null) { + loginOrganizationId = loginUser.getUserPo().getOrganizationId(); + } + String shipperName = customerName.trim(); + if (StringUtils.isEmpty(shipperName)) { + return null; + } + + // 纯数字:按用户ID查询 + if (shipperName.matches("\\d+")) { + try { + Long possibleId = Long.valueOf(shipperName); + AjaxResult ajaxResult = userServiceFeign.getInfo(possibleId); + if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) { + UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class); + if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) { + return userPo; + } + } + } catch (Exception e) { + // ignore + } + return null; + } + + // 按名称查询(用户中心) + try { + AjaxResult ajaxResult = userServiceFeign.getInfoByName(shipperName); + if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) { + UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class); + if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) { + return userPo; + } + } + } catch (Exception e) { + // 继续货主列表兜底 + } + + // 货主列表兜底(与WMS出库导入一致) + try { + AjaxResult shipperListResult = userServiceFeign.userShipperListAll(); + if ("200".equals(String.valueOf(shipperListResult.get("code"))) && shipperListResult.get("data") != null) { + Object data = shipperListResult.get("data"); + List> shipperList = null; + if (data instanceof List) { + List rawList = (List) data; + shipperList = new ArrayList<>(); + for (Object item : rawList) { + if (item instanceof Map) { + Map mapItem = (Map) item; + shipperList.add(mapItem); + } else { + Map mapItem = JSON.parseObject(JSONObject.toJSONString(item), new TypeReference>() { + }); + shipperList.add(mapItem); + } + } + } else { + String jsonStr = JSONObject.toJSONString(data); + shipperList = JSON.parseObject(jsonStr, new TypeReference>>() { + }); + } + + if (shipperList != null && !shipperList.isEmpty()) { + for (Map shipper : shipperList) { + Object userId = shipper.get("userId"); + Object userName = shipper.get("userName"); + if (userName == null) { + userName = shipper.get("name"); + } + if (userName == null) { + userName = shipper.get("shipperName"); + } + boolean matched = false; + if (StringUtils.isNotEmpty(shipperName) && userName != null) { + String normalizedUserName = String.valueOf(userName).trim(); + if (shipperName.equals(normalizedUserName) || normalizedUserName.contains(shipperName)) { + matched = true; + } + } + if (matched && userId != null) { + Long candidateId = Long.valueOf(String.valueOf(userId)); + AjaxResult infoRes = userServiceFeign.getInfo(candidateId); + if ("200".equals(String.valueOf(infoRes.get("code"))) && infoRes.get("data") != null) { + UserPo up = JSON.parseObject(JSONObject.toJSONString(infoRes.get("data")), UserPo.class); + if (shipperMatchesImporterOrganization(up, loginOrganizationId)) { + return up; + } + } + } + } + } + } + } catch (Exception e) { + // ignore + } + return null; + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustHeaderListener.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustHeaderListener.java new file mode 100644 index 000000000..c42569cc5 --- /dev/null +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustHeaderListener.java @@ -0,0 +1,201 @@ +package com.mhd.oms.domain.outEntrustOrder.repository.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO; +import lombok.Getter; +import org.apache.poi.ss.usermodel.DateUtil; // Excel序列号转日期,POI已随EasyExcel引入 +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 出仓委托单导入-表头信息监听器(处理前11行,与WMS出库管理导入模板一致) + * + * @author rcx + * @date 2026-09-14 + */ +@Getter +public class OutEntrustHeaderListener extends AnalysisEventListener> { + private final OutEntrustOrderDO outEntrustOrder = new OutEntrustOrderDO(); + private int rowNum = 0; + /** 日期单元格有值但格式无法识别的收集器(类上已有@Getter,自动生成getDateParseErrors()) */ + private final List dateParseErrors = new ArrayList<>(); + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public void invoke(Map data, AnalysisContext context) { + // 将Map转换为Map + Map stringData = convertToStringMap(data); + // 将Map转换为List再转为数组 + List dataList = mapToList(stringData); + Object[] rowData = listToArray(dataList); + String stringValue1 = getStringValue(rowData[0]); + if (stringValue1 != null && !stringValue1.isEmpty()) { + if ("备注:".equals(stringValue1)) { + outEntrustOrder.setRemark(getStringValue(rowData[1])); + } + } + // 根据行号提取对应信息 + switch (rowNum) { + case 4: // 第5行:to、仓库联系人、车型及车牌、委托编码 + outEntrustOrder.setToReceiver(getStringValue(rowData[2])); + outEntrustOrder.setContactPerson(getStringValue(rowData[5])); + outEntrustOrder.setVehicleModelPlate(getStringValue(rowData[12])); + outEntrustOrder.setEntrustNo(getStringValue(rowData[16])); + break; + case 5: // 第6行:承运方、tel、柜号、司机签名 + outEntrustOrder.setCarrier(getStringValue(rowData[2])); + outEntrustOrder.setTel(getStringValue(rowData[5])); + outEntrustOrder.setContainerNoName(getStringValue(rowData[12])); + outEntrustOrder.setDriverSign(getStringValue(rowData[16])); + break; + case 6: // 第7行:出仓日期、fax、运输方式、申报地关区 + Date outboundDate = parseDateSmart(getStringValue(rowData[2]), "出仓日期"); + if (outboundDate != null) { + outEntrustOrder.setOutboundDate(outboundDate); + } + outEntrustOrder.setFax(getStringValue(rowData[5])); + outEntrustOrder.setTransportModeName(getStringValue(rowData[12])); + outEntrustOrder.setDeclareCustoms(getStringValue(rowData[16])); + break; + case 7: // 第8行:生产商、客户名称、报关姓名及联系方式、完成时间 + outEntrustOrder.setManufacturer(getStringValue(rowData[2])); + outEntrustOrder.setShipperName(getStringValue(rowData[5])); + outEntrustOrder.setCustomsDeclarerInfo(getStringValue(rowData[12])); + Date completeTime = parseDateSmart(getStringValue(rowData[16]), "完成时间"); + if (completeTime != null) { + outEntrustOrder.setCompleteTime(completeTime); + } + break; + case 8: // 第9行:监管方式、送货地址、海关是否查验货物、进出境关别 + outEntrustOrder.setSupervisionModeName(getStringValue(rowData[2])); + outEntrustOrder.setDeliveryAddr(getStringValue(rowData[5])); + String stringValue = getStringValue(rowData[12]); + outEntrustOrder.setCustomsInspectionFlag(stringValue); + outEntrustOrder.setEntryExitCustoms(getStringValue(rowData[16])); + break; + case 9: // 第10行:下单日期、起运国 + Date orderDate = parseDateSmart(getStringValue(rowData[2]), "下单日期"); + if (orderDate != null) { + outEntrustOrder.setOrderDate(orderDate); + } + outEntrustOrder.setDepartureArrivalCountry(getStringValue(rowData[5])); + break; + } + rowNum++; + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 解析完成后的处理 + } + + /** + * 将List转换为数组 + */ + private Object[] listToArray(List list) { + // 创建一个足够大的数组来容纳最多30列数据 + Object[] array = new Object[30]; + for (int i = 0; i < 30 && i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + /** + * 将Object的Map转换为String的Map + */ + private Map convertToStringMap(Map dataMap) { + if (dataMap == null) { + return new HashMap<>(); + } + + Map stringMap = new HashMap<>(); + for (Map.Entry entry : dataMap.entrySet()) { + Object value = entry.getValue(); + String stringValue = value != null ? value.toString() : ""; + stringMap.put(entry.getKey(), stringValue); + } + + return stringMap; + } + + /** + * 将Map转换为List + */ + private List mapToList(Map map) { + if (map == null) { + return new ArrayList<>(); + } + + // 找到最大索引以确定列表大小 + int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1; + maxSize = Math.max(maxSize, 30); // 至少30个位置 + + List list = new ArrayList<>(Collections.nCopies(maxSize, "")); + + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() >= 0 && entry.getKey() < maxSize) { + String value = entry.getValue(); + list.set(entry.getKey(), value != null ? value : ""); + } + } + + return list; + } + + /** + * 转换为字符串,处理null值 + */ + private String getStringValue(Object value) { + return value == null ? "" : value.toString().trim(); + } + + /** + * 智能解析日期:兼容 Excel 日期单元格序列号 + 常见文本格式。 + * 解析失败记入 dateParseErrors,由解析服务汇总返回给用户,不再静默丢失。 + * @param raw 单元格原始字符串 + * @param fieldLabel 字段中文名(用于错误提示) + */ + private Date parseDateSmart(String raw, String fieldLabel) { + String s = raw == null ? "" : raw.trim(); + if (s.isEmpty()) { + return null; // 单元格为空:保持原行为,不算错误 + } + // 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) { + } + } + // 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; + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustItemListener.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustItemListener.java new file mode 100644 index 000000000..9bde977cf --- /dev/null +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustItemListener.java @@ -0,0 +1,282 @@ +package com.mhd.oms.domain.outEntrustOrder.repository.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO; +import lombok.Getter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 出仓委托单导入-商品明细监听器(从第12行开始,与WMS出库管理导入模板一致) + * + * @author rcx + * @date 2026-09-14 + */ +@Getter +public class OutEntrustItemListener extends AnalysisEventListener> { + private final List itemList = new ArrayList<>(); + + @Override + public void invoke(Map data, AnalysisContext context) { + // 将Map转换为Map + Map stringData = convertToStringMap(data); + // 将Map转换为List再转为数组 + List dataList = mapToList(stringData); + Object[] rowData = listToArray(dataList); + + // 提取序号(第一列),过滤空行和标题行 + String serialNoStr = getStringValue(rowData[0]); + String commodityName = getStringValue(rowData[1]); + String materialNo = getStringValue(rowData[2]); + + if (commodityName.isEmpty() && materialNo.isEmpty()) { + return; + } + if (serialNoStr.isEmpty() || "序号".equals(serialNoStr) || !isSequenceNumber(serialNoStr)) { + return; + } + + OutEntrustOrderDetailDO item = new OutEntrustOrderDetailDO(); + + // 填充商品明细数据 + //商品名称 料号 规格型号/ITEM 商品SKU码 Invoice No. 申报数量 申报单位 升 箱数 件数 尺寸 出库数量 "总净重 + //(KG)" 总毛重 (KG) "总体积 + //(CBM)" 总面积(SQM) Batch Ref NO's Sheet Ref NO's 原产国 箱号/卡板号 总价 币制 备注 + item.setCommodityName(getStringValue(rowData[1])); + item.setMaterialNo(getStringValue(rowData[2])); + item.setSpecificationModel(getStringValue(rowData[3])); + item.setSkucode(getStringValue(rowData[4])); + item.setInvoiceNo(getStringValue(rowData[5])); + item.setDeclareQuantity(parseBigDecimal(rowData[6])); + item.setDeclareUnit(getStringValue(rowData[7])); + item.setLiter(normalizeNumeric(getStringValue(rowData[8]))); + item.setCaseCount(parseBigDecimal(rowData[9])); + item.setPieceCount(parseBigDecimal(rowData[10])); + item.setSize(getStringValue(rowData[11])); + item.setOutboundQuantity(parseBigDecimal(rowData[12])); + item.setTotalNetWeight(parseBigDecimal(rowData[13])); + item.setTotalGrossWeight(parseBigDecimal(rowData[14])); + item.setTotalVolume(parseBigDecimal(rowData[15])); + item.setTotalArea(parseBigDecimal(rowData[16])); + item.setBatchRefNo(getStringValue(rowData[17])); + item.setSheetRefNo(getStringValue(rowData[18])); + item.setCountryOfOrigin(getStringValue(rowData[19])); + item.setBoxPalletNo(getStringValue(rowData[20])); + item.setTotalAmount(parseBigDecimal(rowData[21])); + item.setCurrency(getStringValue(rowData[22])); + item.setRemark(getStringValue(rowData[23])); + + itemList.add(item); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 解析完成后的处理 + } + + /** + * 将List转换为数组 + */ + private Object[] listToArray(List list) { + // 创建一个足够大的数组来容纳最多30列数据 + Object[] array = new Object[30]; + for (int i = 0; i < 30 && i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + /** + * 将Object的Map转换为String的Map + */ + private Map convertToStringMap(Map dataMap) { + if (dataMap == null) { + return new HashMap<>(); + } + + Map stringMap = new HashMap<>(); + for (Map.Entry entry : dataMap.entrySet()) { + Object value = entry.getValue(); + String stringValue = convertToString(value); + stringMap.put(entry.getKey(), stringValue); + } + + return stringMap; + } + + /** + * 将Object转换为String,处理BigDecimal等数字类型,去除小数点后的零 + */ + private String convertToString(Object value) { + if (value == null) { + return ""; + } + + // 如果是BigDecimal类型,需要特殊处理,去除小数点后的零 + if (value instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) value; + // 如果小数部分为0,转换为整数形式 + if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) { + return String.valueOf(bd.longValue()); + } + // 否则去除尾部零 + return bd.stripTrailingZeros().toPlainString(); + } + + // 如果是Double或Float类型,也需要处理 + if (value instanceof Double) { + Double d = (Double) value; + if (d == d.longValue()) { + return String.valueOf(d.longValue()); + } + return d.toString(); + } + + if (value instanceof Float) { + Float f = (Float) value; + if (f == f.longValue()) { + return String.valueOf(f.longValue()); + } + return f.toString(); + } + + if (value instanceof String) { + String str = value.toString().trim(); + if (str.isEmpty()) { + return ""; + } + return str; + } + + return value.toString().trim(); + } + + /** + * 将Map转换为List + */ + private List mapToList(Map map) { + if (map == null) { + return new ArrayList<>(); + } + + // 找到最大索引以确定列表大小 + int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1; + maxSize = Math.max(maxSize, 30); // 至少30个位置 + + List list = new ArrayList<>(Collections.nCopies(maxSize, "")); + + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() >= 0 && entry.getKey() < maxSize) { + String value = entry.getValue(); + list.set(entry.getKey(), value != null ? value : ""); + } + } + + return list; + } + + private String getStringValue(Object value) { + if (value == null) { + return ""; + } + + // 如果是BigDecimal类型,需要特殊处理,去除小数点后的零 + if (value instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) value; + // 如果小数部分为0,转换为整数形式 + if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) { + return String.valueOf(bd.longValue()); + } + // 否则去除尾部零 + return bd.stripTrailingZeros().toPlainString(); + } + + // 如果是Double或Float类型,也需要处理 + if (value instanceof Double) { + Double d = (Double) value; + if (d == d.longValue()) { + return String.valueOf(d.longValue()); + } + return d.toString(); + } + + if (value instanceof Float) { + Float f = (Float) value; + if (f == f.longValue()) { + return String.valueOf(f.longValue()); + } + return f.toString(); + } + + if (value instanceof String) { + String str = value.toString().trim(); + if (str.isEmpty()) { + return ""; + } + // 尝试解析为数字,如果是整数形式的小数(如 "123.0"),转换为整数 + try { + BigDecimal bd = new BigDecimal(str); + // 如果小数部分为0,转换为整数形式 + if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) { + return String.valueOf(bd.longValue()); + } + // 否则去除尾部零 + return bd.stripTrailingZeros().toPlainString(); + } catch (NumberFormatException e) { + // 不是数字字符串,直接返回 + return str; + } + } + + return value.toString().trim(); + } + + /** + * 解析BigDecimal,失败返回0 + */ + private BigDecimal parseBigDecimal(Object value) { + try { + String strValue = getStringValue(value); + if (strValue.isEmpty()) { + return BigDecimal.ZERO; + } + return new BigDecimal(strValue); + } catch (Exception e) { + return BigDecimal.ZERO; + } + } + + /** + * 升列归一化:去千分位逗号、去空白;空值统一存 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()) { + return false; + } + str = str.trim(); + + return str.matches("^[1-9]\\d*$") || str.matches("^[1-9]\\d*\\.\\d+$") || str.matches("^0\\.\\d+$"); + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java index fcf8d0e61..32528c646 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrder; import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO; import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO; +import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -25,4 +26,25 @@ public interface OutEntrustOrderMapper extends BaseMapper { * 根据编号集合查询出仓委托单列表 */ List getInfoByIds(@Param("idList") List idList); + + /** + * 导入用:按料号查询货主下的物料基础信息(跨库WMS物料表) + */ + List getMaterialByMaterialCode(@Param("materialCode") String materialCode, + @Param("shipperId") Long shipperId, + @Param("topOrganizationId") Long topOrganizationId); + + /** + * 导入用:按SKU码(条形码)查询货主下的物料基础信息(跨库WMS物料表) + */ + List getMaterialByBarCode(@Param("barCode") String barCode, + @Param("shipperId") Long shipperId, + @Param("topOrganizationId") Long topOrganizationId); + + /** + * 导入用:按物料名称查询货主下的物料基础信息(跨库WMS物料表) + */ + List getMaterialByMaterialName(@Param("materialName") String materialName, + @Param("shipperId") Long shipperId, + @Param("topOrganizationId") Long topOrganizationId); } diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java index 19c8cdfba..1ad2051aa 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java @@ -10,6 +10,7 @@ import com.mhd.common.core.domain.po.UserPo; import com.mhd.common.core.enums.DictCode; import com.mhd.common.core.exception.ServiceException; import com.mhd.common.core.utils.StringUtils; +import com.mhd.common.core.utils.poi.ExcelUtil; import com.mhd.common.core.web.domain.AjaxResult; import com.mhd.common.security.utils.SecurityUtils; import com.mhd.oms.domain.erpCountry.entity.ErpCountry; @@ -18,6 +19,7 @@ import com.mhd.oms.domain.gwLog.repository.mapper.GwLogMapper; import com.mhd.oms.domain.reservationInMaterialDetail.entity.ReservationInMaterialDetail; import com.mhd.oms.domain.reservationInMaterialDetail.repository.mapper.ReservationInMaterialDetailMapper; import com.mhd.oms.domain.reservationInMaterialDetail.repository.po.ReservationInMaterialDetailPO; +import com.mhd.oms.domain.reservationInMaterialDetail.repository.todo.ReservationInMaterialDetailDO; import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService; import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper; import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder; @@ -26,6 +28,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfo import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO; import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO; import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO; +import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderImportRowDTO; import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwInDetail; import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwStockInOrder; import com.mhd.oms.interfaces.dto.erpCountry.ErpCountryDTO; @@ -47,6 +50,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; @@ -1590,6 +1594,635 @@ public class ReservationStockInOrderApplicationService { // } // } + /** + * 导入入仓委托单(模板与WMS入库管理导入一致:第1-12行表头,第13行列名,第14行起明细) + * 以【委托编号NO】作为分组键,同一委托编号的多条明细归为一张入库单 + */ + public Boolean importData(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) { + if (file == null || file.isEmpty()) { + throw new ServiceException("导入文件不能为空"); + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser == null) { + throw new ServiceException("登录已失效,请重新登录"); + } + Long topOrganizationId = loginUser.getUserPo() != null ? loginUser.getUserPo().getTopOrganizationId() : null; + + try (InputStream is = file.getInputStream()) { + // 明细表的列名在第13行(索引12),数据从第14行开始 + int detailTitleRowIndex = 12; + + // 1. 读取表头信息(第1-12行),按标签扫描提取字段 + ReservationStockInOrderImportRowDTO headerInfo = new ReservationStockInOrderImportRowDTO(); + try (InputStream isForHeader = file.getInputStream()) { + Workbook wb = WorkbookFactory.create(isForHeader); + Sheet sheet = wb.getSheetAt(0); + fillHeaderInfoByLabels(sheet, headerInfo); + } catch (Exception e) { + log.error("读取Excel表头信息失败", e); + // 不抛出异常,继续尝试从明细行读取 + } + log.info("表头信息读取完成 - 委托编号NO: {}, 客户名称: {}, To: {}, 联系人: {}, 承运商: {}, 入仓日期: {}, 完成时间: {}, 下单日期: {}", + headerInfo.getConsignmentNo(), headerInfo.getShipperName(), headerInfo.getTo(), + headerInfo.getContactPerson(), headerInfo.getCarrier(), headerInfo.getWarehouseDate(), + headerInfo.getCompletionTime(), headerInfo.getOrderDate()); + + // 2. 读取明细数据(ExcelUtil从第13行列名开始) + ExcelUtil util = new ExcelUtil<>(ReservationStockInOrderImportRowDTO.class); + List rows = util.importExcel(is, detailTitleRowIndex); + if (rows == null || rows.isEmpty()) { + throw new ServiceException("导入数据为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行"); + } + log.info("成功读取 {} 行明细数据", rows.size()); + + // 3. 将表头信息应用到每一行(明细行值为空时使用表头值) + applyHeaderToRows(headerInfo, rows); + + // 4. 过滤掉商品名称为空的空行 + List validRows = rows.stream() + .filter(r -> r != null && StringUtils.isNotBlank(r.getMaterialName())) + .collect(Collectors.toList()); + if (validRows.isEmpty()) { + throw new ServiceException("导入数据为空(可能全为空行)。共读取 " + rows.size() + + " 行数据,但商品名称都为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行"); + } + + // 5. 仓库必填校验(与出库单导入保持一致,仓库由导入弹窗选择传入) + if (warehouseId == null) { + throw new ServiceException("导入失败:仓库ID不能为空"); + } + if (StringUtils.isBlank(warehouseCode)) { + throw new ServiceException("导入失败:仓库编码不能为空"); + } + if (StringUtils.isBlank(warehouseName)) { + throw new ServiceException("导入失败:仓库名称不能为空"); + } + + // 6. 以委托编号NO分组;若为空则归到同一组 + Map> groupMap = validRows.stream() + .collect(Collectors.groupingBy(r -> StringUtils.isBlank(r.getConsignmentNo()) ? "__EMPTY_CONSIGNMENT__" : r.getConsignmentNo())); + + List orderList = new ArrayList<>(); + for (Map.Entry> entry : groupMap.entrySet()) { + ReservationStockInOrderDO order = buildStockInOrderFromImport( + entry.getValue(), headerInfo, loginUser, topOrganizationId, + warehouseId, warehouseCode, warehouseName); + if (order != null) { + orderList.add(order); + } + } + + // 如果所有订单都被跳过(orderList为空),抛出异常提示用户 + if (orderList.isEmpty()) { + throw new ServiceException("导入失败:所有入库单的明细行都因数据不完整(数量为空或物料基础信息缺失)被跳过,请检查Excel数据"); + } + + return stockInOrderDomainService.batchInsert(orderList); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("导入入库单失败", e); + throw new ServiceException("导入入库单失败:" + e.getMessage()); + } + } + + /** + * 单个委托编号分组的入库单组装(解析+校验,不落库) + * 明细行数量为空/容器类型行跳过;料号必填且必须在当前货主下存在,规格型号/SKU码与物料档案不一致直接报错 + * + * @return 组装完成的入库单;该组明细全部被跳过时返回null + */ + private ReservationStockInOrderDO buildStockInOrderFromImport(List groupRows, + ReservationStockInOrderImportRowDTO headerInfo, + LoginUser loginUser, Long topOrganizationId, + Long warehouseId, String warehouseCode, String warehouseName) { + ReservationStockInOrderImportRowDTO h = groupRows.get(0); + + // 客户名称优先使用表头,表头为空时使用明细行 + String customerName = StringUtils.isNotBlank(headerInfo.getShipperName()) ? headerInfo.getShipperName() : h.getShipperName(); + if (StringUtils.isBlank(customerName)) { + throw new ServiceException("导入失败:客户名称不能为空(委托编号NO=" + h.getConsignmentNo() + + ")。请确保Excel表头(第1-12行)中包含'客户名称'字段"); + } + + ReservationStockInOrderDO order = new ReservationStockInOrderDO(); + // 组织/审计字段 + if (loginUser.getUserPo() != null) { + order.setOrganizationId(loginUser.getUserPo().getOrganizationId()); + order.setOrganizationName(loginUser.getUserPo().getOrganizationName()); + order.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId()); + } + order.setCreateBy(loginUser.getUserid()); + order.setCreateByName(resolveUserRealName(loginUser)); + order.setCreateTime(new Date()); + + // 仓库(导入弹窗选择传入) + order.setWarehouseId(warehouseId); + order.setWarehouseCode(warehouseCode); + order.setWarehouseName(warehouseName); + + // 货主信息:名称/编号解析货主ID + String shipperCode = StringUtils.isNotBlank(headerInfo.getShipperCode()) ? headerInfo.getShipperCode() : h.getShipperCode(); + Long shipperId = getShipperIdByNameOrCode(customerName, shipperCode); + order.setShipperId(shipperId); + if (StringUtils.isNotBlank(customerName)) { + order.setShipperName(customerName); + } + if (StringUtils.isNotBlank(shipperCode)) { + order.setShipperCode(shipperCode); + } + order.setConsignmentNo(h.getConsignmentNo()); + order.setRemark(h.getRemark()); + // 导入入库单时,如果入库单类型为空,默认设置为普通入库 + order.setOrderTypeCode("pu_tong_ru_ku"); + order.setOrderTypeName("普通入库单"); + // 订单来源:数据导入(与入库业务单多页签导入保持一致) + order.setReservationOrderSource("数据导入"); + + // 报关表头字段(优先使用表头字段,表头为空则使用明细行字段) + order.setTo(firstNotBlank(headerInfo.getTo(), h.getTo())); + order.setContactPerson(firstNotBlank(headerInfo.getContactPerson(), h.getContactPerson())); + order.setVehicleInfo(firstNotBlank(headerInfo.getVehicleInfo(), h.getVehicleInfo())); + order.setTel(firstNotBlank(headerInfo.getTel(), h.getTel())); + order.setDriverSignature(firstNotBlank(headerInfo.getDriverSignature(), h.getDriverSignature())); + order.setFax(firstNotBlank(headerInfo.getFax(), h.getFax())); + order.setDeclarationArea(firstNotBlank(headerInfo.getDeclarationArea(), h.getDeclarationArea())); + order.setManufacturer(firstNotBlank(headerInfo.getManufacturer(), h.getManufacturer())); + order.setCustomsBrokerInfo(firstNotBlank(headerInfo.getCustomsBrokerInfo(), h.getCustomsBrokerInfo())); + order.setDeliveryAddress(firstNotBlank(headerInfo.getDeliveryAddress(), h.getDeliveryAddress())); + order.setEntryExitCustoms(firstNotBlank(headerInfo.getEntryExitCustoms(), h.getEntryExitCustoms())); + order.setOriginDestinationCountry(firstNotBlank(headerInfo.getOriginDestinationCountry(), h.getOriginDestinationCountry())); + order.setCarrier(firstNotBlank(headerInfo.getCarrier(), h.getCarrier())); + order.setContainerNo(firstNotBlank(headerInfo.getContainerNo(), h.getContainerNo())); + order.setTransportMethod(firstNotBlank(headerInfo.getTransportMethod(), h.getTransportMethod())); + order.setSupervisionMethod(firstNotBlank(headerInfo.getSupervisionMethod(), h.getSupervisionMethod())); + + // 报关字段默认值(优先使用表头字段) + Integer customsInspection = headerInfo.getCustomsInspection() != null ? headerInfo.getCustomsInspection() : h.getCustomsInspection(); + order.setCustomsInspection(customsInspection == null ? 0 : customsInspection); // 默认:否 + Integer needCustomsDeclaration = headerInfo.getNeedCustomsDeclaration() != null ? headerInfo.getNeedCustomsDeclaration() : h.getNeedCustomsDeclaration(); + order.setNeedCustomsDeclaration(needCustomsDeclaration == null ? 1 : needCustomsDeclaration); // 默认:是 + Integer needTransport = headerInfo.getNeedTransport() != null ? headerInfo.getNeedTransport() : h.getNeedTransport(); + order.setNeedTransport(needTransport == null ? 0 : needTransport); // 默认:否 + + // 日期字段(优先使用表头字段) + String warehouseDateStr = firstNotBlank(headerInfo.getWarehouseDate(), h.getWarehouseDate()); + if (StringUtils.isNotBlank(warehouseDateStr)) { + Date warehouseDate = parseDateString(warehouseDateStr); + if (warehouseDate != null) { + order.setWarehouseDate(warehouseDate); + } + } + String completionTimeStr = firstNotBlank(headerInfo.getCompletionTime(), h.getCompletionTime()); + if (StringUtils.isNotBlank(completionTimeStr)) { + Date completionTime = parseDateString(completionTimeStr); + if (completionTime != null) { + order.setCompletionTime(completionTime); + } + } + String orderDateStr = firstNotBlank(headerInfo.getOrderDate(), h.getOrderDate()); + if (StringUtils.isNotBlank(orderDateStr)) { + Date orderDate = parseDateString(orderDateStr); + if (orderDate != null) { + order.setOrderDate(orderDate); + } + } + + // 明细 + List details = new ArrayList<>(); + for (ReservationStockInOrderImportRowDTO r : groupRows) { + // 先检查数量(仓库数量优先,入库数量兜底),数量为空直接跳过该明细行 + BigDecimal quantity = r.getWarehouseQuantity(); + if (quantity == null) { + quantity = r.getInboundQuantity(); + } + if (quantity == null) { + log.warn("跳过明细行:仓库数量或入库数量不能为空 - 委托编号NO: [{}], 商品名称: [{}], 料号: [{}]", + h.getConsignmentNo(), r.getMaterialName(), r.getMaterialCode()); + continue; + } + + // 检查是否为容器类型等非物料信息,如果是则跳过 + String materialName = r.getMaterialName(); + if (StringUtils.isNotBlank(materialName)) { + String lowerName = materialName.trim(); + if (lowerName.contains("尺柜") || lowerName.contains("尺集装箱") + || lowerName.contains("Container") || lowerName.contains("container") + || (lowerName.contains("柜") && (lowerName.contains("20") || lowerName.contains("40") || lowerName.contains("45")))) { + log.info("跳过明细行:识别为容器类型信息,非实际物料 - 委托编号NO: [{}], 商品名称: [{}]", + h.getConsignmentNo(), materialName); + continue; + } + } + + // 导入文件中必须填写料号,根据料号获取商品信息,必须是货主下有的才可以匹配 + if (StringUtils.isBlank(r.getMaterialCode())) { + throw new ServiceException("导入失败:请在导入文件中填写正确的料号"); + } + + // 根据料号跨库查询物料基础信息(getByCode按组织过滤),查到后需在Java侧核对货主归属 + MaterialBaseInfoPO materialBaseInfoPO = safeGetMaterialByCode(r.getMaterialCode(), loginUser); + if (materialBaseInfoPO != null && shipperId != null + && (materialBaseInfoPO.getShipperId() == null || !materialBaseInfoPO.getShipperId().equals(shipperId))) { + // 查到但不属于该货主,等同于不存在,不允许导入 + materialBaseInfoPO = null; + } + if (materialBaseInfoPO == null) { + // 如果提供了料号但查询不到(或不属于该货主),直接报错,不允许导入 + throw new ServiceException(String.format("导入失败:该货主下不存在该料号(货主名称=%s,料号=%s)。请确保该料号已在物料管理中创建,且属于该货主", + order.getShipperName(), r.getMaterialCode())); + } + + // 校验料号与规格型号/SKU码一致性(与WMS入库管理导入一致):任何一项不符直接报对应错误 + String excelSpec = StringUtils.trimToEmpty(r.getSpecificationModel()); + String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel()); + if (!excelSpec.equals(dbSpec)) { + throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)规格型号与物料档案不一致(Excel:%s / 档案:%s)", + r.getMaterialCode(), r.getMaterialName(), + excelSpec.isEmpty() ? "空" : excelSpec, dbSpec.isEmpty() ? "空" : dbSpec)); + } + String excelSku = StringUtils.trimToEmpty(r.getSkuCode()); + String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode()); + if (!excelSku.equals(dbSku)) { + throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)SKU码与物料档案不一致(Excel:%s / 档案:%s)", + r.getMaterialCode(), r.getMaterialName(), + excelSku.isEmpty() ? "空" : excelSku, dbSku.isEmpty() ? "空" : dbSku)); + } + + ReservationInMaterialDetailDO d = new ReservationInMaterialDetailDO(); + d.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId()); + // 从物料档案获取名称(与WMS入库导入一致) + d.setMaterialName(materialBaseInfoPO.getMaterialName()); + d.setMaterialCode(r.getMaterialCode()); + d.setQuantity(quantity); + d.setSpecificationModel(r.getSpecificationModel()); + d.setSkuCode(r.getSkuCode()); + d.setInvoiceNo(r.getInvoiceNo()); + d.setDeclaredQuantity(r.getDeclaredQuantity()); + d.setDeclarationUnit(r.getDeclarationUnit()); + d.setLiter(r.getLiter()); + d.setBoxCount(r.getBoxCount()); + d.setPieceCount(r.getPieceCount()); + d.setDimensions(r.getDimensions()); + // 入库数量:优先使用入库数量,如果为空则使用仓库数量 + BigDecimal inboundQuantity = r.getInboundQuantity(); + if (inboundQuantity == null) { + inboundQuantity = r.getWarehouseQuantity(); + } + d.setInboundQuantity(inboundQuantity); + // 单位:优先使用Excel中的"单位"字段,如果为空则使用"申报单位",如果还为空则从物料主数据获取 + String unit = StringUtils.isNotBlank(r.getUnit()) ? r.getUnit() : null; + if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(r.getDeclarationUnit())) { + unit = r.getDeclarationUnit(); + } + if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) { + unit = materialBaseInfoPO.getUnitName(); + } + d.setUnit(unit); + // 总净重/总毛重/总体积/总面积:优先使用主字段,如果为空则使用备用字段 + d.setTotalNetWeight(r.getTotalNetWeight() != null ? r.getTotalNetWeight() : r.getTotalNetWeight2()); + d.setTotalGrossWeight(r.getTotalGrossWeight() != null ? r.getTotalGrossWeight() : r.getTotalGrossWeight2()); + d.setTotalVolume(r.getTotalVolume() != null ? r.getTotalVolume() : r.getTotalVolume2()); + d.setTotalArea(r.getTotalArea() != null ? r.getTotalArea() : r.getTotalArea2()); + d.setBatchRefNo(r.getBatchRefNo()); + d.setSheetRefNo(r.getSheetRefNo()); + // 原产国:优先使用Excel中的数据,如果为空则从物料主数据获取 + String originCountry = StringUtils.isNotBlank(r.getOriginCountry()) ? r.getOriginCountry() : null; + if (StringUtils.isBlank(originCountry) && StringUtils.isNotBlank(materialBaseInfoPO.getOriginCountry())) { + originCountry = materialBaseInfoPO.getOriginCountry(); + } + d.setOriginCountry(originCountry); + d.setBoxPalletNo(r.getBoxPalletNo()); + d.setTotalPrice(r.getTotalPrice()); + // 币制:优先使用Excel中的数据,如果为空则从物料主数据获取 + String currency = StringUtils.isNotBlank(r.getCurrency()) ? r.getCurrency() : null; + if (StringUtils.isBlank(currency) && StringUtils.isNotBlank(materialBaseInfoPO.getCurrency())) { + currency = materialBaseInfoPO.getCurrency(); + } + d.setCurrency(currency); + d.setRemark(r.getDetailRemark()); + + details.add(d); + } + + // 如果该订单的所有明细行都被跳过(details为空),跳过该订单 + if (details.isEmpty()) { + log.warn("跳过入库单:委托编号NO [{}] 的所有明细行都因数据不完整被跳过,该入库单将不会被导入", h.getConsignmentNo()); + return null; + } + + order.setMaterialDetailList(details); + + // 导入入库单时,计划数量取所有明细行的入库数量之和 + BigDecimal totalInboundQuantity = details.stream() + .map(detail -> detail.getInboundQuantity() != null ? detail.getInboundQuantity() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + order.setQuantity(totalInboundQuantity); + + // 导入时补全货主/供应商/数据字典信息(与手动新增链路一致) + try { + shipperParam(order); + } catch (Exception e) { + log.error("导入入库单时设置货主信息失败,委托编号NO:{},货主名称:{},错误:{}", + h.getConsignmentNo(), order.getShipperName(), e.getMessage()); + throw new ServiceException("导入失败:设置货主信息失败(委托编号NO=" + h.getConsignmentNo() + + ",货主名称=" + order.getShipperName() + ")。" + e.getMessage()); + } + supplierParam(order); + setDataDict(order); + return order; + } + + /** + * 导入用:按料号跨库查询物料基础信息(查询结果需在Java侧核对货主归属) + */ + private MaterialBaseInfoPO safeGetMaterialByCode(String materialCode, LoginUser loginUser) { + try { + Long orgId = loginUser.getUserPo() != null ? loginUser.getUserPo().getOrganizationId() : null; + return reservationStockInOrderMapper.getByCode(materialCode, orgId); + } catch (Exception e) { + log.warn("按料号查询物料基础信息失败,料号: [{}], 错误: {}", materialCode, e.getMessage()); + return null; + } + } + + /** + * 将表头信息应用到每一行明细(明细行值为空时使用表头值;客户名称/货主编号表头有值时强制应用) + */ + private void applyHeaderToRows(ReservationStockInOrderImportRowDTO headerInfo, List rows) { + for (ReservationStockInOrderImportRowDTO row : rows) { + if (row == null) { + continue; + } + // 应用表头中的委托编号NO + if (StringUtils.isBlank(row.getConsignmentNo()) && StringUtils.isNotBlank(headerInfo.getConsignmentNo())) { + row.setConsignmentNo(headerInfo.getConsignmentNo()); + } + // 表头有客户名称/货主编号时,强制应用到所有明细行 + if (StringUtils.isNotBlank(headerInfo.getShipperName())) { + row.setShipperName(headerInfo.getShipperName()); + } + if (StringUtils.isNotBlank(headerInfo.getShipperCode())) { + row.setShipperCode(headerInfo.getShipperCode()); + } + // 应用其他表头字段(如果明细行为空,则使用表头值) + row.setTo(firstNotBlank(row.getTo(), headerInfo.getTo())); + row.setContactPerson(firstNotBlank(row.getContactPerson(), headerInfo.getContactPerson())); + row.setVehicleInfo(firstNotBlank(row.getVehicleInfo(), headerInfo.getVehicleInfo())); + row.setCarrier(firstNotBlank(row.getCarrier(), headerInfo.getCarrier())); + row.setTel(firstNotBlank(row.getTel(), headerInfo.getTel())); + row.setDriverSignature(firstNotBlank(row.getDriverSignature(), headerInfo.getDriverSignature())); + row.setFax(firstNotBlank(row.getFax(), headerInfo.getFax())); + row.setDeclarationArea(firstNotBlank(row.getDeclarationArea(), headerInfo.getDeclarationArea())); + row.setManufacturer(firstNotBlank(row.getManufacturer(), headerInfo.getManufacturer())); + row.setCustomsBrokerInfo(firstNotBlank(row.getCustomsBrokerInfo(), headerInfo.getCustomsBrokerInfo())); + row.setDeliveryAddress(firstNotBlank(row.getDeliveryAddress(), headerInfo.getDeliveryAddress())); + row.setEntryExitCustoms(firstNotBlank(row.getEntryExitCustoms(), headerInfo.getEntryExitCustoms())); + row.setOriginDestinationCountry(firstNotBlank(row.getOriginDestinationCountry(), headerInfo.getOriginDestinationCountry())); + row.setContainerNo(firstNotBlank(row.getContainerNo(), headerInfo.getContainerNo())); + row.setTransportMethod(firstNotBlank(row.getTransportMethod(), headerInfo.getTransportMethod())); + row.setSupervisionMethod(firstNotBlank(row.getSupervisionMethod(), headerInfo.getSupervisionMethod())); + if (row.getCustomsInspection() == null) { + row.setCustomsInspection(headerInfo.getCustomsInspection()); + } + if (row.getNeedCustomsDeclaration() == null) { + row.setNeedCustomsDeclaration(headerInfo.getNeedCustomsDeclaration()); + } + if (row.getNeedTransport() == null) { + row.setNeedTransport(headerInfo.getNeedTransport()); + } + row.setWarehouseDate(firstNotBlank(row.getWarehouseDate(), headerInfo.getWarehouseDate())); + row.setCompletionTime(firstNotBlank(row.getCompletionTime(), headerInfo.getCompletionTime())); + row.setOrderDate(firstNotBlank(row.getOrderDate(), headerInfo.getOrderDate())); + row.setRemark(firstNotBlank(row.getRemark(), headerInfo.getRemark())); + } + } + + /** + * 扫描表头区域(第1-12行)按标签提取字段值: + * 值优先取标签同行右侧最近的合法单元格,其次取下一行同列,最后取标签文本冒号后的内容; + * 看起来仍是标签文本(含冒号或命中标签关键词)的值会被跳过 + */ + private void fillHeaderInfoByLabels(Sheet sheet, ReservationStockInOrderImportRowDTO headerInfo) { + for (int rowIdx = 0; rowIdx < 12; rowIdx++) { + Row row = sheet.getRow(rowIdx); + if (row == null) { + continue; + } + for (int colIdx = 0; colIdx < row.getPhysicalNumberOfCells(); colIdx++) { + Cell labelCell = row.getCell(colIdx); + if (labelCell == null) { + continue; + } + String labelValue = getCellValueAsString(labelCell); + if (StringUtils.isBlank(labelValue)) { + continue; + } + // 标签文本冒号后的内容(兜底值) + String extractedFromSameCell = null; + if (labelValue.contains(":")) { + String[] parts = labelValue.split(":", 2); + if (parts.length > 1) { + extractedFromSameCell = parts[1].trim(); + } + } + String value = findValueRightwards(row, colIdx); + if (StringUtils.isBlank(value)) { + value = findValueBelow(sheet, rowIdx, colIdx); + } + if (StringUtils.isBlank(value)) { + value = extractedFromSameCell; + } + if (StringUtils.isBlank(value)) { + continue; + } + dispatchHeaderField(labelValue, value, headerInfo); + } + } + } + + /** + * 在同行右侧(最多10列)查找第一个合法值 + */ + private String findValueRightwards(Row row, int colIdx) { + for (int searchColIdx = colIdx + 1; searchColIdx < colIdx + 10 && searchColIdx < row.getPhysicalNumberOfCells(); searchColIdx++) { + Cell valueCell = row.getCell(searchColIdx); + if (valueCell == null) { + continue; + } + String cellValue = getCellValueAsString(valueCell); + if (StringUtils.isNotBlank(cellValue)) { + return cellValue; + } + } + return null; + } + + /** + * 在下一行同列查找合法值(模板变体:值写在标签下一行) + */ + private String findValueBelow(Sheet sheet, int rowIdx, int colIdx) { + Row nextRow = sheet.getRow(rowIdx + 1); + if (nextRow == null) { + return null; + } + Cell nextRowCell = nextRow.getCell(colIdx); + if (nextRowCell == null) { + return null; + } + String nextRowValue = getCellValueAsString(nextRowCell); + return StringUtils.isNotBlank(nextRowValue) ? nextRowValue : null; + } + + /** + * 按标签关键词分发到表头字段 + */ + private void dispatchHeaderField(String labelValue, String value, ReservationStockInOrderImportRowDTO headerInfo) { + // 备注:值内容任意,不做标签过滤 + if (labelValue.contains("备注")) { + if (StringUtils.isBlank(headerInfo.getRemark())) { + headerInfo.setRemark(value); + } + return; + } + // 委托编号NO(支持"委托编号NO"、"委托编号"等格式,排除标签本身) + if (labelValue.contains("委托编号")) { + if (StringUtils.isBlank(headerInfo.getConsignmentNo()) + && !value.contains("委托编号") && !value.contains("NO") + && !"委托编号NO".equals(value) && !"委托编号".equals(value)) { + headerInfo.setConsignmentNo(value); + } + return; + } + // 客户名称 + if (labelValue.contains("客户名称")) { + if (StringUtils.isBlank(headerInfo.getShipperName()) + && !value.contains("客户") && !"客户名称".equals(value)) { + headerInfo.setShipperName(value); + } + return; + } + // To(需要精确匹配,避免误匹配) + if (isToLabel(labelValue)) { + if (StringUtils.isBlank(headerInfo.getTo()) && !isToLabel(value) && !looksLikeLabel(value)) { + headerInfo.setTo(value); + } + return; + } + // 值是标签文本(含冒号或命中标签关键词)时跳过 + if (value.contains(":") || value.contains(":") || looksLikeLabel(value)) { + return; + } + if (labelValue.contains("联系人") && StringUtils.isBlank(headerInfo.getContactPerson())) { + headerInfo.setContactPerson(value); + } else if ((labelValue.contains("车型") || labelValue.contains("车牌")) && StringUtils.isBlank(headerInfo.getVehicleInfo())) { + headerInfo.setVehicleInfo(value); + } else if ((labelValue.contains("承运方") || labelValue.contains("承运商")) && StringUtils.isBlank(headerInfo.getCarrier())) { + headerInfo.setCarrier(value); + } else if (labelValue.contains("Tel") && StringUtils.isBlank(headerInfo.getTel())) { + headerInfo.setTel(value); + } else if (labelValue.contains("司机签名") && StringUtils.isBlank(headerInfo.getDriverSignature())) { + headerInfo.setDriverSignature(value); + } else if (labelValue.contains("入仓日期") && StringUtils.isBlank(headerInfo.getWarehouseDate())) { + headerInfo.setWarehouseDate(value); + } else if (labelValue.contains("Fax") && StringUtils.isBlank(headerInfo.getFax())) { + headerInfo.setFax(value); + } else if (labelValue.contains("申报地关区") && StringUtils.isBlank(headerInfo.getDeclarationArea())) { + headerInfo.setDeclarationArea(value); + } else if (labelValue.contains("生产商") && StringUtils.isBlank(headerInfo.getManufacturer())) { + headerInfo.setManufacturer(value); + } else if (labelValue.contains("报关员") && StringUtils.isBlank(headerInfo.getCustomsBrokerInfo())) { + headerInfo.setCustomsBrokerInfo(value); + } else if (labelValue.contains("送货地址") && StringUtils.isBlank(headerInfo.getDeliveryAddress())) { + headerInfo.setDeliveryAddress(value); + } else if (labelValue.contains("进出境关别") && StringUtils.isBlank(headerInfo.getEntryExitCustoms())) { + headerInfo.setEntryExitCustoms(value); + } else if ((labelValue.contains("起运") || labelValue.contains("运抵国")) && StringUtils.isBlank(headerInfo.getOriginDestinationCountry())) { + headerInfo.setOriginDestinationCountry(value); + } else if (labelValue.contains("柜号") && StringUtils.isBlank(headerInfo.getContainerNo())) { + headerInfo.setContainerNo(value); + } else if (labelValue.contains("运输方式") && StringUtils.isBlank(headerInfo.getTransportMethod())) { + headerInfo.setTransportMethod(value); + } else if (labelValue.contains("监管方式") && StringUtils.isBlank(headerInfo.getSupervisionMethod())) { + headerInfo.setSupervisionMethod(value); + } else if (labelValue.contains("海关是否查验货物") && headerInfo.getCustomsInspection() == null) { + headerInfo.setCustomsInspection(parseYesNo(value)); + } else if (labelValue.contains("是否需要报关") && headerInfo.getNeedCustomsDeclaration() == null) { + headerInfo.setNeedCustomsDeclaration(parseYesNo(value)); + } else if (labelValue.contains("是否需要运输") && headerInfo.getNeedTransport() == null) { + headerInfo.setNeedTransport(parseYesNo(value)); + } else if (labelValue.contains("完成时间") && StringUtils.isBlank(headerInfo.getCompletionTime())) { + headerInfo.setCompletionTime(value); + } else if (labelValue.contains("下单日期") && StringUtils.isBlank(headerInfo.getOrderDate())) { + headerInfo.setOrderDate(value); + } + } + + /** + * 是否为To标签(精确匹配,避免把Value/Total等英文词误认为To) + */ + private boolean isToLabel(String text) { + if (text == null) { + return false; + } + String t = text.trim(); + return "To".equals(t) || "To:".equals(t) || "To:".equals(t) || t.startsWith("To:") || t.startsWith("To:"); + } + + /** + * 值是否看起来仍是标签文本(命中常见标签关键词) + */ + private boolean looksLikeLabel(String value) { + if (value == null) { + return true; + } + String v = value.trim(); + return v.contains("委托编号") || v.contains("司机签名") || v.contains("完成时间") || v.contains("下单日期") + || v.contains("入仓日期") || v.contains("联系人") || v.contains("Tel") || v.contains("Fax") + || v.contains("客户") || v.contains("地址") || v.contains("日期") || v.contains("方式") + || v.contains("关区") || v.contains("关别") || v.contains("报关员") || v.contains("生产商") + || v.contains("承运方") || v.contains("承运商") || v.contains("起运") || v.contains("运抵国") + || v.contains("地区") || v.contains("车型") || v.contains("车牌") || v.contains("柜号") + || v.contains("海关是否查验货物") || v.contains("是否需要报关") || v.contains("是否需要运输"); + } + + /** + * 是/否(或1/0)转Integer;无法识别返回null + */ + private Integer parseYesNo(String value) { + if (value == null) { + return null; + } + String v = value.trim(); + if (v.contains("是") || "1".equals(v)) { + return 1; + } + if (v.contains("否") || "0".equals(v)) { + return 0; + } + return null; + } + + /** + * 取第一个非空字符串 + */ + private String firstNotBlank(String a, String b) { + return StringUtils.isNotBlank(a) ? a : b; + } + + /** + * 获取登录人姓名(优先UserPo.userName,其次realname,最后username兜底) + */ + private String resolveUserRealName(LoginUser loginUser) { + if (loginUser.getUserPo() != null && StringUtils.isNotBlank(loginUser.getUserPo().getUserName())) { + return loginUser.getUserPo().getUserName(); + } + if (StringUtils.isNotBlank(loginUser.getRealname())) { + return loginUser.getRealname(); + } + return loginUser.getUsername(); + } + /** * 解析日期字符串为Date对象 * 支持多种日期格式:yyyy-MM-dd, yyyy/MM/dd, yyyy年MM月dd日, Date.toString()格式等 diff --git a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java index 4f24f5cfe..f42808288 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java +++ b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java @@ -12,8 +12,10 @@ import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -80,6 +82,42 @@ public class OutEntrustOrderApi extends BaseController { return AjaxResult.success(outEntrustOrderApplicationService.getInfo(id)); } + /** + * 上传并解析出仓委托单Excel(模板与WMS出库管理导入一致:前11行表头+第12行起明细) + * 解析校验通过后落库为出仓委托单(生成CKWT委托单号,状态待审核/未下发) + */ + @ApiOperation("导入出仓委托单") + @PostMapping("/import") + public AjaxResult importOutEntrustOrder( + @RequestParam("file") MultipartFile file, + @RequestParam("warehouseId") Long warehouseId, + @RequestParam("warehouseCode") String warehouseCode, + @RequestParam("warehouseName") String warehouseName) { + + // 验证文件 + if (file.isEmpty()) { + return error("文件为空"); + } + + // 验证文件类型 + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return error("文件类型错误"); + } + + try { + // 解析Excel并返回结果 + StringBuilder result = outEntrustOrderApplicationService.importOutEntrustOrder(file, warehouseId, warehouseCode, warehouseName); + if (result.length() > 0) { + return AjaxResult.error(result.toString()); + } else { + return AjaxResult.success(result); + } + } catch (IOException e) { + return error("解析失败"); + } + } + /** * 批量审核:待审核/已驳回 → 已审核(通过)或已驳回 * 请求体:{"idList":[1,2],"orderStatus":3,"auditRemark":"备注"},orderStatus:2-审核通过 3-驳回(auditRemark选填) diff --git a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java index cd6a3701e..e104d34a3 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java +++ b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java @@ -99,18 +99,28 @@ public class ReservationStockInOrderApi extends BaseController { } /** - * 导入入库单 + * 导入入仓委托单(模板与WMS入库管理导入一致:第1-12行表头,第13行列名,第14行起明细) + * 按【委托编号NO】分组,同一委托编号的多条明细归为一张入库单 */ -// @ApiOperation("导入入库单") -// @PostMapping(value = "/importData") -// public AjaxResult importData( -// @RequestParam("file") MultipartFile file, -// @RequestParam("warehouseId") Long warehouseId, -// @RequestParam("warehouseCode") String warehouseCode, -// @RequestParam("warehouseName") String warehouseName) -// { -// return AjaxResult.success(stockInOrderApplicationService.importData(file, warehouseId, warehouseCode, warehouseName)); -// } + @ApiOperation("导入入仓委托单") + @PostMapping(value = "/importData") + public AjaxResult importData( + @RequestParam("file") MultipartFile file, + @RequestParam("warehouseId") Long warehouseId, + @RequestParam("warehouseCode") String warehouseCode, + @RequestParam("warehouseName") String warehouseName) + { + // 验证文件 + if (file.isEmpty()) { + return error("文件为空"); + } + // 验证文件类型 + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return error("文件类型错误"); + } + return AjaxResult.success(stockInOrderApplicationService.importData(file, warehouseId, warehouseCode, warehouseName)); + } /** * 审核入库单 diff --git a/mhd_oms/src/main/resources/mapper/outEntrustOrder/OutEntrustOrderMapper.xml b/mhd_oms/src/main/resources/mapper/outEntrustOrder/OutEntrustOrderMapper.xml index d5a2017aa..ae09c13c3 100644 --- a/mhd_oms/src/main/resources/mapper/outEntrustOrder/OutEntrustOrderMapper.xml +++ b/mhd_oms/src/main/resources/mapper/outEntrustOrder/OutEntrustOrderMapper.xml @@ -178,4 +178,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + select * from "NGWL_TEST_WMS".MATERIAL_BASE_INFO + where DEL_FLAG = 1 + and SHIPPER_ID = #{shipperId} + + and TOP_ORGANIZATION_ID = #{topOrganizationId} + + + + + + + + +