From a378f38df1785cbc8c9f1d55ace6a37952d2f65b Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 10 八月 2026 14:01:56 +0800
Subject: [PATCH] feat(crm): 新增开票功能并完善权限查询

---
 yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java |   49 ++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
index 0c68f1e..0652c25 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -14,6 +14,7 @@
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
@@ -23,9 +24,12 @@
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
+import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceService;
 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.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
 import com.mzt.logapi.starter.annotation.LogRecord;
@@ -72,11 +76,15 @@
     private CrmReceivablePlanService receivablePlanService;
     @Resource
     private CrmPermissionService permissionService;
+    @Resource
+    private CrmInvoiceService invoiceService;
 
     @Resource
     private AdminUserApi adminUserApi;
     @Resource
     private BpmProcessInstanceApi bpmProcessInstanceApi;
+    @Resource
+    private StorageAttachmentApi storageAttachmentApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -85,9 +93,11 @@
     public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
         // 1.1 鏍¢獙鍙洖娆鹃噾棰濊秴杩囦笂闄�
         validateReceivablePriceExceedsLimit(createReqVO);
-        // 1.2 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
+        // 1.2 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃绱宸插紑绁ㄩ噾棰濓紙鍏堝紑绁ㄥ悗鍥炴锛�
+        validateReceivablePriceExceedsInvoiceLimit(createReqVO);
+        // 1.3 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
         validateRelationDataExists(createReqVO);
-        // 1.3 鐢熸垚鍥炴缂栧彿
+        // 1.4 鐢熸垚鍥炴缂栧彿
         String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
         if (receivableMapper.selectByNo(no) != null) {
             throw exception(RECEIVABLE_NO_EXISTS);
@@ -131,6 +141,37 @@
         }
     }
 
+    /**
+     * 鏍¢獙绱鍥炴閲戦涓嶈秴杩囩疮璁″凡寮�绁ㄤ笖宸蹭笂浼犲彂绁ㄩ檮浠剁殑閲戦锛堝厛寮�绁ㄥ悗鍥炴锛�
+     */
+    private void validateReceivablePriceExceedsInvoiceLimit(CrmReceivableSaveReqVO reqVO) {
+        // 1. 璁$畻鍚堝悓绱鍥炴閲戦锛堜笉鍚湰娆★紝缁熻鍙e緞鍚屽悎鍚岄噾棰濇牎楠岋細瀹℃壒閫氳繃 + 瀹℃壒涓級
+        List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
+                Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
+        if (reqVO.getId() != null) {
+            receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
+        }
+        BigDecimal receivablePriceSum = CollectionUtils.getSumValue(receivables, CrmReceivableDO::getPrice,
+                BigDecimal::add, BigDecimal.ZERO);
+
+        // 2. 璁$畻鍚堝悓涓嬨�屽鎵归�氳繃涓斿凡涓婁紶鍙戠エ闄勪欢銆嶇殑绱寮�绁ㄩ噾棰�
+        List<CrmInvoiceDO> invoices = invoiceService.getInvoiceListByContractIdAndStatus(reqVO.getContractId(),
+                List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
+        BigDecimal invoicePriceSum = BigDecimal.ZERO;
+        for (CrmInvoiceDO invoice : invoices) {
+            if (CollUtil.isNotEmpty(storageAttachmentApi.listAttachments(
+                    StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
+                invoicePriceSum = invoicePriceSum.add(invoice.getPrice());
+            }
+        }
+
+        // 3. 鏍¢獙鏈鍥炴閲戦涓嶈秴杩囧墿浣欏彲鍥炴閲戦锛堝凡寮�绁ㄩ噾棰� - 宸插洖娆鹃噾棰濓級
+        BigDecimal availablePrice = invoicePriceSum.subtract(receivablePriceSum);
+        if (reqVO.getPrice().compareTo(availablePrice) > 0) {
+            throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ENOUGH, availablePrice);
+        }
+    }
+
     private void validateRelationDataExists(CrmReceivableSaveReqVO reqVO) {
         if (reqVO.getOwnerUserId() != null) {
             adminUserApi.validateUser(reqVO.getOwnerUserId()); // 鏍¢獙璐熻矗浜哄瓨鍦�
@@ -167,8 +208,10 @@
                 .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 璁剧疆宸插瓨鍦ㄧ殑鍊�
         // 1.2 鏍¢獙鍙洖娆鹃噾棰濊秴杩囦笂闄�
         validateReceivablePriceExceedsLimit(updateReqVO);
+        // 1.3 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃绱宸插紑绁ㄩ噾棰濓紙鍏堝紑绁ㄥ悗鍥炴锛�
+        validateReceivablePriceExceedsInvoiceLimit(updateReqVO);
 
-        // 1.3 鍙湁鑽夌銆佸鎵逛腑锛屽彲浠ョ紪杈戯紱
+        // 1.4 鍙湁鑽夌銆佸鎵逛腑锛屽彲浠ョ紪杈戯紱
         if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                 CrmAuditStatusEnum.PROCESS.getStatus())) {
             throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);

--
Gitblit v1.9.3