From 9fceeaad9af96bc58ba714560814c6e94f8af406 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 10 八月 2026 17:13:01 +0800
Subject: [PATCH] feat(crm): 增强合同开票与回款功能
---
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java | 6
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java | 16
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java | 96 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java | 1
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeContractDO.java | 141 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java | 8
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java | 234 ++++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java | 21
docs/hrm_employee_contract_integration.md | 182 +++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java | 6
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceSaveReqVO.java | 45
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java | 119 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmInvoiceParseFunction.java | 44
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java | 30
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractTerminateReqVO.java | 28
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java | 17
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java | 51
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseInvoiceStatusListener.java | 43
docs/purchase_invoice_payment_link.md | 155 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java | 7
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java | 11
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java | 5
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java | 2
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java | 6
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java | 1
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java | 6
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractRespVO.java | 94 +
docs/receivable_invoice_link.md | 119 ++
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java | 15
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java | 7
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractService.java | 68 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java | 248 ++++
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java | 84 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java | 5
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java | 30
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java | 8
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractPageReqVO.java | 68 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java | 82 +
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeContractMapper.java | 190 +++
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractSaveReqVO.java | 73 +
sql/mysql/hrm_employee_contract_init.sql | 100 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java | 7
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractServiceImpl.java | 321 +++++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java | 94 +
docs/contract_invoice_amount.md | 62 +
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java | 5
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java | 5
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java | 4
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java | 10
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java | 34
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java | 74 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java | 39
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java | 55 +
53 files changed, 3,139 insertions(+), 43 deletions(-)
diff --git a/docs/contract_invoice_amount.md b/docs/contract_invoice_amount.md
new file mode 100644
index 0000000..8ac6c03
--- /dev/null
+++ b/docs/contract_invoice_amount.md
@@ -0,0 +1,62 @@
+# 鍚堝悓绠$悊鏂板"宸插紑绁ㄩ噾棰�/鏈紑绁ㄩ噾棰�"瀛楁 - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+- 鍚堝悓绠$悊鍒楄〃椤�
+- 鍚堝悓璇︽儏椤�
+- 鍒涘缓寮�绁� / 鍒涘缓鍥炴鏃堕�夋嫨鍚堝悓鐨勪笅鎷夛紙simple-list锛�
+
+## API
+
+鏈**涓嶆敼鍔ㄤ换浣曟帴鍙h矾寰勩�佹柟娉曘�佽姹傚弬鏁�**锛屼粎鍚堝悓鐩稿叧鎺ュ彛鐨�**鍝嶅簲缁撴瀯鏂板涓や釜瀛楁**銆�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| GET | /crm/contract/page | 鍚堝悓鍒嗛〉鍒楄〃锛堟柊澧炲瓧娈碉級 |
+| GET | /crm/contract/get | 鍚堝悓璇︽儏锛堟柊澧炲瓧娈碉級 |
+| GET | /crm/contract/page-by-customer | 鎸夊鎴峰垎椤碉紙鏂板瀛楁锛� |
+| GET | /crm/contract/page-by-business | 鎸夊晢鏈哄垎椤碉紙鏂板瀛楁锛� |
+| GET | /crm/contract/simple-list | 鍚堝悓涓嬫媺锛堟柊澧炲瓧娈碉級 |
+
+**鍝嶅簲瀛楁鏂板锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| totalInvoicePrice | BigDecimal | 宸插紑绁ㄩ噾棰濓紙鍏冿級锛岀粺璁�**瀹℃壒閫氳繃 + 瀹℃壒涓�**鐨勫紑绁ㄥ崟閲戦鍚堣 |
+| unInvoicePrice | BigDecimal | 鏈紑绁ㄩ噾棰濓紙鍏冿級锛岀瓑浜� `totalPrice - totalInvoicePrice`锛屼粎鍒楄〃/璇︽儏鎺ュ彛杩斿洖锛宻imple-list 涓嶈繑鍥� |
+
+**鍝嶅簲绀轰緥锛堝垪琛�/璇︽儏锛夛細**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "totalPrice": 1000000.00,
+ "totalReceivablePrice": 400000.00,
+ "totalInvoicePrice": 1000000.00,
+ "unInvoicePrice": 0.00
+ }
+}
+```
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| 宸插紑绁ㄩ噾棰濓紙totalInvoicePrice锛� | 鍒楄〃琛ㄦ牸鍒椼�佽鎯呴〉閲戦鍖哄煙 | 鍚鎵逛腑鐨勫紑绁ㄥ崟閲戦锛屼笌鍚庣闃茶秴鏍¢獙鍙e緞涓�鑷� |
+| 鏈紑绁ㄩ噾棰濓紙unInvoicePrice锛� | 鍒楄〃琛ㄦ牸鍒椼�佽鎯呴〉閲戦鍖哄煙 | 鍚堝悓閲戦 - 宸插紑绁ㄩ噾棰濓紝鍙悓鏃剁敤浜庡睍绀�"鍓╀綑鍙紑绁ㄩ噾棰�" |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 瀹℃壒涓殑寮�绁ㄥ崟 | 璁″叆"宸插紑绁ㄩ噾棰�"锛屽崰鐢ㄥ悎鍚岄搴︼紙涓庡悗绔彁浜ゅ紑绁ㄦ椂鐨勬牎楠屼竴鑷达紝鍓嶇灞曠ず鐨勫墿浣欓搴︽墠鍑嗙‘锛� |
+| 瀹℃壒琚┏鍥� / 鍙栨秷鐨勫紑绁ㄥ崟 | 涓嶈鍏�"宸插紑绁ㄩ噾棰�"锛岄搴﹁嚜鍔ㄩ噴鏀� |
+| 鏈紑绁ㄩ噾棰� | 鑻ヤ负璐熸暟璇存槑宸茶秴寮�锛堟甯告祦绋嬪悗绔凡鎷︽埅锛屼笉浼氬嚭鐜帮級 |
+| 鍚堝悓涓嬫媺锛坰imple-list锛� | 杩斿洖 totalInvoicePrice锛屽彲鐢ㄤ簬鍦ㄥ垱寤哄紑绁�/鍥炴鏃舵彁绀哄墿浣欏彲寮�閲戦 |
+
+## 娉ㄦ剰浜嬮」
+
+- 瀛楁璇箟鏄�"鍚鎵逛腑"锛屼笉鏄�"瀹為檯宸插紑鍑虹エ"閲戦锛屽墠绔睍绀烘椂鏂囨寤鸿鐢�"宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛�"閬垮厤姝т箟銆�
+- 鏂板瀛楁浠呯敤浜庡睍绀猴紝鍓嶇**涓嶈**鎻愪氦杩欎袱涓瓧娈碉紙鍚庣涓嶆帴鏀讹紝灞炲彧璇绘眹鎬诲瓧娈碉級銆�
+- Excel 瀵煎嚭鏈鏈柊澧炶繖涓ゅ垪锛屽闇�瀵煎嚭鍙彟琛岀敵璇枫��
diff --git a/docs/hrm_employee_contract_integration.md b/docs/hrm_employee_contract_integration.md
new file mode 100644
index 0000000..3820072
--- /dev/null
+++ b/docs/hrm_employee_contract_integration.md
@@ -0,0 +1,182 @@
+# 鍛樺伐鍚堝悓绠$悊 - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+- 鏂板銆屽憳宸ュ悎鍚屻�嶇鐞嗛〉锛歚views/hrm/employee/contract/index.vue`
+- 鏂板銆屽憳宸ュ悎鍚屻�嶈彍鍗曪紙宸查厤缃悗绔彍鍗曪紝鎸傝浇鍦� HRM 鈫� 浜哄姏璧勬簮 涓嬨�佷笌銆屽憳宸ョ鐞嗐�嶅钩绾э紝component = `hrm/employee/contract/index`锛涗笉瑕佹寕鍒般�屽憳宸ョ鐞嗐�嶄笅锛屽惁鍒欒鑿滃崟浼氬彉鎴愮洰褰曞鑷村憳宸ュ垪琛ㄩ〉鏃犳硶鐩磋揪锛�
+- 閫夋嫨鍛樺伐涓嬫媺锛氬鐢ㄧ幇鏈夊憳宸ョ簿绠�鍒楄〃鎺ュ彛
+- 鍛樺伐璇︽儏椤碉細鏂板銆屽憳宸ュ悎鍚屻�峊ab锛屽睍绀鸿鍛樺伐绛捐鐨勫叏閮ㄥ悎鍚岋紙澶嶇敤 `/hrm/employee-contract/page`锛屾寜 `employeeId` 杩囨护锛�
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+1. 鍒涘缓鍚堝悓锛氬憳宸ヤ粠 `/hrm/employee/simple-list` 涓嬫媺閫夋嫨 鈫� 鑷姩甯﹀叆鍛樺伐锛堟棤闇�鍐嶉�夐儴闂�/宀椾綅锛�
+2. 缁鍚堝悓锛氬悎鍚屽垪琛ㄦ搷浣滃垪銆岀画绛俱�嶆寜閽� 鈫� 鑷姩甯﹀叆璇ュ憳宸� + 涓婁唤鍚堝悓鐨勭被鍨�/鏈熼檺/绛剧害涓讳綋/宸ヨ祫锛屾棩鏈熼渶閲嶆柊濉啓锛涙彁浜ゆ椂鏄惧紡浼� `parentId` 鎸囧悜琚画绛惧悎鍚�
+3. 鍚堝悓鍒楄〃锛氭寜鍚堝悓缂栧彿銆佸憳宸ャ�佸悎鍚岀被鍨嬨�佹湡闄愮被鍨嬨�佸悎鍚岀姸鎬併�佸埌鏈熺被鍨嬬瓫閫�
+3. 瑙i櫎/缁堟锛氫粎瀵�"寰呯敓鏁�/鐢熸晥涓�/鍗冲皢鍒版湡"鐨勫悎鍚屽彲鎿嶄綔锛屽脊鍑虹‘璁ゆ濉啓瑙i櫎鏃ユ湡涓庡師鍥�
+4. 鍒版湡鎻愰啋锛氬垪琛ㄦ敮鎸併�屽嵆灏嗗埌鏈� / 宸插埌鏈熴�嶅揩鎹风瓫閫夛紱HRM 棣栭〉鍙皟鐢� `expiring-count` 灞曠ず鍗冲皢鍒版湡鍚堝悓鏁伴噺
+
+## API
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /hrm/employee-contract/create | 鏂板鍚堝悓 |
+| PUT | /hrm/employee-contract/update | 淇敼鍚堝悓 |
+| PUT | /hrm/employee-contract/terminate | 瑙i櫎/缁堟鍚堝悓 |
+| DELETE | /hrm/employee-contract/delete | 鍒犻櫎鍚堝悓 |
+| GET | /hrm/employee-contract/get | 鍚堝悓璇︽儏锛堝惈闄勪欢锛� |
+| GET | /hrm/employee-contract/page | 鍚堝悓鍒嗛〉 |
+| GET | /hrm/employee-contract/expiring-count | 鍗冲皢鍒版湡鍚堝悓鏁伴噺锛堜緵寰呭姙/棣栭〉鎻愰啋锛� |
+
+### 鍒涘缓/淇敼鍚堝悓
+
+**璇锋眰鍙傛暟锛坈reate/update 鍏辩敤锛夛細**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鍚� | 鍚堝悓ID锛堜慨鏀规椂蹇呬紶锛� |
+| employeeId | Long | 鏄� | 鍛樺伐ID |
+| parentId | Long | 鍚� | 缁涓婁竴浠藉悎鍚孖D銆�**缁鎸夐挳**鐐瑰嚮鏃跺墠绔細浼狅紙鎸囧悜琚画绛剧殑鍚堝悓锛夛紱鏅�氭柊澧炰笉浼狅紝鍚庣鑷姩鍏宠仈璇ュ憳宸ユ渶杩戜竴浠藉悎鍚� |
+| contractType | Integer | 鏄� | 鍚堝悓绫诲瀷锛屽瓧鍏� hrm_contract_type锛�1-鍔冲姩鍚堝悓 2-鍔冲姟鍚堝悓 3-瀹炰範鍗忚 4-鍔冲姟娲鹃仯 5-鍏朵粬 |
+| contractTermType | Integer | 鏄� | 鏈熼檺绫诲瀷锛屽瓧鍏� hrm_contract_term_type锛�1-鍥哄畾鏈熼檺 2-鏃犲浐瀹氭湡闄� 3-浠ュ畬鎴愪竴瀹氬伐浣滀换鍔′负鏈熼檺 |
+| signCompany | String | 鍚� | 绛剧害涓讳綋锛堝叕鍙稿叏绉帮級 |
+| signDate | date | 鍚� | 绛捐鏃ユ湡 |
+| startDate | date | 鏄� | 鍚堝悓寮�濮嬫棩鏈� |
+| endDate | date | 鍚� | 鍚堝悓缁撴潫鏃ユ湡锛堟棤鍥哄畾鏈熼檺鍙负绌猴級 |
+| probationStartDate | date | 鍚� | 璇曠敤鏈熷紑濮嬫棩鏈� |
+| probationEndDate | date | 鍚� | 璇曠敤鏈熺粨鏉熸棩鏈� |
+| probationSalary | BigDecimal | 鍚� | 璇曠敤鏈熷伐璧� |
+| regularSalary | BigDecimal | 鍚� | 杞宸ヨ祫 |
+| remark | String | 鍚� | 澶囨敞 |
+| blobIds | Array | 鍚� | 鍚堝悓闄勪欢 blobId 鍒楄〃 |
+
+**鏍¢獙瑙勫垯锛堝悗绔己鍒讹級锛�**
+- 鏈熼檺绫诲瀷 鈮� 鏃犲浐瀹氭湡闄�(2) 鏃讹紝endDate 蹇呭~
+- startDate 涓嶈兘鏅氫簬 endDate锛涜瘯鐢ㄦ湡寮�濮嬩笉鑳芥櫄浜庤瘯鐢ㄦ湡缁撴潫
+- 鍚堝悓缂栧彿鐢卞悗绔嚜鍔ㄧ敓鎴愶紙HT + 鏃ユ湡 + 搴忓彿锛夛紝鍓嶇鏃犻渶濉啓銆佷笉鍙慨鏀�
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": 1
+}
+```
+
+### 瑙i櫎/缁堟鍚堝悓
+
+**璇锋眰鍙傛暟锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鏄� | 鍚堝悓ID |
+| terminateStatus | Integer | 鏄� | 1-宸茶В闄� 2-宸茬粓姝� |
+| terminateDate | date | 鏄� | 瑙i櫎/缁堟鏃ユ湡 |
+| terminateReason | String | 鏄� | 瑙i櫎/缁堟鍘熷洜 |
+
+**鍝嶅簲锛�** `{ "code": 0, "data": true }`
+
+### 鍚堝悓鍒嗛〉
+
+**璇锋眰鍙傛暟锛堢瓫閫夛級锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| pageNo / pageSize | Integer | 鍒嗛〉鍙傛暟 |
+| contractNo | String | 鍚堝悓缂栧彿锛堟ā绯婂尮閰嶏級 |
+| employeeId | Long | 鍛樺伐ID |
+| contractType | Integer | 鍚堝悓绫诲瀷 |
+| contractTermType | Integer | 鏈熼檺绫诲瀷 |
+| terminateStatus | Integer | 0-姝e父 1-宸茶В闄� 2-宸茬粓姝� |
+| status | Integer | 鍚堝悓鐘舵�侊紙鍔ㄦ�佽绠楋級锛�1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝� |
+| expiryType | Integer | 鍒版湡绫诲瀷锛�1-鍗冲皢鍒版湡 2-宸插埌鏈燂紙寰呭姙鎻愰啋鐢紝涓� status 浜掓枼锛� |
+
+**鍝嶅簲瀛楁鏂板锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| contractNo | String | 鍚堝悓缂栧彿锛堣嚜鍔ㄧ敓鎴愶級 |
+| parentId | Long | 缁涓婁竴浠藉悎鍚孖D |
+| parentNo | String | 缁涓婁竴浠藉悎鍚岀紪鍙凤紙鍓嶇灞曠ず"缁鑷�"锛� |
+| isCurrent | Boolean | 鏄惁鍛樺伐褰撳墠鐢熸晥鍚堝悓锛坱rue 鏃跺墠绔墦鏍囷級 |
+| employeeName | String | 鍛樺伐濮撳悕锛堣嚜鍔ㄥ~鍏咃級 |
+| employeeNo | String | 鍛樺伐宸ュ彿锛堣嚜鍔ㄥ~鍏咃級 |
+| deptName | String | 閮ㄩ棬鍚嶇О锛堣嚜鍔ㄥ~鍏咃級 |
+| status | Integer | 鍚堝悓鐘舵�侊紙鍚庣鎸夊綋鍓嶆棩鏈熷姩鎬佽绠楋級 |
+| attachmentList | Array | 闄勪欢鍒楄〃锛堜粎璇︽儏鎺ュ彛杩斿洖锛� |
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "list": [
+ {
+ "id": 1,
+ "contractNo": "HT202608100001",
+ "employeeId": 1,
+ "employeeName": "寮犱笁",
+ "employeeNo": "EMP202401010001",
+ "deptName": "鐮斿彂閮�",
+ "contractType": 1,
+ "contractTermType": 1,
+ "startDate": "2026-01-01",
+ "endDate": "2027-01-01",
+ "status": 2,
+ "terminateStatus": 0
+ }
+ ],
+ "total": 1
+ }
+}
+```
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 鍒楄〃 | 琛ㄥ崟 | 璇︽儏 | 璇存槑 |
+|------|------|------|------|------|
+| 鍚堝悓缂栧彿 | 鉁� | 鍙鑷姩鐢熸垚 | 鉁� | HT + 鏃ユ湡 + 搴忓彿 |
+| 鍛樺伐濮撳悕 | 鉁� | 涓嬫媺閫夋嫨 | 鉁� | 澶嶇敤鍛樺伐绮剧畝鍒楄〃 |
+| 閮ㄩ棬鍚嶇О | 鉁� | 鑷姩甯﹀叆 | 鉁� | 閫夋嫨鍛樺伐鍚庡睍绀� |
+| 鍚堝悓绫诲瀷 | 鉁� | 涓嬫媺 | 鉁� | 瀛楀吀 hrm_contract_type |
+| 鏈熼檺绫诲瀷 | 鉁� | 涓嬫媺 | 鉁� | 瀛楀吀 hrm_contract_term_type |
+| 绛捐鏃ユ湡 | 鉁� | 鏃ユ湡 | 鉁� | |
+| 寮�濮�/缁撴潫鏃ユ湡 | 鉁� | 鏃ユ湡 | 鉁� | 鏃犲浐瀹氭湡闄愮粨鏉熸棩鏈熷彲绌� |
+| 璇曠敤鏈熻捣姝� | 鉁� | 鏃ユ湡 | 鉁� | |
+| 璇曠敤鏈�/杞宸ヨ祫 | 鉁� | 鏁板瓧 | 鉁� | |
+| 鍚堝悓鐘舵�� | 鉁� 鏍囩 | - | 鉁� | 鍔ㄦ�佽绠楋紝鎸夐鑹插尯鍒嗭細鐢熸晥涓�(缁�)/鍗冲皢鍒版湡(姗�)/宸插埌鏈�(鐏�)/宸茶В闄ぢ峰凡缁堟(绾�)/寰呯敓鏁�(钃�) |
+| 闄勪欢 | - | 涓婁紶 | 鉁� | 璧扮郴缁熼檮浠朵笂浼� |
+| 鎿嶄綔 | 鉁� | - | - | 缂栬緫 / 瑙i櫎/缁堟 / 鍒犻櫎 |
+
+## 闄勪欢瀵规帴娴佺▼
+
+1. 涓婁紶锛氳皟鐢� `POST /system/storage-blob/upload` 鑾峰彇 blobId
+2. 淇濆瓨鍚堝悓锛氳〃鍗� `blobIds` 瀛楁浼� blobId 鏁扮粍锛屽悗绔粦瀹�
+3. 鏌ヨ锛氳鎯呮帴鍙h繑鍥� `attachmentList`锛涘垪琛ㄩ〉濡傞渶鍗曠嫭鍒锋柊闄勪欢锛屽彲璋冪敤 `GET /system/storage-attachment/list?recordType=hrm_employee_contract&recordId=xxx`
+4. 涓氬姟琛ㄤ笉瀛樻枃浠跺湴鍧�锛岀姝㈠湪鍓嶇璋冪敤 `/infra/file/*` 鏃ф帴鍙�
+
+## 涓氬姟瑙勫垯璇存槑
+
+> **澶氬悎鍚岃璁�**锛氫竴涓憳宸ュ彲绛捐澶氭鍚堝悓锛堢画绛�/鎹㈢锛夈�傛暟鎹〃閫氳繃 `parent_id`锛堢画绛句笂涓�浠藉悎鍚孖D锛夊舰鎴愮画绛鹃摼锛岄�氳繃 `is_current`锛堟槸鍚﹀綋鍓嶇敓鏁堝悎鍚岋級鏍囪瘑鍛樺伐褰撳墠鍚堝悓锛涘悓涓�鍛樺伐浠绘剰鏃跺埢浠呬竴浠� `is_current=true` 鐨勫悎鍚屻��
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 鐘舵�佽绠� | 宸茶В闄�(5)/宸茬粓姝�(6) > 宸插埌鏈�(4) > 寰呯敓鏁�(1) > 鍗冲皢鍒版湡(3) > 鐢熸晥涓�(2)锛屽悗绔寜褰撳墠鏃ユ湡鍔ㄦ�佽绠楋紝鍓嶇鏃犻渶缁存姢 |
+| 鍗冲皢鍒版湡 | 缁撴潫鏃ユ湡鍦ㄥ綋澶╄捣鐨� 30 澶╁唴锛堝惈褰撳ぉ锛変笖鏈В闄�/缁堟 |
+| 缁鍏宠仈 | 鎿嶄綔鍒椼�岀画绛俱�嶆寜閽偣鍑绘椂鍓嶇鏄惧紡浼� `parent_id` 鎸囧悜琚画绛惧悎鍚岋紱鏅�氭柊澧炰笉浼狅紝鍚庣鑷姩鎶婅鍛樺伐鏈�杩戜竴浠藉悎鍚屼綔涓� `parent_id`锛堢画绛鹃摼锛夈�傚墠绔垪琛�/璇︽儏灞曠ず"缁鑷� xxx" |
+| 褰撳墠鍚堝悓 | 鍒涘缓鏂板悎鍚屾椂锛屽悗绔嚜鍔ㄦ竻闄よ鍛樺伐鏃у悎鍚岀殑 `is_current` 骞剁疆鏂板悎鍚屼负 true锛涜В闄�/缁堟褰撳墠鍚堝悓鏃惰嚜鍔ㄦ竻闄ゆ爣璁� |
+| 鏃堕棿閲嶅彔 | 鍚庣寮哄埗鏍¢獙锛氬悓涓�鍛樺伐涓嶈兘瀛樺湪鏃堕棿閲嶅彔鐨勬湭瑙i櫎/鏈粓姝㈠悎鍚岋紙鏃犲浐瀹氭湡闄愯涓烘棤闄愰暱锛夛紝鎶ラ敊"璇ュ憳宸ュ湪鎵�閫夋椂闂存宸插瓨鍦ㄧ敓鏁堝悎鍚�"锛岄渶鍏堣В闄�/缁堟鏃у悎鍚屾垨璋冩暣鏃ユ湡 |
+| 瑙i櫎/缁堟 | 浠呮湭瑙i櫎/鏈粓姝㈢殑鍚堝悓鍙搷浣滐紱宸茶В闄�/宸茬粓姝㈠悎鍚屼笉鍙噸澶嶆搷浣滐紱鎿嶄綔鍚庣姸鎬佸浐瀹氫负宸茶В闄�/宸茬粓姝� |
+| 缂栬緫闄愬埗 | 宸茶В闄�/宸茬粓姝㈢殑鍚堝悓寤鸿鍙灞曠ず锛屼笉鍏佽淇敼璧锋鏃ユ湡锛堝悗绔湭寮哄埗锛屽墠绔彁绀哄嵆鍙級 |
+| 鍒犻櫎 | 鐩存帴鍒犻櫎璁板綍骞剁骇鑱斿垹闄ら檮浠� |
+| 鍒版湡鎻愰啋 | 鍒楄〃銆屽嵆灏嗗埌鏈熴�嶇瓫閫� = status=3 鎴� expiryType=1锛涢椤靛彲閫氳繃 expiring-count 灞曠ず鏁伴噺 |
+| 鍛樺伐缁村害 | 鍚堝悓椤垫寜鍛樺伐绛涢�夊彲鏌ョ湅鏌愬憳宸ュ叏閮ㄥ悎鍚岋紱鍛樺伐璇︽儏椤点�屽憳宸ュ悎鍚屻�峊ab 灞曠ず璇ュ憳宸ュ叏閮ㄥ悎鍚屽苟鏍囪褰撳墠鍚堝悓 |
+
+## 娉ㄦ剰浜嬮」
+
+- 鍚堝悓缂栧彿鍞竴锛岀敱鍚庣鐢熸垚锛屽墠绔笉鑳戒慨鏀�
+- 鏂板/缂栬緫鍚堝悓鐨勬牎楠岋紙鍥哄畾鏈熼檺蹇呴』濉粨鏉熸棩鏈熴�佹棩鏈熷厛鍚庯級鐢卞悗绔繑鍥炴槑纭敊璇彁绀猴紝鍓嶇琛ㄥ崟鏍¢獙鍙悓姝ュ姞涓�
+- 鍒楄〃鐘舵�佸垪闇�鏍规嵁 status 鍊兼槧灏勫瓧鍏� hrm_contract_status 灞曠ず涓枃涓庨鑹�
+- 瑙i櫎/缁堟鎿嶄綔闇�浜屾纭锛岄伩鍏嶈鎿嶄綔
+- 鍒嗛〉鏌ヨ闄勪欢鍒楄〃涓虹┖鏃朵笉杩斿洖璇ュ瓧娈碉紝鍓嶇闇�鍋氱┖鍊煎吋瀹�
diff --git a/docs/purchase_invoice_payment_link.md b/docs/purchase_invoice_payment_link.md
new file mode 100644
index 0000000..fa6e939
--- /dev/null
+++ b/docs/purchase_invoice_payment_link.md
@@ -0,0 +1,155 @@
+# 閲囪喘鏉ョエ + 浠樻鍏宠仈鏉ョエ - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+- 閲囪喘鏉ョエ绠$悊椤碉紙**鏂板缓椤甸潰**锛屽弬鑰冮噰璐敵璇�/閲囪喘璁㈠崟椤甸潰缁撴瀯锛�
+- 閲囪喘璁㈠崟璇︽儏椤碉紙灞曠ず璇ヨ鍗曚笅宸插叧鑱旂殑鏉ョエ淇℃伅锛�
+- 浠樻鍗曠鐞嗛〉锛堟柊寤�/缂栬緫寮圭獥銆佸垪琛ㄣ�佽鎯咃級
+- 浠樻鍗曡鎯呴〉锛堝睍绀哄叧鑱旂殑鏉ョエ淇℃伅锛�
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+1. 閲囪喘璁㈠崟锛堝鏍搁�氳繃锛�-> 鍒涘缓鏉ョエ锛歚purchaseOrderId` 蹇呴�夛紝渚涘簲鍟嗚嚜鍔ㄥ甫鍑猴紙浠ラ噰璐鍗曠殑渚涘簲鍟嗕负鍑嗭紝鍓嶇鏃犻渶浼� `supplierId`锛岀敱鍚庣鎸夐噰璐鍗曟煡璇㈣缃級銆�
+2. 鏉ョエ鍒涘缓鍚� -> 涓婁紶鍙戠エ闄勪欢锛堣蛋 system 妯″潡 Storage API锛夈��
+3. 鏉ョエ鎻愪氦瀹℃壒 -> 瀹℃壒閫氳繃 -> 鎵嶅彲浣滀负浠樻鍏宠仈瀵硅薄銆�
+4. 鏂板缓浠樻鍗� -> 閫夋嫨鍏宠仈鏉ョエ锛坄invoiceId` 蹇呭~锛夛細浠� `GET /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId=` 閫夋嫨锛屼粎鏄剧ず"瀹℃壒閫氳繃 + 宸蹭笂浼犻檮浠� + 鍓╀綑鍙粯閲戦 > 0"鐨勬潵绁紱閫夋嫨鍚� `remainingInvoicePrice` 鍙敤浜庨濉粯娆鹃噾棰濅笂闄愩��
+
+瀵瑰簲鍏崇郴锛�**閲囪喘璁㈠崟(1) -> 鏉ョエ(N) -> 浠樻(M)**銆�
+
+## API
+
+### 1. 鏉ョエ绠$悊锛堟柊锛�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /erp/purchase-invoice/create | 鍒涘缓鏉ョエ |
+| PUT | /erp/purchase-invoice/update | 鏇存柊鏉ョエ |
+| DELETE | /erp/purchase-invoice/delete?id= | 鍒犻櫎鏉ョエ |
+| GET | /erp/purchase-invoice/get?id= | 鑾峰緱鏉ョエ璇︽儏 |
+| GET | /erp/purchase-invoice/page | 鏉ョエ鍒嗛〉 |
+| GET | /erp/purchase-invoice/export-excel | 瀵煎嚭鏉ョエ Excel |
+| PUT | /erp/purchase-invoice/submit?id= | 鎻愪氦鏉ョエ瀹℃壒 |
+| GET | /erp/purchase-invoice/audit-count | 寰呭鏍告潵绁ㄦ暟閲� |
+| GET | /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId= | 绮剧畝鍒楄〃锛堜粯娆捐〃鍗曢�夋潵绁級 |
+
+**鍒涘缓/鏇存柊鏉ョエ璇锋眰鍙傛暟锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| id | Long | 鍚� | 鏇存柊鏃跺繀濉� |
+| purchaseOrderId | Long | 鏄� | 閲囪喘璁㈠崟缂栧彿锛屽繀閫� |
+| supplierId | Long | 鍚� | 渚涘簲鍟嗙紪鍙凤紝**涓嶄紶**锛岀敱鍚庣鎸夐噰璐鍗曟煡璇㈣缃� |
+| invoiceNo | String | 鍚� | 鍙戠エ鍙风爜 |
+| invoiceTitle | String | 鍚� | 鍙戠エ鎶ご |
+| price | BigDecimal | 鏄� | 鏉ョエ閲戦锛堝厓锛� |
+| invoiceTime | LocalDateTime | 鏄� | 鏉ョエ鏃ユ湡 |
+| remark | String | 鍚� | 澶囨敞 |
+| blobIds | List\<Long\> | 鍚� | 鍙戠エ闄勪欢 blobId 鍒楄〃 |
+
+**鏉ョエ鍒嗛〉/璇︽儏鍝嶅簲鏂板瀛楁锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| no | String | 鏉ョエ缂栧彿锛堝悗绔敓鎴愶紝鍓嶇紑 FPD锛� |
+| supplierName | String | 渚涘簲鍟嗗悕绉� |
+| order | Object | 閲囪喘璁㈠崟淇℃伅锛堝祵濂楋級 |
+| auditStatus | Integer | 瀹℃壒鐘舵�侊紙0鏈彁浜�/10瀹℃壒涓�/20瀹℃牳閫氳繃/30瀹℃牳涓嶉�氳繃/40宸插彇娑堬級 |
+| attachmentList | List | 鍙戠エ闄勪欢鍒楄〃 |
+| hasAttachment | Boolean | 鏄惁宸蹭笂浼犲彂绁ㄩ檮浠� |
+| remainingInvoicePrice | BigDecimal | 鍓╀綑鍙粯閲戦 = 鏉ョエ閲戦 - 璇ユ潵绁ㄥ凡浠樻閲戦 |
+
+**simple-list 鍝嶅簲锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | Long | 鏉ョエ缂栧彿 |
+| no | String | 鏉ョエ缂栧彿 |
+| invoiceNo | String | 鍙戠エ鍙风爜 |
+| invoiceTitle | String | 鍙戠エ鎶ご |
+| price | BigDecimal | 鏉ョエ閲戦 |
+| supplierId | Long | 渚涘簲鍟嗙紪鍙� |
+| purchaseOrderId | Long | 閲囪喘璁㈠崟缂栧彿 |
+| auditStatus | Integer | 瀹℃壒鐘舵�� |
+| hasAttachment | Boolean | 鏄惁宸蹭笂浼犻檮浠讹紙鎭掍负 true锛� |
+| remainingInvoicePrice | BigDecimal | 鍓╀綑鍙粯閲戦 |
+
+### 2. 浠樻鍗曪紙鎺ュ彛鍙樻洿锛�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /erp/finance-payment/create | 鍒涘缓浠樻鍗曪紝璇锋眰浣撴柊澧� `invoiceId`锛堝繀濉級 |
+| PUT | /erp/finance-payment/update | 鏇存柊浠樻鍗曪紝璇锋眰浣撴柊澧� `invoiceId`锛堝繀濉級 |
+| GET | /erp/finance-payment/get | 鍝嶅簲鏂板 `invoiceId`銆乣invoice` 宓屽瀵硅薄 |
+| GET | /erp/finance-payment/page | 鍝嶅簲鏂板 `invoiceId`銆乣invoice` 宓屽瀵硅薄 |
+
+**璇锋眰鍙傛暟鏂板锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| invoiceId | Long | 鏄� | 鍏宠仈鐨勬潵绁ㄧ紪鍙凤紙鏂板缓浠樻鍗曞繀濉級 |
+
+**鍝嶅簲瀛楁鏂板锛�**
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| invoiceId | Long | 鍏宠仈鐨勬潵绁ㄧ紪鍙� |
+| invoice | Object | 鍏宠仈鐨勬潵绁ㄤ俊鎭紙鍚� no銆乮nvoiceNo銆乮nvoiceTitle銆乸rice銆乤uditStatus 绛夛級 |
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": {
+ "id": 1,
+ "no": "FKD20260810000001",
+ "invoiceId": 100,
+ "invoice": {
+ "id": 100,
+ "no": "FPD20260810000001",
+ "invoiceNo": "02564178",
+ "invoiceTitle": "XX绉戞妧鏈夐檺鍏徃",
+ "price": 9000.00,
+ "auditStatus": 20
+ },
+ "paymentPrice": 5000.00
+ }
+}
+```
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| 鏉ョエ缂栧彿 | 鏉ョエ鍒楄〃銆佽鎯呫�佷粯娆捐鎯� | 鍓嶇紑 FPD |
+| 渚涘簲鍟� | 鏉ョエ鍒楄〃銆佽鎯� | 鎸夐噰璐鍗曞甫鍑� |
+| 閲囪喘璁㈠崟 | 鏉ョエ鍒楄〃銆佽鎯� | 灞曠ず璁㈠崟缂栧彿/淇℃伅 |
+| 鍙戠エ鍙风爜/鎶ご | 鏉ョエ鍒楄〃銆佽鎯� | 褰曞叆瀛楁 |
+| 鏉ョエ閲戦 | 鏉ョエ鍒楄〃銆佽鎯呫�佷粯娆捐〃鍗� | 鍏� |
+| 鏉ョエ鏃ユ湡 | 鏉ョエ鍒楄〃銆佽鎯� | |
+| 瀹℃壒鐘舵�� | 鏉ョエ鍒楄〃銆佽鎯� | 瀛楀吀灞曠ず |
+| 鍙戠エ闄勪欢 | 鏉ョエ璇︽儏銆佹潵绁ㄥ垪琛� | 涓婁紶/棰勮/涓嬭浇 |
+| 鍓╀綑鍙粯閲戦 | 鏉ョエ鍒楄〃銆佷粯娆捐〃鍗� | 鏉ョエ閲戦 - 宸蹭粯娆鹃噾棰� |
+| 鍏宠仈鏉ョエ | 浠樻鍒楄〃銆佷粯娆捐鎯� | 灞曠ず invoice.no / invoice.invoiceNo / invoice.price |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 鏉ョエ鍒涘缓 | 閲囪喘璁㈠崟蹇呴』瀹℃牳閫氳繃锛涙潵绁ㄤ緵搴斿晢涓庨噰璐鍗曚竴鑷达紱鏉ョエ閲戦涓嶈秴杩囪鍗曞墿浣欏彲寮�绁ㄩ噾棰� |
+| 鏉ョエ缂栬緫 | 浠�"鏈彁浜�"鐘舵�佸彲缂栬緫 |
+| 鏉ョエ鍒犻櫎 | 瀹℃壒閫氳繃鍚庝笉鍙垹闄� |
+| 鏉ョエ鎻愪氦瀹℃壒 | 浠�"鏈彁浜�"鐘舵�佸彲鎻愪氦锛屾彁浜ゅ悗杩涘叆瀹℃壒涓� |
+| 浠樻閫夋嫨鏉ョエ | 浠呭鎵归�氳繃 + 宸蹭笂浼犲彂绁ㄩ檮浠剁殑鏉ョエ鍙�� |
+| 浠樻閲戦涓婇檺 | 浠樻閲戦 <= 璇ユ潵绁ㄩ噾棰� - 璇ユ潵绁ㄥ凡浠樻閲戦锛堟湭瀹℃牳+瀹℃壒涓�+宸插鏍镐粯娆撅紝鎺掗櫎浣滃簾锛� |
+| 渚涘簲鍟嗕竴鑷存�� | 浠樻鐨勪緵搴斿晢蹇呴』涓庡叧鑱旀潵绁ㄧ殑渚涘簲鍟嗕竴鑷� |
+| 瀛橀噺浠樻鍗� | invoiceId 涓虹┖鐨勫瓨閲忎粯娆惧崟锛岀紪杈戞椂闇�閲嶆柊閫夋嫨鍚堟牸鏉ョエ |
+
+## 娉ㄦ剰浜嬮」
+
+- 鏉ョエ渚涘簲鍟嗕笉浠庡墠绔紶鍏ワ紝鍒涘缓/鏇存柊鏃剁敱鍚庣鏍规嵁閲囪喘璁㈠崟鑷姩璁剧疆锛屽墠绔〃鍗曞彧灞曠ず銆�
+- 浠樻琛ㄥ崟閫夋嫨鏉ョエ鏃讹紝鍏堟寜渚涘簲鍟嗚繃婊わ紙`supplierId`锛夛紝鍙�夎鍗曟椂浼� `purchaseOrderId` 杩涗竴姝ヨ繃婊ゃ��
+- `remainingInvoicePrice` 涓鸿礋鏁版垨 0 鐨勬潵绁ㄤ笉浼氬嚭鐜板湪 simple-list 涓紝鍓嶇鍙洿鎺ユ寜杩斿洖鍒楄〃娓叉煋銆�
+- 浠樻閲戦鏍¢獙鍦ㄥ悗绔紝鍓嶇鍙皢鏉ョエ鐨� `remainingInvoicePrice` 浣滀负浠樻閲戦杈撳叆涓婇檺鎻愮ず銆�
+- 鏉ョエ瀹℃壒鐘舵�佸彉鍖栭�氳繃 BPM 鍥炶皟鏇存柊锛屽墠绔棤闇�澶勭悊锛屽垎椤垫帴鍙g洿鎺ヨ繑鍥炴渶鏂扮姸鎬併��
+- 鏉ョエ鍙戠エ闄勪欢涓婁紶浣跨敤 system 妯″潡 Storage API锛坲pload -> bind锛夛紝涓嶈浣跨敤鏃х増 /infra/file 鎺ュ彛銆�
diff --git a/docs/receivable_invoice_link.md b/docs/receivable_invoice_link.md
new file mode 100644
index 0000000..a3f65bc
--- /dev/null
+++ b/docs/receivable_invoice_link.md
@@ -0,0 +1,119 @@
+# 寮�绁ㄤ笌鍥炴鍏宠仈锛堝厛寮�绁ㄥ悗鍥炴锛� - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+- 鍥炴绠$悊鍒楄〃椤�
+- 鍥炴鍒涘缓 / 缂栬緫琛ㄥ崟
+- 鍥炴璇︽儏椤�
+- 寮�绁ㄧ鐞嗗垪琛ㄩ〉銆佸紑绁ㄨ鎯呴〉锛�**鍓嶇鐩墠缂哄け锛岄渶鏂板缓**锛�
+- 鍚堝悓绠$悊鍒楄〃椤点�佸悎鍚岃鎯呴〉锛堜笂涓换鍔″凡鎺ュ叆"宸插紑绁ㄩ噾棰�/鏈紑绁ㄩ噾棰�"锛屾湰娆′笉娑夊強锛�
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+1. **寮�绁� 鈫� 鍥炴**锛氬紑绁ㄥ崟瀹℃壒閫氳繃 + 涓婁紶鍙戠エ闄勪欢鍚庯紝鎵嶄細鍑虹幇鍦ㄥ洖娆惧垱寤鸿〃鍗曠殑"鍏宠仈寮�绁�"涓嬫媺涓��
+2. **鍥炴鍒涘缓**锛氶�夋嫨瀹㈡埛 鈫� 閫夋嫨鍚堝悓 鈫� 閫夋嫨鍏宠仈寮�绁紙鑷姩甯﹀叆瀹㈡埛銆佸悎鍚岋級鈫掞紙鍙�夛級閫夋嫨鍥炴璁″垝 鈫� 濉洖娆鹃噾棰濓紙鍙濉负璇ュ紑绁ㄧ殑鍓╀綑鍙洖娆鹃噾棰濓級銆�
+3. **鍚堝悓涓嬫媺锛坰imple-list锛�**锛氬凡杩斿洖 `totalInvoicePrice`锛堝凡寮�绁ㄩ噾棰濓紝鍚鎵逛腑锛夛紝鐢ㄤ簬灞曠ず鍚堝悓鍓╀綑鍙紑棰濆害銆�
+4. **寮�绁ㄤ笅鎷夛紙simple-list锛�**锛氳繑鍥� `remainingInvoicePrice`锛堣寮�绁ㄥ墿浣欏彲鍥炴閲戦锛夛紝鐢ㄤ簬鍒涘缓鍥炴鏃堕濉噾棰濄��
+
+## API
+
+### 1. 鏂板鎺ュ彛锛氭寜鍚堝悓鏌ヨ鍙�夊紑绁�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| GET | `/crm/invoice/simple-list?contractId=` | 鍥炴鍒涘缓鏃堕�夋嫨鍏宠仈寮�绁紝鍙繑鍥炲悎鏍煎紑绁� |
+
+**绛涢�夎鍒欙紙鍚庣宸插疄鐜帮級**锛氬鎵归�氳繃 + 宸蹭笂浼犲彂绁ㄩ檮浠� + 鍓╀綑鍙洖娆鹃噾棰� > 0銆�
+
+**鍝嶅簲绀轰緥锛�**
+
+```json
+{
+ "code": 0,
+ "data": [
+ {
+ "id": 1,
+ "no": "KP20260810000001",
+ "invoiceNo": "02564178",
+ "invoiceTitle": "XX绉戞妧鏈夐檺鍏徃",
+ "price": 1090.00,
+ "customerId": 1,
+ "contractId": 31,
+ "auditStatus": 20,
+ "hasAttachment": true,
+ "remainingInvoicePrice": 1090.00
+ }
+ ]
+}
+```
+
+### 2. 鍙樻洿鎺ュ彛锛氬洖娆惧垱寤� / 鏇存柊
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | `/crm/receivable/create` | 璇锋眰浣撴柊澧� `invoiceId`锛�**蹇呭~**锛� |
+| PUT | `/crm/receivable/update` | 璇锋眰浣撴柊澧� `invoiceId`锛堝彲淇敼锛� |
+
+**璇锋眰鍙傛暟鏂板锛�**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| invoiceId | Long | 鏄� | 鍏宠仈鐨勫紑绁ㄧ紪鍙凤紝蹇呴』涓哄鎵归�氳繃涓斿凡涓婁紶闄勪欢鐨勫紑绁� |
+
+### 3. 鍙樻洿鎺ュ彛锛氬洖娆炬煡璇紙鍝嶅簲鏂板瀛楁锛�
+
+| 鏂规硶 | 璺緞 | 鏂板鍝嶅簲瀛楁 |
+|------|------|--------------|
+| GET | `/crm/receivable/page` | `invoiceId`銆乣invoice`锛堝祵濂楀璞★級 |
+| GET | `/crm/receivable/get` | `invoiceId`銆乣invoice`锛堝祵濂楀璞★級 |
+| GET | `/crm/receivable/page-by-customer` | `invoiceId`銆乣invoice`锛堝祵濂楀璞★級 |
+
+**`invoice` 宓屽瀵硅薄鍏抽敭瀛楁锛�** `id`銆乣no`锛堝紑绁ㄧ紪鍙凤級銆乣invoiceNo`锛堝彂绁ㄥ彿鐮侊級銆乣invoiceTitle`锛堝彂绁ㄦ姮澶达級銆乣price`锛堝紑绁ㄩ噾棰濓級銆�
+
+### 4. 鍙樻洿鎺ュ彛锛氬紑绁ㄦ煡璇紙鍝嶅簲鏂板瀛楁锛�
+
+| 鏂规硶 | 璺緞 | 鏂板鍝嶅簲瀛楁 |
+|------|------|--------------|
+| GET | `/crm/invoice/page` | `hasAttachment`銆乣remainingInvoicePrice` |
+| GET | `/crm/invoice/get` | `hasAttachment`銆乣remainingInvoicePrice` |
+
+**璇存槑锛�**
+- `hasAttachment`锛圔oolean锛夛細鏄惁宸蹭笂浼犲彂绁ㄩ檮浠讹紝鐢ㄤ簬鍒楄〃蹇�熷垽鏂摢浜涘紑绁ㄥ彲琚洖娆惧叧鑱斻��
+- `remainingInvoicePrice`锛圔igDecimal锛夛細鍓╀綑鍙洖娆鹃噾棰� = 寮�绁ㄩ噾棰� - 璇ュ紑绁ㄥ凡鍥炴閲戦锛堢粺璁¤崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃鐨勫洖娆撅級銆�
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| 鍏宠仈寮�绁� | 鍥炴鍒楄〃鍒椼�佸洖娆捐鎯呴〉 | 灞曠ず `invoice.no` / `invoice.invoiceNo` / `invoice.price` |
+| 鍏宠仈寮�绁ㄩ�夋嫨鍣� | 鍥炴鍒涘缓/缂栬緫琛ㄥ崟 | 閫夋嫨鍚庡甫鍏ュ鎴枫�佸悎鍚屻�侀濉洖娆鹃噾棰濅负 `remainingInvoicePrice` |
+| 鍓╀綑鍙洖娆鹃噾棰� | 寮�绁ㄥ垪琛ㄥ垪銆佸紑绁ㄨ鎯呴〉 | 寮�绁ㄩ噾棰� - 璇ュ紑绁ㄥ凡鍥炴閲戦 |
+| 鏄惁宸蹭笂浼犻檮浠� | 寮�绁ㄥ垪琛ㄥ垪 | 杈呭姪鍒ゆ柇寮�绁ㄦ槸鍚﹀彲鍥炴锛堥渶瀹℃壒閫氳繃 + 宸蹭笂浼犻檮浠讹級 |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 瀹℃壒涓殑寮�绁� | 涓嶅彲閫変负鍥炴渚濇嵁锛堝悗绔嫤鎴細鍏宠仈寮�绁ㄤ笉鏄鎵归�氳繃鐘舵�侊級 |
+| 鏈笂浼犲彂绁ㄩ檮浠剁殑寮�绁� | 涓嶅彲閫変负鍥炴渚濇嵁锛堝悗绔嫤鎴細鍏宠仈寮�绁ㄦ湭涓婁紶鍙戠エ闄勪欢锛� |
+| 宸插叏閮ㄥ洖娆剧殑寮�绁� | 浠�"鍏宠仈寮�绁�"涓嬫媺杩囨护锛堝墿浣欏彲鍥炴 鈮� 0锛� |
+| 鍥炴閲戦瓒呴檺 | 鍚庣鎷︽埅锛氬洖娆鹃噾棰濊秴鍑鸿寮�绁ㄥ墿浣欏彲鍥炴閲戦 |
+| 寮�绁ㄤ笌鍚堝悓涓嶅尮閰� | 鍚庣鎷︽埅锛氭墍閫夊紑绁ㄤ笌鍥炴鍚堝悓涓嶄竴鑷� |
+| 缂栬緫鍥炴鏃舵敼寮�绁� | 鑽夌/瀹℃壒涓姸鎬佸彲淇敼鍏宠仈寮�绁紝鏀瑰悗閲嶆柊鎸夋柊寮�绁ㄦ牎楠岄噾棰� |
+
+## 鍓嶇缂哄彛锛堥渶鏂板缓锛�
+
+> 鍚庣寮�绁ㄦā鍧楀凡瀹屾暣锛堝惈瀹℃壒娴佺▼銆侀檮浠朵笂浼狅級锛屼絾鍓嶇灏氭棤寮�绁ㄩ〉闈笌 API锛岄渶鍓嶇鏂板缓銆�
+
+1. **寮�绁ㄧ鐞嗛〉**锛歚views/crm/invoice/`锛堝垪琛ㄣ�佸垱寤�/缂栬緫銆佸鎵规彁浜ゃ�佸彂绁ㄩ檮浠朵笂浼犱笌灞曠ず锛夈��
+2. **寮�绁� API**锛歚api/crm/invoice/`锛坧age / get / create / update / delete / submit / audit-count / export-excel / simple-list锛夈��
+3. **鍥炴琛ㄥ崟寮�绁ㄩ�夋嫨鍣�**锛氶�夊悎鍚屽悗璋冪敤 `/crm/invoice/simple-list?contractId=`锛屽睍绀�"寮�绁ㄧ紪鍙� / 鍙戠エ鍙风爜 / 寮�绁ㄩ噾棰� / 鍓╀綑鍙洖娆�"锛岄�変腑鍚庨濉噾棰濄��
+4. **鑿滃崟**锛氬凡鐢卞悗绔� SQL 閰嶇疆瀹屾垚锛堣惀閿�绠$悊 -> 寮�绁ㄧ鐞嗭紝`component = crm/invoice/index`锛夛紝鍓嶇寤哄ソ椤甸潰鍚庤彍鍗曞嵆鍙敤銆�
+
+## 娉ㄦ剰浜嬮」
+
+- `invoiceId` 涓哄繀濉紝鍥炴鍒涘缓/鏇存柊**蹇呴』**鎼哄甫锛屽惁鍒欒繑鍥炲弬鏁版牎楠岄敊璇��
+- `invoice`銆乣remainingInvoicePrice`銆乣hasAttachment` 鍧囦负鍚庣璁$畻鐨勫彧璇诲瓧娈碉紝鍓嶇**涓嶈**鎻愪氦銆�
+- 鍥炴閲戦寤鸿鍦ㄩ�変腑寮�绁ㄥ悗棰勫~涓� `remainingInvoicePrice`锛岄伩鍏嶈秴闄愯鍚庣鎷︽埅銆�
+- 瀛橀噺鍥炴鏁版嵁鑻ユ湭鍏宠仈寮�绁紙`invoiceId` 涓虹┖锛夛紝缂栬緫鏃朵細琚姹傞噸鏂伴�夋嫨鍚堟牸寮�绁ㄣ��
+- 寮�绁ㄥ垪琛ㄥ睍绀�"鍓╀綑鍙洖娆鹃噾棰�"鏃讹紝鏂囨寤鸿鐢�"鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�"閬垮厤姝т箟锛屼笌鍚堝悓"宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛�"鍙e緞涓�鑷淬��
diff --git a/sql/mysql/hrm_employee_contract_init.sql b/sql/mysql/hrm_employee_contract_init.sql
new file mode 100644
index 0000000..d480d96
--- /dev/null
+++ b/sql/mysql/hrm_employee_contract_init.sql
@@ -0,0 +1,100 @@
+-- =============================================
+-- HRM 鍛樺伐鍚堝悓绠$悊 SQL 鍒濆鍖栬剼鏈�
+-- 鍖呭惈锛氬缓琛� + 鏁版嵁瀛楀吀 + 鑿滃崟
+-- =============================================
+
+-- =============================================
+-- 1. 鍛樺伐鍚堝悓琛�
+-- =============================================
+CREATE TABLE IF NOT EXISTS `hrm_employee_contract` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '鍚堝悓ID',
+ `parent_id` bigint DEFAULT NULL COMMENT '缁涓婁竴浠藉悎鍚孖D锛堜竴涓憳宸ュ彲绛捐澶氭鍚堝悓锛岄�氳繃璇ュ瓧娈靛舰鎴愮画绛鹃摼锛�',
+ `is_current` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁褰撳墠鐢熸晥鍚堝悓锛堝憳宸ユ渶鏂扮璁㈢殑鏈В闄�/鏈粓姝㈠悎鍚岋級',
+ `contract_no` varchar(64) NOT NULL COMMENT '鍚堝悓缂栧彿',
+ `employee_id` bigint NOT NULL COMMENT '鍛樺伐ID',
+ `contract_type` tinyint NOT NULL COMMENT '鍚堝悓绫诲瀷锛�1-鍔冲姩鍚堝悓锛�2-鍔冲姟鍚堝悓锛�3-瀹炰範鍗忚锛�4-鍔冲姟娲鹃仯锛�5-鍏朵粬',
+ `contract_term_type` tinyint NOT NULL COMMENT '鍚堝悓鏈熼檺绫诲瀷锛�1-鍥哄畾鏈熼檺锛�2-鏃犲浐瀹氭湡闄愶紝3-浠ュ畬鎴愪竴瀹氬伐浣滀换鍔′负鏈熼檺',
+ `sign_company` varchar(128) DEFAULT NULL COMMENT '绛剧害涓讳綋锛堝叕鍙稿叏绉帮級',
+ `sign_date` date DEFAULT NULL COMMENT '绛捐鏃ユ湡',
+ `start_date` date NOT NULL COMMENT '鍚堝悓寮�濮嬫棩鏈�',
+ `end_date` date DEFAULT NULL COMMENT '鍚堝悓缁撴潫鏃ユ湡锛堟棤鍥哄畾鏈熼檺鍙负绌猴級',
+ `probation_start_date` date DEFAULT NULL COMMENT '璇曠敤鏈熷紑濮嬫棩鏈�',
+ `probation_end_date` date DEFAULT NULL COMMENT '璇曠敤鏈熺粨鏉熸棩鏈�',
+ `probation_salary` decimal(10,2) DEFAULT NULL COMMENT '璇曠敤鏈熷伐璧�',
+ `regular_salary` decimal(10,2) DEFAULT NULL COMMENT '杞宸ヨ祫',
+ `terminate_status` tinyint NOT NULL DEFAULT 0 COMMENT '瑙i櫎/缁堟鐘舵�侊細0-姝e父锛�1-宸茶В闄わ紝2-宸茬粓姝�',
+ `terminate_date` date DEFAULT NULL COMMENT '瑙i櫎/缁堟鏃ユ湡',
+ `terminate_reason` varchar(255) DEFAULT NULL COMMENT '瑙i櫎/缁堟鍘熷洜',
+ `remark` varchar(255) DEFAULT NULL COMMENT '澶囨敞',
+ `creator` varchar(64) DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_contract_no` (`contract_no`),
+ KEY `idx_employee_id` (`employee_id`),
+ KEY `idx_end_date` (`end_date`),
+ KEY `idx_terminate_status` (`terminate_status`),
+ KEY `idx_parent_id` (`parent_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HRM鍛樺伐鍚堝悓琛�';
+
+-- =============================================
+-- 1.1 澧為噺鍗囩骇锛堥拡瀵瑰凡瀛樺湪鐨勮〃鎵ц锛屾敞鎰� ADD COLUMN 涓嶆敮鎸� IF NOT EXISTS锛岄渶纭鍒椾笉瀛樺湪锛�
+-- =============================================
+ALTER TABLE `hrm_employee_contract`
+ ADD COLUMN `parent_id` bigint DEFAULT NULL COMMENT '缁涓婁竴浠藉悎鍚孖D' AFTER `id`,
+ ADD COLUMN `is_current` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁褰撳墠鐢熸晥鍚堝悓' AFTER `parent_id`,
+ ADD INDEX `idx_parent_id` (`parent_id`);
+
+-- =============================================
+-- 2. 鏁版嵁瀛楀吀绫诲瀷
+-- =============================================
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES
+(1061123, '鍚堝悓绫诲瀷', 'hrm_contract_type', 0, NULL, '', NOW(), '', NOW(), b'0'),
+(1061124, '鍚堝悓鏈熼檺绫诲瀷', 'hrm_contract_term_type', 0, NULL, '', NOW(), '', NOW(), b'0'),
+(1061125, '鍚堝悓鐘舵��', 'hrm_contract_status', 0, NULL, '', NOW(), '', NOW(), b'0'),
+(1061126, '瑙i櫎缁堟鐘舵��', 'hrm_contract_terminate_status', 0, NULL, '', NOW(), '', NOW(), b'0');
+
+-- =============================================
+-- 3. 鏁版嵁瀛楀吀鏁版嵁
+-- =============================================
+-- 3.1 鍚堝悓绫诲瀷 hrm_contract_type
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES
+(1061231, 1, '鍔冲姩鍚堝悓', '1', 'hrm_contract_type', 0, 'primary', NULL, '', NOW(), '', NOW(), b'0'),
+(1061232, 2, '鍔冲姟鍚堝悓', '2', 'hrm_contract_type', 0, 'success', NULL, '', NOW(), '', NOW(), b'0'),
+(1061233, 3, '瀹炰範鍗忚', '3', 'hrm_contract_type', 0, 'warning', NULL, '', NOW(), '', NOW(), b'0'),
+(1061234, 4, '鍔冲姟娲鹃仯', '4', 'hrm_contract_type', 0, 'info', NULL, '', NOW(), '', NOW(), b'0'),
+(1061235, 5, '鍏朵粬', '5', 'hrm_contract_type', 0, 'default', NULL, '', NOW(), '', NOW(), b'0');
+
+-- 3.2 鍚堝悓鏈熼檺绫诲瀷 hrm_contract_term_type
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES
+(1061236, 1, '鍥哄畾鏈熼檺', '1', 'hrm_contract_term_type', 0, 'primary', NULL, '', NOW(), '', NOW(), b'0'),
+(1061237, 2, '鏃犲浐瀹氭湡闄�', '2', 'hrm_contract_term_type', 0, 'success', NULL, '', NOW(), '', NOW(), b'0'),
+(1061238, 3, '浠ュ畬鎴愪竴瀹氬伐浣滀换鍔′负鏈熼檺', '3', 'hrm_contract_term_type', 0, 'warning', NULL, '', NOW(), '', NOW(), b'0');
+
+-- 3.3 鍚堝悓鐘舵�� hrm_contract_status锛堝姩鎬佽绠楀睍绀虹敤锛�
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES
+(1061239, 1, '寰呯敓鏁�', '1', 'hrm_contract_status', 0, 'info', NULL, '', NOW(), '', NOW(), b'0'),
+(1061240, 2, '鐢熸晥涓�', '2', 'hrm_contract_status', 0, 'success', NULL, '', NOW(), '', NOW(), b'0'),
+(1061241, 3, '鍗冲皢鍒版湡', '3', 'hrm_contract_status', 0, 'warning', NULL, '', NOW(), '', NOW(), b'0'),
+(1061242, 4, '宸插埌鏈�', '4', 'hrm_contract_status', 0, 'default', NULL, '', NOW(), '', NOW(), b'0'),
+(1061243, 5, '宸茶В闄�', '5', 'hrm_contract_status', 0, 'danger', NULL, '', NOW(), '', NOW(), b'0'),
+(1061244, 6, '宸茬粓姝�', '6', 'hrm_contract_status', 0, 'danger', NULL, '', NOW(), '', NOW(), b'0');
+
+-- 3.4 瑙i櫎缁堟鐘舵�� hrm_contract_terminate_status
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES
+(1061245, 1, '姝e父', '0', 'hrm_contract_terminate_status', 0, 'success', NULL, '', NOW(), '', NOW(), b'0'),
+(1061246, 2, '宸茶В闄�', '1', 'hrm_contract_terminate_status', 0, 'danger', NULL, '', NOW(), '', NOW(), b'0'),
+(1061247, 3, '宸茬粓姝�', '2', 'hrm_contract_terminate_status', 0, 'warning', NULL, '', NOW(), '', NOW(), b'0');
+
+-- =============================================
+-- 4. 鑿滃崟锛堟寕杞藉埌 HRM 浜哄姏璧勬簮 6001 涓嬶紝涓庡憳宸ョ鐞� 6006 骞崇骇锛�
+-- 娉ㄦ剰锛氫笉鑳芥寕鍦�"鍛樺伐绠$悊"涓嬶紝鍚﹀垯 6006 浼氬彉鎴愬甫瀛愯彍鍗曠殑鐩綍锛屽鑷村憳宸ュ垪琛ㄩ〉鏃犳硶鐩磋揪
+-- =============================================
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `path`, `component`, `parent_id`, `icon`, `sort`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES
+(1063105, '鍛樺伐鍚堝悓', '', 2, 'employee-contract', 'hrm/employee/contract/index', 6001, 'ep:files', 0, 0, b'1', b'1', b'1', '', NOW(), '', NOW(), b'0'),
+(1063106, '鍚堝悓鏌ヨ', 'hrm:employee-contract:query', 3, '', '', 1063105, '#', 1, 0, b'1', b'1', b'1', '', NOW(), '', NOW(), b'0'),
+(1063107, '鍚堝悓鏂板', 'hrm:employee-contract:create', 3, '', '', 1063105, '#', 2, 0, b'1', b'1', b'1', '', NOW(), '', NOW(), b'0'),
+(1063108, '鍚堝悓淇敼', 'hrm:employee-contract:update', 3, '', '', 1063105, '#', 3, 0, b'1', b'1', b'1', '', NOW(), '', NOW(), b'0'),
+(1063109, '鍚堝悓鍒犻櫎', 'hrm:employee-contract:delete', 3, '', '', 1063105, '#', 4, 0, b'1', b'1', b'1', '', NOW(), '', NOW(), b'0');
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
index 4de695d..d78cba5 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -26,6 +26,7 @@
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.invoice.CrmInvoiceService;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -71,6 +72,8 @@
private CrmBusinessService businessService;
@Resource
private CrmReceivableService receivableService;
+ @Resource
+ private CrmInvoiceService invoiceService;
@Resource
private MdmItemApi mdmItemApi;
@@ -218,6 +221,9 @@
// 1.5 鑾峰緱宸插洖娆鹃噾棰�
Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByContractId(
convertSet(contractList, CrmContractDO::getId));
+ // 1.6 鑾峰緱宸插紑绁ㄩ噾棰濓紙瀹℃壒閫氳繃 + 瀹℃壒涓紝涓庡紑绁ㄩ槻瓒呮牎楠屽彛寰勪竴鑷达級
+ Map<Long, BigDecimal> invoicePriceMap = invoiceService.getInvoicePriceMapByContractId(
+ convertSet(contractList, CrmContractDO::getId));
// 2. 鎷兼帴鏁版嵁
return BeanUtils.toBean(contractList, CrmContractRespVO.class, contractVO -> {
// 2.1 璁剧疆瀹㈡埛淇℃伅
@@ -235,6 +241,12 @@
findAndThen(businessMap, contractVO.getBusinessId(), business -> contractVO.setBusinessName(business.getName()));
// 2.5 璁剧疆宸插洖娆鹃噾棰�
contractVO.setTotalReceivablePrice(receivablePriceMap.getOrDefault(contractVO.getId(), BigDecimal.ZERO));
+ // 2.6 璁剧疆宸插紑绁ㄩ噾棰�
+ contractVO.setTotalInvoicePrice(invoicePriceMap.getOrDefault(contractVO.getId(), BigDecimal.ZERO));
+ // 2.7 璁剧疆鏈紑绁ㄩ噾棰�
+ if (contractVO.getTotalPrice() != null && contractVO.getTotalInvoicePrice() != null) {
+ contractVO.setUnInvoicePrice(contractVO.getTotalPrice().subtract(contractVO.getTotalInvoicePrice()));
+ }
});
}
@@ -266,10 +278,13 @@
// 鎷兼帴鏁版嵁
Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByContractId(
convertSet(pageResult.getList(), CrmContractDO::getId));
+ Map<Long, BigDecimal> invoicePriceMap = invoiceService.getInvoicePriceMapByContractId(
+ convertSet(pageResult.getList(), CrmContractDO::getId));
return success(convertList(pageResult.getList(), contract -> new CrmContractRespVO() // 鍙繑鍥� id銆乶ame 绛夌簿绠�瀛楁
.setId(contract.getId()).setName(contract.getName()).setAuditStatus(contract.getAuditStatus())
.setTotalPrice(contract.getTotalPrice())
- .setTotalReceivablePrice(receivablePriceMap.getOrDefault(contract.getId(), BigDecimal.ZERO))));
+ .setTotalReceivablePrice(receivablePriceMap.getOrDefault(contract.getId(), BigDecimal.ZERO))
+ .setTotalInvoicePrice(invoicePriceMap.getOrDefault(contract.getId(), BigDecimal.ZERO))));
}
@PostMapping("/generate-sale-order")
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 4410428..e9acd92 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
@@ -96,6 +96,12 @@
@ExcelProperty(value = "鏈洖娆鹃噾棰濓紙鍏冿級", index = 13)
private BigDecimal unReceivablePrice;
+ @Schema(description = "宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛夛紝缁熻瀹℃壒閫氳繃 + 瀹℃壒涓殑寮�绁�", example = "5617")
+ private BigDecimal totalInvoicePrice;
+
+ @Schema(description = "鏈紑绁ㄩ噾棰濓紝绛変簬 鍚堝悓閲戦 - 宸插紑绁ㄩ噾棰�", example = "1000.00")
+ private BigDecimal unInvoicePrice;
+
@Schema(description = "鍏宠仈 ERP 閿�鍞鍗曠紪鍙�", example = "12345")
private Long orderId;
@Schema(description = "ERP 閿�鍞鍗曞崟鍙�", example = "SO001")
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
index 0a960e5..8397ad9 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
@@ -16,14 +16,17 @@
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.dal.dataobject.invoice.CrmInvoiceDO;
+import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
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.invoice.CrmInvoiceService;
+import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
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.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -35,6 +38,8 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -60,6 +65,8 @@
private CrmContractService contractService;
@Resource
private CrmCustomerService customerService;
+ @Resource
+ private CrmReceivableService receivableService;
@Resource
private AdminUserApi adminUserApi;
@@ -151,6 +158,9 @@
// 1.3 鑾峰緱鍚堝悓鍒楄〃
Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
convertSet(invoiceList, CrmInvoiceDO::getContractId));
+ // 1.4 鑾峰緱姣忓紶寮�绁ㄥ凡鍥炴閲戦锛堣崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛�
+ Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByInvoiceId(
+ convertSet(invoiceList, CrmInvoiceDO::getId));
// 2. 鎷兼帴缁撴灉
return BeanUtils.toBean(invoiceList, CrmInvoiceRespVO.class, (invoiceVO) -> {
// 2.1 鎷兼帴瀹㈡埛鍚嶇О
@@ -166,7 +176,13 @@
findAndThen(contractMap, invoiceVO.getContractId(), contract ->
invoiceVO.setContract(BeanUtils.toBean(contract, CrmContractRespVO.class)));
// 2.4 鎷兼帴鍙戠エ闄勪欢
- invoiceVO.setAttachmentList(storageAttachmentApi.listAttachments("crm_invoice", invoiceVO.getId()));
+ invoiceVO.setAttachmentList(storageAttachmentApi.listAttachments(
+ StorageRecordTypeEnum.CRM_INVOICE.getType(), invoiceVO.getId()));
+ // 2.5 鎷兼帴鏄惁宸蹭笂浼犻檮浠�
+ invoiceVO.setHasAttachment(CollUtil.isNotEmpty(invoiceVO.getAttachmentList()));
+ // 2.6 鎷兼帴鍓╀綑鍙洖娆鹃噾棰�
+ invoiceVO.setRemainingInvoicePrice(invoiceVO.getPrice().subtract(
+ receivablePriceMap.getOrDefault(invoiceVO.getId(), BigDecimal.ZERO)));
});
}
@@ -185,4 +201,41 @@
return success(invoiceService.getAuditInvoiceCount(getLoginUserId()));
}
+ @GetMapping("/simple-list")
+ @Operation(summary = "鑾峰緱寮�绁ㄧ簿绠�鍒楄〃", description = "鍙寘鍚鏍搁�氳繃涓斿凡涓婁紶鍙戠エ闄勪欢鐨勫紑绁紝涓昏鐢ㄤ簬鍥炴鍒涘缓鏃堕�夋嫨鍏宠仈寮�绁�")
+ @Parameter(name = "contractId", description = "鍚堝悓缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('crm:invoice:query')")
+ public CommonResult<List<CrmInvoiceRespVO>> getInvoiceSimpleList(@RequestParam("contractId") Long contractId) {
+ List<CrmInvoiceDO> invoices = invoiceService.getInvoiceListByContractIdAndStatus(contractId,
+ List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
+ if (CollUtil.isEmpty(invoices)) {
+ return success(Collections.emptyList());
+ }
+ // 杩囨护锛氬凡涓婁紶鍙戠エ闄勪欢
+ invoices.removeIf(invoice -> CollUtil.isEmpty(storageAttachmentApi.listAttachments(
+ StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId())));
+ if (CollUtil.isEmpty(invoices)) {
+ return success(Collections.emptyList());
+ }
+ // 璁$畻姣忓紶寮�绁ㄥ凡鍥炴閲戦锛堣崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛屽彛寰勪笌鍒涘缓鍥炴鏍¢獙涓�鑷达級
+ Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByInvoiceId(
+ convertSet(invoices, CrmInvoiceDO::getId));
+ // 杩囨护锛氬墿浣欏彲鍥炴閲戦 > 0 鎵嶅彲閫�
+ List<CrmInvoiceRespVO> result = new ArrayList<>();
+ for (CrmInvoiceDO invoice : invoices) {
+ BigDecimal remainingPrice = invoice.getPrice()
+ .subtract(receivablePriceMap.getOrDefault(invoice.getId(), BigDecimal.ZERO));
+ if (remainingPrice.compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ result.add(new CrmInvoiceRespVO() // 鍙繑鍥炵簿绠�瀛楁
+ .setId(invoice.getId()).setNo(invoice.getNo()).setInvoiceNo(invoice.getInvoiceNo())
+ .setInvoiceTitle(invoice.getInvoiceTitle()).setPrice(invoice.getPrice())
+ .setCustomerId(invoice.getCustomerId()).setContractId(invoice.getContractId())
+ .setAuditStatus(invoice.getAuditStatus()).setHasAttachment(true)
+ .setRemainingInvoicePrice(remainingPrice));
+ }
+ return success(result);
+ }
+
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java
index 05b1e68..57f86c5 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/vo/invoice/CrmInvoiceRespVO.java
@@ -85,6 +85,12 @@
@Schema(description = "鍙戠エ闄勪欢鍒楄〃")
private List<StorageBlobRespDTO> attachmentList;
+ @Schema(description = "鏄惁宸蹭笂浼犲彂绁ㄩ檮浠�", example = "true")
+ private Boolean hasAttachment;
+
+ @Schema(description = "鍓╀綑鍙洖娆鹃噾棰濓紝绛変簬 寮�绁ㄩ噾棰� - 璇ュ紑绁ㄥ凡鍥炴閲戦锛堣崏绋�+瀹℃壒涓�+瀹℃壒閫氳繃锛�", example = "1000.00")
+ private BigDecimal remainingInvoicePrice;
+
@Schema(description = "鍒涘缓鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("鍒涘缓鏃堕棿")
private LocalDateTime createTime;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
index 58e02d1..e3c6ba8 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
@@ -10,14 +10,17 @@
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.contract.vo.contract.CrmContractRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
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.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
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.invoice.CrmInvoiceService;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -59,6 +62,8 @@
private CrmContractService contractService;
@Resource
private CrmCustomerService customerService;
+ @Resource
+ private CrmInvoiceService invoiceService;
@Resource
private AdminUserApi adminUserApi;
@@ -148,6 +153,9 @@
// 1.3 鑾峰緱鍚堝悓鍒楄〃
Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
convertSet(receivableList, CrmReceivableDO::getContractId));
+ // 1.4 鑾峰緱鍏宠仈寮�绁ㄥ垪琛�
+ Map<Long, CrmInvoiceDO> invoiceMap = invoiceService.getInvoiceMap(
+ convertSet(receivableList, CrmReceivableDO::getInvoiceId));
// 2. 鎷兼帴缁撴灉
return BeanUtils.toBean(receivableList, CrmReceivableRespVO.class, (receivableVO) -> {
// 2.1 鎷兼帴瀹㈡埛鍚嶇О
@@ -162,6 +170,9 @@
// 2.3 鎷兼帴鍚堝悓淇℃伅
findAndThen(contractMap, receivableVO.getContractId(), contract ->
receivableVO.setContract(BeanUtils.toBean(contract, CrmContractRespVO.class)));
+ // 2.4 鎷兼帴鍏宠仈寮�绁ㄤ俊鎭�
+ findAndThen(invoiceMap, receivableVO.getInvoiceId(), invoice ->
+ receivableVO.setInvoice(BeanUtils.toBean(invoice, CrmInvoiceRespVO.class)));
});
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
index 75827db..aab6fc4 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceRespVO;
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
@@ -54,6 +55,12 @@
@Schema(description = "鍚堝悓淇℃伅")
private CrmContractRespVO contract;
+ @Schema(description = "鍏宠仈鐨勫紑绁ㄧ紪鍙�", example = "25787")
+ @ExcelProperty("鍏宠仈寮�绁ㄧ紪鍙�")
+ private Long invoiceId;
+ @Schema(description = "鍏宠仈寮�绁ㄤ俊鎭�")
+ private CrmInvoiceRespVO invoice;
+
@Schema(description = "璐熻矗浜虹殑鐢ㄦ埛缂栧彿", example = "25682")
private Long ownerUserId;
@Schema(description = "璐熻矗浜哄悕瀛�", example = "25682")
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java
index 8ab85da..50d5d56 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java
@@ -32,6 +32,11 @@
@NotNull(message = "鍚堝悓缂栧彿涓嶈兘涓虹┖")
private Long contractId;
+ @Schema(description = "鍏宠仈鐨勫紑绁ㄧ紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
+ @DiffLogField(name = "鍏宠仈寮�绁�", function = CrmInvoiceParseFunction.NAME)
+ @NotNull(message = "鍏宠仈寮�绁ㄧ紪鍙蜂笉鑳戒负绌�")
+ private Long invoiceId;
+
@Schema(description = "鍥炴璁″垝缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@DiffLogField(name = "鍚堝悓", function = CrmReceivablePlanParseFunction.NAME)
private Long planId;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
index c24614a..6d4a2d0 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
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.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableReturnTypeEnum;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -57,6 +58,12 @@
*/
private Long contractId;
/**
+ * 鍏宠仈鐨勫紑绁ㄧ紪鍙�
+ *
+ * 鍏宠仈 {@link CrmInvoiceDO#getId()}锛屽彧鏈夊鎵归�氳繃涓斿凡涓婁紶鍙戠エ闄勪欢鐨勫紑绁ㄦ墠鍙叧鑱�
+ */
+ private Long invoiceId;
+ /**
* 璐熻矗浜虹紪鍙凤紝鍏宠仈 {@link AdminUserRespDTO#getId()}
*/
private Long ownerUserId;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java
index a5a814f..9dcbca0 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/invoice/CrmInvoiceMapper.java
@@ -76,10 +76,11 @@
if (CollUtil.isEmpty(contractIds)) {
return Collections.emptyMap();
}
- // SQL sum 鏌ヨ
+ // SQL sum 鏌ヨ锛堝鎵归�氳繃 + 瀹℃壒涓紝涓庡紑绁ㄩ槻瓒呮牎楠屽彛寰勪竴鑷达級
List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmInvoiceDO>()
.select("contract_id, SUM(price) AS total_price")
- .eq("audit_status", CrmAuditStatusEnum.APPROVE.getStatus())
+ .in("audit_status", CrmAuditStatusEnum.APPROVE.getStatus(),
+ CrmAuditStatusEnum.PROCESS.getStatus())
.groupBy("contract_id")
.in("contract_id", contractIds));
// 鑾峰緱閲戦
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
index 433648b..496afea 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
@@ -75,6 +75,27 @@
.in(CrmReceivableDO::getAuditStatus, auditStatuses));
}
+ default List<CrmReceivableDO> selectListByInvoiceIdAndStatus(Long invoiceId, Collection<Integer> auditStatuses) {
+ return selectList(new LambdaQueryWrapperX<CrmReceivableDO>()
+ .eq(CrmReceivableDO::getInvoiceId, invoiceId)
+ .in(CrmReceivableDO::getAuditStatus, auditStatuses));
+ }
+
+ default Map<Long, BigDecimal> selectReceivablePriceMapByInvoiceId(Collection<Long> invoiceIds) {
+ if (CollUtil.isEmpty(invoiceIds)) {
+ return Collections.emptyMap();
+ }
+ // SQL sum 鏌ヨ
+ List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmReceivableDO>()
+ .select("invoice_id, SUM(price) AS total_price")
+ .in("audit_status", CrmAuditStatusEnum.DRAFT.getStatus(), // 鑽夌 + 瀹℃壒涓� + 瀹℃壒閫氳繃
+ CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus())
+ .groupBy("invoice_id")
+ .in("invoice_id", invoiceIds));
+ // 鑾峰緱閲戦
+ return convertMap(result, obj -> (Long) obj.get("invoice_id"), obj -> (BigDecimal) obj.get("total_price"));
+ }
+
default Map<Long, BigDecimal> selectReceivablePriceMapByContractId(Collection<Long> contractIds) {
if (CollUtil.isEmpty(contractIds)) {
return Collections.emptyMap();
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 835b74a..d514bbe 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
@@ -128,6 +128,10 @@
ErrorCode INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_016_005, "鏇存柊寮�绁ㄥ鏍哥姸鎬佸け璐ワ紝鍘熷洜锛氬紑绁ㄤ笉鏄鏍镐腑鐘舵��");
ErrorCode INVOICE_CREATE_FAIL_CONTRACT_NOT_APPROVE = new ErrorCode(1_020_016_006, "鍒涘缓寮�绁ㄥけ璐ワ紝鍘熷洜锛氬悎鍚屼笉鏄鏍搁�氳繃鐘舵��");
ErrorCode INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_020_016_007, "鍒涘缓寮�绁ㄥけ璐ワ紝鍘熷洜锛氬紑绁ㄩ噾棰濊秴鍑哄悎鍚岄噾棰濓紝鐩墠鍙紑绁細{} 鍏�");
- ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ENOUGH = new ErrorCode(1_020_016_008, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍚堝悓绱寮�绁ㄩ噾棰濅笉瓒筹紝鐩墠鍙洖娆撅細{} 鍏�");
+ ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_NOT_EXISTS = new ErrorCode(1_020_016_008, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍏宠仈鐨勫紑绁ㄤ笉瀛樺湪");
+ ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_NOT_APPROVE = new ErrorCode(1_020_016_009, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍏宠仈鐨勫紑绁ㄤ笉鏄鎵归�氳繃鐘舵��");
+ ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ATTACHMENT = new ErrorCode(1_020_016_010, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍏宠仈鐨勫紑绁ㄦ湭涓婁紶鍙戠エ闄勪欢");
+ ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH = new ErrorCode(1_020_016_011, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍏宠仈鐨勫紑绁ㄤ笌鎵�閫夊悎鍚屼笉鍖归厤");
+ ErrorCode RECEIVABLE_CREATE_FAIL_INVOICE_PRICE_EXCEEDS = new ErrorCode(1_020_016_012, "鍒涘缓鍥炴澶辫触锛屽師鍥狅細鍥炴閲戦瓒呭嚭璇ュ紑绁ㄥ墿浣欏彲鍥炴閲戦锛岀洰鍓嶅彲鍥炴锛歿} 鍏�");
}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmInvoiceParseFunction.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmInvoiceParseFunction.java
new file mode 100644
index 0000000..d2a7600
--- /dev/null
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmInvoiceParseFunction.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.crm.framework.operatelog.core;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
+import cn.iocoder.yudao.module.crm.service.invoice.CrmInvoiceService;
+import com.mzt.logapi.service.IParseFunction;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * CRM 寮�绁ㄧ殑 {@link IParseFunction} 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Component
+@Slf4j
+public class CrmInvoiceParseFunction implements IParseFunction {
+
+ public static final String NAME = "getInvoiceById";
+
+ @Resource
+ private CrmInvoiceService invoiceService;
+
+ @Override
+ public boolean executeBefore() {
+ return false; // 涓嶅湪 _DIFF 涓娇鐢紝鏃犻渶鎻愬墠姹傚��
+ }
+
+ @Override
+ public String functionName() {
+ return NAME;
+ }
+
+ @Override
+ public String apply(Object value) {
+ if (StrUtil.isEmptyIfStr(value)) {
+ return "";
+ }
+ CrmInvoiceDO invoice = invoiceService.getInvoice(Long.parseLong(value.toString()));
+ return invoice == null ? "" : invoice.getNo();
+ }
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java
index 490be7c..0390851 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceService.java
@@ -121,7 +121,7 @@
List<CrmInvoiceDO> getInvoiceListByContractIdAndStatus(Long contractId, Collection<Integer> auditStatuses);
/**
- * 鑾峰緱鍚堝悓宸插紑绁ㄩ噾棰� Map锛堜粎瀹℃壒閫氳繃锛�
+ * 鑾峰緱鍚堝悓宸插紑绁ㄩ噾棰� Map锛堝鎵归�氳繃 + 瀹℃壒涓紝涓庡紑绁ㄩ槻瓒呮牎楠屽彛寰勪竴鑷达級
*
* @param contractIds 鍚堝悓缂栧彿
* @return 寮�绁ㄩ噾棰� Map
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
index 0844616..4d0fd42 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -119,10 +119,10 @@
}
private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
- // 1. 璁$畻鍓╀綑鍙紑绁ㄩ噾棰濓紝涓嶅寘鎷� reqVO 鑷韩
+ // 1. 璁$畻鍓╀綑鍙紑绁ㄩ噾棰濓紝涓嶅寘鎷� reqVO 鑷韩锛堝鎵逛腑涔熷崰鐢ㄩ搴︼紝闃叉绱寮�绁ㄨ秴杩囧悎鍚岄噾棰濓級
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
- List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
+ List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
if (reqVO.getId() != null) {
invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
}
@@ -232,6 +232,10 @@
throw exception(INVOICE_SUBMIT_FAIL_NOT_DRAFT);
}
+ // 1.1 閲嶆柊鏍¢獙鍙紑绁ㄩ噾棰濓紝闃叉绱寮�绁ㄨ秴杩囧悎鍚岄噾棰濓紙鑽夌鏈熷悗鍙兘鏈夊叾浠栧崟杩涘叆瀹℃壒涓級
+ validateInvoicePriceExceedsLimit(new CrmInvoiceSaveReqVO()
+ .setId(invoice.getId()).setContractId(invoice.getContractId()).setPrice(invoice.getPrice()));
+
// 2. 鍒涘缓寮�绁ㄥ鎵规祦绋嬪疄渚�
String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id)));
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
index bd0d5dc..1713c29 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
@@ -123,6 +123,14 @@
Map<Long, BigDecimal> getReceivablePriceMapByContractId(Collection<Long> contractIds);
/**
+ * 鑾峰緱寮�绁ㄥ凡鍥炴閲戦 Map锛堣崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛�
+ *
+ * @param invoiceIds 寮�绁ㄧ紪鍙�
+ * @return 宸插洖娆鹃噾棰� Map
+ */
+ Map<Long, BigDecimal> getReceivablePriceMapByInvoiceId(Collection<Long> invoiceIds);
+
+ /**
* 鏍规嵁鍚堝悓缂栧彿鏌ヨ鍥炴鏁伴噺
*
* @param contractId 鍚堝悓缂栧彿
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
index 0652c25..f655977 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -91,13 +91,15 @@
@LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_CREATE_SUB_TYPE, bizNo = "{{#receivable.id}}",
success = CRM_RECEIVABLE_CREATE_SUCCESS)
public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
- // 1.1 鏍¢獙鍙洖娆鹃噾棰濊秴杩囦笂闄�
- validateReceivablePriceExceedsLimit(createReqVO);
- // 1.2 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃绱宸插紑绁ㄩ噾棰濓紙鍏堝紑绁ㄥ悗鍥炴锛�
- validateReceivablePriceExceedsInvoiceLimit(createReqVO);
- // 1.3 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
+ // 1.1 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
validateRelationDataExists(createReqVO);
- // 1.4 鐢熸垚鍥炴缂栧彿
+ // 1.2 鏍¢獙鍏宠仈寮�绁ㄥ悎鏍硷紙瀹℃壒閫氳繃 + 宸蹭笂浼犲彂绁ㄩ檮浠� + 涓庡悎鍚屽尮閰嶏級
+ validateInvoiceForReceivable(createReqVO);
+ // 1.3 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃鍚堝悓鍓╀綑鍙洖娆鹃噾棰�
+ validateReceivablePriceExceedsLimit(createReqVO);
+ // 1.4 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃璇ュ紑绁ㄥ墿浣欏彲鍥炴閲戦锛堝厛寮�绁ㄥ悗鍥炴锛�
+ validateReceivablePriceExceedsInvoiceRemaining(createReqVO);
+ // 1.5 鐢熸垚鍥炴缂栧彿
String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
if (receivableMapper.selectByNo(no) != null) {
throw exception(RECEIVABLE_NO_EXISTS);
@@ -128,8 +130,10 @@
private void validateReceivablePriceExceedsLimit(CrmReceivableSaveReqVO reqVO) {
// 1. 璁$畻鍓╀綑鍙��娆鹃噾棰濓紝涓嶅寘鎷� reqVO 鑷韩
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
+ // 缁熻鍙e緞锛氳崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛堜笌寮�绁ㄧ骇鏍¢獙銆佺粺璁℃柟娉� selectReceivablePriceMapByContractId 淇濇寔涓�鑷达級
List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
- Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
+ Arrays.asList(CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus(),
+ CrmAuditStatusEnum.APPROVE.getStatus()));
if (reqVO.getId() != null) {
receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
}
@@ -142,33 +146,52 @@
}
/**
- * 鏍¢獙绱鍥炴閲戦涓嶈秴杩囩疮璁″凡寮�绁ㄤ笖宸蹭笂浼犲彂绁ㄩ檮浠剁殑閲戦锛堝厛寮�绁ㄥ悗鍥炴锛�
+ * 鏍¢獙鍏宠仈寮�绁ㄥ悎鏍硷細瀛樺湪銆佸鎵归�氳繃銆佸凡涓婁紶鍙戠エ闄勪欢銆佷笌鎵�閫夊悎鍚屼竴鑷�
*/
- private void validateReceivablePriceExceedsInvoiceLimit(CrmReceivableSaveReqVO reqVO) {
- // 1. 璁$畻鍚堝悓绱鍥炴閲戦锛堜笉鍚湰娆★紝缁熻鍙e緞鍚屽悎鍚岄噾棰濇牎楠岋細瀹℃壒閫氳繃 + 瀹℃壒涓級
- List<CrmReceivableDO> receivables = receivableMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
- Arrays.asList(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
+ private void validateInvoiceForReceivable(CrmReceivableSaveReqVO reqVO) {
+ // 1. 鏍¢獙寮�绁ㄥ瓨鍦�
+ CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId());
+ if (invoice == null) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_EXISTS);
+ }
+ // 2. 鏍¢獙寮�绁ㄥ鎵归�氳繃
+ if (ObjectUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_APPROVE);
+ }
+ // 3. 鏍¢獙寮�绁ㄥ凡涓婁紶鍙戠エ闄勪欢
+ if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(
+ StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ATTACHMENT);
+ }
+ // 4. 鏍¢獙寮�绁ㄤ笌鎵�閫夊悎鍚屼竴鑷�
+ if (ObjectUtil.notEqual(invoice.getContractId(), reqVO.getContractId())) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH);
+ }
+ // 5. 鏍¢獙寮�绁ㄤ笌鎵�閫夊悎鍚屽鎴蜂竴鑷达紙闃插尽鑴忔暟鎹細鍚堝悓-寮�绁�-鍥炴涓�瀵瑰瀵瑰锛屽紑绁ㄥ繀椤诲睘浜庤鍚堝悓瀹㈡埛锛�
+ if (reqVO.getCustomerId() != null && ObjectUtil.notEqual(invoice.getCustomerId(), reqVO.getCustomerId())) {
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_CONTRACT_MISMATCH);
+ }
+ }
+
+ /**
+ * 鏍¢獙绱鍥炴閲戦涓嶈秴杩囪寮�绁ㄥ墿浣欏彲鍥炴閲戦锛堝厛寮�绁ㄥ悗鍥炴锛�
+ */
+ private void validateReceivablePriceExceedsInvoiceRemaining(CrmReceivableSaveReqVO reqVO) {
+ // 1. 璁$畻璇ュ紑绁ㄧ疮璁″洖娆鹃噾棰濓紙涓嶅惈鏈锛岀粺璁″彛寰勫悓鍚堝悓閲戦鏍¢獙锛氳崏绋� + 瀹℃壒涓� + 瀹℃壒閫氳繃锛�
+ CrmInvoiceDO invoice = invoiceService.getInvoice(reqVO.getInvoiceId());
+ List<CrmReceivableDO> receivables = receivableMapper.selectListByInvoiceIdAndStatus(reqVO.getInvoiceId(),
+ Arrays.asList(CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus(),
+ CrmAuditStatusEnum.APPROVE.getStatus()));
if (reqVO.getId() != null) {
receivables.removeIf(receivable -> ObjectUtil.equal(receivable.getId(), reqVO.getId()));
}
BigDecimal receivablePriceSum = CollectionUtils.getSumValue(receivables, CrmReceivableDO::getPrice,
BigDecimal::add, BigDecimal.ZERO);
- // 2. 璁$畻鍚堝悓涓嬨�屽鎵归�氳繃涓斿凡涓婁紶鍙戠エ闄勪欢銆嶇殑绱寮�绁ㄩ噾棰�
- List<CrmInvoiceDO> invoices = invoiceService.getInvoiceListByContractIdAndStatus(reqVO.getContractId(),
- List.of(CrmAuditStatusEnum.APPROVE.getStatus()));
- BigDecimal invoicePriceSum = BigDecimal.ZERO;
- for (CrmInvoiceDO invoice : invoices) {
- if (CollUtil.isNotEmpty(storageAttachmentApi.listAttachments(
- StorageRecordTypeEnum.CRM_INVOICE.getType(), invoice.getId()))) {
- invoicePriceSum = invoicePriceSum.add(invoice.getPrice());
- }
- }
-
- // 3. 鏍¢獙鏈鍥炴閲戦涓嶈秴杩囧墿浣欏彲鍥炴閲戦锛堝凡寮�绁ㄩ噾棰� - 宸插洖娆鹃噾棰濓級
- BigDecimal availablePrice = invoicePriceSum.subtract(receivablePriceSum);
+ // 2. 鏍¢獙鏈鍥炴閲戦涓嶈秴杩囪寮�绁ㄥ墿浣欏彲鍥炴閲戦锛堝紑绁ㄩ噾棰� - 宸插洖娆鹃噾棰濓級
+ BigDecimal availablePrice = invoice.getPrice().subtract(receivablePriceSum);
if (reqVO.getPrice().compareTo(availablePrice) > 0) {
- throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_NOT_ENOUGH, availablePrice);
+ throw exception(RECEIVABLE_CREATE_FAIL_INVOICE_PRICE_EXCEEDS, availablePrice);
}
}
@@ -201,17 +224,23 @@
@CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
public void updateReceivable(CrmReceivableSaveReqVO updateReqVO) {
Assert.notNull(updateReqVO.getId(), "鍥炴缂栧彿涓嶈兘涓虹┖");
- updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 涓嶅厑璁镐慨鏀圭殑瀛楁
+ updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 涓嶅厑璁镐慨鏀圭殑瀛楁锛坕nvoiceId 鍏佽淇敼锛�
// 1.1 鏍¢獙瀛樺湪
CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId());
updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId())
.setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 璁剧疆宸插瓨鍦ㄧ殑鍊�
- // 1.2 鏍¢獙鍙洖娆鹃噾棰濊秴杩囦笂闄�
+ // 1.2 鑻ユ湭浼犻�掑叧鑱斿紑绁紝娌跨敤鏃у��
+ if (updateReqVO.getInvoiceId() == null) {
+ updateReqVO.setInvoiceId(oldReceivable.getInvoiceId());
+ }
+ // 1.3 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃鍚堝悓鍓╀綑鍙洖娆鹃噾棰�
validateReceivablePriceExceedsLimit(updateReqVO);
- // 1.3 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃绱宸插紑绁ㄩ噾棰濓紙鍏堝紑绁ㄥ悗鍥炴锛�
- validateReceivablePriceExceedsInvoiceLimit(updateReqVO);
+ // 1.4 鏍¢獙鍏宠仈寮�绁ㄥ悎鏍�
+ validateInvoiceForReceivable(updateReqVO);
+ // 1.5 鏍¢獙鍙洖娆鹃噾棰濅笉瓒呰繃璇ュ紑绁ㄥ墿浣欏彲鍥炴閲戦
+ validateReceivablePriceExceedsInvoiceRemaining(updateReqVO);
- // 1.4 鍙湁鑽夌銆佸鎵逛腑锛屽彲浠ョ紪杈戯紱
+ // 1.6 鍙湁鑽夌銆佸鎵逛腑锛屽彲浠ョ紪杈戯紱
if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
@@ -346,6 +375,11 @@
}
@Override
+ public Map<Long, BigDecimal> getReceivablePriceMapByInvoiceId(Collection<Long> invoiceIds) {
+ return receivableMapper.selectReceivablePriceMapByInvoiceId(invoiceIds);
+ }
+
+ @Override
public Long getReceivableCountByContractId(Long contractId) {
return receivableMapper.selectCountByContractId(contractId);
}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java
index 718419a..21fe41e 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java
@@ -12,13 +12,16 @@
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentSaveReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -53,6 +56,8 @@
private SrmSupplierApi srmSupplierApi;
@Resource
private ErpAccountService accountService;
+ @Resource
+ private ErpPurchaseInvoiceService invoiceService;
@Resource
private AdminUserApi adminUserApi;
@@ -102,9 +107,23 @@
return success(null);
}
List<ErpFinancePaymentItemDO> paymentItemList = financePaymentService.getFinancePaymentItemListByPaymentId(id);
- return success(BeanUtils.toBean(payment, ErpFinancePaymentRespVO.class, financePaymentVO ->
- financePaymentVO.setItems(BeanUtils.toBean(paymentItemList, ErpFinancePaymentRespVO.Item.class))
- .setAttachmentList(storageAttachmentApi.listAttachments("erp_finance_payment", id))));
+ return success(buildFinancePaymentVO(payment, paymentItemList));
+ }
+
+ private ErpFinancePaymentRespVO buildFinancePaymentVO(ErpFinancePaymentDO payment,
+ List<ErpFinancePaymentItemDO> paymentItemList) {
+ ErpPurchaseInvoiceRespVO invoiceVO = null;
+ if (payment.getInvoiceId() != null) {
+ ErpPurchaseInvoiceDO invoice = invoiceService.getPurchaseInvoice(payment.getInvoiceId());
+ if (invoice != null) {
+ invoiceVO = BeanUtils.toBean(invoice, ErpPurchaseInvoiceRespVO.class);
+ }
+ }
+ ErpPurchaseInvoiceRespVO invoiceFinal = invoiceVO;
+ return BeanUtils.toBean(payment, ErpFinancePaymentRespVO.class, financePaymentVO ->
+ financePaymentVO.setInvoice(invoiceFinal)
+ .setItems(BeanUtils.toBean(paymentItemList, ErpFinancePaymentRespVO.Item.class))
+ .setAttachmentList(storageAttachmentApi.listAttachments("erp_finance_payment", payment.getId())));
}
@GetMapping("/page")
@@ -145,6 +164,9 @@
// 1.4 绠$悊鍛樹俊鎭�
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getFinanceUserId())));
+ // 1.5 鏉ョエ淇℃伅
+ Map<Long, ErpPurchaseInvoiceDO> invoiceMap = invoiceService.getPurchaseInvoiceMap(
+ convertSet(pageResult.getList(), ErpFinancePaymentDO::getInvoiceId));
// 2. 寮�濮嬫嫾鎺�
return BeanUtils.toBean(pageResult, ErpFinancePaymentRespVO.class, payment -> {
payment.setItems(BeanUtils.toBean(financePaymentItemMap.get(payment.getId()), ErpFinancePaymentRespVO.Item.class));
@@ -152,6 +174,8 @@
MapUtils.findAndThen(accountMap, payment.getAccountId(), account -> payment.setAccountName(account.getName()));
MapUtils.findAndThen(userMap, Long.parseLong(payment.getCreator()), user -> payment.setCreatorName(user.getNickname()));
MapUtils.findAndThen(userMap, payment.getFinanceUserId(), user -> payment.setFinanceUserName(user.getNickname()));
+ MapUtils.findAndThen(invoiceMap, payment.getInvoiceId(), invoice ->
+ payment.setInvoice(BeanUtils.toBean(invoice, ErpPurchaseInvoiceRespVO.class)));
});
}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
index 14ab4c1..4bc732e 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
@@ -2,6 +2,7 @@
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceRespVO;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
@@ -38,6 +39,11 @@
@Schema(description = "渚涘簲鍟嗗悕绉�", requiredMode = Schema.RequiredMode.REQUIRED, example = "灏忕暘鑼勫叕鍙�")
private String supplierName;
+ @Schema(description = "鍏宠仈鐨勬潵绁ㄧ紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
+ private Long invoiceId;
+ @Schema(description = "鍏宠仈鐨勬潵绁ㄤ俊鎭�")
+ private ErpPurchaseInvoiceRespVO invoice;
+
@Schema(description = "浠樻璐︽埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "28989")
private Long accountId;
@Schema(description = "浠樻璐︽埛鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "寮犱笁")
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java
index 2e3bac0..8a981b4 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java
@@ -28,6 +28,10 @@
@NotNull(message = "渚涘簲鍟嗙紪鍙蜂笉鑳戒负绌�")
private Long supplierId;
+ @Schema(description = "鍏宠仈鐨勬潵绁ㄧ紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
+ @NotNull(message = "鍏宠仈鏉ョエ缂栧彿涓嶈兘涓虹┖")
+ private Long invoiceId;
+
@Schema(description = "浠樻璐︽埛缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "28989")
@NotNull(message = "浠樻璐︽埛缂栧彿涓嶈兘涓虹┖")
private Long accountId;
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java
new file mode 100644
index 0000000..8270f23
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java
@@ -0,0 +1,234 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase;
+
+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.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
+import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseOrderService;
+import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
+import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
+import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+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.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+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.pojo.PageParam.PAGE_SIZE_NONE;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - ERP 閲囪喘鏉ョエ")
+@RestController
+@RequestMapping("/erp/purchase-invoice")
+@Validated
+public class ErpPurchaseInvoiceController {
+
+ @Resource
+ private ErpPurchaseInvoiceService invoiceService;
+ @Resource
+ private ErpPurchaseOrderService purchaseOrderService;
+ @Resource
+ private ErpFinancePaymentService financePaymentService;
+ @Resource
+ private SrmSupplierApi srmSupplierApi;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+ @Resource
+ private StorageAttachmentApi storageAttachmentApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓鏉ョエ")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:create')")
+ public CommonResult<Long> createPurchaseInvoice(@Valid @RequestBody ErpPurchaseInvoiceSaveReqVO createReqVO) {
+ return success(invoiceService.createPurchaseInvoice(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊鏉ョエ")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:update')")
+ public CommonResult<Boolean> updatePurchaseInvoice(@Valid @RequestBody ErpPurchaseInvoiceSaveReqVO updateReqVO) {
+ invoiceService.updatePurchaseInvoice(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎鏉ョエ")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:delete')")
+ public CommonResult<Boolean> deletePurchaseInvoice(@RequestParam("id") Long id) {
+ invoiceService.deletePurchaseInvoice(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱鏉ョエ")
+ @Parameter(name = "id", description = "缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
+ public CommonResult<ErpPurchaseInvoiceRespVO> getPurchaseInvoice(@RequestParam("id") Long id) {
+ ErpPurchaseInvoiceDO invoice = invoiceService.getPurchaseInvoice(id);
+ return success(buildInvoiceDetail(invoice));
+ }
+
+ private ErpPurchaseInvoiceRespVO buildInvoiceDetail(ErpPurchaseInvoiceDO invoice) {
+ if (invoice == null) {
+ return null;
+ }
+ return buildInvoiceDetailList(Collections.singletonList(invoice)).get(0);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱鏉ョエ鍒嗛〉")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
+ public CommonResult<PageResult<ErpPurchaseInvoiceRespVO>> getPurchaseInvoicePage(@Valid ErpPurchaseInvoicePageReqVO pageReqVO) {
+ PageResult<ErpPurchaseInvoiceDO> pageResult = invoiceService.getPurchaseInvoicePage(pageReqVO);
+ return success(new PageResult<>(buildInvoiceDetailList(pageResult.getList()), pageResult.getTotal()));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "瀵煎嚭鏉ョエ Excel")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:export')")
+ @ApiAccessLog(operateType = EXPORT)
+ public void exportPurchaseInvoiceExcel(@Valid ErpPurchaseInvoicePageReqVO pageReqVO,
+ HttpServletResponse response) throws IOException {
+ pageReqVO.setPageSize(PAGE_SIZE_NONE);
+ List<ErpPurchaseInvoiceDO> list = invoiceService.getPurchaseInvoicePage(pageReqVO).getList();
+ // 瀵煎嚭 Excel
+ ExcelUtils.write(response, "鏉ョエ.xls", "鏁版嵁", ErpPurchaseInvoiceRespVO.class,
+ buildInvoiceDetailList(list));
+ }
+
+ private List<ErpPurchaseInvoiceRespVO> buildInvoiceDetailList(List<ErpPurchaseInvoiceDO> invoiceList) {
+ if (CollUtil.isEmpty(invoiceList)) {
+ return Collections.emptyList();
+ }
+ // 1.1 鑾峰彇渚涘簲鍟嗗垪琛�
+ Map<Long, SrmSupplierRespDTO> supplierMap = srmSupplierApi.getSupplierList(
+ convertSet(invoiceList, ErpPurchaseInvoiceDO::getSupplierId)).getCheckedData().stream()
+ .collect(java.util.stream.Collectors.toMap(SrmSupplierRespDTO::getId, v -> v, (a, b) -> a));
+ // 1.2 鑾峰彇閲囪喘璁㈠崟鍒楄〃
+ Map<Long, ErpPurchaseOrderDO> orderMap = convertMap(
+ purchaseOrderService.getPurchaseOrderList(convertList(invoiceList, ErpPurchaseInvoiceDO::getPurchaseOrderId)),
+ ErpPurchaseOrderDO::getId);
+ // 1.3 鑾峰彇鍒涘缓浜哄垪琛�
+ Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+ convertSet(invoiceList, invoice -> NumberUtils.parseLong(invoice.getCreator())));
+ // 1.4 鑾峰緱姣忓紶鏉ョエ宸蹭粯娆鹃噾棰濓紙鏈鏍� + 瀹℃壒涓� + 宸插鏍革級
+ Map<Long, BigDecimal> paymentPriceMap = financePaymentService.getPaymentPriceMapByInvoiceId(
+ convertSet(invoiceList, ErpPurchaseInvoiceDO::getId));
+ // 2. 鎷兼帴缁撴灉
+ return BeanUtils.toBean(invoiceList, ErpPurchaseInvoiceRespVO.class, (invoiceVO) -> {
+ // 2.1 鎷兼帴渚涘簲鍟嗗悕绉�
+ findAndThen(supplierMap, invoiceVO.getSupplierId(), supplier -> invoiceVO.setSupplierName(supplier.getName()));
+ // 2.2 鎷兼帴閲囪喘璁㈠崟淇℃伅
+ findAndThen(orderMap, invoiceVO.getPurchaseOrderId(), order ->
+ invoiceVO.setOrder(BeanUtils.toBean(order, ErpPurchaseOrderRespVO.class)));
+ // 2.3 鎷兼帴鍒涘缓浜哄悕绉�
+ MapUtils.findAndThen(userMap, NumberUtils.parseLong(invoiceVO.getCreator()),
+ user -> invoiceVO.setCreatorName(user.getNickname()));
+ // 2.4 鎷兼帴鍙戠エ闄勪欢 + 鏄惁宸蹭笂浼犻檮浠�
+ invoiceVO.setAttachmentList(storageAttachmentApi.listAttachments(
+ ErpPurchaseInvoiceServiceImpl.PURCHASE_INVOICE_RECORD_TYPE, invoiceVO.getId()));
+ invoiceVO.setHasAttachment(CollUtil.isNotEmpty(invoiceVO.getAttachmentList()));
+ // 2.5 鎷兼帴鍓╀綑鍙粯閲戦
+ invoiceVO.setRemainingInvoicePrice(invoiceVO.getPrice().subtract(
+ paymentPriceMap.getOrDefault(invoiceVO.getId(), BigDecimal.ZERO)));
+ });
+ }
+
+ @PutMapping("/submit")
+ @Operation(summary = "鎻愪氦鏉ョエ瀹℃壒")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:update')")
+ public CommonResult<Boolean> submitPurchaseInvoice(@RequestParam("id") Long id) {
+ invoiceService.submitPurchaseInvoice(id, getLoginUserId());
+ return success(true);
+ }
+
+ @GetMapping("/audit-count")
+ @Operation(summary = "鑾峰緱寰呭鏍告潵绁ㄦ暟閲�")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
+ public CommonResult<Long> getAuditInvoiceCount() {
+ return success(invoiceService.getAuditInvoiceCount());
+ }
+
+ @GetMapping("/simple-list")
+ @Operation(summary = "鑾峰緱鏉ョエ绮剧畝鍒楄〃", description = "鍙寘鍚鏍搁�氳繃涓斿凡涓婁紶鍙戠エ闄勪欢鐨勬潵绁紝涓昏鐢ㄤ簬浠樻鍒涘缓鏃堕�夋嫨鍏宠仈鏉ョエ")
+ @Parameter(name = "supplierId", description = "渚涘簲鍟嗙紪鍙�", required = true)
+ @Parameter(name = "purchaseOrderId", description = "閲囪喘璁㈠崟缂栧彿锛堝彲閫夛紝鐢ㄤ簬鎸夎鍗曡繃婊わ級")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
+ public CommonResult<List<ErpPurchaseInvoiceRespVO>> getInvoiceSimpleList(
+ @RequestParam("supplierId") Long supplierId,
+ @RequestParam(value = "purchaseOrderId", required = false) Long purchaseOrderId) {
+ List<ErpPurchaseInvoiceDO> invoices;
+ if (purchaseOrderId != null) {
+ invoices = invoiceService.getPurchaseInvoiceListByOrderIdAndStatus(purchaseOrderId,
+ List.of(ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
+ } else {
+ invoices = invoiceService.getPurchaseInvoiceListBySupplierIdAndStatus(supplierId,
+ List.of(ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
+ }
+ if (CollUtil.isEmpty(invoices)) {
+ return success(Collections.emptyList());
+ }
+ // 杩囨护锛氬凡涓婁紶鍙戠エ闄勪欢
+ invoices.removeIf(invoice -> CollUtil.isEmpty(storageAttachmentApi.listAttachments(
+ ErpPurchaseInvoiceServiceImpl.PURCHASE_INVOICE_RECORD_TYPE, invoice.getId())));
+ if (CollUtil.isEmpty(invoices)) {
+ return success(Collections.emptyList());
+ }
+ // 璁$畻姣忓紶鏉ョエ宸蹭粯娆鹃噾棰濓紙鏈鏍� + 瀹℃壒涓� + 宸插鏍革紝鍙e緞涓庡垱寤轰粯娆炬牎楠屼竴鑷达級
+ Map<Long, BigDecimal> paymentPriceMap = financePaymentService.getPaymentPriceMapByInvoiceId(
+ convertSet(invoices, ErpPurchaseInvoiceDO::getId));
+ // 杩囨护锛氬墿浣欏彲浠橀噾棰� > 0 鎵嶅彲閫�
+ List<ErpPurchaseInvoiceRespVO> result = new ArrayList<>();
+ for (ErpPurchaseInvoiceDO invoice : invoices) {
+ BigDecimal remainingPrice = invoice.getPrice()
+ .subtract(paymentPriceMap.getOrDefault(invoice.getId(), BigDecimal.ZERO));
+ if (remainingPrice.compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ result.add(new ErpPurchaseInvoiceRespVO() // 鍙繑鍥炵簿绠�瀛楁
+ .setId(invoice.getId()).setNo(invoice.getNo()).setInvoiceNo(invoice.getInvoiceNo())
+ .setInvoiceTitle(invoice.getInvoiceTitle()).setPrice(invoice.getPrice())
+ .setSupplierId(invoice.getSupplierId()).setPurchaseOrderId(invoice.getPurchaseOrderId())
+ .setAuditStatus(invoice.getAuditStatus()).setHasAttachment(true)
+ .setRemainingInvoicePrice(remainingPrice));
+ }
+ return success(result);
+ }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java
new file mode 100644
index 0000000..e92270c
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "绠$悊鍚庡彴 - ERP 閲囪喘鏉ョエ鍒嗛〉 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpPurchaseInvoicePageReqVO extends PageParam {
+
+ @Schema(description = "鏉ョエ缂栧彿")
+ private String no;
+
+ @Schema(description = "渚涘簲鍟嗙紪鍙�", example = "29399")
+ private Long supplierId;
+
+ @Schema(description = "閲囪喘璁㈠崟缂栧彿", example = "17386")
+ private Long purchaseOrderId;
+
+ @Schema(description = "瀹℃壒鐘舵��", example = "20")
+ @InEnum(ErpPurchaseInvoiceAuditStatusEnum.class)
+ private Integer auditStatus;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java
new file mode 100644
index 0000000..3d84083
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java
@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
+import cn.iocoder.yudao.module.erp.enums.DictTypeConstants;
+import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - ERP 閲囪喘鏉ョエ Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpPurchaseInvoiceRespVO {
+
+ @Schema(description = "缂栧彿", example = "25787")
+ @ExcelProperty("缂栧彿")
+ private Long id;
+
+ @Schema(description = "鏉ョエ缂栧彿", example = "FPD888")
+ @ExcelProperty("鏉ョエ缂栧彿")
+ private String no;
+
+ @Schema(description = "渚涘簲鍟嗙紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "29399")
+ private Long supplierId;
+ @Schema(description = "渚涘簲鍟嗗悕绉�", requiredMode = Schema.RequiredMode.REQUIRED, example = "灏忕暘鑼勫叕鍙�")
+ @ExcelProperty("渚涘簲鍟嗗悕绉�")
+ private String supplierName;
+
+ @Schema(description = "閲囪喘璁㈠崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+ @ExcelProperty("閲囪喘璁㈠崟缂栧彿")
+ private Long purchaseOrderId;
+ @Schema(description = "閲囪喘璁㈠崟淇℃伅")
+ private ErpPurchaseOrderRespVO order;
+
+ @Schema(description = "鍙戠エ鍙风爜", example = "02564178")
+ @ExcelProperty("鍙戠エ鍙风爜")
+ private String invoiceNo;
+
+ @Schema(description = "鍙戠エ鎶ご", example = "XX绉戞妧鏈夐檺鍏徃")
+ @ExcelProperty("鍙戠エ鎶ご")
+ private String invoiceTitle;
+
+ @Schema(description = "鏉ョエ閲戦锛屽崟浣嶏細鍏�", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+ @ExcelProperty("鏉ョエ閲戦")
+ private BigDecimal price;
+
+ @Schema(description = "鏉ョエ鏃ユ湡", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+ @ExcelProperty("鏉ョエ鏃ユ湡")
+ private LocalDateTime invoiceTime;
+
+ @Schema(description = "瀹℃壒鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @ExcelProperty(value = "瀹℃壒鐘舵��", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.AUDIT_STATUS)
+ private Integer auditStatus;
+
+ @Schema(description = "宸ヤ綔娴佺紪鍙�", example = "1043")
+ @ExcelProperty("宸ヤ綔娴佺紪鍙�")
+ private String processInstanceId;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ @ExcelProperty("澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍙戠エ闄勪欢鍒楄〃")
+ private List<StorageBlobRespDTO> attachmentList;
+
+ @Schema(description = "鏄惁宸蹭笂浼犲彂绁ㄩ檮浠�", example = "true")
+ private Boolean hasAttachment;
+
+ @Schema(description = "鍓╀綑鍙粯閲戦锛岀瓑浜� 鏉ョエ閲戦 - 璇ユ潵绁ㄥ凡浠樻閲戦锛堟湭瀹℃牳+瀹℃壒涓�+宸插鏍革級", example = "1000.00")
+ private BigDecimal remainingInvoicePrice;
+
+ @Schema(description = "鍒涘缓浜�", example = "25682")
+ private String creator;
+ @Schema(description = "鍒涘缓浜哄悕绉�", example = "test")
+ @ExcelProperty("鍒涘缓浜哄悕绉�")
+ private String creatorName;
+
+ @Schema(description = "鍒涘缓鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+ @Schema(description = "鏇存柊鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("鏇存柊鏃堕棿")
+ private LocalDateTime updateTime;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceSaveReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceSaveReqVO.java
new file mode 100644
index 0000000..77a2916
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceSaveReqVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - ERP 閲囪喘鏉ョエ鏂板/淇敼 Request VO")
+@Data
+public class ErpPurchaseInvoiceSaveReqVO {
+
+ @Schema(description = "缂栧彿", example = "25787")
+ private Long id;
+
+ @Schema(description = "渚涘簲鍟嗙紪鍙�", example = "29399")
+ private Long supplierId; // 璇ュ瓧娈典笉閫氳繃鍓嶇浼犻�掞紝鑰屾槸 purchaseOrderId 鏌ヨ鍑烘潵璁剧疆杩涘幓
+
+ @Schema(description = "閲囪喘璁㈠崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+ @NotNull(message = "閲囪喘璁㈠崟缂栧彿涓嶈兘涓虹┖")
+ private Long purchaseOrderId;
+
+ @Schema(description = "鍙戠エ鍙风爜", example = "02564178")
+ private String invoiceNo;
+
+ @Schema(description = "鍙戠エ鎶ご", example = "XX绉戞妧鏈夐檺鍏徃")
+ private String invoiceTitle;
+
+ @Schema(description = "鏉ョエ閲戦锛屽崟浣嶏細鍏�", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+ @NotNull(message = "鏉ョエ閲戦涓嶈兘涓虹┖")
+ private BigDecimal price;
+
+ @Schema(description = "鏉ョエ鏃ユ湡", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+ @NotNull(message = "鏉ョエ鏃ユ湡涓嶈兘涓虹┖")
+ private LocalDateTime invoiceTime;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍙戠エ闄勪欢 BlobId 鍒楄〃", example = "[1, 2, 3]")
+ private List<Long> blobIds;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java
index dea7913..8453b75 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.finance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -57,6 +58,12 @@
*/
private Long supplierId;
/**
+ * 鍏宠仈鐨勬潵绁ㄧ紪鍙�
+ *
+ * 鍏宠仈 {@link ErpPurchaseInvoiceDO#getId()}
+ */
+ private Long invoiceId;
+ /**
* 浠樻璐︽埛缂栧彿
*
* 鍏宠仈 {@link ErpAccountDO#getId()}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java
new file mode 100644
index 0000000..f86231c
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java
@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
+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;
+
+/**
+ * ERP 閲囪喘鏉ョエ DO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("erp_purchase_invoice")
+@KeySequence("erp_purchase_invoice_seq") // 鐢ㄤ簬 Oracle銆丳ostgreSQL銆並ingbase銆丏B2銆丠2 鏁版嵁搴撶殑涓婚敭鑷銆傚鏋滄槸 MySQL 绛夋暟鎹簱锛屽彲涓嶅啓銆�
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseInvoiceDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 鏉ョエ缂栧彿
+ */
+ private String no;
+ /**
+ * 渚涘簲鍟嗙紪鍙�
+ *
+ * 鍏宠仈 {@link ErpSupplierDO#getId()}
+ */
+ private Long supplierId;
+ /**
+ * 閲囪喘璁㈠崟缂栧彿
+ *
+ * 鍏宠仈 {@link ErpPurchaseOrderDO#getId()}
+ */
+ private Long purchaseOrderId;
+ /**
+ * 鍙戠エ鍙风爜
+ */
+ private String invoiceNo;
+ /**
+ * 鍙戠エ鎶ご
+ */
+ private String invoiceTitle;
+ /**
+ * 鏉ョエ閲戦锛屽崟浣嶏細鍏�
+ */
+ private BigDecimal price;
+ /**
+ * 鏉ョエ鏃ユ湡
+ */
+ private LocalDateTime invoiceTime;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+ /**
+ * 宸ヤ綔娴佺紪鍙�
+ *
+ * 鍏宠仈 ProcessInstance 鐨� id 灞炴��
+ */
+ private String processInstanceId;
+ /**
+ * 瀹℃壒鐘舵��
+ *
+ * 鏋氫妇 {@link ErpPurchaseInvoiceAuditStatusEnum}
+ */
+ private Integer auditStatus;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
index 0c884d6..b08d529 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
@@ -1,13 +1,25 @@
package cn.iocoder.yudao.module.erp.dal.mysql.finance;
+import cn.hutool.core.collection.CollUtil;
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.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* ERP 浠樻鍗� Mapper
@@ -45,4 +57,31 @@
return selectOne(ErpFinancePaymentDO::getNo, no);
}
+ default List<ErpFinancePaymentDO> selectListByInvoiceIdAndStatus(Long invoiceId, Collection<Integer> statuses) {
+ return selectList(new LambdaQueryWrapperX<ErpFinancePaymentDO>()
+ .eq(ErpFinancePaymentDO::getInvoiceId, invoiceId)
+ .in(ErpFinancePaymentDO::getStatus, statuses));
+ }
+
+ /**
+ * 缁熻姣忓紶鏉ョエ宸蹭粯娆鹃噾棰濓紙鏈鏍� + 瀹℃壒涓� + 宸插鏍革紝鎺掗櫎浣滃簾锛夛紝鐢ㄤ簬鏍¢獙鍓╀綑鍙粯閲戦
+ *
+ * @param invoiceIds 鏉ョエ缂栧彿鍒楄〃
+ * @return key 涓烘潵绁ㄧ紪鍙凤紝value 涓哄凡浠樻閲戦
+ */
+ default Map<Long, BigDecimal> selectPaymentPriceMapByInvoiceId(Collection<Long> invoiceIds) {
+ if (CollUtil.isEmpty(invoiceIds)) {
+ return Collections.emptyMap();
+ }
+ // SQL sum 鏌ヨ
+ List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpFinancePaymentDO>()
+ .select("invoice_id, SUM(payment_price) AS total_payment_price")
+ .in("status", ErpAuditStatus.PROCESS.getStatus(), // 鏈鏍� + 瀹℃壒涓� + 宸插鏍�
+ ErpAuditStatus.APPROVING.getStatus(), ErpAuditStatus.APPROVE.getStatus())
+ .groupBy("invoice_id")
+ .in("invoice_id", invoiceIds));
+ // 鑾峰緱閲戦
+ return convertMap(result, obj -> (Long) obj.get("invoice_id"), obj -> (BigDecimal) obj.get("total_payment_price"));
+ }
+
}
\ No newline at end of file
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java
new file mode 100644
index 0000000..2c9b681
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+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.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 閲囪喘鏉ョエ Mapper
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface ErpPurchaseInvoiceMapper extends BaseMapperX<ErpPurchaseInvoiceDO> {
+
+ default ErpPurchaseInvoiceDO selectByNo(String no) {
+ return selectOne(ErpPurchaseInvoiceDO::getNo, no);
+ }
+
+ default PageResult<ErpPurchaseInvoiceDO> selectPage(ErpPurchaseInvoicePageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
+ .likeIfPresent(ErpPurchaseInvoiceDO::getNo, reqVO.getNo())
+ .eqIfPresent(ErpPurchaseInvoiceDO::getSupplierId, reqVO.getSupplierId())
+ .eqIfPresent(ErpPurchaseInvoiceDO::getPurchaseOrderId, reqVO.getPurchaseOrderId())
+ .eqIfPresent(ErpPurchaseInvoiceDO::getAuditStatus, reqVO.getAuditStatus())
+ .orderByDesc(ErpPurchaseInvoiceDO::getId));
+ }
+
+ default List<ErpPurchaseInvoiceDO> selectListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses) {
+ return selectList(new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
+ .eq(ErpPurchaseInvoiceDO::getPurchaseOrderId, orderId)
+ .in(ErpPurchaseInvoiceDO::getAuditStatus, auditStatuses));
+ }
+
+ default List<ErpPurchaseInvoiceDO> selectListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses) {
+ return selectList(new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
+ .eq(ErpPurchaseInvoiceDO::getSupplierId, supplierId)
+ .in(ErpPurchaseInvoiceDO::getAuditStatus, auditStatuses));
+ }
+
+ default Long selectCountByAudit() {
+ return selectCount(ErpPurchaseInvoiceDO::getAuditStatus, ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus());
+ }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
index fa36e91..bdce48c 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -35,6 +35,11 @@
public static final String PURCHASE_REQUEST_NO_PREFIX = "CGSQ";
/**
+ * 閲囪喘鏉ョエ {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO}
+ */
+ public static final String PURCHASE_INVOICE_NO_PREFIX = "FPD";
+
+ /**
* 浠樻鍗� {@link cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO}
*/
public static final String FINANCE_PAYMENT_NO_PREFIX = "FKD";
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java
new file mode 100644
index 0000000..3363972
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.erp.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * ERP 閲囪喘鏉ョエ瀹℃壒鐘舵�佹灇涓�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@RequiredArgsConstructor
+@Getter
+public enum ErpPurchaseInvoiceAuditStatusEnum implements ArrayValuable<Integer> {
+
+ DRAFT(0, "鏈彁浜�"),
+ PROCESS(10, "瀹℃壒涓�"),
+ APPROVE(20, "瀹℃牳閫氳繃"),
+ REJECT(30, "瀹℃牳涓嶉�氳繃"),
+ CANCEL(40, "宸插彇娑�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(ErpPurchaseInvoiceAuditStatusEnum::getStatus).toArray(Integer[]::new);
+
+ private final Integer status;
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
index 5666784..b295513 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -54,6 +54,17 @@
ErrorCode PURCHASE_REQUEST_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_104_008, "閲囪喘鐢宠瀹℃壒鍒嗙被涓嶅瓨鍦紝璇峰厛鍦˙PM妯″潡鍒涘缓閲囪喘鐢宠瀹℃壒娴佺▼鍒嗙被");
ErrorCode PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_104_009, "閲囪喘鐢宠瀹℃壒鍒嗙被涓嬫病鏈夊彲鐢ㄧ殑娴佺▼妯″瀷锛岃鍏堝垱寤哄苟鍙戝竷娴佺▼");
+ // ========== ERP 閲囪喘鏉ョエ锛�1-030-105-000锛� ==========
+ ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "鏉ョエ涓嶅瓨鍦�");
+ ErrorCode PURCHASE_INVOICE_NO_EXISTS = new ErrorCode(1_030_105_001, "鐢熸垚鏉ョエ缂栧彿澶辫触锛岃閲嶆柊鎻愪氦");
+ ErrorCode PURCHASE_INVOICE_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_030_105_002, "鏉ョエ({})涓嶆槸鑽夌鐘舵�侊紝鏃犳硶淇敼");
+ ErrorCode PURCHASE_INVOICE_DELETE_FAIL_IS_APPROVE = new ErrorCode(1_030_105_003, "鍒犻櫎鏉ョエ澶辫触锛屽師鍥狅細鏉ョエ瀹℃壒宸查�氳繃");
+ ErrorCode PURCHASE_INVOICE_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_105_004, "鏉ョエ鎻愪氦瀹℃牳澶辫触锛屽師鍥狅細鏉ョエ娌″鍦ㄦ湭鎻愪氦鐘舵��");
+ ErrorCode PURCHASE_INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_105_005, "鏇存柊鏉ョエ瀹℃牳鐘舵�佸け璐ワ紝鍘熷洜锛氭潵绁ㄤ笉鏄鏍镐腑鐘舵��");
+ ErrorCode PURCHASE_INVOICE_CREATE_FAIL_ORDER_NOT_APPROVE = new ErrorCode(1_030_105_006, "鍒涘缓鏉ョエ澶辫触锛屽師鍥狅細閲囪喘璁㈠崟涓嶆槸瀹℃牳閫氳繃鐘舵��");
+ ErrorCode PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH = new ErrorCode(1_030_105_007, "鍒涘缓鏉ョエ澶辫触锛屽師鍥狅細鏉ョエ渚涘簲鍟嗕笌閲囪喘璁㈠崟涓嶄竴鑷�");
+ ErrorCode PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_030_105_008, "鍒涘缓鏉ョエ澶辫触锛屽師鍥狅細鏉ョエ閲戦瓒呭嚭閲囪喘璁㈠崟閲戦锛岀洰鍓嶅彲寮�绁細{} 鍏�");
+
// ========== ERP 閲囪喘閫�璐э紙1-030-103-000锛� ==========
ErrorCode PURCHASE_RETURN_NOT_EXISTS = new ErrorCode(1_030_103_000, "閲囪喘閫�璐у崟涓嶅瓨鍦�");
ErrorCode PURCHASE_RETURN_DELETE_FAIL_APPROVE = new ErrorCode(1_030_103_001, "閲囪喘閫�璐у崟({})宸插鏍革紝鏃犳硶鍒犻櫎");
@@ -180,6 +191,11 @@
ErrorCode FINANCE_PAYMENT_APPROVE_FAIL = new ErrorCode(1_030_601_003, "瀹℃牳澶辫触锛屽彧鏈夋湭瀹℃牳鐨勪粯娆惧崟鎵嶈兘瀹℃牳");
ErrorCode FINANCE_PAYMENT_NO_EXISTS = new ErrorCode(1_030_601_004, "鐢熸垚浠樻鍗曞彿澶辫触锛岃閲嶆柊鎻愪氦");
ErrorCode FINANCE_PAYMENT_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_601_005, "浠樻鍗�({})宸插鏍革紝鏃犳硶淇敼");
+ ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_EXISTS = new ErrorCode(1_030_601_006, "鍒涘缓浠樻鍗曞け璐ワ紝鍘熷洜锛氬叧鑱旂殑鏉ョエ涓嶅瓨鍦�");
+ ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_APPROVE = new ErrorCode(1_030_601_007, "鍒涘缓浠樻鍗曞け璐ワ紝鍘熷洜锛氬叧鑱旂殑鏉ョエ涓嶆槸瀹℃壒閫氳繃鐘舵��");
+ ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_ATTACHMENT = new ErrorCode(1_030_601_008, "鍒涘缓浠樻鍗曞け璐ワ紝鍘熷洜锛氬叧鑱旂殑鏉ョエ鏈笂浼犲彂绁ㄩ檮浠�");
+ ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_SUPPLIER_MISMATCH = new ErrorCode(1_030_601_009, "鍒涘缓浠樻鍗曞け璐ワ紝鍘熷洜锛氬叧鑱旂殑鏉ョエ涓庢墍閫変緵搴斿晢涓嶅尮閰�");
+ ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_PRICE_EXCEEDS = new ErrorCode(1_030_601_010, "鍒涘缓浠樻鍗曞け璐ワ紝鍘熷洜锛氫粯娆鹃噾棰濊秴鍑鸿鏉ョエ鍓╀綑鍙粯閲戦锛岀洰鍓嶅彲浠橈細{} 鍏�");
// ========== ERP 鏀舵鍗� 1-030-602-000 ==========
ErrorCode FINANCE_RECEIPT_NOT_EXISTS = new ErrorCode(1_030_602_000, "鏀舵鍗曚笉瀛樺湪");
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java
index 5b09756..c064166 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java
@@ -18,6 +18,7 @@
PURCHASE_ORDER(10, "閲囪喘璁㈠崟"),
PURCHASE_IN(11, "閲囪喘鍏ュ簱"),
PURCHASE_RETURN(12, "閲囪喘閫�璐�"),
+ PURCHASE_INVOICE(13, "閲囪喘鏉ョエ"),
SALE_ORDER(20, "閿�鍞鍗�"),
SALE_OUT(21, "閿�鍞嚭搴�"),
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java
index 87c8ab8..b7c2547 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java
@@ -7,8 +7,10 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
import jakarta.validation.Valid;
+import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
/**
* ERP 浠樻鍗� Service 鎺ュ彛
@@ -63,6 +65,14 @@
*/
PageResult<ErpFinancePaymentDO> getFinancePaymentPage(ErpFinancePaymentPageReqVO pageReqVO);
+ /**
+ * 缁熻姣忓紶鏉ョエ宸蹭粯娆鹃噾棰濓紙鏈鏍� + 瀹℃壒涓� + 宸插鏍革紝鎺掗櫎浣滃簾锛�
+ *
+ * @param invoiceIds 鏉ョエ缂栧彿鍒楄〃
+ * @return key 涓烘潵绁ㄧ紪鍙凤紝value 涓哄凡浠樻閲戦
+ */
+ Map<Long, BigDecimal> getPaymentPriceMapByInvoiceId(Collection<Long> invoiceIds);
+
// ==================== 浠樻鍗曢」 ====================
/**
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
index 0281b30..6be9fa0 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -1,16 +1,21 @@
package cn.iocoder.yudao.module.erp.service.finance;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -23,6 +28,7 @@
import java.util.Collection;
import java.util.Collections;
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.*;
@@ -49,6 +55,8 @@
private ErpSupplierService supplierService;
@Resource
private ErpAccountService accountService;
+ @Resource
+ private ErpPurchaseInvoiceService invoiceService;
@Resource
private AdminUserApi adminUserApi;
@@ -72,7 +80,13 @@
if (createReqVO.getFinanceUserId() != null) {
adminUserApi.validateUser(createReqVO.getFinanceUserId());
}
- // 1.5 鐢熸垚浠樻鍗曞彿锛屽苟鏍¢獙鍞竴鎬�
+ // 1.5 鏍¢獙鍏宠仈鏉ョエ
+ validateInvoiceForPayment(createReqVO.getInvoiceId(), createReqVO.getSupplierId());
+ // 1.6 鏍¢獙浠樻閲戦涓嶈秴杩囪鏉ョエ鍓╀綑鍙粯閲戦
+ validatePaymentPriceExceedsInvoiceRemaining(createReqVO.getInvoiceId(),
+ getSumValue(paymentItems, ErpFinancePaymentItemDO::getPaymentPrice, BigDecimal::add, BigDecimal.ZERO)
+ .subtract(createReqVO.getDiscountPrice()), null);
+ // 1.7 鐢熸垚浠樻鍗曞彿锛屽苟鏍¢獙鍞竴鎬�
String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX);
if (financePaymentMapper.selectByNo(no) != null) {
throw exception(FINANCE_PAYMENT_NO_EXISTS);
@@ -117,6 +131,12 @@
// 1.5 鏍¢獙浠樻鍗曢」鐨勬湁鏁堟��
List<ErpFinancePaymentItemDO> paymentItems = validateFinancePaymentItems(
updateReqVO.getSupplierId(), updateReqVO.getItems());
+ // 1.6 鏍¢獙鍏宠仈鏉ョエ
+ validateInvoiceForPayment(updateReqVO.getInvoiceId(), updateReqVO.getSupplierId());
+ // 1.7 鏍¢獙浠樻閲戦涓嶈秴杩囪鏉ョエ鍓╀綑鍙粯閲戦锛堟帓闄よ浠樻鍗曡嚜韬綋鍓嶅凡浠橀噾棰濓級
+ validatePaymentPriceExceedsInvoiceRemaining(updateReqVO.getInvoiceId(),
+ getSumValue(paymentItems, ErpFinancePaymentItemDO::getPaymentPrice, BigDecimal::add, BigDecimal.ZERO)
+ .subtract(updateReqVO.getDiscountPrice()), payment.getPaymentPrice());
// 2.1 鏇存柊浠樻鍗�
ErpFinancePaymentDO updateObj = BeanUtils.toBean(updateReqVO, ErpFinancePaymentDO.class);
@@ -224,6 +244,53 @@
return payment;
}
+ /**
+ * 鏍¢獙浠樻鍗曞叧鑱旂殑鏉ョエ锛氬瓨鍦ㄣ�佸鎵归�氳繃銆佸凡涓婁紶鍙戠エ闄勪欢銆佷緵搴斿晢涓庢潵绁ㄤ竴鑷�
+ *
+ * @param invoiceId 鏉ョエ缂栧彿
+ * @param supplierId 渚涘簲鍟嗙紪鍙�
+ */
+ private void validateInvoiceForPayment(Long invoiceId, Long supplierId) {
+ // 1. 鏍¢獙鏉ョエ瀛樺湪
+ ErpPurchaseInvoiceDO invoice = invoiceService.getPurchaseInvoice(invoiceId);
+ if (invoice == null) {
+ throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_EXISTS);
+ }
+ // 2. 鏍¢獙鏉ョエ瀹℃壒閫氳繃
+ if (ObjectUtil.notEqual(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus())) {
+ throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_APPROVE);
+ }
+ // 3. 鏍¢獙宸蹭笂浼犲彂绁ㄩ檮浠�
+ if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(
+ ErpPurchaseInvoiceServiceImpl.PURCHASE_INVOICE_RECORD_TYPE, invoiceId))) {
+ throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_ATTACHMENT);
+ }
+ // 4. 鏍¢獙渚涘簲鍟嗕笌鏉ョエ涓�鑷达紙闃茶剰鏁版嵁锛�
+ if (ObjectUtil.notEqual(invoice.getSupplierId(), supplierId)) {
+ throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_SUPPLIER_MISMATCH);
+ }
+ }
+
+ /**
+ * 鏍¢獙浠樻閲戦涓嶈秴杩囪鏉ョエ鍓╀綑鍙粯閲戦锛堟潵绁ㄩ噾棰� - 璇ユ潵绁ㄥ凡浠樻閲戦锛�
+ *
+ * @param invoiceId 鏉ョエ缂栧彿
+ * @param paymentPrice 鏈浠樻閲戦
+ * @param oldPaymentPrice 鏇存柊鍦烘櫙涓嬶紝璇ヤ粯娆惧崟鑷韩褰撳墠鐨勪粯娆鹃噾棰濓紙鐢ㄤ簬鎺掗櫎鑷韩锛夛紝鍒涘缓鍦烘櫙浼� null
+ */
+ private void validatePaymentPriceExceedsInvoiceRemaining(Long invoiceId, BigDecimal paymentPrice, BigDecimal oldPaymentPrice) {
+ ErpPurchaseInvoiceDO invoice = invoiceService.getPurchaseInvoice(invoiceId);
+ BigDecimal paidPrice = financePaymentMapper.selectPaymentPriceMapByInvoiceId(List.of(invoiceId))
+ .getOrDefault(invoiceId, BigDecimal.ZERO);
+ if (oldPaymentPrice != null) {
+ paidPrice = paidPrice.subtract(oldPaymentPrice); // 鎺掗櫎璇ヤ粯娆惧崟鑷韩褰撳墠宸蹭粯閲戦
+ }
+ BigDecimal remainingPrice = invoice.getPrice().subtract(paidPrice);
+ if (paymentPrice.compareTo(remainingPrice) > 0) {
+ throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_PRICE_EXCEEDS, remainingPrice);
+ }
+ }
+
@Override
public ErpFinancePaymentDO getFinancePayment(Long id) {
return financePaymentMapper.selectById(id);
@@ -234,6 +301,11 @@
return financePaymentMapper.selectPage(pageReqVO);
}
+ @Override
+ public Map<Long, BigDecimal> getPaymentPriceMapByInvoiceId(Collection<Long> invoiceIds) {
+ return financePaymentMapper.selectPaymentPriceMapByInvoiceId(invoiceIds);
+ }
+
// ==================== 浠樻鍗曢」 ====================
@Override
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java
new file mode 100644
index 0000000..8f8b01b
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java
@@ -0,0 +1,119 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 閲囪喘鏉ョエ Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface ErpPurchaseInvoiceService {
+
+ /**
+ * 鍒涘缓鏉ョエ
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @return 缂栧彿
+ */
+ Long createPurchaseInvoice(@Valid ErpPurchaseInvoiceSaveReqVO createReqVO);
+
+ /**
+ * 鏇存柊鏉ョエ
+ *
+ * @param updateReqVO 鏇存柊淇℃伅
+ */
+ void updatePurchaseInvoice(@Valid ErpPurchaseInvoiceSaveReqVO updateReqVO);
+
+ /**
+ * 鍒犻櫎鏉ョエ
+ *
+ * @param id 缂栧彿
+ */
+ void deletePurchaseInvoice(Long id);
+
+ /**
+ * 鎻愪氦鏉ョエ瀹℃壒
+ *
+ * @param id 缂栧彿
+ * @param userId 鐢ㄦ埛缂栧彿
+ */
+ void submitPurchaseInvoice(Long id, Long userId);
+
+ /**
+ * 鏇存柊鏉ョエ瀹℃壒缁撴灉锛圔PM 鍥炶皟锛�
+ *
+ * @param id 缂栧彿
+ * @param bpmResult BPM 瀹℃壒缁撴灉
+ */
+ void updatePurchaseInvoiceAuditStatus(Long id, Integer bpmResult);
+
+ /**
+ * 鑾峰緱鏉ョエ
+ *
+ * @param id 缂栧彿
+ * @return 鏉ョエ
+ */
+ ErpPurchaseInvoiceDO getPurchaseInvoice(Long id);
+
+ /**
+ * 鑾峰緱鏉ョエ鍒楄〃
+ *
+ * @param ids 缂栧彿鍒楄〃
+ * @return 鏉ョエ鍒楄〃
+ */
+ List<ErpPurchaseInvoiceDO> getPurchaseInvoiceList(Collection<Long> ids);
+
+ /**
+ * 鑾峰緱鏉ョエ Map
+ *
+ * @param ids 缂栧彿鍒楄〃
+ * @return 鏉ョエ Map
+ */
+ default Map<Long, ErpPurchaseInvoiceDO> getPurchaseInvoiceMap(Collection<Long> ids) {
+ return convertMap(getPurchaseInvoiceList(ids), ErpPurchaseInvoiceDO::getId);
+ }
+
+ /**
+ * 鑾峰緱鏉ョエ鍒嗛〉
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 鏉ョエ鍒嗛〉
+ */
+ PageResult<ErpPurchaseInvoiceDO> getPurchaseInvoicePage(ErpPurchaseInvoicePageReqVO pageReqVO);
+
+ /**
+ * 鏍规嵁閲囪喘璁㈠崟缂栧彿鍜屽鎵圭姸鎬佽幏寰楁潵绁ㄥ垪琛�
+ *
+ * @param orderId 閲囪喘璁㈠崟缂栧彿
+ * @param auditStatuses 瀹℃壒鐘舵�侀泦鍚�
+ * @return 鏉ョエ鍒楄〃
+ */
+ List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses);
+
+ /**
+ * 鏍规嵁渚涘簲鍟嗙紪鍙峰拰瀹℃壒鐘舵�佽幏寰楁潵绁ㄥ垪琛�
+ *
+ * @param supplierId 渚涘簲鍟嗙紪鍙�
+ * @param auditStatuses 瀹℃壒鐘舵�侀泦鍚�
+ * @return 鏉ョエ鍒楄〃
+ */
+ List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses);
+
+ /**
+ * 鑾峰緱寰呭鏍告潵绁ㄦ暟閲�
+ *
+ * @return 鏁伴噺
+ */
+ Long getAuditInvoiceCount();
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
new file mode 100644
index 0000000..b9a9a1c
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
@@ -0,0 +1,248 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
+import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+/**
+ * ERP 閲囪喘鏉ョエ Service 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+@Validated
+@Slf4j
+public class ErpPurchaseInvoiceServiceImpl implements ErpPurchaseInvoiceService {
+
+ /**
+ * BPM 鏉ョエ瀹℃壒娴佺▼鏍囪瘑
+ */
+ public static final String BPM_PROCESS_DEFINITION_KEY = "erp-purchase-invoice-audit";
+
+ /**
+ * 鏉ョエ闄勪欢鐨勪笟鍔¤褰曠被鍨嬶紝瀵瑰簲 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#ERP_PURCHASE_INVOICE}
+ */
+ public static final String PURCHASE_INVOICE_RECORD_TYPE = "erp_purchase_invoice";
+
+ @Resource
+ private ErpPurchaseInvoiceMapper invoiceMapper;
+
+ @Resource
+ private ErpNoRedisDAO noRedisDAO;
+
+ @Resource
+ private ErpPurchaseOrderService purchaseOrderService;
+
+ @Resource
+ private BpmProcessInstanceApi bpmProcessInstanceApi;
+ @Resource
+ private StorageAttachmentApi storageAttachmentApi;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createPurchaseInvoice(ErpPurchaseInvoiceSaveReqVO createReqVO) {
+ // 1.1 鏍¢獙鍏宠仈鏁版嵁瀛樺湪
+ validateRelationDataExists(createReqVO);
+ // 1.2 鏍¢獙鍙紑绁ㄩ噾棰濊秴杩囦笂闄�
+ validatePurchaseInvoicePriceExceedsLimit(createReqVO);
+ // 1.3 鐢熸垚鏉ョエ缂栧彿
+ String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_INVOICE_NO_PREFIX);
+ if (invoiceMapper.selectByNo(no) != null) {
+ throw exception(PURCHASE_INVOICE_NO_EXISTS);
+ }
+
+ // 2.1 鎻掑叆鏉ョエ
+ ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class)
+ .setNo(no).setAuditStatus(ErpPurchaseInvoiceAuditStatusEnum.DRAFT.getStatus());
+ invoiceMapper.insert(invoice);
+
+ // 2.2 缁戝畾鍙戠エ闄勪欢
+ if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
+ storageAttachmentApi.bindAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds());
+ }
+ return invoice.getId();
+ }
+
+ private void validateRelationDataExists(ErpPurchaseInvoiceSaveReqVO reqVO) {
+ // 鏍¢獙閲囪喘璁㈠崟锛屽繀椤诲鏍搁�氳繃
+ ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(reqVO.getPurchaseOrderId());
+ // 鏍¢獙渚涘簲鍟嗕笌閲囪喘璁㈠崟涓�鑷达紙闃插尽鑴忔暟鎹細鏉ョエ蹇呴』灞炰簬璇ラ噰璐鍗曠殑渚涘簲鍟嗭級
+ if (reqVO.getSupplierId() != null && ObjectUtil.notEqual(purchaseOrder.getSupplierId(), reqVO.getSupplierId())) {
+ throw exception(PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH);
+ }
+ reqVO.setSupplierId(purchaseOrder.getSupplierId()); // 渚涘簲鍟嗕互閲囪喘璁㈠崟涓哄噯
+ }
+
+ private void validatePurchaseInvoicePriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO) {
+ // 1. 璁$畻鍓╀綑鍙紑绁ㄩ噾棰濓紝涓嶅寘鎷� reqVO 鑷韩锛堝鎵逛腑涔熷崰鐢ㄩ搴︼紝闃叉绱鏉ョエ瓒呰繃閲囪喘璁㈠崟閲戦锛�
+ ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(reqVO.getPurchaseOrderId());
+ List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderIdAndStatus(reqVO.getPurchaseOrderId(),
+ List.of(ErpPurchaseInvoiceAuditStatusEnum.DRAFT.getStatus(),
+ ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus(),
+ ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
+ if (reqVO.getId() != null) {
+ invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
+ }
+ BigDecimal remainPrice = purchaseOrder.getTotalPrice().subtract(
+ getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
+ // 2. 鏍¢獙閲戦鏄惁瓒呰繃
+ if (reqVO.getPrice().compareTo(remainPrice) > 0) {
+ throw exception(PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, remainPrice);
+ }
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updatePurchaseInvoice(ErpPurchaseInvoiceSaveReqVO updateReqVO) {
+ Assert.notNull(updateReqVO.getId(), "鏉ョエ缂栧彿涓嶈兘涓虹┖");
+ // 1.1 鏍¢獙瀛樺湪
+ ErpPurchaseInvoiceDO oldInvoice = validatePurchaseInvoiceExists(updateReqVO.getId());
+ // 1.2 鍙湁鑽夌鍙互缂栬緫
+ if (ObjectUtil.notEqual(oldInvoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.DRAFT.getStatus())) {
+ throw exception(PURCHASE_INVOICE_UPDATE_FAIL_EDITING_PROHIBITED, oldInvoice.getNo());
+ }
+ // 1.3 鏍¢獙鍏宠仈鏁版嵁 + 鍙紑绁ㄩ噾棰�
+ validateRelationDataExists(updateReqVO);
+ validatePurchaseInvoicePriceExceedsLimit(updateReqVO);
+
+ // 2. 鏇存柊鏉ョエ
+ ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class);
+ invoiceMapper.updateById(updateObj);
+
+ // 3. 澧為噺鏇存柊鍙戠エ闄勪欢
+ storageAttachmentApi.updateAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deletePurchaseInvoice(Long id) {
+ // 1.1 鏍¢獙瀛樺湪
+ ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
+ // 1.2 瀹℃壒閫氳繃鏃讹紝涓嶅厑璁稿垹闄�
+ if (ObjectUtil.equal(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus())) {
+ throw exception(PURCHASE_INVOICE_DELETE_FAIL_IS_APPROVE);
+ }
+
+ // 2.1 鍒犻櫎鍙戠エ闄勪欢
+ storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id);
+ // 2.2 鍒犻櫎鏉ョエ
+ invoiceMapper.deleteById(id);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void submitPurchaseInvoice(Long id, Long userId) {
+ // 1. 鏍¢獙鏉ョエ鏄惁鍦ㄥ鎵�
+ ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
+ if (ObjectUtil.notEqual(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.DRAFT.getStatus())) {
+ throw exception(PURCHASE_INVOICE_SUBMIT_FAIL_NOT_DRAFT);
+ }
+
+ // 1.1 閲嶆柊鏍¢獙鍙紑绁ㄩ噾棰濓紝闃叉绱鏉ョエ瓒呰繃閲囪喘璁㈠崟閲戦锛堣崏绋挎湡鍚庡彲鑳芥湁鍏朵粬鍗曡繘鍏ュ鎵逛腑锛�
+ validatePurchaseInvoicePriceExceedsLimit(new ErpPurchaseInvoiceSaveReqVO()
+ .setId(invoice.getId()).setPurchaseOrderId(invoice.getPurchaseOrderId())
+ .setSupplierId(invoice.getSupplierId()).setPrice(invoice.getPrice()));
+
+ // 2. 鍒涘缓鏉ョエ瀹℃壒娴佺▼瀹炰緥
+ String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id)));
+
+ // 3. 鏇存柊鏉ョエ宸ヤ綔娴佺紪鍙�
+ invoiceMapper.updateById(new ErpPurchaseInvoiceDO().setId(id).setProcessInstanceId(processInstanceId)
+ .setAuditStatus(ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus()));
+ }
+
+ @Override
+ public void updatePurchaseInvoiceAuditStatus(Long id, Integer bpmResult) {
+ // 1. 鏍¢獙瀛樺湪 + 瀹℃壒涓姸鎬�
+ ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
+ if (ObjectUtil.notEqual(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus())) {
+ log.warn("[updatePurchaseInvoiceAuditStatus][鏉ョエ({}) 涓嶅浜庡鎵逛腑锛屾棤娉曟洿鏂板鎵圭粨鏋�({})]", id, bpmResult);
+ return;
+ }
+ // 2. 鏍规嵁瀹℃壒缁撴灉鏇存柊鐘舵��
+ Integer auditStatus = convertBpmResultToAuditStatus(bpmResult);
+ if (auditStatus != null) {
+ invoiceMapper.updateById(new ErpPurchaseInvoiceDO().setId(id).setAuditStatus(auditStatus));
+ }
+ }
+
+ private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
+ if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
+ return ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus();
+ } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
+ return ErpPurchaseInvoiceAuditStatusEnum.REJECT.getStatus();
+ } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
+ return ErpPurchaseInvoiceAuditStatusEnum.CANCEL.getStatus();
+ }
+ return null;
+ }
+
+ private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) {
+ ErpPurchaseInvoiceDO invoice = invoiceMapper.selectById(id);
+ if (invoice == null) {
+ throw exception(PURCHASE_INVOICE_NOT_EXISTS);
+ }
+ return invoice;
+ }
+
+ @Override
+ public ErpPurchaseInvoiceDO getPurchaseInvoice(Long id) {
+ return invoiceMapper.selectById(id);
+ }
+
+ @Override
+ public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceList(Collection<Long> ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return List.of();
+ }
+ return invoiceMapper.selectByIds(ids);
+ }
+
+ @Override
+ public PageResult<ErpPurchaseInvoiceDO> getPurchaseInvoicePage(ErpPurchaseInvoicePageReqVO pageReqVO) {
+ return invoiceMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses) {
+ return invoiceMapper.selectListByOrderIdAndStatus(orderId, auditStatuses);
+ }
+
+ @Override
+ public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses) {
+ return invoiceMapper.selectListBySupplierIdAndStatus(supplierId, auditStatuses);
+ }
+
+ @Override
+ public Long getAuditInvoiceCount() {
+ return invoiceMapper.selectCountByAudit();
+ }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseInvoiceStatusListener.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseInvoiceStatusListener.java
new file mode 100644
index 0000000..e93b790
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseInvoiceStatusListener.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.erp.service.purchase.listener;
+
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 閲囪喘鏉ョエ瀹℃壒鐘舵�佺洃鍚櫒
+ *
+ * 鐩戝惉 BPM 娴佺▼瀹炰緥鐘舵�佸彉鏇翠簨浠讹紝鏇存柊鏉ョエ鐨勫鎵圭姸鎬�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Component
+@Slf4j
+public class ErpPurchaseInvoiceStatusListener extends BpmProcessInstanceStatusEventListener {
+
+ @Resource
+ private ErpPurchaseInvoiceService invoiceService;
+
+ @Override
+ protected String getProcessDefinitionKey() {
+ return ErpPurchaseInvoiceServiceImpl.BPM_PROCESS_DEFINITION_KEY;
+ }
+
+ @Override
+ protected void onEvent(BpmProcessInstanceStatusEvent event) {
+ Long invoiceId;
+ try {
+ invoiceId = Long.parseLong(event.getBusinessKey());
+ } catch (NumberFormatException e) {
+ log.warn("[onEvent] businessKey({}) 涓嶆槸鏈夋晥鐨勬潵绁ㄧ紪鍙�", event.getBusinessKey());
+ return;
+ }
+ log.info("[onEvent] 鏀跺埌BPM瀹℃壒浜嬩欢锛宨nvoiceId={}, status={}", invoiceId, event.getStatus());
+ invoiceService.updatePurchaseInvoiceAuditStatus(invoiceId, event.getStatus());
+ }
+
+}
diff --git a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
index ff891ea..dd2eb29 100644
--- a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
+++ b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -21,6 +21,11 @@
ErrorCode EMPLOYEE_POST_NAME_NOT_EXISTS = new ErrorCode(1_030_000_008, "宀椾綅銆恵}銆戜笉瀛樺湪");
ErrorCode EMPLOYEE_ROLE_NAME_NOT_EXISTS = new ErrorCode(1_030_000_009, "瑙掕壊銆恵}銆戜笉瀛樺湪");
ErrorCode EMPLOYEE_IMPORT_FIELD_REQUIRED = new ErrorCode(1_030_000_010, "瀵煎叆瀛楁銆恵}銆戜笉鑳戒负绌�");
+ ErrorCode EMPLOYEE_CONTRACT_NOT_EXISTS = new ErrorCode(1_030_000_011, "鍛樺伐鍚堝悓涓嶅瓨鍦�");
+ ErrorCode EMPLOYEE_CONTRACT_TERMINATE_REQUIRE_DATE_REASON = new ErrorCode(1_030_000_012, "瑙i櫎/缁堟鍚堝悓蹇呴』濉啓鏃ユ湡鍜屽師鍥�");
+ ErrorCode EMPLOYEE_CONTRACT_END_DATE_REQUIRE_FOR_FIXED = new ErrorCode(1_030_000_013, "鍥哄畾鏈熼檺鍚堝悓蹇呴』濉啓缁撴潫鏃ユ湡");
+ ErrorCode EMPLOYEE_CONTRACT_DATE_INVALID = new ErrorCode(1_030_000_014, "鍚堝悓鏃ユ湡涓嶅悎娉曪紝寮�濮嬫棩鏈熶笉鑳芥櫄浜庣粨鏉熸棩鏈�");
+ ErrorCode EMPLOYEE_CONTRACT_DATE_OVERLAP = new ErrorCode(1_030_000_015, "璇ュ憳宸ュ湪鎵�閫夋椂闂存宸插瓨鍦ㄦ湭瑙i櫎/鏈粓姝㈢殑鍚堝悓锛岃鍏堣В闄�/缁堟鍚庡啀绛捐");
// ========== 璇峰亣鐢宠 1-030-001-000 ==========
ErrorCode LEAVE_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_001_000, "璇峰亣鐢宠涓嶅瓨鍦�");
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
new file mode 100644
index 0000000..d956af0
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.employee;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractPageReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractSaveReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractTerminateReqVO;
+import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeContractService;
+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.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "绠$悊鍚庡彴 - HRM 鍛樺伐鍚堝悓")
+@RestController
+@RequestMapping("/hrm/employee-contract")
+@Validated
+public class HrmEmployeeContractController {
+
+ @Resource
+ private HrmEmployeeContractService contractService;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓鍛樺伐鍚堝悓")
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:create')")
+ public CommonResult<Long> createContract(@Valid @RequestBody HrmEmployeeContractSaveReqVO createReqVO) {
+ return success(contractService.createContract(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊鍛樺伐鍚堝悓")
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:update')")
+ public CommonResult<Boolean> updateContract(@Valid @RequestBody HrmEmployeeContractSaveReqVO updateReqVO) {
+ contractService.updateContract(updateReqVO);
+ return success(true);
+ }
+
+ @PutMapping("/terminate")
+ @Operation(summary = "瑙i櫎/缁堟鍛樺伐鍚堝悓")
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:update')")
+ public CommonResult<Boolean> terminateContract(@Valid @RequestBody HrmEmployeeContractTerminateReqVO reqVO) {
+ contractService.terminateContract(reqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎鍛樺伐鍚堝悓")
+ @Parameter(name = "id", description = "鍚堝悓ID", required = true)
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:delete')")
+ public CommonResult<Boolean> deleteContract(@RequestParam("id") Long id) {
+ contractService.deleteContract(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱鍛樺伐鍚堝悓璇︽儏")
+ @Parameter(name = "id", description = "鍚堝悓ID", required = true)
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
+ public CommonResult<HrmEmployeeContractRespVO> getContract(@RequestParam("id") Long id) {
+ return success(contractService.getContract(id));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱鍛樺伐鍚堝悓鍒嗛〉")
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
+ public CommonResult<PageResult<HrmEmployeeContractRespVO>> getContractPage(@Valid HrmEmployeeContractPageReqVO pageReqVO) {
+ return success(contractService.getContractPage(pageReqVO));
+ }
+
+ @GetMapping("/expiring-count")
+ @Operation(summary = "鑾峰緱鍗冲皢鍒版湡鍚堝悓鏁伴噺", description = "渚� HRM 寰呭姙/棣栭〉鎻愰啋浣跨敤")
+ @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
+ public CommonResult<Long> getExpiringCount() {
+ return success(contractService.getExpiringCount());
+ }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractPageReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractPageReqVO.java
new file mode 100644
index 0000000..8fba285
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractPageReqVO.java
@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Schema(description = "绠$悊鍚庡彴 - 鍛樺伐鍚堝悓鍒嗛〉 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class HrmEmployeeContractPageReqVO extends PageParam {
+
+ /**
+ * 鍚堝悓鐘舵�� - 寰呯敓鏁�
+ */
+ public static final Integer STATUS_PENDING = 1;
+ /**
+ * 鍚堝悓鐘舵�� - 鐢熸晥涓�
+ */
+ public static final Integer STATUS_ACTIVE = 2;
+ /**
+ * 鍚堝悓鐘舵�� - 鍗冲皢鍒版湡
+ */
+ public static final Integer STATUS_EXPIRING_SOON = 3;
+ /**
+ * 鍚堝悓鐘舵�� - 宸插埌鏈�
+ */
+ public static final Integer STATUS_EXPIRED = 4;
+ /**
+ * 鍚堝悓鐘舵�� - 宸茶В闄�
+ */
+ public static final Integer STATUS_CANCELED = 5;
+ /**
+ * 鍚堝悓鐘舵�� - 宸茬粓姝�
+ */
+ public static final Integer STATUS_TERMINATED = 6;
+
+ /**
+ * 鍒版湡绫诲瀷 - 鍗冲皢鍒版湡
+ */
+ public static final Integer EXPIRY_TYPE_ABOUT_TO_EXPIRE = 1;
+ /**
+ * 鍒版湡绫诲瀷 - 宸插埌鏈�
+ */
+ public static final Integer EXPIRY_TYPE_EXPIRED = 2;
+
+ @Schema(description = "鍚堝悓缂栧彿", example = "HT202608100001")
+ private String contractNo;
+
+ @Schema(description = "鍛樺伐ID", example = "1")
+ private Long employeeId;
+
+ @Schema(description = "鍚堝悓绫诲瀷", example = "1")
+ private Integer contractType;
+
+ @Schema(description = "鍚堝悓鏈熼檺绫诲瀷", example = "1")
+ private Integer contractTermType;
+
+ @Schema(description = "瑙i櫎/缁堟鐘舵�侊細0-姝e父 1-宸茶В闄� 2-宸茬粓姝�", example = "0")
+ private Integer terminateStatus;
+
+ @Schema(description = "鍚堝悓鐘舵�侊細1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝紙鍔ㄦ�佽绠楋級", example = "3")
+ private Integer status;
+
+ @Schema(description = "鍒版湡绫诲瀷锛�1-鍗冲皢鍒版湡 2-宸插埌鏈燂紙寰呭姙鎻愰啋鐢級", example = "1")
+ private Integer expiryType;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractRespVO.java
new file mode 100644
index 0000000..8b000b5
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractRespVO.java
@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
+
+import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - 鍛樺伐鍚堝悓 Response VO")
+@Data
+public class HrmEmployeeContractRespVO {
+
+ @Schema(description = "鍚堝悓ID", example = "1")
+ private Long id;
+
+ @Schema(description = "缁涓婁竴浠藉悎鍚孖D", example = "1")
+ private Long parentId;
+
+ @Schema(description = "缁涓婁竴浠藉悎鍚岀紪鍙�", example = "HT202608100001")
+ private String parentNo;
+
+ @Schema(description = "鏄惁褰撳墠鐢熸晥鍚堝悓", example = "true")
+ private Boolean isCurrent;
+
+ @Schema(description = "鍚堝悓缂栧彿", example = "HT202608100001")
+ private String contractNo;
+
+ @Schema(description = "鍛樺伐ID", example = "1")
+ private Long employeeId;
+
+ @Schema(description = "鍛樺伐濮撳悕", example = "寮犱笁")
+ private String employeeName;
+
+ @Schema(description = "鍛樺伐宸ュ彿", example = "EMP202401010001")
+ private String employeeNo;
+
+ @Schema(description = "閮ㄩ棬鍚嶇О", example = "鐮斿彂閮�")
+ private String deptName;
+
+ @Schema(description = "鍚堝悓绫诲瀷", example = "1")
+ private Integer contractType;
+
+ @Schema(description = "鍚堝悓鏈熼檺绫诲瀷", example = "1")
+ private Integer contractTermType;
+
+ @Schema(description = "绛剧害涓讳綋锛堝叕鍙稿叏绉帮級", example = "鍗楅�歑X绉戞妧鏈夐檺鍏徃")
+ private String signCompany;
+
+ @Schema(description = "绛捐鏃ユ湡")
+ private LocalDate signDate;
+
+ @Schema(description = "鍚堝悓寮�濮嬫棩鏈�")
+ private LocalDate startDate;
+
+ @Schema(description = "鍚堝悓缁撴潫鏃ユ湡锛堟棤鍥哄畾鏈熼檺鍙负绌猴級")
+ private LocalDate endDate;
+
+ @Schema(description = "璇曠敤鏈熷紑濮嬫棩鏈�")
+ private LocalDate probationStartDate;
+
+ @Schema(description = "璇曠敤鏈熺粨鏉熸棩鏈�")
+ private LocalDate probationEndDate;
+
+ @Schema(description = "璇曠敤鏈熷伐璧�", example = "8000.00")
+ private BigDecimal probationSalary;
+
+ @Schema(description = "杞宸ヨ祫", example = "10000.00")
+ private BigDecimal regularSalary;
+
+ @Schema(description = "鍚堝悓鐘舵�侊細1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝紙鍔ㄦ�佽绠楋級", example = "2")
+ private Integer status;
+
+ @Schema(description = "瑙i櫎/缁堟鐘舵�侊細0-姝e父 1-宸茶В闄� 2-宸茬粓姝�", example = "0")
+ private Integer terminateStatus;
+
+ @Schema(description = "瑙i櫎/缁堟鏃ユ湡")
+ private LocalDate terminateDate;
+
+ @Schema(description = "瑙i櫎/缁堟鍘熷洜", example = "鍚堝悓鍒版湡涓嶇画绛�")
+ private String terminateReason;
+
+ @Schema(description = "澶囨敞", example = "绗竴娆$画绛�")
+ private String remark;
+
+ @Schema(description = "闄勪欢鍒楄〃")
+ private List<StorageBlobRespDTO> attachmentList;
+
+ @Schema(description = "鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractSaveReqVO.java
new file mode 100644
index 0000000..f39675d
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractSaveReqVO.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - 鍛樺伐鍚堝悓鏂板/淇敼 Request VO")
+@Data
+public class HrmEmployeeContractSaveReqVO {
+
+ @Schema(description = "鍚堝悓ID", example = "1")
+ private Long id;
+
+ @Schema(description = "缁涓婁竴浠藉悎鍚孖D锛堢画绛炬椂鎸囧畾锛涗竴鑸笉浼狅紝鍚庣鑷姩鍏宠仈璇ュ憳宸ユ渶杩戜竴浠藉悎鍚岋級", example = "1")
+ private Long parentId;
+
+ @Schema(description = "鍛樺伐ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍛樺伐涓嶈兘涓虹┖")
+ private Long employeeId;
+
+ @Schema(description = "鍚堝悓绫诲瀷", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍚堝悓绫诲瀷涓嶈兘涓虹┖")
+ private Integer contractType;
+
+ @Schema(description = "鍚堝悓鏈熼檺绫诲瀷", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍚堝悓鏈熼檺绫诲瀷涓嶈兘涓虹┖")
+ private Integer contractTermType;
+
+ @Schema(description = "绛剧害涓讳綋锛堝叕鍙稿叏绉帮級", example = "鍗楅�歑X绉戞妧鏈夐檺鍏徃")
+ private String signCompany;
+
+ @Schema(description = "绛捐鏃ユ湡")
+ private LocalDate signDate;
+
+ @Schema(description = "鍚堝悓寮�濮嬫棩鏈�", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "鍚堝悓寮�濮嬫棩鏈熶笉鑳戒负绌�")
+ private LocalDate startDate;
+
+ @Schema(description = "鍚堝悓缁撴潫鏃ユ湡锛堟棤鍥哄畾鏈熼檺鍙负绌猴級")
+ private LocalDate endDate;
+
+ @Schema(description = "璇曠敤鏈熷紑濮嬫棩鏈�")
+ private LocalDate probationStartDate;
+
+ @Schema(description = "璇曠敤鏈熺粨鏉熸棩鏈�")
+ private LocalDate probationEndDate;
+
+ @Schema(description = "璇曠敤鏈熷伐璧�", example = "8000.00")
+ private BigDecimal probationSalary;
+
+ @Schema(description = "杞宸ヨ祫", example = "10000.00")
+ private BigDecimal regularSalary;
+
+ @Schema(description = "瑙i櫎/缁堟鐘舵�侊細0-姝e父 1-宸茶В闄� 2-宸茬粓姝�", example = "0")
+ private Integer terminateStatus;
+
+ @Schema(description = "瑙i櫎/缁堟鏃ユ湡")
+ private LocalDate terminateDate;
+
+ @Schema(description = "瑙i櫎/缁堟鍘熷洜", example = "鍚堝悓鍒版湡涓嶇画绛�")
+ private String terminateReason;
+
+ @Schema(description = "澶囨敞", example = "绗竴娆$画绛�")
+ private String remark;
+
+ @Schema(description = "闄勪欢 blobId 鍒楄〃", example = "[1, 2]")
+ private List<Long> blobIds;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractTerminateReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractTerminateReqVO.java
new file mode 100644
index 0000000..f7113ac
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeContractTerminateReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+@Schema(description = "绠$悊鍚庡彴 - 鍛樺伐鍚堝悓瑙i櫎/缁堟 Request VO")
+@Data
+public class HrmEmployeeContractTerminateReqVO {
+
+ @Schema(description = "鍚堝悓ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍚堝悓ID涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "瑙i櫎/缁堟鐘舵�侊細1-宸茶В闄� 2-宸茬粓姝�", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "瑙i櫎/缁堟鐘舵�佷笉鑳戒负绌�")
+ private Integer terminateStatus;
+
+ @Schema(description = "瑙i櫎/缁堟鏃ユ湡", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "瑙i櫎/缁堟鏃ユ湡涓嶈兘涓虹┖")
+ private LocalDate terminateDate;
+
+ @Schema(description = "瑙i櫎/缁堟鍘熷洜", requiredMode = Schema.RequiredMode.REQUIRED, example = "鍗忓晢涓�鑷磋В闄�")
+ private String terminateReason;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeContractDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeContractDO.java
new file mode 100644
index 0000000..a82e305
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeContractDO.java
@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.module.hrm.dal.dataobject.employee;
+
+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;
+import java.time.LocalDate;
+
+/**
+ * 鍛樺伐鍚堝悓 DO
+ *
+ * 璁板綍鍛樺伐涓庡叕鍙哥璁㈢殑鍔冲姩鍚堝悓/缁鍚堝悓绛夛紝涓�涓憳宸ュ彲鏈夊浠藉悎鍚岋紙鍘嗗彶鍚堝悓 + 褰撳墠鍚堝悓锛�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("hrm_employee_contract")
+@KeySequence("hrm_employee_contract_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class HrmEmployeeContractDO extends BaseDO {
+
+ /**
+ * 鍚堝悓ID
+ */
+ @TableId
+ private Long id;
+
+ /**
+ * 缁涓婁竴浠藉悎鍚孖D
+ *
+ * 涓�涓憳宸ュ彲绛捐澶氭鍚堝悓锛岄�氳繃璇ュ瓧娈靛舰鎴愮画绛鹃摼锛屼究浜庤拷婧悎鍚屽巻鍙�
+ */
+ private Long parentId;
+
+ /**
+ * 鏄惁褰撳墠鐢熸晥鍚堝悓
+ *
+ * 鍛樺伐鏈�鏂扮璁㈢殑銆佹湭瑙i櫎/鏈粓姝㈢殑鍚堝悓鏍囪涓哄綋鍓嶅悎鍚岋紝鍚屼竴鍛樺伐浠呬竴浠�
+ */
+ private Boolean isCurrent;
+
+ /**
+ * 鍚堝悓缂栧彿
+ *
+ * 鑷姩鐢熸垚锛欻T + yyyyMMdd + 4浣嶈嚜澧�
+ */
+ private String contractNo;
+
+ /**
+ * 鍛樺伐ID
+ *
+ * 鍏宠仈 HrmEmployeeDO#getId()
+ */
+ private Long employeeId;
+
+ /**
+ * 鍚堝悓绫诲瀷
+ *
+ * 瀛楀吀 hrm_contract_type锛�1-鍔冲姩鍚堝悓 2-鍔冲姟鍚堝悓 3-瀹炰範鍗忚 4-鍔冲姟娲鹃仯 5-鍏朵粬
+ */
+ private Integer contractType;
+
+ /**
+ * 鍚堝悓鏈熼檺绫诲瀷
+ *
+ * 瀛楀吀 hrm_contract_term_type锛�1-鍥哄畾鏈熼檺 2-鏃犲浐瀹氭湡闄� 3-浠ュ畬鎴愪竴瀹氬伐浣滀换鍔′负鏈熼檺
+ */
+ private Integer contractTermType;
+
+ /**
+ * 绛剧害涓讳綋锛堝叕鍙稿叏绉帮級
+ */
+ private String signCompany;
+
+ /**
+ * 绛捐鏃ユ湡
+ */
+ private LocalDate signDate;
+
+ /**
+ * 鍚堝悓寮�濮嬫棩鏈�
+ */
+ private LocalDate startDate;
+
+ /**
+ * 鍚堝悓缁撴潫鏃ユ湡
+ *
+ * 鏃犲浐瀹氭湡闄愬悎鍚屽彲涓虹┖
+ */
+ private LocalDate endDate;
+
+ /**
+ * 璇曠敤鏈熷紑濮嬫棩鏈�
+ */
+ private LocalDate probationStartDate;
+
+ /**
+ * 璇曠敤鏈熺粨鏉熸棩鏈�
+ */
+ private LocalDate probationEndDate;
+
+ /**
+ * 璇曠敤鏈熷伐璧�
+ */
+ private BigDecimal probationSalary;
+
+ /**
+ * 杞宸ヨ祫
+ */
+ private BigDecimal regularSalary;
+
+ /**
+ * 瑙i櫎/缁堟鐘舵��
+ *
+ * 0-姝e父 1-宸茶В闄� 2-宸茬粓姝�
+ */
+ private Integer terminateStatus;
+
+ /**
+ * 瑙i櫎/缁堟鏃ユ湡
+ */
+ private LocalDate terminateDate;
+
+ /**
+ * 瑙i櫎/缁堟鍘熷洜
+ */
+ private String terminateReason;
+
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeContractMapper.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeContractMapper.java
new file mode 100644
index 0000000..6d7e74f
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeContractMapper.java
@@ -0,0 +1,190 @@
+package cn.iocoder.yudao.module.hrm.dal.mysql.employee;
+
+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.module.hrm.controller.admin.employee.vo.HrmEmployeeContractPageReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 鍛樺伐鍚堝悓 Mapper
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface HrmEmployeeContractMapper extends BaseMapperX<HrmEmployeeContractDO> {
+
+ /**
+ * 鍒版湡鎻愰啋澶╂暟
+ */
+ int REMIND_DAYS = 30;
+
+ /**
+ * 鍚堝悓鐘舵�侊細寰呯敓鏁�
+ */
+ int STATUS_PENDING = 1;
+ /**
+ * 鍚堝悓鐘舵�侊細鐢熸晥涓�
+ */
+ int STATUS_ACTIVE = 2;
+ /**
+ * 鍚堝悓鐘舵�侊細鍗冲皢鍒版湡
+ */
+ int STATUS_EXPIRING_SOON = 3;
+ /**
+ * 鍚堝悓鐘舵�侊細宸插埌鏈�
+ */
+ int STATUS_EXPIRED = 4;
+
+ /**
+ * 瑙i櫎缁堟鐘舵�侊細姝e父
+ */
+ int TERMINATE_NORMAL = 0;
+ /**
+ * 瑙i櫎缁堟鐘舵�侊細宸茶В闄�
+ */
+ int TERMINATE_CANCELED = 1;
+ /**
+ * 瑙i櫎缁堟鐘舵�侊細宸茬粓姝�
+ */
+ int TERMINATE_TERMINATED = 2;
+
+ default PageResult<HrmEmployeeContractDO> selectPage(HrmEmployeeContractPageReqVO reqVO, LocalDate today) {
+ LambdaQueryWrapperX<HrmEmployeeContractDO> query = new LambdaQueryWrapperX<HrmEmployeeContractDO>()
+ .likeIfPresent(HrmEmployeeContractDO::getContractNo, reqVO.getContractNo())
+ .eqIfPresent(HrmEmployeeContractDO::getEmployeeId, reqVO.getEmployeeId())
+ .eqIfPresent(HrmEmployeeContractDO::getContractType, reqVO.getContractType())
+ .eqIfPresent(HrmEmployeeContractDO::getContractTermType, reqVO.getContractTermType())
+ .eqIfPresent(HrmEmployeeContractDO::getTerminateStatus, reqVO.getTerminateStatus())
+ .orderByDesc(HrmEmployeeContractDO::getId);
+ // 鍔ㄦ�佺姸鎬佺瓫閫夛紙涓� terminateStatus 浜掓枼锛屼紭鍏堟寜鐘舵�佺瓫閫夛級
+ if (reqVO.getStatus() != null) {
+ appendStatusCondition(query, reqVO.getStatus(), today);
+ } else if (reqVO.getExpiryType() != null) {
+ appendExpiryCondition(query, reqVO.getExpiryType(), today);
+ }
+ return selectPage(reqVO, query);
+ }
+
+ /**
+ * 鎷兼帴鍔ㄦ�佺姸鎬佺瓫閫夋潯浠�
+ *
+ * @param status 鍚堝悓鐘舵�侊細1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝�
+ */
+ default void appendStatusCondition(LambdaQueryWrapperX<HrmEmployeeContractDO> query, Integer status, LocalDate today) {
+ switch (status) {
+ case STATUS_PENDING: // 寰呯敓鏁�
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .gt(HrmEmployeeContractDO::getStartDate, today);
+ break;
+ case STATUS_ACTIVE: // 鐢熸晥涓細鏈В闄�/缁堟锛屽凡寮�濮嬶紝涓旀湭缁撴潫锛堝惈鏃犲浐瀹氭湡闄愶級
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .le(HrmEmployeeContractDO::getStartDate, today)
+ .and(w -> w.isNull(HrmEmployeeContractDO::getEndDate)
+ .or().ge(HrmEmployeeContractDO::getEndDate, today));
+ break;
+ case STATUS_EXPIRING_SOON: // 鍗冲皢鍒版湡
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .le(HrmEmployeeContractDO::getStartDate, today)
+ .ge(HrmEmployeeContractDO::getEndDate, today)
+ .le(HrmEmployeeContractDO::getEndDate, today.plusDays(REMIND_DAYS));
+ break;
+ case STATUS_EXPIRED: // 宸插埌鏈�
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .isNotNull(HrmEmployeeContractDO::getEndDate)
+ .lt(HrmEmployeeContractDO::getEndDate, today);
+ break;
+ case 5: // 宸茶В闄�
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_CANCELED);
+ break;
+ case 6: // 宸茬粓姝�
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_TERMINATED);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 鎷兼帴鍒版湡绫诲瀷绛涢�夋潯浠讹紙寰呭姙鎻愰啋鐢級
+ *
+ * @param expiryType 鍒版湡绫诲瀷锛�1-鍗冲皢鍒版湡 2-宸插埌鏈�
+ */
+ default void appendExpiryCondition(LambdaQueryWrapperX<HrmEmployeeContractDO> query, Integer expiryType, LocalDate today) {
+ if (expiryType == 1) { // 鍗冲皢鍒版湡
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .le(HrmEmployeeContractDO::getStartDate, today)
+ .between(HrmEmployeeContractDO::getEndDate, today, today.plusDays(REMIND_DAYS));
+ } else if (expiryType == 2) { // 宸插埌鏈�
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .isNotNull(HrmEmployeeContractDO::getEndDate)
+ .lt(HrmEmployeeContractDO::getEndDate, today);
+ }
+ }
+
+ /**
+ * 缁熻鍗冲皢鍒版湡鐨勫悎鍚屾暟閲忥紙渚涘緟鍔�/棣栭〉鎻愰啋锛�
+ */
+ default Long selectCountByExpiring(LocalDate today) {
+ return selectCount(new LambdaQueryWrapperX<HrmEmployeeContractDO>()
+ .eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
+ .le(HrmEmployeeContractDO::getStartDate, today)
+ .between(HrmEmployeeContractDO::getEndDate, today, today.plusDays(REMIND_DAYS)));
+ }
+
+ /**
+ * 鏌ヨ鍚屼竴鍛樺伐鏃堕棿閲嶅彔鐨勬湭瑙i櫎/鏈粓姝㈠悎鍚�
+ *
+ * @param employeeId 鍛樺伐ID
+ * @param startDate 鏂板悎鍚屽紑濮嬫棩鏈�
+ * @param endDate 鏂板悎鍚岀粨鏉熸棩鏈燂紙鏃犲浐瀹氭湡闄愪负 null锛岃涓烘棤闄愰暱锛�
+ * @param excludeId 闇�瑕佹帓闄ょ殑鍚堝悓ID锛堟洿鏂版椂浼犺嚜韬� id锛屽垱寤烘椂浼� null锛�
+ * @return 閲嶅彔鐨勫悎鍚屽垪琛�
+ */
+ default List<HrmEmployeeContractDO> selectOverlapping(Long employeeId, LocalDate startDate,
+ LocalDate endDate, Long excludeId) {
+ LambdaQueryWrapperX<HrmEmployeeContractDO> query = new LambdaQueryWrapperX<HrmEmployeeContractDO>();
+ query.eq(HrmEmployeeContractDO::getEmployeeId, employeeId);
+ query.eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL);
+ if (excludeId != null) {
+ query.ne(HrmEmployeeContractDO::getId, excludeId);
+ }
+ if (endDate != null) {
+ // 鏂板悎鍚屾湁缁撴潫鏃ユ湡锛氫笌宸插瓨鍦ㄥ悎鍚岀殑 [startDate, endDate] 鍖洪棿瀛樺湪浜ら泦鍗冲啿绐�
+ query.lt(HrmEmployeeContractDO::getStartDate, endDate);
+ query.and(w -> w.isNull(HrmEmployeeContractDO::getEndDate)
+ .or().gt(HrmEmployeeContractDO::getEndDate, startDate));
+ }
+ // 鏂板悎鍚屾棤鍥哄畾鏈熼檺锛坋ndDate 涓� null锛夛細鍛樺伐瀛樺湪浠绘剰鏈В闄�/缁堟鍚堝悓鍗冲啿绐�
+ return selectList(query);
+ }
+
+ /**
+ * 娓呴櫎璇ュ憳宸ュ叏閮ㄥ悎鍚岀殑"褰撳墠鍚堝悓"鏍囪
+ *
+ * @param employeeId 鍛樺伐ID
+ */
+ default void clearCurrentByEmployee(Long employeeId) {
+ update(null, new LambdaUpdateWrapper<HrmEmployeeContractDO>()
+ .eq(HrmEmployeeContractDO::getEmployeeId, employeeId)
+ .set(HrmEmployeeContractDO::getIsCurrent, false));
+ }
+
+ /**
+ * 鏌ヨ鍛樺伐鐨勫叏閮ㄥ悎鍚岋紙鎸夊垱寤烘椂闂村�掑簭锛�
+ *
+ * @param employeeId 鍛樺伐ID
+ * @return 鍚堝悓鍒楄〃
+ */
+ default List<HrmEmployeeContractDO> selectListByEmployee(Long employeeId) {
+ return selectList(new LambdaQueryWrapperX<HrmEmployeeContractDO>()
+ .eq(HrmEmployeeContractDO::getEmployeeId, employeeId)
+ .orderByDesc(HrmEmployeeContractDO::getId));
+ }
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
index dbcbdd9..44009fd 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -52,6 +52,11 @@
*/
public static final String RESIGNATION_APPLICATION_NO_PREFIX = "RA";
+ /**
+ * 鍛樺伐鍚堝悓缂栧彿鍓嶇紑
+ */
+ public static final String EMPLOYEE_CONTRACT_NO_PREFIX = "HT";
+
@Resource
private StringRedisTemplate stringRedisTemplate;
@@ -96,6 +101,16 @@
}
/**
+ * 鐢熸垚鍛樺伐鍚堝悓缂栧彿锛屾牸寮忥細HT + yyyyMMdd + 4浣嶈嚜澧�
+ * 渚嬪锛欻T202401010001
+ *
+ * @return 鍛樺伐鍚堝悓缂栧彿
+ */
+ public String generateEmployeeContractNo() {
+ return generate(EMPLOYEE_CONTRACT_NO_PREFIX, 4);
+ }
+
+ /**
* 鐢熸垚钖叕鏍哥畻缂栧彿锛屾牸寮忥細SC + yyyyMMdd + 4浣嶈嚜澧�
* 渚嬪锛歋C202401010001
*
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractService.java
new file mode 100644
index 0000000..2d6167c
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractService.java
@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.hrm.service.employee;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractPageReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractSaveReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractTerminateReqVO;
+
+/**
+ * 鍛樺伐鍚堝悓 Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface HrmEmployeeContractService {
+
+ /**
+ * 鍒涘缓鍛樺伐鍚堝悓
+ *
+ * @param createReqVO 鍒涘缓璇锋眰
+ * @return 鍚堝悓ID
+ */
+ Long createContract(HrmEmployeeContractSaveReqVO createReqVO);
+
+ /**
+ * 鏇存柊鍛樺伐鍚堝悓
+ *
+ * @param updateReqVO 鏇存柊璇锋眰
+ */
+ void updateContract(HrmEmployeeContractSaveReqVO updateReqVO);
+
+ /**
+ * 鍒犻櫎鍛樺伐鍚堝悓
+ *
+ * @param id 鍚堝悓ID
+ */
+ void deleteContract(Long id);
+
+ /**
+ * 瑙i櫎/缁堟鍛樺伐鍚堝悓
+ *
+ * @param reqVO 瑙i櫎/缁堟璇锋眰
+ */
+ void terminateContract(HrmEmployeeContractTerminateReqVO reqVO);
+
+ /**
+ * 鑾峰緱鍛樺伐鍚堝悓璇︽儏
+ *
+ * @param id 鍚堝悓ID
+ * @return 鍚堝悓璇︽儏锛堝惈闄勪欢銆佸憳宸ヤ俊鎭�佸姩鎬佺姸鎬侊級
+ */
+ HrmEmployeeContractRespVO getContract(Long id);
+
+ /**
+ * 鑾峰緱鍛樺伐鍚堝悓鍒嗛〉
+ *
+ * @param pageReqVO 鍒嗛〉璇锋眰
+ * @return 鍚堝悓鍒嗛〉缁撴灉锛堝惈鍛樺伐淇℃伅銆佸姩鎬佺姸鎬侊級
+ */
+ PageResult<HrmEmployeeContractRespVO> getContractPage(HrmEmployeeContractPageReqVO pageReqVO);
+
+ /**
+ * 缁熻鍗冲皢鍒版湡鐨勫悎鍚屾暟閲忥紙渚涘緟鍔�/棣栭〉鎻愰啋锛�
+ *
+ * @return 鍗冲皢鍒版湡鍚堝悓鏁伴噺
+ */
+ Long getExpiringCount();
+
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractServiceImpl.java
new file mode 100644
index 0000000..7243ed7
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeContractServiceImpl.java
@@ -0,0 +1,321 @@
+package cn.iocoder.yudao.module.hrm.service.employee;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractPageReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractSaveReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractTerminateReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
+import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper;
+import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
+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.storage.StorageAttachmentApi;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
+
+/**
+ * 鍛樺伐鍚堝悓 Service 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+@Validated
+public class HrmEmployeeContractServiceImpl implements HrmEmployeeContractService {
+
+ /**
+ * 闄勪欢涓氬姟璁板綍绫诲瀷
+ */
+ private static final String RECORD_TYPE = "hrm_employee_contract";
+
+ @Resource
+ private HrmEmployeeContractMapper contractMapper;
+ @Resource
+ private HrmEmployeeService employeeService;
+ @Resource
+ private DeptApi deptApi;
+ @Resource
+ private StorageAttachmentApi storageAttachmentApi;
+ @Resource
+ private HrmNoRedisDAO noRedisDAO;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createContract(HrmEmployeeContractSaveReqVO createReqVO) {
+ // 1. 鏍¢獙鍛樺伐瀛樺湪
+ employeeService.validateEmployee(createReqVO.getEmployeeId());
+ // 2. 鏍¢獙鍚堝悓鏃ユ湡
+ validateContractDate(createReqVO);
+ // 3. 鏍¢獙鍚屼竴鍛樺伐鏃堕棿閲嶅彔鐨勫悎鍚�
+ validateNoOverlap(createReqVO.getEmployeeId(), createReqVO.getStartDate(),
+ createReqVO.getEndDate(), null);
+ // 4. 鐢熸垚鍚堝悓缂栧彿
+ String contractNo = noRedisDAO.generateEmployeeContractNo();
+ // 5. 鏌ヨ璇ュ憳宸ュ凡鏈夊悎鍚岋紝纭畾缁鍏崇郴
+ List<HrmEmployeeContractDO> existing = contractMapper.selectListByEmployee(createReqVO.getEmployeeId());
+ // 6. 鎻掑叆鍚堝悓璁板綍锛堟柊绛惧悎鍚屽嵆鍛樺伐褰撳墠鍚堝悓锛�
+ HrmEmployeeContractDO contract = BeanUtils.toBean(createReqVO, HrmEmployeeContractDO.class);
+ contract.setContractNo(contractNo);
+ contract.setIsCurrent(true);
+ // 缁锛氫紭鍏堜娇鐢ㄥ墠绔寚瀹氱殑 parentId锛堢敤鎴风偣鍑荤殑閭d唤鍚堝悓锛夛紝鍚﹀垯鑷姩鍏宠仈鍛樺伐鏈�杩戜竴浠藉悎鍚�
+ if (createReqVO.getParentId() == null) {
+ contract.setParentId(CollUtil.isEmpty(existing) ? null : existing.get(0).getId());
+ }
+ if (contract.getTerminateStatus() == null) {
+ contract.setTerminateStatus(HrmEmployeeContractMapper.TERMINATE_NORMAL);
+ }
+ // 7. 鍏堟竻闄ゆ棫鍚堝悓鐨�"褰撳墠鍚堝悓"鏍囪锛屽啀鎻掑叆鏂板悎鍚�
+ contractMapper.clearCurrentByEmployee(createReqVO.getEmployeeId());
+ contractMapper.insert(contract);
+ // 8. 缁戝畾闄勪欢
+ if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
+ storageAttachmentApi.bindAttachments("file", RECORD_TYPE, contract.getId(), createReqVO.getBlobIds());
+ }
+ return contract.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateContract(HrmEmployeeContractSaveReqVO updateReqVO) {
+ // 1. 鏍¢獙瀛樺湪
+ validateContractExists(updateReqVO.getId());
+ // 2. 鏍¢獙鍛樺伐瀛樺湪
+ employeeService.validateEmployee(updateReqVO.getEmployeeId());
+ // 3. 鏍¢獙鍚堝悓鏃ユ湡
+ validateContractDate(updateReqVO);
+ // 4. 鏍¢獙鍚屼竴鍛樺伐鏃堕棿閲嶅彔鐨勫悎鍚岋紙鎺掗櫎鑷韩锛�
+ validateNoOverlap(updateReqVO.getEmployeeId(), updateReqVO.getStartDate(),
+ updateReqVO.getEndDate(), updateReqVO.getId());
+ // 5. 鏇存柊鍚堝悓璁板綍
+ HrmEmployeeContractDO updateObj = BeanUtils.toBean(updateReqVO, HrmEmployeeContractDO.class);
+ contractMapper.updateById(updateObj);
+ // 6. 鏇存柊闄勪欢
+ storageAttachmentApi.updateAttachments("file", RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteContract(Long id) {
+ // 1. 鏍¢獙瀛樺湪
+ validateContractExists(id);
+ // 2. 鍒犻櫎鍚堝悓
+ contractMapper.deleteById(id);
+ // 3. 鍒犻櫎闄勪欢
+ storageAttachmentApi.deleteAttachmentsByRecord(RECORD_TYPE, id);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void terminateContract(HrmEmployeeContractTerminateReqVO reqVO) {
+ // 1. 鏍¢獙瀛樺湪
+ HrmEmployeeContractDO contract = validateContractExists(reqVO.getId());
+ // 2. 鏍¢獙瑙i櫎/缁堟鍙傛暟
+ Integer terminateStatus = reqVO.getTerminateStatus();
+ if (terminateStatus == null
+ || (terminateStatus != HrmEmployeeContractMapper.TERMINATE_CANCELED
+ && terminateStatus != HrmEmployeeContractMapper.TERMINATE_TERMINATED)
+ || reqVO.getTerminateDate() == null || StrUtil.isBlank(reqVO.getTerminateReason())) {
+ throw exception(EMPLOYEE_CONTRACT_TERMINATE_REQUIRE_DATE_REASON);
+ }
+ // 3. 宸茶В闄�/宸茬粓姝㈢殑鍚堝悓涓嶅厑璁搁噸澶嶆搷浣�
+ if (contract.getTerminateStatus() != null
+ && contract.getTerminateStatus() != HrmEmployeeContractMapper.TERMINATE_NORMAL) {
+ throw exception(EMPLOYEE_CONTRACT_TERMINATE_REQUIRE_DATE_REASON);
+ }
+ // 4. 鏇存柊瑙i櫎/缁堟淇℃伅
+ HrmEmployeeContractDO updateObj = new HrmEmployeeContractDO();
+ updateObj.setId(contract.getId());
+ updateObj.setTerminateStatus(terminateStatus);
+ updateObj.setTerminateDate(reqVO.getTerminateDate());
+ updateObj.setTerminateReason(reqVO.getTerminateReason());
+ // 5. 鑻ヨВ闄�/缁堟鐨勬槸鍛樺伐褰撳墠鍚堝悓锛屾竻闄ゅ綋鍓嶆爣璁�
+ if (Boolean.TRUE.equals(contract.getIsCurrent())) {
+ updateObj.setIsCurrent(false);
+ }
+ contractMapper.updateById(updateObj);
+ }
+
+ @Override
+ public HrmEmployeeContractRespVO getContract(Long id) {
+ HrmEmployeeContractDO contract = validateContractExists(id);
+ HrmEmployeeContractRespVO respVO = BeanUtils.toBean(contract, HrmEmployeeContractRespVO.class);
+ // 濉厖鍛樺伐淇℃伅
+ HrmEmployeeDO employee = employeeService.getEmployee(contract.getEmployeeId());
+ if (employee != null) {
+ Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(Collections.singleton(employee.getDeptId()));
+ fillEmployeeInfo(respVO, employee, deptMap);
+ }
+ // 濉厖缁涓婁竴浠藉悎鍚岀紪鍙�
+ fillParentNo(respVO, contract.getParentId());
+ // 濉厖闄勪欢
+ respVO.setAttachmentList(storageAttachmentApi.listAttachments(RECORD_TYPE, id));
+ // 鍔ㄦ�佽绠楃姸鎬�
+ respVO.setStatus(calculateStatus(contract, LocalDate.now()));
+ return respVO;
+ }
+
+ @Override
+ public PageResult<HrmEmployeeContractRespVO> getContractPage(HrmEmployeeContractPageReqVO pageReqVO) {
+ LocalDate today = LocalDate.now();
+ PageResult<HrmEmployeeContractDO> pageResult = contractMapper.selectPage(pageReqVO, today);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return PageResult.empty();
+ }
+ // 鎵归噺鑾峰彇鍛樺伐淇℃伅
+ Set<Long> employeeIds = pageResult.getList().stream()
+ .map(HrmEmployeeContractDO::getEmployeeId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Map<Long, HrmEmployeeDO> employeeMap = CollUtil.isEmpty(employeeIds) ? Collections.emptyMap() :
+ employeeService.getEmployeeList(employeeIds).stream()
+ .collect(Collectors.toMap(HrmEmployeeDO::getId, e -> e));
+ // 鎵归噺鑾峰彇閮ㄩ棬淇℃伅
+ Set<Long> deptIds = employeeMap.values().stream()
+ .map(HrmEmployeeDO::getDeptId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Map<Long, DeptRespDTO> deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds);
+ // 鎵归噺鑾峰彇缁涓婁竴浠藉悎鍚屼俊鎭�
+ Set<Long> parentIds = pageResult.getList().stream()
+ .map(HrmEmployeeContractDO::getParentId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Map<Long, HrmEmployeeContractDO> parentMap = CollUtil.isEmpty(parentIds) ? Collections.emptyMap() :
+ contractMapper.selectList(HrmEmployeeContractDO::getId, parentIds).stream()
+ .collect(Collectors.toMap(HrmEmployeeContractDO::getId, c -> c));
+ // 缁勮 VO
+ List<HrmEmployeeContractRespVO> list = pageResult.getList().stream().map(contract -> {
+ HrmEmployeeContractRespVO respVO = BeanUtils.toBean(contract, HrmEmployeeContractRespVO.class);
+ fillEmployeeInfo(respVO, employeeMap.get(contract.getEmployeeId()), deptMap);
+ fillParentNo(respVO, contract.getParentId(), parentMap);
+ respVO.setStatus(calculateStatus(contract, today));
+ return respVO;
+ }).collect(Collectors.toList());
+ return new PageResult<>(list, pageResult.getTotal());
+ }
+
+ @Override
+ public Long getExpiringCount() {
+ return contractMapper.selectCountByExpiring(LocalDate.now());
+ }
+
+ private HrmEmployeeContractDO validateContractExists(Long id) {
+ HrmEmployeeContractDO contract = contractMapper.selectById(id);
+ if (contract == null) {
+ throw exception(EMPLOYEE_CONTRACT_NOT_EXISTS);
+ }
+ return contract;
+ }
+
+ /**
+ * 鏍¢獙鍚屼竴鍛樺伐鏃堕棿閲嶅彔鐨勫悎鍚岋紝閬垮厤涓�涓憳宸ュ悓鏃跺瓨鍦ㄥ浠芥湭瑙i櫎/鏈粓姝㈢殑鐢熸晥鍚堝悓
+ */
+ private void validateNoOverlap(Long employeeId, LocalDate startDate, LocalDate endDate, Long excludeId) {
+ List<HrmEmployeeContractDO> overlaps = contractMapper.selectOverlapping(employeeId, startDate, endDate, excludeId);
+ if (CollUtil.isNotEmpty(overlaps)) {
+ throw exception(EMPLOYEE_CONTRACT_DATE_OVERLAP);
+ }
+ }
+
+ /**
+ * 濉厖缁涓婁竴浠藉悎鍚岀紪鍙�
+ */
+ private void fillParentNo(HrmEmployeeContractRespVO respVO, Long parentId) {
+ if (parentId != null) {
+ HrmEmployeeContractDO parent = contractMapper.selectById(parentId);
+ if (parent != null) {
+ respVO.setParentNo(parent.getContractNo());
+ }
+ }
+ }
+
+ /**
+ * 鎵归噺濉厖缁涓婁竴浠藉悎鍚岀紪鍙�
+ */
+ private void fillParentNo(HrmEmployeeContractRespVO respVO, Long parentId, Map<Long, HrmEmployeeContractDO> parentMap) {
+ if (parentId != null) {
+ HrmEmployeeContractDO parent = parentMap.get(parentId);
+ if (parent != null) {
+ respVO.setParentNo(parent.getContractNo());
+ }
+ }
+ }
+
+ private void validateContractDate(HrmEmployeeContractSaveReqVO reqVO) {
+ // 闈炴棤鍥哄畾鏈熼檺鍚堝悓锛堟湡闄愮被鍨� != 2锛夊繀椤诲~鍐欑粨鏉熸棩鏈�
+ if (!Integer.valueOf(2).equals(reqVO.getContractTermType()) && reqVO.getEndDate() == null) {
+ throw exception(EMPLOYEE_CONTRACT_END_DATE_REQUIRE_FOR_FIXED);
+ }
+ // 寮�濮嬫棩鏈熶笉鑳芥櫄浜庣粨鏉熸棩鏈�
+ if (reqVO.getStartDate() != null && reqVO.getEndDate() != null
+ && reqVO.getStartDate().isAfter(reqVO.getEndDate())) {
+ throw exception(EMPLOYEE_CONTRACT_DATE_INVALID);
+ }
+ // 璇曠敤鏈熷紑濮嬫棩鏈熶笉鑳芥櫄浜庤瘯鐢ㄦ湡缁撴潫鏃ユ湡
+ if (reqVO.getProbationStartDate() != null && reqVO.getProbationEndDate() != null
+ && reqVO.getProbationStartDate().isAfter(reqVO.getProbationEndDate())) {
+ throw exception(EMPLOYEE_CONTRACT_DATE_INVALID);
+ }
+ }
+
+ /**
+ * 鍔ㄦ�佽绠楀悎鍚岀姸鎬�
+ *
+ * @param contract 鍚堝悓璁板綍
+ * @param today 褰撳墠鏃ユ湡
+ * @return 鐘舵�侊細1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝�
+ */
+ private Integer calculateStatus(HrmEmployeeContractDO contract, LocalDate today) {
+ Integer terminateStatus = contract.getTerminateStatus();
+ if (terminateStatus != null && terminateStatus == HrmEmployeeContractMapper.TERMINATE_CANCELED) {
+ return HrmEmployeeContractPageReqVO.STATUS_CANCELED;
+ }
+ if (terminateStatus != null && terminateStatus == HrmEmployeeContractMapper.TERMINATE_TERMINATED) {
+ return HrmEmployeeContractPageReqVO.STATUS_TERMINATED;
+ }
+ LocalDate endDate = contract.getEndDate();
+ if (endDate != null && endDate.isBefore(today)) {
+ return HrmEmployeeContractPageReqVO.STATUS_EXPIRED;
+ }
+ LocalDate startDate = contract.getStartDate();
+ if (startDate != null && startDate.isAfter(today)) {
+ return HrmEmployeeContractPageReqVO.STATUS_PENDING;
+ }
+ if (endDate != null && !endDate.isAfter(today.plusDays(HrmEmployeeContractMapper.REMIND_DAYS))) {
+ return HrmEmployeeContractPageReqVO.STATUS_EXPIRING_SOON;
+ }
+ return HrmEmployeeContractPageReqVO.STATUS_ACTIVE;
+ }
+
+ private void fillEmployeeInfo(HrmEmployeeContractRespVO respVO, HrmEmployeeDO employee,
+ Map<Long, DeptRespDTO> deptMap) {
+ if (employee != null) {
+ respVO.setEmployeeName(employee.getName());
+ respVO.setEmployeeNo(employee.getEmployeeNo());
+ if (employee.getDeptId() != null && deptMap != null) {
+ DeptRespDTO dept = deptMap.get(employee.getDeptId());
+ if (dept != null) {
+ respVO.setDeptName(dept.getName());
+ }
+ }
+ }
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
index 67c57d7..599e513 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -35,6 +35,7 @@
// ===== ERP 閲囪喘 =====
ERP_PURCHASE_REQUEST("erp_purchase_request", "ERP 閲囪喘鐢宠"),
ERP_PURCHASE_ORDER("erp_purchase_order", "ERP 閲囪喘璁㈠崟"),
+ ERP_PURCHASE_INVOICE("erp_purchase_invoice", "ERP 閲囪喘鏉ョエ"),
// ===== ERP 閿�鍞� =====
ERP_SALE_ORDER("erp_sale_order", "ERP 閿�鍞鍗�"),
--
Gitblit v1.9.3