| | |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptImportExcelVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.line.MesWmItemReceiptLineSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.wm.vo.MesWmDocImportRespVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO; |
| | |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.context.annotation.Lazy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.PlatformTransactionManager; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.transaction.support.TransactionTemplate; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | |
| | | @Resource |
| | | private MesWmItemReceiptLineService itemReceiptLineService; |
| | | @Resource |
| | | private PlatformTransactionManager transactionManager; |
| | | @Resource |
| | | private MesWmItemReceiptDetailService itemReceiptDetailService; |
| | | @Resource |
| | | @Lazy |
| | |
| | | |
| | | @Resource |
| | | private MesWmArrivalNoticeLineService arrivalNoticeLineService; |
| | | |
| | | @Resource |
| | | @Lazy |
| | | private MesMdItemService mdItemService; |
| | | |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | |
| | | return "IR" + System.currentTimeMillis(); |
| | | } |
| | | |
| | | // ==================== 批量导入 ==================== |
| | | |
| | | @Override |
| | | public MesWmDocImportRespVO importItemReceiptList(List<MesWmItemReceiptImportExcelVO> importList) { |
| | | MesWmDocImportRespVO respVO = MesWmDocImportRespVO.empty(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | return respVO; |
| | | } |
| | | // 1. 按「入库单编码」归并成单据 + 明细行 |
| | | Map<String, List<MesWmItemReceiptImportExcelVO>> grouped = new LinkedHashMap<>(); |
| | | int index = 0; |
| | | for (MesWmItemReceiptImportExcelVO row : importList) { |
| | | index++; |
| | | String code = StrUtil.trim(row.getReceiptCode()); |
| | | if (StrUtil.isBlank(code)) { |
| | | respVO.getFailureCodes().put("第 " + index + " 行", "入库单编码不能为空"); |
| | | continue; |
| | | } |
| | | grouped.computeIfAbsent(code, k -> new ArrayList<>()).add(row); |
| | | } |
| | | |
| | | // 2. 预加载供应商,按名称建索引 |
| | | // 注意:getSupplierSimpleList() 只返回特定状态的供应商,可能缺少历史/停用供应商, |
| | | // 因此这里以「供应商名称」逐行兜底查询(见 resolveVendorId)。 |
| | | Map<String, Long> vendorMap = new HashMap<>(); |
| | | try { |
| | | List<SrmSupplierRespDTO> suppliers = srmSupplierApi.getSupplierSimpleList().getCheckedData(); |
| | | if (CollUtil.isNotEmpty(suppliers)) { |
| | | suppliers.forEach(s -> { |
| | | if (StrUtil.isNotBlank(s.getName())) { |
| | | vendorMap.put(s.getName().trim(), s.getId()); |
| | | } |
| | | }); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[importItemReceiptList][加载供应商简洁列表失败,将逐行查询]", e); |
| | | } |
| | | |
| | | // 3. 逐张单据处理 |
| | | for (Map.Entry<String, List<MesWmItemReceiptImportExcelVO>> entry : grouped.entrySet()) { |
| | | String code = entry.getKey(); |
| | | List<MesWmItemReceiptImportExcelVO> rows = entry.getValue(); |
| | | MesWmItemReceiptImportExcelVO head = rows.get(0); |
| | | try { |
| | | // 3.1 校验供应商 |
| | | String vendorName = StrUtil.trim(head.getVendorName()); |
| | | Long vendorId = vendorName == null ? null : vendorMap.get(vendorName); |
| | | if (vendorId == null && vendorName != null) { |
| | | // 兜底:简洁列表可能未覆盖该供应商,按名称查询 |
| | | SrmSupplierRespDTO supplier = findSupplierByName(vendorName); |
| | | if (supplier != null) { |
| | | vendorId = supplier.getId(); |
| | | vendorMap.put(vendorName, vendorId); |
| | | } |
| | | } |
| | | if (vendorId == null) { |
| | | respVO.getFailureCodes().put(code, "供应商[" + vendorName + "]不存在"); |
| | | continue; |
| | | } |
| | | // 3.2 校验日期与编码 |
| | | LocalDateTime receiptDate = parseImportDateTime(head.getReceiptDate()); |
| | | if (receiptDate == null) { |
| | | respVO.getFailureCodes().put(code, "入库日期不能为空或格式不正确(支持 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss)"); |
| | | continue; |
| | | } |
| | | if (itemReceiptMapper.selectByCode(code) != null) { |
| | | respVO.getFailureCodes().put(code, "入库单编码已存在"); |
| | | continue; |
| | | } |
| | | // 3.3 创建单据头 |
| | | MesWmItemReceiptSaveReqVO saveReqVO = new MesWmItemReceiptSaveReqVO(); |
| | | saveReqVO.setCode(code); |
| | | saveReqVO.setName(StrUtil.blankToDefault(StrUtil.trim(head.getReceiptName()), code)); |
| | | saveReqVO.setVendorId(vendorId); |
| | | saveReqVO.setReceiptDate(receiptDate); |
| | | saveReqVO.setPurchaseOrderCode(StrUtil.trim(head.getPurchaseOrderCode())); |
| | | saveReqVO.setRemark(StrUtil.trim(head.getRemark())); |
| | | Long receiptId = createItemReceipt(saveReqVO); |
| | | // 3.4 逐行创建明细 |
| | | int createdLines = 0; |
| | | for (MesWmItemReceiptImportExcelVO row : rows) { |
| | | if (createItemReceiptLineByImport(receiptId, row, respVO, code)) { |
| | | createdLines++; |
| | | } |
| | | } |
| | | if (createdLines == 0) { |
| | | // 没有任何有效明细时不保留空单,也不报告导入成功。 |
| | | itemReceiptMapper.deleteById(receiptId); |
| | | } else { |
| | | respVO.getCreateCodes().add(code); |
| | | } |
| | | } catch (Exception e) { |
| | | respVO.getFailureCodes().put(code, e.getMessage() == null ? "导入失败" : e.getMessage()); |
| | | } |
| | | } |
| | | return respVO; |
| | | } |
| | | |
| | | /** |
| | | * 导入时创建单行明细;失败只记录不抛出,避免影响整张单据的其它行 |
| | | */ |
| | | private boolean createItemReceiptLineByImport(Long receiptId, MesWmItemReceiptImportExcelVO row, |
| | | MesWmDocImportRespVO respVO, String receiptCode) { |
| | | String itemCode = StrUtil.trim(row.getItemCode()); |
| | | MesMdItemDO item = itemCode == null ? null : mdItemService.getItemByCode(itemCode); |
| | | if (item == null) { |
| | | respVO.getFailureCodes().put(receiptCode, "物料编码[" + itemCode + "]不存在,该行已跳过"); |
| | | return false; |
| | | } |
| | | if (row.getReceivedQuantity() == null || row.getReceivedQuantity().signum() <= 0) { |
| | | respVO.getFailureCodes().put(receiptCode, "物料[" + itemCode + "]入库数量必须大于 0,该行已跳过"); |
| | | return false; |
| | | } |
| | | try { |
| | | MesWmItemReceiptLineSaveReqVO lineReqVO = new MesWmItemReceiptLineSaveReqVO(); |
| | | lineReqVO.setReceiptId(receiptId); |
| | | lineReqVO.setItemId(item.getId()); |
| | | lineReqVO.setReceivedQuantity(row.getReceivedQuantity()); |
| | | lineReqVO.setProductionDate(parseImportDateTime(row.getProductionDate())); |
| | | lineReqVO.setExpireDate(parseImportDateTime(row.getExpireDate())); |
| | | lineReqVO.setLotNumber(StrUtil.trim(row.getLotNumber())); |
| | | lineReqVO.setRemark(StrUtil.trim(row.getRemark())); |
| | | // 每行独立事务:批次校验失败时只回滚该行,避免污染整次导入事务。 |
| | | new TransactionTemplate(transactionManager).executeWithoutResult( |
| | | status -> itemReceiptLineService.createItemReceiptLine(lineReqVO)); |
| | | return true; |
| | | } catch (Exception e) { |
| | | respVO.getFailureCodes().put(receiptCode, "物料[" + itemCode + "]明细创建失败:" + e.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 按名称查找供应商(兜底) |
| | | * |
| | | * getSupplierSimpleList() 只返回已通过准入审批的供应商, |
| | | * 历史导入或未走准入流程的供应商不在其中,因此退化为全量匹配。 |
| | | */ |
| | | private SrmSupplierRespDTO findSupplierByName(String name) { |
| | | try { |
| | | var simple = srmSupplierApi.getSupplierSimpleList(); |
| | | if (simple != null && CollUtil.isNotEmpty(simple.getCheckedData())) { |
| | | SrmSupplierRespDTO hit = simple.getCheckedData().stream() |
| | | .filter(s -> name.equals(StrUtil.trim(s.getName()))) |
| | | .findFirst().orElse(null); |
| | | if (hit != null) { |
| | | return hit; |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[findSupplierByName][简洁列表查询失败,改用全量]", e); |
| | | } |
| | | try { |
| | | var all = srmSupplierApi.getSupplierAllList(); |
| | | if (all != null && CollUtil.isNotEmpty(all.getCheckedData())) { |
| | | return all.getCheckedData().stream() |
| | | .filter(s -> name.equals(StrUtil.trim(s.getName()))) |
| | | .findFirst().orElse(null); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[findSupplierByName][全量列表查询失败]", e); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 解析导入日期,支持 yyyy-MM-dd 与 yyyy-MM-dd HH:mm:ss,失败返回 null |
| | | */ |
| | | private LocalDateTime parseImportDateTime(String text) { |
| | | if (StrUtil.isBlank(text)) { |
| | | return null; |
| | | } |
| | | String value = text.trim().replace('/', '-'); |
| | | try { |
| | | if (value.length() <= 10) { |
| | | return LocalDate.parse(value).atStartOfDay(); |
| | | } |
| | | return LocalDateTime.parse(value.replace(' ', 'T')); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |