package cn.iocoder.yudao.module.mes.service.wm.productsales; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLinePageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLineSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper; import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeLineDO; import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeLineService; 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.util.List; import java.util.Objects; 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 MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineService { @Resource private MesWmProductSalesLineMapper productSalesLineMapper; @Resource private MesWmProductSalesDetailService productSalesDetailService; @Resource private MesMdmItemService mdmItemService; @Resource private MesWmBatchService batchService; @Resource @Lazy private MesWmProductSalesService productSalesService; @Resource @Lazy private MesWmSalesNoticeLineService salesNoticeLineService; @Resource @Lazy private MesQcOqcService oqcService; @Override public Long createProductSalesLine(MesWmProductSalesLineSaveReqVO createReqVO) { // 校验数据 validateLineSaveData(createReqVO); // 新增 MesWmProductSalesLineDO line = BeanUtils.toBean(createReqVO, MesWmProductSalesLineDO.class); productSalesLineMapper.insert(line); return line.getId(); } @Override public void updateProductSalesLine(MesWmProductSalesLineSaveReqVO updateReqVO) { // 校验存在 MesWmProductSalesLineDO oldLine = validateProductSalesLineExists(updateReqVO.getId()); // 校验数据 updateReqVO.setSalesId(oldLine.getSalesId()); validateLineSaveData(updateReqVO); // 更新 MesWmProductSalesLineDO updateObj = BeanUtils.toBean(updateReqVO, MesWmProductSalesLineDO.class); productSalesLineMapper.updateById(updateObj); // 检查是否需要创建 OQC 检验单:从不需要检验变为需要检验 if (Boolean.TRUE.equals(updateReqVO.getOqcCheckFlag()) && !Boolean.TRUE.equals(oldLine.getOqcCheckFlag())) { // 获取出库单信息 MesWmProductSalesDO sales = productSalesService.getProductSales(updateReqVO.getSalesId()); if (sales == null) { log.warn("[updateProductSalesLine] 出库单不存在,salesId={}", updateReqVO.getSalesId()); return; } // 构建出库明细 DO MesWmProductSalesLineDO line = new MesWmProductSalesLineDO(); line.setId(updateReqVO.getId()); line.setItemId(updateReqVO.getItemId()); line.setBatchCode(updateReqVO.getBatchCode()); line.setQuantity(updateReqVO.getQuantity()); // 创建 OQC 检验单,让异常抛出 Long oqcId = oqcService.createOqcFromProductSales( sales.getId(), line, sales.getCode(), sales.getClientId()); // 更新出库明细的 oqcId 和质量状态 productSalesLineMapper.updateById(new MesWmProductSalesLineDO() .setId(updateReqVO.getId()) .setOqcId(oqcId) .setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteProductSalesLine(Long id) { // 校验存在 MesWmProductSalesLineDO line = validateProductSalesLineExists(id); // 校验主单为草稿状态才允许删除行 productSalesService.validateProductSalesExistsAndDraft(line.getSalesId()); // 级联删除明细 productSalesDetailService.deleteProductSalesDetailByLineId(id); // 删除 productSalesLineMapper.deleteById(id); } @Override public MesWmProductSalesLineDO getProductSalesLine(Long id) { return productSalesLineMapper.selectById(id); } @Override public List getProductSalesLineListBySalesId(Long salesId) { return productSalesLineMapper.selectListBySalesId(salesId); } @Override public void deleteProductSalesLineBySalesId(Long salesId) { productSalesLineMapper.deleteBySalesId(salesId); } @Override public cn.iocoder.yudao.framework.common.pojo.PageResult getProductSalesLinePage( MesWmProductSalesLinePageReqVO pageReqVO) { // 如果传了 clientId,先查出该客户的所有出库单 ID,设置到 salesIds if (pageReqVO.getSalesId() == null && pageReqVO.getClientId() != null) { List salesList = productSalesService.getProductSalesListByClientId(pageReqVO.getClientId()); if (CollUtil.isEmpty(salesList)) { return cn.iocoder.yudao.framework.common.pojo.PageResult.empty(); } pageReqVO.setSalesIds(convertList(salesList, MesWmProductSalesDO::getId)); } // 查询分页 return productSalesLineMapper.selectPage(pageReqVO); } @Override public MesWmProductSalesLineDO validateProductSalesLineExists(Long id) { MesWmProductSalesLineDO line = productSalesLineMapper.selectById(id); if (line == null) { throw exception(WM_PRODUCT_SALES_LINE_NOT_EXISTS); } return line; } /** * 根据 batchCode 解析 batchId */ private void fillBatchId(MesWmProductSalesLineSaveReqVO reqVO) { if (StrUtil.isBlank(reqVO.getBatchCode())) { reqVO.setBatchId(null); return; } MesWmBatchDO batch = batchService.getBatchByCode(reqVO.getBatchCode()); reqVO.setBatchId(batch != null ? batch.getId() : null); } /** * 校验保存时的关联数据 */ private void validateLineSaveData(MesWmProductSalesLineSaveReqVO reqVO) { // 校验主单存在且为草稿状态 MesWmProductSalesDO sales = productSalesService.validateProductSalesExistsAndDraft(reqVO.getSalesId()); // 校验物料存在 mdmItemService.validateItemExistsAndEnable(reqVO.getItemId()); // 校验关联发货通知单行(存在性 + 归属 + 字段一致性) validateSalesNoticeLine(sales, reqVO); // 根据 batchCode 解析 batchId fillBatchId(reqVO); } /** * 校验发货通知单行 * * @param sales 出库单 * @param reqVO 出库行请求 */ private void validateSalesNoticeLine(MesWmProductSalesDO sales, MesWmProductSalesLineSaveReqVO reqVO) { Long noticeLineId = reqVO.getNoticeLineId(); // 情况一:如果出库单关联了发货通知单,则必须关联发货通知单行 if (sales.getNoticeId() != null) { if (noticeLineId == null) { throw exception(WM_PRODUCT_SALES_LINE_SALES_NOTICE_LINE_REQUIRED); } MesWmSalesNoticeLineDO noticeLine = salesNoticeLineService.validateSalesNoticeLineExists( noticeLineId, sales.getNoticeId()); // 校验关键字段一致性:物料、数量、批次号 // 注:OQC 检验标识允许在出库时独立设置,不强制与通知单一致 if (ObjUtil.notEqual(reqVO.getItemId(), noticeLine.getItemId())) { throw exception(WM_PRODUCT_SALES_LINE_NOTICE_LINE_ITEM_MISMATCH); } if (noticeLine.getQuantity() != null && reqVO.getQuantity() != null && reqVO.getQuantity().compareTo(noticeLine.getQuantity()) != 0) { throw exception(WM_PRODUCT_SALES_LINE_NOTICE_LINE_QUANTITY_MISMATCH); } if (StrUtil.isNotBlank(noticeLine.getBatchCode()) && ObjUtil.notEqual(reqVO.getBatchCode(), noticeLine.getBatchCode())) { throw exception(WM_PRODUCT_SALES_LINE_NOTICE_LINE_BATCH_MISMATCH); } return; } // 情况二:如果出库单没有关联发货通知单,则不允许关联发货通知单行 if (noticeLineId != null) { throw exception(WM_PRODUCT_SALES_LINE_SALES_NOTICE_LINE_NOT_ALLOWED); } } @Override @Transactional(rollbackFor = Exception.class) public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) { // 1. 更新行的 oqcId + qualityStatus Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.QUALIFIED.getType()) ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus(); productSalesLineMapper.updateById(new MesWmProductSalesLineDO() .setId(id).setOqcId(oqcId).setQualityStatus(qualityStatus)); // 2. 检查同一出库单下所有需要 OQC 检验的行 MesWmProductSalesLineDO currentLine = productSalesLineMapper.selectById(id); if (currentLine == null || currentLine.getSalesId() == null) { return; } Long salesId = currentLine.getSalesId(); List allLines = productSalesLineMapper.selectListBySalesId(salesId); // 2.1 检查是否有不合格行 → 取消出库单 boolean hasNg = CollectionUtils.anyMatch(allLines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) && Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())); if (hasNg) { log.info("[updateProductSalesLineWhenOqcFinish][销售出库单({}) 存在质检不合格行,取消出库单]", salesId); productSalesService.cancelProductSales(salesId); return; } // 2.2 检查是否所有 OQC 行都已完成,自动流转 CONFIRMED → APPROVING boolean hasUnchecked = CollectionUtils.anyMatch(allLines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) && line.getOqcId() == null); if (!hasUnchecked) { log.info("[updateProductSalesLineWhenOqcFinish][销售出库单({}) 所有 OQC 行检验完成,流转到待拣货]", salesId); productSalesService.confirmProductSales(salesId); } } @Override public void updateProductSalesLineQualityStatus(List ids, Integer qualityStatus) { if (CollUtil.isEmpty(ids)) { return; } productSalesLineMapper.updateQualityStatusByIds(ids, qualityStatus); } }