liu
17 小时以前 4bc513be11e671caf20ec6013a51dee42c2ed96e
fix(crm): 禁止物料类合同开票
已添加1个文件
已修改2个文件
92 ■■■■■ 文件已修改
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImplTest.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -145,6 +145,7 @@
    ErrorCode INVOICE_SUBMIT_FAIL_NOT_EDITABLE = new ErrorCode(1_020_016_004, "只有草稿或审核不通过状态的开票才能提交审批");
    ErrorCode INVOICE_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_020_016_005, "只有审批中状态的开票才能审核");
    ErrorCode INVOICE_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_020_016_013, "审核不通过时必须填写不通过原因");
    ErrorCode INVOICE_CREATE_FAIL_MATERIAL_NOT_ALLOWED = new ErrorCode(1_020_016_014, "物料不允许开票");
    ErrorCode INVOICE_CREATE_FAIL_CONTRACT_NOT_APPROVE = new ErrorCode(1_020_016_006, "创建开票失败,原因:合同不是审核通过状态");
    ErrorCode INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_020_016_007, "创建开票失败,原因:开票金额超出合同金额,目前可开票:{} å…ƒ");
    ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_NOT_EXISTS = new ErrorCode(1_020_016_008, "创建回款失败,原因:关联的开票不存在");
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.dal.mysql.invoice.CrmInvoiceMapper;
import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
@@ -30,6 +31,9 @@
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
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.enums.MdmItemTypeEnum;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
@@ -87,6 +91,8 @@
    private CrmContractService contractService;
    @Resource
    private CrmPermissionService permissionService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private AdminUserApi adminUserApi;
@@ -104,6 +110,7 @@
    public Long createInvoice(CrmInvoiceSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒå…³è”数据存在(开票金额不再受合同金额限制,仅前端提示可开票金额)
        validateRelationDataExists(createReqVO);
        validateContractItemsInvoiceable(createReqVO.getContractId());
        // 1.3 ç”Ÿæˆå¼€ç¥¨ç¼–号
        String no = noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
        if (invoiceMapper.selectByNo(no) != null) {
@@ -140,6 +147,29 @@
                throw exception(INVOICE_CREATE_FAIL_CONTRACT_NOT_APPROVE);
            }
            reqVO.setCustomerId(contract.getCustomerId()); // è®¾ç½®å®¢æˆ·ç¼–号
        }
    }
    /**
     * åˆåŒä¸­åŒ…含物料时不允许开票,只有产品类销售内容才可进入开票流程。
     */
    void validateContractItemsInvoiceable(Long contractId) {
        if (contractId == null) {
            return;
        }
        List<CrmContractProductDO> contractProducts = contractService.getContractProductListByContractId(contractId);
        if (CollUtil.isEmpty(contractProducts)) {
            return;
        }
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                CollectionUtils.convertSet(contractProducts, CrmContractProductDO::getItemId));
        boolean containsMaterial = contractProducts.stream()
                .map(CrmContractProductDO::getItemId)
                .map(itemMap::get)
                .filter(ObjectUtil::isNotNull)
                .anyMatch(item -> MdmItemTypeEnum.MATERIAL.getType().equals(item.getItemType()));
        if (containsMaterial) {
            throw exception(INVOICE_CREATE_FAIL_MATERIAL_NOT_ALLOWED);
        }
    }
@@ -210,6 +240,7 @@
                && ObjUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(INVOICE_SUBMIT_FAIL_NOT_EDITABLE);
        }
        validateContractItemsInvoiceable(invoice.getContractId());
        // 2. æ ¡éªŒå®¡æ‰¹é…ç½®å·²å¯ç”¨ä¸”配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(INVOICE_APPROVE_BIZ_TYPE);
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImplTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.crm.service.invoice;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractProductDO;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
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.enums.MdmItemTypeEnum;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CrmInvoiceServiceImplTest {
    @Mock
    private CrmContractService contractService;
    @Mock
    private MdmItemApi mdmItemApi;
    @InjectMocks
    private CrmInvoiceServiceImpl invoiceService;
    @Test
    void validateContractItemsInvoiceable_rejectsMaterial() {
        mockContractItem(MdmItemTypeEnum.MATERIAL);
        ServiceException exception = assertThrows(ServiceException.class,
                () -> invoiceService.validateContractItemsInvoiceable(1L));
        assertEquals("物料不允许开票", exception.getMessage());
    }
    @Test
    void validateContractItemsInvoiceable_allowsProduct() {
        mockContractItem(MdmItemTypeEnum.PRODUCT);
        assertDoesNotThrow(() -> invoiceService.validateContractItemsInvoiceable(1L));
    }
    private void mockContractItem(MdmItemTypeEnum itemType) {
        CrmContractProductDO contractProduct = CrmContractProductDO.builder().itemId(10L).build();
        MdmItemRespDTO item = new MdmItemRespDTO();
        item.setId(10L);
        item.setItemType(itemType.getType());
        when(contractService.getContractProductListByContractId(1L)).thenReturn(List.of(contractProduct));
        when(mdmItemApi.getItemMap(Set.of(10L))).thenReturn(Map.of(10L, item));
    }
}