gaoluyang
3 天以前 c5dfd00d511914ee59b10f5c6cc84a1d1ae50bb9
feat(crm): 回款合同与关联开票改为非必填

- 保存 VO 去掉 contractId、invoiceId 的 @NotNull 校验
- 新增/修改回款时,未填合同或开票则跳过对应的金额与开票校验
- crm_receivable.contract_id 放开为可空,补充迁移脚本并同步初始化脚本

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改3个文件
53 ■■■■■ 文件已修改
docs/sql/config_export_all_20260904.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/crm_receivable_optional_contract.sql 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20260904.sql
@@ -4401,7 +4401,7 @@
  `no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '回款编号',
  `plan_id` bigint DEFAULT NULL COMMENT '回款计划编号',
  `customer_id` bigint NOT NULL COMMENT '客户编号',
  `contract_id` bigint NOT NULL COMMENT '合同编号',
  `contract_id` bigint DEFAULT NULL COMMENT '合同编号',
  `invoice_id` bigint DEFAULT NULL COMMENT '关联的开票编号,关联 crm_invoice.id',
  `owner_user_id` bigint DEFAULT NULL COMMENT '负责人编号',
  `return_time` datetime DEFAULT NULL COMMENT '回款日期',
docs/sql/crm_receivable_optional_contract.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
-- å›žæ¬¾ç®¡ç†çš„「合同名称」「关联开票」改为非必填
--
-- èƒŒæ™¯ï¼šä¸šåŠ¡å…è®¸ä¸å…³è”åˆåŒã€ä¸å…³è”å¼€ç¥¨ç›´æŽ¥ç™»è®°å›žæ¬¾ã€‚
--       ä»£ç ä¾§å·²åŽ»æŽ‰æ ¡éªŒï¼Œä½† crm_receivable.contract_id ä»ä¸º NOT NULL,
--       ä¸æ”¾å¼€ä¼šå¯¼è‡´æ–°å¢žï¼ˆæ— åˆåŒï¼‰å›žæ¬¾æ—¶æŠ¥ Field 'contract_id' doesn't have a default value。
--       invoice_id æœ¬èº«å·²æ˜¯ NULL å¯ç©ºï¼Œæ— éœ€å¤„理。
--
-- æœ¬è„šæœ¬å¹‚ç­‰ï¼Œå¯é‡å¤æ‰§è¡Œã€‚
ALTER TABLE crm_receivable
    MODIFY COLUMN contract_id bigint DEFAULT NULL COMMENT '合同编号';
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java
@@ -27,14 +27,12 @@
    @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
    private Long customerId; // è¯¥å­—段不通过前端传递,而是 contractId æŸ¥è¯¢å‡ºæ¥è®¾ç½®è¿›åŽ»
    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
    @Schema(description = "合同编号", example = "2")
    @DiffLogField(name = "合同", function = CrmContractParseFunction.NAME)
    @NotNull(message = "合同编号不能为空")
    private Long contractId;
    @Schema(description = "关联的开票编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
    @Schema(description = "关联的开票编号", example = "25787")
    @DiffLogField(name = "关联开票", function = CrmInvoiceParseFunction.NAME)
    @NotNull(message = "关联开票编号不能为空")
    private Long invoiceId;
    @Schema(description = "回款计划编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -80,12 +80,17 @@
    public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒå…³è”数据存在
        validateRelationDataExists(createReqVO);
        // 1.2 æ ¡éªŒå…³è”开票合格(审批通过 + å·²ä¸Šä¼ å‘票附件 + ä¸ŽåˆåŒåŒ¹é…ï¼‰
        validateInvoiceForReceivable(createReqVO);
        // 1.3 æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过合同剩余可回款金额
        validateReceivablePriceExceedsLimit(createReqVO);
        // 1.4 æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过该开票剩余可回款金额(先开票后回款)
        validateReceivablePriceExceedsInvoiceRemaining(createReqVO);
        // 1.2 åˆåŒã€å…³è”开票均为非必填:未选择时跳过对应校验,仅按已填写的维度约束金额
        if (createReqVO.getContractId() != null) {
            // æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过合同剩余可回款金额
            validateReceivablePriceExceedsLimit(createReqVO);
        }
        if (createReqVO.getInvoiceId() != null) {
            // æ ¡éªŒå…³è”开票合格(审批通过 + å·²ä¸Šä¼ å‘票附件 + ä¸ŽåˆåŒåŒ¹é…ï¼‰
            validateInvoiceForReceivable(createReqVO);
            // æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过该开票剩余可回款金额(先开票后回款)
            validateReceivablePriceExceedsInvoiceRemaining(createReqVO);
        }
        // 1.5 ç”Ÿæˆå›žæ¬¾ç¼–号
        String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
        if (receivableMapper.selectByNo(no) != null) {
@@ -220,12 +225,17 @@
        if (updateReqVO.getInvoiceId() == null) {
            updateReqVO.setInvoiceId(oldReceivable.getInvoiceId());
        }
        // 1.3 æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过合同剩余可回款金额
        validateReceivablePriceExceedsLimit(updateReqVO);
        // 1.4 æ ¡éªŒå…³è”开票合格
        validateInvoiceForReceivable(updateReqVO);
        // 1.5 æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过该开票剩余可回款金额
        validateReceivablePriceExceedsInvoiceRemaining(updateReqVO);
        // 1.3 åˆåŒã€å…³è”开票均为非必填:未填写时跳过对应校验
        if (updateReqVO.getContractId() != null) {
            // æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过合同剩余可回款金额
            validateReceivablePriceExceedsLimit(updateReqVO);
        }
        if (updateReqVO.getInvoiceId() != null) {
            // æ ¡éªŒå…³è”开票合格
            validateInvoiceForReceivable(updateReqVO);
            // æ ¡éªŒå¯å›žæ¬¾é‡‘额不超过该开票剩余可回款金额
            validateReceivablePriceExceedsInvoiceRemaining(updateReqVO);
        }
        // 1.6 åªæœ‰æœªå®¡æ ¸ï¼Œå¯ä»¥ç¼–辑;
        if (ObjectUtil.notEqual(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) {