gaoluyang
3 天以前 d39e4f7d839167c16425086dfa2f8747bccfdff9
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -14,17 +14,26 @@
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;
@@ -46,9 +55,12 @@
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;
@@ -77,8 +89,36 @@
     */
    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;
@@ -100,25 +140,31 @@
    @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())
@@ -390,6 +436,212 @@
        }
    }
    /**
     * 校验销售订单、销售出库单,并回填出库单号
     *
     * 校验顺序:销售订单存在 -> 已审核 -> 客户/合同一致 -> 订单剩余可开票;
     * 再校验出库单存在 -> 已完成 -> 属于该订单 -> 客户一致 -> 出库单剩余可开票。
     */
    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}}",
@@ -410,15 +662,25 @@
                    StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(oldInvoice.getAuditStatus()), "未知状态"));
        }
        // 1.3 校验可开票金额超过上限
        // 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()); // 避免操作日志出现“删除负责人”的情况
@@ -461,6 +723,8 @@
        invoiceMapper.deleteById(id);
        // 2.3 删除数据权限
        permissionService.deletePermission(CrmBizTypeEnum.CRM_INVOICE.getType(), id);
        // 2.4 删除开票生成的收款节点(审批通过的开票不允许删除,故此处节点必然未产生回款)
        deletePaymentNodesByInvoiceId(id);
        // 3. 记录操作日志上下文
        LogRecordContext.putVariable("invoice", invoice);
@@ -555,4 +819,80 @@
        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);
    }
}