9 小时以前 1b3fd46c7ccbf636208bcf098c5588441bb71f78
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -9,23 +9,40 @@
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.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.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.mes.service.md.client.MesMdClientService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
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.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 cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
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;
@@ -40,27 +57,43 @@
/**
 * MES 销售出库单 Service 实现类
 *
 * @author 芋道源码
 * @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 wmBatchService;
    @Resource
    private MesWmProductSalesLineService productSalesLineService;
    @Resource
    private MesWmProductSalesDetailService productSalesDetailService;
    @Resource
    private MesMdClientService clientService;
    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 ErpSaleOrderApi saleOrderApi;
@@ -109,7 +142,8 @@
    @Override
    public PageResult<MesWmProductSalesDO> getProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) {
        return productSalesMapper.selectPage(pageReqVO);
        List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
        return productSalesMapper.selectPage(pageReqVO, permittedClientIds);
    }
    @Override
@@ -129,10 +163,26 @@
            }
        }
        // 获取出库单信息
        MesWmProductSalesDO sales = productSalesMapper.selectById(id);
        // 检查所有行的 oqcCheckFlag:如果有需要 OQC 检验的行,进入待检测状态
        boolean needOqc = CollectionUtils.anyMatch(lines,
                line -> Boolean.TRUE.equals(line.getOqcCheckFlag()));
        if (needOqc) {
            // 为需要 OQC 检验且尚未创建检验单的明细创建 OQC 检验单
            List<MesWmProductSalesLineDO> 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<Long> oqcLineIds = convertList(
                    CollectionUtils.filterList(lines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag())),
@@ -168,6 +218,66 @@
    @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);
        }
        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);
        }
        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 batchId = pallet != null ? pallet.getBatchId() : bag.getBatchId();
        MesProBatchDO batch = wmBatchService.getBatch(batchId);
        if (batch == 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.getMaterialStockListByItemAndBatch(batch.getItemId(), batchId);
        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);
        }
        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);
        }
        MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setLineId(line.getId())
                .setSalesId(reqVO.getSalesId()).setItemId(batch.getItemId()).setQuantity(quantity)
                .setMaterialStockId(stock.getId()).setBatchId(batchId).setBatchCode(batch.getCode())
                .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId())
                .setBagCodeId(pallet == null ? codeId : null).setPalletCodeId(pallet != null ? codeId : null);
        productSalesDetailMapper.insert(detail);
        return new MesWmProductSalesScanRespVO().setScanType(pallet != null ? "PALLET" : "BAG")
                .setCodeId(codeId).setScanContent(reqVO.getScanContent()).setItemId(batch.getItemId())
                .setBatchId(batchId).setBatchCode(batch.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("扫码成功");
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void stockProductSales(Long id) {
        // 校验存在
        MesWmProductSalesDO sales = validateProductSalesExists(id);
@@ -181,7 +291,7 @@
            if (CollUtil.isEmpty(details)) {
                throw exception(WM_PRODUCT_SALES_DETAILS_EMPTY);
            }
            // 校验库存充足
            // 校验库存充足并创建占用
            for (MesWmProductSalesDetailDO detail : details) {
                materialStockService.validateSelectedStock(
                        detail.getMaterialStockId(),
@@ -193,12 +303,28 @@
                        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()));
        // 回写 ERP 销售订单出库状态为"部分出库"(表示已拣货/占用)
        if (sales.getSaleOrderId() != null) {
            saleOrderApi.updateOrderOutStatusTo(sales.getSaleOrderId(),
                    ErpSaleOrderOutStatusEnum.PART_OUT.getStatus());
        }
    }
    @Override
@@ -227,20 +353,40 @@
            throw exception(WM_PRODUCT_SALES_CANNOT_FINISH);
        }
        // 2. 遍历所有明细,创建库存事务(扣减库存 + 记录流水)
        // 2. 确认占用转出库(释放占用量)
        stockReserveService.confirmReserveToOutbound(MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id);
        // 3. 遍历所有明细,创建库存事务(扣减库存 + 记录流水)
        createTransactionList(sales);
        // 3. 更新出库单状态
        // 4. 更新出库单状态
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.FINISHED.getStatus()));
        // 4. 更新关联的发货通知单状态
        // 5. 更新关联的发货通知单出库数量和状态
        if (sales.getNoticeId() != null) {
            salesNoticeService.finishSalesNotice(sales.getNoticeId());
            updateSalesNoticeOutCount(sales);
        }
        // 5. 回写 ERP 销售订单的出库数量和状态
        // 6. 回写 ERP 销售订单的出库数量和状态
        updateErpSaleOrderOutCount(sales);
    }
    /**
     * 更新发货通知单的出库数量和状态
     */
    private void updateSalesNoticeOutCount(MesWmProductSalesDO sales) {
        // 获取出库单行
        List<MesWmProductSalesLineDO> 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());
            }
        }
    }
    /**
@@ -282,7 +428,8 @@
                .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                .setBizType(MesBizTypeConstants.WM_PRODUCT_SALES).setBizId(sales.getId())
                .setBizCode(sales.getCode()).setBizLineId(detail.getLineId())));
                .setBizCode(sales.getCode()).setBizLineId(detail.getLineId())
                .setMaterialStockId(detail.getMaterialStockId()))); // 指定库存记录ID,避免重新匹配
    }
    @Override
@@ -296,6 +443,8 @@
                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()));
@@ -322,7 +471,7 @@
        // 校验编码唯一
        validateCodeUnique(id, reqVO.getCode());
        // 校验客户存在
        clientService.validateClientExistsAndEnable(reqVO.getClientId());
        customerApi.validateCustomer(reqVO.getClientId());
        // 校验发货通知单
        if (reqVO.getNoticeId() != null) {
            MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(reqVO.getNoticeId());