gaoluyang
8 小时以前 a1dfce6c7266bf922ab913a922219a554f71ade2
宁夏万通需求:CRM 开票新增时客户、合同改为非必填

- CrmInvoiceSaveReqVO.contractId 去掉 @NotNull(原来「合同编号不能为空」)
- CrmInvoiceServiceImpl:
· validateInvoicePriceExceedsLimit 在未关联合同时直接返回——没有合同就没有「合同剩余可开票金额」可校验
· validateRelationDataExists 补上 customerService.validateCustomer(客户非空时校验存在)
- crm_invoice.customer_id / contract_id 由 NOT NULL 改为可空(生产库 mom-wtxc 已执行 ALTER,数据未动)
- 合并初始化脚本 config_export_all_20260928.sql 的 crm_invoice 建表段同步为 DEFAULT NULL

Excel 导入路径仍要求合同(模板带合同编号列,用于算剩余可开票额度),本次只放开表单新增。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改3个文件
21 ■■■■■ 文件已修改
docs/sql/config_export_all_20260928.sql 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceSaveReqVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20260928.sql
@@ -2508,8 +2508,8 @@
CREATE TABLE `crm_invoice` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '开票编号',
  `customer_id` bigint NOT NULL COMMENT '客户编号',
  `contract_id` bigint NOT NULL COMMENT '合同编号',
  `customer_id` bigint DEFAULT NULL COMMENT '客户编号',
  `contract_id` bigint DEFAULT NULL COMMENT '合同编号',
  `owner_user_id` bigint NOT NULL COMMENT '负责人编号',
  `invoice_type` int NOT NULL COMMENT '发票类型(字典 crm_invoice_type)',
  `invoice_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '发票号码',
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceSaveReqVO.java
@@ -19,11 +19,10 @@
    private String no;
    @Schema(description = "客户编号", example = "2")
    private Long customerId; // 该字段不通过前端传递,而是 contractId 查询出来设置进去
    private Long customerId; // 传了 contractId 时以合同上的客户为准(由 Service 覆盖)
    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "合同编号不能为空")
    private Long contractId;
    @Schema(description = "合同编号", example = "1024")
    private Long contractId; // 非必填:允许不关联合同开票
    @Schema(description = "负责人编号", example = "25682")
    private Long ownerUserId;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -26,6 +26,7 @@
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
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.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
@@ -84,6 +85,8 @@
    @Resource
    private CrmContractService contractService;
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private CrmPermissionService permissionService;
@@ -347,6 +350,10 @@
    }
    private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
        // 未关联合同的(合同非必填),不占用任何合同的开票额度,无需校验上限
        if (reqVO.getContractId() == null) {
            return;
        }
        // 1. 计算剩余可开票金额,不包括 reqVO 自身(审批中也占用额度,防止累计开票超过合同金额)
        CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
        BigDecimal notInvoicePrice = getRemainingInvoicePrice(contract, reqVO.getId());
@@ -376,6 +383,9 @@
        if (reqVO.getOwnerUserId() != null) {
            adminUserApi.validateUser(reqVO.getOwnerUserId()); // 校验负责人存在
        }
        if (reqVO.getCustomerId() != null) {
            customerService.validateCustomer(reqVO.getCustomerId()); // 校验客户存在
        }
        if (reqVO.getContractId() != null) {
            CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
            if (ObjectUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {