| | |
| | | import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; |
| | | 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.enums.task.BpmProcessInstanceStatusEnum; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseRequestApi; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestRespDTO; |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | | import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; |
| | | import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi; |
| | |
| | | |
| | | private static final DateTimeFormatter NO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMM"); |
| | | |
| | | /** |
| | | * 采购申请「审核通过」状态值,与 ERP 模块 ErpPurchaseRequestStatusEnum.APPROVE 保持一致 |
| | | */ |
| | | private static final Integer PURCHASE_REQUEST_STATUS_APPROVED = 20; |
| | | |
| | | @Resource |
| | | private SrmInquiryMapper inquiryMapper; |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | | @Resource |
| | | private SrmInquiryItemMapper inquiryItemMapper; |
| | | @Resource |
| | |
| | | private MdmUnitMeasureApi mdmUnitMeasureApi; |
| | | @Resource |
| | | private ErpPurchaseOrderApi erpPurchaseOrderApi; |
| | | @Resource |
| | | private ErpPurchaseRequestApi erpPurchaseRequestApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createInquiryFromPurchaseRequest(Long purchaseRequestId) { |
| | | ErpPurchaseRequestRespDTO purchaseRequest = erpPurchaseRequestApi |
| | | .getPurchaseRequest(purchaseRequestId).getCheckedData(); |
| | | if (purchaseRequest == null) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_NOT_EXISTS); |
| | | } |
| | | if (!PURCHASE_REQUEST_STATUS_APPROVED.equals(purchaseRequest.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_NOT_APPROVED.getCode(), |
| | | "采购申请【" + purchaseRequest.getNo() + "】当前状态不是「审核通过」,无法发起询价。" |
| | | + "请先在采购申请中完成审批,审批通过后再发起询价。"); |
| | | } |
| | | if (purchaseRequest.getOrderId() != null) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_HAS_ORDER.getCode(), |
| | | "采购申请【" + purchaseRequest.getNo() + "】已生成采购台账【" |
| | | + StrUtil.blankToDefault(purchaseRequest.getOrderNo(), "ID=" + purchaseRequest.getOrderId()) |
| | | + "】,无法再发起询价。请到采购台账中查看价格明细。"); |
| | | } |
| | | SrmInquiryDO linkedInquiry = purchaseRequest.getInquiryId() != null |
| | | ? inquiryMapper.selectById(purchaseRequest.getInquiryId()) : null; |
| | | if (linkedInquiry != null |
| | | && !InquiryStatusEnum.CANCELLED.getStatus().equals(linkedInquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_HAS_INQUIRY.getCode(), |
| | | "采购申请【" + purchaseRequest.getNo() + "】已发起过询价【" |
| | | + StrUtil.blankToDefault(purchaseRequest.getInquiryNo(), "ID=" + purchaseRequest.getInquiryId()) |
| | | + "】,请直接打开该询价单继续处理,不要重复发起。"); |
| | | } |
| | | List<ErpPurchaseRequestRespDTO.Item> requestItems = purchaseRequest.getItems(); |
| | | if (CollUtil.isEmpty(requestItems)) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY.getCode(), |
| | | "采购申请【" + purchaseRequest.getNo() + "】没有明细行,无法发起询价。请回到采购申请补充明细后再试。"); |
| | | } |
| | | |
| | | SrmInquiryDO inquiry = new SrmInquiryDO(); |
| | | inquiry.setNo(generateInquiryNo()); |
| | | inquiry.setTitle("采购申请【" + purchaseRequest.getNo() + "】询价"); |
| | | inquiry.setStatus(InquiryStatusEnum.DRAFT.getStatus()); |
| | | inquiry.setInquiryDate(LocalDate.now()); |
| | | inquiry.setPurchaserUserId(getLoginUserId()); |
| | | inquiry.setRemark("由采购申请【" + purchaseRequest.getNo() + "】发起"); |
| | | inquiry.setPurchaseRequestId(purchaseRequest.getId()); |
| | | inquiry.setPurchaseRequestNo(purchaseRequest.getNo()); |
| | | inquiryMapper.insert(inquiry); |
| | | |
| | | List<SrmInquirySaveReqVO.Item> itemVOs = new ArrayList<>(); |
| | | Set<Long> productIds = new HashSet<>(); |
| | | for (ErpPurchaseRequestRespDTO.Item requestItem : requestItems) { |
| | | if (!productIds.add(requestItem.getProductId())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY.getCode(), |
| | | "采购申请【" + purchaseRequest.getNo() + "】中产品【" |
| | | + resolveItemName(requestItem.getProductId()) + "】存在多行明细,无法直接发起询价。" |
| | | + "请先在采购申请中合并同一产品的数量后再试。"); |
| | | } |
| | | SrmInquirySaveReqVO.Item itemVO = new SrmInquirySaveReqVO.Item(); |
| | | itemVO.setProductId(requestItem.getProductId()); |
| | | itemVO.setProductUnitId(requestItem.getProductUnitId()); |
| | | itemVO.setQuantity(requestItem.getCount()); |
| | | itemVO.setRemark(requestItem.getRemark()); |
| | | itemVOs.add(itemVO); |
| | | } |
| | | saveItems(inquiry.getId(), itemVOs); |
| | | |
| | | erpPurchaseRequestApi.updatePurchaseRequestInquiry(purchaseRequest.getId(), inquiry.getId(), inquiry.getNo()); |
| | | return inquiry.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateInquiry(SrmInquirySaveReqVO updateReqVO) { |
| | | SrmInquiryDO inquiry = validateInquiryExists(updateReqVO.getId()); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus())) { |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(updateReqVO.getId()); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_EDITABLE.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) |
| | | + "】,仅草稿状态可编辑。如需调整产品行或供应商,请取消后重新创建询价单。"); |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteInquiry(Long id) { |
| | | SrmInquiryDO inquiry = validateInquiryExists(id); |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(id); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_EDITABLE.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) |
| | |
| | | inquirySupplierMapper.deleteByInquiryId(id); |
| | | inquiryQuoteMapper.deleteByInquiryId(id); |
| | | inquiryMapper.deleteById(id); |
| | | clearPurchaseRequestLink(inquiry); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitApproval(Long id, Long userId) { |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(id); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), "当前状态不可提交审核"); |
| | | } |
| | | List<SrmInquiryItemDO> items = inquiryItemMapper.selectListByInquiryId(id); |
| | | if (CollUtil.isEmpty(items)) throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_ITEM_EMPTY); |
| | | Map<Long, SrmInquiryQuoteDO> quotes = inquiryQuoteMapper.selectListByInquiryId(id).stream() |
| | | .collect(Collectors.toMap(SrmInquiryQuoteDO::getId, Function.identity())); |
| | | Set<Long> suppliers = inquirySupplierMapper.selectListByInquiryId(id).stream() |
| | | .map(SrmInquirySupplierDO::getSupplierId).collect(Collectors.toSet()); |
| | | for (SrmInquiryItemDO item : items) { |
| | | SrmInquiryQuoteDO quote = quotes.get(item.getSelectedQuoteId()); |
| | | if (quote == null || !item.getId().equals(quote.getInquiryItemId()) |
| | | || !suppliers.contains(quote.getSupplierId()) || quote.getPrice() == null |
| | | || quote.getPrice().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_WINNER_REQUIRED.getCode(), "请先为每个产品选择有效的中标报价"); |
| | | } |
| | | inquiryItemMapper.updateById(new SrmInquiryItemDO().setId(item.getId()) |
| | | .setSelectedSupplierId(quote.getSupplierId()).setSelectedPrice(quote.getPrice())); |
| | | } |
| | | inquiryMapper.updateById(new SrmInquiryDO().setId(id).setStatus(InquiryStatusEnum.APPROVING.getStatus()).setProcessInstanceId("")); |
| | | String processId = processInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey("srm_inquiry_approve").setBusinessKey(String.valueOf(id)) |
| | | .setVariables(Map.of("inquiryNo", inquiry.getNo(), "inquiryTitle", inquiry.getTitle()))); |
| | | inquiryMapper.updateById(new SrmInquiryDO().setId(id).setProcessInstanceId(processId)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateApprovalStatus(Long id, String processInstanceId, Integer status) { |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(id); |
| | | if (!InquiryStatusEnum.APPROVING.getStatus().equals(inquiry.getStatus()) |
| | | || (StrUtil.isNotEmpty(inquiry.getProcessInstanceId()) && !Objects.equals(inquiry.getProcessInstanceId(), processInstanceId))) return; |
| | | Integer next = null; |
| | | if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(status)) next = InquiryStatusEnum.APPROVED.getStatus(); |
| | | if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(status) || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(status)) next = InquiryStatusEnum.REJECTED.getStatus(); |
| | | if (next != null) inquiryMapper.updateById(new SrmInquiryDO().setId(id).setStatus(next)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void publishInquiry(Long id) { |
| | | SrmInquiryDO inquiry = validateInquiryExists(id); |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(id); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_QUOTING.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void cancelInquiry(Long id) { |
| | | SrmInquiryDO inquiry = validateInquiryExists(id); |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(id); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus())) { |
| | | && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CANCELLABLE.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) |
| | | + "】,仅草稿或询价中状态可取消。"); |
| | | } |
| | | inquiryMapper.updateById(new SrmInquiryDO() |
| | | .setId(id).setStatus(InquiryStatusEnum.CANCELLED.getStatus())); |
| | | // 询价取消后解除采购申请关联,采购员可重新选择「发起询价」或「直接生成台账」 |
| | | clearPurchaseRequestLink(inquiry); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void saveQuotes(SrmInquiryQuoteSaveReqVO saveReqVO) { |
| | | SrmInquiryDO inquiry = validateInquiryExists(saveReqVO.getInquiryId()); |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(saveReqVO.getInquiryId()); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus())) { |
| | | && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.REJECTED.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_QUOTING.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) |
| | | + "】,仅草稿或询价中状态可录入报价。"); |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public SrmInquiryConfirmRespVO confirmInquiry(SrmInquiryConfirmReqVO confirmReqVO) { |
| | | SrmInquiryDO inquiry = validateInquiryExists(confirmReqVO.getId()); |
| | | if (!InquiryStatusEnum.DRAFT.getStatus().equals(inquiry.getStatus()) |
| | | && !InquiryStatusEnum.QUOTING.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】当前状态为【" + statusName(inquiry.getStatus()) |
| | | + "】,仅草稿或询价中状态可确认转订单。"); |
| | | SrmInquiryDO inquiry = validateInquiryForUpdate(confirmReqVO.getId()); |
| | | if (!InquiryStatusEnum.APPROVED.getStatus().equals(inquiry.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), "询价单审核通过后才能转订单"); |
| | | } |
| | | if (CollUtil.isNotEmpty(confirmReqVO.getSelections())) { |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_STATUS_NOT_CONFIRMABLE.getCode(), "审核通过后不能更改中标结果"); |
| | | } |
| | | List<SrmInquiryItemDO> items = inquiryItemMapper.selectListByInquiryId(inquiry.getId()); |
| | | if (CollUtil.isEmpty(items)) { |
| | |
| | | ErpPurchaseOrderCreateReqDTO createReqDTO = new ErpPurchaseOrderCreateReqDTO(); |
| | | createReqDTO.setSupplierId(supplierId); |
| | | createReqDTO.setOrderTime(LocalDateTime.now()); |
| | | createReqDTO.setPaymentDate(confirmReqVO.getPaymentDate().atStartOfDay()); |
| | | createReqDTO.setDiscountPercent(BigDecimal.ZERO); |
| | | createReqDTO.setDepositPrice(BigDecimal.ZERO); |
| | | createReqDTO.setSourceType(2); |
| | | createReqDTO.setSourceNo(inquiry.getNo()); |
| | | createReqDTO.setSourceId(inquiry.getId()); |
| | | createReqDTO.setPurchaseRequestId(inquiry.getPurchaseRequestId()); |
| | | createReqDTO.setRemark("SRM询价自动生成,询价单号:" + inquiry.getNo()); |
| | | createReqDTO.setItems(orderItems); |
| | | |
| | |
| | | orderResult.setPurchaseOrderId(purchaseOrderId); |
| | | orderResult.setPurchaseOrderNo(purchaseOrderNo); |
| | | orders.add(orderResult); |
| | | } |
| | | |
| | | // 3.1 询价单来源于采购申请时,回写采购申请关联的采购台账 |
| | | // 一个询价单可能按中标供应商拆成多张台账,采购申请只记录第一张,完整清单可通过台账的采购申请字段反查 |
| | | if (inquiry.getPurchaseRequestId() != null && CollUtil.isNotEmpty(orders)) { |
| | | SrmInquiryConfirmRespVO.Order firstOrder = orders.get(0); |
| | | if (orders.size() > 1) { |
| | | log.info("[confirmInquiry] 询价单({}) 为采购申请({}) 生成 {} 张台账,采购申请仅回写首张台账({})", |
| | | inquiry.getNo(), inquiry.getPurchaseRequestNo(), orders.size(), firstOrder.getPurchaseOrderNo()); |
| | | } |
| | | try { |
| | | erpPurchaseRequestApi.updatePurchaseRequestOrder(inquiry.getPurchaseRequestId(), |
| | | firstOrder.getPurchaseOrderId(), firstOrder.getPurchaseOrderNo()); |
| | | } catch (Exception ex) { |
| | | log.error("[confirmInquiry] 询价单({}) 回写采购申请({}) 关联台账失败", |
| | | inquiry.getNo(), inquiry.getPurchaseRequestNo(), ex); |
| | | throw new ServiceException(ErrorCodeConstants.INQUIRY_ORDER_GENERATE_FAIL.getCode(), |
| | | "询价单【" + inquiry.getNo() + "】已生成采购台账【" |
| | | + StrUtil.blankToDefault(firstOrder.getPurchaseOrderNo(), "ID=" + firstOrder.getPurchaseOrderId()) |
| | | + "】,但回写采购申请【" + inquiry.getPurchaseRequestNo() + "】失败:" + ex.getMessage() |
| | | + "。请到采购申请中核对关联台账,或联系管理员处理。"); |
| | | } |
| | | } |
| | | |
| | | // 4. 更新询价单为已转订单(状态条件更新,避免并发重复生成) |
| | |
| | | |
| | | // ========== 校验 ========== |
| | | |
| | | private SrmInquiryDO validateInquiryForUpdate(Long id) { |
| | | SrmInquiryDO inquiry = inquiryMapper.selectByIdForUpdate(id); |
| | | if (inquiry == null) throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_NOT_EXISTS); |
| | | return inquiry; |
| | | } |
| | | |
| | | private SrmInquiryDO validateInquiryExists(Long id) { |
| | | SrmInquiryDO inquiry = inquiryMapper.selectById(id); |
| | | if (inquiry == null) { |
| | |
| | | "询价单号在多次尝试后仍重复,请稍后重试。"); |
| | | } |
| | | |
| | | /** |
| | | * 询价单被取消/删除时,解除对采购申请的关联,让采购员可重新选择「发起询价」或「直接生成台账」。 |
| | | * 解除失败只记录日志,不阻断取消操作;发起询价时会再按询价单状态做兜底判断。 |
| | | */ |
| | | private void clearPurchaseRequestLink(SrmInquiryDO inquiry) { |
| | | if (inquiry.getPurchaseRequestId() == null) { |
| | | return; |
| | | } |
| | | try { |
| | | erpPurchaseRequestApi.clearPurchaseRequestInquiry(inquiry.getPurchaseRequestId()); |
| | | } catch (Exception ex) { |
| | | log.warn("[clearPurchaseRequestLink] 询价单({}) 解除采购申请({}) 关联失败,请人工核对", |
| | | inquiry.getNo(), inquiry.getPurchaseRequestNo(), ex); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 解析产品名称,仅用于异常提示;解析失败时回退为可读 ID,避免二次异常 |
| | | */ |
| | | private String resolveItemName(Long productId) { |
| | | if (productId == null) { |
| | | return "未选择"; |
| | | } |
| | | try { |
| | | MdmItemRespDTO item = mdmItemApi.getItem(productId).getCheckedData(); |
| | | if (item != null && StrUtil.isNotBlank(item.getName())) { |
| | | return item.getName(); |
| | | } |
| | | } catch (Exception ex) { |
| | | log.warn("[resolveItemName] 查询产品({}) 名称失败", productId, ex); |
| | | } |
| | | return "ID=" + productId; |
| | | } |
| | | |
| | | private String statusName(Integer status) { |
| | | for (InquiryStatusEnum e : InquiryStatusEnum.values()) { |
| | | if (e.getStatus().equals(status)) { |