| | |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | 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.BpmProcessInstanceStatusEnum; |
| | | 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 com.mzt.logapi.service.impl.DiffParseFunction; |
| | | import com.mzt.logapi.starter.annotation.LogRecord; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.context.annotation.Lazy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | */ |
| | | @Service |
| | | @Validated |
| | | @Slf4j |
| | | public class CrmReceivableServiceImpl implements CrmReceivableService { |
| | | |
| | | /** |
| | | * BPM 回款审批流程标识 |
| | | * |
| | | * 对应 act_re_procdef.KEY_ = crm-receivable-audit;BPM 分类「回款审批」的编码为 1111(bpm_category.id=76) |
| | | */ |
| | | public static final String BPM_PROCESS_DEFINITION_KEY = "crm-receivable-audit"; |
| | | |
| | | @Resource |
| | | private CrmReceivableMapper receivableMapper; |
| | |
| | | private AdminUserApi adminUserApi; |
| | | @Resource |
| | | private StorageAttachmentApi storageAttachmentApi; |
| | | @Resource |
| | | private BpmProcessInstanceApi bpmProcessInstanceApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | throw exception(RECEIVABLE_NO_EXISTS); |
| | | } |
| | | |
| | | // 2.1 插入回款(创建后即为未审核状态) |
| | | // 2.1 插入回款(创建后为未提交,需显式「提交审批」才进入 BPM 审批中) |
| | | CrmReceivableDO receivable = BeanUtils.toBean(createReqVO, CrmReceivableDO.class) |
| | | .setNo(no).setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()); |
| | | .setNo(no).setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus()); |
| | | receivableMapper.insert(receivable); |
| | | // 2.2 保存核销明细(未传时按关联开票自动生成一条) |
| | | syncReceivableItems(receivable, createReqVO.getItems()); |
| | |
| | | * 校验关联开票合格:存在、审批通过、已上传发票附件、与所选合同一致 |
| | | */ |
| | | private void validateInvoiceForReceivable(CrmReceivableSaveReqVO reqVO) { |
| | | // 未关联开票时直接跳过(允许先回款后开票) |
| | | // 回款必须先关联已审批且已录入的开票单。 |
| | | if (reqVO.getInvoiceId() == null) { |
| | | return; |
| | | throw new ServiceException(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_EXISTS.getCode(), |
| | | "回款必须先关联开票单。请先完成开票申请、审批和发票录入,再创建回款。"); |
| | | } |
| | | // 1. 校验开票存在 |
| | | CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId()); |
| | |
| | | // 2. 校验开票审批通过 |
| | | if (ObjectUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) { |
| | | throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_APPROVE); |
| | | } |
| | | if (!Objects.equals(invoice.getEntryStatus(), 10)) { |
| | | throw new ServiceException(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_APPROVE.getCode(), |
| | | "关联开票单尚未录入发票。请先填写发票号码、日期并上传附件,再创建回款。"); |
| | | } |
| | | // 3. 校验开票已上传发票附件 |
| | | if (CollUtil.isEmpty(storageAttachmentApi.listAttachments( |
| | |
| | | // 1.5 校验可回款金额不超过该开票剩余可回款金额 |
| | | validateReceivablePriceExceedsInvoiceRemaining(updateReqVO); |
| | | |
| | | // 1.6 只有未审核,可以编辑; |
| | | if (ObjectUtil.notEqual(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { |
| | | throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED); |
| | | // 1.6 仅未提交、审核不通过可以编辑;审批中等待 BPM,审核通过需先反审核 |
| | | if (ObjectUtil.notEqual(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus()) |
| | | && ObjectUtil.notEqual(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) { |
| | | throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED, |
| | | StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(oldReceivable.getAuditStatus()), "未知状态")); |
| | | } |
| | | |
| | | // 2. 更新回款 |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_SUBMIT_SUB_TYPE, bizNo = "{{#id}}", |
| | | success = CRM_RECEIVABLE_SUBMIT_SUCCESS) |
| | | public void submitReceivable(Long id, String remark, Long userId) { |
| | | // 1.1 校验回款存在 |
| | | CrmReceivableDO receivable = validateReceivableExists(id); |
| | | // 1.2 仅未提交、审核不通过可以提交审批 |
| | | if (ObjectUtil.notEqual(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus()) |
| | | && ObjectUtil.notEqual(receivable.getAuditStatus(), CrmAuditStatusEnum.REJECT.getStatus())) { |
| | | throw exception(RECEIVABLE_SUBMIT_FAIL_NOT_DRAFT, |
| | | StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(receivable.getAuditStatus()), "未知状态")); |
| | | } |
| | | |
| | | // 1.3 提交前重新校验开票、合同/开票剩余可回款金额,避免草稿期间额度被其他单据占用 |
| | | CrmReceivableSaveReqVO submitReqVO = new CrmReceivableSaveReqVO() |
| | | .setId(receivable.getId()).setCustomerId(receivable.getCustomerId()) |
| | | .setContractId(receivable.getContractId()).setInvoiceId(receivable.getInvoiceId()) |
| | | .setPrice(receivable.getPrice()); |
| | | validateInvoiceForReceivable(submitReqVO); |
| | | validateReceivablePriceExceedsLimit(submitReqVO); |
| | | validateReceivablePriceExceedsInvoiceRemaining(submitReqVO); |
| | | |
| | | // 2. 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型 |
| | | if (bpmProcessInstanceApi.isGlobalAutoApprove()) { |
| | | receivableMapper.updateById(new CrmReceivableDO().setId(id).setProcessInstanceId("") |
| | | .setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus())); |
| | | updateReceivableAuditStatus(id, BpmProcessInstanceStatusEnum.APPROVE.getStatus()); |
| | | LogRecordContext.putVariable("receivableNo", receivable.getNo()); |
| | | return; |
| | | } |
| | | |
| | | // 3. 创建 BPM 回款审批流程实例 |
| | | Map<String, Object> variables = new HashMap<>(); |
| | | if (StrUtil.isNotBlank(remark)) { |
| | | variables.put("submitRemark", remark); |
| | | } |
| | | String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY) |
| | | .setBusinessKey(String.valueOf(id)) |
| | | .setVariables(variables.isEmpty() ? null : variables)); |
| | | |
| | | // 4. 更新工作流编号 + 审批状态 |
| | | receivableMapper.updateById(new CrmReceivableDO().setId(id) |
| | | .setProcessInstanceId(processInstanceId) |
| | | .setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus())); |
| | | |
| | | // 5. 记录操作日志上下文 |
| | | LogRecordContext.putVariable("receivableNo", receivable.getNo()); |
| | | LogRecordContext.putVariable("submitRemark", remark); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateReceivableAuditStatus(Long id, Integer bpmResult) { |
| | | // 1.1 校验存在:回款可能在审批期间被删除,此时静默忽略,避免影响 BPM 流程完成 |
| | | CrmReceivableDO receivable = receivableMapper.selectById(id); |
| | | if (receivable == null) { |
| | | log.warn("[updateReceivableAuditStatus][回款({}) 不存在,忽略 BPM 审批结果({})]", id, bpmResult); |
| | | return; |
| | | } |
| | | // 1.2 只有审批中才处理:避免流程事件重复投递、或人工反审核后迟到的流程事件导致重复回写 |
| | | if (ObjectUtil.notEqual(receivable.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { |
| | | log.warn("[updateReceivableAuditStatus][回款({}) 当前状态({}) 不是审批中,忽略 BPM 审批结果({})]", |
| | | id, receivable.getAuditStatus(), bpmResult); |
| | | return; |
| | | } |
| | | |
| | | // 2. 转换 BPM 审批结果(不复用 CrmAuditStatusUtils:其 CANCEL 分支误返回 BPM 状态 4,而非 CRM 状态 40) |
| | | Integer auditStatus; |
| | | if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) { |
| | | auditStatus = CrmAuditStatusEnum.APPROVE.getStatus(); |
| | | } else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult)) { |
| | | auditStatus = CrmAuditStatusEnum.REJECT.getStatus(); |
| | | } else if (BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) { |
| | | auditStatus = CrmAuditStatusEnum.CANCEL.getStatus(); |
| | | } else { |
| | | log.warn("[updateReceivableAuditStatus][回款({}) 的 BPM 审批结果({}) 不是终态,忽略]", id, bpmResult); |
| | | return; |
| | | } |
| | | |
| | | // 3. 条件更新,确保并发/重复事件只有一次生效 |
| | | int updateCount = receivableMapper.updateByIdAndStatus(id, CrmAuditStatusEnum.PROCESS.getStatus(), |
| | | new CrmReceivableDO().setAuditStatus(auditStatus)); |
| | | if (updateCount == 0) { |
| | | log.warn("[updateReceivableAuditStatus][回款({}) 状态已被其他操作变更,忽略 BPM 审批结果({})]", id, bpmResult); |
| | | return; |
| | | } |
| | | |
| | | // 4. 仅审批通过时回写回款计划;驳回、取消不回写金额 |
| | | if (ObjectUtil.equal(auditStatus, CrmAuditStatusEnum.APPROVE.getStatus())) { |
| | | syncReceivablePlanReceipted(receivable, true); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateReceivableStatus(Long id, Integer status) { |
| | | boolean approve = CrmAuditStatusEnum.APPROVE.getStatus().equals(status); |
| | | // 1.1 校验存在 |
| | | CrmReceivableDO receivable = validateReceivableExists(id); |
| | | // 1.2 校验状态 |
| | | // 1.2 已提交 BPM 协同审批的回款,审批结果由流程监听器驱动,禁止手工审核,避免金额双重回写 |
| | | if (StrUtil.isNotBlank(receivable.getProcessInstanceId())) { |
| | | if (approve) { |
| | | throw exception(RECEIVABLE_MANUAL_AUDIT_FAIL_BPM_PROCESSING, receivable.getProcessInstanceId()); |
| | | } |
| | | // 审批中不能反审核,否则会解除流程关联,导致流程完成后无法回写;审批通过后的反审核仍保留 |
| | | if (ObjectUtil.equal(receivable.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { |
| | | throw exception(RECEIVABLE_MANUAL_REVERSE_FAIL_BPM_PROCESSING, receivable.getProcessInstanceId()); |
| | | } |
| | | } |
| | | // 1.3 校验状态 |
| | | if (receivable.getAuditStatus().equals(status)) { |
| | | throw exception(approve ? RECEIVABLE_APPROVE_FAIL : RECEIVABLE_PROCESS_FAIL); |
| | | } |
| | | |
| | | // 2. 更新状态 |
| | | int updateCount = receivableMapper.updateByIdAndStatus(id, receivable.getAuditStatus(), |
| | | new CrmReceivableDO().setAuditStatus(status)); |
| | | // 2. 更新状态;反审核时解除 BPM 关联,回归手工审核兼容流程 |
| | | CrmReceivableDO updateObj = new CrmReceivableDO().setAuditStatus(status); |
| | | if (!approve) { |
| | | updateObj.setProcessInstanceId(""); |
| | | } |
| | | int updateCount = receivableMapper.updateByIdAndStatus(id, receivable.getAuditStatus(), updateObj); |
| | | if (updateCount == 0) { |
| | | throw exception(approve ? RECEIVABLE_APPROVE_FAIL : RECEIVABLE_PROCESS_FAIL); |
| | | } |
| | | |
| | | // 3. 回款计划回写实际回款金额与状态 |
| | | if (receivable.getPlanId() != null) { |
| | | if (approve) { |
| | | receivablePlanService.updateReceivablePlanReceipted(receivable.getPlanId(), |
| | | receivable.getPrice(), receivable.getReturnTime()); |
| | | } else { |
| | | receivablePlanService.resetReceivablePlanReceipted(receivable.getPlanId()); |
| | | } |
| | | syncReceivablePlanReceipted(receivable, approve); |
| | | } |
| | | |
| | | /** |
| | | * 同步回款计划的已回款金额与状态 |
| | | * |
| | | * @param receivable 回款单 |
| | | * @param approve 是否审批通过:true 回写已回款;false 重置已回款 |
| | | */ |
| | | private void syncReceivablePlanReceipted(CrmReceivableDO receivable, boolean approve) { |
| | | if (receivable.getPlanId() == null) { |
| | | return; |
| | | } |
| | | if (approve) { |
| | | receivablePlanService.updateReceivablePlanReceipted(receivable.getPlanId(), |
| | | receivable.getPrice(), receivable.getReturnTime()); |
| | | } else { |
| | | receivablePlanService.resetReceivablePlanReceipted(receivable.getPlanId()); |
| | | } |
| | | } |
| | | |