| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | |
| | | |
| | | @Resource |
| | | private MesTraceIntegrityService integrityService; |
| | | @Resource private MesProBagCodeMapper pickingBagMapper; |
| | | @Resource private MesProPalletMapper pickingPalletMapper; |
| | | @Resource private MesProBatchMapper pickingBatchMapper; |
| | | @Resource private MesWmMaterialStockBagMapper pickingStockBagMapper; |
| | | |
| | | @Override |
| | | public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) { |
| | |
| | | return true; |
| | | } |
| | | |
| | | private record PickingScope(String type, Long id, MesProBatchDO batch, List<MesProBagCodeDO> bags) { } |
| | | |
| | | private PickingScope pickingScope(String code, String type, boolean lock) { |
| | | if (type == null || type.isBlank()) { |
| | | type = palletService.getPalletByScanCode(code) != null ? "PALLET" |
| | | : bagCodeService.getBagCodeByScanCode(code) != null ? "BAG" : "BATCH"; |
| | | } |
| | | MesProBatchDO batch; |
| | | List<MesProBagCodeDO> bags; |
| | | Long id; |
| | | switch (type) { |
| | | case "BAG" -> { |
| | | MesProBagCodeDO bag = lock ? pickingBagMapper.selectByScanCodeForUpdate(code) : bagCodeService.getBagCodeByScanCode(code); |
| | | if (bag == null || Integer.valueOf(30).equals(bag.getStatus())) throw new IllegalArgumentException("袋码不存在或已作废"); |
| | | id = bag.getId(); batch = proBatchService.getBatch(bag.getBatchId()); bags = List.of(bag); |
| | | } |
| | | case "PALLET" -> { |
| | | MesProPalletDO pallet = lock ? pickingPalletMapper.selectByScanCodeForUpdate(code) : palletService.getPalletByScanCode(code); |
| | | if (pallet == null || Integer.valueOf(20).equals(pallet.getStatus())) throw new IllegalArgumentException("托盘不存在或已作废"); |
| | | if (pallet.getBatchId() == null) throw exception(PRO_PALLET_NOT_BIND_BATCH); |
| | | id = pallet.getId(); batch = proBatchService.getBatch(pallet.getBatchId()); |
| | | bags = lock ? pickingBagMapper.selectListByPalletIdForUpdate(id) : bagCodeService.getBagCodeListByPalletId(id); |
| | | } |
| | | case "BATCH" -> { |
| | | batch = lock ? pickingBatchMapper.selectByScanCodeForUpdate(code) : proBatchService.getBatchByScanCode(code); |
| | | if (batch == null) throw new IllegalArgumentException("生产批次码不存在"); |
| | | id = batch.getId(); bags = lock ? pickingBagMapper.selectListByBatchIdForUpdate(id) : bagCodeService.getBagCodeListByBatchId(id); |
| | | } |
| | | default -> throw new IllegalArgumentException("不支持的拣货扫码类型"); |
| | | } |
| | | if (batch == null || batch.getItemId() == null || Integer.valueOf(40).equals(batch.getStatus())) |
| | | throw new IllegalArgumentException("未绑定有效品种或生产批次已作废"); |
| | | return new PickingScope(type, id, batch, bags.stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())) |
| | | .sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList()); |
| | | } |
| | | |
| | | private String unavailable(MesProBagCodeDO bag, MesWmMaterialStockBagDO stockBag, |
| | | Map<Long, MesWmMaterialStockDO> stocks, List<MesWmProductSalesDetailDO> occupied, MesProBatchDO batch) { |
| | | if (!Objects.equals(bag.getBatchId(), batch.getId())) return "袋码批次归属异常"; |
| | | if (bag.getItemId() != null && !Objects.equals(bag.getItemId(), batch.getItemId())) return "袋码品种归属异常"; |
| | | if (!MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(bag.getQcStatus())) return "尚未质检合格"; |
| | | if (stockBag == null || !Integer.valueOf(1).equals(stockBag.getStatus())) return "不在库或已出库"; |
| | | if (Boolean.TRUE.equals(stockBag.getFrozen())) return "袋码已冻结"; |
| | | if (!Integer.valueOf(20).equals(stockBag.getStockState()) || !Objects.equals(stockBag.getItemId(), batch.getItemId()) |
| | | || !stocks.containsKey(stockBag.getMaterialStockId()) || stockBag.getQuantity() == null || BigDecimal.ONE.compareTo(stockBag.getQuantity()) != 0) |
| | | return "袋码不在有效合格库存中"; |
| | | if (occupied.stream().anyMatch(d -> Objects.equals(d.getBagCodeId(), bag.getId()) |
| | | || d.getBagCodeId() == null && bag.getPalletId() != null && Objects.equals(d.getPalletCodeId(), bag.getPalletId()))) |
| | | return "已加入出库单拣货"; |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public MesWmProductSalesScanRespVO previewScan(String code) { return previewScan(code, null, null); } |
| | | |
| | | /** 只读识别,并返回每袋的可选状态,不创建库存批次或拣货记录。 */ |
| | | @Override |
| | | public MesWmProductSalesScanRespVO previewScan(String code, String scanType, Long salesId) { |
| | | PickingScope scope = pickingScope(code, scanType, false); |
| | | if (salesId != null) { |
| | | MesWmProductSalesDO sales = validateProductSalesExists(salesId); |
| | | if (!Integer.valueOf(2).equals(sales.getStatus())) throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | if (productSalesLineService.getProductSalesLineListBySalesId(salesId).stream() |
| | | .noneMatch(l -> Objects.equals(l.getItemId(), scope.batch().getItemId()))) throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | } |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scope.batch().getCode()); |
| | | Map<Long, MesWmMaterialStockDO> stocks = wmBatch == null ? Map.of() : materialStockService |
| | | .getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()).stream() |
| | | .collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); |
| | | Map<Long, MesWmMaterialStockBagDO> stockBags = pickingStockBagMapper.selectByBagIds(scope.bags().stream().map(MesProBagCodeDO::getId).toList(), false) |
| | | .stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); |
| | | List<MesWmProductSalesDetailDO> occupied = productSalesDetailMapper.selectPickingByBatchCode(scope.batch().getCode()); |
| | | List<MesWmProductSalesScanRespVO.BagOption> options = scope.bags().stream().map(b -> { |
| | | String reason = unavailable(b, stockBags.get(b.getId()), stocks, occupied, scope.batch()); |
| | | return new MesWmProductSalesScanRespVO.BagOption().setId(b.getId()).setCode(b.getCode()).setBusinessCode(b.getBusinessCode()) |
| | | .setPalletId(b.getPalletId()).setSelectable(reason == null).setUnavailableReason(reason); |
| | | }).toList(); |
| | | return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()).setScanContent(code) |
| | | .setBatchCode(scope.batch().getCode()).setItemId(scope.batch().getItemId()).setBagCount(options.size()).setBags(options) |
| | | .setScanQuantity(BigDecimal.valueOf(options.stream().filter(o -> Boolean.TRUE.equals(o.getSelectable())).count())); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesWmProductSalesScanRespVO scanProductSales(MesWmProductSalesScanReqVO reqVO) { |
| | | MesWmProductSalesDO sales = validateProductSalesExists(reqVO.getSalesId()); |
| | | if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVING.getStatus(), sales.getStatus())) { |
| | | throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | MesWmProductSalesDO sales = productSalesMapper.selectByIdForUpdate(reqVO.getSalesId()); |
| | | if (sales == null) throw exception(WM_PRODUCT_SALES_NOT_EXISTS); |
| | | if (!Integer.valueOf(2).equals(sales.getStatus())) throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | PickingScope scope = pickingScope(reqVO.getScanContent(), reqVO.getScanType(), true); |
| | | List<Long> ids = reqVO.getBagIds(); |
| | | if (ids == null && "BAG".equals(scope.type())) ids = List.of(scope.id()); |
| | | if (ids == null || ids.isEmpty() || new HashSet<>(ids).size() != ids.size()) |
| | | throw new IllegalArgumentException("请选择具体袋码,不允许重复袋码"); |
| | | BigDecimal quantity = BigDecimal.valueOf(ids.size()); |
| | | if (reqVO.getQuantity() != null && quantity.compareTo(reqVO.getQuantity()) != 0) |
| | | throw new IllegalArgumentException("拣货数量必须与所选袋码数量一致"); |
| | | Set<Long> selectedIds = new HashSet<>(ids); |
| | | List<MesProBagCodeDO> selected = scope.bags().stream().filter(b -> selectedIds.contains(b.getId())).toList(); |
| | | if (selected.size() != ids.size()) throw new IllegalArgumentException("所选袋码不属于当前扫码对象或已作废,请重新核对"); |
| | | // 锁定实际袋码和库存袋明细,跨出库单的扫码也不能重复拣选同一袋。 |
| | | List<MesWmMaterialStockBagDO> stockBagRows = pickingStockBagMapper.selectByBagIds(ids, true); |
| | | Map<Long, MesWmMaterialStockBagDO> stockBags = stockBagRows.stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); |
| | | List<MesWmProductSalesDetailDO> occupied = productSalesDetailMapper.selectPickingByBatchCodeForUpdate(scope.batch().getCode()); |
| | | List<MesWmProductSalesDetailDO> own = occupied.stream().filter(d -> Objects.equals(d.getSalesId(), sales.getId())).toList(); |
| | | long already = selected.stream().filter(b -> own.stream().anyMatch(d -> Objects.equals(d.getBagCodeId(), b.getId()))).count(); |
| | | if (already == selected.size()) return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()) |
| | | .setScanContent(reqVO.getScanContent()).setSelectedBagIds(ids).setAlreadyScanned(true).setOutboundQuantity(quantity) |
| | | .setDetailId(own.stream().filter(d -> selectedIds.contains(d.getBagCodeId())).findFirst().orElseThrow().getId()).setMessage("所选袋码已在本单拣货明细中"); |
| | | if (already > 0) throw new IllegalArgumentException("所选袋码部分已加入本单,请重新选择未拣袋码"); |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scope.batch().getCode()); |
| | | if (wmBatch == null) throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | Map<Long, MesWmMaterialStockDO> stocks = materialStockService.getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()) |
| | | .stream().collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); |
| | | for (MesProBagCodeDO bag : selected) { |
| | | String reason = unavailable(bag, stockBags.get(bag.getId()), stocks, occupied, scope.batch()); |
| | | if (reason != null) throw new IllegalArgumentException("袋码 " + bag.getCode() + ":" + reason); |
| | | } |
| | | MesProPalletDO pallet = palletService.getPalletByCode(reqVO.getScanContent()); |
| | | MesProBagCodeDO bag = pallet == null ? bagCodeService.getBagCodeByCode(reqVO.getScanContent()) : null; |
| | | if (pallet == null && bag == null) { |
| | | throw exception(WM_MATERIAL_STOCK_NOT_EXISTS); |
| | | } |
| | | // 未绑定生产批次的临时托盘不允许参与销售出库 |
| | | if (pallet != null && pallet.getBatchId() == null) { |
| | | throw exception(PRO_PALLET_NOT_BIND_BATCH); |
| | | } |
| | | // 种子质检按袋码粒度流转后,出库只允许已质检合格的袋码/托盘 |
| | | if (pallet != null) { |
| | | List<MesProBagCodeDO> palletBags = bagCodeService.getBagCodeListByPalletId(pallet.getId()); |
| | | if (palletBags.stream().anyMatch(b -> !MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(b.getQcStatus()))) { |
| | | throw exception(WM_PRODUCT_SALES_SCAN_PALLET_NOT_ALL_QUALIFIED); |
| | | } |
| | | } else if (!MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(bag.getQcStatus())) { |
| | | throw exception(WM_PRODUCT_SALES_SCAN_BAG_NOT_QUALIFIED); |
| | | } |
| | | Long codeId = pallet != null ? pallet.getId() : bag.getId(); |
| | | MesWmProductSalesDetailDO existed = pallet != null |
| | | ? productSalesDetailMapper.selectBySalesIdAndPalletCodeId(reqVO.getSalesId(), codeId) |
| | | : productSalesDetailMapper.selectBySalesIdAndBagCodeId(reqVO.getSalesId(), codeId); |
| | | if (existed != null) { |
| | | return new MesWmProductSalesScanRespVO().setScanType(pallet != null ? "PALLET" : "BAG") |
| | | .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setAlreadyScanned(true) |
| | | .setDetailId(existed.getId()).setOutboundQuantity(existed.getQuantity()).setMessage("该码已扫描"); |
| | | } |
| | | Long proBatchId = pallet != null ? pallet.getBatchId() : bag.getBatchId(); |
| | | MesProBatchDO batch = proBatchService.getBatch(proBatchId); |
| | | if (batch == null) { |
| | | throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | } |
| | | // 生产批次桥接仓库批次:扫码入库写入的是仓库批次(mes_wm_batch),此处必须用仓库批次 id 查库存 |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(batch); |
| | | if (wmBatch == null) { |
| | | throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | } |
| | | List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(reqVO.getSalesId()); |
| | | MesWmProductSalesLineDO line = lines.stream().filter(item -> ObjUtil.equal(item.getItemId(), batch.getItemId())).findFirst().orElse(null); |
| | | if (line == null) { |
| | | throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | } |
| | | BigDecimal quantity = reqVO.getQuantity(); |
| | | if (quantity == null) { |
| | | quantity = line.getQuantity(); |
| | | } |
| | | // 只从合格库记录中选库存(同批次暂存库记录不可出库) |
| | | List<MesWmMaterialStockDO> stocks = materialStockService.getQualifiedMaterialStockListByItemAndBatch(batch.getItemId(), wmBatch.getId()); |
| | | BigDecimal outboundQuantity = quantity; |
| | | MesWmMaterialStockDO stock = stocks.stream().filter(item -> item.getQuantity().compareTo(outboundQuantity) >= 0).findFirst().orElse(null); |
| | | if (stock == null) { |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | | MesWmProductSalesLineDO line = productSalesLineService.getProductSalesLineListBySalesId(sales.getId()).stream() |
| | | .filter(l -> Objects.equals(l.getItemId(), scope.batch().getItemId())).findFirst().orElse(null); |
| | | if (line == null) throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | BigDecimal scanned = productSalesDetailService.getProductSalesDetailListByLineId(line.getId()).stream() |
| | | .map(MesWmProductSalesDetailDO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (scanned.add(quantity).compareTo(line.getQuantity()) > 0) { |
| | | throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); |
| | | if (line.getQuantity() == null || scanned.add(quantity).compareTo(line.getQuantity()) > 0) throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); |
| | | Map<Long, Long> byStock = selected.stream().collect(Collectors.groupingBy(b -> stockBags.get(b.getId()).getMaterialStockId(), Collectors.counting())); |
| | | for (var allocation : byStock.entrySet()) { |
| | | BigDecimal ownStockQty = own.stream().filter(d -> Objects.equals(d.getMaterialStockId(), allocation.getKey())) |
| | | .map(MesWmProductSalesDetailDO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (stocks.get(allocation.getKey()).getQuantity().compareTo(ownStockQty.add(BigDecimal.valueOf(allocation.getValue()))) < 0) |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | | MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setLineId(line.getId()) |
| | | .setSalesId(reqVO.getSalesId()).setItemId(batch.getItemId()).setQuantity(quantity) |
| | | .setMaterialStockId(stock.getId()).setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()) |
| | | Long firstDetail = null; |
| | | for (MesProBagCodeDO bag : selected) { |
| | | MesWmMaterialStockDO stock = stocks.get(stockBags.get(bag.getId()).getMaterialStockId()); |
| | | MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setSalesId(sales.getId()).setLineId(line.getId()) |
| | | .setItemId(scope.batch().getItemId()).setQuantity(BigDecimal.ONE).setBagCodeId(bag.getId()) |
| | | .setPalletCodeId(bag.getPalletId()).setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()).setMaterialStockId(stock.getId()) |
| | | .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()) |
| | | .setBagCodeId(pallet == null ? codeId : null).setPalletCodeId(pallet != null ? codeId : null); |
| | | .setRemark("扫码类型 " + scope.type() + ";来源码 " + reqVO.getScanContent() + ";袋码 " + bag.getCode()); |
| | | productSalesDetailMapper.insert(detail); |
| | | return new MesWmProductSalesScanRespVO().setScanType(pallet != null ? "PALLET" : "BAG") |
| | | .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setItemId(batch.getItemId()) |
| | | .setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()).setMaterialStockId(stock.getId()) |
| | | .setAvailableQuantity(stock.getQuantity()).setScanQuantity(quantity).setOutboundQuantity(quantity) |
| | | .setAlreadyScanned(false).setDetailId(detail.getId()).setWarehouseId(stock.getWarehouseId()) |
| | | .setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()).setMessage("扫码成功"); |
| | | if (firstDetail == null) firstDetail = detail.getId(); |
| | | } |
| | | return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()).setScanContent(reqVO.getScanContent()) |
| | | .setItemId(scope.batch().getItemId()).setBatchCode(wmBatch.getCode()).setSelectedBagIds(ids) |
| | | .setScanQuantity(quantity).setOutboundQuantity(quantity).setAlreadyScanned(false).setDetailId(firstDetail).setMessage("已按所选袋码加入拣货"); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (detail.getBagCodeId() != null) { |
| | | bagCodeService.markOutbound(detail.getBagCodeId(), id); |
| | | } |
| | | if (detail.getPalletCodeId() != null) { |
| | | // 部分托盘拣货可能分给多个经销商,不能把整个托盘标记为本单出库。 |
| | | if (detail.getPalletCodeId() != null && detail.getBagCodeId() == null) { |
| | | palletService.markOutbound(detail.getPalletCodeId(), id); |
| | | } |
| | | } |
| | |
| | | .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId()) |
| | | .setQuantity(detail.getQuantity().negate()) // 出库数量为负数 |
| | | .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) |
| | | .setBagCodeId(detail.getBagCodeId()) // 袋码型明细出库时置袋码已出库;托盘型明细不逐袋处理 |
| | | .setBagCodeId(detail.getBagCodeId()) // 新扫码明细逐袋关联,执行出库逐袋更新库存状态 |
| | | .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId()) |
| | | .setBizType(MesBizTypeConstants.WM_PRODUCT_SALES).setBizId(sales.getId()) |
| | | .setBizCode(sales.getCode()).setBizLineId(detail.getLineId()) |