Files
shanxia2.0/docs/crm-migration/plans/quote/05-special.md
T
2026-06-01 08:21:21 +08:00

215 lines
6.8 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.
# 报价单管理模块迁移方案 — 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`(产品模块)