From a647bc8a43c0212f482bee0630ebd14dca120a8a Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期一, 28 九月 2026 14:13:28 +0800
Subject: [PATCH] feat: CRM 发票/合同导入、MES 质检批量导入与检测台账、回款与报价校验、权限码细化

---
 yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java |  235 +++++++++++++++++-----------------------------------------
 1 files changed, 71 insertions(+), 164 deletions(-)

diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
index da476ff..a65a717 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
@@ -1,16 +1,10 @@
 package cn.iocoder.yudao.module.crm.service.quotation;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
 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.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO;
@@ -20,17 +14,22 @@
 import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper;
 import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationProductMapper;
 import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
+import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
 import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
 import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
 import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
 import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 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;
@@ -45,17 +44,12 @@
 /**
  * CRM 閿�鍞姤浠峰崟 Service 瀹炵幇绫�
  *
- * @author 鑺嬮亾婧愮爜
+ * @author 瓒呯骇绠$悊鍛�
  */
 @Service
 @Validated
 @Slf4j
 public class CrmSaleQuotationServiceImpl implements CrmSaleQuotationService {
-
-    /**
-     * 閿�鍞姤浠� BPM 鍒嗙被缂栫爜
-     */
-    private static final String SALE_QUOTATION_APPROVE_CATEGORY_CODE = "sale_quotation_approve";
 
     @Resource
     private CrmSaleQuotationMapper saleQuotationMapper;
@@ -71,31 +65,28 @@
     @Resource
     private CrmCustomerService customerService;
     @Resource
+    private CrmCustomerApi customerApi;
+    @Resource
     private CrmBusinessService businessService;
     @Resource
     private CrmContactService contactService;
     @Resource
     private CrmPermissionService permissionService;
     @Resource
+    private StorageAttachmentApi storageAttachmentApi;
+    @Resource
     private CrmContractService contractService;
-
-    @Resource
-    private BpmCategoryService bpmCategoryService;
-    @Resource
-    private BpmProcessDefinitionService bpmProcessDefinitionService;
-    @Resource
-    private BpmProcessInstanceApi bpmProcessInstanceApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createSaleQuotation(CrmSaleQuotationSaveReqVO createReqVO, Long userId) {
         // 1.1 鏍¢獙鐗╂枡椤圭殑鏈夋晥鎬�
         List<CrmSaleQuotationProductDO> quotationProducts = validateQuotationItems(createReqVO.getItems());
-        // 1.2 鐢熸垚鎶ヤ环鍗曞彿锛屽苟鏍¢獙鍞竴鎬�
-        String no = noRedisDAO.generate(CrmNoRedisDAO.SALE_QUOTATION_NO_PREFIX);
-        if (saleQuotationMapper.selectByNo(no) != null) {
-            throw exception(SALE_QUOTATION_NO_EXISTS);
-        }
+        // 1.2 纭畾鎶ヤ环鍗曞彿锛堝墠绔~鍐欏垯鐢ㄨ嚜瀹氫箟鍗曞彿锛岀暀绌哄垯鑷姩鐢熸垚锛夛紝骞舵牎楠屽敮涓�鎬�
+        String customNo = StrUtil.trim(createReqVO.getNo());
+        String no = StrUtil.isBlank(customNo)
+                ? noRedisDAO.generate(CrmNoRedisDAO.SALE_QUOTATION_NO_PREFIX) : customNo;
+        validateQuotationNoUnique(no, null);
         // 1.3 鏍¢獙鍏宠仈瀛楁
         validateRelationDataExists(createReqVO);
 
@@ -109,7 +100,13 @@
         quotationProducts.forEach(p -> p.setQuotationId(saleQuotation.getId()));
         saleQuotationProductMapper.insertBatch(quotationProducts);
         // 2.3 鍒涘缓鏁版嵁鏉冮檺
-        permissionService.createPermission(saleQuotation.getId(), userId);
+        permissionService.createPermission(new CrmPermissionCreateReqBO()
+                .setUserId(userId)
+                .setBizType(CrmBizTypeEnum.CRM_SALE_QUOTATION.getType())
+                .setBizId(saleQuotation.getId())
+                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+        storageAttachmentApi.bindAttachments("file", "crm_sale_quotation",
+                saleQuotation.getId(), createReqVO.getBlobIds());
         return saleQuotation.getId();
     }
 
@@ -127,12 +124,20 @@
         // 1.4 鏍¢獙鍏宠仈瀛楁
         validateRelationDataExists(updateReqVO);
 
-        // 2.1 鏇存柊鎶ヤ环鍗�
+        // 2.1 鏇存柊鎶ヤ环鍗曪紙鎶ヤ环鍗曞彿鐣欑┖琛ㄧず淇濇寔鍘熷�硷紝濉啓鍒欐牎楠屽敮涓�鎬у悗鏇挎崲锛�
         CrmSaleQuotationDO updateObj = BeanUtils.toBean(updateReqVO, CrmSaleQuotationDO.class);
+        String no = StrUtil.trim(updateReqVO.getNo());
+        if (StrUtil.isBlank(no)) {
+            updateObj.setNo(saleQuotation.getNo());
+        } else {
+            validateQuotationNoUnique(no, saleQuotation.getId());
+            updateObj.setNo(no);
+        }
         calculateTotalPrice(updateObj, quotationProducts);
         saleQuotationMapper.updateById(updateObj);
         // 2.2 鏇存柊鎶ヤ环鏄庣粏
         updateQuotationProductList(updateReqVO.getId(), quotationProducts);
+        storageAttachmentApi.updateAttachments("file", "crm_sale_quotation", updateReqVO.getId(), updateReqVO.getBlobIds());
     }
 
     @Override
@@ -151,12 +156,14 @@
 
         // 2. 閬嶅巻鍒犻櫎
         saleQuotations.forEach(saleQuotation -> {
+            // 2.0 鍒犻櫎闄勪欢鍏宠仈
+            storageAttachmentApi.deleteAttachmentsByRecord("crm_sale_quotation", saleQuotation.getId());
             // 2.1 鍒犻櫎鎶ヤ环鍗�
             saleQuotationMapper.deleteById(saleQuotation.getId());
             // 2.2 鍒犻櫎鎶ヤ环鏄庣粏
             saleQuotationProductMapper.deleteByQuotationId(saleQuotation.getId());
             // 2.3 鍒犻櫎鏁版嵁鏉冮檺
-            permissionService.deletePermission(saleQuotation.getId());
+            permissionService.deletePermission(CrmBizTypeEnum.CRM_SALE_QUOTATION.getType(), saleQuotation.getId());
         });
     }
 
