| | |
| | | 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; |
| | |
| | | /** |
| | | * 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; |
| | | |
| | | @Override |
| | | public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) { |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesWmProductSalesDO> getProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) { |
| | | return productSalesMapper.selectPage(pageReqVO); |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return productSalesMapper.selectPage(pageReqVO, permittedClientIds); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | } |
| | | |
| | | // 获取出库单信息 |
| | | 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())), |
| | |
| | | |
| | | @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); |
| | | if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVING.getStatus(), sales.getStatus())) { |
| | | throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | } |
| | | // 校验每行至少有拣货明细 |
| | | // 校验每行至少有拣货明细,并校验库存充足 |
| | | List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(id); |
| | | for (MesWmProductSalesLineDO line : lines) { |
| | | List<MesWmProductSalesDetailDO> 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())); |
| | | |
| | | // 回写 ERP 销售订单出库状态为"部分出库"(表示已拣货/占用) |
| | | if (sales.getSaleOrderId() != null) { |
| | | saleOrderApi.updateOrderOutStatusTo(sales.getSaleOrderId(), |
| | | ErpSaleOrderOutStatusEnum.PART_OUT.getStatus()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | 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())); |
| | | |
| | | // 5. 更新关联的发货通知单出库数量和状态 |
| | | if (sales.getNoticeId() != null) { |
| | | updateSalesNoticeOutCount(sales); |
| | | } |
| | | |
| | | // 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()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 回写 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) { |
| | |
| | | .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 |
| | |
| | | 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())); |
| | |
| | | // 校验编码唯一 |
| | | validateCodeUnique(id, reqVO.getCode()); |
| | | // 校验客户存在 |
| | | clientService.validateClientExistsAndEnable(reqVO.getClientId()); |
| | | customerApi.validateCustomer(reqVO.getClientId()); |
| | | // 校验发货通知单 |
| | | if (reqVO.getNoticeId() != null) { |
| | | MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(reqVO.getNoticeId()); |
| | |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void updateStatus(Long id, Integer status) { |
| | | productSalesMapper.updateById(new MesWmProductSalesDO().setId(id).setStatus(status)); |
| | | } |
| | | |
| | | } |