package cn.iocoder.yudao.module.mes.service.wm.productsales; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; 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.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesScanReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesScanRespVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesDetailMapper; import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService; import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmSalesNoticeStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveBizTypeEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO; import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService; import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService; import cn.iocoder.yudao.module.mes.service.trace.integrity.MesTraceIntegrityService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.service.wm.stockreserve.MesWmStockReserveService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; 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; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 销售出库单 Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class MesWmProductSalesServiceImpl implements MesWmProductSalesService { @Resource private MesWmProductSalesMapper productSalesMapper; @Resource private MesWmProductSalesLineMapper productSalesLineMapper; @Resource private MesWmProductSalesDetailMapper productSalesDetailMapper; @Resource private MesProBagCodeService bagCodeService; @Resource private MesProPalletService palletService; @Resource private MesProBatchService proBatchService; @Resource private MesWmBatchService mesWmBatchService; @Resource private MesWmProductSalesLineService productSalesLineService; @Resource private MesWmProductSalesDetailService productSalesDetailService; @Resource private CrmCustomerApi customerApi; @Resource private MesWmSalesNoticeService salesNoticeService; @Resource private MesWmTransactionService wmTransactionService; @Resource private MesWmMaterialStockService materialStockService; @Resource private MesWmStockReserveService stockReserveService; @Resource @Lazy private MesQcOqcService oqcService; @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) { // 校验数据 validateProductSalesSaveData(null, createReqVO); // 插入 MesWmProductSalesDO sales = BeanUtils.toBean(createReqVO, MesWmProductSalesDO.class); sales.setStatus(MesWmProductSalesStatusEnum.PREPARE.getStatus()); productSalesMapper.insert(sales); return sales.getId(); } @Override public void updateProductSales(MesWmProductSalesSaveReqVO updateReqVO) { // 校验存在 + 草稿状态 validateProductSalesExistsAndDraft(updateReqVO.getId()); // 校验数据 validateProductSalesSaveData(updateReqVO.getId(), updateReqVO); // 更新 MesWmProductSalesDO updateObj = BeanUtils.toBean(updateReqVO, MesWmProductSalesDO.class); productSalesMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteProductSales(Long id) { // 校验存在 + 草稿状态 validateProductSalesExistsAndDraft(id); // 级联删除明细和行 productSalesDetailService.deleteProductSalesDetailBySalesId(id); productSalesLineService.deleteProductSalesLineBySalesId(id); // 删除 productSalesMapper.deleteById(id); } @Override public MesWmProductSalesDO getProductSales(Long id) { return productSalesMapper.selectById(id); } @Override public PageResult getProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) { List permittedClientIds = customerApi.getPermittedCustomerIds(); return productSalesMapper.selectPage(pageReqVO, permittedClientIds); } @Override @Transactional(rollbackFor = Exception.class) public void submitProductSales(Long id) { // 校验存在 + 草稿状态 validateProductSalesExistsAndDraft(id); // 校验至少有一条行 List lines = productSalesLineService.getProductSalesLineListBySalesId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_PRODUCT_SALES_LINES_EMPTY); } // 校验每行出库数量 > 0 for (MesWmProductSalesLineDO line : lines) { if (line.getQuantity() == null || BigDecimal.ZERO.compareTo(line.getQuantity()) >= 0) { throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); } } // 获取出库单信息 MesWmProductSalesDO sales = productSalesMapper.selectById(id); // 检查所有行的 oqcCheckFlag:如果有需要 OQC 检验的行,进入待检测状态 boolean needOqc = CollectionUtils.anyMatch(lines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag())); if (needOqc) { // 为需要 OQC 检验且尚未创建检验单的明细创建 OQC 检验单 List oqcLines = CollectionUtils.filterList(lines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) && line.getOqcId() == null); for (MesWmProductSalesLineDO line : oqcLines) { // 直接调用,让异常抛出,保证数据一致性 Long oqcId = oqcService.createOqcFromProductSales( sales.getId(), line, sales.getCode(), sales.getClientId()); // 更新出库明细的 oqcId productSalesLineMapper.updateById(new MesWmProductSalesLineDO() .setId(line.getId()) .setOqcId(oqcId)); } // 批量初始化 OQC 行的质量状态为"待检验" List oqcLineIds = convertList( CollectionUtils.filterList(lines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag())), MesWmProductSalesLineDO::getId); productSalesLineService.updateProductSalesLineQualityStatus(oqcLineIds, MesWmQualityStatusEnum.PENDING.getStatus()); // 需要检验,进入待检测 productSalesMapper.updateById(new MesWmProductSalesDO() .setId(id).setStatus(MesWmProductSalesStatusEnum.CONFIRMED.getStatus())); return; } // 不需要检验,直接进入待拣货 productSalesMapper.updateById(new MesWmProductSalesDO() .setId(id).setStatus(MesWmProductSalesStatusEnum.APPROVING.getStatus())); } @Override public boolean checkProductSalesQuantity(Long id) { // 校验每行明细数量之和是否等于行出库数量 List lines = productSalesLineService.getProductSalesLineListBySalesId(id); for (MesWmProductSalesLineDO line : lines) { List details = productSalesDetailService.getProductSalesDetailListByLineId(line.getId()); if (CollUtil.isEmpty(details)) { return false; } BigDecimal totalDetailQty = CollectionUtils.getSumValue(details, MesWmProductSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); if (line.getQuantity() != null && totalDetailQty.compareTo(line.getQuantity()) != 0) { return false; } } return true; } private record PickingScope(String type, Long id, MesProBatchDO batch, List 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 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 stocks, List 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 stocks = wmBatch == null ? Map.of() : materialStockService .getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()).stream() .collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); Map stockBags = pickingStockBagMapper.selectByBagIds(scope.bags().stream().map(MesProBagCodeDO::getId).toList(), false) .stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); List occupied = productSalesDetailMapper.selectPickingByBatchCode(scope.batch().getCode()); List 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 = 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 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 selectedIds = new HashSet<>(ids); List selected = scope.bags().stream().filter(b -> selectedIds.contains(b.getId())).toList(); if (selected.size() != ids.size()) throw new IllegalArgumentException("所选袋码不属于当前扫码对象或已作废,请重新核对"); // 锁定实际袋码和库存袋明细,跨出库单的扫码也不能重复拣选同一袋。 List stockBagRows = pickingStockBagMapper.selectByBagIds(ids, true); Map stockBags = stockBagRows.stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); List occupied = productSalesDetailMapper.selectPickingByBatchCodeForUpdate(scope.batch().getCode()); List 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 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); } 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 (line.getQuantity() == null || scanned.add(quantity).compareTo(line.getQuantity()) > 0) throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); Map 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); } 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()) .setRemark("扫码类型 " + scope.type() + ";来源码 " + reqVO.getScanContent() + ";袋码 " + bag.getCode()); productSalesDetailMapper.insert(detail); 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 @Transactional(rollbackFor = Exception.class) public void stockProductSales(Long id) { // 校验存在 MesWmProductSalesDO sales = validateProductSalesExists(id); if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVING.getStatus(), sales.getStatus())) { throw exception(WM_PRODUCT_SALES_CANNOT_PICK); } // 校验每行至少有拣货明细,并校验库存充足 List lines = productSalesLineService.getProductSalesLineListBySalesId(id); for (MesWmProductSalesLineDO line : lines) { List details = productSalesDetailService.getProductSalesDetailListByLineId(line.getId()); if (CollUtil.isEmpty(details)) { throw exception(WM_PRODUCT_SALES_DETAILS_EMPTY); } // 校验库存充足并创建占用 for (MesWmProductSalesDetailDO detail : details) { materialStockService.validateSelectedStock( detail.getMaterialStockId(), detail.getItemId(), detail.getBatchId(), detail.getBatchCode(), detail.getWarehouseId(), detail.getLocationId(), detail.getAreaId(), detail.getQuantity() ); // 创建库存占用 stockReserveService.createReserve( detail.getMaterialStockId(), MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id, sales.getCode(), detail.getId(), detail.getItemId(), detail.getQuantity() ); } } // 执行拣货(待拣货 → 待填写运单) productSalesMapper.updateById(new MesWmProductSalesDO() .setId(id).setStatus(MesWmProductSalesStatusEnum.SHIPPING.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void shippingProductSales(MesWmProductSalesShippingReqVO reqVO) { // 校验存在 MesWmProductSalesDO sales = validateProductSalesExists(reqVO.getId()); if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.SHIPPING.getStatus(), sales.getStatus())) { throw exception(WM_PRODUCT_SALES_CANNOT_SHIPPING); } // 填写运单(待填写运单 → 待执行出库) productSalesMapper.updateById(new MesWmProductSalesDO() .setId(reqVO.getId()) .setCarrier(reqVO.getCarrier()) .setShippingNumber(reqVO.getShippingNumber()) .setStatus(MesWmProductSalesStatusEnum.APPROVED.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void finishProductSales(Long id) { // 1. 校验存在 MesWmProductSalesDO sales = validateProductSalesExists(id); if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVED.getStatus(), sales.getStatus())) { throw exception(WM_PRODUCT_SALES_CANNOT_FINISH); } // 2. 确认占用转出库(释放占用量) stockReserveService.confirmReserveToOutbound(MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id); // 3. 遍历所有明细,创建库存事务(扣减库存 + 记录流水) createTransactionList(sales); // 4. 固化经销商与实际出库操作信息 CrmCustomerRespDTO customer = customerApi.getCustomer(sales.getClientId()); productSalesMapper.updateById(new MesWmProductSalesDO() .setId(id) .setClientName(customer != null ? customer.getName() : null) .setOutboundTime(java.time.LocalDateTime.now()) .setOutboundUserId(SecurityFrameworkUtils.getLoginUserId()) .setStatus(MesWmProductSalesStatusEnum.FINISHED.getStatus())); // 5. 更新关联的发货通知单出库数量和状态 if (sales.getNoticeId() != null) { updateSalesNoticeOutCount(sales); } // 6. 回写袋码/托盘出库去向(供窜货稽查定位经销商地区),并生成出库完整性摘要 List outDetails = productSalesDetailService.getProductSalesDetailListBySalesId(id); for (MesWmProductSalesDetailDO detail : outDetails) { if (detail.getBagCodeId() != null) { bagCodeService.markOutbound(detail.getBagCodeId(), id); } // 部分托盘拣货可能分给多个经销商,不能把整个托盘标记为本单出库。 if (detail.getPalletCodeId() != null && detail.getBagCodeId() == null) { palletService.markOutbound(detail.getPalletCodeId(), id); } } integrityService.recordSalesIntegrity(id); } /** * 更新发货通知单的出库数量和状态 */ private void updateSalesNoticeOutCount(MesWmProductSalesDO sales) { // 获取出库单行 List lines = productSalesLineService.getProductSalesLineListBySalesId(sales.getId()); if (CollUtil.isEmpty(lines)) { return; } // 遍历行,按 noticeLineId 更新出库数量 for (MesWmProductSalesLineDO line : lines) { if (line.getNoticeLineId() != null && line.getQuantity() != null) { salesNoticeService.updateSalesNoticeLineOutCount(line.getNoticeLineId(), line.getQuantity()); } } } private void createTransactionList(MesWmProductSalesDO sales) { List details = productSalesDetailService.getProductSalesDetailListBySalesId(sales.getId()); wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId()) .setQuantity(detail.getQuantity().negate()) // 出库数量为负数 .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) .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()) .setMaterialStockId(detail.getMaterialStockId()))); // 指定库存记录ID,避免重新匹配 } @Override @Transactional(rollbackFor = Exception.class) public void cancelProductSales(Long id) { // 校验存在 MesWmProductSalesDO sales = validateProductSalesExists(id); // 已完成和已取消不允许取消 if (ObjectUtils.equalsAny(sales.getStatus(), MesWmProductSalesStatusEnum.FINISHED.getStatus(), MesWmProductSalesStatusEnum.CANCELED.getStatus())) { throw exception(WM_PRODUCT_SALES_CANNOT_CANCEL); } // 释放库存占用 stockReserveService.releaseReserve(MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id); // 取消 productSalesMapper.updateById(new MesWmProductSalesDO() .setId(id).setStatus(MesWmProductSalesStatusEnum.CANCELED.getStatus())); } @Override public void confirmProductSales(Long id) { // 校验存在 + 待检测状态 MesWmProductSalesDO sales = validateProductSalesExists(id); if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.CONFIRMED.getStatus(), sales.getStatus())) { throw exception(WM_PRODUCT_SALES_CANNOT_CONFIRM); } // OQC 检验完成,流转到待拣货 productSalesMapper.updateById(new MesWmProductSalesDO() .setId(id).setStatus(MesWmProductSalesStatusEnum.APPROVING.getStatus())); } @Override public List getProductSalesListByClientId(Long clientId) { return productSalesMapper.selectListByClientId(clientId); } private void validateProductSalesSaveData(Long id, MesWmProductSalesSaveReqVO reqVO) { // 校验编码唯一 validateCodeUnique(id, reqVO.getCode()); // 校验客户存在 customerApi.validateCustomer(reqVO.getClientId()); // 校验发货通知单 if (reqVO.getNoticeId() != null) { MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(reqVO.getNoticeId()); if (ObjUtil.notEqual(notice.getStatus(), MesWmSalesNoticeStatusEnum.APPROVED.getStatus())) { throw exception(WM_SALES_NOTICE_STATUS_NOT_APPROVED); } if (ObjUtil.notEqual(notice.getClientId(), reqVO.getClientId())) { throw exception(WM_SALES_NOTICE_CLIENT_MISMATCH); } } } private MesWmProductSalesDO validateProductSalesExists(Long id) { MesWmProductSalesDO sales = productSalesMapper.selectById(id); if (sales == null) { throw exception(WM_PRODUCT_SALES_NOT_EXISTS); } return sales; } /** * 校验销售出库单存在且为草稿状态 */ @Override public MesWmProductSalesDO validateProductSalesExistsAndDraft(Long id) { MesWmProductSalesDO sales = validateProductSalesExists(id); if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.PREPARE.getStatus(), sales.getStatus())) { throw exception(WM_PRODUCT_SALES_NOT_PREPARE); } return sales; } private void validateCodeUnique(Long id, String code) { MesWmProductSalesDO sales = productSalesMapper.selectByCode(code); if (sales == null) { return; } if (ObjUtil.notEqual(id, sales.getId())) { throw exception(WM_PRODUCT_SALES_CODE_DUPLICATE); } } @Override public void updateStatus(Long id, Integer status) { productSalesMapper.updateById(new MesWmProductSalesDO().setId(id).setStatus(status)); } }