package cn.iocoder.yudao.module.erp.service.purchase; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO; import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanSaveReqVO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchasePlanDO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchasePlanItemDO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO; import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchasePlanItemMapper; import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchasePlanMapper; import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper; import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper; import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO; import cn.iocoder.yudao.module.erp.enums.ErpPurchasePlanStatusEnum; import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum; import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi; import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSafetyStockRespDTO; import cn.iocoder.yudao.module.mes.api.wms.MesWmStockApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; 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.erp.enums.ErrorCodeConstants.*; /** * ERP 采购计划 Service 实现类 * * @author xiaoyi */ @Service @Validated @Slf4j public class ErpPurchasePlanServiceImpl implements ErpPurchasePlanService { /** * BPM 采购计划审批分类编码 */ private static final String PURCHASE_PLAN_APPROVE_CATEGORY_CODE = "purchase_plan_approve"; /** * 需求来源:库存预警 */ public static final Integer DEMAND_SOURCE_STOCK_ALERT = 1; /** * 需求来源:运维/生产计划 */ public static final Integer DEMAND_SOURCE_PLAN = 2; @Resource private ErpPurchasePlanMapper purchasePlanMapper; @Resource private ErpPurchasePlanItemMapper purchasePlanItemMapper; @Resource private ErpPurchaseRequestMapper purchaseRequestMapper; @Resource private ErpPurchaseRequestItemMapper purchaseRequestItemMapper; @Resource private ErpNoRedisDAO noRedisDAO; @Resource private BpmProcessInstanceApi processInstanceApi; @Resource private BpmCategoryService bpmCategoryService; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Resource private AdminUserApi adminUserApi; @Resource private MesMdItemApi mesMdItemApi; @Resource private MesWmStockApi mesWmStockApi; @Override @Transactional(rollbackFor = Exception.class) public Long createPurchasePlan(ErpPurchasePlanSaveReqVO createReqVO) { // 1. 校验明细 List planItems = convertList(createReqVO.getItems(), item -> BeanUtils.toBean(item, ErpPurchasePlanItemDO.class)); if (CollUtil.isEmpty(planItems)) { throw exception(PURCHASE_PLAN_ITEM_EMPTY); } // 2. 生成计划单号 String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_PLAN_NO_PREFIX); if (purchasePlanMapper.selectByNo(no) != null) { throw exception(PURCHASE_PLAN_NO_EXISTS); } // 3. 获取当前用户信息 Long userId = Long.parseLong(getCurrentUserId()); AdminUserRespDTO user = adminUserApi.getUser(userId); Long deptId = user != null ? user.getDeptId() : null; // 4. 插入采购计划 ErpPurchasePlanDO plan = BeanUtils.toBean(createReqVO, ErpPurchasePlanDO.class, req -> req .setNo(no) .setStatus(ErpPurchasePlanStatusEnum.DRAFT.getStatus()) .setPlanUserId(userId) .setPlanDeptId(deptId) .setGeneratedFlag(false) .setTotalCount(planItems.stream() .map(ErpPurchasePlanItemDO::getCount).filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add))); purchasePlanMapper.insert(plan); // 5. 插入明细 purchasePlanItemMapper.insertBatch(convertList(planItems, item -> item.setPlanId(plan.getId()))); return plan.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updatePurchasePlan(ErpPurchasePlanSaveReqVO updateReqVO) { // 1.1 校验存在 + 草稿状态 ErpPurchasePlanDO oldPlan = validatePurchasePlanCanModify(updateReqVO.getId()); // 1.2 校验明细 List planItems = convertList(updateReqVO.getItems(), item -> BeanUtils.toBean(item, ErpPurchasePlanItemDO.class)); if (CollUtil.isEmpty(planItems)) { throw exception(PURCHASE_PLAN_ITEM_EMPTY); } // 2. 更新主表 ErpPurchasePlanDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchasePlanDO.class, req -> req .setNo(oldPlan.getNo()) .setStatus(oldPlan.getStatus()) .setPlanUserId(oldPlan.getPlanUserId()) .setPlanDeptId(oldPlan.getPlanDeptId()) .setGeneratedFlag(oldPlan.getGeneratedFlag()) .setTotalCount(planItems.stream() .map(ErpPurchasePlanItemDO::getCount).filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add))); purchasePlanMapper.updateById(updateObj); // 3. 更新明细:先删后插 purchasePlanItemMapper.deleteByPlanId(updateReqVO.getId()); purchasePlanItemMapper.insertBatch(convertList(planItems, item -> item.setPlanId(updateReqVO.getId()))); } @Override @Transactional(rollbackFor = Exception.class) public void deletePurchasePlan(Collection ids) { if (CollUtil.isEmpty(ids)) { return; } for (Long id : ids) { // 1. 校验存在 + 草稿状态 validatePurchasePlanCanModify(id); // 2. 删除明细 purchasePlanItemMapper.deleteByPlanId(id); // 3. 删除主表 purchasePlanMapper.deleteById(id); } } @Override public ErpPurchasePlanDO getPurchasePlan(Long id) { return purchasePlanMapper.selectById(id); } @Override public List getPurchasePlanItemListByPlanId(Long planId) { return purchasePlanItemMapper.selectListByPlanId(planId); } @Override public List getPurchasePlanItemListByPlanIds(Collection planIds) { return purchasePlanItemMapper.selectListByPlanIds(planIds); } @Override public PageResult getPurchasePlanPage(ErpPurchasePlanPageReqVO pageReqVO) { return purchasePlanMapper.selectPage(pageReqVO); } @Override @Transactional(rollbackFor = Exception.class) public void submitPurchasePlan(Long id, String processDefinitionKey, Long userId) { // 1. 校验存在 + 草稿状态 ErpPurchasePlanDO plan = validatePurchasePlanCanModify(id); // 2. 校验是否有明细 List items = purchasePlanItemMapper.selectListByPlanId(id); if (CollUtil.isEmpty(items)) { throw exception(PURCHASE_PLAN_ITEM_EMPTY); } // 3. 创建 BPM 流程实例 String processInstanceId = processInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(processDefinitionKey) .setBusinessKey(String.valueOf(id))); // 4. 更新采购计划状态为审批中 purchasePlanMapper.updateById(new ErpPurchasePlanDO() .setId(id) .setStatus(ErpPurchasePlanStatusEnum.PROCESS.getStatus()) .setProcessInstanceId(processInstanceId)); } @Override public List> getPurchasePlanApproveProcessDefinitionList() { // 1. 校验分类和流程定义是否存在 validatePurchasePlanApproveCategoryAndProcessDefinition(); // 2. 获取分类下的流程定义信息 List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(PURCHASE_PLAN_APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { return Collections.emptyList(); } // 3. 遍历获取流程定义详情,过滤激活状态,保留最新版本 Map latestVersionMap = new HashMap<>(); for (BpmProcessDefinitionInfoDO info : definitionInfoList) { ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId()); if (pd == null || pd.isSuspended()) { continue; } ProcessDefinition existing = latestVersionMap.get(pd.getKey()); if (existing == null || pd.getVersion() > existing.getVersion()) { latestVersionMap.put(pd.getKey(), pd); } } // 4. 返回流程定义列表 List> result = new ArrayList<>(); for (ProcessDefinition pd : latestVersionMap.values()) { Map item = new HashMap<>(); item.put("id", pd.getId()); item.put("key", pd.getKey()); item.put("name", pd.getName()); result.add(item); } return result; } @Override @Transactional(rollbackFor = Exception.class) public void updatePurchasePlanAuditStatus(Long id, Integer bpmResult) { // 1. 校验存在 ErpPurchasePlanDO plan = validatePurchasePlanExists(id); if (!ErpPurchasePlanStatusEnum.PROCESS.getStatus().equals(plan.getStatus())) { log.warn("[updatePurchasePlanAuditStatus] 采购计划({}) 不处于审批中状态", id); return; } // 2. 根据审批结果更新状态 Integer newStatus = convertBpmResultToStatus(bpmResult); if (newStatus != null) { purchasePlanMapper.updateById(new ErpPurchasePlanDO().setId(id).setStatus(newStatus)); } // 3. 审批通过且未生成申请,自动生成采购申请 if (ErpPurchasePlanStatusEnum.APPROVE.getStatus().equals(newStatus) && !Boolean.TRUE.equals(plan.getGeneratedFlag())) { try { List requestIds = doGeneratePurchaseRequests(plan); log.info("[updatePurchasePlanAuditStatus] 采购计划({}) 自动生成采购申请({})", id, requestIds); } catch (Exception e) { log.error("[updatePurchasePlanAuditStatus] 采购计划({}) 自动生成采购申请失败", id, e); // 不抛出异常,避免影响状态更新 } } } @Override @Transactional(rollbackFor = Exception.class) public ErpPurchasePlanDO generatePlanFromStockAlert(String name, Long supplierId) { // 1. 获取启用安全库存的物料 List safetyStockItems = mesMdItemApi.getSafetyStockItemList(); if (CollUtil.isEmpty(safetyStockItems)) { throw exception(PURCHASE_PLAN_ITEM_EMPTY); } // 2. 计算缺量,生成计划明细 List items = new ArrayList<>(); for (MesMdItemSafetyStockRespDTO item : safetyStockItems) { if (item.getMinStock() == null) { continue; } BigDecimal stock = mesWmStockApi.getStockQuantity(item.getId()); BigDecimal shortage = item.getMinStock().subtract(stock == null ? BigDecimal.ZERO : stock); if (shortage.compareTo(BigDecimal.ZERO) <= 0) { continue; } ErpPurchasePlanSaveReqVO.Item planItem = new ErpPurchasePlanSaveReqVO.Item(); planItem.setProductId(item.getMdmItemId()); planItem.setProductUnitId(item.getUnitMeasureId()); planItem.setCount(shortage); items.add(planItem); } if (CollUtil.isEmpty(items)) { throw exception(PURCHASE_PLAN_ITEM_EMPTY); } // 3. 创建草稿采购计划 ErpPurchasePlanSaveReqVO createReqVO = new ErpPurchasePlanSaveReqVO(); createReqVO.setName(name != null ? name : "库存预警采购计划"); createReqVO.setDemandSource(DEMAND_SOURCE_STOCK_ALERT); createReqVO.setPlanDate(LocalDateTime.now()); createReqVO.setSupplierId(supplierId); createReqVO.setItems(items); Long planId = createPurchasePlan(createReqVO); return purchasePlanMapper.selectById(planId); } @Override @Transactional(rollbackFor = Exception.class) public List generatePurchaseRequests(Long id) { // 1. 校验存在 + 审核通过状态 ErpPurchasePlanDO plan = validatePurchasePlanExists(id); if (!ErpPurchasePlanStatusEnum.APPROVE.getStatus().equals(plan.getStatus())) { throw exception(PURCHASE_PLAN_NOT_APPROVE); } // 2. 校验是否已生成申请 if (Boolean.TRUE.equals(plan.getGeneratedFlag())) { throw exception(PURCHASE_PLAN_REQUEST_EXISTS); } // 3. 生成采购申请 return doGeneratePurchaseRequests(plan); } /** * 执行生成采购申请(一个计划生成一张申请单) * * @param plan 采购计划 * @return 采购申请编号列表 */ private List doGeneratePurchaseRequests(ErpPurchasePlanDO plan) { // 1. 获取计划明细 List planItems = purchasePlanItemMapper.selectListByPlanId(plan.getId()); if (CollUtil.isEmpty(planItems)) { throw exception(PURCHASE_PLAN_ITEM_EMPTY); } // 2. 生成申请单号 String requestNo = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX); if (purchaseRequestMapper.selectByNo(requestNo) != null) { throw exception(PURCHASE_REQUEST_NO_EXISTS); } // 3. 创建采购申请 ErpPurchaseRequestDO request = new ErpPurchaseRequestDO(); request.setNo(requestNo); request.setStatus(ErpPurchaseRequestStatusEnum.DRAFT.getStatus()); request.setRequestUserId(plan.getPlanUserId()); request.setRequestDeptId(plan.getPlanDeptId()); request.setRequestTime(LocalDateTime.now()); request.setSupplierId(plan.getSupplierId()); request.setTotalCount(planItems.stream() .map(ErpPurchasePlanItemDO::getCount).filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add)); request.setTotalPrice(BigDecimal.ZERO); request.setTotalProductPrice(BigDecimal.ZERO); request.setTotalTaxPrice(BigDecimal.ZERO); request.setRemark(plan.getRemark()); request.setInStatus(0); purchaseRequestMapper.insert(request); // 4. 创建采购申请明细 List requestItems = convertList(planItems, item -> { ErpPurchaseRequestItemDO requestItem = new ErpPurchaseRequestItemDO(); requestItem.setRequestId(request.getId()); requestItem.setProductId(item.getProductId()); requestItem.setProductUnitId(item.getProductUnitId()); requestItem.setCount(item.getCount()); requestItem.setDemandTime(item.getDemandTime()); requestItem.setRemark(item.getRemark()); requestItem.setInCount(BigDecimal.ZERO); return requestItem; }); purchaseRequestItemMapper.insertBatch(requestItems); // 5. 回写计划已生成标记 purchasePlanMapper.updateById(new ErpPurchasePlanDO() .setId(plan.getId()).setGeneratedFlag(true)); return List.of(request.getId()); } private Integer convertBpmResultToStatus(Integer bpmResult) { if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) { return ErpPurchasePlanStatusEnum.APPROVE.getStatus(); } if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) { return ErpPurchasePlanStatusEnum.REJECT.getStatus(); } if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) { return ErpPurchasePlanStatusEnum.CANCEL.getStatus(); } return null; } private void validatePurchasePlanApproveCategoryAndProcessDefinition() { List categories = bpmCategoryService.getCategoryListByCode(Collections.singletonList(PURCHASE_PLAN_APPROVE_CATEGORY_CODE)); if (CollUtil.isEmpty(categories)) { throw exception(PURCHASE_PLAN_BPM_CATEGORY_NOT_EXISTS); } List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(PURCHASE_PLAN_APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { throw exception(PURCHASE_PLAN_BPM_PROCESS_DEFINITION_NOT_EXISTS); } } private ErpPurchasePlanDO validatePurchasePlanExists(Long id) { ErpPurchasePlanDO plan = purchasePlanMapper.selectById(id); if (plan == null) { throw exception(PURCHASE_PLAN_NOT_EXISTS); } return plan; } private ErpPurchasePlanDO validatePurchasePlanCanModify(Long id) { ErpPurchasePlanDO plan = validatePurchasePlanExists(id); if (!ErpPurchasePlanStatusEnum.DRAFT.getStatus().equals(plan.getStatus())) { throw exception(PURCHASE_PLAN_UPDATE_FAIL_NOT_DRAFT, plan.getNo()); } return plan; } private String getCurrentUserId() { return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId().toString(); } }