245 lines
10 KiB
Markdown
245 lines
10 KiB
Markdown
# 合同管理 — 跨模块调用梳理(步骤 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 / 开票双轨 / 入站出站 / 门户策略 |
|