| | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| | | import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; |
| | | import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceEntryReqVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceImportExcelVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceImportRespVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaleOutRespVO; |
| | | 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.invoice.CrmInvoiceDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.invoice.CrmInvoiceMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO; |
| | | import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.receivable.CrmPaymentStageEnum; |
| | | import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi; |
| | | import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO; |
| | | import cn.iocoder.yudao.module.mes.api.productsales.MesWmProductSalesApi; |
| | | import cn.iocoder.yudao.module.mes.api.productsales.dto.MesWmProductSalesRespDTO; |
| | | 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.permission.CrmPermissionService; |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | |
| | | */ |
| | | private static final String INVOICE_RECORD_TYPE = "crm_invoice"; |
| | | |
| | | /** |
| | | * ERP 销售订单审核通过状态,对应 cn.iocoder.yudao.module.erp.enums.ErpAuditStatus#APPROVE |
| | | */ |
| | | private static final Integer ERP_SALE_ORDER_STATUS_APPROVE = 20; |
| | | |
| | | /** |
| | | * MES 销售出库单已完成状态,对应 cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum#FINISHED |
| | | */ |
| | | private static final Integer MES_PRODUCT_SALES_STATUS_FINISHED = 4; |
| | | |
| | | /** |
| | | * MES 销售出库单已取消状态 |
| | | */ |
| | | private static final Integer MES_PRODUCT_SALES_STATUS_CANCELED = 5; |
| | | |
| | | /** |
| | | * 开票占用额度的审批状态:审批中 + 审批通过 |
| | | */ |
| | | private static final List<Integer> INVOICE_OCCUPY_STATUSES = List.of( |
| | | CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus()); |
| | | |
| | | @Resource |
| | | private CrmInvoiceMapper invoiceMapper; |
| | | @Resource |
| | | private CrmReceivablePlanMapper receivablePlanMapper; |
| | | |
| | | @Resource |
| | | private ErpSaleOrderApi saleOrderApi; |
| | | @Resource |
| | | private MesWmProductSalesApi productSalesApi; |
| | | |
| | | @Resource |
| | | private CrmNoRedisDAO noRedisDAO; |
| | |
| | | @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); |
| | | // 1.3 生成开票编号 |
| | | // 1.2 校验销售订单、销售出库单,并回填出库单号 |
| | | validateSaleOrderAndOut(createReqVO); |
| | | // 1.3 校验可开票金额超过上限 |
| | | validateInvoicePriceExceedsLimit(createReqVO); |
| | | // 1.4 生成开票编号 |
| | | String no = noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX); |
| | | if (invoiceMapper.selectByNo(no) != null) { |
| | | throw exception(INVOICE_NO_EXISTS); |
| | | } |
| | | |
| | | // 2.1 插入开票 |
| | | // 2.1 插入开票:已填写发票号码视为已录入发票 |
| | | CrmInvoiceDO invoice = BeanUtils.toBean(createReqVO, CrmInvoiceDO.class) |
| | | .setNo(no).setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus()); |
| | | applyEntryStatus(invoice, createReqVO.getInvoiceNo(), null); |
| | | invoiceMapper.insert(invoice); |
| | | |
| | | // 2.2 绑定发票附件 |
| | | if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) { |
| | | storageAttachmentApi.bindAttachments("file", INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds()); |
| | | } |
| | | |
| | | // 2.3 同步收款节点(合同款/预付款/到货款/验收款) |
| | | syncPaymentNodes(invoice, createReqVO.getPaymentNodes()); |
| | | |
| | | // 3. 创建数据权限 |
| | | permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_INVOICE.getType()) |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_IMPORT_SUB_TYPE, bizNo = "0", |
| | | success = CRM_INVOICE_IMPORT_SUCCESS) |
| | | public CrmInvoiceImportRespVO importInvoiceList(List<CrmInvoiceImportExcelVO> importList) { |
| | | CrmInvoiceImportRespVO respVO = CrmInvoiceImportRespVO.builder() |
| | | .createList(new ArrayList<>()) |
| | | .failureList(new LinkedHashMap<>()) |
| | | .build(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | // 空文件也要给日志文案兜底,避免 {{#importSummary}} 取不到值 |
| | | OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList()); |
| | | return respVO; |
| | | } |
| | | |
| | |
| | | respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); |
| | | } |
| | | } |
| | | // 记录导入结果汇总,供 @LogRecord 的 success 文案引用(开票导入无「更新」分支,故传 null) |
| | | OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList()); |
| | | return respVO; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 校验销售订单、销售出库单,并回填出库单号 |
| | | * |
| | | * 校验顺序:销售订单存在 -> 已审核 -> 客户/合同一致 -> 订单剩余可开票; |
| | | * 再校验出库单存在 -> 已完成 -> 属于该订单 -> 客户一致 -> 出库单剩余可开票。 |
| | | */ |
| | | private void validateSaleOrderAndOut(CrmInvoiceSaveReqVO reqVO) { |
| | | if (reqVO.getSaleOrderId() == null) { |
| | | return; |
| | | } |
| | | // 1. 销售订单 |
| | | ErpSaleOrderRespDTO saleOrder = saleOrderApi.getSaleOrder(reqVO.getSaleOrderId()).getCheckedData(); |
| | | if (saleOrder == null) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_NOT_EXISTS, reqVO.getSaleOrderId()); |
| | | } |
| | | if (ObjectUtil.notEqual(saleOrder.getStatus(), ERP_SALE_ORDER_STATUS_APPROVE)) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_NOT_APPROVE, saleOrder.getNo(), |
| | | erpAuditStatusName(saleOrder.getStatus())); |
| | | } |
| | | if (ObjectUtil.notEqual(saleOrder.getCustomerId(), reqVO.getCustomerId()) |
| | | || (saleOrder.getContractId() != null |
| | | && ObjectUtil.notEqual(saleOrder.getContractId(), reqVO.getContractId()))) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_MISMATCH, saleOrder.getNo(), |
| | | saleOrder.getCustomerId(), saleOrder.getContractId(), reqVO.getCustomerId(), reqVO.getContractId()); |
| | | } |
| | | // 1.1 订单剩余可开票金额 = 订单金额 - 该订单已占用开票金额(审批中 + 审批通过) |
| | | BigDecimal orderTotalPrice = zeroIfNull(saleOrder.getTotalPrice()); |
| | | BigDecimal orderInvoicedPrice = sumInvoicePrice(invoiceMapper.selectListBySaleOrderIdAndStatus( |
| | | reqVO.getSaleOrderId(), INVOICE_OCCUPY_STATUSES), reqVO.getId()); |
| | | BigDecimal orderRemainPrice = orderTotalPrice.subtract(orderInvoicedPrice); |
| | | if (reqVO.getPrice() != null && reqVO.getPrice().compareTo(orderRemainPrice) > 0) { |
| | | throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_SALE_ORDER, saleOrder.getNo(), |
| | | formatPrice(orderRemainPrice), formatPrice(orderTotalPrice), formatPrice(orderInvoicedPrice)); |
| | | } |
| | | |
| | | // 2. 销售出库单(可选) |
| | | if (reqVO.getSaleOutId() == null) { |
| | | return; |
| | | } |
| | | MesWmProductSalesRespDTO saleOut = productSalesApi.getProductSales(reqVO.getSaleOutId()); |
| | | if (saleOut == null) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_OUT_NOT_EXISTS, reqVO.getSaleOutId()); |
| | | } |
| | | if (ObjectUtil.notEqual(saleOut.getStatus(), MES_PRODUCT_SALES_STATUS_FINISHED)) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_OUT_NOT_FINISHED, saleOut.getCode(), |
| | | productSalesStatusName(saleOut.getStatus())); |
| | | } |
| | | if (saleOut.getSaleOrderId() != null && ObjectUtil.notEqual(saleOut.getSaleOrderId(), reqVO.getSaleOrderId())) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_OUT_MISMATCH_ORDER, saleOut.getCode(), |
| | | saleOrder.getNo(), saleOut.getSaleOrderId()); |
| | | } |
| | | if (saleOut.getClientId() != null && ObjectUtil.notEqual(saleOut.getClientId(), reqVO.getCustomerId())) { |
| | | throw exception(INVOICE_CREATE_FAIL_SALE_OUT_MISMATCH_CUSTOMER, saleOut.getCode(), |
| | | saleOut.getClientId(), reqVO.getCustomerId()); |
| | | } |
| | | // 2.1 出库单剩余可开票金额 = 出库金额 - 该出库单已占用开票金额(审批中 + 审批通过) |
| | | BigDecimal saleOutTotalPrice = zeroIfNull(saleOut.getTotalPrice()); |
| | | BigDecimal saleOutInvoicedPrice = sumInvoicePrice(invoiceMapper.selectListBySaleOutIdAndStatus( |
| | | reqVO.getSaleOutId(), INVOICE_OCCUPY_STATUSES), reqVO.getId()); |
| | | BigDecimal saleOutRemainPrice = saleOutTotalPrice.subtract(saleOutInvoicedPrice); |
| | | if (reqVO.getPrice() != null && reqVO.getPrice().compareTo(saleOutRemainPrice) > 0) { |
| | | throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_SALE_OUT, saleOut.getCode(), |
| | | formatPrice(saleOutRemainPrice), formatPrice(saleOutTotalPrice), formatPrice(saleOutInvoicedPrice)); |
| | | } |
| | | reqVO.setSaleOutCode(saleOut.getCode()); |
| | | } |
| | | |
| | | private BigDecimal sumInvoicePrice(List<CrmInvoiceDO> invoices, Long excludeInvoiceId) { |
| | | if (CollUtil.isEmpty(invoices)) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return invoices.stream() |
| | | .filter(invoice -> excludeInvoiceId == null || ObjectUtil.notEqual(invoice.getId(), excludeInvoiceId)) |
| | | .map(invoice -> zeroIfNull(invoice.getPrice())) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | } |
| | | |
| | | /** |
| | | * 根据发票号码是否填写,设置发票录入状态 |
| | | */ |
| | | private void applyEntryStatus(CrmInvoiceDO invoice, String invoiceNo, CrmInvoiceDO oldInvoice) { |
| | | if (StrUtil.isBlank(invoiceNo)) { |
| | | invoice.setEntryStatus(0); |
| | | return; |
| | | } |
| | | invoice.setEntryStatus(10); |
| | | boolean changed = oldInvoice == null || !StrUtil.equals(invoiceNo, oldInvoice.getInvoiceNo()) |
| | | || !Integer.valueOf(10).equals(oldInvoice.getEntryStatus()); |
| | | if (changed) { |
| | | invoice.setEntryUserId(getLoginUserId()).setEntryTime(LocalDateTime.now()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 同步开票单的收款节点到回款计划(crm_receivable_plan) |
| | | * |
| | | * 传 null 表示本次不修改节点;传空列表表示清空节点。 |
| | | */ |
| | | private void syncPaymentNodes(CrmInvoiceDO invoice, List<CrmInvoiceSaveReqVO.PaymentNode> nodes) { |
| | | if (nodes == null) { |
| | | return; |
| | | } |
| | | validatePaymentNodes(nodes); |
| | | // 已产生回款的节点不允许被覆盖 |
| | | for (CrmReceivablePlanDO oldPlan : receivablePlanMapper.selectListByInvoiceId(invoice.getId())) { |
| | | if (oldPlan.getReceivableId() != null || zeroIfNull(oldPlan.getReceiptedPrice()).compareTo(BigDecimal.ZERO) > 0) { |
| | | throw exception(INVOICE_PAYMENT_NODE_ALREADY_RECEIPTED, |
| | | CrmPaymentStageEnum.getNameByStage(oldPlan.getStage()), |
| | | formatPrice(zeroIfNull(oldPlan.getReceiptedPrice()))); |
| | | } |
| | | } |
| | | deletePaymentNodesByInvoiceId(invoice.getId()); |
| | | |
| | | Long ownerUserId = invoice.getOwnerUserId() != null ? invoice.getOwnerUserId() : getLoginUserId(); |
| | | for (CrmInvoiceSaveReqVO.PaymentNode node : nodes) { |
| | | CrmReceivablePlanDO maxPeriodPlan = receivablePlanMapper.selectMaxPeriodByContractId(invoice.getContractId()); |
| | | int period = maxPeriodPlan == null ? 1 : maxPeriodPlan.getPeriod() + 1; |
| | | int remindDays = 3; |
| | | CrmReceivablePlanDO plan = CrmReceivablePlanDO.builder() |
| | | .period(period).customerId(invoice.getCustomerId()).contractId(invoice.getContractId()) |
| | | .ownerUserId(ownerUserId).returnTime(node.getReturnTime()) |
| | | .remindDays(remindDays).remindTime(node.getReturnTime().minusDays(remindDays)) |
| | | .price(node.getAmount()).stage(node.getStage()).invoiceId(invoice.getId()) |
| | | .saleOrderId(invoice.getSaleOrderId()) |
| | | .receiptedPrice(BigDecimal.ZERO).status(0).remark(node.getRemark()) |
| | | .build(); |
| | | receivablePlanMapper.insert(plan); |
| | | permissionService.createPermission(new CrmPermissionCreateReqBO() |
| | | .setBizType(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType()).setBizId(plan.getId()) |
| | | .setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); |
| | | } |
| | | } |
| | | |
| | | private void validatePaymentNodes(List<CrmInvoiceSaveReqVO.PaymentNode> nodes) { |
| | | if (CollUtil.isEmpty(nodes)) { |
| | | return; |
| | | } |
| | | Set<Integer> stages = new HashSet<>(); |
| | | for (int i = 0; i < nodes.size(); i++) { |
| | | CrmInvoiceSaveReqVO.PaymentNode node = nodes.get(i); |
| | | int row = i + 1; |
| | | CrmPaymentStageEnum stageEnum = node == null ? null : CrmPaymentStageEnum.valueOf(node.getStage()); |
| | | if (stageEnum == null) { |
| | | throw exception(INVOICE_PAYMENT_NODE_STAGE_INVALID, row, node == null ? "未填写" : node.getStage()); |
| | | } |
| | | if (!stages.add(stageEnum.getStage())) { |
| | | throw exception(INVOICE_PAYMENT_NODE_STAGE_DUPLICATE, stageEnum.getName()); |
| | | } |
| | | if (node.getAmount() == null || node.getAmount().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(INVOICE_PAYMENT_NODE_INVALID, row, stageEnum.getName(), |
| | | node.getAmount() == null ? "未填写" : formatPrice(node.getAmount())); |
| | | } |
| | | if (node.getReturnTime() == null) { |
| | | throw exception(INVOICE_PAYMENT_NODE_TIME_REQUIRED, stageEnum.getName()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void deletePaymentNodesByInvoiceId(Long invoiceId) { |
| | | for (CrmReceivablePlanDO plan : receivablePlanMapper.selectListByInvoiceId(invoiceId)) { |
| | | receivablePlanMapper.deleteById(plan.getId()); |
| | | permissionService.deletePermission(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), plan.getId()); |
| | | } |
| | | } |
| | | |
| | | private String erpAuditStatusName(Integer status) { |
| | | if (Objects.equals(status, ERP_SALE_ORDER_STATUS_APPROVE)) { |
| | | return "已审核"; |
| | | } |
| | | if (status == null) { |
| | | return "未知状态"; |
| | | } |
| | | return switch (status) { |
| | | case 0 -> "未提交"; |
| | | case 10 -> "未审核"; |
| | | case 15 -> "审批中"; |
| | | case 30 -> "已作废"; |
| | | case 35 -> "已驳回"; |
| | | default -> "状态值 " + status; |
| | | }; |
| | | } |
| | | |
| | | private String productSalesStatusName(Integer status) { |
| | | if (status == null) { |
| | | return "未知状态"; |
| | | } |
| | | return switch (status) { |
| | | case 0 -> "草稿"; |
| | | case 1 -> "待检测"; |
| | | case 2 -> "待拣货"; |
| | | case 3 -> "待执行出库"; |
| | | case 4 -> "已完成"; |
| | | case 5 -> "已取消"; |
| | | case 10 -> "待填写运单"; |
| | | default -> "状态值 " + status; |
| | | }; |
| | | } |
| | | |
| | | private static BigDecimal zeroIfNull(BigDecimal value) { |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private static String formatPrice(BigDecimal value) { |
| | | return (value == null ? BigDecimal.ZERO : value).stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", |
| | |
| | | CrmInvoiceDO oldInvoice = validateInvoiceExists(updateReqVO.getId()); |
| | | updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()).setCustomerId(oldInvoice.getCustomerId()) |
| | | .setContractId(oldInvoice.getContractId()); // 设置已存在的值 |
| | | // 1.2 校验可开票金额超过上限 |
| | | validateInvoicePriceExceedsLimit(updateReqVO); |
| | | |
| | | // 1.3 只有草稿、审批中,可以编辑; |
| | | if (!ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), |
| | | // 1.2 免审批时不限制编辑状态;正常审批仅草稿、审批中可以编辑。 |
| | | if (!bpmProcessInstanceApi.isGlobalAutoApprove() |
| | | && !ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), |
| | | CrmAuditStatusEnum.PROCESS.getStatus())) { |
| | | throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED); |
| | | throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED, |
| | | StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(oldInvoice.getAuditStatus()), "未知状态")); |
| | | } |
| | | |
| | | // 1.3 校验销售订单、销售出库单,并回填出库单号 |
| | | validateSaleOrderAndOut(updateReqVO); |
| | | // 1.4 校验可开票金额超过上限 |
| | | validateInvoicePriceExceedsLimit(updateReqVO); |
| | | |
| | | // 2. 更新开票 |
| | | CrmInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, CrmInvoiceDO.class); |
| | | applyEntryStatus(updateObj, updateReqVO.getInvoiceNo(), oldInvoice); |
| | | invoiceMapper.updateById(updateObj); |
| | | // 发票号码被清空时,一并清空录入人、录入时间(updateById 会忽略 null 字段,需显式清空) |
| | | if (StrUtil.isBlank(updateReqVO.getInvoiceNo())) { |
| | | invoiceMapper.clearEntryInfo(updateReqVO.getId()); |
| | | } |
| | | |
| | | // 3. 增量更新发票附件 |
| | | storageAttachmentApi.updateAttachments("file", INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); |
| | | |
| | | // 3.1 同步收款节点(仅前端传了 paymentNodes 才覆盖,避免不传时误删) |
| | | syncPaymentNodes(oldInvoice, updateReqVO.getPaymentNodes()); |
| | | |
| | | // 4. 记录操作日志上下文 |
| | | updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 |
| | |
| | | invoiceMapper.deleteById(id); |
| | | // 2.3 删除数据权限 |
| | | permissionService.deletePermission(CrmBizTypeEnum.CRM_INVOICE.getType(), id); |
| | | // 2.4 删除开票生成的收款节点(审批通过的开票不允许删除,故此处节点必然未产生回款) |
| | | deletePaymentNodesByInvoiceId(id); |
| | | |
| | | // 3. 记录操作日志上下文 |
| | | LogRecordContext.putVariable("invoice", invoice); |
| | |
| | | .setId(invoice.getId()).setContractId(invoice.getContractId()).setPrice(invoice.getPrice())); |
| | | |
| | | // 2. 创建开票审批流程实例 |
| | | // 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型。 |
| | | if (bpmProcessInstanceApi.isGlobalAutoApprove()) { |
| | | invoiceMapper.updateById(new CrmInvoiceDO().setId(id).setProcessInstanceId("") |
| | | .setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus())); |
| | | updateInvoiceAuditStatus(id, cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus()); |
| | | LogRecordContext.putVariable("invoiceNo", invoice.getNo()); |
| | | return; |
| | | } |
| | | |
| | | String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id))); |
| | | |
| | |
| | | return invoiceMapper.selectCountByContractId(contractId); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_ENTRY_SUB_TYPE, bizNo = "{{#reqVO.id}}", |
| | | success = CRM_INVOICE_ENTRY_SUCCESS) |
| | | @CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.WRITE) |
| | | public void entryInvoice(CrmInvoiceEntryReqVO reqVO) { |
| | | // 1.1 校验存在 |
| | | CrmInvoiceDO invoice = validateInvoiceExists(reqVO.getId()); |
| | | // 1.2 只有审批通过的开票申请才能录入发票 |
| | | if (ObjectUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) { |
| | | throw exception(INVOICE_ENTRY_FAIL_NOT_APPROVE, invoice.getNo(), |
| | | StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(invoice.getAuditStatus()), "未知状态")); |
| | | } |
| | | // 1.3 发票号码不可与其他开票单重复 |
| | | String invoiceNo = reqVO.getInvoiceNo().trim(); |
| | | CrmInvoiceDO invoiceNoExists = invoiceMapper.selectByInvoiceNo(invoiceNo); |
| | | if (invoiceNoExists != null && ObjectUtil.notEqual(invoiceNoExists.getId(), invoice.getId())) { |
| | | throw exception(INVOICE_ENTRY_FAIL_INVOICE_NO_EXISTS, invoiceNo, invoiceNoExists.getNo()); |
| | | } |
| | | |
| | | // 2. 录入发票信息,状态置为已录入 |
| | | invoiceMapper.updateById(new CrmInvoiceDO().setId(invoice.getId()) |
| | | .setInvoiceNo(invoiceNo).setInvoiceTitle(reqVO.getInvoiceTitle()) |
| | | .setInvoiceTime(reqVO.getInvoiceTime()).setRemark(reqVO.getRemark()) |
| | | .setEntryStatus(10).setEntryUserId(getLoginUserId()).setEntryTime(LocalDateTime.now())); |
| | | // 3. 更新发票附件 |
| | | if (reqVO.getBlobIds() != null) { |
| | | storageAttachmentApi.updateAttachments("file", INVOICE_RECORD_TYPE, invoice.getId(), reqVO.getBlobIds()); |
| | | } |
| | | // 4. 同步收款节点 |
| | | syncPaymentNodes(invoice, reqVO.getPaymentNodes()); |
| | | |
| | | // 5. 记录操作日志上下文 |
| | | LogRecordContext.putVariable("invoiceNo", invoice.getNo()); |
| | | LogRecordContext.putVariable("entryInvoiceNo", invoiceNo); |
| | | } |
| | | |
| | | @Override |
| | | public List<CrmInvoiceSaleOutRespVO> getSaleOutListForInvoice(Long saleOrderId, Long excludeInvoiceId) { |
| | | if (saleOrderId == null) { |
| | | return ListUtil.empty(); |
| | | } |
| | | List<MesWmProductSalesRespDTO> saleOutList = productSalesApi.getProductSalesListBySaleOrderId(saleOrderId); |
| | | if (CollUtil.isEmpty(saleOutList)) { |
| | | return ListUtil.empty(); |
| | | } |
| | | List<CrmInvoiceSaleOutRespVO> result = new ArrayList<>(); |
| | | for (MesWmProductSalesRespDTO saleOut : saleOutList) { |
| | | // 已取消的出库单不可开票 |
| | | if (ObjectUtil.equal(saleOut.getStatus(), MES_PRODUCT_SALES_STATUS_CANCELED)) { |
| | | continue; |
| | | } |
| | | BigDecimal totalPrice = zeroIfNull(saleOut.getTotalPrice()); |
| | | BigDecimal invoicedPrice = sumInvoicePrice(invoiceMapper.selectListBySaleOutIdAndStatus( |
| | | saleOut.getId(), INVOICE_OCCUPY_STATUSES), excludeInvoiceId); |
| | | CrmInvoiceSaleOutRespVO vo = new CrmInvoiceSaleOutRespVO(); |
| | | vo.setId(saleOut.getId()); |
| | | vo.setCode(saleOut.getCode()); |
| | | vo.setSaleOrderId(saleOut.getSaleOrderId()); |
| | | vo.setSalesOrderCode(saleOut.getSalesOrderCode()); |
| | | vo.setSalesDate(saleOut.getSalesDate()); |
| | | vo.setStatus(saleOut.getStatus()); |
| | | vo.setStatusName(productSalesStatusName(saleOut.getStatus())); |
| | | vo.setTotalPrice(totalPrice); |
| | | vo.setInvoicedPrice(invoicedPrice); |
| | | vo.setRemainInvoicePrice(totalPrice.subtract(invoicedPrice)); |
| | | result.add(vo); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | public List<CrmReceivablePlanDO> getPaymentNodeListByInvoiceId(Long invoiceId) { |
| | | return receivablePlanMapper.selectListByInvoiceId(invoiceId); |
| | | } |
| | | |
| | | } |