3 天以前 1ba0dbfde5634c8bf2ec20891d6b226b996f6b59
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -6,9 +6,20 @@
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;
@@ -16,20 +27,23 @@
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 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;
@@ -58,6 +72,16 @@
    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;
@@ -78,7 +102,7 @@
    private MesQcOqcService oqcService;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    private MesTraceIntegrityService integrityService;
    @Override
    public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
@@ -200,6 +224,85 @@
    @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);
        }
        // 未绑定生产批次的临时托盘不允许参与销售出库
        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);
        }
        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(wmBatch.getId()).setBatchCode(wmBatch.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(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("扫码成功");
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void stockProductSales(Long id) {
        // 校验存在
        MesWmProductSalesDO sales = validateProductSalesExists(id);
@@ -241,12 +344,6 @@
        // 执行拣货(待拣货 → 待填写运单)
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.SHIPPING.getStatus()));
        // 回写 ERP 销售订单出库状态为"部分出库"(表示已拣货/占用)
        if (sales.getSaleOrderId() != null) {
            saleOrderApi.updateOrderOutStatusTo(sales.getSaleOrderId(),
                    ErpSaleOrderOutStatusEnum.PART_OUT.getStatus());
        }
    }
    @Override
@@ -281,17 +378,31 @@
        // 3. 遍历所有明细,创建库存事务(扣减库存 + 记录流水)
        createTransactionList(sales);
        // 4. 更新出库单状态
        // 4. 固化经销商与实际出库操作信息
        CrmCustomerRespDTO customer = customerApi.getCustomer(sales.getClientId());
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.FINISHED.getStatus()));
                .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. 回写 ERP 销售订单的出库数量和状态
        updateErpSaleOrderOutCount(sales);
        // 6. 回写袋码/托盘出库去向(供窜货稽查定位经销商地区),并生成出库完整性摘要
        List<MesWmProductSalesDetailDO> outDetails = productSalesDetailService.getProductSalesDetailListBySalesId(id);
        for (MesWmProductSalesDetailDO detail : outDetails) {
            if (detail.getBagCodeId() != null) {
                bagCodeService.markOutbound(detail.getBagCodeId(), id);
            }
            if (detail.getPalletCodeId() != null) {
                palletService.markOutbound(detail.getPalletCodeId(), id);
            }
        }
        integrityService.recordSalesIntegrity(id);
    }
    /**
@@ -311,43 +422,13 @@
        }
    }
    /**
     * 回写 ERP 销售订单的出库数量和状态
     */
    private void updateErpSaleOrderOutCount(MesWmProductSalesDO sales) {
        // 获取出库单行
        List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(sales.getId());
        if (CollUtil.isEmpty(lines)) {
            return;
        }
        // 用于存储销售订单 ID,避免重复更新
        Long saleOrderId = null;
        // 遍历行,按 saleOrderItemId 更新出库数量
        for (MesWmProductSalesLineDO line : lines) {
            if (line.getSaleOrderItemId() != null && line.getQuantity() != null) {
                saleOrderApi.updateOrderItemOutCount(line.getSaleOrderItemId(), line.getQuantity());
                // 获取销售订单 ID(从第一个有效的明细项获取)
                if (saleOrderId == null) {
                    cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO itemDTO =
                            saleOrderApi.getSaleOrderItem(line.getSaleOrderItemId()).getCheckedData();
                    if (itemDTO != null && itemDTO.getOrderId() != null) {
                        saleOrderId = itemDTO.getOrderId();
                    }
                }
            }
        }
        // 更新销售订单的出库状态
        if (saleOrderId != null) {
            saleOrderApi.updateOrderOutStatus(saleOrderId);
        }
    }
    private void createTransactionList(MesWmProductSalesDO sales) {
        List<MesWmProductSalesDetailDO> 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())