215 lines
6.8 KiB
Markdown
215 lines
6.8 KiB
Markdown
# 报价单管理模块迁移方案 — 05 业务逻辑与特殊场景
|
||
|
||
> **原则**:本节为开发门禁。未在本节逐条对照 Java 快照实现前,**不得**修改 `backend/app/crud/crm/quote*.py` 或新增报价相关路由。
|
||
> **Java 快照**:`docs/crm-migration/reference/java/quote/service/impl/`
|
||
|
||
---
|
||
|
||
## 1. 定时任务 / 触发器
|
||
|
||
| 类型 | 报价单模块 | 说明 |
|
||
|------|------------|------|
|
||
| MySQL 触发器 | 无 | |
|
||
| 定时 Job | 无 | 过期状态可在访问分享链时懒更新,与 Java `viewByShareCode` 一致 |
|
||
| Spring 事件 | 无 | |
|
||
|
||
---
|
||
|
||
## 2. 状态机
|
||
|
||
```
|
||
draft(草稿)
|
||
→ submitted(已提交) # submitQuote
|
||
→ invalid(已作废) # invalidQuote(draft 也可作废)
|
||
|
||
submitted
|
||
→ draft # withdrawApproval(删 pending 审批)
|
||
→ accepted(审批通过) # 全部 approver approved
|
||
→ rejected(审批失败) # 任一 approver rejected
|
||
→ expired(已过期) # expire_date 过期 / 分享访问时懒更新
|
||
→ invalid # invalidQuote
|
||
|
||
accepted
|
||
→ contract(已转合同) # toContract
|
||
→ invalid
|
||
|
||
contract / invalid → 终态(除 invalid 重复作废报错)
|
||
```
|
||
|
||
| 规则 | Java 行为(须对齐) |
|
||
|------|---------------------|
|
||
| 编辑 | 仅 `draft`(`updateByBo`) |
|
||
| 删除 | 仅 `draft`;已 `contract_id` 不可删 |
|
||
| 提交 | 仅 `draft`;提交后创建审批 + 尝试生成 PDF(失败不阻断) |
|
||
| 转合同 | 仅 `accepted`;无产品明细拒绝;不可重复转 |
|
||
| 作废 | 非 `invalid` 均可;remark 追加 `\n【作废原因】{remark}` |
|
||
| 分享访问 | 可能设 `viewed`(字典无此项,保留 Java 行为) |
|
||
|
||
**状态 dict_value 禁止修改**:`draft` / `submitted` / `accepted` / `rejected` / `expired` / `contract` / `invalid`
|
||
|
||
---
|
||
|
||
## 3. 报价单编号 `generateQuoteNo`
|
||
|
||
格式:`{供方编码}DD-{yyyyMMdd}{3位序号}`,例 `SXDD-20251017001`
|
||
|
||
| 规则 | Java |
|
||
|------|------|
|
||
| 默认供方编码 | `SX`(仅 `supplier_id` 为空时;P0 供方模块上线后须从 `crm_supplier_info.supplier_code` 读取) |
|
||
| 序号范围 | 同前缀 + 同 `quote_date` 下递增 |
|
||
| 冲突 | 最多重试 10 次 |
|
||
| 新增 | 空号或含 `***` 预览标记时生成 |
|
||
| 改报价日期 | **整号重生成**,标题同步更新 |
|
||
| 改供方 | 只替换编码前缀,**保留** `DD-yyyyMMdd001` 段 |
|
||
| 改客户/供方 | 按 `quoteType` 重算 `quoteTitle` |
|
||
|
||
---
|
||
|
||
## 4. 报价主题自动生成
|
||
|
||
| quoteType | 格式 |
|
||
|-----------|------|
|
||
| `sales`(默认) | `{供方简称}-{客户简称}({quoteNo})` |
|
||
| `purchase` | `{客户简称}-{供方简称}({quoteNo})` |
|
||
|
||
供方简称默认 `山峡`;客户/供方简称来自主数据 `shortName`。
|
||
|
||
---
|
||
|
||
## 5. 金额计算 `calculateAmount`
|
||
|
||
```
|
||
行小计 = price × quantity × (1 - line_discount/100)
|
||
total_amount = Σ 行小计
|
||
discount_amount = total_amount × discount_rate/100
|
||
final_amount = total_amount - discount_amount
|
||
```
|
||
|
||
无产品时:三项金额均为 0。
|
||
|
||
---
|
||
|
||
## 6. 新增 `insertByBo` 默认值
|
||
|
||
| 字段 | 默认 |
|
||
|------|------|
|
||
| `version` | `V1.0` |
|
||
| `parent_id` | `0` |
|
||
| `is_latest` | `1` |
|
||
| `status` | `draft` |
|
||
| `quote_type` | `sales` |
|
||
| `validity_days` | `30` |
|
||
| `quote_date` | 今天 |
|
||
| `expire_date` | `quote_date + validity_days` |
|
||
| `owner_user_id` | 当前用户 |
|
||
|
||
保存前:`resolveQuoteProductIds` → `calculateAmount` → insert 主表 → `batchInsert` 明细。
|
||
|
||
---
|
||
|
||
## 7. 修改 `updateByBo`
|
||
|
||
- 校验旧单 `status == draft`
|
||
- 产品明细:**先 `deleteByQuoteId` 再 `batchInsert`**
|
||
- 客户/供方/报价日期变更时的编号与标题规则见 §3、§4
|
||
|
||
---
|
||
|
||
## 8. 提交 `submitQuote`
|
||
|
||
1. `status`: `draft` → `submitted`
|
||
2. `createApprovalRecords(id, approverIds)` — step 从 1 递增
|
||
3. 调用 PDF 生成;成功则 `pdf_url`、`pdf_generated=1`(异常仅 log)
|
||
|
||
---
|
||
|
||
## 9. 审批 `approveQuote`
|
||
|
||
| approvalResult | 主单 status |
|
||
|----------------|-------------|
|
||
| `approved` | 若该 quote 无剩余 `pending` → `accepted` |
|
||
| `rejected` | 立即 `rejected`,`reject_reason = "审批拒绝: " + opinion` |
|
||
|
||
审批记录:`pending` → `approved`/`rejected`,写 `approval_time`。
|
||
|
||
---
|
||
|
||
## 10. 撤回 `withdrawApproval`(审批 Service)
|
||
|
||
- 主单须 `submitted`
|
||
- 物理删除(或软删,与 Java delete 一致)所有 `pending` 审批
|
||
- 主单 `status` → `draft`
|
||
|
||
---
|
||
|
||
## 11. 新版本 `createNewVersion`
|
||
|
||
- 复制主表字段 + 产品明细
|
||
- 新单:`status=draft`,`is_latest=1`,`parent_id=旧 id`,新 `version` 字符串
|
||
- 旧单:`is_latest=0`
|
||
- **同 `quote_no`**
|
||
|
||
---
|
||
|
||
## 12. 转合同 `toContract`
|
||
|
||
1. 校验 `accepted`、无 `contract_id`、有产品
|
||
2. 构建 `CrmContractBo`(`contract_type=sale`,金额=`final_amount`,备注含 quoteNo)
|
||
3. 复制产品 → `CrmContractProductBo`(`delivery_status=pending`)
|
||
4. 调用合同 `insertByBo`
|
||
5. 报价单:`status=contract`,`contract_id=新合同 id`
|
||
|
||
**依赖**:P1 合同 `insertByBo` 已实现(见 `plans/contract/01-prerequisite-create-for-quote.md`)。
|
||
|
||
---
|
||
|
||
## 13. 分享 `generateShare` / `viewByShareCode`
|
||
|
||
| 项 | Java |
|
||
|----|------|
|
||
| share_code | 8 位随机 |
|
||
| share_link | 配置域名 + shareCode(Java 硬编码 localhost,门户改配置) |
|
||
| 密码 | 可选明文存 `share_password` |
|
||
| 过期 | `share_expire_time = now + validityDays` |
|
||
| 公开访问 | 无 JWT;校验 share_code、密码、分享过期、报价 expire_date |
|
||
| 访问副作用 | view_count+1、last_view_time、写 share_log、可能改 status |
|
||
|
||
---
|
||
|
||
## 14. PDF / 模板
|
||
|
||
| API | 行为 |
|
||
|-----|------|
|
||
| `saveTemplate` | 仅更新 `template_content`(LambdaUpdate 避免乐观锁) |
|
||
| `getTemplate` | 返回 longtext HTML |
|
||
| `generatePdf` | HTML 渲染 → 存文件(Java 实为 HTML,扩展名 .pdf)→ MinIO URL |
|
||
|
||
模板文件见快照:`resources/templates/quote-pdf.html`
|
||
|
||
---
|
||
|
||
## 15. 产品 ID 解析(外部)
|
||
|
||
`CrmProductServiceImpl.resolveProductIdForQuoteItem`:按名称/规格/分类/单位/税率匹配;未命中则**自动新建产品**。
|
||
|
||
门户须在 `product.py` 实现同名语义,或在 `quote.py` 内对照 Java 内联。
|
||
|
||
---
|
||
|
||
## 16. 开发对照打勾表
|
||
|
||
实施每个 Python 函数前,在 Java 快照中定位方法并在此打勾:
|
||
|
||
- [ ] `buildQueryWrapper`
|
||
- [ ] `insertByBo` / `updateByBo` / `deleteWithValidByIds`
|
||
- [ ] `queryById`(含 productList、冗余填充)
|
||
- [ ] `submitQuote`
|
||
- [ ] `approveQuote` / `withdrawApproval` / `canUserApprove`
|
||
- [ ] `createNewVersion` / `queryVersions`
|
||
- [ ] `toContract`
|
||
- [ ] `generateShare` / `viewByShareCode`
|
||
- [ ] `invalidQuote` / `acceptQuote` / `rejectQuote`
|
||
- [ ] `statisticsOverview` / `statisticsStatus` / `statisticsTrend`
|
||
- [ ] `saveTemplate` / `getTemplate` / `generatePdf`
|
||
- [ ] `resolveProductIdForQuoteItem`(产品模块)
|