| | |
| | | package cn.iocoder.yudao.module.srm.service.tender; |
| | | |
| | | 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.erp.api.purchase.ErpPurchaseOrderApi; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO; |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | | import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderMaterialSaveReqVO; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPageReqVO; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPartialUpdateReqVO; |
| | | import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectSaveReqVO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmBidAwardDO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderMaterialDO; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderProjectDO; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmBidAwardMapper; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderMaterialMapper; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderProjectMapper; |
| | | import cn.iocoder.yudao.module.srm.dal.dataobject.*; |
| | | import cn.iocoder.yudao.module.srm.dal.mysql.*; |
| | | import cn.iocoder.yudao.module.srm.enums.AwardStatusEnum; |
| | | import cn.iocoder.yudao.module.srm.enums.BiddingModeEnum; |
| | | import cn.iocoder.yudao.module.srm.enums.BidStatusEnum; |
| | | import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants; |
| | | import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum; |
| | | import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; |
| | | import com.mzt.logapi.context.LogRecordContext; |
| | | import com.mzt.logapi.starter.annotation.LogRecord; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.module.srm.enums.LogRecordConstants.*; |
| | | |
| | | @Service |
| | | @Validated |
| | | @Slf4j |
| | | public class SrmTenderProjectServiceImpl implements SrmTenderProjectService { |
| | | |
| | | @Resource |
| | |
| | | private SrmTenderMaterialMapper tenderMaterialMapper; |
| | | @Resource |
| | | private SrmBidAwardMapper bidAwardMapper; |
| | | @Resource |
| | | private SrmTenderBidMapper tenderBidMapper; |
| | | |
| | | @Resource |
| | | private SrmTenderConfigService tenderConfigService; |
| | | @Resource |
| | | private SrmSupplierMapper supplierMapper; |
| | | @Resource |
| | | private SrmSupplierQuoteMapper supplierQuoteMapper; |
| | | @Resource |
| | | private MdmItemApi mdmItemApi; |
| | | @Resource |
| | | private ErpPurchaseOrderApi erpPurchaseOrderApi; |
| | | |
| | | @Override |
| | | public Long createTenderProject(SrmTenderProjectSaveReqVO createReqVO) { |
| | | SrmTenderProjectDO project = BeanUtils.toBean(createReqVO, SrmTenderProjectDO.class); |
| | | project.setTenderStatus(TenderStatusEnum.DRAFT.getCode()); |
| | | if (project.getBiddingMode() == null || project.getBiddingMode().isEmpty()) { |
| | | project.setBiddingMode(tenderConfigService.getConfig().getDefaultBiddingMode()); |
| | | } |
| | | tenderProjectMapper.insert(project); |
| | | return project.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE, |
| | | bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS) |
| | | public void updateTenderProject(SrmTenderProjectSaveReqVO updateReqVO) { |
| | | SrmTenderProjectDO project = validateTenderExists(updateReqVO.getId()); |
| | | if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT); |
| | | Integer status = project.getTenderStatus(); |
| | | |
| | | // 草稿状态:全字段可编辑 |
| | | if (TenderStatusEnum.canEditFullProject(status)) { |
| | | SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class); |
| | | updateObj.setTenderStatus(status); // 状态不允许前端传入覆盖 |
| | | tenderProjectMapper.updateById(updateObj); |
| | | LogRecordContext.putVariable("tenderName", project.getTenderName()); |
| | | LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc()); |
| | | LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc()); |
| | | LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements()); |
| | | LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements()); |
| | | return; |
| | | } |
| | | SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class); |
| | | tenderProjectMapper.updateById(updateObj); |
| | | |
| | | // 发布中/投标中:仅允许修改招标说明和资质要求 |
| | | if (TenderStatusEnum.canEditPartialProject(status)) { |
| | | SrmTenderProjectDO updateObj = new SrmTenderProjectDO(); |
| | | updateObj.setId(updateReqVO.getId()); |
| | | updateObj.setTenderDesc(updateReqVO.getTenderDesc()); |
| | | updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements()); |
| | | tenderProjectMapper.updateById(updateObj); |
| | | LogRecordContext.putVariable("tenderName", project.getTenderName()); |
| | | LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc()); |
| | | LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc()); |
| | | LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements()); |
| | | LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements()); |
| | | return; |
| | | } |
| | | |
| | | // 其他状态:不允许编辑 |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT); |
| | | } |
| | | // 检查是否已有投标数据 |
| | | if (tenderBidMapper.countByTenderProjectId(id) > 0) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_HAS_BIDS_CANNOT_DELETE); |
| | | } |
| | | tenderMaterialMapper.deleteByTenderProjectId(id); |
| | | tenderProjectMapper.deleteById(id); |
| | | } |
| | | |
| | | @Override |
| | | public SrmTenderProjectDO getTenderProject(Long id) { |
| | | return tenderProjectMapper.selectById(id); |
| | | SrmTenderProjectDO project = tenderProjectMapper.selectById(id); |
| | | if (project != null) { |
| | | SrmBidAwardDO award = bidAwardMapper.selectByTenderProjectId(id); |
| | | if (award != null) { |
| | | project.setAwardId(award.getId()); |
| | | project.setPurchaseOrderNo(award.getPurchaseOrderNo()); |
| | | } |
| | | } |
| | | return project; |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (award != null) { |
| | | project.setAwardId(award.getId()); |
| | | project.setPurchaseOrderNo(award.getPurchaseOrderNo()); |
| | | // 防御:如果定标已生成订单但项目状态未更新,同步状态 |
| | | if (AwardStatusEnum.ORDER_GENERATED.getCode().equals(award.getAwardStatus())) { |
| | | project.setTenderStatus(TenderStatusEnum.CLOSED.getCode()); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | |
| | | SrmTenderProjectDO project = validateTenderExists(id); |
| | | if (TenderStatusEnum.CLOSED.getCode().equals(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_ALREADY_CLOSED); |
| | | } |
| | | // 草稿状态不允许直接关闭 |
| | | if (TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED); |
| | | } |
| | | project.setTenderStatus(TenderStatusEnum.CLOSED.getCode()); |
| | | tenderProjectMapper.updateById(project); |
| | |
| | | tenderProjectMapper.updateById(project); |
| | | } |
| | | |
| | | // ========== 简易模式操作 ========== |
| | | |
| | | @Override |
| | | public void startPriceComparing(Long id) { |
| | | SrmTenderProjectDO project = validateTenderExists(id); |
| | | if (!BiddingModeEnum.isSimple(project.getBiddingMode())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MODE_NOT_SIMPLE); |
| | | } |
| | | if (!TenderStatusEnum.canStartPriceComparing(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_DRAFT); |
| | | } |
| | | // 检查是否有物料 |
| | | if (tenderMaterialMapper.countByTenderProjectId(id) == 0) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NO_MATERIALS); |
| | | } |
| | | project.setTenderStatus(TenderStatusEnum.PRICE_COMPARING.getCode()); |
| | | tenderProjectMapper.updateById(project); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void confirmTenderProject(Long id, Long bidId) { |
| | | // 1. 校验项目状态 |
| | | SrmTenderProjectDO project = validateTenderExists(id); |
| | | if (!BiddingModeEnum.isSimple(project.getBiddingMode())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MODE_NOT_SIMPLE); |
| | | } |
| | | if (!TenderStatusEnum.canConfirm(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_STATUS_NOT_PRICE_COMPARING); |
| | | } |
| | | |
| | | // 2. 校验投标记录 |
| | | SrmTenderBidDO bid = tenderBidMapper.selectById(bidId); |
| | | if (bid == null) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_NOT_EXISTS); |
| | | } |
| | | if (!bid.getTenderProjectId().equals(id)) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_BID_NOT_IN_PROJECT); |
| | | } |
| | | if (BidStatusEnum.WITHDRAWN.getCode().equals(bid.getBidStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_BID_WITHDRAWN); |
| | | } |
| | | |
| | | // 3. 创建定标记录(直接已定标,跳过审批) |
| | | SrmBidAwardDO award = new SrmBidAwardDO(); |
| | | award.setTenderProjectId(id); |
| | | award.setSupplierId(bid.getSupplierId()); |
| | | award.setBidId(bidId); |
| | | award.setAwardNo("SMP" + System.currentTimeMillis()); |
| | | award.setAwardStatus(AwardStatusEnum.AWARDED.getCode()); |
| | | award.setAwardAmount(bid.getBidTotalAmount()); |
| | | award.setAwardTime(LocalDateTime.now()); |
| | | award.setApprovalTime(LocalDateTime.now()); |
| | | bidAwardMapper.insert(award); |
| | | |
| | | // 4. 设置投标状态为中标 |
| | | bid.setBidStatus(BidStatusEnum.WIN.getCode()); |
| | | tenderBidMapper.updateById(bid); |
| | | |
| | | // 5. 自动生成采购订单 |
| | | generatePurchaseOrderForSimple(award, project); |
| | | |
| | | // 6. 更新项目状态 |
| | | project.setTenderStatus(TenderStatusEnum.CONFIRMED.getCode()); |
| | | tenderProjectMapper.updateById(project); |
| | | } |
| | | |
| | | /** |
| | | * 简易模式:根据定标信息自动生成采购订单 |
| | | */ |
| | | private void generatePurchaseOrderForSimple(SrmBidAwardDO award, SrmTenderProjectDO project) { |
| | | // 查供应商 |
| | | SrmSupplierDO supplier = supplierMapper.selectById(award.getSupplierId()); |
| | | if (supplier == null) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_SUPPLIER_NOT_SYNCED); |
| | | } |
| | | |
| | | // 查招标物料 |
| | | List<SrmTenderMaterialDO> materials = tenderMaterialMapper |
| | | .selectListByTenderProjectId(award.getTenderProjectId()); |
| | | if (CollUtil.isEmpty(materials)) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_NOT_EXISTS); |
| | | } |
| | | |
| | | // 查中标报价 |
| | | List<SrmSupplierQuoteDO> quotes = supplierQuoteMapper.selectListByBidId(award.getBidId()); |
| | | if (CollUtil.isEmpty(quotes)) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_QUOTE_NOT_EXISTS); |
| | | } |
| | | Map<Long, SrmSupplierQuoteDO> quoteMap = quotes.stream() |
| | | .collect(Collectors.toMap( |
| | | SrmSupplierQuoteDO::getTenderMaterialId, |
| | | Function.identity(), |
| | | (a, b) -> a)); |
| | | |
| | | // 查 MDM 物料 |
| | | Set<Long> productIds = materials.stream() |
| | | .map(SrmTenderMaterialDO::getProductId) |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemList(productIds).getCheckedData() |
| | | .stream().collect(Collectors.toMap(MdmItemRespDTO::getId, Function.identity())); |
| | | |
| | | // 计算价格 |
| | | BigDecimal totalQuoteAmount = quotes.stream() |
| | | .map(SrmSupplierQuoteDO::getQuotePrice) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | BigDecimal awardAmount = award.getAwardAmount() != null ? award.getAwardAmount() : totalQuoteAmount; |
| | | BigDecimal ratio = totalQuoteAmount.compareTo(BigDecimal.ZERO) > 0 |
| | | ? awardAmount.divide(totalQuoteAmount, 8, RoundingMode.HALF_UP) |
| | | : BigDecimal.ONE; |
| | | |
| | | // 构建订单项 |
| | | List<ErpPurchaseOrderCreateReqDTO.Item> orderItems = new ArrayList<>(); |
| | | for (SrmTenderMaterialDO material : materials) { |
| | | SrmSupplierQuoteDO quote = quoteMap.get(material.getId()); |
| | | if (quote == null) { |
| | | continue; |
| | | } |
| | | MdmItemRespDTO mdmItem = itemMap.get(material.getProductId()); |
| | | if (mdmItem == null) { |
| | | continue; |
| | | } |
| | | BigDecimal materialTotal = quote.getQuotePrice().multiply(ratio); |
| | | BigDecimal unitPrice = materialTotal.divide(material.getQuantity(), 4, RoundingMode.HALF_UP); |
| | | ErpPurchaseOrderCreateReqDTO.Item item = new ErpPurchaseOrderCreateReqDTO.Item(); |
| | | item.setProductId(material.getProductId()); |
| | | item.setProductUnitId(mdmItem.getUnitMeasureId()); |
| | | item.setProductPrice(unitPrice); |
| | | item.setCount(material.getQuantity()); |
| | | item.setTaxPercent(quote.getTaxRate()); |
| | | item.setRemark("定标编号: " + award.getAwardNo()); |
| | | orderItems.add(item); |
| | | } |
| | | |
| | | // 调用 ERP 创建采购订单 |
| | | ErpPurchaseOrderCreateReqDTO createReq = new ErpPurchaseOrderCreateReqDTO(); |
| | | createReq.setSupplierId(supplier.getId()); |
| | | createReq.setOrderTime(LocalDateTime.now()); |
| | | createReq.setDepositPrice(BigDecimal.ZERO); |
| | | createReq.setItems(orderItems); |
| | | createReq.setRemark("SRM 简易招投标自动生成, 定标编号: " + award.getAwardNo()); |
| | | |
| | | Long purchaseOrderId = erpPurchaseOrderApi.createPurchaseOrder(createReq).getCheckedData(); |
| | | |
| | | // 获取采购订单号 |
| | | String purchaseOrderNo = null; |
| | | try { |
| | | purchaseOrderNo = erpPurchaseOrderApi.getPurchaseOrder(purchaseOrderId) |
| | | .getCheckedData().getNo(); |
| | | } catch (Exception e) { |
| | | log.warn("[generatePurchaseOrderForSimple] 获取采购订单号失败,purchaseOrderId={}", purchaseOrderId, e); |
| | | } |
| | | |
| | | // 更新定标记录 |
| | | award.setPurchaseOrderId(purchaseOrderId); |
| | | award.setPurchaseOrderNo(purchaseOrderNo); |
| | | award.setAwardStatus(AwardStatusEnum.ORDER_GENERATED.getCode()); |
| | | bidAwardMapper.updateById(award); |
| | | } |
| | | |
| | | @Override |
| | | @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE, |
| | | bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS) |
| | | public void updateTenderProjectPartial(SrmTenderProjectPartialUpdateReqVO updateReqVO) { |
| | | SrmTenderProjectDO project = validateTenderExists(updateReqVO.getId()); |
| | | if (!TenderStatusEnum.canEditPartialProject(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_PROJECT_LOCKED); |
| | | } |
| | | SrmTenderProjectDO updateObj = new SrmTenderProjectDO(); |
| | | updateObj.setId(updateReqVO.getId()); |
| | | updateObj.setTenderDesc(updateReqVO.getTenderDesc()); |
| | | updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements()); |
| | | tenderProjectMapper.updateById(updateObj); |
| | | LogRecordContext.putVariable("tenderName", project.getTenderName()); |
| | | LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc()); |
| | | LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc()); |
| | | LogRecordContext.putVariable("oldQualification", project.getQualificationRequirements()); |
| | | LogRecordContext.putVariable("newQualification", updateReqVO.getQualificationRequirements()); |
| | | } |
| | | |
| | | // ========== 物料子表 ========== |
| | | |
| | | @Override |
| | | public void createMaterial(SrmTenderMaterialSaveReqVO createReqVO) { |
| | | SrmTenderProjectDO project = validateTenderExists(createReqVO.getTenderProjectId()); |
| | | if (!TenderStatusEnum.canEditMaterial(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED); |
| | | } |
| | | SrmTenderMaterialDO material = BeanUtils.toBean(createReqVO, SrmTenderMaterialDO.class); |
| | | tenderMaterialMapper.insert(material); |
| | | } |
| | | |
| | | @Override |
| | | public void updateMaterial(SrmTenderMaterialSaveReqVO updateReqVO) { |
| | | SrmTenderMaterialDO existing = tenderMaterialMapper.selectById(updateReqVO.getId()); |
| | | if (existing != null) { |
| | | SrmTenderProjectDO project = tenderProjectMapper.selectById(existing.getTenderProjectId()); |
| | | if (project != null && !TenderStatusEnum.canEditMaterial(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED); |
| | | } |
| | | } |
| | | SrmTenderMaterialDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderMaterialDO.class); |
| | | tenderMaterialMapper.updateById(updateObj); |
| | | } |
| | | |
| | | @Override |
| | | public void deleteMaterial(Long id) { |
| | | SrmTenderMaterialDO material = tenderMaterialMapper.selectById(id); |
| | | if (material != null) { |
| | | SrmTenderProjectDO project = tenderProjectMapper.selectById(material.getTenderProjectId()); |
| | | if (project != null && !TenderStatusEnum.canEditMaterial(project.getTenderStatus())) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_MATERIAL_LOCKED); |
| | | } |
| | | } |
| | | tenderMaterialMapper.deleteById(id); |
| | | } |
| | | |