liu
2 天以前 4bc513be11e671caf20ec6013a51dee42c2ed96e
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;
@@ -23,10 +24,16 @@
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
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.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi;
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;
@@ -65,6 +72,11 @@
    private static final String INVOICE_APPROVE_BIZ_TYPE = "crm_invoice_approve";
    /**
     * CRM 开票审批的 BPM 流程定义 Key
     */
    private static final String INVOICE_APPROVE_PROCESS_KEY = "crm-invoice-audit";
    /**
     * 发票附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#CRM_INVOICE}
     */
    private static final String INVOICE_RECORD_TYPE = "crm_invoice";
@@ -79,6 +91,8 @@
    private CrmContractService contractService;
    @Resource
    private CrmPermissionService permissionService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private AdminUserApi adminUserApi;
@@ -86,16 +100,17 @@
    private ApprovalConfigApi approvalConfigApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_CREATE_SUB_TYPE, bizNo = "{{#invoice.id}}",
            success = CRM_INVOICE_CREATE_SUCCESS)
    public Long createInvoice(CrmInvoiceSaveReqVO createReqVO) {
        // 1.1 校验可开票金额超过上限
        validateInvoicePriceExceedsLimit(createReqVO);
        // 1.2 校验关联数据存在
        // 1.1 校验关联数据存在(开票金额不再受合同金额限制,仅前端提示可开票金额)
        validateRelationDataExists(createReqVO);
        validateContractItemsInvoiceable(createReqVO.getContractId());
        // 1.3 生成开票编号
        String no = noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
        if (invoiceMapper.selectByNo(no) != null) {
@@ -122,22 +137,6 @@
        return invoice.getId();
    }
    private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
        // 1. 计算剩余可开票金额,不包括 reqVO 自身(审批中也占用额度,防止累计开票超过合同金额)
        CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
        List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
                List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
        if (reqVO.getId() != null) {
            invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
        }
        BigDecimal notInvoicePrice = contract.getTotalPrice().subtract(
                CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
        // 2. 校验金额是否超过
        if (reqVO.getPrice().compareTo(notInvoicePrice) > 0) {
            throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notInvoicePrice);
        }
    }
    private void validateRelationDataExists(CrmInvoiceSaveReqVO reqVO) {
        if (reqVO.getOwnerUserId() != null) {
            adminUserApi.validateUser(reqVO.getOwnerUserId()); // 校验负责人存在
@@ -148,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);
        }
    }
@@ -163,12 +185,9 @@
        CrmInvoiceDO oldInvoice = validateInvoiceExists(updateReqVO.getId());
        updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()).setCustomerId(oldInvoice.getCustomerId())
                .setContractId(oldInvoice.getContractId()); // 设置已存在的值
        // 1.2 校验可开票金额超过上限
        validateInvoicePriceExceedsLimit(updateReqVO);
        // 1.3 只有草稿、审批中,可以编辑;
        // 1.2 只有草稿、审核不通过,可以编辑;
        if (!ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus())) {
                CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED);
        }
@@ -213,6 +232,7 @@
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
            success = CRM_INVOICE_SUBMIT_SUCCESS)
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
    public void submitInvoice(Long id) {
        // 1. 校验存在 + 可提交状态(未提交 / 审核不通过)
        CrmInvoiceDO invoice = validateInvoiceExists(id);
@@ -220,18 +240,23 @@
                && ObjUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(INVOICE_SUBMIT_FAIL_NOT_EDITABLE);
        }
        validateContractItemsInvoiceable(invoice.getContractId());
        // 2. 重新校验可开票金额,防止累计开票超过合同金额(草稿期后可能有其他单进入审批中)
        validateInvoicePriceExceedsLimit(new CrmInvoiceSaveReqVO()
                .setId(invoice.getId()).setContractId(invoice.getContractId()).setPrice(invoice.getPrice()));
        // 3. 校验审批配置已启用且配置了有效审批人(未配置时抛出带明确提示的业务异常)
        // 2. 校验审批配置已启用且配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(INVOICE_APPROVE_BIZ_TYPE);
        // 4. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        invoiceMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus());
        if (invoiceMapper.submitAndResetAuditInfo(id, CrmAuditStatusEnum.PROCESS.getStatus()) != 1) {
            throw exception(INVOICE_SUBMIT_FAIL_NOT_EDITABLE);
        }
        // 5. 记录日志
        // 5. 创建 BPM 流程实例,使审批出现在「协同办公 - 我的流程」
        bpmProcessInstanceApi.createProcessInstance(getLoginUserId(),
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(INVOICE_APPROVE_PROCESS_KEY)
                        .setBusinessKey(String.valueOf(id)));
        // 6. 记录日志
        LogRecordContext.putVariable("invoiceNo", invoice.getNo());
    }
@@ -257,14 +282,37 @@
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(INVOICE_AUDIT_REJECT_REASON_REQUIRED);
            }
            invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            if (invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark) != 1) {
                throw exception(INVOICE_AUDIT_FAIL_NOT_PROCESS);
            }
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        //    (开票无额外后置业务逻辑,审批通过后即可用于创建回款 / 关联开票选择)
        invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        if (invoiceMapper.auditInvoice(id, CrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark) != 1) {
            throw exception(INVOICE_AUDIT_FAIL_NOT_PROCESS);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateInvoiceAuditStatus(Long id, Integer bpmStatus, String reason) {
        // BPM 审批结果映射为开票审核状态:通过=20 / 不通过=30 / 取消=40
        Integer auditStatus;
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmStatus)) {
            auditStatus = CrmAuditStatusEnum.APPROVE.getStatus();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmStatus)) {
            auditStatus = CrmAuditStatusEnum.REJECT.getStatus();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmStatus)) {
            auditStatus = CrmAuditStatusEnum.CANCEL.getStatus();
        } else {
            return;
        }
        if (invoiceMapper.updateAuditStatusByBpm(id, auditStatus, reason) != 1) {
            log.warn("[updateInvoiceAuditStatus] 开票({}) 不处于审批中状态,忽略 BPM 结果({})", id, bpmStatus);
        }
    }
    /**
@@ -273,8 +321,7 @@
     * @param userId 用户编号
     * @return 昵称,用户不存在时返回 null
     */
    private String getUserNickname(Long userId) {
        if (userId == null) {
    private String getUserNickname(Long userId) {        if (userId == null) {
            return null;
        }
        AdminUserRespDTO user = adminUserApi.getUser(userId);