From d39e4f7d839167c16425086dfa2f8747bccfdff9 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 05 十一月 2026 17:39:01 +0800
Subject: [PATCH] feat: 打通销售采购财务闭环并完成累计业务优化

---
 yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java |  352 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 346 insertions(+), 6 deletions(-)

diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
index e805567..b2cb267 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
+++ b/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());
+        }
+    }
+
+    /**
+     * 鍚屾寮�绁ㄥ崟鐨勬敹娆捐妭鐐瑰埌鍥炴璁″垝锛坈rm_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()); // 閬垮厤鎿嶄綔鏃ュ織鍑虹幇鈥滃垹闄よ礋璐d汉鈥濈殑鎯呭喌
@@ -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);
+    }
+
 }

--
Gitblit v1.9.3