package cn.iocoder.yudao.module.mes.service.wm.itemreceipt;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.StrUtil;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
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.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptPageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.detail.MesWmItemReceiptDetailSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptLineDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper;
|
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
|
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
|
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
|
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
|
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeLineService;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
|
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.bpm.api.task.BpmProcessInstanceApi;
|
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
|
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
|
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
|
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
|
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
|
import jakarta.annotation.Resource;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
import org.flowable.engine.repository.ProcessDefinition;
|
import lombok.extern.slf4j.Slf4j;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.util.Arrays;
|
import java.util.Collection;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
|
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.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 采购入库单 Service 实现类
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class MesWmItemReceiptServiceImpl implements MesWmItemReceiptService {
|
|
private static final String ITEM_RECEIPT_APPROVE_CATEGORY_CODE = "item_receipt_approve";
|
|
@Resource
|
private MesWmItemReceiptMapper itemReceiptMapper;
|
|
@Resource
|
private MesWmItemReceiptLineService itemReceiptLineService;
|
@Resource
|
private MesWmItemReceiptDetailService itemReceiptDetailService;
|
@Resource
|
@Lazy
|
private MesWmArrivalNoticeService arrivalNoticeService;
|
@Resource
|
@Lazy
|
private SrmSupplierApi srmSupplierApi;
|
@Resource
|
@Lazy
|
private MesQcIqcService iqcService;
|
@Resource
|
private MesWmTransactionService wmTransactionService;
|
|
@Resource
|
private MesWmArrivalNoticeLineService arrivalNoticeLineService;
|
|
@Resource
|
private BpmProcessInstanceApi processInstanceApi;
|
@Resource
|
private BpmProcessDefinitionService bpmProcessDefinitionService;
|
|
@Resource
|
private ErpPurchaseOrderApi purchaseOrderApi;
|
|
@Resource
|
private MdmItemApi mdmItemApi;
|
|
@Resource
|
private MesWmWarehouseService warehouseService;
|
@Resource
|
private MesWmWarehouseLocationService warehouseLocationService;
|
@Resource
|
private MesWmWarehouseAreaService warehouseAreaService;
|
@Resource
|
private MesWmBatchService batchService;
|
|
@Override
|
public Long createItemReceipt(MesWmItemReceiptSaveReqVO createReqVO) {
|
// 校验数据
|
validateItemReceiptSaveData(createReqVO);
|
|
// 插入
|
MesWmItemReceiptDO receipt = BeanUtils.toBean(createReqVO, MesWmItemReceiptDO.class);
|
receipt.setStatus(MesWmItemReceiptStatusEnum.PREPARE.getStatus());
|
itemReceiptMapper.insert(receipt);
|
return receipt.getId();
|
}
|
|
@Override
|
public void updateItemReceipt(MesWmItemReceiptSaveReqVO updateReqVO) {
|
// 校验存在 + 草稿状态
|
validateItemReceiptExistsAndDraft(updateReqVO.getId());
|
// 校验数据
|
validateItemReceiptSaveData(updateReqVO);
|
|
// 更新
|
MesWmItemReceiptDO updateObj = BeanUtils.toBean(updateReqVO, MesWmItemReceiptDO.class);
|
itemReceiptMapper.updateById(updateObj);
|
}
|
|
private void validateItemReceiptSaveData(MesWmItemReceiptSaveReqVO reqVO) {
|
// 校验编码唯一
|
validateCodeUnique(reqVO.getId(), reqVO.getCode());
|
// 校验供应商存在(采购入库使用 ERP 供应商)
|
srmSupplierApi.getSupplier(reqVO.getVendorId());
|
// 校验到货通知单存在
|
if (reqVO.getNoticeId() != null) {
|
MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(reqVO.getNoticeId());
|
if (ObjUtil.notEqual(notice.getVendorId(), reqVO.getVendorId())) {
|
throw exception(WM_ARRIVAL_NOTICE_VENDOR_MISMATCH);
|
}
|
}
|
// 校验来料检验单存在
|
if (reqVO.getIqcId() != null) {
|
iqcService.validateIqcExists(reqVO.getIqcId());
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteItemReceipt(Long id) {
|
// 校验存在 + 草稿状态
|
validateItemReceiptExistsAndDraft(id);
|
|
// 级联删除明细和行
|
itemReceiptDetailService.deleteItemReceiptDetailByReceiptId(id);
|
itemReceiptLineService.deleteItemReceiptLineByReceiptId(id);
|
// 删除
|
itemReceiptMapper.deleteById(id);
|
}
|
|
@Override
|
public MesWmItemReceiptDO getItemReceipt(Long id) {
|
return itemReceiptMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesWmItemReceiptDO> getItemReceiptPage(MesWmItemReceiptPageReqVO pageReqVO) {
|
return itemReceiptMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void submitItemReceipt(Long id) {
|
// 校验存在 + 草稿状态
|
validateItemReceiptExistsAndDraft(id);
|
// 校验至少有一条行
|
List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(id);
|
if (CollUtil.isEmpty(lines)) {
|
throw exception(WM_ITEM_RECEIPT_NO_LINE);
|
}
|
|
// 校验物料批次管理:如果物料开启批次管理,则明细的批次号不能为空
|
validateBatchManagement(lines);
|
|
// 提交(草稿 → 待上架)
|
itemReceiptMapper.updateById(new MesWmItemReceiptDO()
|
.setId(id).setStatus(MesWmItemReceiptStatusEnum.APPROVING.getStatus()));
|
}
|
|
/**
|
* 校验物料批次管理:如果物料开启批次管理,则行的批次号不能为空
|
*/
|
private void validateBatchManagement(List<MesWmItemReceiptLineDO> lines) {
|
// 获取所有物料ID
|
List<Long> itemIds = CollectionUtils.convertList(lines, MesWmItemReceiptLineDO::getItemId);
|
Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds);
|
|
// 遍历每一行,检查物料是否开启批次管理
|
for (MesWmItemReceiptLineDO line : lines) {
|
MdmItemRespDTO item = itemMap.get(line.getItemId());
|
if (item == null) {
|
continue;
|
}
|
// 如果物料开启批次管理,检查行上的批次号是否为空
|
if (Boolean.TRUE.equals(item.getIsBatchManaged()) && line.getBatchId() == null) {
|
throw exception(WM_ITEM_RECEIPT_BATCH_REQUIRED, item.getCode(), item.getName());
|
}
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void stockItemReceipt(Long id) {
|
// 校验存在
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
if (ObjUtil.notEqual(MesWmItemReceiptStatusEnum.APPROVING.getStatus(), receipt.getStatus())) {
|
throw exception(WM_ITEM_RECEIPT_STATUS_ERROR);
|
}
|
// 校验每行明细数量之和是否等于行入库数量
|
List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(id);
|
if (CollUtil.isEmpty(lines)) {
|
throw exception(WM_ITEM_RECEIPT_NO_LINE);
|
}
|
for (MesWmItemReceiptLineDO line : lines) {
|
List<MesWmItemReceiptDetailDO> details = itemReceiptDetailService.getItemReceiptDetailListByLineId(line.getId());
|
BigDecimal totalDetailQty = CollectionUtils.getSumValue(details,
|
MesWmItemReceiptDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO);
|
if (line.getReceivedQuantity() == null || totalDetailQty.compareTo(line.getReceivedQuantity()) != 0) {
|
throw exception(WM_ITEM_RECEIPT_DETAIL_QUANTITY_MISMATCH);
|
}
|
}
|
|
// 执行上架(待上架 → 待入库)
|
itemReceiptMapper.updateById(new MesWmItemReceiptDO()
|
.setId(id).setStatus(MesWmItemReceiptStatusEnum.APPROVED.getStatus()));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@Deprecated
|
public void finishItemReceipt(Long id) {
|
// 1. 校验存在
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
if (ObjUtil.notEqual(MesWmItemReceiptStatusEnum.APPROVED.getStatus(), receipt.getStatus())) {
|
throw exception(WM_ITEM_RECEIPT_STATUS_ERROR);
|
}
|
|
// 2. 完成入库(库存事务 + 状态 + 到货通知 + 采购订单回写)
|
completeItemReceipt(receipt);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void confirmItemReceipt(Long id, Long warehouseId, Long locationId, Long areaId) {
|
// 1. 校验单据存在
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
// 2. 校验状态:草稿、旧流程的待上架(且不在 BPM 审批中)、待执行入库 均允许确认
|
validateItemReceiptConfirmable(receipt);
|
// 3. 校验行
|
List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(id);
|
if (CollUtil.isEmpty(lines)) {
|
throw exception(WM_ITEM_RECEIPT_NO_LINE);
|
}
|
// 4. 校验入库位置(仓库 / 库区 / 库位)的层级关系
|
validateConfirmLocation(receipt.getCode(), warehouseId, locationId, areaId);
|
// 5. 逐行校验入库数量,并为启用批次管理但还没批次的物料补齐批次
|
Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
|
convertList(lines, MesWmItemReceiptLineDO::getItemId));
|
for (MesWmItemReceiptLineDO line : lines) {
|
validateConfirmLineQuantity(receipt.getCode(), line, itemMap);
|
fillLineBatch(receipt, line, itemMap);
|
}
|
// 6. 补齐上架明细:没有明细的行按“行入库数量 + 所选位置”生成一条;已有明细的保持原样
|
createDetailsForConfirm(receipt, lines, warehouseId, locationId, areaId);
|
|
// 7. 完成入库(库存事务 + 状态 + 到货通知 + 采购订单回写)
|
completeItemReceipt(receipt);
|
}
|
|
/**
|
* 完成入库:创建库存事务、更新单据与关联单据状态
|
*/
|
private void completeItemReceipt(MesWmItemReceiptDO receipt) {
|
// 1. 遍历所有明细,创建库存事务(增加库存 + 记录流水)
|
createTransactionList(receipt);
|
|
// 2. 更新入库单状态
|
itemReceiptMapper.updateById(new MesWmItemReceiptDO()
|
.setId(receipt.getId()).setStatus(MesWmItemReceiptStatusEnum.FINISHED.getStatus()));
|
|
// 3. 更新关联的到货通知单状态
|
if (receipt.getNoticeId() != null) {
|
arrivalNoticeService.finishArrivalNotice(receipt.getNoticeId());
|
}
|
|
// 4. 回写 ERP 采购订单的入库数量
|
updateErpPurchaseOrderInCount(receipt);
|
}
|
|
/**
|
* 校验采购入库单是否允许确认入库
|
*
|
* 草稿(新流程)、旧流程的待上架(无 BPM 审批)、待执行入库(旧流程已上架)都允许;
|
* 审批中的单据必须等审批结束,已完成/已取消的不允许。
|
*/
|
private void validateItemReceiptConfirmable(MesWmItemReceiptDO receipt) {
|
MesWmItemReceiptStatusEnum status = Arrays.stream(MesWmItemReceiptStatusEnum.values())
|
.filter(item -> ObjUtil.equal(item.getStatus(), receipt.getStatus()))
|
.findFirst().orElse(null);
|
String statusName = status != null ? status.getName() : String.valueOf(receipt.getStatus());
|
|
// 1. BPM 审批中的单据不允许直接入库,否则会绕过审批
|
if (ObjUtil.equal(MesWmItemReceiptStatusEnum.APPROVING.getStatus(), receipt.getStatus())
|
&& StrUtil.isNotBlank(receipt.getProcessInstanceId())) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_STATUS_ERROR.getCode(),
|
"采购入库单[" + receipt.getCode() + "]正在审批中(当前状态:" + statusName + "),不能确认入库。"
|
+ "请等待审批通过后再确认入库;若审批已驳回,请按审批意见修改后重新提交。");
|
}
|
// 2. 已完成 / 已取消不允许重复确认
|
if (ObjUtil.equal(MesWmItemReceiptStatusEnum.FINISHED.getStatus(), receipt.getStatus())
|
|| ObjUtil.equal(MesWmItemReceiptStatusEnum.CANCELED.getStatus(), receipt.getStatus())) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_STATUS_ERROR.getCode(),
|
"采购入库单[" + receipt.getCode() + "]当前状态为[" + statusName + "],不能确认入库。"
|
+ "只有草稿、待上架或待执行入库的单据可以确认入库,请刷新列表确认单据状态。");
|
}
|
}
|
|
/**
|
* 校验确认入库时选择的仓库 / 库区 / 库位
|
*/
|
private void validateConfirmLocation(String receiptCode, Long warehouseId, Long locationId, Long areaId) {
|
if (warehouseId == null || locationId == null || areaId == null) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(),
|
"采购入库单[" + receiptCode + "]确认入库前必须选择入库仓库、库区和库位(当前"
|
+ "仓库=" + (warehouseId != null ? warehouseId : "未选择")
|
+ ",库区=" + (locationId != null ? locationId : "未选择")
|
+ ",库位=" + (areaId != null ? areaId : "未选择")
|
+ ")。请点击「确认入库」,在弹窗中依次选择仓库、库区、库位后重新提交。");
|
}
|
MesWmWarehouseDO warehouse = warehouseService.getWarehouse(warehouseId);
|
if (warehouse == null) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(),
|
"所选入库仓库(ID=" + warehouseId + ")不存在或已被删除,无法确认入库。"
|
+ "请刷新页面后重新选择仓库、库区、库位。");
|
}
|
MesWmWarehouseLocationDO location = warehouseLocationService.getWarehouseLocation(locationId);
|
if (location == null) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(),
|
"所选入库库区(ID=" + locationId + ")不存在或已被删除,无法确认入库。"
|
+ "请刷新页面后重新选择仓库、库区、库位。");
|
}
|
MesWmWarehouseAreaDO area = warehouseAreaService.getWarehouseArea(areaId);
|
if (area == null) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(),
|
"所选入库库位(ID=" + areaId + ")不存在或已被删除,无法确认入库。"
|
+ "请刷新页面后重新选择仓库、库区、库位。");
|
}
|
// 仓库 → 库区 → 库位 的层级必须一致
|
if (ObjUtil.notEqual(location.getWarehouseId(), warehouse.getId())) {
|
MesWmWarehouseDO locationWarehouse = warehouseService.getWarehouse(location.getWarehouseId());
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_MISMATCH.getCode(),
|
"入库仓库与库区不匹配:所选仓库[" + warehouse.getName() + "],库区[" + location.getName()
|
+ "]属于仓库[" + (locationWarehouse != null ? locationWarehouse.getName()
|
: "ID=" + location.getWarehouseId()) + "]。请重新选择同一仓库下的库区。");
|
}
|
if (ObjUtil.notEqual(area.getLocationId(), location.getId())) {
|
MesWmWarehouseLocationDO areaLocation = warehouseLocationService.getWarehouseLocation(area.getLocationId());
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_MISMATCH.getCode(),
|
"入库库区与库位不匹配:所选库区[" + location.getName() + "],库位[" + area.getName()
|
+ "]属于库区[" + (areaLocation != null ? areaLocation.getName()
|
: "ID=" + area.getLocationId()) + "]。请重新选择同一库区下的库位。");
|
}
|
}
|
|
/**
|
* 校验单行入库数量
|
*/
|
private void validateConfirmLineQuantity(String receiptCode, MesWmItemReceiptLineDO line,
|
Map<Long, MdmItemRespDTO> itemMap) {
|
if (line.getReceivedQuantity() != null && line.getReceivedQuantity().compareTo(BigDecimal.ZERO) > 0) {
|
return;
|
}
|
MdmItemRespDTO item = itemMap.get(line.getItemId());
|
String itemText = item != null ? item.getCode() + " " + item.getName() : "ID=" + line.getItemId();
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_QUANTITY_INVALID.getCode(),
|
"采购入库单[" + receiptCode + "]的物料行[" + itemText + "]入库数量为["
|
+ (line.getReceivedQuantity() != null ? line.getReceivedQuantity().toPlainString() : "空")
|
+ "],必须大于 0 才能入库。请编辑该物料行,核对并填写实际收货数量后重试。");
|
}
|
|
/**
|
* 为启用批次管理但还没有批次的物料行补齐批次
|
*
|
* 由到货通知单生成的入库行不会预先生成批次,这里在确认入库时统一生成,
|
* 避免库管逐行打开物料行保存一次。
|
*/
|
private void fillLineBatch(MesWmItemReceiptDO receipt, MesWmItemReceiptLineDO line,
|
Map<Long, MdmItemRespDTO> itemMap) {
|
MdmItemRespDTO item = itemMap.get(line.getItemId());
|
if (item == null || !Boolean.TRUE.equals(item.getIsBatchManaged()) || line.getBatchId() != null) {
|
return;
|
}
|
MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO();
|
batchReqVO.setItemId(line.getItemId()).setProduceDate(line.getProductionDate())
|
.setExpireDate(line.getExpireDate()).setLotNumber(line.getLotNumber())
|
.setVendorId(receipt.getVendorId()).setReceiptDate(receipt.getReceiptDate())
|
.setPurchaseOrderCode(receipt.getPurchaseOrderCode());
|
MesWmBatchDO batch;
|
try {
|
batch = batchService.getOrGenerateBatchCode(batchReqVO);
|
} catch (ServiceException ex) {
|
// 批次配置要求的字段(生产日期、有效期、生产批号等)没有填:把行上下文补进提示
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_BATCH_GENERATE_FAILED.getCode(),
|
"物料[" + item.getCode() + " " + item.getName() + "]生成批次失败:" + ex.getMessage()
|
+ "。请编辑采购入库单里该物料的物料行,补充批次要求的字段(生产日期、有效期、生产批号)后重试。");
|
}
|
if (batch == null) {
|
return;
|
}
|
line.setBatchId(batch.getId()).setBatchCode(batch.getCode());
|
itemReceiptLineService.updateItemReceiptLineBatch(line.getId(), batch.getId(), batch.getCode());
|
}
|
|
/**
|
* 确认入库时补齐上架明细:没有明细的行按行入库数量生成一条明细,已有明细的校验数量合计
|
*/
|
private void createDetailsForConfirm(MesWmItemReceiptDO receipt, List<MesWmItemReceiptLineDO> lines,
|
Long warehouseId, Long locationId, Long areaId) {
|
for (MesWmItemReceiptLineDO line : lines) {
|
List<MesWmItemReceiptDetailDO> details = itemReceiptDetailService
|
.getItemReceiptDetailListByLineId(line.getId());
|
if (CollUtil.isEmpty(details)) {
|
// 旧流程“待执行入库”的单据应当已经有上架明细,缺了说明数据不完整
|
if (ObjUtil.equal(MesWmItemReceiptStatusEnum.APPROVED.getStatus(), receipt.getStatus())) {
|
throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_DETAIL_MISSING.getCode(),
|
"采购入库单[" + receipt.getCode() + "]处于旧流程的「待执行入库」状态,但物料行(ID="
|
+ line.getId() + ")没有上架明细,无法确认入库。"
|
+ "请将该单据取消后重新生成,或联系管理员补齐上架明细。");
|
}
|
MesWmItemReceiptDetailSaveReqVO detailReqVO = new MesWmItemReceiptDetailSaveReqVO();
|
detailReqVO.setReceiptId(receipt.getId()).setLineId(line.getId())
|
.setItemId(line.getItemId()).setQuantity(line.getReceivedQuantity())
|
.setBatchId(line.getBatchId())
|
.setWarehouseId(warehouseId).setLocationId(locationId).setAreaId(areaId);
|
itemReceiptDetailService.createItemReceiptDetail(detailReqVO);
|
continue;
|
}
|
// 已有上架明细(旧流程):数量合计必须与行入库数量一致
|
BigDecimal totalDetailQty = CollectionUtils.getSumValue(details,
|
MesWmItemReceiptDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO);
|
if (line.getReceivedQuantity() == null || totalDetailQty.compareTo(line.getReceivedQuantity()) != 0) {
|
MdmItemRespDTO item = mdmItemApi.getItem(line.getItemId()).getCheckedData();
|
String itemText = item != null ? item.getCode() + " " + item.getName() : "ID=" + line.getItemId();
|
throw new ServiceException(WM_ITEM_RECEIPT_DETAIL_QUANTITY_MISMATCH.getCode(),
|
"采购入库单[" + receipt.getCode() + "]的物料行[" + itemText + "]上架明细数量合计为["
|
+ totalDetailQty.toPlainString() + "],与行入库数量["
|
+ (line.getReceivedQuantity() != null ? line.getReceivedQuantity().toPlainString() : "空")
|
+ "]不一致,无法确认入库。请在物料行上核对入库数量,或取消该单据后按简化流程重新生成。");
|
}
|
}
|
}
|
|
/**
|
* 回写 ERP 采购订单的入库数量
|
*/
|
private void updateErpPurchaseOrderInCount(MesWmItemReceiptDO receipt) {
|
// 1. 确定采购订单 ID:优先从入库单头取(从到货通知单带入),其次从行反查
|
Long purchaseOrderId = receipt.getPurchaseOrderId();
|
if (purchaseOrderId == null && receipt.getNoticeId() != null) {
|
// 从关联的到货通知单获取采购订单 ID
|
MesWmArrivalNoticeDO notice = arrivalNoticeService.getArrivalNotice(receipt.getNoticeId());
|
if (notice != null) {
|
purchaseOrderId = notice.getPurchaseOrderId();
|
}
|
}
|
|
// 2. 获取入库单行,按 purchaseOrderItemId 更新入库数量
|
List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(receipt.getId());
|
if (CollUtil.isNotEmpty(lines)) {
|
for (MesWmItemReceiptLineDO line : lines) {
|
if (line.getPurchaseOrderItemId() != null && line.getReceivedQuantity() != null) {
|
purchaseOrderApi.updateOrderItemInCount(line.getPurchaseOrderItemId(), line.getReceivedQuantity());
|
// 如果还没拿到 purchaseOrderId,从明细项反查
|
if (purchaseOrderId == null) {
|
cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO itemDTO =
|
purchaseOrderApi.getPurchaseOrderItem(line.getPurchaseOrderItemId()).getCheckedData();
|
if (itemDTO != null && itemDTO.getOrderId() != null) {
|
purchaseOrderId = itemDTO.getOrderId();
|
}
|
}
|
}
|
}
|
} else if (receipt.getNoticeId() != null) {
|
// 兜底:入库行为空时,从到货通知单行回写入库数量
|
List<MesWmArrivalNoticeLineDO> noticeLines = arrivalNoticeLineService
|
.getArrivalNoticeLineListByNoticeId(receipt.getNoticeId());
|
for (MesWmArrivalNoticeLineDO noticeLine : noticeLines) {
|
if (noticeLine.getPurchaseOrderItemId() != null) {
|
// 使用合格数量(有 IQC 时为合格数量,否则为到货数量)
|
BigDecimal qty = noticeLine.getQualifiedQuantity() != null
|
? noticeLine.getQualifiedQuantity() : noticeLine.getArrivalQuantity();
|
if (qty != null && qty.compareTo(BigDecimal.ZERO) > 0) {
|
purchaseOrderApi.updateOrderItemInCount(noticeLine.getPurchaseOrderItemId(), qty);
|
}
|
}
|
}
|
}
|
|
// 3. 更新采购订单的入库状态
|
if (purchaseOrderId != null) {
|
purchaseOrderApi.updateOrderInStatus(purchaseOrderId);
|
}
|
}
|
|
private void createTransactionList(MesWmItemReceiptDO receipt) {
|
List<MesWmItemReceiptDetailDO> details = itemReceiptDetailService.getItemReceiptDetailListByReceiptId(receipt.getId());
|
// 获取所有行信息,用于获取批次号
|
List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(receipt.getId());
|
Map<Long, MesWmItemReceiptLineDO> lineMap = CollectionUtils.convertMap(lines, MesWmItemReceiptLineDO::getId);
|
|
wmTransactionService.createTransactionList(convertList(details, detail -> {
|
// 从行上获取批次信息(如果明细没有批次号)
|
MesWmItemReceiptLineDO line = lineMap.get(detail.getLineId());
|
Long batchId = detail.getBatchId() != null ? detail.getBatchId() :
|
(line != null ? line.getBatchId() : null);
|
String batchCode = line != null ? line.getBatchCode() : null;
|
|
return new MesWmTransactionSaveReqDTO()
|
.setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId())
|
.setQuantity(detail.getQuantity())
|
.setBatchId(batchId).setBatchCode(batchCode)
|
.setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
|
.setVendorId(receipt.getVendorId()).setReceiptTime(receipt.getReceiptDate())
|
.setBizType(MesBizTypeConstants.WM_ITEM_RECEIPT_IN).setBizId(receipt.getId())
|
.setBizCode(receipt.getCode()).setBizLineId(detail.getLineId());
|
}));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void cancelItemReceipt(Long id) {
|
// 校验存在
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
// 已完成和已取消不允许取消
|
if (ObjectUtils.equalsAny(receipt.getStatus(),
|
MesWmItemReceiptStatusEnum.FINISHED.getStatus(),
|
MesWmItemReceiptStatusEnum.CANCELED.getStatus())) {
|
throw exception(WM_ITEM_RECEIPT_CANCEL_NOT_ALLOWED);
|
}
|
// 取消
|
itemReceiptMapper.updateById(new MesWmItemReceiptDO()
|
.setId(id).setStatus(MesWmItemReceiptStatusEnum.CANCELED.getStatus()));
|
}
|
|
@Override
|
public MesWmItemReceiptDO validateItemReceiptEditable(Long id) {
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
if (ObjUtil.notEqual(receipt.getStatus(), MesWmItemReceiptStatusEnum.PREPARE.getStatus())
|
&& ObjUtil.notEqual(receipt.getStatus(), MesWmItemReceiptStatusEnum.APPROVING.getStatus())) {
|
throw exception(WM_ITEM_RECEIPT_STATUS_NOT_PREPARE);
|
}
|
return receipt;
|
}
|
|
private MesWmItemReceiptDO validateItemReceiptExists(Long id) {
|
MesWmItemReceiptDO receipt = itemReceiptMapper.selectById(id);
|
if (receipt == null) {
|
throw exception(WM_ITEM_RECEIPT_NOT_EXISTS);
|
}
|
return receipt;
|
}
|
|
/**
|
* 校验采购入库单存在且为草稿状态
|
*/
|
private MesWmItemReceiptDO validateItemReceiptExistsAndDraft(Long id) {
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
if (ObjUtil.notEqual(MesWmItemReceiptStatusEnum.PREPARE.getStatus(), receipt.getStatus())) {
|
throw exception(WM_ITEM_RECEIPT_STATUS_NOT_PREPARE);
|
}
|
return receipt;
|
}
|
|
private void validateCodeUnique(Long id, String code) {
|
MesWmItemReceiptDO receipt = itemReceiptMapper.selectByCode(code);
|
if (receipt == null) {
|
return;
|
}
|
if (ObjUtil.notEqual(id, receipt.getId())) {
|
throw exception(WM_ITEM_RECEIPT_CODE_DUPLICATE);
|
}
|
}
|
|
@Override
|
public Long getItemReceiptCountByVendorId(Long vendorId) {
|
return itemReceiptMapper.selectCountByVendorId(vendorId);
|
}
|
|
@Override
|
public List<MesWmItemReceiptDO> getItemReceiptListByVendorId(Long vendorId) {
|
return itemReceiptMapper.selectListByVendorId(vendorId);
|
}
|
|
@Override
|
public List<MesWmItemReceiptDO> getItemReceiptList(Collection<Long> ids) {
|
return itemReceiptMapper.selectByIds(ids);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long generateItemReceiptFromArrivalNotice(Long arrivalNoticeId) {
|
// 1. 校验到货通知单存在且状态为待入库
|
MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(arrivalNoticeId);
|
|
// 2. 校验是否已生成过入库单(防止重复生成)
|
MesWmItemReceiptDO existReceipt = itemReceiptMapper.selectByNoticeId(arrivalNoticeId);
|
if (existReceipt != null) {
|
throw exception(WM_ITEM_RECEIPT_ALREADY_EXISTS);
|
}
|
|
// 3. 获取到货通知单行
|
List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(arrivalNoticeId);
|
if (CollUtil.isEmpty(lines)) {
|
throw exception(WM_ARRIVAL_NOTICE_NO_LINE);
|
}
|
|
// 4. 生成入库单编码
|
String code = generateItemReceiptCode();
|
|
// 5. 创建入库单主表
|
MesWmItemReceiptDO receipt = new MesWmItemReceiptDO();
|
receipt.setCode(code);
|
receipt.setName(notice.getName() + "入库单");
|
receipt.setNoticeId(arrivalNoticeId);
|
receipt.setPurchaseOrderId(notice.getPurchaseOrderId());
|
receipt.setPurchaseOrderCode(notice.getPurchaseOrderCode());
|
receipt.setVendorId(notice.getVendorId());
|
receipt.setReceiptDate(java.time.LocalDateTime.now()); // 入库时间为当前时间
|
receipt.setStatus(MesWmItemReceiptStatusEnum.PREPARE.getStatus());
|
itemReceiptMapper.insert(receipt);
|
|
// 6. 遍历行创建入库单行:需检行只入质检合格数量,无需检行入到货数量
|
int createdLineCount = 0;
|
for (MesWmArrivalNoticeLineDO noticeLine : lines) {
|
BigDecimal receivable = Boolean.TRUE.equals(noticeLine.getIqcCheckFlag())
|
? noticeLine.getQualifiedQuantity() : noticeLine.getArrivalQuantity();
|
// 整行不合格(合格数量为 0)或无需检行未填写到货数量时,不生成零数量入库行
|
if (receivable == null || receivable.signum() <= 0) {
|
continue;
|
}
|
MesWmItemReceiptLineDO receiptLine = new MesWmItemReceiptLineDO();
|
receiptLine.setReceiptId(receipt.getId());
|
receiptLine.setArrivalNoticeLineId(noticeLine.getId());
|
receiptLine.setPurchaseOrderItemId(noticeLine.getPurchaseOrderItemId());
|
receiptLine.setItemId(noticeLine.getItemId());
|
receiptLine.setReceivedQuantity(receivable);
|
// 按采购订单行单价快照金额,供来票/对账使用
|
fillReceiptLinePrice(receiptLine);
|
itemReceiptLineService.createItemReceiptLineFromArrivalNoticeLine(receiptLine);
|
createdLineCount++;
|
}
|
if (createdLineCount == 0) {
|
throw new ServiceException(WM_ARRIVAL_NOTICE_NO_QUALIFIED_LINE.getCode(),
|
"到货通知单[" + notice.getCode() + "]没有可入库的合格数量:所有需检行的质检合格数量均为 0 或为空。"
|
+ "请先完成不合格品的处置(退货或让步接收),处置完成后再生成入库单。");
|
}
|
|
return receipt.getId();
|
}
|
|
/**
|
* 按采购订单行单价回填入库行金额,避免对账时再联表取价
|
*/
|
private void fillReceiptLinePrice(MesWmItemReceiptLineDO receiptLine) {
|
if (receiptLine.getPurchaseOrderItemId() == null) {
|
return;
|
}
|
try {
|
var item = purchaseOrderApi.getPurchaseOrderItem(receiptLine.getPurchaseOrderItemId()).getData();
|
if (item == null || item.getProductPrice() == null) {
|
return;
|
}
|
receiptLine.setPrice(item.getProductPrice());
|
if (receiptLine.getReceivedQuantity() != null) {
|
receiptLine.setTotalPrice(item.getProductPrice().multiply(receiptLine.getReceivedQuantity()));
|
}
|
} catch (Exception ex) {
|
// 价格只是快照,采购订单行取不到时不阻断入库
|
log.warn("[fillReceiptLinePrice][采购订单行({}) 价格快照失败]", receiptLine.getPurchaseOrderItemId(), ex);
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void submitItemReceiptApproval(Long id, String processDefinitionKey) {
|
// 1. 校验存在 + 草稿状态
|
MesWmItemReceiptDO receipt = validateItemReceiptExistsAndDraft(id);
|
// 审批入口与普通提交入口保持一致,空行单据不能进入审批。
|
List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(id);
|
if (CollUtil.isEmpty(lines)) {
|
throw exception(WM_ITEM_RECEIPT_NO_LINE);
|
}
|
|
// 2. 创建 BPM 流程实例
|
Long userId = getCurrentUserId();
|
// 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型。
|
if (processInstanceApi.isGlobalAutoApprove()) {
|
itemReceiptMapper.updateById(new MesWmItemReceiptDO()
|
.setId(id)
|
.setStatus(MesWmItemReceiptStatusEnum.APPROVING.getStatus())
|
.setProcessInstanceId(""));
|
updateItemReceiptAuditStatus(id, cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus());
|
return;
|
}
|
|
if (StrUtil.isBlank(processDefinitionKey)) {
|
throw exception(WM_ITEM_RECEIPT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
|
}
|
ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
|
if (processDefinition == null || bpmProcessDefinitionService
|
.getProcessDefinitionInfoListByCategory(ITEM_RECEIPT_APPROVE_CATEGORY_CODE).stream()
|
.noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) {
|
throw exception(WM_ITEM_RECEIPT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
|
}
|
|
String processInstanceId = processInstanceApi.createProcessInstance(userId,
|
new BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(processDefinitionKey)
|
.setBusinessKey(String.valueOf(id))
|
.setVariables(Map.of("businessNo", receipt.getCode())));
|
|
// 3. 更新入库单状态为审批中
|
itemReceiptMapper.updateById(new MesWmItemReceiptDO()
|
.setId(id)
|
.setStatus(MesWmItemReceiptStatusEnum.APPROVING.getStatus())
|
.setProcessInstanceId(processInstanceId));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateItemReceiptAuditStatus(Long id, Integer bpmResult) {
|
// 1. 校验存在
|
MesWmItemReceiptDO receipt = validateItemReceiptExists(id);
|
if (!MesWmItemReceiptStatusEnum.APPROVING.getStatus().equals(receipt.getStatus())) {
|
log.warn("[updateItemReceiptAuditStatus] 入库单({}) 不处于审批中状态", id);
|
return;
|
}
|
|
// 2. 根据审批结果更新状态
|
Integer newStatus = convertBpmResultToReceiptStatus(bpmResult);
|
if (newStatus != null) {
|
itemReceiptMapper.update(null, new LambdaUpdateWrapper<MesWmItemReceiptDO>()
|
.eq(MesWmItemReceiptDO::getId, id)
|
.set(MesWmItemReceiptDO::getStatus, newStatus)
|
.set(MesWmItemReceiptDO::getApproveTime,
|
BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult) ? LocalDateTime.now() : null));
|
}
|
}
|
|
/**
|
* 转换 BPM 审批结果为入库单状态
|
*/
|
private Integer convertBpmResultToReceiptStatus(Integer bpmResult) {
|
if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
|
return MesWmItemReceiptStatusEnum.APPROVED.getStatus();
|
} else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
|
return MesWmItemReceiptStatusEnum.PREPARE.getStatus();
|
} else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
|
return MesWmItemReceiptStatusEnum.CANCELED.getStatus();
|
}
|
return null;
|
}
|
|
/**
|
* 获取当前用户 ID
|
*/
|
private Long getCurrentUserId() {
|
return Objects.requireNonNull(getLoginUserId(), "采购入库审批需要登录用户");
|
}
|
|
/**
|
* 生成入库单编码
|
*/
|
private String generateItemReceiptCode() {
|
// TODO: 使用编码规则服务生成编码
|
return "IR" + System.currentTimeMillis();
|
}
|
|
}
|