Files
shanxia2.0/docs/crm-migration/plans/contract/05-cross-module-calls.md
T
2026-06-01 08:21:21 +08:00

245 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 合同管理 — 跨模块调用梳理(步骤 6)
> **权威源码**:`shanxia/crm/backend/ruoyi-modules/ruoyi-crm/`
> **日期**:2026-05-31
> **目的**:Python 重写 `CrmContractServiceImpl` 时,明确哪些金额字段由合同自身维护、哪些由回款/开票模块回调,避免漏实现或双轨不一致。
---
## 1. 总览
```mermaid
flowchart LR
subgraph inbound["入站:谁调用合同 Service"]
Q[QuoteServiceImpl]
O[OpportunityServiceImpl]
P[PaymentServiceImpl]
CC[CrmContractController]
end
subgraph contract["CrmContractServiceImpl"]
insert[insertByBo / updateByBo]
recv[updateReceivedAmount]
inv[updateInvoiceAmount ⚠️ 未被调用]
end
subgraph outbound["出站:合同调用谁"]
CP[ContractProductService]
PP[PaymentPlanService.batchGenerate]
SI[SupplierInfo / Customer / Contact / User]
QT[Quote / Opportunity Mapper 写回]
end
subgraph invoice["CrmInvoiceServiceImpl"]
inline[updateContractInvoiceAmount 私有方法]
end
Q -->|insertByBo| insert
O -->|insertByBo + generateContractNo| insert
P -->|queryById 校验| contract
P -->|5 处回调| recv
CC --> contract
insert --> CP
insert --> PP
insert --> SI
invoice -->|issue / invalid| inline
inline -->|直接 UPDATE crm_contract| contract
```
| 合同金额字段 | 写入方式 | 当前门户 shanxia2.0 |
|-------------|---------|---------------------|
| `received_amount` / `unreceived_amount` | 回款模块回调 `updateReceivedAmount` | 回款模块**未移植**;列表/详情**只读展示** DB 字段 |
| `invoice_amount` / `uninvoice_amount` | 开票模块私有 `updateContractInvoiceAmount`(**非** `updateInvoiceAmount`) | 开票模块**未移植**;同上只读 |
---
## 2. `updateReceivedAmount(contractId)`
### 2.1 实现逻辑(Java)
位置:`CrmContractServiceImpl.java` L1264–1295。
1. 查询 `crm_payment`:`contract_id = ?` 且 `del_flag = 0`
2. **对所有记录** `amount` 求和 → `receivedAmount`(**不过滤** `status`)
3. 读合同 `amount`
4. 更新合同:`received_amount = 求和`,`unreceived_amount = amount - received_amount`
5. `@Transactional`;合同不存在时 log error 并返回 `false`
**与回款统计的差异**:`PaymentServiceImpl.statisticsOverview` 仅把 `status = confirmed` 计入金额;`updateReceivedAmount` **包含 pending / confirmed / invalid / rejected** 等所有未删记录。新增 pending 回款时也会立刻回调,而 `reject()` **不**回调(注释写「待确认未计入」,与 insert 行为矛盾)。Python 重写须**原样复刻**此行为,除非产品单独立项修正。
### 2.2 触发点(`CrmPaymentServiceImpl`)
| 方法 | 条件 | 说明 |
|------|------|------|
| `insertByBo` | `entity.contractId != null` | 插入后立即回调(状态为 `pending`) |
| `updateByBo` | 更新成功且 `entity.contractId != null` | 仅更新**新** contractId;若改绑合同,**不**刷新旧合同 |
| `deleteWithValidByIds` | 删除成功后,收集到的每个 `contractId` | 仅允许删 `pending` |
| `confirm` | 确认成功且有关联合同 | |
| `invalid` | 作废成功且有关联合同 | |
| `reject` | — | **不调用** `updateReceivedAmount` |
插入前校验:`queryById(contractId)` 读 `unreceivedAmount`(空则取 `amount`),要求 `bo.amount <= unreceivedAmount`。
### 2.3 门户现状
- `module-status.md`:回款管理(`crm_payment` + 计划表)**未开始**
- 合同重写阶段:**实现** `updateReceivedAmount` 函数供将来回款模块 import;合同 CRUD **不**主动重算(除非 Java 同等逻辑在 insert 初始化 0)
- 步骤 26 恢复 `quote.to_contract` 后,新合同 `received_amount = 0` 即可
---
## 3. 开票金额:两套逻辑并存
### 3.1 `updateInvoiceAmount(contractId)` — 接口存在、**无调用方**
位置:`CrmContractServiceImpl.java` L1981–2012。
1. 查 `crm_invoice`:`contract_id = ?`,`del_flag = 0`
2. 对 **`amount`(不含税)** 求和
3. `invoice_amount = 求和`,`uninvoice_amount = contract.amount - 求和`
全库 grep:**无任何 Service 调用**此方法(仅接口 + 实现定义)。
### 3.2 `CrmInvoiceServiceImpl.updateContractInvoiceAmount` — **实际生效路径**
私有方法 L838–873;**直接** `contractMapper.updateById`,不经过 `ICrmContractService`。
| 发票方法 | 何时更新合同 | 累加字段 | 方向 |
|---------|-------------|---------|------|
| `insertByBo` | 否 | — | 申请中不计入 |
| `updateByBo` | 否 | — | |
| `approve` | 否 | — | 审批通过仍不计入 |
| **`issue`** | 是 | **`totalAmount`(价税合计)** | `isAdd = true` |
| **`invalid`** | 是(状态为 invoiced/delivered/received) | **`totalAmount`** | `isAdd = false` |
| `express` / `receive` | 否 | — | |
新增/审批前校验:读合同 `uninvoiceAmount`,用本次 **`totalAmount`** 与剩余可开比较(`insertByBo` L131–141)。
**与 `updateInvoiceAmount` 的不一致**:
| 维度 | `updateInvoiceAmount` | `updateContractInvoiceAmount` |
|------|----------------------|------------------------------|
| 汇总字段 | `invoice.amount` | 开票/作废时的 `totalAmount` |
| 触发时机 | 全表重算(若被调用) | 增量加减 |
| 是否在用 | 否 | 是 |
Python 重写建议:
- 合同模块保留 `update_invoice_amount()`(全量重算版,对齐 Java 接口,供运维/迁移脚本)
- 开票模块移植时实现 **`update_contract_invoice_amount_increment`**,行为对齐 `CrmInvoiceServiceImpl` 私有方法
- **不要**在开票 `issue/invalid` 时改调全量重算,除非产品确认统一
### 3.3 门户现状
- `crm_invoice` / `crm_invoice_item`:**未移植**
- 已有 `crm_invoice_info` 是**客户开票信息**,与合同已开票金额无关
---
## 4. 合同模块内部对金额字段的处理
### 4.1 `insertByBo`
- `received_amount` / `invoice_amount` 默认 `0`
- `unreceived_amount = amount - received_amount`
- `uninvoice_amount = amount - invoice_amount`
- 若 BO 带 `paymentPlans` → `paymentPlanService.batchGenerate(contractId, plans)`(回款**计划**,非实收)
### 4.2 `updateByBo`(仅 draft)
- 修改 `amount` 时:**保留**原 `received_amount` / `invoice_amount`
- 重算 `unreceived_amount` / `uninvoice_amount` = 新 `amount` − 原已收/已开
### 4.3 `renewContract`
- 新合同:`received_amount = 0`,`invoice_amount = 0`,`unreceived/uninvoice = amount`
### 4.4 统计 API
`statisticsOverview` / `statisticsTrend` 等对列表合同字段 **SUM**,不实时查 payment/invoice 子表。
---
## 5. 其他入站调用(非金额)
| 调用方 | 方法 | 用途 |
|--------|------|------|
| `CrmQuoteServiceImpl.toContract` | `contractService.insertByBo` | 报价转合同(非 HTTP `createFromQuote`) |
| `CrmOpportunityServiceImpl` 赢单 | `generateContractNo` + `insertByBo` | 自动建 draft 合同,回写 `opp.contract_id` |
| `CrmContractController` | 全套 REST | 见 `04-api-spec.md` |
| `CrmPaymentServiceImpl` | `queryById` | 回款金额校验 |
`createFromQuote` / `createFromOpportunity`(Controller 入口)内部也是 `insertByBo` + 更新 quote/opportunity 状态。
---
## 6. 合同模块出站依赖(重写时一并注册)
| 依赖 | 用途 |
|------|------|
| `ICrmContractProductService` | 产品明细 CRUD、续签复制 |
| `CrmContractProductPriceTierMapper` | 框架合同调价区间 |
| `ICrmContractApprovalService` | `canUserApprove` |
| `ICrmPaymentPlanService` | 新建合同时 `batchGenerate` |
| `CrmPaymentMapper` | 仅 `updateReceivedAmount` 读表 |
| `CrmInvoiceMapper` | 仅 `updateInvoiceAmount` 读表(当前无外部触发) |
| `CrmQuoteMapper` / `CrmOpportunityMapper` | createFrom* / 商机赢单 |
| `CrmContractTemplateMapper` | createFromTemplate |
| `ICrmSupplierInfoService` | 供方简称、编号规则 |
| `ICrmProductService` | 商机转合同自动建产品 |
| `UserService` | 负责人姓名 |
| Customer / Contact Mapper | 冗余名称填充 |
---
## 7. 前端关联(步骤 7+ 适配时注意)
| 位置 | 字段 | 说明 |
|------|------|------|
| `contract/index.vue` 列表/详情 | `receivedAmount`、`unreceivedAmount`、`invoiceAmount`、`uninvoiceAmount` | 来自合同 VO,非子表实时查询 |
| `ExecuteDialog.vue` | 同上 + 回款率 | 打开执行弹窗前读合同详情 |
| `InvalidDialog.vue` L111 | `res.data.invoicedAmount` | **字段名错误**:Java VO 为 `invoiceAmount`;作废前开票提示可能始终为 0,步骤 27 UI 适配时修正 |
---
## 8. shanxia2.0 实施策略
### 阶段 D(合同后端重写)
1. 在 `crud/crm/contract.py` 实现 `update_received_amount(db, contract_id)`,逻辑对齐 §2.1
2. 同文件实现 `update_invoice_amount(db, contract_id)`(对齐 §3.1,预留接口)
3. `insert_by_bo` / `update_by_bo` 按 §4 维护四个金额字段
4. **不**在合同路由内实现回款/开票 CRUD
### 回款/开票模块移植后
| 模块 | 必须回调 |
|------|---------|
| 回款 | `insert` / `update` / `delete` / `confirm` / `invalid` → `update_received_amount`(**不含** `reject`) |
| 开票 | `issue` / `invalid` → 增量更新(对齐 §3.2),或封装为 shared helper |
### 步骤 26
恢复 `quote.to_contract` 时调用新 `insert_by_bo`,无需触发回款/开票回调。
---
## 9. Python 重写检查清单
- [ ] `update_received_amount`:payment 求和 **无 status 过滤**,`del_flag=0`
- [ ] 回款 5 个触发点与 Java 一致;`reject` 不回调
- [ ] `update_invoice_amount` 全量重算实现但默认不被开票模块调用
- [ ] 开票模块:`issue/invalid` 用 **totalAmount** 增量更新(将来实现)
- [ ] `update_by_bo` 改 `amount` 时不覆盖已收/已开累计值
- [ ] `renew_contract` 新合同金额字段清零
- [ ] `insert_by_bo` 可选 `payment_plans` → 调用 payment_plan 模块(若未移植则跳过或 stub)
- [ ] 前端 `InvalidDialog`:`invoicedAmount` → `invoiceAmount`
---
## 变更记录
| 日期 | 说明 |
|------|------|
| 2026-05-31 | 步骤 6:梳理 updateReceivedAmount / 开票双轨 / 入站出站 / 门户策略 |