| | |
| | | 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; |
| | | 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.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.mes.service.md.client.MesMdClientService; |
| | | 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.erp.api.sale.ErpSaleOrderApi; |
| | | 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.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | |
| | | /** |
| | | * 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 proBatchService; |
| | | @Resource |
| | | private MesWmBatchService mesWmBatchService; |
| | | |
| | | @Resource |
| | | private MesWmProductSalesLineService productSalesLineService; |
| | | @Resource |
| | | private MesWmProductSalesDetailService productSalesDetailService; |
| | | @Resource |
| | | private MesMdClientService clientService; |
| | | private CrmCustomerApi customerApi; |
| | | @Resource |
| | | private MesWmSalesNoticeService salesNoticeService; |
| | | @Resource |
| | |
| | | private MesWmMaterialStockService materialStockService; |
| | | @Resource |
| | | private MesWmStockReserveService stockReserveService; |
| | | @Resource |
| | | @Lazy |
| | | private MesQcOqcService oqcService; |
| | | |
| | | @Resource |
| | | private ErpSaleOrderApi saleOrderApi; |
| | | private MesTraceIntegrityService integrityService; |
| | | @Resource private MesProBagCodeMapper pickingBagMapper; |
| | | @Resource private MesProPalletMapper pickingPalletMapper; |
| | | @Resource private MesProBatchMapper pickingBatchMapper; |
| | | @Resource private MesWmMaterialStockBagMapper pickingStockBagMapper; |
| | | |
| | | @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())), |
| | |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private record PickingScope(String type, Long id, MesProBatchDO batch, List<MesProBagCodeDO> bags) { } |
| | | |
| | | private PickingScope pickingScope(String code, String type, boolean lock) { |
| | | if (type == null || type.isBlank()) { |
| | | type = palletService.getPalletByScanCode(code) != null ? "PALLET" |
| | | : bagCodeService.getBagCodeByScanCode(code) != null ? "BAG" : "BATCH"; |
| | | } |
| | | MesProBatchDO batch; |
| | | List<MesProBagCodeDO> bags; |
| | | Long id; |
| | | switch (type) { |
| | | case "BAG" -> { |
| | | MesProBagCodeDO bag = lock ? pickingBagMapper.selectByScanCodeForUpdate(code) : bagCodeService.getBagCodeByScanCode(code); |
| | | if (bag == null || Integer.valueOf(30).equals(bag.getStatus())) throw new IllegalArgumentException("袋码不存在或已作废"); |
| | | id = bag.getId(); batch = proBatchService.getBatch(bag.getBatchId()); bags = List.of(bag); |
| | | } |
| | | case "PALLET" -> { |
| | | MesProPalletDO pallet = lock ? pickingPalletMapper.selectByScanCodeForUpdate(code) : palletService.getPalletByScanCode(code); |
| | | if (pallet == null || Integer.valueOf(20).equals(pallet.getStatus())) throw new IllegalArgumentException("托盘不存在或已作废"); |
| | | if (pallet.getBatchId() == null) throw exception(PRO_PALLET_NOT_BIND_BATCH); |
| | | id = pallet.getId(); batch = proBatchService.getBatch(pallet.getBatchId()); |
| | | bags = lock ? pickingBagMapper.selectListByPalletIdForUpdate(id) : bagCodeService.getBagCodeListByPalletId(id); |
| | | } |
| | | case "BATCH" -> { |
| | | batch = lock ? pickingBatchMapper.selectByScanCodeForUpdate(code) : proBatchService.getBatchByScanCode(code); |
| | | if (batch == null) throw new IllegalArgumentException("生产批次码不存在"); |
| | | id = batch.getId(); bags = lock ? pickingBagMapper.selectListByBatchIdForUpdate(id) : bagCodeService.getBagCodeListByBatchId(id); |
| | | } |
| | | default -> throw new IllegalArgumentException("不支持的拣货扫码类型"); |
| | | } |
| | | if (batch == null || batch.getItemId() == null || Integer.valueOf(40).equals(batch.getStatus())) |
| | | throw new IllegalArgumentException("未绑定有效品种或生产批次已作废"); |
| | | return new PickingScope(type, id, batch, bags.stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())) |
| | | .sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList()); |
| | | } |
| | | |
| | | private String unavailable(MesProBagCodeDO bag, MesWmMaterialStockBagDO stockBag, |
| | | Map<Long, MesWmMaterialStockDO> stocks, List<MesWmProductSalesDetailDO> occupied, MesProBatchDO batch) { |
| | | if (!Objects.equals(bag.getBatchId(), batch.getId())) return "袋码批次归属异常"; |
| | | if (bag.getItemId() != null && !Objects.equals(bag.getItemId(), batch.getItemId())) return "袋码品种归属异常"; |
| | | if (!MesProBagCodeQcStatusEnum.QUALIFIED.getStatus().equals(bag.getQcStatus())) return "尚未质检合格"; |
| | | if (stockBag == null || !Integer.valueOf(1).equals(stockBag.getStatus())) return "不在库或已出库"; |
| | | if (Boolean.TRUE.equals(stockBag.getFrozen())) return "袋码已冻结"; |
| | | if (!Integer.valueOf(20).equals(stockBag.getStockState()) || !Objects.equals(stockBag.getItemId(), batch.getItemId()) |
| | | || !stocks.containsKey(stockBag.getMaterialStockId()) || stockBag.getQuantity() == null || BigDecimal.ONE.compareTo(stockBag.getQuantity()) != 0) |
| | | return "袋码不在有效合格库存中"; |
| | | if (occupied.stream().anyMatch(d -> Objects.equals(d.getBagCodeId(), bag.getId()) |
| | | || d.getBagCodeId() == null && bag.getPalletId() != null && Objects.equals(d.getPalletCodeId(), bag.getPalletId()))) |
| | | return "已加入出库单拣货"; |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public MesWmProductSalesScanRespVO previewScan(String code) { return previewScan(code, null, null); } |
| | | |
| | | /** 只读识别,并返回每袋的可选状态,不创建库存批次或拣货记录。 */ |
| | | @Override |
| | | public MesWmProductSalesScanRespVO previewScan(String code, String scanType, Long salesId) { |
| | | PickingScope scope = pickingScope(code, scanType, false); |
| | | if (salesId != null) { |
| | | MesWmProductSalesDO sales = validateProductSalesExists(salesId); |
| | | if (!Integer.valueOf(2).equals(sales.getStatus())) throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | if (productSalesLineService.getProductSalesLineListBySalesId(salesId).stream() |
| | | .noneMatch(l -> Objects.equals(l.getItemId(), scope.batch().getItemId()))) throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | } |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scope.batch().getCode()); |
| | | Map<Long, MesWmMaterialStockDO> stocks = wmBatch == null ? Map.of() : materialStockService |
| | | .getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()).stream() |
| | | .collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); |
| | | Map<Long, MesWmMaterialStockBagDO> stockBags = pickingStockBagMapper.selectByBagIds(scope.bags().stream().map(MesProBagCodeDO::getId).toList(), false) |
| | | .stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); |
| | | List<MesWmProductSalesDetailDO> occupied = productSalesDetailMapper.selectPickingByBatchCode(scope.batch().getCode()); |
| | | List<MesWmProductSalesScanRespVO.BagOption> options = scope.bags().stream().map(b -> { |
| | | String reason = unavailable(b, stockBags.get(b.getId()), stocks, occupied, scope.batch()); |
| | | return new MesWmProductSalesScanRespVO.BagOption().setId(b.getId()).setCode(b.getCode()).setBusinessCode(b.getBusinessCode()) |
| | | .setPalletId(b.getPalletId()).setSelectable(reason == null).setUnavailableReason(reason); |
| | | }).toList(); |
| | | return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()).setScanContent(code) |
| | | .setBatchCode(scope.batch().getCode()).setItemId(scope.batch().getItemId()).setBagCount(options.size()).setBags(options) |
| | | .setScanQuantity(BigDecimal.valueOf(options.stream().filter(o -> Boolean.TRUE.equals(o.getSelectable())).count())); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesWmProductSalesScanRespVO scanProductSales(MesWmProductSalesScanReqVO reqVO) { |
| | | MesWmProductSalesDO sales = productSalesMapper.selectByIdForUpdate(reqVO.getSalesId()); |
| | | if (sales == null) throw exception(WM_PRODUCT_SALES_NOT_EXISTS); |
| | | if (!Integer.valueOf(2).equals(sales.getStatus())) throw exception(WM_PRODUCT_SALES_CANNOT_PICK); |
| | | PickingScope scope = pickingScope(reqVO.getScanContent(), reqVO.getScanType(), true); |
| | | List<Long> ids = reqVO.getBagIds(); |
| | | if (ids == null && "BAG".equals(scope.type())) ids = List.of(scope.id()); |
| | | if (ids == null || ids.isEmpty() || new HashSet<>(ids).size() != ids.size()) |
| | | throw new IllegalArgumentException("请选择具体袋码,不允许重复袋码"); |
| | | BigDecimal quantity = BigDecimal.valueOf(ids.size()); |
| | | if (reqVO.getQuantity() != null && quantity.compareTo(reqVO.getQuantity()) != 0) |
| | | throw new IllegalArgumentException("拣货数量必须与所选袋码数量一致"); |
| | | Set<Long> selectedIds = new HashSet<>(ids); |
| | | List<MesProBagCodeDO> selected = scope.bags().stream().filter(b -> selectedIds.contains(b.getId())).toList(); |
| | | if (selected.size() != ids.size()) throw new IllegalArgumentException("所选袋码不属于当前扫码对象或已作废,请重新核对"); |
| | | // 锁定实际袋码和库存袋明细,跨出库单的扫码也不能重复拣选同一袋。 |
| | | List<MesWmMaterialStockBagDO> stockBagRows = pickingStockBagMapper.selectByBagIds(ids, true); |
| | | Map<Long, MesWmMaterialStockBagDO> stockBags = stockBagRows.stream().collect(Collectors.toMap(MesWmMaterialStockBagDO::getBagCodeId, x -> x)); |
| | | List<MesWmProductSalesDetailDO> occupied = productSalesDetailMapper.selectPickingByBatchCodeForUpdate(scope.batch().getCode()); |
| | | List<MesWmProductSalesDetailDO> own = occupied.stream().filter(d -> Objects.equals(d.getSalesId(), sales.getId())).toList(); |
| | | long already = selected.stream().filter(b -> own.stream().anyMatch(d -> Objects.equals(d.getBagCodeId(), b.getId()))).count(); |
| | | if (already == selected.size()) return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()) |
| | | .setScanContent(reqVO.getScanContent()).setSelectedBagIds(ids).setAlreadyScanned(true).setOutboundQuantity(quantity) |
| | | .setDetailId(own.stream().filter(d -> selectedIds.contains(d.getBagCodeId())).findFirst().orElseThrow().getId()).setMessage("所选袋码已在本单拣货明细中"); |
| | | if (already > 0) throw new IllegalArgumentException("所选袋码部分已加入本单,请重新选择未拣袋码"); |
| | | MesWmBatchDO wmBatch = mesWmBatchService.getBatchByCode(scope.batch().getCode()); |
| | | if (wmBatch == null) throw exception(WM_TRANSACTION_BATCH_NOT_EXISTS); |
| | | Map<Long, MesWmMaterialStockDO> stocks = materialStockService.getQualifiedMaterialStockListByItemAndBatch(scope.batch().getItemId(), wmBatch.getId()) |
| | | .stream().collect(Collectors.toMap(MesWmMaterialStockDO::getId, x -> x)); |
| | | for (MesProBagCodeDO bag : selected) { |
| | | String reason = unavailable(bag, stockBags.get(bag.getId()), stocks, occupied, scope.batch()); |
| | | if (reason != null) throw new IllegalArgumentException("袋码 " + bag.getCode() + ":" + reason); |
| | | } |
| | | MesWmProductSalesLineDO line = productSalesLineService.getProductSalesLineListBySalesId(sales.getId()).stream() |
| | | .filter(l -> Objects.equals(l.getItemId(), scope.batch().getItemId())).findFirst().orElse(null); |
| | | if (line == null) throw exception(WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH); |
| | | BigDecimal scanned = productSalesDetailService.getProductSalesDetailListByLineId(line.getId()).stream() |
| | | .map(MesWmProductSalesDetailDO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (line.getQuantity() == null || scanned.add(quantity).compareTo(line.getQuantity()) > 0) throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID); |
| | | Map<Long, Long> byStock = selected.stream().collect(Collectors.groupingBy(b -> stockBags.get(b.getId()).getMaterialStockId(), Collectors.counting())); |
| | | for (var allocation : byStock.entrySet()) { |
| | | BigDecimal ownStockQty = own.stream().filter(d -> Objects.equals(d.getMaterialStockId(), allocation.getKey())) |
| | | .map(MesWmProductSalesDetailDO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (stocks.get(allocation.getKey()).getQuantity().compareTo(ownStockQty.add(BigDecimal.valueOf(allocation.getValue()))) < 0) |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | | Long firstDetail = null; |
| | | for (MesProBagCodeDO bag : selected) { |
| | | MesWmMaterialStockDO stock = stocks.get(stockBags.get(bag.getId()).getMaterialStockId()); |
| | | MesWmProductSalesDetailDO detail = new MesWmProductSalesDetailDO().setSalesId(sales.getId()).setLineId(line.getId()) |
| | | .setItemId(scope.batch().getItemId()).setQuantity(BigDecimal.ONE).setBagCodeId(bag.getId()) |
| | | .setPalletCodeId(bag.getPalletId()).setBatchId(wmBatch.getId()).setBatchCode(wmBatch.getCode()).setMaterialStockId(stock.getId()) |
| | | .setWarehouseId(stock.getWarehouseId()).setLocationId(stock.getLocationId()).setAreaId(stock.getAreaId()) |
| | | .setRemark("扫码类型 " + scope.type() + ";来源码 " + reqVO.getScanContent() + ";袋码 " + bag.getCode()); |
| | | productSalesDetailMapper.insert(detail); |
| | | if (firstDetail == null) firstDetail = detail.getId(); |
| | | } |
| | | return new MesWmProductSalesScanRespVO().setScanType(scope.type()).setCodeId(scope.id()).setScanContent(reqVO.getScanContent()) |
| | | .setItemId(scope.batch().getItemId()).setBatchCode(wmBatch.getCode()).setSelectedBagIds(ids) |
| | | .setScanQuantity(quantity).setOutboundQuantity(quantity).setAlreadyScanned(false).setDetailId(firstDetail).setMessage("已按所选袋码加入拣货"); |
| | | } |
| | | |
| | | @Override |
| | |
| | | // 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. 更新关联的发货通知单状态 |
| | | // 5. 更新关联的发货通知单出库数量和状态 |
| | | if (sales.getNoticeId() != null) { |
| | | salesNoticeService.finishSalesNotice(sales.getNoticeId()); |
| | | 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 && detail.getBagCodeId() == null) { |
| | | palletService.markOutbound(detail.getPalletCodeId(), id); |
| | | } |
| | | } |
| | | integrityService.recordSalesIntegrity(id); |
| | | } |
| | | |
| | | /** |
| | | * 回写 ERP 销售订单的出库数量和状态 |
| | | * 更新发货通知单的出库数量和状态 |
| | | */ |
| | | private void updateErpSaleOrderOutCount(MesWmProductSalesDO sales) { |
| | | private void updateSalesNoticeOutCount(MesWmProductSalesDO sales) { |
| | | // 获取出库单行 |
| | | List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(sales.getId()); |
| | | if (CollUtil.isEmpty(lines)) { |
| | | return; |
| | | } |
| | | // 用于存储销售订单 ID,避免重复更新 |
| | | Long saleOrderId = null; |
| | | // 遍历行,按 saleOrderItemId 更新出库数量 |
| | | // 遍历行,按 noticeLineId 更新出库数量 |
| | | 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 (line.getNoticeLineId() != null && line.getQuantity() != null) { |
| | | salesNoticeService.updateSalesNoticeLineOutCount(line.getNoticeLineId(), line.getQuantity()); |
| | | } |
| | | } |
| | | // 更新销售订单的出库状态 |
| | | if (saleOrderId != null) { |
| | | saleOrderApi.updateOrderOutStatus(saleOrderId); |
| | | } |
| | | } |
| | | |
| | |
| | | .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()))); |
| | | .setBizCode(sales.getCode()).setBizLineId(detail.getLineId()) |
| | | .setMaterialStockId(detail.getMaterialStockId()))); // 指定库存记录ID,避免重新匹配 |
| | | } |
| | | |
| | | @Override |
| | |
| | | // 校验编码唯一 |
| | | validateCodeUnique(id, reqVO.getCode()); |
| | | // 校验客户存在 |
| | | clientService.validateClientExistsAndEnable(reqVO.getClientId()); |
| | | customerApi.validateCustomer(reqVO.getClientId()); |
| | | // 校验发货通知单 |
| | | if (reqVO.getNoticeId() != null) { |
| | | MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(reqVO.getNoticeId()); |