gaoluyang
5 天以前 8168aef3c7557c9e1361892270a1ebe35dbddba5
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/inquiry/SrmInquiryServiceImpl.java
@@ -7,9 +7,14 @@
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;
@@ -46,8 +51,15 @@
    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
@@ -64,6 +76,8 @@
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private ErpPurchaseOrderApi erpPurchaseOrderApi;
    @Resource
    private ErpPurchaseRequestApi erpPurchaseRequestApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -90,9 +104,77 @@
    @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())
                            + "】,仅草稿状态可编辑。如需调整产品行或供应商,请取消后重新创建询价单。");
@@ -123,7 +205,7 @@
    @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())
@@ -133,6 +215,7 @@
        inquirySupplierMapper.deleteByInquiryId(id);
        inquiryQuoteMapper.deleteByInquiryId(id);
        inquiryMapper.deleteById(id);
        clearPurchaseRequestLink(inquiry);
    }
    @Override
@@ -161,8 +244,52 @@
    @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())
@@ -175,23 +302,27 @@
    @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())
                            + "】,仅草稿或询价中状态可录入报价。");
@@ -269,12 +400,12 @@
    @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)) {
@@ -406,6 +537,7 @@
            createReqDTO.setSourceType(2);
            createReqDTO.setSourceNo(inquiry.getNo());
            createReqDTO.setSourceId(inquiry.getId());
            createReqDTO.setPurchaseRequestId(inquiry.getPurchaseRequestId());
            createReqDTO.setRemark("SRM询价自动生成,询价单号:" + inquiry.getNo());
            createReqDTO.setItems(orderItems);
@@ -438,6 +570,28 @@
            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. 更新询价单为已转订单(状态条件更新,避免并发重复生成)
@@ -647,6 +801,12 @@
    // ========== 校验 ==========
    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) {
@@ -668,6 +828,40 @@
                "询价单号在多次尝试后仍重复,请稍后重试。");
    }
    /**
     * 询价单被取消/删除时,解除对采购申请的关联,让采购员可重新选择「发起询价」或「直接生成台账」。
     * 解除失败只记录日志,不阻断取消操作;发起询价时会再按询价单状态做兜底判断。
     */
    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)) {