| | |
| | | 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 java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | |
| | | @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) { |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | @Deprecated |
| | | public void finishItemReceipt(Long id) { |
| | | // 1. 校验存在 |
| | | MesWmItemReceiptDO receipt = validateItemReceiptExists(id); |
| | |
| | | throw exception(WM_ITEM_RECEIPT_STATUS_ERROR); |
| | | } |
| | | |
| | | // 2. 遍历所有明细,创建库存事务(增加库存 + 记录流水) |
| | | // 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); |
| | | |
| | | // 3. 更新入库单状态 |
| | | // 2. 更新入库单状态 |
| | | itemReceiptMapper.updateById(new MesWmItemReceiptDO() |
| | | .setId(id).setStatus(MesWmItemReceiptStatusEnum.FINISHED.getStatus())); |
| | | .setId(receipt.getId()).setStatus(MesWmItemReceiptStatusEnum.FINISHED.getStatus())); |
| | | |
| | | // 4. 更新关联的到货通知单状态 |
| | | // 3. 更新关联的到货通知单状态 |
| | | if (receipt.getNoticeId() != null) { |
| | | arrivalNoticeService.finishArrivalNotice(receipt.getNoticeId()); |
| | | } |
| | | |
| | | // 5. 回写 ERP 采购订单的入库数量 |
| | | // 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) { |