From 6e312c64c056acf479a6dd8393dd42a80b69b7d4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 02 七月 2026 10:25:07 +0800
Subject: [PATCH] 1.新增销售报价 2.crm整合mdm
---
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java | 238 ++++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java | 13
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java | 36
.claude/settings.local.json | 4
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java | 24
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationSaveReqVO.java | 95 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationDO.java | 136 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java | 5
docs/crm_sales_flow.md | 444 ++++++++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/quotation/CrmSaleQuotationStatusEnum.java | 36
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationService.java | 125 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java | 446 ++++++++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/listener/CrmSaleQuotationStatusListener.java | 56 +
docs/crm_erp_integration_frontend.md | 986 +++++++++++++++++-
docs/crm_sale_quotation.sql | 86 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationProductDO.java | 80 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java | 3
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationPageReqVO.java | 38
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java | 171 +++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationMapper.java | 77 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java | 12
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java | 23
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractProductDO.java | 19
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationProductMapper.java | 34
24 files changed, 3,088 insertions(+), 99 deletions(-)
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index d156d7e..ec7ef59 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -23,9 +23,7 @@
"Bash(dir /s /b \"C:\\\\Program Files \\(x86\\)\\\\MySQL\\\\*\\\\bin\\\\mysql.exe\")",
"Bash(cmd.exe /c \"where mysql\")",
"Bash(cmd.exe /c \"dir /s /b C:\\\\ProgramData\\\\MySQL\\\\*\\\\mysql.exe\")",
- "Bash(cmd.exe /c \"dir /s /b C:\\\\Program Files\\\\MySQL\\\\*\\\\mysql.exe\")",
- "Bash(mysql -u root -p123456 ruoyi-vue-pro -e \"SELECT id, name, code, deleted FROM bpm_category WHERE code = 'purchase_approve' OR name LIKE '%閲囪喘瀹℃牳%';\")",
- "Bash(dir /s /b \"D:\\\\鐗涢┈\\\\鍗楅�歕\\\鍚庣\\\\mom-pro2-after\\\\yudao-module-mdm-api\\\\*.java\")"
+ "Bash(cmd.exe /c \"dir /s /b C:\\\\Program Files\\\\MySQL\\\\*\\\\mysql.exe\")"
],
"deny": [
"Bash(rm -rf *)",
diff --git a/docs/crm_erp_integration_frontend.md b/docs/crm_erp_integration_frontend.md
index 066a188..3a3afbc 100644
--- a/docs/crm_erp_integration_frontend.md
+++ b/docs/crm_erp_integration_frontend.md
@@ -1,46 +1,52 @@
-# CRM 涓� ERP 閿�鍞竴浣撳寲鏀归�� - 鍓嶇鏂囨。
+# CRM 涓� ERP 閿�鍞竴浣撳寲鏀归�� - 鍓嶇鏂囨。锛堝畬鏁寸増锛�
## 姒傝堪
-鏈鏀归�犲皢 CRM 瀹㈡埛浣滀负缁熶竴瀹㈡埛涓绘暟鎹紝ERP 閿�鍞ā鍧楋紙閿�鍞鍗曘�侀攢鍞嚭搴撱�侀攢鍞��璐с�佹敹娆惧崟锛夌粺涓�浣跨敤 CRM 瀹㈡埛銆�
+鏈鏀归�犲疄鐜� CRM 涓� ERP 閿�鍞ā鍧楃殑娣卞害闆嗘垚锛屽寘鎷細
+- **Phase 1**锛氬鎴锋暟鎹粺涓� - ERP 浣跨敤 CRM 瀹㈡埛
+- **Phase 2**锛氫骇鍝佹暟鎹粺涓� - CRM/ERP 浣跨敤 MDM 鐗╂枡
+- **Phase 3**锛氬悎鍚岀敓鎴愰攢鍞鍗�
+- **Phase 4**锛氶攢鍞叏娴佺▼杩芥函
+- **Phase 5**锛氳惀閿�鏁版嵁鎶ヨ〃
+- **Phase 6**锛氭暟鎹縼绉讳笌娓呯悊锛圫QL 閮ㄥ垎锛�
---
-## 涓�銆佹敼閫犳竻鍗�
+## 涓�銆佹敼閫犳竻鍗曟眹鎬�
-### 鍚庣鏀归�狅紙宸插畬鎴愶級
+### 1.1 鏂板妯″潡
+
+| 妯″潡 | 璇存槑 |
+|------|------|
+| `yudao-module-crm-api` | CRM API 妯″潡锛堝鎴� API銆佹姤琛� API銆佽拷婧� API锛� |
+| `yudao-module-erp-api` | ERP API 妯″潡锛堥攢鍞鍗� API锛� |
+| `yudao-module-mdm-api` | MDM API 妯″潡锛堢墿鏂� API锛� |
+
+### 1.2 鍚庣鏀归�犳枃浠�
| 鏂囦欢 | 鏀瑰姩璇存槑 |
|------|----------|
-| `yudao-module-crm-api/` | 鏂板 CRM 瀹㈡埛 API 妯″潡 |
-| `CrmCustomerApi.java` | 鏂板瀹㈡埛 API 鎺ュ彛 |
-| `CrmCustomerRespDTO.java` | 鏂板瀹㈡埛 DTO |
-| `CrmCustomerApiImpl.java` | 瀹炵幇 CRM 瀹㈡埛 API |
+| `CrmCustomerApi.java` | CRM 瀹㈡埛 API 鎺ュ彛 |
+| `CrmCustomerRespDTO.java` | 瀹㈡埛鍝嶅簲 DTO |
+| `CrmReportApi.java` | 钀ラ攢鎶ヨ〃 API 鎺ュ彛 |
+| `CrmSaleTraceApi.java` | 閿�鍞拷婧� API 鎺ュ彛 |
+| `ErpSaleOrderApi.java` | 閿�鍞鍗� API 鎺ュ彛 |
+| `MdmItemApi.java` | MDM 鐗╂枡 API 鎺ュ彛 |
| `ErpSaleOrderServiceImpl.java` | 浣跨敤 CRM 瀹㈡埛 API |
| `ErpFinanceReceiptServiceImpl.java` | 浣跨敤 CRM 瀹㈡埛 API |
-| `ErpSaleOrderController.java` | 浣跨敤 CRM 瀹㈡埛 API |
-| `ErpSaleOrderDO.java` | 鏇存柊娉ㄩ噴鍏宠仈 CRM 瀹㈡埛 |
-| `ErpSaleOutDO.java` | 鏇存柊娉ㄩ噴鍏宠仈 CRM 瀹㈡埛 |
-| `ErpSaleReturnDO.java` | 鏇存柊娉ㄩ噴鍏宠仈 CRM 瀹㈡埛 |
-| `ErpFinanceReceiptDO.java` | 鏇存柊娉ㄩ噴鍏宠仈 CRM 瀹㈡埛 |
-
-### 鍚庣鏀归�狅紙寰呭畬鎴愶級
-
-| 鏂囦欢 | 鏀瑰姩璇存槑 |
-|------|----------|
-| `ErpSaleOutController.java` | 浣跨敤 CRM 瀹㈡埛 API |
-| `ErpSaleReturnController.java` | 浣跨敤 CRM 瀹㈡埛 API |
-| `ErpFinanceReceiptController.java` | 浣跨敤 CRM 瀹㈡埛 API |
+| `CrmContractServiceImpl.java` | 鏂板鐢熸垚閿�鍞鍗曟柟娉� |
+| `CrmProductServiceImpl.java` | 浣跨敤 MDM 鐗╂枡 API |
+| `ErpProductServiceImpl.java` | 浣跨敤 MDM 鐗╂枡 API |
---
## 浜屻�佹秹鍙婇〉闈�
-### ERP 閿�鍞ā鍧�
+### 2.1 ERP 閿�鍞ā鍧�
| 椤甸潰 | 璺緞 | 鏀瑰姩璇存槑 |
|------|------|----------|
-| 閿�鍞鍗曞垪琛� | `/erp/sale/order/index` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛閫夋嫨缁勪欢 |
+| 閿�鍞鍗曞垪琛� | `/erp/sale/order/index` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛锛屾柊澧炲叧鑱斿悎鍚屽垪 |
| 閿�鍞鍗曡〃鍗� | `/erp/sale/order/form` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛閫夋嫨缁勪欢 |
| 閿�鍞嚭搴撳垪琛� | `/erp/sale/out/index` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛閫夋嫨缁勪欢 |
| 閿�鍞嚭搴撹〃鍗� | `/erp/sale/out/form` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛閫夋嫨缁勪欢 |
@@ -49,35 +55,43 @@
| 鏀舵鍗曞垪琛� | `/erp/finance/receipt/index` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛閫夋嫨缁勪欢 |
| 鏀舵鍗曡〃鍗� | `/erp/finance/receipt/form` | 瀹㈡埛閫夋嫨鏀逛负 CRM 瀹㈡埛閫夋嫨缁勪欢 |
+### 2.2 CRM 鍚堝悓妯″潡
+
+| 椤甸潰 | 璺緞 | 鏀瑰姩璇存槑 |
+|------|------|----------|
+| 鍚堝悓鍒楄〃 | `/crm/contract/index` | 娣诲姞"鐢熸垚閿�鍞鍗�"鎸夐挳 |
+| 鍚堝悓璇︽儏 | `/crm/contract/detail` | 鏄剧ず閿�鍞鍗曚俊鎭�侀攢鍞祦绋嬭拷婧� |
+
+### 2.3 CRM 鍟嗘満妯″潡
+
+| 椤甸潰 | 璺緞 | 鏀瑰姩璇存槑 |
+|------|------|----------|
+| 鍟嗘満璇︽儏 | `/crm/business/detail` | 娣诲姞閿�鍞祦绋嬭拷婧叆鍙� |
+
+### 2.4 CRM 瀹㈡埛妯″潡
+
+| 椤甸潰 | 璺緞 | 鏀瑰姩璇存槑 |
+|------|------|----------|
+| 瀹㈡埛璇︽儏 | `/crm/customer/detail` | 娣诲姞閿�鍞祦绋嬭拷婧垪琛� |
+
+### 2.5 CRM 鎶ヨ〃妯″潡锛堟柊澧烇級
+
+| 椤甸潰 | 璺緞 | 璇存槑 |
+|------|------|------|
+| 鎶ヨ〃姒傝 | `/crm/report/dashboard` | 鏁版嵁姒傝椤甸潰 |
+
---
-## 涓夈�佸鎴烽�夋嫨缁勪欢
+## 涓夈�丄PI 鎺ュ彛
-### 3.1 浣跨敤 CRM 瀹㈡埛閫夋嫨缁勪欢
-
-ERP 閿�鍞ā鍧楃殑瀹㈡埛閫夋嫨缁熶竴浣跨敤 CRM 瀹㈡埛閫夋嫨缁勪欢銆�
-
-```html
-<!-- 鍘熸潵锛欵RP 瀹㈡埛閫夋嫨 -->
-<el-select v-model="form.customerId" placeholder="璇烽�夋嫨瀹㈡埛">
- <el-option
- v-for="item in customerList"
- :key="item.id"
- :label="item.name"
- :value="item.id"
- />
-</el-select>
-
-<!-- 鏀逛负锛欳RM 瀹㈡埛閫夋嫨缁勪欢 -->
-<CrmCustomerSelect v-model="form.customerId" />
-```
-
-### 3.2 CRM 瀹㈡埛 API
+### 3.1 CRM 瀹㈡埛 API
| 鏂规硶 | 璺緞 | 璇存槑 |
|------|------|------|
| GET | /crm/customer/simple-list | 鑾峰彇瀹㈡埛绮剧畝鍒楄〃锛堢敤浜庝笅鎷夐�夋嫨锛� |
| GET | /crm/customer/list-by-ids | 鎵归噺鑾峰彇瀹㈡埛淇℃伅 |
+| GET | /rpc-api/crm/customer/get | 鑾峰彇鍗曚釜瀹㈡埛 |
+| POST | /rpc-api/crm/customer/validate | 鏍¢獙瀹㈡埛瀛樺湪 |
**瀹㈡埛绮剧畝鍒楄〃鍝嶅簲锛�**
@@ -96,26 +110,670 @@
}
```
----
+### 3.2 CRM 鍚堝悓 API锛堟柊澧烇級
-## 鍥涖�佸簾寮冪殑 ERP 瀹㈡埛绠$悊
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /crm/contract/generate-sale-order | 鏍规嵁鍚堝悓鐢熸垚閿�鍞鍗� |
-### 4.1 ERP 瀹㈡埛绠$悊椤甸潰搴熷純
+**璇锋眰鍙傛暟锛�**
-浠ヤ笅椤甸潰鏍囪涓哄簾寮冿紝寤鸿鍦ㄨ彍鍗曚腑闅愯棌鎴栧垹闄わ細
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 鍚堝悓缂栧彿 |
-- `/erp/customer/index` - ERP 瀹㈡埛鍒楄〃
-- `/erp/customer/form` - ERP 瀹㈡埛琛ㄥ崟
+**鍝嶅簲锛�**
+
+```json
+{
+ "code": 0,
+ "data": 12345,
+ "msg": "鎿嶄綔鎴愬姛"
+}
+```
+
+### 3.3 CRM 閿�鍞拷婧� API锛堟柊澧烇級
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| GET | /rpc-api/crm/sale-trace/get-by-business-id | 鏍规嵁鍟嗘満缂栧彿鑾峰彇杩芥函淇℃伅 |
+| GET | /rpc-api/crm/sale-trace/get-by-contract-id | 鏍规嵁鍚堝悓缂栧彿鑾峰彇杩芥函淇℃伅 |
+| GET | /rpc-api/crm/sale-trace/get-list-by-customer-id | 鏍规嵁瀹㈡埛缂栧彿鑾峰彇杩芥函鍒楄〃 |
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "businessId": 1,
+ "businessName": "鍟嗘満A",
+ "businessPrice": 100000,
+ "customerId": 1,
+ "customerName": "瀹㈡埛A",
+ "contractId": 1,
+ "contractName": "鍚堝悓A",
+ "contractPrice": 95000,
+ "contractStatus": 20,
+ "orderId": 1,
+ "orderNo": "SO001",
+ "orderPrice": 95000,
+ "receivablePrice": 50000,
+ "receivableItems": [
+ { "receivableId": 1, "receivableNo": "RK001", "price": 30000 }
+ ]
+ }
+}
+```
+
+### 3.4 CRM 钀ラ攢鎶ヨ〃 API锛堟柊澧烇級
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| GET | /rpc-api/crm/report/get-customer-analyze | 鑾峰彇瀹㈡埛鍒嗘瀽缁熻 |
+| GET | /rpc-api/crm/report/get-business-analyze | 鑾峰彇鍟嗘満鍒嗘瀽缁熻 |
+| GET | /rpc-api/crm/report/get-contract-analyze | 鑾峰彇鍚堝悓鍒嗘瀽缁熻 |
+| GET | /rpc-api/crm/report/get-sales-funnel | 鑾峰彇閿�鍞紡鏂楃粺璁� |
+| GET | /rpc-api/crm/report/get-sales-performance | 鑾峰彇閿�鍞笟缁╃粺璁� |
+
+**瀹㈡埛鍒嗘瀽鍝嶅簲锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "totalCount": 1000,
+ "todayCount": 15,
+ "dealCount": 300,
+ "dealRate": 30.00,
+ "lockCount": 50,
+ "poolCount": 200
+ }
+}
+```
+
+**鍟嗘満鍒嗘瀽鍝嶅簲锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "totalCount": 500,
+ "todayCount": 8,
+ "totalPrice": 5000000.00,
+ "successCount": 150,
+ "successPrice": 2000000.00,
+ "successRate": 30.00,
+ "processingCount": 250,
+ "failCount": 100
+ }
+}
+```
+
+**鍚堝悓鍒嗘瀽鍝嶅簲锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "totalCount": 400,
+ "todayCount": 5,
+ "totalPrice": 3000000.00,
+ "approveCount": 300,
+ "approvePrice": 2500000.00,
+ "processCount": 20,
+ "receivablePrice": 1500000.00,
+ "receivableRate": 60.00
+ }
+}
+```
+
+**閿�鍞紡鏂楀搷搴旓細**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "stages": [
+ { "name": "瀹㈡埛", "count": 1000, "price": 0, "conversionRate": 100.00 },
+ { "name": "鍟嗘満", "count": 500, "price": 5000000.00, "conversionRate": 50.00 },
+ { "name": "鍚堝悓", "count": 300, "price": 2500000.00, "conversionRate": 30.00 },
+ { "name": "鍥炴", "count": null, "price": 1500000.00, "conversionRate": 15.00 }
+ ]
+ }
+}
+```
+
+**閿�鍞笟缁╁搷搴旓細**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "items": [
+ {
+ "userId": 1,
+ "userName": "寮犱笁",
+ "deptName": "閿�鍞竴閮�",
+ "contractCount": 50,
+ "contractPrice": 500000.00,
+ "receivablePrice": 300000.00,
+ "businessCount": 80,
+ "businessPrice": 800000.00
+ }
+ ]
+ }
+}
+```
+
+### 3.5 ERP 閿�鍞鍗� API锛堟柊澧� RPC锛�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /rpc-api/erp/sale-order/create | 鍒涘缓閿�鍞鍗� |
+| GET | /rpc-api/erp/sale-order/get | 鑾峰彇閿�鍞鍗� |
+| GET | /rpc-api/erp/sale-order/get-by-contract-id | 鏍规嵁鍚堝悓缂栧彿鑾峰彇璁㈠崟 |
+
+### 3.6 MDM 鐗╂枡 API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /rpc-api/mdm/item/create | 鍒涘缓鐗╂枡 |
+| PUT | /rpc-api/mdm/item/update | 鏇存柊鐗╂枡 |
+| GET | /rpc-api/mdm/item/get | 鑾峰彇鐗╂枡 |
+| POST | /rpc-api/mdm/item/list | 鑾峰彇鐗╂枡鍒楄〃 |
+| GET | /rpc-api/mdm/item/validate-exists | 鏍¢獙鐗╂枡瀛樺湪 |
---
-## 浜斻�丼QL 鎵ц
+## 鍥涖�佸墠绔慨鏀圭偣
-瑙� `docs/crm_erp_integration_sql.sql`
+### 4.1 瀹㈡埛閫夋嫨缁勪欢
+
+ERP 閿�鍞ā鍧楃殑瀹㈡埛閫夋嫨缁熶竴浣跨敤 CRM 瀹㈡埛閫夋嫨缁勪欢銆�
+
+```html
+<!-- 鍘熸潵锛欵RP 瀹㈡埛閫夋嫨 -->
+<el-select v-model="form.customerId" placeholder="璇烽�夋嫨瀹㈡埛">
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+</el-select>
+
+<!-- 鏀逛负锛欳RM 瀹㈡埛閫夋嫨缁勪欢 -->
+<CrmCustomerSelect v-model="form.customerId" />
+```
+
+**缁勪欢瀹炵幇锛�**
+
+```html
+<!-- CrmCustomerSelect.vue -->
+<template>
+ <el-select
+ v-model="selectedValue"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ filterable
+ remote
+ :remote-method="searchCustomer"
+ @focus="loadCustomers"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ >
+ <span>{{ item.name }}</span>
+ <span style="color: #999; margin-left: 8px;">{{ item.mobile }}</span>
+ </el-option>
+ </el-select>
+</template>
+
+<script>
+import { getCustomerSimpleList } from '@/api/crm/customer'
+
+export default {
+ props: {
+ value: [Number, String]
+ },
+ data() {
+ return {
+ customerList: [],
+ selectedValue: this.value
+ }
+ },
+ watch: {
+ value(val) {
+ this.selectedValue = val
+ },
+ selectedValue(val) {
+ this.$emit('input', val)
+ }
+ },
+ methods: {
+ async loadCustomers() {
+ if (this.customerList.length === 0) {
+ const { data } = await getCustomerSimpleList()
+ this.customerList = data
+ }
+ },
+ async searchCustomer(query) {
+ if (query) {
+ // 鍙�夛細瀹炵幇杩滅▼鎼滅储
+ }
+ }
+ }
+}
+</script>
+```
+
+### 4.2 鍚堝悓鍒楄〃椤甸潰
+
+**鏂囦欢璺緞锛�** `/src/views/crm/contract/index.vue`
+
+**鏂板鎸夐挳锛�**
+
+```html
+<!-- 鍦ㄥ悎鍚屾搷浣滃垪娣诲姞 -->
+<el-table-column label="鎿嶄綔" width="200">
+ <template #default="{ row }">
+ <el-button
+ v-if="row.auditStatus === 20 && !row.orderId"
+ type="success"
+ size="small"
+ @click="handleGenerateOrder(row)"
+ >
+ 鐢熸垚閿�鍞鍗�
+ </el-button>
+ <el-button
+ v-if="row.orderId"
+ type="primary"
+ size="small"
+ @click="handleViewOrder(row)"
+ >
+ 鏌ョ湅璁㈠崟
+ </el-button>
+ </template>
+</el-table-column>
+```
+
+**鏂板鏂规硶锛�**
+
+```js
+import { generateSaleOrder } from '@/api/crm/contract'
+
+methods: {
+ async handleGenerateOrder(row) {
+ try {
+ await this.$confirm('纭鏍规嵁璇ュ悎鍚岀敓鎴愰攢鍞鍗曪紵', '鎻愮ず')
+ const { data } = await generateSaleOrder(row.id)
+ this.$message.success('鐢熸垚閿�鍞鍗曟垚鍔�')
+ this.getList()
+ } catch (e) {
+ if (e !== 'cancel') {
+ this.$message.error(e.message || '鐢熸垚澶辫触')
+ }
+ }
+ },
+ handleViewOrder(row) {
+ this.$router.push({
+ path: '/erp/sale/order/detail',
+ query: { id: row.orderId }
+ })
+ }
+}
+```
+
+### 4.3 閿�鍞鍗曞垪琛ㄩ〉闈�
+
+**鏂囦欢璺緞锛�** `/src/views/erp/sale/order/index.vue`
+
+**鏂板鍒楋細**
+
+```html
+<el-table-column label="鍏宠仈鍚堝悓" prop="contractNo" width="150">
+ <template #default="{ row }">
+ <el-link
+ v-if="row.contractId"
+ type="primary"
+ @click="handleViewContract(row)"
+ >
+ {{ row.contractNo }}
+ </el-link>
+ <span v-else>-</span>
+ </template>
+</el-table-column>
+```
+
+### 4.4 鍟嗘満璇︽儏椤甸潰 - 閿�鍞拷婧�
+
+**鏂囦欢璺緞锛�** `/src/views/crm/business/detail.vue`
+
+```html
+<el-card header="閿�鍞祦绋嬭拷婧�">
+ <el-timeline v-if="traceInfo">
+ <el-timeline-item
+ :timestamp="traceInfo.businessCreateTime"
+ color="#409EFF"
+ >
+ <el-card>
+ <h4>鍟嗘満鍒涘缓</h4>
+ <p>鍟嗘満鍚嶇О锛歿{ traceInfo.businessName }}</p>
+ <p>鍟嗘満閲戦锛毬{ traceInfo.businessPrice }}</p>
+ </el-card>
+ </el-timeline-item>
+
+ <el-timeline-item
+ v-if="traceInfo.contractId"
+ :timestamp="traceInfo.contractCreateTime"
+ color="#67C23A"
+ >
+ <el-card>
+ <h4>鍚堝悓绛捐</h4>
+ <p>鍚堝悓鍚嶇О锛歿{ traceInfo.contractName }}</p>
+ <p>鍚堝悓閲戦锛毬{ traceInfo.contractPrice }}</p>
+ </el-card>
+ </el-timeline-item>
+
+ <el-timeline-item
+ v-if="traceInfo.orderId"
+ :timestamp="traceInfo.orderCreateTime"
+ color="#E6A23C"
+ >
+ <el-card>
+ <h4>閿�鍞鍗�</h4>
+ <p>璁㈠崟鍙凤細
+ <el-link type="primary" @click="viewOrder(traceInfo.orderId)">
+ {{ traceInfo.orderNo }}
+ </el-link>
+ </p>
+ <p>璁㈠崟閲戦锛毬{ traceInfo.orderPrice }}</p>
+ </el-card>
+ </el-timeline-item>
+
+ <el-timeline-item v-if="traceInfo.receivablePrice > 0" color="#F56C6C">
+ <el-card>
+ <h4>鍥炴璁板綍</h4>
+ <p>宸插洖娆鹃噾棰濓細楼{{ traceInfo.receivablePrice }}</p>
+ </el-card>
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤閿�鍞祦绋嬫暟鎹�" />
+</el-card>
+```
+
+### 4.5 鎶ヨ〃姒傝椤甸潰
+
+**鏂囦欢璺緞锛�** `/src/views/crm/report/dashboard.vue`
+
+```html
+<template>
+ <div class="report-dashboard">
+ <!-- 缁熻鍗$墖 -->
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-title">瀹㈡埛鎬绘暟</div>
+ <div class="stat-value">{{ customerAnalyze.totalCount }}</div>
+ <div class="stat-footer">浠婃棩鏂板锛歿{ customerAnalyze.todayCount }}</div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-title">鍟嗘満鎬绘暟</div>
+ <div class="stat-value">{{ businessAnalyze.totalCount }}</div>
+ <div class="stat-footer">杞寲鐜囷細{{ businessAnalyze.successRate }}%</div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-title">鍚堝悓閲戦</div>
+ <div class="stat-value">楼{{ contractAnalyze.totalPrice }}</div>
+ <div class="stat-footer">宸插洖娆撅細楼{{ contractAnalyze.receivablePrice }}</div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-title">鍥炴鐜�</div>
+ <div class="stat-value">{{ contractAnalyze.receivableRate }}%</div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 閿�鍞笟缁╂帓琛� -->
+ <el-card header="閿�鍞笟缁╂帓琛�">
+ <el-table :data="performanceData.items" stripe>
+ <el-table-column prop="userName" label="閿�鍞汉鍛�" />
+ <el-table-column prop="deptName" label="閮ㄩ棬" />
+ <el-table-column prop="contractCount" label="鍚堝悓鏁伴噺" width="100" />
+ <el-table-column prop="contractPrice" label="鍚堝悓閲戦" width="150" />
+ <el-table-column prop="receivablePrice" label="鍥炴閲戦" width="150" />
+ </el-table>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import {
+ getCustomerAnalyze,
+ getBusinessAnalyze,
+ getContractAnalyze,
+ getSalesPerformance
+} from '@/api/crm/report'
+
+export default {
+ data() {
+ return {
+ customerAnalyze: {},
+ businessAnalyze: {},
+ contractAnalyze: {},
+ performanceData: { items: [] }
+ }
+ },
+ created() {
+ this.loadData()
+ },
+ methods: {
+ async loadData() {
+ const [c, b, ct, p] = await Promise.all([
+ getCustomerAnalyze(),
+ getBusinessAnalyze(),
+ getContractAnalyze(),
+ getSalesPerformance()
+ ])
+ this.customerAnalyze = c.data
+ this.businessAnalyze = b.data
+ this.contractAnalyze = ct.data
+ this.performanceData = p.data
+ }
+ }
+}
+</script>
+```
+
+### 4.6 API 瀹氫箟鏂囦欢
+
+**鏂囦欢璺緞锛�** `/src/api/crm/contract.js`
+
+```js
+// 鐢熸垚閿�鍞鍗�
+export function generateSaleOrder(id) {
+ return request({
+ url: '/crm/contract/generate-sale-order',
+ method: 'post',
+ params: { id }
+ })
+}
+```
+
+**鏂囦欢璺緞锛�** `/src/api/crm/saleTrace.js`
+
+```js
+// 鏍规嵁鍟嗘満缂栧彿鑾峰彇閿�鍞祦绋嬭拷婧俊鎭�
+export function getTraceByBusinessId(businessId) {
+ return request({
+ url: '/crm/sale-trace/get-by-business-id',
+ method: 'get',
+ params: { businessId }
+ })
+}
+
+// 鏍规嵁鍚堝悓缂栧彿鑾峰彇閿�鍞祦绋嬭拷婧俊鎭�
+export function getTraceByContractId(contractId) {
+ return request({
+ url: '/crm/sale-trace/get-by-contract-id',
+ method: 'get',
+ params: { contractId }
+ })
+}
+
+// 鏍规嵁瀹㈡埛缂栧彿鑾峰彇閿�鍞祦绋嬭拷婧垪琛�
+export function getTraceListByCustomerId(customerId) {
+ return request({
+ url: '/crm/sale-trace/get-list-by-customer-id',
+ method: 'get',
+ params: { customerId }
+ })
+}
+```
+
+**鏂囦欢璺緞锛�** `/src/api/crm/report.js`
+
+```js
+// 鑾峰彇瀹㈡埛鍒嗘瀽缁熻
+export function getCustomerAnalyze(startTime, endTime) {
+ return request({
+ url: '/crm/report/get-customer-analyze',
+ method: 'get',
+ params: { startTime, endTime }
+ })
+}
+
+// 鑾峰彇鍟嗘満鍒嗘瀽缁熻
+export function getBusinessAnalyze(startTime, endTime) {
+ return request({
+ url: '/crm/report/get-business-analyze',
+ method: 'get',
+ params: { startTime, endTime }
+ })
+}
+
+// 鑾峰彇鍚堝悓鍒嗘瀽缁熻
+export function getContractAnalyze(startTime, endTime) {
+ return request({
+ url: '/crm/report/get-contract-analyze',
+ method: 'get',
+ params: { startTime, endTime }
+ })
+}
+
+// 鑾峰彇閿�鍞紡鏂楃粺璁�
+export function getSalesFunnel() {
+ return request({
+ url: '/crm/report/get-sales-funnel',
+ method: 'get'
+ })
+}
+
+// 鑾峰彇閿�鍞笟缁╃粺璁�
+export function getSalesPerformance(startTime, endTime) {
+ return request({
+ url: '/crm/report/get-sales-performance',
+ method: 'get',
+ params: { startTime, endTime }
+ })
+}
+```
---
-## 鍏�佹敼閫犲畬鎴愮姸鎬�
+## 浜斻�佹暟鎹ā鍨嬪彉鏇�
+
+### 5.1 CRM 鍚堝悓琛�
+
+```sql
+ALTER TABLE `crm_contract`
+ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '鍏宠仈 ERP 閿�鍞鍗曠紪鍙�' AFTER `total_price`,
+ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP 閿�鍞鍗曞崟鍙�' AFTER `order_id`;
+```
+
+### 5.2 CRM 鍟嗘満琛�
+
+```sql
+ALTER TABLE `crm_business`
+ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '鍏宠仈 ERP 閿�鍞鍗曠紪鍙�' AFTER `total_price`,
+ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP 閿�鍞鍗曞崟鍙�' AFTER `order_id`;
+```
+
+### 5.3 ERP 閿�鍞鍗曡〃
+
+```sql
+ALTER TABLE `erp_sale_order`
+ADD COLUMN `contract_id` bigint DEFAULT NULL COMMENT '鍏宠仈 CRM 鍚堝悓缂栧彿' AFTER `customer_id`,
+ADD COLUMN `contract_no` varchar(32) DEFAULT NULL COMMENT 'CRM 鍚堝悓鍗曞彿' AFTER `contract_id`;
+```
+
+---
+
+## 鍏�佷笟鍔¤鍒�
+
+### 6.1 鐢熸垚閿�鍞鍗曟潯浠�
+
+- 鍚堝悓蹇呴』瀹℃壒閫氳繃锛坅uditStatus = 20锛�
+- 鍚堝悓鏈叧鑱旈攢鍞鍗曪紙orderId 涓虹┖锛�
+- 鍚堝悓蹇呴』鍖呭惈浜у搧淇℃伅
+
+### 6.2 瀹㈡埛鏁版嵁瑙勫垯
+
+- ERP 閿�鍞ā鍧楃粺涓�浣跨敤 CRM 瀹㈡埛
+- 鍘� ERP 瀹㈡埛琛紙erp_customer锛夊簾寮�
+- 瀹㈡埛閫夋嫨閫氳繃 CRM 瀹㈡埛 API 鑾峰彇
+
+### 6.3 浜у搧鏁版嵁瑙勫垯
+
+- CRM 鍜� ERP 浜у搧缁熶竴浣跨敤 MDM 鐗╂枡锛坕temType=2锛�
+- MDM 鐗╂枡绫诲瀷锛�1-鐗╂枡锛�2-浜у搧锛�3-鍗婃垚鍝�
+
+---
+
+## 涓冦�佷笟鍔℃祦绋嬪浘
+
+```
+瀹㈡埛绠$悊(CRM) 鈫� 鍟嗘満璺熻繘(CRM) 鈫� 鍚堝悓绛捐(CRM) 鈫� 鍚堝悓瀹℃壒
+ 鈫�
+ 鐢熸垚閿�鍞鍗�(ERP)
+ 鈫�
+ 璁㈠崟瀹℃壒(ERP)
+ 鈫�
+ 閿�鍞嚭搴�(ERP)
+ 鈫�
+ 瀹㈡埛鍥炴(ERP)
+```
+
+**閿�鍞紡鏂楄浆鍖栵細**
+
+```
+瀹㈡埛(1000) 鈫� 鍟嗘満(500) 鈫� 鍚堝悓(300) 鈫� 鍥炴(150)
+ 鈫� 鈫� 鈫� 鈫�
+ 100% 50% 30% 15%
+```
+
+---
+
+## 鍏�佹敞鎰忎簨椤�
+
+1. **鏁版嵁鏉冮檺**锛氭姤琛ㄦ暟鎹渶瑕佹牴鎹敤鎴锋潈闄愯繃婊�
+2. **鐘舵�佹帶鍒�**锛氬彧鏈夊鎵归�氳繃鐨勫悎鍚屾墠鑳界敓鎴愯鍗�
+3. **鍞竴鎬�**锛氫竴涓悎鍚屽彧鑳界敓鎴愪竴涓攢鍞鍗�
+4. **鏁版嵁鍚屾**锛氱敓鎴愯鍗曞悗锛屽悎鍚屼笌璁㈠崟鍙屽悜鍏宠仈
+5. **搴熷純椤甸潰**锛欵RP 瀹㈡埛绠$悊椤甸潰锛�/erp/customer/index锛夊缓璁湪鑿滃崟涓殣钘�
+
+---
+
+## 涔濄�佸畬鎴愮姸鎬�
| Phase | 鍐呭 | 鐘舵�� |
|-------|------|------|
@@ -128,13 +786,223 @@
---
-## 涓冦�佷骇鍑烘枃妗f竻鍗�
+## 鍗佷竴銆侀攢鍞姤浠锋ā鍧楋紙鏂板锛�
+
+### 11.1 娑夊強椤甸潰
+
+| 椤甸潰 | 璺緞 | 璇存槑 |
+|------|------|------|
+| 鎶ヤ环鍗曞垪琛� | `/crm/sale-quotation/index` | 鎶ヤ环鍗曞垪琛ㄣ�佹搷浣滄寜閽� |
+| 鎶ヤ环鍗曡〃鍗� | `/crm/sale-quotation/form` | 鏂板缓/缂栬緫鎶ヤ环鍗� |
+| 鎶ヤ环鍗曡鎯� | `/crm/sale-quotation/detail` | 鏌ョ湅璇︽儏銆佸鎵硅繘搴� |
+
+### 11.2 API 鎺ュ彛
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /crm/sale-quotation/create | 鍒涘缓鎶ヤ环鍗� |
+| PUT | /crm/sale-quotation/update | 鏇存柊鎶ヤ环鍗� |
+| DELETE | /crm/sale-quotation/delete | 鍒犻櫎鎶ヤ环鍗� |
+| GET | /crm/sale-quotation/get | 鑾峰彇鎶ヤ环鍗曡鎯� |
+| GET | /crm/sale-quotation/page | 鍒嗛〉鏌ヨ |
+| GET | /crm/sale-quotation/page-by-customer | 鎸夊鎴峰垎椤� |
+| GET | /crm/sale-quotation/page-by-business | 鎸夊晢鏈哄垎椤� |
+| PUT | /crm/sale-quotation/submit | 鎻愪氦瀹℃壒锛堥渶浼� processDefinitionKey锛� |
+| GET | /crm/sale-quotation/approve-process-list | 鑾峰彇鍙�夊鎵规祦绋嬪垪琛� |
+| POST | /crm/sale-quotation/convert-contract | 杞负鍚堝悓 |
+
+**璇锋眰鍙傛暟绀轰緥**锛堝垱寤烘姤浠峰崟锛夛細
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| name | String | 鏄� | 鎶ヤ环鍗曞悕绉� |
+| customerId | Long | 鏄� | 瀹㈡埛缂栧彿 |
+| businessId | Long | 鍚� | 鍟嗘満缂栧彿 |
+| contactId | Long | 鍚� | 鑱旂郴浜虹紪鍙� |
+| quotationTime | LocalDateTime | 鍚� | 鎶ヤ环鏃ユ湡 |
+| validUntil | LocalDateTime | 鍚� | 鏈夋晥鏈熻嚦 |
+| discountPercent | BigDecimal | 鍚� | 鏁村崟鎶樻墸 |
+| taxRate | BigDecimal | 鍚� | 绋庣巼 |
+| fileUrl | String | 鍚� | 闄勪欢鍦板潃 |
+| remark | String | 鍚� | 澶囨敞 |
+| items | List | 鏄� | 鐗╂枡鏄庣粏鍒楄〃 |
+
+**鐗╂枡鏄庣粏 Item**锛�
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| itemId | Long | 鏄� | MDM 鐗╂枡缂栧彿 |
+| quotationPrice | BigDecimal | 鏄� | 鎶ヤ环鍗曚环 |
+| count | BigDecimal | 鏄� | 鏁伴噺 |
+| taxPercent | BigDecimal | 鍚� | 绋庣巼 |
+| remark | String | 鍚� | 澶囨敞 |
+
+### 11.3 鍓嶇淇敼鐐�
+
+#### 鎶ヤ环鍗曞垪琛ㄩ〉闈�
+
+**鏂囦欢璺緞锛�** `/src/views/crm/sale-quotation/index.vue`
+
+```html
+<!-- 鎿嶄綔鍒� -->
+<el-table-column label="鎿嶄綔" width="280">
+ <template #default="{ row }">
+ <el-button v-if="row.status === 0" type="primary" size="small" @click="handleUpdate(row)">缂栬緫</el-button>
+ <el-button v-if="row.status === 0" type="success" size="small" @click="handleSubmit(row)">鎻愪氦瀹℃壒</el-button>
+ <el-button v-if="row.status === 20 && !row.contractId" type="warning" size="small" @click="handleConvertContract(row)">杞悎鍚�</el-button>
+ <el-button v-if="row.contractId" type="primary" size="small" @click="handleViewContract(row)">鏌ョ湅鍚堝悓</el-button>
+ <el-button v-if="row.status === 0" type="danger" size="small" @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+</el-table-column>
+```
+
+**鎻愪氦瀹℃壒寮圭獥**锛�
+
+```html
+<el-dialog title="閫夋嫨瀹℃壒娴佺▼" v-model="submitDialogVisible" width="400px">
+ <el-form label-width="100px">
+ <el-form-item label="瀹℃壒娴佺▼" required>
+ <el-select v-model="selectedProcessKey" placeholder="璇烽�夋嫨" style="width: 100%">
+ <el-option v-for="item in processList" :key="item.key" :label="item.name" :value="item.key" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="confirmSubmit" :loading="submitLoading">纭畾</el-button>
+ </template>
+</el-dialog>
+```
+
+**JS 鏂规硶**锛�
+
+```js
+import { getSaleQuotationApproveProcessList, submitSaleQuotation, convertSaleQuotationToContract } from '@/api/crm/saleQuotation'
+
+methods: {
+ async handleSubmit(row) {
+ const { data } = await getSaleQuotationApproveProcessList()
+ this.processList = data
+ this.currentRow = row
+ this.submitDialogVisible = true
+ },
+ async confirmSubmit() {
+ if (!this.selectedProcessKey) {
+ this.$message.warning('璇烽�夋嫨瀹℃壒娴佺▼')
+ return
+ }
+ this.submitLoading = true
+ try {
+ await submitSaleQuotation(this.currentRow.id, this.selectedProcessKey)
+ this.$message.success('鎻愪氦鎴愬姛')
+ this.submitDialogVisible = false
+ this.getList()
+ } finally {
+ this.submitLoading = false
+ }
+ },
+ async handleConvertContract(row) {
+ try {
+ await this.$confirm('纭灏嗚鎶ヤ环鍗曡浆涓哄悎鍚岋紵', '鎻愮ず')
+ const { data } = await convertSaleQuotationToContract(row.id)
+ this.$message.success('杞悎鍚屾垚鍔�')
+ this.$router.push({ path: '/crm/contract/detail', query: { id: data } })
+ } catch (e) {
+ if (e !== 'cancel') this.$message.error(e.message || '杞悎鍚屽け璐�')
+ }
+ }
+}
+```
+
+#### 鎶ヤ环鍗曡〃鍗曢〉闈� - MDM 鐗╂枡閫夋嫨
+
+**鏂囦欢璺緞锛�** `/src/views/crm/sale-quotation/form.vue`
+
+鐢变簬鎶ヤ环鍗曚娇鐢� MDM 鐗╂枡锛岄渶瑕佷娇鐢� MDM 鐗╂枡閫夋嫨缁勪欢锛�
+
+```html
+<MdmItemSelect v-model="item.itemId" @change="handleItemChange" />
+```
+
+**API 瀹氫箟鏂囦欢**锛歚/src/api/crm/saleQuotation.js`
+
+```js
+import request from '@/utils/request'
+
+// 鍒涘缓鎶ヤ环鍗�
+export function createSaleQuotation(data) {
+ return request({ url: '/crm/sale-quotation/create', method: 'post', data })
+}
+
+// 鏇存柊鎶ヤ环鍗�
+export function updateSaleQuotation(data) {
+ return request({ url: '/crm/sale-quotation/update', method: 'put', data })
+}
+
+// 鍒犻櫎鎶ヤ环鍗�
+export function deleteSaleQuotation(ids) {
+ return request({ url: '/crm/sale-quotation/delete', method: 'delete', params: { ids } })
+}
+
+// 鑾峰彇鎶ヤ环鍗曡鎯�
+export function getSaleQuotation(id) {
+ return request({ url: '/crm/sale-quotation/get', method: 'get', params: { id } })
+}
+
+// 鍒嗛〉鏌ヨ
+export function getSaleQuotationPage(params) {
+ return request({ url: '/crm/sale-quotation/page', method: 'get', params })
+}
+
+// 鎻愪氦瀹℃壒
+export function submitSaleQuotation(id, processDefinitionKey) {
+ return request({ url: '/crm/sale-quotation/submit', method: 'put', params: { id, processDefinitionKey } })
+}
+
+// 鑾峰彇瀹℃壒娴佺▼鍒楄〃
+export function getSaleQuotationApproveProcessList() {
+ return request({ url: '/crm/sale-quotation/approve-process-list', method: 'get' })
+}
+
+// 杞悎鍚�
+export function convertSaleQuotationToContract(id) {
+ return request({ url: '/crm/sale-quotation/convert-contract', method: 'post', params: { id } })
+}
+```
+
+### 11.4 涓氬姟娴佺▼
+
+```
+鍟嗘満(鍙��) 鈫� 閿�鍞姤浠�(鑽夌) 鈫� 鎻愪氦瀹℃壒 鈫� 瀹℃壒涓� 鈫� 瀹℃壒閫氳繃 鈫� 杞悎鍚�
+ 鈫�
+ 瀹℃壒鎷掔粷 鈫� 閲嶆柊鎻愪氦鎴栦慨鏀�
+```
+
+### 11.5 娉ㄦ剰浜嬮」
+
+1. 鎶ヤ环鍗曞拰鍚堝悓缁熶竴浣跨敤 **MDM 鐗╂枡**锛坄MdmItemApi`锛夛紝涓� ERP 閿�鍞鍗曚竴鑷�
+2. BPM 鍒嗙被缂栫爜 `sale_quotation_approve` 闇�瑕佸湪 BPM 绠$悊鍚庡彴鍒涘缓
+3. 鎶ヤ环鍗曡浆鍚堝悓鏃讹紝鐗╂枡鏄庣粏鐩存帴澶嶅埗鍒板悎鍚屼骇鍝佽〃锛堝瓧娈典竴鑷达級
+4. 鎶ヤ环鍗曞彿鍓嶇紑涓� `QT`锛屾牸寮忥細QT + yyyyMMdd + 6浣嶈嚜澧炲簭鍙�
+5. 鍟嗘満鍏宠仈涓哄彲閫夛紝鎶ヤ环鍗曞彲浠ョ嫭绔嬪垱寤轰笉渚濊禆鍟嗘満
+
+### 11.6 鍚堝悓浜у搧瀛楁鍙樻洿璇存槑
+
+鍚堝悓浜у搧琛� `crm_contract_product` 宸叉敼涓轰娇鐢� MDM 鐗╂枡锛�
+
+| 鍘熷瓧娈� | 鏂板瓧娈� | 璇存槑 |
+|--------|--------|------|
+| `product_id` | `item_id` | MDM 鐗╂枡缂栧彿 |
+| `product_price` | `item_price` | 鐗╂枡鍘熶环 |
+| - | `item_unit_id` | 璁¢噺鍗曚綅缂栧彿 |
+
+鍓嶇琛ㄥ崟闇�瑕佷娇鐢� MDM 鐗╂枡閫夋嫨缁勪欢鏇夸唬 CRM 浜у搧閫夋嫨缁勪欢銆�
+
+---
+
+## 鍗併�佷骇鍑烘枃妗f竻鍗�
| 鏂囨。 | 璇存槑 |
|------|------|
-| `docs/crm_erp_integration_frontend.md` | CRM 瀹㈡埛涓�浣撳寲鍓嶇鏂囨。 |
+| `docs/crm_erp_integration_frontend.md` | 鏈枃妗� |
| `docs/crm_erp_integration_sql.sql` | SQL 鍙樻洿鑴氭湰 |
-| `docs/mdm_product_integration_frontend.md` | MDM 浜у搧涓�浣撳寲鍓嶇鏂囨。 |
-| `docs/contract_generate_order_frontend.md` | 鍚堝悓鐢熸垚璁㈠崟鍓嶇鏂囨。 |
-| `docs/sales_traceability_frontend.md` | 閿�鍞拷婧墠绔枃妗� |
-| `docs/marketing_report_frontend.md` | 钀ラ攢鎶ヨ〃鍓嶇鏂囨。 |
diff --git a/docs/crm_sale_quotation.sql b/docs/crm_sale_quotation.sql
new file mode 100644
index 0000000..e8f82dd
--- /dev/null
+++ b/docs/crm_sale_quotation.sql
@@ -0,0 +1,86 @@
+-- =============================================
+-- 閿�鍞姤浠锋ā鍧楁暟鎹簱琛�
+-- =============================================
+
+-- 1. 鎶ヤ环鍗曚富琛�
+CREATE TABLE IF NOT EXISTS `crm_sale_quotation` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+ `no` varchar(32) NOT NULL COMMENT '鎶ヤ环鍗曞彿',
+ `name` varchar(128) NOT NULL COMMENT '鎶ヤ环鍗曞悕绉�',
+ `customer_id` bigint NOT NULL COMMENT '瀹㈡埛缂栧彿',
+ `business_id` bigint DEFAULT NULL COMMENT '鍟嗘満缂栧彿',
+ `contact_id` bigint DEFAULT NULL COMMENT '鑱旂郴浜虹紪鍙�',
+ `owner_user_id` bigint NOT NULL COMMENT '璐熻矗浜虹紪鍙�',
+ `status` int NOT NULL DEFAULT 0 COMMENT '鐘舵�侊紙0鑽夌 10瀹℃壒涓� 20宸查�氳繃 30宸叉嫆缁� 40宸插彇娑� 50宸茶浆鍚堝悓锛�',
+ `process_instance_id` varchar(64) DEFAULT NULL COMMENT '宸ヤ綔娴佺紪鍙�',
+ `quotation_time` datetime DEFAULT NULL COMMENT '鎶ヤ环鏃ユ湡',
+ `valid_until` datetime DEFAULT NULL COMMENT '鏈夋晥鏈熻嚦',
+ `total_product_price` decimal(24,6) DEFAULT NULL COMMENT '浜у搧鎬婚噾棰�',
+ `discount_percent` decimal(10,2) DEFAULT 0 COMMENT '鏁村崟鎶樻墸(%)',
+ `discount_price` decimal(24,6) DEFAULT 0 COMMENT '浼樻儬閲戦',
+ `total_price` decimal(24,6) DEFAULT NULL COMMENT '鎶ヤ环鎬婚噾棰�',
+ `tax_rate` decimal(10,2) DEFAULT NULL COMMENT '绋庣巼(%)',
+ `tax_price` decimal(24,6) DEFAULT NULL COMMENT '绋庨',
+ `contract_id` bigint DEFAULT NULL COMMENT '鍏宠仈鍚堝悓缂栧彿',
+ `contract_no` varchar(32) DEFAULT NULL COMMENT '鍏宠仈鍚堝悓鍗曞彿',
+ `file_url` varchar(512) DEFAULT NULL COMMENT '闄勪欢鍦板潃',
+ `remark` varchar(1024) DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) DEFAULT NULL COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) DEFAULT NULL COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '绉熸埛缂栧彿',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_no` (`no`),
+ KEY `idx_customer_id` (`customer_id`),
+ KEY `idx_business_id` (`business_id`),
+ KEY `idx_owner_user_id` (`owner_user_id`)
+) ENGINE=InnoDB COMMENT='CRM 閿�鍞姤浠峰崟';
+
+-- 2. 鎶ヤ环鍗曚骇鍝佹槑缁嗚〃
+CREATE TABLE IF NOT EXISTS `crm_sale_quotation_product` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '缂栧彿',
+ `quotation_id` bigint NOT NULL COMMENT '鎶ヤ环鍗曠紪鍙�',
+ `item_id` bigint NOT NULL COMMENT 'MDM鐗╂枡缂栧彿',
+ `item_unit_id` bigint DEFAULT NULL COMMENT '璁¢噺鍗曚綅缂栧彿',
+ `item_price` decimal(24,6) NOT NULL COMMENT '鐗╂枡鍘熶环锛堝啑浣欙級',
+ `quotation_price` decimal(24,6) NOT NULL COMMENT '鎶ヤ环鍗曚环',
+ `count` decimal(24,6) NOT NULL COMMENT '鏁伴噺',
+ `total_price` decimal(24,6) NOT NULL COMMENT '灏忚閲戦',
+ `tax_percent` decimal(10,2) DEFAULT NULL COMMENT '绋庣巼(%)',
+ `tax_price` decimal(24,6) DEFAULT NULL COMMENT '绋庨',
+ `remark` varchar(256) DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) DEFAULT NULL COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) DEFAULT NULL COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '绉熸埛缂栧彿',
+ PRIMARY KEY (`id`),
+ KEY `idx_quotation_id` (`quotation_id`)
+) ENGINE=InnoDB COMMENT='CRM 閿�鍞姤浠峰崟浜у搧鏄庣粏';
+
+-- =============================================
+-- 鍚堝悓浜у搧琛ㄤ慨鏀癸紙浣跨敤 MDM 鐗╂枡锛�
+-- =============================================
+
+-- 3. 淇敼鍚堝悓浜у搧琛ㄧ粨鏋�
+-- 娉ㄦ剰锛氬鏋滃凡鏈夋暟鎹紝闇�瑕佸厛澶勭悊鏁版嵁杩佺Щ
+-- ALTER TABLE `crm_contract_product` DROP COLUMN `product_id`;
+-- ALTER TABLE `crm_contract_product` DROP COLUMN `product_price`;
+
+ALTER TABLE `crm_contract_product`
+ ADD COLUMN `item_id` bigint DEFAULT NULL COMMENT 'MDM鐗╂枡缂栧彿' AFTER `contract_id`,
+ ADD COLUMN `item_unit_id` bigint DEFAULT NULL COMMENT '璁¢噺鍗曚綅缂栧彿' AFTER `item_id`,
+ ADD COLUMN `item_price` decimal(24,6) DEFAULT NULL COMMENT '鐗╂枡鍘熶环锛堝啑浣欙級' AFTER `item_unit_id`;
+
+-- 鏁版嵁杩佺Щ锛氬皢 product_id 閲嶅懡鍚嶄负 item_id锛堝鏋滈渶瑕佷繚鐣欏師鏈夋暟鎹級
+-- UPDATE `crm_contract_product` SET `item_id` = `product_id`, `item_price` = `product_price`;
+
+-- =============================================
+-- BPM 鍒嗙被锛堥渶瑕佸湪 BPM 绠$悊鍚庡彴鍒涘缓锛岃繖閲屾彁渚� SQL 鍙傝�冿級
+-- =============================================
+
+-- INSERT INTO `bpm_category` (`name`, `code`, `description`, `status`, `sort`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+-- VALUES ('閿�鍞姤浠峰鎵�', 'sale_quotation_approve', '閿�鍞姤浠峰崟瀹℃壒娴佺▼鍒嗙被', 0, 0, '1', NOW(), '1', NOW(), b'0');
diff --git a/docs/crm_sales_flow.md b/docs/crm_sales_flow.md
new file mode 100644
index 0000000..e844d5b
--- /dev/null
+++ b/docs/crm_sales_flow.md
@@ -0,0 +1,444 @@
+# CRM 涓� ERP 閿�鍞祦绋嬪叏鏅浘
+
+## 涓�銆佹暣浣撲笟鍔℃祦绋�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� CRM 閿�鍞嚎绱㈢鐞嗭紙鍓嶇锛� 鈹�
+鈹溾攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鈹�
+鈹� 绾跨储(clue) 鈹�鈹�杞寲鈹�鈹�鈻� 瀹㈡埛(customer) 鈹�鈹�璺熻繘鈹�鈹�鈻� 鍟嗘満(business) 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈻� 鈻� 鈻� 鈹�
+鈹� 绾跨储鍏ュ簱 瀹㈡埛鍏捣姹� 鍟嗘満鐘舵�佹帹杩� 鈹�
+鈹� 绾跨储鍒嗛厤 瀹㈡埛閿佸畾 璧㈠崟/杈撳崟 鈹�
+鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� CRM 閿�鍞姤浠蜂笌鍚堝悓锛堟柊澧烇級 鈹�
+鈹溾攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鈹�
+鈹� 閿�鍞姤浠�(quotation) 鈹�鈹�BPM瀹℃壒鈹�鈹�鈻� 鍚堝悓(contract) 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈻� 鈻� 鈻� 鈹�
+鈹� 鍒涘缓鎶ヤ环鍗� 瀹℃壒閫氳繃 鍚堝悓绛捐 鈹�
+鈹� 閫夋嫨MDM鐗╂枡 瀹℃壒鎷掔粷 鍚堝悓瀹℃壒 鈹�
+鈹� 鎻愪氦瀹℃壒 瀹℃壒鍙栨秷 鐢熸垚璁㈠崟 鈹�
+鈹� 杞负鍚堝悓 鍏宠仈鍟嗘満 鈹�
+鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� ERP 閿�鍞鍗曚笌鎵ц 鈹�
+鈹溾攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鈹�
+鈹� 閿�鍞鍗�(order) 鈹�鈹�鈻� 閿�鍞嚭搴�(out) 鈹�鈹�鈻� 閿�鍞��璐�(return) 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈻� 鈻� 鈻� 鈹�
+鈹� 璁㈠崟瀹℃壒 鍑哄簱瀹℃牳 閫�璐у鏍� 鈹�
+鈹� 鍏宠仈鍚堝悓 鍑哄簱鍗曞彿 閫�璐у崟鍙� 鈹�
+鈹� 閫夋嫨MDM鐗╂枡 鍑哄簱鏁伴噺 閫�璐ф暟閲� 鈹�
+鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� ERP 璐㈠姟鏀舵 鈹�
+鈹溾攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鈹�
+鈹� 鏀舵鍗�(receipt) 鈹�鈹�鈻� 鏀舵鏍搁攢 鈹�
+鈹� 鈹� 鈹�
+鈹� 鈻� 鈹�
+鈹� 鍏宠仈閿�鍞鍗� 鈹�
+鈹� 鍏宠仈CRM瀹㈡埛 鈹�
+鈹� 鏀舵閲戦 鈹�
+鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 浜屻�佹暟鎹祦杞瑙�
+
+### 2.1 瀹㈡埛鏁版嵁锛堢粺涓�鏉ユ簮锛�
+
+| 妯″潡 | 鏁版嵁鏉ユ簮 | 璇存槑 |
+|------|----------|------|
+| CRM 瀹㈡埛 | `crm_customer` | **鍞竴瀹㈡埛鏁版嵁婧�** |
+| ERP 閿�鍞鍗� | `crm_customer` (閫氳繃 API) | 浣跨敤 CRM 瀹㈡埛 |
+| ERP 閿�鍞嚭搴� | `crm_customer` (閫氳繃 API) | 浣跨敤 CRM 瀹㈡埛 |
+| ERP 閿�鍞��璐� | `crm_customer` (閫氳繃 API) | 浣跨敤 CRM 瀹㈡埛 |
+| ERP 鏀舵鍗� | `crm_customer` (閫氳繃 API) | 浣跨敤 CRM 瀹㈡埛 |
+
+**鍏抽敭 API**锛歚CrmCustomerApi`
+- `/rpc-api/crm/customer/get` - 鑾峰彇鍗曚釜瀹㈡埛
+- `/rpc-api/crm/customer/list-by-ids` - 鎵归噺鑾峰彇瀹㈡埛
+- `/rpc-api/crm/customer/validate` - 鏍¢獙瀹㈡埛瀛樺湪
+
+### 2.2 鐗╂枡鏁版嵁锛堢粺涓�鏉ユ簮锛�
+
+| 妯″潡 | 鏁版嵁鏉ユ簮 | 璇存槑 |
+|------|----------|------|
+| MDM 鐗╂枡 | `mdm_item` | **鍞竴鐗╂枡鏁版嵁婧�** |
+| 閿�鍞姤浠� | `mdm_item` (閫氳繃 API) | 浣跨敤 MDM 鐗╂枡 |
+| CRM 鍚堝悓 | `mdm_item` (閫氳繃 API) | 浣跨敤 MDM 鐗╂枡 |
+| ERP 閿�鍞鍗� | `mdm_item` (閫氳繃 API) | 浣跨敤 MDM 鐗╂枡 |
+
+**鍏抽敭 API**锛歚MdmItemApi`
+- `/rpc-api/mdm/item/get` - 鑾峰彇鐗╂枡
+- `/rpc-api/mdm/item/list` - 鎵归噺鑾峰彇鐗╂枡
+- `/rpc-api/mdm/item/validate-item-exists` - 鏍¢獙鐗╂枡瀛樺湪
+
+---
+
+## 涓夈�佽缁嗘祦绋嬪浘
+
+### 3.1 CRM 閿�鍞姤浠锋祦绋嬶紙鏂板锛�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍒涘缓鎶ヤ环鍗� 鈹傗攢鈹�鈹�鈹�鈻衡攤 鎻愪氦瀹℃壒 鈹傗攢鈹�鈹�鈹�鈻衡攤 BPM瀹℃壒 鈹傗攢鈹�鈹�鈹�鈻衡攤 瀹℃壒閫氳繃 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 閫夋嫨瀹㈡埛 鈹� 鈹� 閫夋嫨娴佺▼ 鈹� 鈹� 瀹℃壒涓姸鎬� 鈹� 鈹� 宸查�氳繃鐘舵�� 鈹�
+鈹� 閫夋嫨MDM鐗╂枡 鈹� 鈹� 鍔ㄦ�侀�夋嫨 鈹� 鈹� status=10 鈹� 鈹� status=20 鈹�
+鈹� 濉啓鎶ヤ环閲戦 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鍙叧鑱斿晢鏈� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 杞负鍚堝悓 鈹傗攢鈹�鈹�鈹�鈻衡攤 鍚堝悓鐢熸垚 鈹�
+鈹� 鈹� 鈹� 鈹�
+鈹� 鐗╂枡鐩存帴澶嶅埗 鈹� 鈹� 鍚堝悓瀹℃壒娴佺▼ 鈹�
+鈹� 瀛楁涓�鑷� 鈹� 鈹� 鐢熸垚閿�鍞鍗� 鈹�
+鈹� 鐘舵��=50 鈹� 鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+**鐘舵�佹祦杞�**锛�
+
+| 鐘舵�佸�� | 鐘舵�佸悕 | 鍙搷浣� |
+|--------|--------|--------|
+| 0 | 鑽夌 | 缂栬緫銆佸垹闄ゃ�佹彁浜ゅ鎵� |
+| 10 | 瀹℃壒涓� | 鏃� |
+| 20 | 宸查�氳繃 | 杞悎鍚� |
+| 30 | 宸叉嫆缁� | 閲嶆柊鎻愪氦 |
+| 40 | 宸插彇娑� | 鏃� |
+| 50 | 宸茶浆鍚堝悓 | 鏌ョ湅鍚堝悓 |
+
+### 3.2 CRM 鍚堝悓娴佺▼
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍒涘缓鍚堝悓 鈹傗攢鈹�鈹�鈹�鈻衡攤 鎻愪氦瀹℃壒 鈹傗攢鈹�鈹�鈹�鈻衡攤 BPM瀹℃壒 鈹傗攢鈹�鈹�鈹�鈻衡攤 瀹℃壒閫氳繃 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 閫夋嫨瀹㈡埛 鈹� 鈹� 鍥哄畾娴佺▼Key 鈹� 鈹� 瀹℃壒涓姸鎬� 鈹� 鈹� 宸查�氳繃鐘舵�� 鈹�
+鈹� 閫夋嫨MDM鐗╂枡 鈹� 鈹� crm-contract 鈹� 鈹� auditStatus 鈹� 鈹� auditStatus 鈹�
+鈹� 鍚堝悓閲戦 鈹� 鈹� -audit 鈹� 鈹� =10 鈹� 鈹� =20 鈹�
+鈹� 鍙叧鑱斿晢鏈� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+ 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹� 鐢熸垚閿�鍞鍗� 鈹�
+ 鈹� 鈹�
+ 鈹� ERP璁㈠崟鍒涘缓 鈹�
+ 鈹� 鍏宠仈鍚堝悓缂栧彿 鈹�
+ 鈹� 鐗╂枡鏄庣粏澶嶅埗 鈹�
+ 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+### 3.3 ERP 閿�鍞鍗曟祦绋�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍒涘缓璁㈠崟 鈹傗攢鈹�鈹�鈹�鈻衡攤 璁㈠崟瀹℃壒 鈹傗攢鈹�鈹�鈹�鈻衡攤 瀹℃壒閫氳繃 鈹傗攢鈹�鈹�鈹�鈻衡攤 閿�鍞嚭搴� 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� CRM瀹㈡埛 鈹� 鈹� ERP瀹℃壒娴佺▼ 鈹� 鈹� 鍙墽琛屽嚭搴� 鈹� 鈹� 鍑哄簱鍗曠敓鎴� 鈹�
+鈹� MDM鐗╂枡 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鍑哄簱鏁伴噺绱 鈹�
+鈹� 鍏宠仈鍚堝悓(鍙�) 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 璁㈠崟閲戦 鈹� 鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+ 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹� 鏀舵鏍搁攢 鈹�
+ 鈹� 鈹�
+ 鈹� 鏀舵鍗曞垱寤� 鈹�
+ 鈹� 鍏宠仈璁㈠崟 鈹�
+ 鈹� 鏀舵閲戦绱 鈹�
+ 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 鍥涖�佹ā鍧楀叧绯诲浘
+
+```
+ 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹� MDM 涓绘暟鎹ā鍧� 鈹�
+ 鈹� 鈹�
+ 鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+ 鈹� 鈹� mdm_item 鈹� 鈹� mdm_unit 鈹� 鈹�
+ 鈹� 鈹� (鐗╂枡) 鈹� 鈹� (鍗曚綅) 鈹� 鈹�
+ 鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+ 鈹� 鈹�
+ 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈹� MdmItemApi
+ 鈹�
+ 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹尖攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹� 鈹� 鈹�
+ 鈻� 鈻� 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� CRM 妯″潡 鈹� 鈹� ERP 妯″潡 鈹� 鈹� BPM 妯″潡 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹俢rm_customer 鈹� 鈹� 鈹� 鈹俥rp_sale_ 鈹� 鈹� 鈹� 鈹� bpm_category鈹� 鈹�
+鈹� 鈹�(瀹㈡埛) 鈹傗梽鈹溾攢鈹�鈹�鈹�鈹� 鈹� order 鈹� 鈹� 鈹� 鈹� (瀹℃壒鍒嗙被) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹�(閿�鍞鍗�) 鈹� 鈹� 鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� 鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹俢rm_sale_ 鈹� 鈹� 鈹� 鈹俥rp_sale_ 鈹� 鈹� 鈹� 鈹� bpm_process 鈹� 鈹�
+鈹� 鈹� quotation 鈹� 鈹� 鈹� 鈹� out 鈹� 鈹� 鈹� 鈹� _definition 鈹� 鈹�
+鈹� 鈹�(閿�鍞姤浠�) 鈹� 鈹� 鈹� 鈹�(閿�鍞嚭搴�) 鈹� 鈹� 鈹� 鈹� (娴佺▼瀹氫箟) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹�
+鈹� 鈹俢rm_contract 鈹� 鈹� 鈹� 鈹俥rp_finance_ 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹�(鍚堝悓) 鈹傗梽鈹溾攢鈹�鈹�鈹�鈹� 鈹� receipt 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹�(鏀舵鍗�) 鈹� 鈹� 鈹� 鈹�
+鈹� 鈹� 鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹� 鈹� 鈹�
+鈹� 鈹� 鈹� 鈹� 鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 浜斻�佸叧閿〃缁撴瀯
+
+### 5.1 CRM 琛�
+
+| 琛ㄥ悕 | 璇存槑 | 鍏抽敭瀛楁 |
+|------|------|----------|
+| `crm_customer` | 瀹㈡埛涓昏〃 | id, name, owner_user_id |
+| `crm_business` | 鍟嗘満涓昏〃 | id, customer_id, total_price, status_id |
+| `crm_sale_quotation` | 閿�鍞姤浠� | id, no, customer_id, business_id, status |
+| `crm_sale_quotation_product` | 鎶ヤ环鏄庣粏 | quotation_id, **item_id**(MDM鐗╂枡) |
+| `crm_contract` | 鍚堝悓涓昏〃 | id, no, customer_id, business_id, audit_status |
+| `crm_contract_product` | 鍚堝悓鏄庣粏 | contract_id, **item_id**(MDM鐗╂枡) |
+
+### 5.2 ERP 琛�
+
+| 琛ㄥ悕 | 璇存槑 | 鍏抽敭瀛楁 |
+|------|------|----------|
+| `erp_sale_order` | 閿�鍞鍗� | id, no, **customer_id**(CRM), contract_id |
+| `erp_sale_order_items` | 璁㈠崟鏄庣粏 | order_id, **item_id**(MDM鐗╂枡) |
+| `erp_sale_out` | 閿�鍞嚭搴� | id, order_id, customer_id |
+| `erp_finance_receipt` | 鏀舵鍗� | id, **customer_id**(CRM) |
+
+---
+
+## 鍏�丄PI 璋冪敤鍏崇郴
+
+### 6.1 CRM 璋冪敤 MDM
+
+```
+CrmSaleQuotationServiceImpl
+ 鈹斺攢鈻� MdmItemApi.validateItemExists(itemId) // 鏍¢獙鐗╂枡瀛樺湪
+ 鈹斺攢鈻� MdmItemApi.getItemMap(itemIds) // 鎵归噺鑾峰彇鐗╂枡淇℃伅
+
+CrmContractServiceImpl
+ 鈹斺攢鈻� MdmItemApi.validateItemExists(itemId) // 鏍¢獙鐗╂枡瀛樺湪
+ 鈹斺攢鈻� MdmItemApi.getItemMap(itemIds) // 鎵归噺鑾峰彇鐗╂枡淇℃伅
+```
+
+### 6.2 ERP 璋冪敤 CRM
+
+```
+ErpSaleOrderServiceImpl
+ 鈹斺攢鈻� CrmCustomerApi.getCustomer(customerId) // 鑾峰彇CRM瀹㈡埛
+ 鈹斺攢鈻� CrmCustomerApi.validateCustomer(customerId) // 鏍¢獙瀹㈡埛瀛樺湪
+
+ErpFinanceReceiptServiceImpl
+ 鈹斺攢鈻� CrmCustomerApi.getCustomer(customerId) // 鑾峰彇CRM瀹㈡埛
+```
+
+### 6.3 CRM 璋冪敤 BPM
+
+```
+CrmSaleQuotationServiceImpl
+ 鈹斺攢鈻� BpmProcessInstanceApi.createProcessInstance() // 鍒涘缓瀹℃壒娴佺▼瀹炰緥
+ 鈹斺攢鈻� BpmCategoryService.getCategoryMap() // 鑾峰彇瀹℃壒鍒嗙被
+ 鈹斺攢鈻� BpmProcessDefinitionService.getProcessDefinitionInfoListByCategory() // 鑾峰彇娴佺▼瀹氫箟
+
+CrmContractServiceImpl
+ 鈹斺攢鈻� BpmProcessInstanceApi.createProcessInstance() // 鍒涘缓瀹℃壒娴佺▼瀹炰緥
+```
+
+---
+
+## 涓冦�佺姸鎬佹灇涓惧鐓�
+
+### 7.1 閿�鍞姤浠风姸鎬� (CrmSaleQuotationStatusEnum)
+
+| 鍊� | 鏋氫妇 | 璇存槑 |
+|---|------|------|
+| 0 | DRAFT | 鑽夌 |
+| 10 | PROCESS | 瀹℃壒涓� |
+| 20 | APPROVE | 宸查�氳繃 |
+| 30 | REJECT | 宸叉嫆缁� |
+| 40 | CANCEL | 宸插彇娑� |
+| 50 | CONVERTED | 宸茶浆鍚堝悓 |
+
+### 7.2 鍚堝悓瀹℃壒鐘舵�� (CrmAuditStatusEnum)
+
+| 鍊� | 鏋氫妇 | 璇存槑 |
+|---|------|------|
+| 0 | DRAFT | 鏈彁浜� |
+| 10 | PROCESS | 瀹℃壒涓� |
+| 20 | APPROVE | 瀹℃牳閫氳繃 |
+| 30 | REJECT | 瀹℃牳涓嶉�氳繃 |
+| 40 | CANCEL | 宸插彇娑� |
+
+### 7.3 BPM 浠诲姟鐘舵�� (BpmTaskStatusEnum)
+
+| 鍊� | 鏋氫妇 | 璇存槑 |
+|---|------|------|
+| 2 | APPROVE | 瀹℃壒閫氳繃 |
+| 3 | REJECT | 瀹℃壒涓嶉�氳繃 |
+| 4 | CANCEL | 宸插彇娑� |
+
+**鐘舵�佹槧灏�**锛欱PM鐘舵�� 鈫� 涓氬姟鐘舵��
+- `2(APPROVE)` 鈫� `20(宸查�氳繃)`
+- `3(REJECT)` 鈫� `30(宸叉嫆缁�)`
+- `4(CANCEL)` 鈫� `40(宸插彇娑�)`
+
+---
+
+## 鍏�佹搷浣滄潈闄愯鏄�
+
+### 8.1 閿�鍞姤浠锋搷浣滄潈闄�
+
+| 鐘舵�� | 缂栬緫 | 鍒犻櫎 | 鎻愪氦瀹℃壒 | 杞悎鍚� |
+|------|------|------|----------|--------|
+| 鑽夌(0) | 鉁� | 鉁� | 鉁� | 鉂� |
+| 瀹℃壒涓�(10) | 鉂� | 鉂� | 鉂� | 鉂� |
+| 宸查�氳繃(20) | 鉂� | 鉂� | 鉂� | 鉁� |
+| 宸叉嫆缁�(30) | 鉁� | 鉂� | 鉁� | 鉂� |
+| 宸茶浆鍚堝悓(50) | 鉂� | 鉂� | 鉂� | 鉂� |
+
+### 8.2 鍚堝悓鎿嶄綔鏉冮檺
+
+| 鐘舵�� | 缂栬緫 | 鍒犻櫎 | 鎻愪氦瀹℃壒 | 鐢熸垚璁㈠崟 |
+|------|------|------|----------|----------|
+| 鏈彁浜�(0) | 鉁� | 鉁� | 鉁� | 鉂� |
+| 瀹℃壒涓�(10) | 鉁� | 鉂� | 鉂� | 鉂� |
+| 宸查�氳繃(20) | 鉂� | 鉂� | 鉂� | 鉁� |
+| 宸叉嫆缁�(30) | 鉁� | 鉂� | 鉁� | 鉂� |
+
+---
+
+## 涔濄�佸畬鏁翠笟鍔¢摼鏉$ず渚�
+
+```
+绀轰緥锛氫竴涓鎴蜂粠绾跨储鍒板洖娆剧殑瀹屾暣娴佺▼
+
+1. 銆愮嚎绱㈤樁娈点��
+ - 閿�鍞汉鍛樺綍鍏ョ嚎绱� 鈫� 绾跨储鐘舵�侊細寰呰窡杩�
+ - 绾跨储杞寲涓哄鎴� 鈫� 瀹㈡埛鍒涘缓鎴愬姛
+
+2. 銆愬鎴疯窡杩涖��
+ - 鍒涘缓璺熻繘璁板綍 鈫� 璁板綍娌熼�氬唴瀹�
+ - 鍒涘缓鍟嗘満 鈫� 鍟嗘満閲戦锛�100,000鍏�
+ - 鍟嗘満鐘舵�佹帹杩� 鈫� 璧㈠崟
+
+3. 銆愭姤浠烽樁娈点��(鏂板)
+ - 鍒涘缓閿�鍞姤浠峰崟 鈫� 鎶ヤ环閲戦锛�95,000鍏�
+ - 閫夋嫨MDM鐗╂枡 鈫� 鐗╂枡A銆佺墿鏂橞
+ - 鎻愪氦瀹℃壒 鈫� 閫夋嫨瀹℃壒娴佺▼
+ - 瀹℃壒閫氳繃 鈫� 鐘舵�侊細宸查�氳繃
+
+4. 銆愬悎鍚岄樁娈点��
+ - 鎶ヤ环鍗曡浆鍚堝悓 鈫� 鍚堝悓鑷姩鍒涘缓
+ - 鍚堝悓瀹℃壒 鈫� BPM瀹℃壒娴佺▼
+ - 瀹℃壒閫氳繃 鈫� 鐘舵�侊細瀹℃牳閫氳繃
+
+5. 銆愯鍗曢樁娈点��
+ - 鍚堝悓鐢熸垚閿�鍞鍗� 鈫� ERP璁㈠崟鍒涘缓
+ - 璁㈠崟鍏宠仈鍚堝悓 鈫� 鍚堝悓缂栧彿璁板綍
+ - 璁㈠崟瀹℃壒 鈫� ERP瀹℃壒娴佺▼
+ - 瀹℃壒閫氳繃 鈫� 鍙墽琛屽嚭搴�
+
+6. 銆愬嚭搴撻樁娈点��
+ - 鍒涘缓閿�鍞嚭搴撳崟 鈫� 閫夋嫨浠撳簱
+ - 鍑哄簱瀹℃牳 鈫� 鍑哄簱瀹屾垚
+ - 鍑哄簱鏁伴噺鍥炲啓璁㈠崟
+
+7. 銆愭敹娆鹃樁娈点��
+ - 鍒涘缓鏀舵鍗� 鈫� 鍏宠仈璁㈠崟
+ - 鏀舵閲戦锛�50,000鍏�
+ - 鏀舵鏍搁攢 鈫� 璁㈠崟鏀舵绱
+
+8. 銆愯拷婧煡璇€��
+ - 鏌ヨ閿�鍞祦绋嬭拷婧� 鈫� 鏄剧ず瀹屾暣閾炬潯
+ - 鍟嗘満 鈫� 鎶ヤ环 鈫� 鍚堝悓 鈫� 璁㈠崟 鈫� 鍑哄簱 鈫� 鏀舵
+```
+
+---
+
+## 鍗併�佹暟鎹縼绉绘敞鎰忎簨椤�
+
+### 10.1 鍚堝悓浜у搧琛ㄥ彉鏇�
+
+鍘熻〃浣跨敤 CRM 浜у搧锛岀幇鏀逛负 MDM 鐗╂枡锛�
+
+```sql
+-- 鏂板瀛楁
+ALTER TABLE crm_contract_product
+ ADD COLUMN item_id bigint COMMENT 'MDM鐗╂枡缂栧彿',
+ ADD COLUMN item_unit_id bigint COMMENT '璁¢噺鍗曚綅缂栧彿',
+ ADD COLUMN item_price decimal(24,6) COMMENT '鐗╂枡鍘熶环';
+
+-- 鏁版嵁杩佺Щ锛堝鏋滄湁鏄犲皠鍏崇郴锛�
+-- UPDATE crm_contract_product SET item_id = product_id;
+```
+
+### 10.2 BPM 鍒嗙被鍒涘缓
+
+闇�瑕佸湪 BPM 绠$悊鍚庡彴鍒涘缓瀹℃壒鍒嗙被锛�
+
+```sql
+INSERT INTO bpm_category (name, code, description, status, sort)
+VALUES ('閿�鍞姤浠峰鎵�', 'sale_quotation_approve', '閿�鍞姤浠峰崟瀹℃壒娴佺▼鍒嗙被', 0, 0);
+```
+
+---
+
+## 鍗佷竴銆佹�荤粨
+
+### 缁熶竴鏁版嵁婧�
+
+| 鏁版嵁绫诲瀷 | 缁熶竴鏉ユ簮 | 浣跨敤妯″潡 |
+|----------|----------|----------|
+| 瀹㈡埛 | CRM (`crm_customer`) | CRM銆丒RP閿�鍞叏娴佺▼ |
+| 鐗╂枡 | MDM (`mdm_item`) | 閿�鍞姤浠枫�佸悎鍚屻�丒RP璁㈠崟 |
+
+### 鏍稿績娴佺▼
+
+```
+瀹㈡埛(CRM) 鈫� 鍟嗘満(CRM) 鈫� 鎶ヤ环(CRM+BPM) 鈫� 鍚堝悓(CRM+BPM) 鈫� 璁㈠崟(ERP) 鈫� 鍑哄簱(ERP) 鈫� 鏀舵(ERP)
+```
+
+### 鍏抽敭鏀硅繘
+
+1. **鏁版嵁缁熶竴**锛氬鎴峰拰鐗╂枡鏁版嵁涓嶅啀鍒嗘暎锛岀粺涓�绠$悊
+2. **娴佺▼琛旀帴**锛氭姤浠� 鈫� 鍚堝悓 鈫� 璁㈠崟锛屾暟鎹棤缂濇祦杞�
+3. **BPM闆嗘垚**锛氭姤浠峰拰鍚堝悓閮芥敮鎸佸鎵规祦绋�
+4. **杩芥函瀹屾暣**锛氫粠鍟嗘満鍒板洖娆惧叏閾炬潯鍙拷婧�
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
index ca5f36e..23b17b2 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
@@ -142,17 +142,21 @@
@Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
private Long id;
- @Schema(description = "浜у搧缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
- private Long productId;
- @Schema(description = "浜у搧鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鏉庡洓")
- private String productName;
- @Schema(description = "浜у搧鏉$爜", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
- private String productNo;
- @Schema(description = "浜у搧鍗曚綅", requiredMode = Schema.RequiredMode.REQUIRED, example = "鏉庡洓")
- private Integer productUnit;
+ @Schema(description = "MDM 鐗╂枡缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+ private Long itemId;
+ @Schema(description = "鐗╂枡缂栫爜", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
+ private String itemCode;
+ @Schema(description = "鐗╂枡鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鐗╂枡A")
+ private String itemName;
+ @Schema(description = "鐗╂枡鏉$爜", example = "BAR001")
+ private String itemBarCode;
+ @Schema(description = "璁¢噺鍗曚綅缂栧彿", example = "1")
+ private Long itemUnitId;
+ @Schema(description = "璁¢噺鍗曚綅鍚嶇О", example = "涓�")
+ private String itemUnitName;
- @Schema(description = "浜у搧鍗曚环", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
- private BigDecimal productPrice;
+ @Schema(description = "鐗╂枡鍘熶环", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+ private BigDecimal itemPrice;
@Schema(description = "鍚堝悓浠锋牸", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
private BigDecimal contractPrice;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java
index 47491ee..1ef6471 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java
@@ -90,13 +90,13 @@
@AllArgsConstructor
public static class Product {
- @Schema(description = "浜у搧缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
- @NotNull(message = "浜у搧缂栧彿涓嶈兘涓虹┖")
- private Long productId;
+ @Schema(description = "MDM 鐗╂枡缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+ @NotNull(message = "鐗╂枡缂栧彿涓嶈兘涓虹┖")
+ private Long itemId;
- @Schema(description = "浜у搧鍗曚环", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
- @NotNull(message = "浜у搧鍗曚环涓嶈兘涓虹┖")
- private BigDecimal productPrice;
+ @Schema(description = "鐗╂枡鍘熶环", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+ @NotNull(message = "鐗╂枡鍘熶环涓嶈兘涓虹┖")
+ private BigDecimal itemPrice;
@Schema(description = "鍚堝悓浠锋牸", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
@NotNull(message = "鍚堝悓浠锋牸涓嶈兘涓虹┖")
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java
index b59ef80..6739ee2 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java
@@ -28,7 +28,9 @@
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -92,22 +94,40 @@
PageResult<CrmFollowUpRecordDO> pageResult = followUpRecordService.getFollowUpRecordPage(pageReqVO);
// 1.1 鏌ヨ鑱旂郴浜哄拰鍟嗘満
Map<Long, CrmContactDO> contactMap = contactService.getContactMap(
- convertSetByFlatMap(pageResult.getList(), item -> item.getContactIds().stream()));
+ convertSetByFlatMap(pageResult.getList(), item -> {
+ List<Long> contactIds = item.getContactIds();
+ return contactIds != null ? contactIds.stream() : Stream.empty();
+ }));
Map<Long, CrmBusinessDO> businessMap = businessService.getBusinessMap(
- convertSetByFlatMap(pageResult.getList(), item -> item.getBusinessIds().stream()));
+ convertSetByFlatMap(pageResult.getList(), item -> {
+ List<Long> businessIds = item.getBusinessIds();
+ return businessIds != null ? businessIds.stream() : Stream.empty();
+ }));
// 1.2 鏌ヨ鐢ㄦ埛
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
- convertSet(pageResult.getList(), item -> Long.valueOf(item.getCreator())));
+ convertSet(pageResult.getList(), item -> {
+ String creator = item.getCreator();
+ return creator != null && !creator.isEmpty() ? Long.valueOf(creator) : null;
+ }));
// 2. 鎷兼帴鏁版嵁
PageResult<CrmFollowUpRecordRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmFollowUpRecordRespVO.class, record -> {
// 2.1 璁剧疆鑱旂郴浜哄拰鍟嗘満淇℃伅
record.setBusinesses(new ArrayList<>()).setContacts(new ArrayList<>());
- record.getContactIds().forEach(id -> MapUtils.findAndThen(contactMap, id, contact ->
- record.getContacts().add(new CrmBusinessRespVO().setId(contact.getId()).setName(contact.getName()))));
- record.getBusinessIds().forEach(id -> MapUtils.findAndThen(businessMap, id, business ->
- record.getBusinesses().add(new CrmBusinessRespVO().setId(business.getId()).setName(business.getName()))));
+ List<Long> contactIds = record.getContactIds();
+ if (contactIds != null) {
+ contactIds.forEach(id -> MapUtils.findAndThen(contactMap, id, contact ->
+ record.getContacts().add(new CrmBusinessRespVO().setId(contact.getId()).setName(contact.getName()))));
+ }
+ List<Long> businessIds = record.getBusinessIds();
+ if (businessIds != null) {
+ businessIds.forEach(id -> MapUtils.findAndThen(businessMap, id, business ->
+ record.getBusinesses().add(new CrmBusinessRespVO().setId(business.getId()).setName(business.getName()))));
+ }
// 2.2 璁剧疆鐢ㄦ埛淇℃伅
- MapUtils.findAndThen(userMap, Long.valueOf(record.getCreator()), user -> record.setCreatorName(user.getNickname()));
+ String creator = record.getCreator();
+ if (creator != null && !creator.isEmpty()) {
+ MapUtils.findAndThen(userMap, Long.valueOf(creator), user -> record.setCreatorName(user.getNickname()));
+ }
});
return success(voPageResult);
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java
new file mode 100644
index 0000000..e1f2713
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java
@@ -0,0 +1,238 @@
+package cn.iocoder.yudao.module.crm.controller.admin.quotation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationProductDO;
+import cn.iocoder.yudao.module.crm.service.quotation.CrmSaleQuotationService;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
+import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
+import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - CRM 閿�鍞姤浠峰崟")
+@RestController
+@RequestMapping("/crm/sale-quotation")
+@Validated
+public class CrmSaleQuotationController {
+
+ @Resource
+ private CrmSaleQuotationService saleQuotationService;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+ @Resource
+ private DeptApi deptApi;
+ @Resource
+ private MdmItemApi mdmItemApi;
+ @Resource
+ private MdmUnitMeasureApi mdmUnitMeasureApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓閿�鍞姤浠峰崟")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:create')")
+ public CommonResult<Long> createSaleQuotation(@Valid @RequestBody CrmSaleQuotationSaveReqVO createReqVO) {
+ return success(saleQuotationService.createSaleQuotation(createReqVO, getLoginUserId()));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊閿�鍞姤浠峰崟")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:update')")
+ public CommonResult<Boolean> updateSaleQuotation(@Valid @RequestBody CrmSaleQuotationSaveReqVO updateReqVO) {
+ saleQuotationService.updateSaleQuotation(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎閿�鍞姤浠峰崟")
+ @Parameter(name = "ids", description = "缂栧彿鏁扮粍", required = true)
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:delete')")
+ public CommonResult<Boolean> deleteSaleQuotation(@RequestParam("ids") List<Long> ids) {
+ saleQuotationService.deleteSaleQuotation(ids);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱閿�鍞姤浠峰崟")
+ @Parameter(name = "id", description = "缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')")
+ public CommonResult<CrmSaleQuotationRespVO> getSaleQuotation(@RequestParam("id") Long id) {
+ CrmSaleQuotationDO saleQuotation = saleQuotationService.getSaleQuotation(id);
+ if (saleQuotation == null) {
+ return success(null);
+ }
+ List<CrmSaleQuotationProductDO> productList = saleQuotationService.getSaleQuotationProductListByQuotationId(id);
+ return success(buildSaleQuotationRespVO(saleQuotation, productList));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱閿�鍞姤浠峰崟鍒嗛〉")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')")
+ public CommonResult<PageResult<CrmSaleQuotationRespVO>> getSaleQuotationPage(@Valid CrmSaleQuotationPageReqVO pageReqVO) {
+ PageResult<CrmSaleQuotationDO> pageResult = saleQuotationService.getSaleQuotationPage(pageReqVO, getLoginUserId());
+ return success(buildSaleQuotationRespVOPageResult(pageResult));
+ }
+
+ @GetMapping("/page-by-customer")
+ @Operation(summary = "鑾峰緱閿�鍞姤浠峰崟鍒嗛〉锛堝熀浜庡鎴凤級")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')")
+ public CommonResult<PageResult<CrmSaleQuotationRespVO>> getSaleQuotationPageByCustomerId(@Valid CrmSaleQuotationPageReqVO pageReqVO) {
+ PageResult<CrmSaleQuotationDO> pageResult = saleQuotationService.getSaleQuotationPageByCustomerId(pageReqVO);
+ return success(buildSaleQuotationRespVOPageResult(pageResult));
+ }
+
+ @GetMapping("/page-by-business")
+ @Operation(summary = "鑾峰緱閿�鍞姤浠峰崟鍒嗛〉锛堝熀浜庡晢鏈猴級")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')")
+ public CommonResult<PageResult<CrmSaleQuotationRespVO>> getSaleQuotationPageByBusinessId(@Valid CrmSaleQuotationPageReqVO pageReqVO) {
+ PageResult<CrmSaleQuotationDO> pageResult = saleQuotationService.getSaleQuotationPageByBusinessId(pageReqVO);
+ return success(buildSaleQuotationRespVOPageResult(pageResult));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "瀵煎嚭閿�鍞姤浠峰崟 Excel")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:export')")
+ @ApiAccessLog(operateType = EXPORT)
+ public void exportSaleQuotationExcel(@Valid CrmSaleQuotationPageReqVO pageReqVO,
+ HttpServletResponse response) throws IOException {
+ pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+ List<CrmSaleQuotationRespVO> list = buildSaleQuotationRespVOPageResult(
+ saleQuotationService.getSaleQuotationPage(pageReqVO, getLoginUserId())).getList();
+ ExcelUtils.write(response, "閿�鍞姤浠峰崟.xls", "鏁版嵁", CrmSaleQuotationRespVO.class, list);
+ }
+
+ @PutMapping("/submit")
+ @Operation(summary = "鎻愪氦閿�鍞姤浠峰鎵�")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:submit')")
+ public CommonResult<Boolean> submitSaleQuotation(@RequestParam("id") Long id,
+ @RequestParam("processDefinitionKey") String processDefinitionKey) {
+ saleQuotationService.submitSaleQuotation(id, processDefinitionKey, getLoginUserId());
+ return success(true);
+ }
+
+ @GetMapping("/approve-process-list")
+ @Operation(summary = "鑾峰彇閿�鍞姤浠峰鎵规祦绋嬪垪琛�")
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')")
+ public CommonResult<List<Map<String, Object>>> getSaleQuotationApproveProcessList() {
+ return success(saleQuotationService.getSaleQuotationApproveProcessDefinitionList());
+ }
+
+ @PostMapping("/convert-contract")
+ @Operation(summary = "杞负鍚堝悓")
+ @Parameter(name = "id", description = "鎶ヤ环鍗曠紪鍙�", required = true)
+ @PreAuthorize("@ss.hasPermission('crm:sale-quotation:convert-contract')")
+ public CommonResult<Long> convertToContract(@RequestParam("id") Long id) {
+ return success(saleQuotationService.convertToContract(id, getLoginUserId()));
+ }
+
+ // ==================== 鏋勫缓鍝嶅簲 VO ====================
+
+ private CrmSaleQuotationRespVO buildSaleQuotationRespVO(CrmSaleQuotationDO saleQuotation, List<CrmSaleQuotationProductDO> productList) {
+ CrmSaleQuotationRespVO respVO = BeanUtils.toBean(saleQuotation, CrmSaleQuotationRespVO.class);
+ // 1. 鏋勫缓鐗╂枡鏄庣粏
+ if (CollUtil.isNotEmpty(productList)) {
+ Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(productList, CrmSaleQuotationProductDO::getItemId));
+ Map<Long, MdmUnitMeasureRespDTO> unitMap = mdmUnitMeasureApi.getUnitMeasureMap(
+ convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId));
+ respVO.setItems(BeanUtils.toBean(productList, CrmSaleQuotationRespVO.Item.class, item -> {
+ MdmItemRespDTO mdmItem = itemMap.get(item.getItemId());
+ if (mdmItem != null) {
+ item.setItemCode(mdmItem.getCode());
+ item.setItemName(mdmItem.getName());
+ item.setItemBarCode(mdmItem.getBarCode());
+ }
+ MapUtils.findAndThen(unitMap, item.getItemUnitId(), unit -> item.setItemUnitName(unit.getName()));
+ }));
+ }
+ // 2. 濉厖鍏宠仈淇℃伅
+ fillSaleQuotationRelatedFields(respVO);
+ return respVO;
+ }
+
+ private PageResult<CrmSaleQuotationRespVO> buildSaleQuotationRespVOPageResult(PageResult<CrmSaleQuotationDO> pageResult) {
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return PageResult.empty(pageResult.getTotal());
+ }
+ // 1. 鎶ヤ环鏄庣粏
+ List<CrmSaleQuotationProductDO> productList = saleQuotationService.getSaleQuotationProductListByQuotationIds(
+ convertSet(pageResult.getList(), CrmSaleQuotationDO::getId));
+ Map<Long, List<CrmSaleQuotationProductDO>> productMap = convertMultiMap(productList, CrmSaleQuotationProductDO::getQuotationId);
+ // 1.2 鐗╂枡淇℃伅
+ Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(productList, CrmSaleQuotationProductDO::getItemId));
+ // 1.3 鐢ㄦ埛淇℃伅
+ Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+ convertSet(pageResult.getList(), CrmSaleQuotationDO::getOwnerUserId));
+ // 1.4 閮ㄩ棬淇℃伅
+ Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+ convertSet(pageResult.getList(), CrmSaleQuotationDO::getOwnerUserId));
+
+ // 2. 寮�濮嬫嫾鎺�
+ return BeanUtils.toBean(pageResult, CrmSaleQuotationRespVO.class, vo -> {
+ vo.setItems(BeanUtils.toBean(productMap.get(vo.getId()), CrmSaleQuotationRespVO.Item.class,
+ item -> MapUtils.findAndThen(itemMap, item.getItemId(), mdmItem -> item.setItemCode(mdmItem.getCode())
+ .setItemName(mdmItem.getName()).setItemBarCode(mdmItem.getBarCode()))));
+ MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname()));
+ MapUtils.findAndThen(deptMap, vo.getOwnerUserId(), dept -> vo.setOwnerUserDeptName(dept.getName()));
+ });
+ }
+
+ private void fillSaleQuotationRelatedFields(CrmSaleQuotationRespVO vo) {
+ // 濉厖璐熻矗浜哄悕绉�
+ if (vo.getOwnerUserId() != null) {
+ AdminUserRespDTO user = adminUserApi.getUser(vo.getOwnerUserId());
+ if (user != null) {
+ vo.setOwnerUserName(user.getNickname());
+ // 濉厖閮ㄩ棬鍚嶇О
+ if (user.getDeptId() != null) {
+ DeptRespDTO dept = deptApi.getDept(user.getDeptId());
+ if (dept != null) {
+ vo.setOwnerUserDeptName(dept.getName());
+ }
+ }
+ }
+ }
+ // 濉厖鍒涘缓浜哄悕绉�
+ if (vo.getCreator() != null) {
+ try {
+ AdminUserRespDTO user = adminUserApi.getUser(Long.valueOf(vo.getCreator()));
+ if (user != null) {
+ vo.setCreatorName(user.getNickname());
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationPageReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationPageReqVO.java
new file mode 100644
index 0000000..cd4cf9f
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationPageReqVO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.controller.admin.quotation.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞姤浠峰崟鍒嗛〉 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmSaleQuotationPageReqVO extends PageParam {
+
+ @Schema(description = "鎶ヤ环鍗曞彿", example = "QT001")
+ private String no;
+
+ @Schema(description = "鎶ヤ环鍗曞悕绉�", example = "鎶ヤ环鍗旳")
+ private String name;
+
+ @Schema(description = "瀹㈡埛缂栧彿", example = "18336")
+ private Long customerId;
+
+ @Schema(description = "鍟嗘満缂栧彿", example = "10864")
+ private Long businessId;
+
+ @Schema(description = "鍦烘櫙绫诲瀷", example = "1")
+ @InEnum(CrmSceneTypeEnum.class)
+ private Integer sceneType;
+
+ @Schema(description = "瀹℃壒鐘舵��", example = "20")
+ @InEnum(CrmSaleQuotationStatusEnum.class)
+ private Integer status;
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java
new file mode 100644
index 0000000..d9d037f
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java
@@ -0,0 +1,171 @@
+package cn.iocoder.yudao.module.crm.controller.admin.quotation.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞姤浠峰崟 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CrmSaleQuotationRespVO {
+
+ @Schema(description = "鎶ヤ环鍗曠紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+ @ExcelProperty("鎶ヤ环鍗曠紪鍙�")
+ private Long id;
+
+ @Schema(description = "鎶ヤ环鍗曞彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "QT001")
+ @ExcelProperty("鎶ヤ环鍗曞彿")
+ private String no;
+
+ @Schema(description = "鎶ヤ环鍗曞悕绉�", requiredMode = Schema.RequiredMode.REQUIRED, example = "鎶ヤ环鍗旳")
+ @ExcelProperty("鎶ヤ环鍗曞悕绉�")
+ private String name;
+
+ @Schema(description = "瀹㈡埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
+ @ExcelProperty("瀹㈡埛缂栧彿")
+ private Long customerId;
+ @Schema(description = "瀹㈡埛鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "瀹㈡埛A")
+ @ExcelProperty("瀹㈡埛鍚嶇О")
+ private String customerName;
+
+ @Schema(description = "鍟嗘満缂栧彿", example = "10864")
+ @ExcelProperty("鍟嗘満缂栧彿")
+ private Long businessId;
+ @Schema(description = "鍟嗘満鍚嶇О", example = "鍟嗘満A")
+ @ExcelProperty("鍟嗘満鍚嶇О")
+ private String businessName;
+
+ @Schema(description = "鑱旂郴浜虹紪鍙�", example = "18546")
+ private Long contactId;
+ @Schema(description = "鑱旂郴浜哄悕绉�", example = "鑱旂郴浜篈")
+ @ExcelProperty("鑱旂郴浜哄悕绉�")
+ private String contactName;
+
+ @Schema(description = "璐熻矗浜虹殑鐢ㄦ埛缂栧彿", example = "25682")
+ @ExcelProperty("璐熻矗浜虹紪鍙�")
+ private Long ownerUserId;
+ @Schema(description = "璐熻矗浜哄悕瀛�", example = "寮犱笁")
+ @ExcelProperty("璐熻矗浜哄悕瀛�")
+ private String ownerUserName;
+ @Schema(description = "璐熻矗浜洪儴闂�")
+ @ExcelProperty("璐熻矗浜洪儴闂�")
+ private String ownerUserDeptName;
+
+ @Schema(description = "瀹℃壒鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+ @ExcelProperty("瀹℃壒鐘舵��")
+ private Integer status;
+
+ @Schema(description = "宸ヤ綔娴佺紪鍙�", example = "1043")
+ private String processInstanceId;
+
+ @Schema(description = "鎶ヤ环鏃ユ湡")
+ @ExcelProperty("鎶ヤ环鏃ユ湡")
+ private LocalDateTime quotationTime;
+
+ @Schema(description = "鏈夋晥鏈熻嚦")
+ @ExcelProperty("鏈夋晥鏈熻嚦")
+ private LocalDateTime validUntil;
+
+ @Schema(description = "浜у搧鎬婚噾棰�", example = "19510")
+ @ExcelProperty("浜у搧鎬婚噾棰�")
+ private BigDecimal totalProductPrice;
+
+ @Schema(description = "鏁村崟鎶樻墸", example = "10.00")
+ @ExcelProperty("鏁村崟鎶樻墸")
+ private BigDecimal discountPercent;
+
+ @Schema(description = "浼樻儬閲戦", example = "1951")
+ @ExcelProperty("浼樻儬閲戦")
+ private BigDecimal discountPrice;
+
+ @Schema(description = "鎶ヤ环鎬婚噾棰�", example = "17559")
+ @ExcelProperty("鎶ヤ环鎬婚噾棰�")
+ private BigDecimal totalPrice;
+
+ @Schema(description = "绋庣巼", example = "13.00")
+ @ExcelProperty("绋庣巼")
+ private BigDecimal taxRate;
+
+ @Schema(description = "绋庨", example = "2282")
+ @ExcelProperty("绋庨")
+ private BigDecimal taxPrice;
+
+ @Schema(description = "鍏宠仈鍚堝悓缂栧彿", example = "12345")
+ private Long contractId;
+ @Schema(description = "鍏宠仈鍚堝悓鍗曞彿", example = "HT001")
+ @ExcelProperty("鍏宠仈鍚堝悓鍗曞彿")
+ private String contractNo;
+
+ @Schema(description = "闄勪欢鍦板潃", example = "https://www.baidu.com")
+ private String fileUrl;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞淇℃伅")
+ @ExcelProperty("澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+ @Schema(description = "鍒涘缓浜�", example = "25682")
+ private String creator;
+ @Schema(description = "鍒涘缓浜哄悕瀛�", example = "寮犱笁")
+ @ExcelProperty("鍒涘缓浜哄悕瀛�")
+ private String creatorName;
+
+ @Schema(description = "鐗╂枡鏄庣粏鍒楄〃")
+ private List<Item> items;
+
+ @Schema(description = "鐗╂枡鏄庣粏")
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Item {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
+ private Long id;
+
+ @Schema(description = "MDM 鐗╂枡缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+ private Long itemId;
+ @Schema(description = "鐗╂枡缂栫爜", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
+ private String itemCode;
+ @Schema(description = "鐗╂枡鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鐗╂枡A")
+ private String itemName;
+ @Schema(description = "鐗╂枡鏉$爜", example = "BAR001")
+ private String itemBarCode;
+ @Schema(description = "璁¢噺鍗曚綅缂栧彿", example = "1")
+ private Long itemUnitId;
+ @Schema(description = "璁¢噺鍗曚綅鍚嶇О", example = "涓�")
+ private String itemUnitName;
+
+ @Schema(description = "鐗╂枡鍘熶环", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+ private BigDecimal itemPrice;
+
+ @Schema(description = "鎶ヤ环鍗曚环", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+ private BigDecimal quotationPrice;
+
+ @Schema(description = "鏁伴噺", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+ private BigDecimal count;
+
+ @Schema(description = "灏忚閲戦", requiredMode = Schema.RequiredMode.REQUIRED, example = "1230.00")
+ private BigDecimal totalPrice;
+
+ @Schema(description = "绋庣巼", example = "13.00")
+ private BigDecimal taxPercent;
+
+ @Schema(description = "绋庨", example = "159.90")
+ private BigDecimal taxPrice;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ private String remark;
+
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationSaveReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationSaveReqVO.java
new file mode 100644
index 0000000..a235d19
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationSaveReqVO.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.crm.controller.admin.quotation.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "绠$悊鍚庡彴 - CRM 閿�鍞姤浠峰崟鍒涘缓/鏇存柊 Request VO")
+@Data
+public class CrmSaleQuotationSaveReqVO {
+
+ @Schema(description = "鎶ヤ环鍗曠紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+ private Long id;
+
+ @Schema(description = "鎶ヤ环鍗曞悕绉�", requiredMode = Schema.RequiredMode.REQUIRED, example = "鎶ヤ环鍗旳")
+ @NotNull(message = "鎶ヤ环鍗曞悕绉颁笉鑳戒负绌�")
+ private String name;
+
+ @Schema(description = "瀹㈡埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
+ @NotNull(message = "瀹㈡埛缂栧彿涓嶈兘涓虹┖")
+ private Long customerId;
+
+ @Schema(description = "鍟嗘満缂栧彿", example = "10864")
+ private Long businessId;
+
+ @Schema(description = "鑱旂郴浜虹紪鍙�", example = "18546")
+ private Long contactId;
+
+ @Schema(description = "璐熻矗浜虹殑鐢ㄦ埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "17144")
+ @NotNull(message = "璐熻矗浜轰笉鑳戒负绌�")
+ private Long ownerUserId;
+
+ @Schema(description = "鎶ヤ环鏃ユ湡")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime quotationTime;
+
+ @Schema(description = "鏈夋晥鏈熻嚦")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime validUntil;
+
+ @Schema(description = "鏁村崟鎶樻墸", example = "10.00")
+ private BigDecimal discountPercent;
+
+ @Schema(description = "绋庣巼", example = "13.00")
+ private BigDecimal taxRate;
+
+ @Schema(description = "闄勪欢鍦板潃", example = "https://www.baidu.com")
+ private String fileUrl;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞淇℃伅")
+ private String remark;
+
+ @Schema(description = "鐗╂枡鏄庣粏鍒楄〃", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "鐗╂枡鏄庣粏涓嶈兘涓虹┖")
+ private List<Item> items;
+
+ @Schema(description = "鐗╂枡鏄庣粏")
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Item {
+
+ @Schema(description = "缂栧彿", example = "888")
+ private Long id;
+
+ @Schema(description = "MDM 鐗╂枡缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+ @NotNull(message = "鐗╂枡缂栧彿涓嶈兘涓虹┖")
+ private Long itemId;
+
+ @Schema(description = "鎶ヤ环鍗曚环", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+ @NotNull(message = "鎶ヤ环鍗曚环涓嶈兘涓虹┖")
+ private BigDecimal quotationPrice;
+
+ @Schema(description = "鏁伴噺", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+ @NotNull(message = "鏁伴噺涓嶈兘涓虹┖")
+ private BigDecimal count;
+
+ @Schema(description = "绋庣巼", example = "13.00")
+ private BigDecimal taxPercent;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ private String remark;
+
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractProductDO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractProductDO.java
index 6bb3e6b..2693f33 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractProductDO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractProductDO.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.contract;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -36,15 +35,21 @@
*/
private Long contractId;
/**
- * 浜у搧缂栧彿
+ * MDM 鐗╂枡缂栧彿
*
- * 鍏宠仈 {@link CrmProductDO#getId()}
+ * 鍏宠仈 MdmItemRespDTO.id
*/
- private Long productId;
+ private Long itemId;
/**
- * 浜у搧鍗曚环锛屽崟浣嶏細鍏�
+ * 璁¢噺鍗曚綅缂栧彿
+ *
+ * 鍏宠仈 MdmUnitMeasureRespDTO.id
*/
- private BigDecimal productPrice;
+ private Long itemUnitId;
+ /**
+ * 鐗╂枡鍘熶环锛屽崟浣嶏細鍏冿紙鍐椾綑锛�
+ */
+ private BigDecimal itemPrice;
/**
* 鍚堝悓浠锋牸, 鍗曚綅锛氬厓
*/
@@ -56,7 +61,7 @@
/**
* 鎬昏浠锋牸锛屽崟浣嶏細鍏�
*
- * totalPrice = businessPrice * count
+ * totalPrice = contractPrice * count
*/
private BigDecimal totalPrice;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationDO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationDO.java
new file mode 100644
index 0000000..01329f2
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationDO.java
@@ -0,0 +1,136 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.quotation;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * CRM 閿�鍞姤浠峰崟 DO
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@TableName("crm_sale_quotation")
+@KeySequence("crm_sale_quotation_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmSaleQuotationDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 鎶ヤ环鍗曞彿
+ */
+ private String no;
+ /**
+ * 鎶ヤ环鍗曞悕绉�
+ */
+ private String name;
+ /**
+ * 瀹㈡埛缂栧彿
+ *
+ * 鍏宠仈 {@link CrmCustomerDO#getId()}
+ */
+ private Long customerId;
+ /**
+ * 鍟嗘満缂栧彿锛岄潪蹇呴』
+ *
+ * 鍏宠仈 {@link CrmBusinessDO#getId()}
+ */
+ private Long businessId;
+ /**
+ * 鑱旂郴浜虹紪鍙凤紝闈炲繀椤�
+ *
+ * 鍏宠仈 {@link CrmContactDO#getId()}
+ */
+ private Long contactId;
+ /**
+ * 璐熻矗浜虹敤鎴风紪鍙�
+ *
+ * 鍏宠仈 AdminUserDO 鐨� id 瀛楁
+ */
+ private Long ownerUserId;
+ /**
+ * 瀹℃壒鐘舵��
+ *
+ * 鏋氫妇 {@link CrmSaleQuotationStatusEnum}
+ */
+ private Integer status;
+ /**
+ * 宸ヤ綔娴佺紪鍙�
+ *
+ * 鍏宠仈 ProcessInstance 鐨� id 灞炴��
+ */
+ private String processInstanceId;
+
+ /**
+ * 鎶ヤ环鏃ユ湡
+ */
+ private LocalDateTime quotationTime;
+ /**
+ * 鏈夋晥鏈熻嚦
+ */
+ private LocalDateTime validUntil;
+ /**
+ * 浜у搧鎬婚噾棰濓紝鍗曚綅锛氬厓
+ */
+ private BigDecimal totalProductPrice;
+ /**
+ * 鏁村崟鎶樻墸锛岀櫨鍒嗘瘮
+ */
+ private BigDecimal discountPercent;
+ /**
+ * 浼樻儬閲戦锛屽崟浣嶏細鍏�
+ */
+ private BigDecimal discountPrice;
+ /**
+ * 鎶ヤ环鎬婚噾棰濓紝鍗曚綅锛氬厓
+ *
+ * totalPrice = totalProductPrice - discountPrice
+ */
+ private BigDecimal totalPrice;
+ /**
+ * 绋庣巼锛岀櫨鍒嗘瘮
+ */
+ private BigDecimal taxRate;
+ /**
+ * 绋庨锛屽崟浣嶏細鍏�
+ */
+ private BigDecimal taxPrice;
+
+ /**
+ * 鍏宠仈鍚堝悓缂栧彿
+ *
+ * 鍏宠仈 {@link CrmContractDO#getId()}
+ */
+ private Long contractId;
+ /**
+ * 鍏宠仈鍚堝悓鍗曞彿
+ */
+ private String contractNo;
+ /**
+ * 闄勪欢鍦板潃
+ */
+ private String fileUrl;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationProductDO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationProductDO.java
new file mode 100644
index 0000000..436ea42
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationProductDO.java
@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.quotation;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * CRM 閿�鍞姤浠峰崟浜у搧鏄庣粏 DO
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@TableName("crm_sale_quotation_product")
+@KeySequence("crm_sale_quotation_product_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmSaleQuotationProductDO extends BaseDO {
+
+ /**
+ * 涓婚敭
+ */
+ @TableId
+ private Long id;
+ /**
+ * 鎶ヤ环鍗曠紪鍙�
+ *
+ * 鍏宠仈 {@link CrmSaleQuotationDO#getId()}
+ */
+ private Long quotationId;
+ /**
+ * MDM 鐗╂枡缂栧彿
+ *
+ * 鍏宠仈 MdmItemRespDTO.id
+ */
+ private Long itemId;
+ /**
+ * 璁¢噺鍗曚綅缂栧彿
+ *
+ * 鍏宠仈 MdmUnitMeasureRespDTO.id
+ */
+ private Long itemUnitId;
+ /**
+ * 鐗╂枡鍘熶环锛屽崟浣嶏細鍏冿紙鍐椾綑锛�
+ */
+ private BigDecimal itemPrice;
+ /**
+ * 鎶ヤ环鍗曚环锛屽崟浣嶏細鍏�
+ */
+ private BigDecimal quotationPrice;
+ /**
+ * 鏁伴噺
+ */
+ private BigDecimal count;
+ /**
+ * 灏忚閲戦锛屽崟浣嶏細鍏�
+ *
+ * totalPrice = quotationPrice * count
+ */
+ private BigDecimal totalPrice;
+ /**
+ * 绋庣巼锛岀櫨鍒嗘瘮
+ */
+ private BigDecimal taxPercent;
+ /**
+ * 绋庨锛屽崟浣嶏細鍏�
+ */
+ private BigDecimal taxPrice;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationMapper.java
new file mode 100644
index 0000000..f43e8b1
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationMapper.java
@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.quotation;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
+import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
+import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * CRM 閿�鍞姤浠峰崟 Mapper
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Mapper
+public interface CrmSaleQuotationMapper extends BaseMapperX<CrmSaleQuotationDO> {
+
+ default CrmSaleQuotationDO selectByNo(String no) {
+ return selectOne(CrmSaleQuotationDO::getNo, no);
+ }
+
+ default PageResult<CrmSaleQuotationDO> selectPage(CrmSaleQuotationPageReqVO pageReqVO, Long userId) {
+ MPJLambdaWrapperX<CrmSaleQuotationDO> query = new MPJLambdaWrapperX<>();
+ // 鎷兼帴鏁版嵁鏉冮檺鐨勬煡璇㈡潯浠�
+ CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_SALE_QUOTATION.getType(),
+ CrmSaleQuotationDO::getId, userId, pageReqVO.getSceneType());
+ // 鎷兼帴鑷韩鐨勬煡璇㈡潯浠�
+ query.selectAll(CrmSaleQuotationDO.class)
+ .likeIfPresent(CrmSaleQuotationDO::getNo, pageReqVO.getNo())
+ .likeIfPresent(CrmSaleQuotationDO::getName, pageReqVO.getName())
+ .eqIfPresent(CrmSaleQuotationDO::getCustomerId, pageReqVO.getCustomerId())
+ .eqIfPresent(CrmSaleQuotationDO::getBusinessId, pageReqVO.getBusinessId())
+ .eqIfPresent(CrmSaleQuotationDO::getStatus, pageReqVO.getStatus())
+ .orderByDesc(CrmSaleQuotationDO::getId);
+ return selectJoinPage(pageReqVO, CrmSaleQuotationDO.class, query);
+ }
+
+ default PageResult<CrmSaleQuotationDO> selectPageByCustomerId(CrmSaleQuotationPageReqVO pageReqVO) {
+ return selectPage(pageReqVO, new LambdaQueryWrapperX<CrmSaleQuotationDO>()
+ .eq(CrmSaleQuotationDO::getCustomerId, pageReqVO.getCustomerId())
+ .likeIfPresent(CrmSaleQuotationDO::getNo, pageReqVO.getNo())
+ .likeIfPresent(CrmSaleQuotationDO::getName, pageReqVO.getName())
+ .orderByDesc(CrmSaleQuotationDO::getId));
+ }
+
+ default PageResult<CrmSaleQuotationDO> selectPageByBusinessId(CrmSaleQuotationPageReqVO pageReqVO) {
+ return selectPage(pageReqVO, new LambdaQueryWrapperX<CrmSaleQuotationDO>()
+ .eq(CrmSaleQuotationDO::getBusinessId, pageReqVO.getBusinessId())
+ .likeIfPresent(CrmSaleQuotationDO::getNo, pageReqVO.getNo())
+ .likeIfPresent(CrmSaleQuotationDO::getName, pageReqVO.getName())
+ .orderByDesc(CrmSaleQuotationDO::getId));
+ }
+
+ default List<CrmSaleQuotationDO> selectListByCustomerId(Long customerId) {
+ return selectList(CrmSaleQuotationDO::getCustomerId, customerId);
+ }
+
+ default List<CrmSaleQuotationDO> selectListByBusinessId(Long businessId) {
+ return selectList(CrmSaleQuotationDO::getBusinessId, businessId);
+ }
+
+ default Long selectCountByBusinessId(Long businessId) {
+ return selectCount(CrmSaleQuotationDO::getBusinessId, businessId);
+ }
+
+ default Long selectCountByCustomerId(Long customerId) {
+ return selectCount(CrmSaleQuotationDO::getCustomerId, customerId);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationProductMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationProductMapper.java
new file mode 100644
index 0000000..38d62ec
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/quotation/CrmSaleQuotationProductMapper.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.quotation;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationProductDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * CRM 閿�鍞姤浠峰崟浜у搧鏄庣粏 Mapper
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Mapper
+public interface CrmSaleQuotationProductMapper extends BaseMapperX<CrmSaleQuotationProductDO> {
+
+ default List<CrmSaleQuotationProductDO> selectListByQuotationId(Long quotationId) {
+ return selectList(new LambdaQueryWrapperX<CrmSaleQuotationProductDO>()
+ .eq(CrmSaleQuotationProductDO::getQuotationId, quotationId));
+ }
+
+ default List<CrmSaleQuotationProductDO> selectListByQuotationIds(Collection<Long> quotationIds) {
+ return selectList(new LambdaQueryWrapperX<CrmSaleQuotationProductDO>()
+ .in(CrmSaleQuotationProductDO::getQuotationId, quotationIds));
+ }
+
+ default void deleteByQuotationId(Long quotationId) {
+ delete(new LambdaQueryWrapperX<CrmSaleQuotationProductDO>()
+ .eq(CrmSaleQuotationProductDO::getQuotationId, quotationId));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
index e917b13..a5f1962 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
@@ -29,6 +29,11 @@
*/
public static final String RECEIVABLE_PREFIX = "HK";
+ /**
+ * 閿�鍞姤浠� {@link cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO}
+ */
+ public static final String SALE_QUOTATION_NO_PREFIX = "QT";
+
@Resource
private StringRedisTemplate stringRedisTemplate;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index fa1c157..4149e75 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -109,4 +109,17 @@
// ========== 鏁版嵁缁熻 1_020_014_000 ==========
+ // ========== 閿�鍞姤浠� 1_020_015_000 ==========
+ ErrorCode SALE_QUOTATION_NOT_EXISTS = new ErrorCode(1_020_015_000, "閿�鍞姤浠峰崟涓嶅瓨鍦�");
+ ErrorCode SALE_QUOTATION_NO_EXISTS = new ErrorCode(1_020_015_001, "閿�鍞姤浠峰崟鍙峰凡瀛樺湪");
+ ErrorCode SALE_QUOTATION_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_020_015_002, "閿�鍞姤浠峰崟({})涓嶆槸鑽夌鐘舵�侊紝鏃犳硶淇敼");
+ ErrorCode SALE_QUOTATION_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_020_015_003, "閿�鍞姤浠峰崟({})涓嶆槸鑽夌鐘舵�侊紝鏃犳硶鍒犻櫎");
+ ErrorCode SALE_QUOTATION_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_015_004, "閿�鍞姤浠峰崟涓嶆槸鑽夌鐘舵�侊紝鏃犳硶鎻愪氦瀹℃壒");
+ ErrorCode SALE_QUOTATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_015_005, "閿�鍞姤浠峰崟涓嶆槸瀹℃壒涓姸鎬侊紝鏃犳硶鏇存柊瀹℃壒鐘舵��");
+ ErrorCode SALE_QUOTATION_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_020_015_006, "閿�鍞姤浠峰鎵瑰垎绫讳笉瀛樺湪锛岃妫�鏌� BPM 閰嶇疆");
+ ErrorCode SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_020_015_007, "閿�鍞姤浠峰鎵规祦绋嬩笉瀛樺湪鎴栨湭婵�娲伙紝璇锋鏌� BPM 閰嶇疆");
+ ErrorCode SALE_QUOTATION_NOT_APPROVE = new ErrorCode(1_020_015_008, "閿�鍞姤浠峰崟鏈鎵归�氳繃锛屾棤娉曡浆鍚堝悓");
+ ErrorCode SALE_QUOTATION_CONTRACT_EXISTS = new ErrorCode(1_020_015_009, "閿�鍞姤浠峰崟宸茶浆鍚堝悓锛岃鍕块噸澶嶆搷浣�");
+ ErrorCode SALE_QUOTATION_ITEM_NOT_EXISTS = new ErrorCode(1_020_015_010, "鐗╂枡涓嶅瓨鍦ㄦ垨鏈惎鐢�");
+
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
index f2d3aaa..6220ddc 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
@@ -24,7 +24,8 @@
CRM_CONTRACT(5, "鍚堝悓"),
CRM_PRODUCT(6, "浜у搧"),
CRM_RECEIVABLE(7, "鍥炴"),
- CRM_RECEIVABLE_PLAN(8, "鍥炴璁″垝")
+ CRM_RECEIVABLE_PLAN(8, "鍥炴璁″垝"),
+ CRM_SALE_QUOTATION(9, "閿�鍞姤浠�")
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(CrmBizTypeEnum::getType).toArray(Integer[]::new);
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/quotation/CrmSaleQuotationStatusEnum.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/quotation/CrmSaleQuotationStatusEnum.java
new file mode 100644
index 0000000..e8029d4
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/quotation/CrmSaleQuotationStatusEnum.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.crm.enums.quotation;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * CRM 閿�鍞姤浠峰鎵圭姸鎬佹灇涓�
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmSaleQuotationStatusEnum implements ArrayValuable<Integer> {
+
+ DRAFT(0, "鑽夌"),
+ PROCESS(10, "瀹℃壒涓�"),
+ APPROVE(20, "宸查�氳繃"),
+ REJECT(30, "宸叉嫆缁�"),
+ CANCEL(40, "宸插彇娑�"),
+ CONVERTED(50, "宸茶浆鍚堝悓");
+
+ private final Integer status;
+ private final String name;
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(CrmSaleQuotationStatusEnum::getStatus).toArray(Integer[]::new);
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
index b997a26..7e423f1 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -34,6 +34,8 @@
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -49,6 +51,7 @@
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -100,6 +103,8 @@
private BpmProcessInstanceApi bpmProcessInstanceApi;
@Resource
private ErpSaleOrderApi saleOrderApi;
+ @Resource
+ private MdmItemApi mdmItemApi;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -213,11 +218,19 @@
}
private List<CrmContractProductDO> validateContractProducts(List<CrmContractSaveReqVO.Product> list) {
- // 1. 鏍¢獙浜у搧瀛樺湪
- productService.validProductList(convertSet(list, CrmContractSaveReqVO.Product::getProductId));
+ // 1. 鏍¢獙鐗╂枡瀛樺湪涓斿惎鐢�
+ List<Long> itemIds = convertSet(list, CrmContractSaveReqVO.Product::getItemId);
+ Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds);
// 2. 杞寲涓� CrmContractProductDO 鍒楄〃
- return convertList(list, o -> BeanUtils.toBean(o, CrmContractProductDO.class,
- item -> item.setTotalPrice(MoneyUtils.priceMultiply(item.getContractPrice(), item.getCount()))));
+ return convertList(list, o -> BeanUtils.toBean(o, CrmContractProductDO.class, item -> {
+ MdmItemRespDTO mdmItem = itemMap.get(item.getItemId());
+ if (mdmItem == null) {
+ throw exception(PRODUCT_NOT_EXISTS);
+ }
+ item.setItemUnitId(mdmItem.getUnitMeasureId());
+ item.setItemPrice(mdmItem.getSalesPrice());
+ item.setTotalPrice(MoneyUtils.priceMultiply(item.getContractPrice(), item.getCount()));
+ }));
}
private void calculateTotalPrice(CrmContractDO contract, List<CrmContractProductDO> contractProducts) {
@@ -455,7 +468,7 @@
List<ErpSaleOrderCreateReqDTO.Item> items = contractProducts.stream()
.map(cp -> {
ErpSaleOrderCreateReqDTO.Item item = new ErpSaleOrderCreateReqDTO.Item();
- item.setProductId(cp.getProductId());
+ item.setItemId(cp.getItemId());
item.setProductPrice(cp.getContractPrice());
item.setCount(cp.getCount());
return item;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationService.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationService.java
new file mode 100644
index 0000000..3170988
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationService.java
@@ -0,0 +1,125 @@
+package cn.iocoder.yudao.module.crm.service.quotation;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationProductDO;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CRM 閿�鍞姤浠峰崟 Service 鎺ュ彛
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+public interface CrmSaleQuotationService {
+
+ /**
+ * 鍒涘缓閿�鍞姤浠峰崟
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 缂栧彿
+ */
+ Long createSaleQuotation(CrmSaleQuotationSaveReqVO createReqVO, Long userId);
+
+ /**
+ * 鏇存柊閿�鍞姤浠峰崟
+ *
+ * @param updateReqVO 鏇存柊淇℃伅
+ */
+ void updateSaleQuotation(CrmSaleQuotationSaveReqVO updateReqVO);
+
+ /**
+ * 鍒犻櫎閿�鍞姤浠峰崟
+ *
+ * @param ids 缂栧彿鍒楄〃
+ */
+ void deleteSaleQuotation(List<Long> ids);
+
+ /**
+ * 鑾峰緱閿�鍞姤浠峰崟
+ *
+ * @param id 缂栧彿
+ * @return 閿�鍞姤浠峰崟
+ */
+ CrmSaleQuotationDO getSaleQuotation(Long id);
+
+ /**
+ * 鑾峰緱閿�鍞姤浠峰崟鍒嗛〉
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 閿�鍞姤浠峰崟鍒嗛〉
+ */
+ PageResult<CrmSaleQuotationDO> getSaleQuotationPage(CrmSaleQuotationPageReqVO pageReqVO, Long userId);
+
+ /**
+ * 鑾峰緱閿�鍞姤浠峰崟鍒嗛〉锛堝熀浜庡鎴凤級
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 閿�鍞姤浠峰崟鍒嗛〉
+ */
+ PageResult<CrmSaleQuotationDO> getSaleQuotationPageByCustomerId(CrmSaleQuotationPageReqVO pageReqVO);
+
+ /**
+ * 鑾峰緱閿�鍞姤浠峰崟鍒嗛〉锛堝熀浜庡晢鏈猴級
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 閿�鍞姤浠峰崟鍒嗛〉
+ */
+ PageResult<CrmSaleQuotationDO> getSaleQuotationPageByBusinessId(CrmSaleQuotationPageReqVO pageReqVO);
+
+ /**
+ * 鑾峰緱閿�鍞姤浠峰崟浜у搧鏄庣粏鍒楄〃
+ *
+ * @param quotationId 鎶ヤ环鍗曠紪鍙�
+ * @return 浜у搧鏄庣粏鍒楄〃
+ */
+ List<CrmSaleQuotationProductDO> getSaleQuotationProductListByQuotationId(Long quotationId);
+
+ /**
+ * 鑾峰緱閿�鍞姤浠峰崟浜у搧鏄庣粏鍒楄〃
+ *
+ * @param quotationIds 鎶ヤ环鍗曠紪鍙峰垪琛�
+ * @return 浜у搧鏄庣粏鍒楄〃
+ */
+ List<CrmSaleQuotationProductDO> getSaleQuotationProductListByQuotationIds(Collection<Long> quotationIds);
+
+ /**
+ * 鎻愪氦閿�鍞姤浠峰崟瀹℃壒
+ *
+ * @param id 鎶ヤ环鍗曠紪鍙�
+ * @param processDefinitionKey 娴佺▼瀹氫箟 Key
+ * @param userId 鐢ㄦ埛缂栧彿
+ */
+ void submitSaleQuotation(Long id, String processDefinitionKey, Long userId);
+
+ /**
+ * 鏇存柊閿�鍞姤浠峰崟瀹℃壒鐘舵��
+ *
+ * @param id 鎶ヤ环鍗曠紪鍙�
+ * @param bpmResult BPM 瀹℃壒缁撴灉
+ */
+ void updateSaleQuotationAuditStatus(Long id, Integer bpmResult);
+
+ /**
+ * 鑾峰彇閿�鍞姤浠峰鎵规祦绋嬪垪琛�
+ *
+ * @return 娴佺▼瀹氫箟鍒楄〃
+ */
+ List<Map<String, Object>> getSaleQuotationApproveProcessDefinitionList();
+
+ /**
+ * 杞负鍚堝悓
+ *
+ * @param id 鎶ヤ环鍗曠紪鍙�
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 鍚堝悓缂栧彿
+ */
+ Long convertToContract(Long id, Long userId);
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
new file mode 100644
index 0000000..da476ff
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
@@ -0,0 +1,446 @@
+package cn.iocoder.yudao.module.crm.service.quotation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationProductDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper;
+import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationProductMapper;
+import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
+import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
+import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
+import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+/**
+ * CRM 閿�鍞姤浠峰崟 Service 瀹炵幇绫�
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Service
+@Validated
+@Slf4j
+public class CrmSaleQuotationServiceImpl implements CrmSaleQuotationService {
+
+ /**
+ * 閿�鍞姤浠� BPM 鍒嗙被缂栫爜
+ */
+ private static final String SALE_QUOTATION_APPROVE_CATEGORY_CODE = "sale_quotation_approve";
+
+ @Resource
+ private CrmSaleQuotationMapper saleQuotationMapper;
+ @Resource
+ private CrmSaleQuotationProductMapper saleQuotationProductMapper;
+
+ @Resource
+ private CrmNoRedisDAO noRedisDAO;
+
+ @Resource
+ private MdmItemApi mdmItemApi;
+
+ @Resource
+ private CrmCustomerService customerService;
+ @Resource
+ private CrmBusinessService businessService;
+ @Resource
+ private CrmContactService contactService;
+ @Resource
+ private CrmPermissionService permissionService;
+ @Resource
+ private CrmContractService contractService;
+
+ @Resource
+ private BpmCategoryService bpmCategoryService;
+ @Resource
+ private BpmProcessDefinitionService bpmProcessDefinitionService;
+ @Resource
+ private BpmProcessInstanceApi bpmProcessInstanceApi;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createSaleQuotation(CrmSaleQuotationSaveReqVO createReqVO, Long userId) {
+ // 1.1 鏍¢獙鐗╂枡椤圭殑鏈夋晥鎬�
+ List<CrmSaleQuotationProductDO> quotationProducts = validateQuotationItems(createReqVO.getItems());
+ // 1.2 鐢熸垚鎶ヤ环鍗曞彿锛屽苟鏍¢獙鍞竴鎬�
+ String no = noRedisDAO.generate(CrmNoRedisDAO.SALE_QUOTATION_NO_PREFIX);
+ if (saleQuotationMapper.selectByNo(no) != null) {
+ throw exception(SALE_QUOTATION_NO_EXISTS);
+ }
+ // 1.3 鏍¢獙鍏宠仈瀛楁
+ validateRelationDataExists(createReqVO);
+
+ // 2.1 鎻掑叆鎶ヤ环鍗�
+ CrmSaleQuotationDO saleQuotation = BeanUtils.toBean(createReqVO, CrmSaleQuotationDO.class, q -> q
+ .setNo(no)
+ .setStatus(CrmSaleQuotationStatusEnum.DRAFT.getStatus()));
+ calculateTotalPrice(saleQuotation, quotationProducts);
+ saleQuotationMapper.insert(saleQuotation);
+ // 2.2 鎻掑叆鎶ヤ环鏄庣粏
+ quotationProducts.forEach(p -> p.setQuotationId(saleQuotation.getId()));
+ saleQuotationProductMapper.insertBatch(quotationProducts);
+ // 2.3 鍒涘缓鏁版嵁鏉冮檺
+ permissionService.createPermission(saleQuotation.getId(), userId);
+ return saleQuotation.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateSaleQuotation(CrmSaleQuotationSaveReqVO updateReqVO) {
+ // 1.1 鏍¢獙瀛樺湪
+ CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(updateReqVO.getId());
+ // 1.2 鏍¢獙鐘舵�侊紝鍙湁鑽夌鐘舵�佹墠鑳戒慨鏀�
+ if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus())) {
+ throw exception(SALE_QUOTATION_UPDATE_FAIL_NOT_DRAFT, saleQuotation.getNo());
+ }
+ // 1.3 鏍¢獙鐗╂枡椤圭殑鏈夋晥鎬�
+ List<CrmSaleQuotationProductDO> quotationProducts = validateQuotationItems(updateReqVO.getItems());
+ // 1.4 鏍¢獙鍏宠仈瀛楁
+ validateRelationDataExists(updateReqVO);
+
+ // 2.1 鏇存柊鎶ヤ环鍗�
+ CrmSaleQuotationDO updateObj = BeanUtils.toBean(updateReqVO, CrmSaleQuotationDO.class);
+ calculateTotalPrice(updateObj, quotationProducts);
+ saleQuotationMapper.updateById(updateObj);
+ // 2.2 鏇存柊鎶ヤ环鏄庣粏
+ updateQuotationProductList(updateReqVO.getId(), quotationProducts);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteSaleQuotation(List<Long> ids) {
+ // 1. 鏍¢獙涓嶅浜庨潪鑽夌鐘舵��
+ List<CrmSaleQuotationDO> saleQuotations = saleQuotationMapper.selectByIds(ids);
+ if (CollUtil.isEmpty(saleQuotations)) {
+ return;
+ }
+ saleQuotations.forEach(saleQuotation -> {
+ if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus())) {
+ throw exception(SALE_QUOTATION_DELETE_FAIL_NOT_DRAFT, saleQuotation.getNo());
+ }
+ });
+
+ // 2. 閬嶅巻鍒犻櫎
+ saleQuotations.forEach(saleQuotation -> {
+ // 2.1 鍒犻櫎鎶ヤ环鍗�
+ saleQuotationMapper.deleteById(saleQuotation.getId());
+ // 2.2 鍒犻櫎鎶ヤ环鏄庣粏
+ saleQuotationProductMapper.deleteByQuotationId(saleQuotation.getId());
+ // 2.3 鍒犻櫎鏁版嵁鏉冮檺
+ permissionService.deletePermission(saleQuotation.getId());
+ });
+ }
+
+ private CrmSaleQuotationDO validateSaleQuotationExists(Long id) {
+ CrmSaleQuotationDO saleQuotation = saleQuotationMapper.selectById(id);
+ if (saleQuotation == null) {
+ throw exception(SALE_QUOTATION_NOT_EXISTS);
+ }
+ return saleQuotation;
+ }
+
+ private List<CrmSaleQuotationProductDO> validateQuotationItems(List<CrmSaleQuotationSaveReqVO.Item> list) {
+ // 1. 鏍¢獙鐗╂枡瀛樺湪涓斿惎鐢�
+ List<Long> itemIds = convertSet(list, CrmSaleQuotationSaveReqVO.Item::getItemId);
+ Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds);
+ // 2. 杞崲涓� CrmSaleQuotationProductDO 鍒楄〃
+ return convertList(list, o -> BeanUtils.toBean(o, CrmSaleQuotationProductDO.class, item -> {
+ MdmItemRespDTO mdmItem = itemMap.get(item.getItemId());
+ if (mdmItem == null) {
+ throw exception(SALE_QUOTATION_ITEM_NOT_EXISTS);
+ }
+ item.setItemUnitId(mdmItem.getUnitMeasureId());
+ item.setItemPrice(mdmItem.getSalesPrice());
+ if (item.getQuotationPrice() != null && item.getCount() != null) {
+ item.setTotalPrice(MoneyUtils.priceMultiply(item.getQuotationPrice(), item.getCount()));
+ if (item.getTaxPercent() != null) {
+ item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+ }
+ }
+ }));
+ }
+
+ private void validateRelationDataExists(CrmSaleQuotationSaveReqVO reqVO) {
+ // 1. 鏍¢獙瀹㈡埛
+ customerService.validateCustomer(reqVO.getCustomerId());
+ // 2. 鏍¢獙鍟嗘満锛堝彲閫夛級
+ if (reqVO.getBusinessId() != null) {
+ businessService.validateBusiness(reqVO.getBusinessId());
+ }
+ // 3. 鏍¢獙鑱旂郴浜猴紙鍙�夛級
+ if (reqVO.getContactId() != null) {
+ contactService.validateContact(reqVO.getContactId());
+ }
+ }
+
+ private void updateQuotationProductList(Long id, List<CrmSaleQuotationProductDO> newList) {
+ // 绗竴姝ワ紝瀵规瘮鏂拌�佹暟鎹紝鑾峰緱娣诲姞銆佷慨鏀广�佸垹闄ょ殑鍒楄〃
+ List<CrmSaleQuotationProductDO> oldList = saleQuotationProductMapper.selectListByQuotationId(id);
+ List<List<CrmSaleQuotationProductDO>> diffList = diffList(oldList, newList,
+ (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+ // 绗簩姝ワ紝鎵归噺娣诲姞銆佷慨鏀广�佸垹闄�
+ if (CollUtil.isNotEmpty(diffList.get(0))) {
+ diffList.get(0).forEach(o -> o.setQuotationId(id));
+ saleQuotationProductMapper.insertBatch(diffList.get(0));
+ }
+ if (CollUtil.isNotEmpty(diffList.get(1))) {
+ saleQuotationProductMapper.updateBatch(diffList.get(1));
+ }
+ if (CollUtil.isNotEmpty(diffList.get(2))) {
+ saleQuotationProductMapper.deleteByIds(convertList(diffList.get(2), CrmSaleQuotationProductDO::getId));
+ }
+ }
+
+ private void calculateTotalPrice(CrmSaleQuotationDO saleQuotation, List<CrmSaleQuotationProductDO> items) {
+ saleQuotation.setTotalProductPrice(getSumValue(items, CrmSaleQuotationProductDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+ saleQuotation.setTotalTaxPrice(getSumValue(items, CrmSaleQuotationProductDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+ // 璁$畻浼樻儬浠锋牸
+ if (saleQuotation.getDiscountPercent() == null) {
+ saleQuotation.setDiscountPercent(BigDecimal.ZERO);
+ }
+ BigDecimal basePrice = saleQuotation.getTotalProductPrice().add(saleQuotation.getTotalTaxPrice());
+ saleQuotation.setDiscountPrice(MoneyUtils.priceMultiplyPercent(basePrice, saleQuotation.getDiscountPercent()));
+ saleQuotation.setTotalPrice(basePrice.subtract(saleQuotation.getDiscountPrice()));
+ }
+
+ @Override
+ public CrmSaleQuotationDO getSaleQuotation(Long id) {
+ return saleQuotationMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult<CrmSaleQuotationDO> getSaleQuotationPage(CrmSaleQuotationPageReqVO pageReqVO, Long userId) {
+ return saleQuotationMapper.selectPage(pageReqVO, userId);
+ }
+
+ @Override
+ public PageResult<CrmSaleQuotationDO> getSaleQuotationPageByCustomerId(CrmSaleQuotationPageReqVO pageReqVO) {
+ return saleQuotationMapper.selectPageByCustomerId(pageReqVO);
+ }
+
+ @Override
+ public PageResult<CrmSaleQuotationDO> getSaleQuotationPageByBusinessId(CrmSaleQuotationPageReqVO pageReqVO) {
+ return saleQuotationMapper.selectPageByBusinessId(pageReqVO);
+ }
+
+ @Override
+ public List<CrmSaleQuotationProductDO> getSaleQuotationProductListByQuotationId(Long quotationId) {
+ return saleQuotationProductMapper.selectListByQuotationId(quotationId);
+ }
+
+ @Override
+ public List<CrmSaleQuotationProductDO> getSaleQuotationProductListByQuotationIds(Collection<Long> quotationIds) {
+ if (CollUtil.isEmpty(quotationIds)) {
+ return Collections.emptyList();
+ }
+ return saleQuotationProductMapper.selectListByQuotationIds(quotationIds);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void submitSaleQuotation(Long id, String processDefinitionKey, Long userId) {
+ // 1. 鏍¢獙鎶ヤ环鍗曞瓨鍦ㄤ笖涓鸿崏绋跨姸鎬�
+ CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id);
+ if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus())) {
+ throw exception(SALE_QUOTATION_SUBMIT_FAIL_NOT_DRAFT);
+ }
+
+ // 2. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+ validateSaleQuotationApproveCategoryAndProcessDefinition();
+
+ // 3. 鏍¢獙浼犲叆鐨勬祦绋嬪畾涔� Key 鏄惁灞炰簬璇ュ垎绫讳笖澶勪簬婵�娲荤姸鎬�
+ ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
+ if (processDefinition == null) {
+ throw exception(SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+ }
+
+ // 4. 鍒涘缓娴佺▼瀹炰緥
+ String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId,
+ new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey(processDefinitionKey)
+ .setBusinessKey(String.valueOf(id)));
+
+ // 5. 鏇存柊鎶ヤ环鍗曠姸鎬�
+ saleQuotationMapper.updateById(new CrmSaleQuotationDO()
+ .setId(id)
+ .setProcessInstanceId(processInstanceId)
+ .setStatus(CrmSaleQuotationStatusEnum.PROCESS.getStatus()));
+ }
+
+ /**
+ * 鏍¢獙 BPM 閿�鍞姤浠峰鎵瑰垎绫诲拰娴佺▼瀹氫箟鏄惁瀛樺湪
+ */
+ private void validateSaleQuotationApproveCategoryAndProcessDefinition() {
+ // 1. 鏍¢獙鍒嗙被鏄惁瀛樺湪
+ List<String> codes = Collections.singletonList(SALE_QUOTATION_APPROVE_CATEGORY_CODE);
+ Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
+ if (!categoryMap.containsKey(SALE_QUOTATION_APPROVE_CATEGORY_CODE)) {
+ throw exception(SALE_QUOTATION_BPM_CATEGORY_NOT_EXISTS);
+ }
+
+ // 2. 鏍¢獙鍒嗙被涓嬫槸鍚︽湁鍙敤鐨勬祦绋嬪畾涔�
+ List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
+ .getProcessDefinitionInfoListByCategory(SALE_QUOTATION_APPROVE_CATEGORY_CODE);
+ if (CollUtil.isEmpty(definitionInfoList)) {
+ throw exception(SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+ }
+
+ // 3. 鏍¢獙鏄惁鏈夋縺娲荤姸鎬佺殑娴佺▼瀹氫箟
+ Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+ List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
+ boolean hasActiveDefinition = processDefinitions.stream()
+ .anyMatch(pd -> !pd.isSuspended());
+ if (!hasActiveDefinition) {
+ throw exception(SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+ }
+ }
+
+ @Override
+ public void updateSaleQuotationAuditStatus(Long id, Integer bpmResult) {
+ // 1. 鏍¢獙鎶ヤ环鍗曞瓨鍦ㄤ笖涓哄鎵逛腑鐘舵��
+ CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id);
+ if (!CrmSaleQuotationStatusEnum.PROCESS.getStatus().equals(saleQuotation.getStatus())) {
+ log.error("[updateSaleQuotationAuditStatus] 鎶ヤ环鍗�({}) 涓嶅浜庡鎵逛腑", id);
+ throw exception(SALE_QUOTATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
+ }
+
+ // 2. 杞崲瀹℃壒缁撴灉
+ Integer auditStatus = convertBpmResultToAuditStatus(bpmResult);
+ saleQuotationMapper.updateById(new CrmSaleQuotationDO()
+ .setId(id)
+ .setStatus(auditStatus));
+ }
+
+ /**
+ * BPM 瀹℃壒缁撴灉杞崲
+ */
+ private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
+ Integer auditStatus = BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)
+ ? CrmSaleQuotationStatusEnum.APPROVE.getStatus()
+ : BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)
+ ? CrmSaleQuotationStatusEnum.REJECT.getStatus()
+ : BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)
+ ? CrmSaleQuotationStatusEnum.CANCEL.getStatus() : null;
+ Assert.notNull(auditStatus, "BPM 瀹℃壒缁撴灉({}) 杞崲澶辫触", bpmResult);
+ return auditStatus;
+ }
+
+ @Override
+ public List<Map<String, Object>> getSaleQuotationApproveProcessDefinitionList() {
+ // 1. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+ validateSaleQuotationApproveCategoryAndProcessDefinition();
+
+ // 2. 鑾峰彇鍒嗙被涓嬬殑娴佺▼瀹氫箟淇℃伅
+ List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
+ .getProcessDefinitionInfoListByCategory(SALE_QUOTATION_APPROVE_CATEGORY_CODE);
+
+ // 3. 鑾峰彇娴佺▼瀹氫箟璇︽儏
+ Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+ List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
+
+ // 4. 杩囨护锛氬彧淇濈暀婵�娲荤姸鎬侊紝涓旂浉鍚� key 鍙繚鐣欐渶鏂扮増鏈�
+ Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
+ for (ProcessDefinition pd : processDefinitions) {
+ if (pd.isSuspended()) {
+ continue;
+ }
+ ProcessDefinition existing = latestVersionMap.get(pd.getKey());
+ if (existing == null || pd.getVersion() > existing.getVersion()) {
+ latestVersionMap.put(pd.getKey(), pd);
+ }
+ }
+
+ // 5. 杩斿洖娴佺▼瀹氫箟鍒楄〃
+ List<Map<String, Object>> result = new ArrayList<>();
+ for (ProcessDefinition pd : latestVersionMap.values()) {
+ Map<String, Object> item = new HashMap<>();
+ item.put("id", pd.getId());
+ item.put("key", pd.getKey());
+ item.put("name", pd.getName());
+ item.put("version", pd.getVersion());
+ result.add(item);
+ }
+ return result;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long convertToContract(Long id, Long userId) {
+ // 1. 鏍¢獙鎶ヤ环鍗曞瓨鍦ㄤ笖宸插鎵归�氳繃
+ CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id);
+ if (!CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus())) {
+ throw exception(SALE_QUOTATION_NOT_APPROVE);
+ }
+ // 鏍¢獙鏄惁宸茶浆鍚堝悓
+ if (saleQuotation.getContractId() != null) {
+ throw exception(SALE_QUOTATION_CONTRACT_EXISTS);
+ }
+
+ // 2. 鑾峰彇鎶ヤ环鏄庣粏
+ List<CrmSaleQuotationProductDO> quotationProducts = saleQuotationProductMapper.selectListByQuotationId(id);
+
+ // 3. 鏋勫缓鍚堝悓鍒涘缓 VO
+ CrmContractSaveReqVO contractVO = new CrmContractSaveReqVO();
+ contractVO.setName(saleQuotation.getName() + "-鍚堝悓");
+ contractVO.setCustomerId(saleQuotation.getCustomerId());
+ contractVO.setBusinessId(saleQuotation.getBusinessId());
+ contractVO.setOwnerUserId(saleQuotation.getOwnerUserId());
+ contractVO.setOrderDate(saleQuotation.getQuotationTime());
+ contractVO.setDiscountPercent(saleQuotation.getDiscountPercent());
+ contractVO.setRemark("鐢辨姤浠峰崟[" + saleQuotation.getNo() + "]鐢熸垚");
+ // 杞崲鏄庣粏椤癸細鎶ヤ环鍗曠墿鏂� -> 鍚堝悓浜у搧
+ List<CrmContractSaveReqVO.Product> contractProducts = convertList(quotationProducts, qp -> {
+ CrmContractSaveReqVO.Product product = new CrmContractSaveReqVO.Product();
+ product.setItemId(qp.getItemId());
+ product.setItemPrice(qp.getItemPrice());
+ product.setContractPrice(qp.getQuotationPrice());
+ product.setCount(qp.getCount().intValue());
+ return product;
+ });
+ contractVO.setProducts(contractProducts);
+
+ // 4. 鍒涘缓鍚堝悓
+ Long contractId = contractService.createContract(contractVO, userId);
+
+ // 5. 鏇存柊鎶ヤ环鍗曞叧鑱斾俊鎭�
+ CrmContractDO contract = contractService.getContract(contractId);
+ saleQuotationMapper.updateById(new CrmSaleQuotationDO()
+ .setId(id)
+ .setContractId(contractId)
+ .setContractNo(contract.getNo())
+ .setStatus(CrmSaleQuotationStatusEnum.CONVERTED.getStatus()));
+
+ return contractId;
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/listener/CrmSaleQuotationStatusListener.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/listener/CrmSaleQuotationStatusListener.java
new file mode 100644
index 0000000..ee11882
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/listener/CrmSaleQuotationStatusListener.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.crm.service.quotation.listener;
+
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper;
+import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum;
+import cn.iocoder.yudao.module.crm.service.quotation.CrmSaleQuotationService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 閿�鍞姤浠峰鎵圭殑缁撴灉鐩戝惉鍣ㄥ疄鐜扮被
+ *
+ * 閫氳繃 businessKey锛堟姤浠峰崟ID锛夊尮閰嶏紝鑰屼笉鏄浐瀹氱殑娴佺▼瀹氫箟 Key锛�
+ * 杩欐牱鍙互鏀寔鍒嗙被涓嬩换鎰� Key 鐨勬祦绋嬫ā鍨嬨��
+ *
+ * @author 鑺嬮亾婧愮爜
+ */
+@Component
+@Slf4j
+public class CrmSaleQuotationStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
+
+ @Resource
+ private CrmSaleQuotationService saleQuotationService;
+ @Resource
+ private CrmSaleQuotationMapper saleQuotationMapper;
+
+ @Override
+ public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
+ // 閫氳繃 businessKey 鏌ヨ鎶ヤ环鍗�
+ Long quotationId;
+ try {
+ quotationId = Long.parseLong(event.getBusinessKey());
+ } catch (NumberFormatException e) {
+ return; // businessKey 涓嶆槸鏁板瓧锛屽拷鐣�
+ }
+
+ CrmSaleQuotationDO saleQuotation = saleQuotationMapper.selectById(quotationId);
+ if (saleQuotation == null) {
+ return; // 涓嶆槸閿�鍞姤浠凤紝蹇界暐
+ }
+
+ // 鍙湁瀹℃壒涓姸鎬佺殑鎶ヤ环鍗曟墠澶勭悊
+ if (!CrmSaleQuotationStatusEnum.PROCESS.getStatus().equals(saleQuotation.getStatus())) {
+ log.warn("[onApplicationEvent] 閿�鍞姤浠峰崟({}) 涓嶅浜庡鎵逛腑鐘舵�侊紝褰撳墠鐘舵��: {}", quotationId, saleQuotation.getStatus());
+ return;
+ }
+
+ log.info("[onApplicationEvent] 閿�鍞姤浠峰崟({}) 瀹℃壒鐘舵�佹洿鏂�: {}", quotationId, event.getStatus());
+ // 鏇存柊瀹℃壒鐘舵��
+ saleQuotationService.updateSaleQuotationAuditStatus(quotationId, event.getStatus());
+ }
+
+}
\ No newline at end of file
--
Gitblit v1.9.3