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