package cn.iocoder.yudao.module.mes.service.wm.productsales;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
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.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 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.*;
|
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;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 销售出库单 Service 实现类
|
*
|
* @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 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 MesTraceIntegrityService integrityService;
|
@Resource private MesProBagCodeMapper pickingBagMapper;
|
@Resource private MesProPalletMapper pickingPalletMapper;
|
@Resource private MesProBatchMapper pickingBatchMapper;
|
@Resource private MesWmMaterialStockBagMapper pickingStockBagMapper;
|
|
@Override
|
public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
|
// 校验数据
|
validateProductSalesSaveData(null, createReqVO);
|
|
// 插入
|
MesWmProductSalesDO sales = BeanUtils.toBean(createReqVO, MesWmProductSalesDO.class);
|
sales.setStatus(MesWmProductSalesStatusEnum.PREPARE.getStatus());
|
productSalesMapper.insert(sales);
|
return sales.getId();
|
}
|
|
@Override
|
public void updateProductSales(MesWmProductSalesSaveReqVO updateReqVO) {
|
// 校验存在 + 草稿状态
|
validateProductSalesExistsAndDraft(updateReqVO.getId());
|
// 校验数据
|
validateProductSalesSaveData(updateReqVO.getId(), updateReqVO);
|
|
// 更新
|
MesWmProductSalesDO updateObj = BeanUtils.toBean(updateReqVO, MesWmProductSalesDO.class);
|
productSalesMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteProductSales(Long id) {
|
// 校验存在 + 草稿状态
|
validateProductSalesExistsAndDraft(id);
|
|
// 级联删除明细和行
|
productSalesDetailService.deleteProductSalesDetailBySalesId(id);
|
productSalesLineService.deleteProductSalesLineBySalesId(id);
|
// 删除
|
productSalesMapper.deleteById(id);
|
}
|
|
@Override
|
public MesWmProductSalesDO getProductSales(Long id) {
|
return productSalesMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesWmProductSalesDO> getProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) {
|
List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
|
return productSalesMapper.selectPage(pageReqVO, permittedClientIds);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void submitProductSales(Long id) {
|
// 校验存在 + 草稿状态
|
validateProductSalesExistsAndDraft(id);
|
// 校验至少有一条行
|
List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(id);
|
if (CollUtil.isEmpty(lines)) {
|
throw exception(WM_PRODUCT_SALES_LINES_EMPTY);
|
}
|
// 校验每行出库数量 > 0
|
for (MesWmProductSalesLineDO line : lines) {
|
if (line.getQuantity() == null || BigDecimal.ZERO.compareTo(line.getQuantity()) >= 0) {
|
throw exception(WM_PRODUCT_SALES_LINE_QUANTITY_INVALID);
|
}
|
}
|
|
// 获取出库单信息
|
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())),
|
MesWmProductSalesLineDO::getId);
|
productSalesLineService.updateProductSalesLineQualityStatus(oqcLineIds, MesWmQualityStatusEnum.PENDING.getStatus());
|
// 需要检验,进入待检测
|
productSalesMapper.updateById(new MesWmProductSalesDO()
|
.setId(id).setStatus(MesWmProductSalesStatusEnum.CONFIRMED.getStatus()));
|
return;
|
}
|
// 不需要检验,直接进入待拣货
|
productSalesMapper.updateById(new MesWmProductSalesDO()
|
.setId(id).setStatus(MesWmProductSalesStatusEnum.APPROVING.getStatus()));
|
}
|
|
@Override
|
public boolean checkProductSalesQuantity(Long id) {
|
// 校验每行明细数量之和是否等于行出库数量
|
List<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(id);
|
for (MesWmProductSalesLineDO line : lines) {
|
List<MesWmProductSalesDetailDO> details = productSalesDetailService.getProductSalesDetailListByLineId(line.getId());
|
if (CollUtil.isEmpty(details)) {
|
return false;
|
}
|
BigDecimal totalDetailQty = CollectionUtils.getSumValue(details,
|
MesWmProductSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO);
|
if (line.getQuantity() != null && totalDetailQty.compareTo(line.getQuantity()) != 0) {
|
return false;
|
}
|
}
|
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
|
@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()));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void shippingProductSales(MesWmProductSalesShippingReqVO reqVO) {
|
// 校验存在
|
MesWmProductSalesDO sales = validateProductSalesExists(reqVO.getId());
|
if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.SHIPPING.getStatus(), sales.getStatus())) {
|
throw exception(WM_PRODUCT_SALES_CANNOT_SHIPPING);
|
}
|
|
// 填写运单(待填写运单 → 待执行出库)
|
productSalesMapper.updateById(new MesWmProductSalesDO()
|
.setId(reqVO.getId())
|
.setCarrier(reqVO.getCarrier())
|
.setShippingNumber(reqVO.getShippingNumber())
|
.setStatus(MesWmProductSalesStatusEnum.APPROVED.getStatus()));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void finishProductSales(Long id) {
|
// 1. 校验存在
|
MesWmProductSalesDO sales = validateProductSalesExists(id);
|
if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVED.getStatus(), sales.getStatus())) {
|
throw exception(WM_PRODUCT_SALES_CANNOT_FINISH);
|
}
|
|
// 2. 确认占用转出库(释放占用量)
|
stockReserveService.confirmReserveToOutbound(MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id);
|
|
// 3. 遍历所有明细,创建库存事务(扣减库存 + 记录流水)
|
createTransactionList(sales);
|
|
// 4. 固化经销商与实际出库操作信息
|
CrmCustomerRespDTO customer = customerApi.getCustomer(sales.getClientId());
|
productSalesMapper.updateById(new MesWmProductSalesDO()
|
.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. 回写袋码/托盘出库去向(供窜货稽查定位经销商地区),并生成出库完整性摘要
|
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);
|
}
|
|
/**
|
* 更新发货通知单的出库数量和状态
|
*/
|
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());
|
}
|
}
|
}
|
|
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())
|
.setMaterialStockId(detail.getMaterialStockId()))); // 指定库存记录ID,避免重新匹配
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void cancelProductSales(Long id) {
|
// 校验存在
|
MesWmProductSalesDO sales = validateProductSalesExists(id);
|
// 已完成和已取消不允许取消
|
if (ObjectUtils.equalsAny(sales.getStatus(),
|
MesWmProductSalesStatusEnum.FINISHED.getStatus(),
|
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()));
|
}
|
|
@Override
|
public void confirmProductSales(Long id) {
|
// 校验存在 + 待检测状态
|
MesWmProductSalesDO sales = validateProductSalesExists(id);
|
if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.CONFIRMED.getStatus(), sales.getStatus())) {
|
throw exception(WM_PRODUCT_SALES_CANNOT_CONFIRM);
|
}
|
// OQC 检验完成,流转到待拣货
|
productSalesMapper.updateById(new MesWmProductSalesDO()
|
.setId(id).setStatus(MesWmProductSalesStatusEnum.APPROVING.getStatus()));
|
}
|
|
@Override
|
public List<MesWmProductSalesDO> getProductSalesListByClientId(Long clientId) {
|
return productSalesMapper.selectListByClientId(clientId);
|
}
|
|
private void validateProductSalesSaveData(Long id, MesWmProductSalesSaveReqVO reqVO) {
|
// 校验编码唯一
|
validateCodeUnique(id, reqVO.getCode());
|
// 校验客户存在
|
customerApi.validateCustomer(reqVO.getClientId());
|
// 校验发货通知单
|
if (reqVO.getNoticeId() != null) {
|
MesWmSalesNoticeDO notice = salesNoticeService.validateSalesNoticeExists(reqVO.getNoticeId());
|
if (ObjUtil.notEqual(notice.getStatus(), MesWmSalesNoticeStatusEnum.APPROVED.getStatus())) {
|
throw exception(WM_SALES_NOTICE_STATUS_NOT_APPROVED);
|
}
|
if (ObjUtil.notEqual(notice.getClientId(), reqVO.getClientId())) {
|
throw exception(WM_SALES_NOTICE_CLIENT_MISMATCH);
|
}
|
}
|
}
|
|
private MesWmProductSalesDO validateProductSalesExists(Long id) {
|
MesWmProductSalesDO sales = productSalesMapper.selectById(id);
|
if (sales == null) {
|
throw exception(WM_PRODUCT_SALES_NOT_EXISTS);
|
}
|
return sales;
|
}
|
|
/**
|
* 校验销售出库单存在且为草稿状态
|
*/
|
@Override
|
public MesWmProductSalesDO validateProductSalesExistsAndDraft(Long id) {
|
MesWmProductSalesDO sales = validateProductSalesExists(id);
|
if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.PREPARE.getStatus(), sales.getStatus())) {
|
throw exception(WM_PRODUCT_SALES_NOT_PREPARE);
|
}
|
return sales;
|
}
|
|
private void validateCodeUnique(Long id, String code) {
|
MesWmProductSalesDO sales = productSalesMapper.selectByCode(code);
|
if (sales == null) {
|
return;
|
}
|
if (ObjUtil.notEqual(id, sales.getId())) {
|
throw exception(WM_PRODUCT_SALES_CODE_DUPLICATE);
|
}
|
}
|
|
@Override
|
public void updateStatus(Long id, Integer status) {
|
productSalesMapper.updateById(new MesWmProductSalesDO().setId(id).setStatus(status));
|
}
|
|
}
|