@@ -168,9 +175,22 @@
         return saleQuotation;
     }
 
+    /**
+     * 鏍¢獙鎶ヤ环鍗曞彿鍞竴锛堝惈宸查�昏緫鍒犻櫎鐨勬姤浠峰崟锛屽洜 uk_no 鍞竴绱㈠紩瑕嗙洊宸插垹闄よ锛�
+     *
+     * @param no       鎶ヤ环鍗曞彿
+     * @param selfId   鑷韩缂栧彿锛堜慨鏀规椂鎺掗櫎鑷繁锛屾柊澧炰紶 null锛�
+     */
+    private void validateQuotationNoUnique(String no, Long selfId) {
+        CrmSaleQuotationDO exists = saleQuotationMapper.selectAnyByNo(no);
+        if (exists != null && !exists.getId().equals(selfId)) {
+            throw exception(SALE_QUOTATION_NO_EXISTS, no);
+        }
+    }
+
     private List<CrmSaleQuotationProductDO> validateQuotationItems(List<CrmSaleQuotationSaveReqVO.Item> list) {
         // 1. 鏍¢獙鐗╂枡瀛樺湪涓斿惎鐢�
-        List<Long> itemIds = convertSet(list, CrmSaleQuotationSaveReqVO.Item::getItemId);
+        Set<Long> itemIds = convertSet(list, CrmSaleQuotationSaveReqVO.Item::getItemId);
         Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds);
         // 2. 杞崲涓� CrmSaleQuotationProductDO 鍒楄〃
         return convertList(list, o -> BeanUtils.toBean(o, CrmSaleQuotationProductDO.class, item -> {
@@ -179,7 +199,8 @@
                 throw exception(SALE_QUOTATION_ITEM_NOT_EXISTS);
             }
             item.setItemUnitId(mdmItem.getUnitMeasureId());
-            item.setItemPrice(mdmItem.getSalesPrice());
+            // 鐗╂枡鍘熶环锛屽鏋� MDM 涓病鏈夊垯榛樿涓� 0
+            item.setItemPrice(mdmItem.getSalesPrice() != null ? mdmItem.getSalesPrice() : BigDecimal.ZERO);
             if (item.getQuotationPrice() != null && item.getCount() != null) {
                 item.setTotalPrice(MoneyUtils.priceMultiply(item.getQuotationPrice(), item.getCount()));
                 if (item.getTaxPercent() != null) {
@@ -223,12 +244,13 @@
 
     private void calculateTotalPrice(CrmSaleQuotationDO saleQuotation, List<CrmSaleQuotationProductDO> items) {
         saleQuotation.setTotalProductPrice(getSumValue(items, CrmSaleQuotationProductDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
-        saleQuotation.setTotalTaxPrice(getSumValue(items, CrmSaleQuotationProductDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        BigDecimal totalTaxPrice = getSumValue(items, CrmSaleQuotationProductDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO);
+        saleQuotation.setTaxPrice(totalTaxPrice);
         // 璁$畻浼樻儬浠锋牸
         if (saleQuotation.getDiscountPercent() == null) {
             saleQuotation.setDiscountPercent(BigDecimal.ZERO);
         }
-        BigDecimal basePrice = saleQuotation.getTotalProductPrice().add(saleQuotation.getTotalTaxPrice());
+        BigDecimal basePrice = saleQuotation.getTotalProductPrice().add(totalTaxPrice);
         saleQuotation.setDiscountPrice(MoneyUtils.priceMultiplyPercent(basePrice, saleQuotation.getDiscountPercent()));
         saleQuotation.setTotalPrice(basePrice.subtract(saleQuotation.getDiscountPrice()));
     }
@@ -240,7 +262,9 @@
 
     @Override
     public PageResult<CrmSaleQuotationDO> getSaleQuotationPage(CrmSaleQuotationPageReqVO pageReqVO, Long userId) {
-        return saleQuotationMapper.selectPage(pageReqVO, userId);
+        // 鑾峰彇鐢ㄦ埛鏈夋潈闄愮殑瀹㈡埛 ID 鍒楄〃
+        List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds();
+        return saleQuotationMapper.selectPage(pageReqVO, userId, permittedCustomerIds);
     }
 
     @Override
@@ -268,138 +292,11 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void submitSaleQuotation(Long id, String processDefinitionKey, Long userId) {
+    public Long convertToContract(Long id, Long userId) {
         // 1. 鏍¢獙鎶ヤ环鍗曞瓨鍦ㄤ笖涓鸿崏绋跨姸鎬�
         CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id);
         if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus())) {
-            throw exception(SALE_QUOTATION_SUBMIT_FAIL_NOT_DRAFT);
-        }
-
-        // 2. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
-        validateSaleQuotationApproveCategoryAndProcessDefinition();
-
-        // 3. 鏍¢獙浼犲叆鐨勬祦绋嬪畾涔� Key 鏄惁灞炰簬璇ュ垎绫讳笖澶勪簬婵�娲荤姸鎬�
-        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
-        if (processDefinition == null) {
-            throw exception(SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
-        }
-
-        // 4. 鍒涘缓娴佺▼瀹炰緥
-        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId,
-                new BpmProcessInstanceCreateReqDTO()
-                        .setProcessDefinitionKey(processDefinitionKey)
-                        .setBusinessKey(String.valueOf(id)));
-
-        // 5. 鏇存柊鎶ヤ环鍗曠姸鎬�
-        saleQuotationMapper.updateById(new CrmSaleQuotationDO()
-                .setId(id)
-                .setProcessInstanceId(processInstanceId)
-                .setStatus(CrmSaleQuotationStatusEnum.PROCESS.getStatus()));
-    }
-
-    /**
-     * 鏍¢獙 BPM 閿�鍞姤浠峰鎵瑰垎绫诲拰娴佺▼瀹氫箟鏄惁瀛樺湪
-     */
-    private void validateSaleQuotationApproveCategoryAndProcessDefinition() {
-        // 1. 鏍¢獙鍒嗙被鏄惁瀛樺湪
-        List<String> codes = Collections.singletonList(SALE_QUOTATION_APPROVE_CATEGORY_CODE);
-        Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
-        if (!categoryMap.containsKey(SALE_QUOTATION_APPROVE_CATEGORY_CODE)) {
-            throw exception(SALE_QUOTATION_BPM_CATEGORY_NOT_EXISTS);
-        }
-
-        // 2. 鏍¢獙鍒嗙被涓嬫槸鍚︽湁鍙敤鐨勬祦绋嬪畾涔�
-        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
-                .getProcessDefinitionInfoListByCategory(SALE_QUOTATION_APPROVE_CATEGORY_CODE);
-        if (CollUtil.isEmpty(definitionInfoList)) {
-            throw exception(SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
-        }
-
-        // 3. 鏍¢獙鏄惁鏈夋縺娲荤姸鎬佺殑娴佺▼瀹氫箟
-        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
-        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
-        boolean hasActiveDefinition = processDefinitions.stream()
-                .anyMatch(pd -> !pd.isSuspended());
-        if (!hasActiveDefinition) {
-            throw exception(SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
-        }
-    }
-
-    @Override
-    public void updateSaleQuotationAuditStatus(Long id, Integer bpmResult) {
-        // 1. 鏍¢獙鎶ヤ环鍗曞瓨鍦ㄤ笖涓哄鎵逛腑鐘舵��
-        CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id);
-        if (!CrmSaleQuotationStatusEnum.PROCESS.getStatus().equals(saleQuotation.getStatus())) {
-            log.error("[updateSaleQuotationAuditStatus] 鎶ヤ环鍗�({}) 涓嶅浜庡鎵逛腑", id);
-            throw exception(SALE_QUOTATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
-        }
-
-        // 2. 杞崲瀹℃壒缁撴灉
-        Integer auditStatus = convertBpmResultToAuditStatus(bpmResult);
-        saleQuotationMapper.updateById(new CrmSaleQuotationDO()
-                .setId(id)
-                .setStatus(auditStatus));
-    }
-
-    /**
-     * BPM 瀹℃壒缁撴灉杞崲
-     */
-    private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
-        Integer auditStatus = BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)
-                ? CrmSaleQuotationStatusEnum.APPROVE.getStatus()
-                : BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)
-                ? CrmSaleQuotationStatusEnum.REJECT.getStatus()
-                : BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)
-                ? CrmSaleQuotationStatusEnum.CANCEL.getStatus() : null;
-        Assert.notNull(auditStatus, "BPM 瀹℃壒缁撴灉({}) 杞崲澶辫触", bpmResult);
-        return auditStatus;
-    }
-
-    @Override
-    public List<Map<String, Object>> getSaleQuotationApproveProcessDefinitionList() {
-        // 1. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
-        validateSaleQuotationApproveCategoryAndProcessDefinition();
-
-        // 2. 鑾峰彇鍒嗙被涓嬬殑娴佺▼瀹氫箟淇℃伅
-        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
-                .getProcessDefinitionInfoListByCategory(SALE_QUOTATION_APPROVE_CATEGORY_CODE);
-
-        // 3. 鑾峰彇娴佺▼瀹氫箟璇︽儏
-        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
-        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
-
-        // 4. 杩囨护锛氬彧淇濈暀婵�娲荤姸鎬侊紝涓旂浉鍚� key 鍙繚鐣欐渶鏂扮増鏈�
-        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
-        for (ProcessDefinition pd : processDefinitions) {
-            if (pd.isSuspended()) {
-                continue;
-            }
-            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
-            if (existing == null || pd.getVersion() > existing.getVersion()) {
-                latestVersionMap.put(pd.getKey(), pd);
-            }
-        }
-
-        // 5. 杩斿洖娴佺▼瀹氫箟鍒楄〃
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (ProcessDefinition pd : latestVersionMap.values()) {
-            Map<String, Object> item = new HashMap<>();
-            item.put("id", pd.getId());
-            item.put("key", pd.getKey());
-            item.put("name", pd.getName());
-            item.put("version", pd.getVersion());
-            result.add(item);
-        }
-        return result;
-    }
-
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public Long convertToContract(Long id, Long userId) {
-        // 1. 鏍¢獙鎶ヤ环鍗曞瓨鍦ㄤ笖宸插鎵归�氳繃
-        CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id);
-        if (!CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus())) {
-            throw exception(SALE_QUOTATION_NOT_APPROVE);
+            throw exception(SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT, saleQuotation.getNo());
         }
         // 鏍¢獙鏄惁宸茶浆鍚堝悓
         if (saleQuotation.getContractId() != null) {
@@ -429,7 +326,7 @@
         });
         contractVO.setProducts(contractProducts);
 
-        // 4. 鍒涘缓鍚堝悓
+        // 4. 鍒涘缓鍚堝悓锛堜細鑷姩妫�娴嬪悎鍚屽鎵规祦绋嬪苟鎻愪氦瀹℃壒锛�
         Long contractId = contractService.createContract(contractVO, userId);
 
         // 5. 鏇存柊鎶ヤ环鍗曞叧鑱斾俊鎭�
@@ -443,4 +340,14 @@
         return contractId;
     }
 
+    @Override
+    public CrmSaleQuotationDO getByContractId(Long id) {
+        return saleQuotationMapper.selectOne(new LambdaQueryWrapper<CrmSaleQuotationDO>().eq(CrmSaleQuotationDO::getContractId,id));
+    }
+
+    @Override
+    public void clearContractReference(Long quotationId) {
+        saleQuotationMapper.clearContractReference(quotationId);
+    }
+
 }
\ No newline at end of file

--
Gitblit v1.9.3