From 9fceeaad9af96bc58ba714560814c6e94f8af406 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 10 八月 2026 17:13:01 +0800
Subject: [PATCH] feat(crm): 增强合同开票与回款功能
---
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 102 insertions(+), 8 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 642c8e6..f655977 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,22 +76,30 @@
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)
@LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_CREATE_SUB_TYPE, bizNo = "{{#receivable.id}}",
success = CRM_RECEIVABLE_CREATE_SUCCESS)
public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
- // 1.1 鏍¢獙鍙洖娆鹃噾棰濊秴杩囦笂闄�
- validateReceivablePriceExceedsLimit(createReqVO);
- // 1.2 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
+ // 1.1 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
validateRelationDataExists(createReqVO);
- // 1.3 鐢熸垚鍥炴缂栧彿
+ // 1.2 鏍¢獙鍏宠仈寮�绁ㄥ悎鏍硷紙瀹℃壒閫氳繃 + 宸蹭笂浼犲彂绁ㄩ檮浠� + 涓庡悎鍚屽尮閰嶏級
+ validateInvoiceForReceivable(createReqVO);
+ // 1.3 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃鍚堝悓鍓╀綑鍙洖娆鹃噾棰�
+ validateReceivablePriceExceedsLimit(createReqVO);
+ // 1.4 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃璇ュ紑绁ㄥ墿浣欏彲鍥炴閲戦锛堝厛寮�绁ㄥ悗鍥炴锛�
+ validateReceivablePriceExceedsInvoiceRemaining(createReqVO);
+ // 1.5 鐢熸垚鍥炴缂栧彿
String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
if (receivableMapper.selectByNo(no) != null) {
throw exception(RECEIVABLE_NO_EXISTS);
@@ -118,8 +130,10 @@
private void validateReceivablePriceExceedsLimit(CrmReceivableSaveReqVO reqVO) {
// 1. 璁$畻鍓╀綑鍙��娆鹃噾棰濓紝涓嶅寘鎷� reqVO 鑷韩
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
+ // 缁熻鍙e緞锛氳崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛堜笌寮�绁ㄧ骇鏍¢獙銆佺粺璁℃柟娉� selectReceivablePriceMapByContractId 淇濇寔涓�鑷达級
List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
- Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
+ Arrays.asList(CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus(),
+ CrmAuditStatusEnum.APPROVE.getStatus()));
if (reqVO.getId() != null) {
receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
}
@@ -128,6 +142,56 @@
// 2. 鏍¢獙閲戦鏄惁瓒呰繃
if (reqVO.getPrice().compareTo(notReceivablePrice) > 0) {
throw exception(RECEIVABLE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notReceivablePrice);
+ }
+ }
+
+ /**
+ * 鏍¢獙鍏宠仈寮�绁ㄥ悎鏍硷細瀛樺湪銆佸鎵归�氳繃銆佸凡涓婁紶鍙戠エ闄勪欢銆佷笌鎵�閫夊悎鍚屼竴鑷�
+ */
+ private void validateInvoiceForReceivable(CrmReceivableSaveReqVO reqVO) {
+ // 1. 鏍¢獙寮�绁ㄥ瓨鍦�
+ CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId());
+ if (invoice == null) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_EXISTS);
+ }
+ // 2. 鏍¢獙寮�绁ㄥ鎵归�氳繃
+ if (ObjectUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_APPROVE);
+ }
+ // 3. 鏍¢獙寮�绁ㄥ凡涓婁紶鍙戠エ闄勪欢
+ if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(
+ StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ATTACHMENT);
+ }
+ // 4. 鏍¢獙寮�绁ㄤ笌鎵�閫夊悎鍚屼竴鑷�
+ if (ObjectUtil.notEqual(invoice.getContractId(), reqVO.getContractId())) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH);
+ }
+ // 5. 鏍¢獙寮�绁ㄤ笌鎵�閫夊悎鍚屽鎴蜂竴鑷达紙闃插尽鑴忔暟鎹細鍚堝悓-寮�绁�-鍥炴涓�瀵瑰瀵瑰锛屽紑绁ㄥ繀椤诲睘浜庤鍚堝悓瀹㈡埛锛�
+ if (reqVO.getCustomerId() != null && ObjectUtil.notEqual(invoice.getCustomerId(), reqVO.getCustomerId())) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH);
+ }
+ }
+
+ /**
+ * 鏍¢獙绱鍥炴閲戦涓嶈秴杩囪寮�绁ㄥ墿浣欏彲鍥炴閲戦锛堝厛寮�绁ㄥ悗鍥炴锛�
+ */
+ private void validateReceivablePriceExceedsInvoiceRemaining(CrmReceivableSaveReqVO reqVO) {
+ // 1. 璁$畻璇ュ紑绁ㄧ疮璁″洖娆鹃噾棰濓紙涓嶅惈鏈锛岀粺璁″彛寰勫悓鍚堝悓閲戦鏍¢獙锛氳崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛�
+ CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId());
+ List<CrmReceivableDO> receivables = receivableMapper.selectListByInvoiceIdAndStatus(reqVO.getInvoiceId(),
+ Arrays.asList(CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus(),
+ CrmAuditStatusEnum.APPROVE.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. 鏍¢獙鏈鍥炴閲戦涓嶈秴杩囪寮�绁ㄥ墿浣欏彲鍥炴閲戦锛堝紑绁ㄩ噾棰� - 宸插洖娆鹃噾棰濓級
+ BigDecimal availablePrice = invoice.getPrice().subtract(receivablePriceSum);
+ if (reqVO.getPrice().compareTo(availablePrice) > 0) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_PRICE_EXCEEDS, availablePrice);
}
}
@@ -160,15 +224,23 @@
@CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
public void updateReceivable(CrmReceivableSaveReqVO updateReqVO) {
Assert.notNull(updateReqVO.getId(), "鍥炴缂栧彿涓嶈兘涓虹┖");
- updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 涓嶅厑璁镐慨鏀圭殑瀛楁
+ updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 涓嶅厑璁镐慨鏀圭殑瀛楁锛坕nvoiceId 鍏佽淇敼锛�
// 1.1 鏍¢獙瀛樺湪
CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId());
updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId())
.setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 璁剧疆宸插瓨鍦ㄧ殑鍊�
- // 1.2 鏍¢獙鍙洖娆鹃噾棰濊秴杩囦笂闄�
+ // 1.2 鑻ユ湭浼犻�掑叧鑱斿紑绁紝娌跨敤鏃у��
+ if (updateReqVO.getInvoiceId() == null) {
+ updateReqVO.setInvoiceId(oldReceivable.getInvoiceId());
+ }
+ // 1.3 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃鍚堝悓鍓╀綑鍙洖娆鹃噾棰�
validateReceivablePriceExceedsLimit(updateReqVO);
+ // 1.4 鏍¢獙鍏宠仈寮�绁ㄥ悎鏍�
+ validateInvoiceForReceivable(updateReqVO);
+ // 1.5 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃璇ュ紑绁ㄥ墿浣欏彲鍥炴閲戦
+ validateReceivablePriceExceedsInvoiceRemaining(updateReqVO);
- // 1.3 鍙湁鑽夌銆佸鎵逛腑锛屽彲浠ョ紪杈戯紱
+ // 1.6 鍙湁鑽夌銆佸鎵逛腑锛屽彲浠ョ紪杈戯紱
if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
@@ -303,8 +375,30 @@
}
@Override
+ public Map<Long, BigDecimal> getReceivablePriceMapByInvoiceId(Collection<Long> invoiceIds) {
+ return receivableMapper.selectReceivablePriceMapByInvoiceId(invoiceIds);
+ }
+
+ @Override
public Long getReceivableCountByContractId(Long contractId) {
return receivableMapper.selectCountByContractId(contractId);
}
+ @Override
+ public BigDecimal getReceivablePriceByContractId(Long contractId) {
+ Map<Long, BigDecimal> priceMap = receivableMapper.selectReceivablePriceMapByContractId(
+ java.util.Collections.singleton(contractId));
+ return priceMap.getOrDefault(contractId, BigDecimal.ZERO);
+ }
+
+ @Override
+ public BigDecimal getReceivablePriceSum() {
+ return receivableMapper.selectReceivablePriceSum();
+ }
+
+ @Override
+ public BigDecimal getReceivablePriceByOwnerUserId(Long ownerUserId) {
+ return receivableMapper.selectReceivablePriceSumByOwnerUserId(ownerUserId);
+ }
+
}
--
Gitblit v1.9.3