| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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"; |
| | |
| | | private CrmContractService contractService; |
| | | @Resource |
| | | private CrmPermissionService permissionService; |
| | | @Resource |
| | | private MdmItemApi mdmItemApi; |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | |
| | | 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) { |
| | |
| | | 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()); // 校验负责人存在 |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | |
| | |
| | | @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); |
| | |
| | | && 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()); |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | * @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); |