6.8 KiB
6.8 KiB
报价单管理模块迁移方案 — 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
status:draft→submittedcreateApprovalRecords(id, approverIds)— step 从 1 递增- 调用 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
- 校验
accepted、无contract_id、有产品 - 构建
CrmContractBo(contract_type=sale,金额=final_amount,备注含 quoteNo) - 复制产品 →
CrmContractProductBo(delivery_status=pending) - 调用合同
insertByBo - 报价单:
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 快照中定位方法并在此打勾:
buildQueryWrapperinsertByBo/updateByBo/deleteWithValidByIdsqueryById(含 productList、冗余填充)submitQuoteapproveQuote/withdrawApproval/canUserApprovecreateNewVersion/queryVersionstoContractgenerateShare/viewByShareCodeinvalidQuote/acceptQuote/rejectQuotestatisticsOverview/statisticsStatus/statisticsTrendsaveTemplate/getTemplate/generatePdfresolveProductIdForQuoteItem(产品模块)