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 | 638 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 618 insertions(+), 20 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 0844616..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
@@ -5,21 +5,35 @@
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.util.collection.CollectionUtils;
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;
@@ -36,11 +50,21 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+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;
+import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_INVOICE_TYPE;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmResultToAuditStatus;
@@ -65,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;
@@ -88,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())
@@ -118,20 +176,251 @@
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()));
- if (reqVO.getId() != null) {
- invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
+ @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;
}
- BigDecimal notInvoicePrice = contract.getTotalPrice().subtract(
- CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
+
+ Map<String, Integer> firstRowNoByKey = new HashMap<>();
+ for (int i = 0; i < importList.size(); i++) {
+ CrmInvoiceImportExcelVO row = importList.get(i);
+ if (isImportRowEmpty(row)) {
+ continue; // 璺宠繃绌鸿
+ }
+ String label = StrUtil.isNotBlank(row.getInvoiceNo()) ? "鍙戠エ鍙风爜 " + row.getInvoiceNo().trim()
+ : "绗� " + (i + 2) + " 琛�";
+ String importKey = buildInvoiceImportKey(row);
+ try {
+ Integer firstRowNo = firstRowNoByKey.get(importKey);
+ if (firstRowNo != null) {
+ throw duplicateRowException(firstRowNo, buildInvoiceKeyDesc(row));
+ }
+ respVO.getCreateList().add(createInvoiceByImport(row));
+ firstRowNoByKey.putIfAbsent(importKey, i + 2);
+ } catch (Exception ex) {
+ respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
+ }
+ }
+ // 璁板綍瀵煎叆缁撴灉姹囨�伙紝渚� @LogRecord 鐨� success 鏂囨寮曠敤锛堝紑绁ㄥ鍏ユ棤銆屾洿鏂般�嶅垎鏀紝鏁呬紶 null锛�
+ OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList());
+ return respVO;
+ }
+
+ private boolean isImportRowEmpty(CrmInvoiceImportExcelVO row) {
+ return row == null || (StrUtil.isBlank(row.getNo()) && StrUtil.isBlank(row.getContractNo())
+ && StrUtil.isBlank(row.getInvoiceType()) && StrUtil.isBlank(row.getInvoiceNo())
+ && StrUtil.isBlank(row.getInvoiceTitle()) && row.getPrice() == null
+ && StrUtil.isBlank(row.getInvoiceTime()) && StrUtil.isBlank(row.getRemark()));
+ }
+
+ /**
+ * 渚濇嵁瀵煎叆鏁版嵁鍒涘缓涓�寮犲紑绁ㄣ�傚鎴峰彇鍚堝悓鎵�灞炲鎴凤紱璐熻矗浜�=褰撳墠鐧诲綍鐢ㄦ埛锛涘鎵圭姸鎬�=鏈彁浜ゃ��
+ */
+ private String createInvoiceByImport(CrmInvoiceImportExcelVO row) {
+ // 1. 鍚堝悓鏍¢獙
+ if (StrUtil.isBlank(row.getContractNo())) {
+ throw new IllegalArgumentException("鍚堝悓缂栧彿涓嶈兘涓虹┖锛氳濉啓妯℃澘涓琛岀殑銆屽悎鍚岀紪鍙枫�嶅垪");
+ }
+ CrmContractDO contract = contractService.getContractByNo(row.getContractNo().trim());
+ if (contract == null) {
+ throw new IllegalArgumentException("鍚堝悓缂栧彿涓嶅瓨鍦細" + row.getContractNo().trim()
+ + "銆傝鍏堝湪銆孋RM 鍚堝悓绠$悊銆嶄腑鍒涘缓鎴栧鍏ヨ鍚堝悓锛堢紪鍙烽渶涓庢ā鏉垮畬鍏ㄤ竴鑷达級锛屾垨淇敼妯℃澘涓殑鍚堝悓缂栧彿鍚庨噸鏂板鍏�");
+ }
+ if (ObjUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
+ throw new IllegalArgumentException("鍚堝悓[" + contract.getNo() + "]褰撳墠鐘舵�佷负銆�"
+ + CrmAuditStatusEnum.getNameByStatus(contract.getAuditStatus()) + "銆嶏紝鍙湁銆屽鏍搁�氳繃銆嶇殑鍚堝悓鎵嶈兘寮�绁ㄣ��"
+ + "璇峰厛鍦ㄣ�孋RM 鍚堝悓绠$悊銆嶄腑瀹屾垚璇ュ悎鍚屽鎵癸紝鎴栨敼鐢ㄥ叾浠栧凡瀹℃牳閫氳繃鐨勫悎鍚岀紪鍙峰悗閲嶆柊瀵煎叆");
+ }
+
+ // 2. 寮�绁ㄩ噾棰濇牎楠�
+ if (row.getPrice() == null) {
+ throw new IllegalArgumentException("寮�绁ㄩ噾棰濅笉鑳戒负绌猴細璇峰~鍐欐ā鏉夸腑璇ヨ鐨勩�屽紑绁ㄩ噾棰�(鍏�)銆嶅垪");
+ }
+ if (row.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("寮�绁ㄩ噾棰濆繀椤诲ぇ浜� 0锛屽綋鍓嶄负 "
+ + row.getPrice().stripTrailingZeros().toPlainString() + "銆傝淇敼妯℃澘涓殑銆屽紑绁ㄩ噾棰�(鍏�)銆嶅悗閲嶆柊瀵煎叆");
+ }
+ BigDecimal remainingInvoicePrice = getRemainingInvoicePrice(contract, null);
+ if (row.getPrice().compareTo(remainingInvoicePrice) > 0) {
+ throw new IllegalArgumentException("寮�绁ㄩ噾棰濊秴杩囧悎鍚屽墿浣欏彲寮�绁ㄩ噾棰濓細鍚堝悓[" + contract.getNo()
+ + "]鍓╀綑鍙紑绁ㄩ噾棰� " + remainingInvoicePrice.stripTrailingZeros().toPlainString() + " 鍏冿紝鏈瀵煎叆閲戦 "
+ + row.getPrice().stripTrailingZeros().toPlainString()
+ + " 鍏冦�傝璋冩暣妯℃澘涓殑銆屽紑绁ㄩ噾棰�(鍏�)銆嶏紝鎴栧厛鍦ㄧ郴缁熶腑鏍稿宸插紑绁ㄩ噾棰濆悗閲嶆柊瀵煎叆");
+ }
+
+ // 3. 鍙戠エ绫诲瀷鏍¢獙
+ Integer invoiceType = resolveInvoiceType(row.getInvoiceType());
+
+ // 4. 寮�绁ㄧ紪鍙凤細濉啓鍒欎娇鐢紝鐣欑┖鍒欒嚜鍔ㄧ敓鎴�
+ String no = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim()
+ : noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
+ if (invoiceMapper.selectByNo(no) != null) {
+ throw new IllegalArgumentException("寮�绁ㄧ紪鍙峰凡瀛樺湪锛�" + no
+ + "銆傝淇敼妯℃澘涓殑寮�绁ㄧ紪鍙凤紝鎴栧厛鍦ㄧ郴缁熶腑鍒犻櫎璇ョ紪鍙风殑寮�绁ㄥ悗閲嶆柊瀵煎叆");
+ }
+
+ // 5. 鍘婚噸锛氬悓涓�鍚堝悓涓嬶紝鍙戠エ鍙风爜锛堢暀绌烘椂鎸夊紑绁ㄩ噾棰濄�佸彂绁ㄦ姮澶淬�佸紑绁ㄦ棩鏈燂級鐩稿悓鐨勫紑绁ㄤ笉鍙噸澶嶅鍏�
+ // 寮�绁ㄦ棩鏈熺暀绌烘椂瑙嗕綔涓嶆瘮杈冭缁村害锛屾晠鎸夆�滄槸鍚﹀~鍐欌�濅紶 null锛屼笉鑳界敤 parseImportDateTime 鐨� now() 鍏滃簳鍊�
+ LocalDateTime invoiceTime = parseImportDateTime(row.getInvoiceTime());
+ CrmInvoiceDO duplicate = findDuplicateInvoice(contract.getId(), row,
+ StrUtil.isNotBlank(row.getInvoiceTime()) ? invoiceTime : null);
+ if (duplicate != null) {
+ throw duplicateExistingException(buildInvoiceKeyDesc(row), duplicate.getNo());
+ }
+
+ // 6. 淇濆瓨寮�绁�
+ CrmInvoiceDO invoice = new CrmInvoiceDO().setNo(no).setCustomerId(contract.getCustomerId())
+ .setContractId(contract.getId()).setOwnerUserId(getLoginUserId()).setInvoiceType(invoiceType)
+ .setInvoiceNo(row.getInvoiceNo()).setInvoiceTitle(row.getInvoiceTitle()).setPrice(row.getPrice())
+ .setInvoiceTime(invoiceTime).setRemark(row.getRemark())
+ .setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
+ invoiceMapper.insert(invoice);
+
+ // 7. 鍒涘缓鏁版嵁鏉冮檺锛堝惁鍒欏綋鍓嶇敤鎴风湅涓嶅埌璇ュ紑绁級
+ permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_INVOICE.getType())
+ .setBizId(invoice.getId()).setUserId(invoice.getOwnerUserId())
+ .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+ return invoice.getNo();
+ }
+
+ private Integer resolveInvoiceType(String text) {
+ List<String> labelList = DictFrameworkUtils.getDictDataLabelList(CRM_INVOICE_TYPE);
+ if (StrUtil.isBlank(text)) {
+ throw new IllegalArgumentException("鍙戠エ绫诲瀷涓嶈兘涓虹┖锛氳濉啓妯℃澘涓琛岀殑銆屽彂绁ㄧ被鍨嬨�嶅垪锛屽彲閫夊�硷細"
+ + String.join("銆�", labelList));
+ }
+ String value = text.trim();
+ if (value.matches("\\d+") && DictFrameworkUtils.getDictDataValueList(CRM_INVOICE_TYPE).contains(value)) {
+ return Integer.valueOf(value);
+ }
+ String parsedValue = DictFrameworkUtils.parseDictDataValue(CRM_INVOICE_TYPE, value);
+ if (parsedValue == null) {
+ throw new IllegalArgumentException("鍙戠エ绫诲瀷涓嶅瓨鍦細" + value + "銆傛ā鏉夸腑銆屽彂绁ㄧ被鍨嬨�嶈濉啓浠ヤ笅涔嬩竴锛�"
+ + String.join("銆�", labelList));
+ }
+ return Integer.valueOf(parsedValue);
+ }
+
+ private LocalDateTime parseImportDateTime(String text) {
+ if (StrUtil.isBlank(text)) {
+ return LocalDateTime.now();
+ }
+ String value = text.trim();
+ for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
+ try {
+ return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
+ } catch (Exception ignored) {
+ // 缁х画灏濊瘯涓嬩竴涓牸寮�
+ }
+ }
+ for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
+ try {
+ return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
+ } catch (Exception ignored) {
+ // 缁х画灏濊瘯涓嬩竴涓牸寮�
+ }
+ }
+ throw new IllegalArgumentException("寮�绁ㄦ棩鏈熸牸寮忎笉姝g‘锛�" + text + "锛岀ず渚� 2026-01-01 10:00:00");
+ }
+
+ private String resolveImportErrorMessage(Exception ex) {
+ if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
+ return serviceException.getMessage();
+ }
+ return StrUtil.blankToDefault(ex.getMessage(), "瀵煎叆澶辫触");
+ }
+
+ /**
+ * 瀵煎叆鍘婚噸閿細鍙戠エ鍙风爜濉啓鏃朵互 鍚堝悓 + 鍙戠エ鍙风爜 涓哄噯锛涚暀绌烘椂姣旇緝 鍚堝悓 + 寮�绁ㄩ噾棰� + 鍙戠エ鎶ご + 寮�绁ㄦ棩鏈�
+ */
+ private String buildInvoiceImportKey(CrmInvoiceImportExcelVO row) {
+ if (StrUtil.isNotBlank(row.getInvoiceNo())) {
+ return "no|" + StrUtil.trimToEmpty(row.getContractNo()) + "|" + row.getInvoiceNo().trim();
+ }
+ return "amt|" + StrUtil.trimToEmpty(row.getContractNo()) + "|"
+ + (row.getPrice() == null ? "" : row.getPrice().stripTrailingZeros().toPlainString()) + "|"
+ + StrUtil.trimToEmpty(row.getInvoiceTitle()) + "|" + StrUtil.trimToEmpty(row.getInvoiceTime());
+ }
+
+ private String buildInvoiceKeyDesc(CrmInvoiceImportExcelVO row) {
+ if (StrUtil.isNotBlank(row.getInvoiceNo())) {
+ return "鍚堝悓缂栧彿[" + StrUtil.trimToEmpty(row.getContractNo()) + "]銆佸彂绁ㄥ彿鐮乕" + row.getInvoiceNo().trim() + "]";
+ }
+ return "鍚堝悓缂栧彿[" + StrUtil.trimToEmpty(row.getContractNo()) + "]銆佸紑绁ㄩ噾棰漑"
+ + (row.getPrice() == null ? "鏈~鍐�" : row.getPrice().stripTrailingZeros().toPlainString() + " 鍏�")
+ + "]銆佸彂绁ㄦ姮澶碵" + StrUtil.blankToDefault(StrUtil.trimToNull(row.getInvoiceTitle()), "鏈~鍐�") + "]"
+ + (StrUtil.isBlank(row.getInvoiceTime()) ? "" : "銆佸紑绁ㄦ棩鏈焄" + row.getInvoiceTime().trim() + "]");
+ }
+
+ /**
+ * 鏌ヨ鍚屽悎鍚屼笅涓氬姟鏁版嵁瀹屽叏鐩稿悓鐨勫紑绁�
+ */
+ private CrmInvoiceDO findDuplicateInvoice(Long contractId, CrmInvoiceImportExcelVO row, LocalDateTime invoiceTime) {
+ for (CrmInvoiceDO old : invoiceMapper.selectListByContractId(contractId)) {
+ if (StrUtil.isNotBlank(row.getInvoiceNo())) {
+ if (StrUtil.equals(StrUtil.trimToEmpty(old.getInvoiceNo()), row.getInvoiceNo().trim())) {
+ return old;
+ }
+ continue;
+ }
+ // 鍙戠エ鍙风爜鐣欑┖鏃讹紝姣旇緝寮�绁ㄩ噾棰濄�佸彂绁ㄦ姮澶达紱寮�绁ㄦ棩鏈熺暀绌哄垯涓嶆瘮杈冭缁村害
+ if (old.getPrice().compareTo(row.getPrice()) != 0
+ || !StrUtil.equals(StrUtil.trimToEmpty(old.getInvoiceTitle()),
+ StrUtil.trimToEmpty(row.getInvoiceTitle()))) {
+ continue;
+ }
+ if (invoiceTime != null && !invoiceTime.equals(old.getInvoiceTime())) {
+ continue;
+ }
+ return old;
+ }
+ return null;
+ }
+
+ private IllegalArgumentException duplicateRowException(int firstRowNo, String keyDesc) {
+ return new IllegalArgumentException("妫�娴嬪埌閲嶅瀵煎叆锛氭湰娆″鍏ョ殑绗� " + firstRowNo
+ + " 琛屼笌褰撳墠琛岀殑鏁版嵁瀹屽叏鐩稿悓锛�" + keyDesc + "锛夈�傝鍒犻櫎妯℃澘涓噸澶嶇殑琛屽悗閲嶆柊瀵煎叆");
+ }
+
+ private IllegalArgumentException duplicateExistingException(String keyDesc, String existingNo) {
+ return new IllegalArgumentException("妫�娴嬪埌閲嶅瀵煎叆锛�" + keyDesc + " 涓庡凡鏈夊紑绁ㄥ崟銆�" + existingNo
+ + "銆嶅畬鍏ㄧ浉鍚岋紙鍙兘鏄湰娆″鍏ュ墠闈㈢殑琛岋紝涔熷彲鑳芥槸绯荤粺涓凡鏈夊崟鎹級銆傝鍕块噸澶嶅鍏ワ紱濡傜‘闇�閲嶆柊瀵煎叆锛岃鍏堝湪銆岃惀閿�绠$悊-寮�绁ㄧ鐞嗐�嶄腑鍒犻櫎璇ュ紑绁ㄥ崟鍚庨噸璇�");
+ }
+
+ private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
+ // 1. 璁$畻鍓╀綑鍙紑绁ㄩ噾棰濓紝涓嶅寘鎷� reqVO 鑷韩锛堝鎵逛腑涔熷崰鐢ㄩ搴︼紝闃叉绱寮�绁ㄨ秴杩囧悎鍚岄噾棰濓級
+ CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
+ BigDecimal notInvoicePrice = getRemainingInvoicePrice(contract, reqVO.getId());
// 2. 鏍¢獙閲戦鏄惁瓒呰繃
if (reqVO.getPrice().compareTo(notInvoicePrice) > 0) {
throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notInvoicePrice);
}
+ }
+
+ /**
+ * 璁$畻鍚堝悓鍓╀綑鍙紑绁ㄩ噾棰濓紙瀹℃壒涓�佸鏍搁�氳繃鐨勫紑绁ㄥ崰鐢ㄩ搴︼級
+ *
+ * @param contract 鍚堝悓
+ * @param excludeInvoiceId 闇�鎺掗櫎鐨勫紑绁ㄧ紪鍙凤紝鍙负绌�
+ */
+ private BigDecimal getRemainingInvoicePrice(CrmContractDO contract, Long excludeInvoiceId) {
+ List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(contract.getId(),
+ List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
+ if (excludeInvoiceId != null) {
+ invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), excludeInvoiceId));
+ }
+ return contract.getTotalPrice().subtract(
+ CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
}
private void validateRelationDataExists(CrmInvoiceSaveReqVO reqVO) {
@@ -147,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}}",
@@ -159,21 +654,33 @@
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 鍏嶅鎵规椂涓嶉檺鍒剁紪杈戠姸鎬侊紱姝e父瀹℃壒浠呰崏绋裤�佸鎵逛腑鍙互缂栬緫銆�
+ 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()); // 閬垮厤鎿嶄綔鏃ュ織鍑虹幇鈥滃垹闄よ礋璐d汉鈥濈殑鎯呭喌
@@ -216,6 +723,8 @@
invoiceMapper.deleteById(id);
// 2.3 鍒犻櫎鏁版嵁鏉冮檺
permissionService.deletePermission(CrmBizTypeEnum.CRM_INVOICE.getType(), id);
+ // 2.4 鍒犻櫎寮�绁ㄧ敓鎴愮殑鏀舵鑺傜偣锛堝鎵归�氳繃鐨勫紑绁ㄤ笉鍏佽鍒犻櫎锛屾晠姝ゅ鑺傜偣蹇呯劧鏈骇鐢熷洖娆撅級
+ deletePaymentNodesByInvoiceId(id);
// 3. 璁板綍鎿嶄綔鏃ュ織涓婁笅鏂�
LogRecordContext.putVariable("invoice", invoice);
@@ -232,7 +741,20 @@
throw exception(INVOICE_SUBMIT_FAIL_NOT_DRAFT);
}
+ // 1.1 閲嶆柊鏍¢獙鍙紑绁ㄩ噾棰濓紝闃叉绱寮�绁ㄨ秴杩囧悎鍚岄噾棰濓紙鑽夌鏈熷悗鍙兘鏈夊叾浠栧崟杩涘叆瀹℃壒涓級
+ validateInvoicePriceExceedsLimit(new CrmInvoiceSaveReqVO()
+ .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)));
@@ -297,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