package cn.iocoder.yudao.module.mes.service.wm.transfer; import cn.hutool.core.collection.CollUtil; 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.module.mes.controller.admin.wm.transfer.vo.MesWmTransferPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.transfer.vo.MesWmTransferSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transfer.MesWmTransferDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transfer.MesWmTransferDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transfer.MesWmTransferLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.transfer.MesWmTransferMapper; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransferStatusEnum; import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.Collections; import java.util.List; import java.util.Map; 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 实现类 */ @Service @Validated public class MesWmTransferServiceImpl implements MesWmTransferService { @Resource private MesWmTransferMapper transferMapper; @Resource private MesWmTransferLineService transferLineService; @Resource private MesWmTransferDetailService transferDetailService; @Resource private MesWmMaterialStockService materialStockService; @Resource private MesWmTransactionService wmTransactionService; @Override public Long createTransfer(MesWmTransferSaveReqVO createReqVO) { // 校验编码唯一 validateCodeUnique(null, createReqVO.getCode()); // 插入 MesWmTransferDO transfer = BeanUtils.toBean(createReqVO, MesWmTransferDO.class); transfer.setStatus(MesWmTransferStatusEnum.PREPARE.getStatus()); transferMapper.insert(transfer); return transfer.getId(); } @Override public void updateTransfer(MesWmTransferSaveReqVO updateReqVO) { // 校验存在 + 草稿状态 validateTransferExistsAndDraft(updateReqVO.getId()); // 校验编码唯一 validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); // 更新 MesWmTransferDO updateObj = BeanUtils.toBean(updateReqVO, MesWmTransferDO.class); transferMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteTransfer(Long id) { // 校验存在 + 草稿状态 validateTransferExistsAndDraft(id); // 级联删除明细和行 transferDetailService.deleteTransferDetailByTransferId(id); transferLineService.deleteTransferLineByTransferId(id); // 删除 transferMapper.deleteById(id); } @Override public MesWmTransferDO getTransfer(Long id) { return transferMapper.selectById(id); } @Override public PageResult getTransferPage(MesWmTransferPageReqVO pageReqVO) { return transferMapper.selectPage(pageReqVO); } @Override @Transactional(rollbackFor = Exception.class) public void submitTransfer(Long id) { // 校验存在 + 草稿状态 MesWmTransferDO transfer = validateTransferExistsAndDraft(id); List lines = transferLineService.getTransferLineListByTransferId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_TRANSFER_NO_LINE); } // 配送模式下,提交后如果未确认,则进入待确认状态;否则直接进入待上架状态 if (Boolean.TRUE.equals(transfer.getDeliveryFlag()) && !Boolean.TRUE.equals(transfer.getConfirmFlag())) { transfer.setStatus(MesWmTransferStatusEnum.UNCONFIRMED.getStatus()).setConfirmFlag(false); // DONE @AI:冻结库存属于主单状态流转的一部分,保留在主单 service 统一处理 freezeTransferLineStocks(id, true); } else { transfer.setStatus(MesWmTransferStatusEnum.APPROVING.getStatus()); } transferMapper.updateById(transfer); } @Override @Transactional(rollbackFor = Exception.class) public void confirmTransfer(Long id) { // 校验存在 + 待确认状态 MesWmTransferDO transfer = validateTransferExistsAndConfirm(id); // 更新状态:待确认 -> 待上架 transfer.setStatus(MesWmTransferStatusEnum.APPROVING.getStatus()); transfer.setConfirmFlag(true); transferMapper.updateById(transfer); } @Override @Transactional(rollbackFor = Exception.class) public void stockTransfer(Long id) { // 1.1 校验存在 MesWmTransferDO transfer = validateTransferExistsAndApproving(id); List lines = transferLineService.getTransferLineListByTransferId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_TRANSFER_NO_LINE); } // 1.2 检查每个行的明细数量是否完成上架 for (MesWmTransferLineDO line : lines) { List details = transferDetailService.getTransferDetailListByLineId(line.getId()); BigDecimal totalDetailQty = CollectionUtils.getSumValue(details, MesWmTransferDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); if (line.getQuantity() != null && totalDetailQty.compareTo(line.getQuantity()) != 0) { throw exception(WM_TRANSFER_DETAIL_QUANTITY_MISMATCH); } } // 2. 更新状态:待上架 -> 待执行 transfer.setStatus(MesWmTransferStatusEnum.APPROVED.getStatus()); transferMapper.updateById(transfer); } @Override @Transactional(rollbackFor = Exception.class) public void finishTransfer(Long id) { // 1. 校验存在 + 待执行状态 MesWmTransferDO transfer = validateTransferExistsAndApproved(id); List lines = transferLineService.getTransferLineListByTransferId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_TRANSFER_NO_LINE); } // 2. 创建库存事务(调拨移出 + 调拨移入) createTransferTransactions(transfer, lines); // 3. 更新状态:待执行 -> 已完成 transfer.setStatus(MesWmTransferStatusEnum.FINISHED.getStatus()); transferMapper.updateById(transfer); // 4. 配送模式下,执行完成后解除来源库存冻结 if (Boolean.TRUE.equals(transfer.getDeliveryFlag())) { freezeTransferLineStocks(id, false); } } /** * 创建调拨事务:基于 detail (明细) 的视角,不是纯粹的 line(行)的视角。 * 因为明细才代表了实际调往各个目标仓位的动作,所以每条明細产生一笔出库(MOVE_OUT)和一笔入库(MOVE_IN)。 */ private void createTransferTransactions(MesWmTransferDO transfer, List lines) { // 1. 预加载所有明细,按 lineId 分组,避免 N+1 查询 List allDetails = transferDetailService.getTransferDetailListByTransferId(transfer.getId()); Map> detailMap = CollectionUtils.convertMultiMap( allDetails, MesWmTransferDetailDO::getLineId); // 2. 遍历行和对应的明细,创建事务 for (MesWmTransferLineDO line : lines) { List details = detailMap.getOrDefault(line.getId(), Collections.emptyList()); // 从 materialStockId 获取源库存信息(包括 vendor_id, batch_id 等) Long vendorId = null; String batchCode = null; if (line.getMaterialStockId() != null) { MesWmMaterialStockDO sourceStock = materialStockService.getMaterialStock(line.getMaterialStockId()); if (sourceStock != null) { vendorId = sourceStock.getVendorId(); batchCode = sourceStock.getBatchCode(); } } for (MesWmTransferDetailDO detail : details) { // 2.1 先执行出库:调拨移出(从源仓库扣减库存),出库数量基于当前明细的数量 Long outTransactionId = wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.MOVE_OUT.getType()).setItemId(line.getItemId()) .setQuantity(detail.getQuantity().negate()) // 库存减少(按 detail 的数量出) .setBatchId(line.getBatchId()).setBatchCode(batchCode) .setVendorId(vendorId) .setMaterialStockId(line.getMaterialStockId()) .setWarehouseId(line.getFromWarehouseId()).setLocationId(line.getFromLocationId()).setAreaId(line.getFromAreaId()) .setBizType(MesBizTypeConstants.WM_TRANSFER_OUT).setBizId(transfer.getId()) .setBizCode(transfer.getCode()).setBizLineId(line.getId())); // 2.2 再执行入库:调拨移入(向目标仓库增加库存),入库数量等于相同明细的数量 // 注意:入库时不传 vendorId,以忽略供应商维度,合并到目标库位已有库存 wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.MOVE_IN.getType()).setItemId(line.getItemId()) // 物料信息来自 line .setQuantity(detail.getQuantity()) // 库存增加(按 detail 的数量入) .setBatchId(line.getBatchId()).setBatchCode(batchCode) // 批次信息来自 line .setWarehouseId(detail.getToWarehouseId()).setLocationId(detail.getToLocationId()).setAreaId(detail.getToAreaId()) .setBizType(MesBizTypeConstants.WM_TRANSFER_IN).setBizId(transfer.getId()) .setBizCode(transfer.getCode()).setBizLineId(line.getId()) .setRelatedTransactionId(outTransactionId)); // 关联本次的具体出库事务 } } } @Override @Transactional(rollbackFor = Exception.class) public void cancelTransfer(Long id) { // 校验存在 + 非已完成/已取消状态 MesWmTransferDO transfer = validateTransferExistsAndNotFinished(id); // 配送模式下,取消时需解除冻结 if (Boolean.TRUE.equals(transfer.getDeliveryFlag())) { freezeTransferLineStocks(id, false); } // 更新状态 -> 已取消 transfer.setStatus(MesWmTransferStatusEnum.CANCELED.getStatus()); transferMapper.updateById(transfer); } @Override public MesWmTransferDO validateTransferEditable(Long id) { MesWmTransferDO transfer = transferMapper.selectById(id); if (transfer == null) { throw exception(WM_TRANSFER_NOT_EXISTS); } if (!MesWmTransferStatusEnum.PREPARE.getStatus().equals(transfer.getStatus())) { throw exception(WM_TRANSFER_NOT_EDITABLE); } return transfer; } // ==================== 私有方法 ==================== private void validateCodeUnique(Long id, String code) { MesWmTransferDO transfer = transferMapper.selectByCode(code); if (transfer == null) { return; } // 如果 id 为空,说明是新增,存在同名则报错 if (id == null) { throw exception(WM_TRANSFER_CODE_DUPLICATE); } // 如果 id 不为空,说明是修改,存在同名且不是自己则报错 if (!transfer.getId().equals(id)) { throw exception(WM_TRANSFER_CODE_DUPLICATE); } } private MesWmTransferDO validateTransferExistsAndDraft(Long id) { MesWmTransferDO transfer = transferMapper.selectById(id); if (transfer == null) { throw exception(WM_TRANSFER_NOT_EXISTS); } if (!MesWmTransferStatusEnum.PREPARE.getStatus().equals(transfer.getStatus())) { throw exception(WM_TRANSFER_NOT_DRAFT); } return transfer; } private MesWmTransferDO validateTransferExistsAndConfirm(Long id) { MesWmTransferDO transfer = transferMapper.selectById(id); if (transfer == null) { throw exception(WM_TRANSFER_NOT_EXISTS); } if (!MesWmTransferStatusEnum.UNCONFIRMED.getStatus().equals(transfer.getStatus())) { throw exception(WM_TRANSFER_NOT_CONFIRMED); } return transfer; } private MesWmTransferDO validateTransferExistsAndApproving(Long id) { MesWmTransferDO transfer = transferMapper.selectById(id); if (transfer == null) { throw exception(WM_TRANSFER_NOT_EXISTS); } if (!MesWmTransferStatusEnum.APPROVING.getStatus().equals(transfer.getStatus())) { throw exception(WM_TRANSFER_NOT_APPROVING); } return transfer; } private MesWmTransferDO validateTransferExistsAndApproved(Long id) { MesWmTransferDO transfer = transferMapper.selectById(id); if (transfer == null) { throw exception(WM_TRANSFER_NOT_EXISTS); } if (!MesWmTransferStatusEnum.APPROVED.getStatus().equals(transfer.getStatus())) { throw exception(WM_TRANSFER_NOT_APPROVED); } return transfer; } private MesWmTransferDO validateTransferExistsAndNotFinished(Long id) { MesWmTransferDO transfer = transferMapper.selectById(id); if (transfer == null) { throw exception(WM_TRANSFER_NOT_EXISTS); } if (MesWmTransferStatusEnum.FINISHED.getStatus().equals(transfer.getStatus()) || MesWmTransferStatusEnum.CANCELED.getStatus().equals(transfer.getStatus())) { throw exception(WM_TRANSFER_ALREADY_FINISHED); } return transfer; } private void freezeTransferLineStocks(Long transferId, boolean frozen) { List lines = transferLineService.getTransferLineListByTransferId(transferId); if (CollUtil.isEmpty(lines)) { return; } List materialStockIds = convertList(lines, MesWmTransferLineDO::getMaterialStockId, line -> line.getMaterialStockId() != null); materialStockService.updateMaterialStockFrozen(materialStockIds, frozen); } @Override public void updateStatus(Long id, Integer status) { transferMapper.updateById(new MesWmTransferDO().setId(id).setStatus(status)); } }