From f93a8f3d091f1e9d1b9c2df246ad39df0e14cfdd Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 12 五月 2026 15:22:36 +0800
Subject: [PATCH] feat(account): 实现总账科目树形结构及关联功能
---
src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java | 12
src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java | 25
doc/20260512_财务管理模块前端联调文档.md | 288 +++++
src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java | 101 +
src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java | 12
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java | 15
src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java | 206 +++
src/main/resources/mapper/account/AccountSubjectMapper.xml | 10
src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java | 63 +
doc/20260512_add_parent_id_to_account_subject.sql | 5
src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java | 299 +++++
src/main/java/com/ruoyi/account/service/AccountSubjectService.java | 8
src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java | 12
src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java | 13
doc/20260512_AccountSubject树形改造前端修改文档.md | 185 +++
src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java | 250 ++++
src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java | 17
src/main/resources/application-dev.yml | 2
src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java | 39
src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java | 13
src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java | 27
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java | 43
doc/20260512_create_financial_management_tables.sql | 104 ++
src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java | 26
src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java | 362 ++++++
src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java | 85 +
src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java | 26
src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java | 5
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java | 13
src/main/java/com/ruoyi/account/pojo/AccountSubject.java | 6
src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java | 98 +
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java | 37
src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java | 28
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java | 53 +
src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java | 17
src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java | 13
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java | 20
src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java | 63 +
src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java | 231 ++++
src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java | 22
src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java | 83 +
src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java | 69 +
src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java | 21
src/main/java/com/ruoyi/account/controller/AccountSubjectController.java | 8
src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml | 74 +
45 files changed, 3,083 insertions(+), 26 deletions(-)
diff --git "a/doc/20260512_AccountSubject\346\240\221\345\275\242\346\224\271\351\200\240\345\211\215\347\253\257\344\277\256\346\224\271\346\226\207\346\241\243.md" "b/doc/20260512_AccountSubject\346\240\221\345\275\242\346\224\271\351\200\240\345\211\215\347\253\257\344\277\256\346\224\271\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..c798c40
--- /dev/null
+++ "b/doc/20260512_AccountSubject\346\240\221\345\275\242\346\224\271\351\200\240\345\211\215\347\253\257\344\277\256\346\224\271\346\226\207\346\241\243.md"
@@ -0,0 +1,185 @@
+# AccountSubject 鏍戝舰鏀归�犲墠绔慨鏀规枃妗�
+
+鏇存柊鏃堕棿锛�2026-05-12
+
+## 1. 鍙樻洿鑳屾櫙
+
+`AccountSubjectController` 宸叉敼涓虹埗瀛愬眰绾ч�掑綊妯″紡锛宍/accountSubject/list` 鐜板湪杩斿洖鏍戝舰缁撴瀯锛坄children` 閫掑綊锛夛紝涓嶅啀鏄崟绾殑骞抽摵鍒楄〃銆�
+
+---
+
+## 2. 鍚庣鎺ュ彛鍙樺寲
+
+### 2.1 鏌ヨ鎺ュ彛锛堝凡鍙樻洿涓烘爲锛�
+
+- URL锛歚GET /accountSubject/list`
+- 鍏ュ弬锛氫繚鎸佷笉鍙橈紙`current,size,subjectCode,subjectName,subjectType,status`锛�
+- 鍑哄弬锛氫粛鏄垎椤靛3锛坄records,total`锛夛紝浣� `records` 鍙樹负鏍戣妭鐐规暟缁勶紙鏍硅妭鐐瑰垎椤碉紝瀛愯妭鐐归�掑綊鍐呭祵锛�
+
+绀轰緥锛�
+
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛",
+ "data": {
+ "records": [
+ {
+ "id": 1,
+ "parentId": null,
+ "subjectCode": "1002",
+ "subjectName": "閾惰瀛樻",
+ "subjectType": "璧勪骇绫�",
+ "balanceDirection": "鍊熸柟",
+ "status": 0,
+ "leaf": false,
+ "children": [
+ {
+ "id": 2,
+ "parentId": 1,
+ "subjectCode": "100201",
+ "subjectName": "宸ヨ瀛樻",
+ "subjectType": "璧勪骇绫�",
+ "balanceDirection": "鍊熸柟",
+ "status": 0,
+ "leaf": true,
+ "children": []
+ }
+ ]
+ }
+ ],
+ "total": 1
+ }
+}
+```
+
+### 2.2 鏂板/缂栬緫鎺ュ彛瀛楁鍙樺寲
+
+- `POST /accountSubject/add`
+- `PUT /accountSubject/edit`
+
+鏂板鏀寔瀛楁锛�
+
+- `parentId`锛氱埗鑺傜偣ID锛堜负绌鸿〃绀烘牴鑺傜偣锛�
+
+绀轰緥锛�
+
+```json
+{
+ "id": 2,
+ "parentId": 1,
+ "subjectCode": "100201",
+ "subjectName": "宸ヨ瀛樻",
+ "subjectType": "璧勪骇绫�",
+ "balanceDirection": "鍊熸柟",
+ "status": 0,
+ "remark": ""
+}
+```
+
+### 2.3 鍒犻櫎鎺ュ彛琛屼负鍙樺寲
+
+- `DELETE /accountSubject/remove/{ids}`
+
+琛屼负锛�
+
+1. 鍒犻櫎鐖惰妭鐐规椂浼氶�掑綊鍒犻櫎鎵�鏈夊瓙瀛欒妭鐐广��
+2. 鑻ヤ换鎰忓緟鍒犻櫎鑺傜偣锛堝惈瀛愬瓩锛夊凡琚� `fin_voucher_entry.subject_code` 寮曠敤锛屽垯鏁翠綋鍒犻櫎澶辫触銆�
+
+---
+
+## 3. 鍓嶇鏀归�犳竻鍗�
+
+## 3.1 鎬昏处绉戠洰绠$悊椤�
+
+鏂囦欢锛歚src/views/financialManagement/generalLedger/index.vue`
+
+### 蹇呮敼椤�
+
+1. 鏂板鈥滅埗绉戠洰鈥濋�夋嫨鎺т欢锛坄el-cascader` 鎴� `el-tree-select`锛夛紝淇濆瓨鏃跺甫 `parentId`銆�
+2. 鍒楄〃鏀逛负鏍戣〃灞曠ず锛堟帹鑽� `el-table` + `row-key` + `tree-props`锛夈��
+3. 鎼滅储閫昏緫淇濇寔涓嶅彉锛屼絾瑕佸吋瀹� `records` 涓烘爲缁撴瀯銆�
+
+---
+
+## 3.2 鍑瘉椤电鐩笅鎷�
+
+鏂囦欢锛歚src/views/financialManagement/voucher/index.vue`
+
+褰撳墠鍑瘉鍒嗗綍浣跨敤 `el-select`锛堝钩閾猴級銆�
+鍚庣宸茶繑鍥炴爲锛岄渶瑕佸墠绔墎骞冲寲鍚庡啀缁戝畾涓嬫媺銆�
+
+绀轰緥锛堝彲澶嶇敤锛夛細
+
+```js
+const flattenSubjectTree = (nodes, result = []) => {
+ (nodes || []).forEach(node => {
+ result.push({
+ code: node.subjectCode,
+ name: node.subjectName,
+ id: node.id,
+ parentId: node.parentId
+ });
+ if (node.children?.length) {
+ flattenSubjectTree(node.children, result);
+ }
+ });
+ return result;
+};
+
+// list 鎺ュ彛杩斿洖鍚庯細
+const treeRecords = response.data?.records || [];
+subjectList.value = flattenSubjectTree(treeRecords);
+```
+
+---
+
+## 3.3 绉戠洰鎬昏处/鏄庣粏璐﹂〉绾ц仈
+
+鏂囦欢锛�
+
+- `src/views/financialManagement/voucher/generalLedger.vue`
+- `src/views/financialManagement/voucher/detailLedger.vue`
+
+褰撳墠閫昏緫鏄妸 `records` 寮哄埗鏄犲皠鎴� `children: []`锛岄渶瑕佸垹闄よ繖娈碘�滃钩閾烘敼閫犫�濓紝鐩存帴浣跨敤鍚庣鏍戙��
+
+绀轰緥锛堝彲澶嶇敤锛夛細
+
+```js
+const toCascaderTree = (nodes = []) =>
+ nodes
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => ({
+ code: item.subjectCode,
+ name: item.subjectName,
+ children: toCascaderTree(item.children || [])
+ }));
+
+subjectOptions.value = toCascaderTree(response.data?.records || []);
+```
+
+---
+
+## 4. 寤鸿鐨勫墠绔瓧娈电害瀹�
+
+寤鸿鍦ㄥ墠绔� `form` 澧炲姞锛�
+
+- `parentId: null`
+
+骞跺湪缂栬緫鍥炲~鏃朵繚鎸� `parentId`銆�
+
+---
+
+## 5. 鑱旇皟娉ㄦ剰浜嬮」
+
+1. `/accountSubject/list` 鐨� `total` 鏄牴鑺傜偣鏁伴噺锛屼笉鏄叏閲忚妭鐐规暟銆�
+2. 鑻ラ〉闈粛鎸夊钩閾� `records.map(...)` 澶勭悊锛屼細涓㈠け瀛愯妭鐐广��
+3. 鍒犻櫎绉戠洰澶辫触鏃讹紝浼樺厛妫�鏌ユ槸鍚﹁鍑瘉鍒嗗綍寮曠敤銆�
+4. 淇濆瓨澶辫触鍑虹幇鈥滅埗绉戠洰涓嶈兘鏄綋鍓嶇鐩垨鍏跺瓙绉戠洰鈥濇椂锛岄渶瑕佸墠绔檺鍒剁埗鑺傜偣鍙�夎寖鍥淬��
+
+---
+
+## 6. 鏁版嵁搴撳瓧娈佃姹�
+
+`account_subject` 闇�瑕佸寘鍚� `parent_id` 瀛楁锛坄bigint`锛屽彲绌猴級銆�
+鑻ョ嚎涓婂簱灏氭湭娣诲姞锛岃鍏堟墽琛� DDL 鍐嶈仈璋冦��
diff --git a/doc/20260512_add_parent_id_to_account_subject.sql b/doc/20260512_add_parent_id_to_account_subject.sql
new file mode 100644
index 0000000..cf33a33
--- /dev/null
+++ b/doc/20260512_add_parent_id_to_account_subject.sql
@@ -0,0 +1,5 @@
+-- account_subject 澧炲姞鐖剁骇绉戠洰瀛楁锛堟爲褰㈢粨鏋勶級
+ALTER TABLE `account_subject`
+ADD COLUMN `parent_id` bigint NULL COMMENT '鐖剁鐩甀D锛堜负绌鸿〃绀烘牴鑺傜偣锛�' AFTER `id`;
+
+CREATE INDEX `idx_account_subject_parent_id` ON `account_subject` (`parent_id`);
diff --git a/doc/20260512_create_financial_management_tables.sql b/doc/20260512_create_financial_management_tables.sql
new file mode 100644
index 0000000..db3caad
--- /dev/null
+++ b/doc/20260512_create_financial_management_tables.sql
@@ -0,0 +1,104 @@
+-- 璐㈠姟绠$悊妯″潡寤鸿〃鑴氭湰锛堝浐瀹氳祫浜�/鏃犲舰璧勪骇/鍑瘉/绉戠洰璐︼級
+-- 璇存槑锛�
+-- 1) 鎬昏处绉戠洰缁х画澶嶇敤宸叉湁琛� account_subject锛屼笉閲嶅鍒涘缓 fin_account_subject銆�
+-- 2) 閲戦瀛楁缁熶竴 decimal(18,2)銆�
+
+CREATE TABLE IF NOT EXISTS `fin_fixed_asset` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `asset_code` varchar(64) NOT NULL COMMENT '璧勪骇缂栧彿',
+ `asset_name` varchar(128) NOT NULL COMMENT '璧勪骇鍚嶇О',
+ `category` varchar(64) NOT NULL COMMENT '璧勪骇绫诲埆',
+ `specification` varchar(255) DEFAULT NULL COMMENT '瑙勬牸鍨嬪彿',
+ `purchase_date` date NOT NULL COMMENT '璐疆鏃ユ湡',
+ `original_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璧勪骇鍘熷��',
+ `useful_life` int NOT NULL DEFAULT '1' COMMENT '浣跨敤骞撮檺(骞�)',
+ `residual_rate` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '娈嬪�肩巼(%)',
+ `accumulated_depreciation` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '绱鎶樻棫',
+ `net_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍑�鍊�',
+ `location` varchar(255) DEFAULT NULL COMMENT '瀛樻斁鍦扮偣',
+ `department` varchar(128) DEFAULT NULL COMMENT '浣跨敤閮ㄩ棬',
+ `keeper` varchar(64) DEFAULT NULL COMMENT '淇濈浜�',
+ `status` varchar(32) NOT NULL DEFAULT 'in_use' COMMENT '鐘舵��: in_use/idle/repair/scrapped',
+ `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+ `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+ `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_fin_fixed_asset_code` (`asset_code`),
+ KEY `idx_fin_fixed_asset_status` (`status`),
+ KEY `idx_fin_fixed_asset_category` (`category`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鍥哄畾璧勪骇';
+
+CREATE TABLE IF NOT EXISTS `fin_intangible_asset` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `asset_code` varchar(64) NOT NULL COMMENT '璧勪骇缂栧彿',
+ `asset_name` varchar(128) NOT NULL COMMENT '璧勪骇鍚嶇О',
+ `category` varchar(64) NOT NULL COMMENT '璧勪骇绫诲埆',
+ `certificate_no` varchar(128) DEFAULT NULL COMMENT '璇佷功缂栧彿',
+ `acquisition_date` date NOT NULL COMMENT '鍙栧緱鏃ユ湡',
+ `original_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璧勪骇鍘熷��',
+ `amortization_period` int NOT NULL DEFAULT '1' COMMENT '鎽婇攢骞撮檺(骞�)',
+ `residual_rate` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '娈嬪�肩巼(%)',
+ `accumulated_amortization` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '绱鎽婇攢',
+ `net_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍑�鍊�',
+ `validity_date` date DEFAULT NULL COMMENT '鏈夋晥鏈熻嚦',
+ `status` varchar(32) NOT NULL DEFAULT 'in_use' COMMENT '鐘舵��: in_use/expired/amortized',
+ `description` varchar(1000) DEFAULT NULL COMMENT '璧勪骇鎻忚堪',
+ `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+ `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+ `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_fin_intangible_asset_code` (`asset_code`),
+ KEY `idx_fin_intangible_asset_status` (`status`),
+ KEY `idx_fin_intangible_asset_category` (`category`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鏃犲舰璧勪骇';
+
+CREATE TABLE IF NOT EXISTS `fin_voucher` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `voucher_no` varchar(64) NOT NULL COMMENT '鍑瘉瀛楀彿',
+ `voucher_date` date NOT NULL COMMENT '鍑瘉鏃ユ湡',
+ `summary` varchar(500) DEFAULT NULL COMMENT '鎽樿',
+ `debit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍊熸柟鍚堣',
+ `credit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璐锋柟鍚堣',
+ `creator` varchar(64) DEFAULT NULL COMMENT '鍒跺崟浜�',
+ `status` varchar(32) NOT NULL DEFAULT 'unposted' COMMENT '鐘舵��: unposted/posted/cancelled',
+ `attachment_count` int NOT NULL DEFAULT '0' COMMENT '闄勪欢寮犳暟',
+ `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+ `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+ `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_fin_voucher_no` (`voucher_no`),
+ KEY `idx_fin_voucher_date` (`voucher_date`),
+ KEY `idx_fin_voucher_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鍑瘉涓昏〃';
+
+CREATE TABLE IF NOT EXISTS `fin_voucher_entry` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `voucher_id` bigint NOT NULL COMMENT '鍑瘉ID',
+ `row_no` int NOT NULL DEFAULT '1' COMMENT '琛屽彿',
+ `subject_code` varchar(64) NOT NULL COMMENT '绉戠洰缂栫爜',
+ `subject_name` varchar(128) DEFAULT NULL COMMENT '绉戠洰鍚嶇О',
+ `summary` varchar(500) DEFAULT NULL COMMENT '鎽樿',
+ `debit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '鍊熸柟閲戦',
+ `credit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '璐锋柟閲戦',
+ `auxiliary_type` varchar(32) DEFAULT NULL COMMENT '杈呭姪鏍哥畻绫诲瀷',
+ `auxiliary_id` varchar(64) DEFAULT NULL COMMENT '杈呭姪鏍哥畻瀵硅薄ID',
+ `auxiliary_name` varchar(128) DEFAULT NULL COMMENT '杈呭姪鏍哥畻瀵硅薄鍚嶇О',
+ `create_user` varchar(64) DEFAULT NULL COMMENT '鍒涘缓浜�',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_user` varchar(64) DEFAULT NULL COMMENT '淇敼浜�',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '淇敼鏃堕棿',
+ `dept_id` bigint DEFAULT NULL COMMENT '閮ㄩ棬ID',
+ PRIMARY KEY (`id`),
+ KEY `idx_fin_voucher_entry_voucher` (`voucher_id`),
+ KEY `idx_fin_voucher_entry_subject` (`subject_code`),
+ KEY `idx_fin_voucher_entry_aux` (`auxiliary_type`, `auxiliary_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='鍑瘉鍒嗗綍';
diff --git "a/doc/20260512_\350\264\242\345\212\241\347\256\241\347\220\206\346\250\241\345\235\227\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260512_\350\264\242\345\212\241\347\256\241\347\220\206\346\250\241\345\235\227\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..1804d82
--- /dev/null
+++ "b/doc/20260512_\350\264\242\345\212\241\347\256\241\347\220\206\346\250\241\345\235\227\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,288 @@
+# 璐㈠姟绠$悊妯″潡鍓嶇鑱旇皟鏂囨。锛坅ccount 妯″潡锛�
+
+鏇存柊鏃堕棿锛�2026-05-12
+
+## 1. 閫氱敤璇存槑
+
+### 1.1 鍝嶅簲缁撴瀯
+鎴愬姛鍝嶅簲锛�
+
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛",
+ "data": {}
+}
+```
+
+涓氬姟鏍¢獙澶辫触锛堜緥濡傚�熻捶涓嶅钩琛°�佸繀濉己澶憋級鐢卞叏灞�寮傚父杩斿洖锛�
+
+```json
+{
+ "code": 500,
+ "msg": "閿欒淇℃伅",
+ "data": null
+}
+```
+
+### 1.2 鍒嗛〉缁撴瀯
+鍒嗛〉鎺ュ彛缁熶竴浣跨敤 MyBatis-Plus `Page`锛�
+- 璇锋眰鍙傛暟锛歚current`銆乣size`
+- 杩斿洖锛歚data.records`銆乣data.total`
+
+---
+
+## 2. 鎬昏处绉戠洰锛堝凡鍦ㄥ師鎺ュ彛涓婂寮烘牎楠岋級
+
+鎺ュ彛淇濇寔涓嶅彉锛�
+- `GET /accountSubject/list`
+- `POST /accountSubject/add`
+- `PUT /accountSubject/edit`
+- `DELETE /accountSubject/remove/{ids}`
+- `POST /accountSubject/export`
+
+鏂板瑙勫垯锛�
+1. `subjectCode`銆乣subjectName`銆乣subjectType` 蹇呭~銆�
+2. `subjectCode` 鍞竴鏍¢獙銆�
+3. 鍒犻櫎鍓嶅仛寮曠敤鏍¢獙锛氳嫢琚嚟璇佸垎褰�(`fin_voucher_entry.subject_code`)寮曠敤锛岀姝㈠垹闄ゃ��
+
+---
+
+## 3. 鍥哄畾璧勪骇
+
+Base URL锛歚/financial/fixedAsset`
+
+### 3.1 鍒嗛〉鏌ヨ
+- `GET /page`
+- Query锛歚current,size,assetCode,assetName,category,status`
+
+### 3.2 鏂板
+- `POST /add`
+- Body锛圝SON锛夛細
+
+```json
+{
+ "assetCode": "GD20260512001",
+ "assetName": "鍔炲叕鐢佃剳",
+ "category": "electronic",
+ "specification": "ThinkPad X1",
+ "purchaseDate": "2026-05-01",
+ "originalValue": 8000.00,
+ "usefulLife": 5,
+ "residualRate": 5.00,
+ "location": "璐㈠姟閮�",
+ "department": "璐㈠姟閮�",
+ "keeper": "寮犱笁",
+ "status": "in_use",
+ "remark": "绀轰緥"
+}
+```
+
+### 3.3 淇敼
+- `PUT /update`
+- Body锛氬悓鏂板锛岄渶鍖呭惈 `id`
+
+### 3.4 鍒犻櫎
+- `DELETE /delete?ids=1&ids=2`
+
+### 3.5 鎶樻棫璁℃彁锛堟寜鏈堬級
+- `POST /depreciate`
+- Body 鍙�夛細
+ - 鍏ㄩ儴鍦ㄧ敤璧勪骇锛歚{}`
+ - 鎸囧畾璧勪骇锛歚{"ids":[1,2,3]}`
+
+鏍稿績鍏紡锛�
+- `monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)`
+- `accumulatedDepreciation += monthlyDepreciation`
+- `netValue = originalValue - accumulatedDepreciation`
+
+鐘舵�佸缓璁�硷細
+- `in_use`銆乣idle`銆乣repair`銆乣scrapped`
+
+---
+
+## 4. 鏃犲舰璧勪骇
+
+Base URL锛歚/financial/intangibleAsset`
+
+### 4.1 鍒嗛〉鏌ヨ
+- `GET /page`
+- Query锛歚current,size,assetCode,assetName,category,status`
+
+### 4.2 鏂板
+- `POST /add`
+
+```json
+{
+ "assetCode": "WX20260512001",
+ "assetName": "ERP杞欢璁稿彲",
+ "category": "software",
+ "certificateNo": "SW-001",
+ "acquisitionDate": "2026-05-01",
+ "originalValue": 50000.00,
+ "amortizationPeriod": 10,
+ "residualRate": 0.00,
+ "validityDate": "2036-05-01",
+ "status": "in_use",
+ "description": "绀轰緥",
+ "remark": ""
+}
+```
+
+### 4.3 淇敼
+- `PUT /update`
+- Body锛氬悓鏂板锛岄渶鍖呭惈 `id`
+
+### 4.4 鍒犻櫎
+- `DELETE /delete?ids=1&ids=2`
+
+### 4.5 鎽婇攢璁℃彁锛堟寜鏈堬級
+- `POST /amortize`
+- Body 鍙�夛細
+ - 鍏ㄩ儴鍦ㄧ敤璧勪骇锛歚{}`
+ - 鎸囧畾璧勪骇锛歚{"ids":[1,2,3]}`
+
+鏍稿績鍏紡锛�
+- `monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)`
+- `accumulatedAmortization += monthlyAmortization`
+- `netValue = originalValue - accumulatedAmortization`
+- 褰� `netValue <= 0` 鏃讹細`netValue=0` 涓� `status=amortized`
+
+鐘舵�佸缓璁�硷細
+- `in_use`銆乣expired`銆乣amortized`
+
+---
+
+## 5. 鍑瘉
+
+Base URL锛歚/financial/voucher`
+
+### 5.1 鍒嗛〉鏌ヨ
+- `GET /page`
+- Query锛歚current,size,voucherNo,creator,status,startDate,endDate`
+
+### 5.2 鏂板
+- `POST /add`
+
+```json
+{
+ "voucherNo": "璁�-0001",
+ "voucherDate": "2026-05-12",
+ "summary": "閿�鍞敹鍏�",
+ "creator": "寮犱笁",
+ "attachmentCount": 0,
+ "remark": "",
+ "entries": [
+ {
+ "subjectCode": "1002",
+ "subjectName": "閾惰瀛樻",
+ "summary": "鏀跺埌璐ф",
+ "debit": 1000.00,
+ "credit": 0
+ },
+ {
+ "subjectCode": "6001",
+ "subjectName": "涓昏惀涓氬姟鏀跺叆",
+ "summary": "纭鏀跺叆",
+ "debit": 0,
+ "credit": 1000.00
+ }
+ ]
+}
+```
+
+### 5.3 淇敼
+- `PUT /update`
+- Body锛氬悓鏂板锛岄渶鍖呭惈 `id`
+- 浠� `unposted` 鐘舵�佸厑璁镐慨鏀�
+
+### 5.4 杩囪处
+- `POST /post`
+
+```json
+{
+ "id": 1
+}
+```
+
+### 5.5 浣滃簾
+- `POST /cancel`
+
+```json
+{
+ "id": 1
+}
+```
+
+### 5.6 璇︽儏
+- `GET /detail/{id}`
+
+鍏抽敭鏍¢獙锛�
+1. 鍒嗗綍鑷冲皯涓�鏉℃湁鏁堣锛堢鐩笉绌猴紝涓斿�熸柟鎴栬捶鏂� > 0锛夈��
+2. 姣忔潯鏈夋晥鍒嗗綍涓嶈兘鍊熻捶鍚屾椂澶т簬 0銆�
+3. 鍊熻捶骞宠 锛歚sum(debit) == sum(credit)` 涓旈兘 > 0銆�
+4. `subjectCode` 蹇呴』瀛樺湪浜� `account_subject`銆�
+
+鐘舵�佹祦杞細
+- `unposted -> posted`
+- `unposted -> cancelled`
+
+---
+
+## 6. 绉戠洰鎬昏处
+
+### 6.1 鏌ヨ鎺ュ彛
+- `GET /financial/ledger/general`
+
+### 6.2 Query 鍙傛暟
+- `subjectCode`锛堝繀濉級
+- `startMonth`锛圷YYY-MM锛�
+- `endMonth`锛圷YYY-MM锛�
+
+### 6.3 杩斿洖瀛楁
+- `rowType`锛歚opening` / `entry` / `monthly_total` / `yearly_total`
+- `date`
+- `voucherNo`
+- `summary`
+- `debit`
+- `credit`
+- `direction`锛堝��/璐凤級
+- `balance`锛堝�熸璐疯礋锛�
+
+璇存槑锛�
+- 绉戠洰鏀寔鈥滄寚瀹氱鐩強鍏朵笅绾э紙鍓嶇紑鍖归厤锛夆�濇煡璇€��
+- 鑷姩杈撳嚭鈥滄湡鍒濅綑棰� / 鏈湀鍚堣 / 鏈勾绱鈥濄��
+
+---
+
+## 7. 绉戠洰鏄庣粏璐�
+
+### 7.1 鏌ヨ鎺ュ彛
+- `GET /financial/ledger/detail`
+
+### 7.2 Query 鍙傛暟
+- `subjectCode`锛堝繀濉級
+- `auxiliaryType`锛堝彲閫夛細`customer/supplier/department/employee/project`锛�
+- `auxiliaryId`锛堝彲閫夛級
+- `startMonth`锛圷YYY-MM锛�
+- `endMonth`锛圷YYY-MM锛�
+
+### 7.3 杩斿洖瀛楁
+鍚岀鐩�昏处锛�
+- `rowType,date,voucherNo,summary,debit,credit,direction,balance`
+
+---
+
+## 8. 鏁版嵁搴撹剼鏈�
+
+宸叉彁渚涜剼鏈細
+
+- `doc/20260512_create_financial_management_tables.sql`
+
+鍖呭惈锛�
+- `fin_fixed_asset`
+- `fin_intangible_asset`
+- `fin_voucher`
+- `fin_voucher_entry`
+
+鎬昏处绉戠洰澶嶇敤鐜版湁 `account_subject`銆�
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java
new file mode 100644
index 0000000..84a3676
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java
@@ -0,0 +1,22 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 绉戠洰鏄庣粏璐︽煡璇㈠弬鏁帮紙鍚緟鍔╂牳绠楁潯浠讹級銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinDetailLedgerQueryDto extends FinLedgerQueryDto {
+
+ /**
+ * 杈呭姪鏍哥畻绫诲瀷锛歝ustomer/supplier/department/employee/project銆�
+ */
+ private String auxiliaryType;
+
+ /**
+ * 杈呭姪鏍哥畻瀵硅薄ID銆�
+ */
+ private String auxiliaryId;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java
new file mode 100644
index 0000000..c6baeca
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java
@@ -0,0 +1,13 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鍥哄畾璧勪骇鏌ヨ涓庝繚瀛� DTO銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinFixedAssetDto extends FinFixedAsset {
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java
new file mode 100644
index 0000000..e3023ed
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java
@@ -0,0 +1,17 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 鎵归噺ID璇锋眰鍙傛暟銆�
+ */
+@Data
+public class FinIdBatchDto {
+
+ /**
+ * ID闆嗗悎銆�
+ */
+ private List<Long> ids;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java
new file mode 100644
index 0000000..60dd50e
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java
@@ -0,0 +1,13 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鏃犲舰璧勪骇鏌ヨ涓庝繚瀛� DTO銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinIntangibleAssetDto extends FinIntangibleAsset {
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java
new file mode 100644
index 0000000..cfc0553
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java
@@ -0,0 +1,25 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+/**
+ * 绉戠洰璐︽煡璇㈠弬鏁般��
+ */
+@Data
+public class FinLedgerQueryDto {
+
+ /**
+ * 绉戠洰缂栫爜锛堟敮鎸佹湯绾ф垨鎸囧畾绉戠洰锛夈��
+ */
+ private String subjectCode;
+
+ /**
+ * 寮�濮嬫湀浠斤紝鏍煎紡锛歒YYY-MM銆�
+ */
+ private String startMonth;
+
+ /**
+ * 缁撴潫鏈堜唤锛屾牸寮忥細YYYY-MM銆�
+ */
+ private String endMonth;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java
new file mode 100644
index 0000000..c7c6258
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java
@@ -0,0 +1,20 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * 鍑瘉淇濆瓨 DTO锛堜富琛� + 鍒嗗綍锛夈��
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinVoucherDto extends FinVoucher {
+
+ /**
+ * 鍑瘉鏄庣粏鍒嗗綍銆�
+ */
+ private List<FinVoucherEntryDto> entries;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java
new file mode 100644
index 0000000..f722d79
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java
@@ -0,0 +1,13 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鍑瘉鍒嗗綍 DTO銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinVoucherEntryDto extends FinVoucherEntry {
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java
new file mode 100644
index 0000000..9955bcc
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java
@@ -0,0 +1,37 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 鍑瘉鍒嗛〉鏌ヨ鍙傛暟銆�
+ */
+@Data
+public class FinVoucherPageDto {
+
+ /**
+ * 鍑瘉瀛楀彿锛堟ā绯婂尮閰嶏級銆�
+ */
+ private String voucherNo;
+
+ /**
+ * 鍒跺崟浜恒��
+ */
+ private String creator;
+
+ /**
+ * 鍑瘉鐘舵�併��
+ */
+ private String status;
+
+ /**
+ * 寮�濮嬫棩鏈燂紙鍚級銆�
+ */
+ private LocalDate startDate;
+
+ /**
+ * 缁撴潫鏃ユ湡锛堝惈锛夈��
+ */
+ private LocalDate endDate;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java
new file mode 100644
index 0000000..47c0900
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java
@@ -0,0 +1,15 @@
+package com.ruoyi.account.bean.dto.financial;
+
+import lombok.Data;
+
+/**
+ * 鍑瘉鐘舵�佸彉鏇村弬鏁般��
+ */
+@Data
+public class FinVoucherStatusDto {
+
+ /**
+ * 鍑瘉ID銆�
+ */
+ private Long id;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java b/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
index 3154d1c..c6bb078 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
@@ -3,6 +3,19 @@
import com.ruoyi.account.pojo.AccountSubject;
import lombok.Data;
+import java.util.ArrayList;
+import java.util.List;
+
@Data
public class AccountSubjectVo extends AccountSubject {
+
+ /**
+ * 瀛愮鐩垪琛紙閫掑綊缁撴瀯锛夈��
+ */
+ private List<AccountSubjectVo> children = new ArrayList<>();
+
+ /**
+ * 鏄惁鍙跺瓙鑺傜偣銆�
+ */
+ private Boolean leaf;
}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java
new file mode 100644
index 0000000..846b350
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java
@@ -0,0 +1,43 @@
+package com.ruoyi.account.bean.vo.financial;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 绉戠洰璐﹀熀纭�鍒嗗綍鏌ヨ瀵硅薄锛圫QL鏄犲皠浣跨敤锛夈��
+ */
+@Data
+public class FinLedgerEntryRecordVo {
+
+ /**
+ * 鍑瘉鏃ユ湡銆�
+ */
+ private LocalDate voucherDate;
+
+ /**
+ * 鍑瘉瀛楀彿銆�
+ */
+ private String voucherNo;
+
+ /**
+ * 鎽樿銆�
+ */
+ private String summary;
+
+ /**
+ * 鍊熸柟閲戦銆�
+ */
+ private BigDecimal debit;
+
+ /**
+ * 璐锋柟閲戦銆�
+ */
+ private BigDecimal credit;
+
+ /**
+ * 琛屽彿锛堟帓搴忓瓧娈碉級銆�
+ */
+ private Integer rowNo;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java
new file mode 100644
index 0000000..d01baf5
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java
@@ -0,0 +1,53 @@
+package com.ruoyi.account.bean.vo.financial;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 绉戠洰璐﹁鏁版嵁杩斿洖瀵硅薄銆�
+ */
+@Data
+public class FinLedgerRowVo {
+
+ /**
+ * 琛岀被鍨嬶細opening/entry/monthly_total/yearly_total銆�
+ */
+ private String rowType;
+
+ /**
+ * 鏃ユ湡銆�
+ */
+ private LocalDate date;
+
+ /**
+ * 鍑瘉瀛楀彿銆�
+ */
+ private String voucherNo;
+
+ /**
+ * 鎽樿銆�
+ */
+ private String summary;
+
+ /**
+ * 鍊熸柟閲戦銆�
+ */
+ private BigDecimal debit;
+
+ /**
+ * 璐锋柟閲戦銆�
+ */
+ private BigDecimal credit;
+
+ /**
+ * 浣欓鏂瑰悜锛氬��/璐枫��
+ */
+ private String direction;
+
+ /**
+ * 浣欓锛堝�熸璐疯礋锛夈��
+ */
+ private BigDecimal balance;
+}
diff --git a/src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java
new file mode 100644
index 0000000..d1eab48
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java
@@ -0,0 +1,21 @@
+package com.ruoyi.account.bean.vo.financial;
+
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * 鍑瘉璇︽儏杩斿洖瀵硅薄銆�
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FinVoucherDetailVo extends FinVoucher {
+
+ /**
+ * 鍑瘉鍒嗗綍鍒楄〃銆�
+ */
+ private List<FinVoucherEntry> entries;
+}
diff --git a/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java b/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
index 8282883..38dd0ce 100644
--- a/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
+++ b/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
@@ -33,7 +33,7 @@
@GetMapping("/list")
@Log(title = "鎬昏处绉戠洰鏁版嵁闆嗗悎", businessType = BusinessType.OTHER)
- @Operation(summary = "鎬昏处绉戠洰鍒嗛〉鏌ヨ")
+ @Operation(summary = "鎬昏处绉戠洰鏍戝舰鏌ヨ锛堥�掑綊锛�")
public R<IPage<AccountSubjectVo>> AccountSubjectDtoList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
IPage<AccountSubjectVo> paramList = accountSubjectService.baseList(page, accountSubjectDto);
return R.ok(paramList);
@@ -43,21 +43,21 @@
@Log(title = "鏂板鎬昏处绉戠洰", businessType = BusinessType.INSERT)
@Operation(summary = "鏂板鎬昏处绉戠洰")
public R AccountSubjectDtoAdd(@RequestBody AccountSubjectDto accountSubjectDto) {
- return R.ok(accountSubjectService.save(accountSubjectDto));
+ return R.ok(accountSubjectService.saveAccountSubject(accountSubjectDto));
}
@PutMapping("/edit")
@Log(title = "淇敼鎬昏处绉戠洰", businessType = BusinessType.UPDATE)
@Operation(summary = "淇敼鎬昏处绉戠洰")
public R AccountSubjectDtoEdit(@RequestBody AccountSubjectDto accountSubjectDto) {
- return R.ok(accountSubjectService.updateById(accountSubjectDto));
+ return R.ok(accountSubjectService.updateAccountSubject(accountSubjectDto));
}
@DeleteMapping("/remove/{ids}")
@Log(title = "鍒犻櫎鎬昏处绉戠洰", businessType = BusinessType.DELETE)
@Operation(summary = "鍒犻櫎鎬昏处绉戠洰")
public R AccountSubjectDtooRemove(@PathVariable Long[] ids) {
- return R.ok(accountSubjectService.removeBatchByIds(Arrays.asList(ids)));
+ return R.ok(accountSubjectService.removeAccountSubjectByIds(Arrays.asList(ids)));
}
@PostMapping("/export")
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java b/src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java
new file mode 100644
index 0000000..a18c9da
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java
@@ -0,0 +1,63 @@
+package com.ruoyi.account.controller.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
+import com.ruoyi.account.bean.dto.financial.FinIdBatchDto;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import com.ruoyi.account.service.financial.FinFixedAssetService;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+
+/**
+ * 鍥哄畾璧勪骇鎺у埗鍣ㄣ��
+ */
+@RestController
+@RequestMapping("/financial/fixedAsset")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-鍥哄畾璧勪骇")
+public class FinFixedAssetController {
+
+ private final FinFixedAssetService finFixedAssetService;
+
+ @GetMapping("/page")
+ @Operation(summary = "鍥哄畾璧勪骇鍒嗛〉鏌ヨ")
+ public R<IPage<FinFixedAsset>> page(Page<FinFixedAsset> page, FinFixedAssetDto queryDto) {
+ return R.ok(finFixedAssetService.pageList(page, queryDto));
+ }
+
+ @PostMapping("/add")
+ @Log(title = "鍥哄畾璧勪骇", businessType = BusinessType.INSERT)
+ @Operation(summary = "鏂板鍥哄畾璧勪骇")
+ public R<Boolean> add(@RequestBody FinFixedAssetDto dto) {
+ return R.ok(finFixedAssetService.add(dto));
+ }
+
+ @PutMapping("/update")
+ @Log(title = "鍥哄畾璧勪骇", businessType = BusinessType.UPDATE)
+ @Operation(summary = "淇敼鍥哄畾璧勪骇")
+ public R<Boolean> update(@RequestBody FinFixedAssetDto dto) {
+ return R.ok(finFixedAssetService.update(dto));
+ }
+
+ @DeleteMapping("/delete")
+ @Log(title = "鍥哄畾璧勪骇", businessType = BusinessType.DELETE)
+ @Operation(summary = "鍒犻櫎鍥哄畾璧勪骇")
+ public R<Boolean> delete(@RequestParam("ids") Long[] ids) {
+ return R.ok(finFixedAssetService.deleteByIds(Arrays.asList(ids)));
+ }
+
+ @PostMapping("/depreciate")
+ @Log(title = "鍥哄畾璧勪骇鎶樻棫璁℃彁", businessType = BusinessType.UPDATE)
+ @Operation(summary = "鍥哄畾璧勪骇鎸夋湀璁℃彁鎶樻棫")
+ public R depreciate(@RequestBody(required = false) FinIdBatchDto dto) {
+ return R.ok(finFixedAssetService.depreciate(dto == null ? null : dto.getIds()));
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java b/src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java
new file mode 100644
index 0000000..cb12f8c
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java
@@ -0,0 +1,63 @@
+package com.ruoyi.account.controller.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.bean.dto.financial.FinIdBatchDto;
+import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import com.ruoyi.account.service.financial.FinIntangibleAssetService;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+
+/**
+ * 鏃犲舰璧勪骇鎺у埗鍣ㄣ��
+ */
+@RestController
+@RequestMapping("/financial/intangibleAsset")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-鏃犲舰璧勪骇")
+public class FinIntangibleAssetController {
+
+ private final FinIntangibleAssetService finIntangibleAssetService;
+
+ @GetMapping("/page")
+ @Operation(summary = "鏃犲舰璧勪骇鍒嗛〉鏌ヨ")
+ public R<IPage<FinIntangibleAsset>> page(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto) {
+ return R.ok(finIntangibleAssetService.pageList(page, queryDto));
+ }
+
+ @PostMapping("/add")
+ @Log(title = "鏃犲舰璧勪骇", businessType = BusinessType.INSERT)
+ @Operation(summary = "鏂板鏃犲舰璧勪骇")
+ public R<Boolean> add(@RequestBody FinIntangibleAssetDto dto) {
+ return R.ok(finIntangibleAssetService.add(dto));
+ }
+
+ @PutMapping("/update")
+ @Log(title = "鏃犲舰璧勪骇", businessType = BusinessType.UPDATE)
+ @Operation(summary = "淇敼鏃犲舰璧勪骇")
+ public R<Boolean> update(@RequestBody FinIntangibleAssetDto dto) {
+ return R.ok(finIntangibleAssetService.update(dto));
+ }
+
+ @DeleteMapping("/delete")
+ @Log(title = "鏃犲舰璧勪骇", businessType = BusinessType.DELETE)
+ @Operation(summary = "鍒犻櫎鏃犲舰璧勪骇")
+ public R<Boolean> delete(@RequestParam("ids") Long[] ids) {
+ return R.ok(finIntangibleAssetService.deleteByIds(Arrays.asList(ids)));
+ }
+
+ @PostMapping("/amortize")
+ @Log(title = "鏃犲舰璧勪骇鎽婇攢璁℃彁", businessType = BusinessType.UPDATE)
+ @Operation(summary = "鏃犲舰璧勪骇鎸夋湀璁℃彁鎽婇攢")
+ public R amortize(@RequestBody(required = false) FinIdBatchDto dto) {
+ return R.ok(finIntangibleAssetService.amortize(dto == null ? null : dto.getIds()));
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java b/src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java
new file mode 100644
index 0000000..423030a
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java
@@ -0,0 +1,39 @@
+package com.ruoyi.account.controller.financial;
+
+import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
+import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
+import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
+import com.ruoyi.account.service.financial.FinLedgerService;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 绉戠洰鎬昏处/鏄庣粏璐︽帶鍒跺櫒銆�
+ */
+@RestController
+@RequestMapping("/financial/ledger")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-绉戠洰璐�")
+public class FinLedgerController {
+
+ private final FinLedgerService finLedgerService;
+
+ @GetMapping("/general")
+ @Operation(summary = "绉戠洰鎬昏处鏌ヨ")
+ public R<List<FinLedgerRowVo>> general(FinLedgerQueryDto queryDto) {
+ return R.ok(finLedgerService.queryGeneralLedger(queryDto));
+ }
+
+ @GetMapping("/detail")
+ @Operation(summary = "绉戠洰鏄庣粏璐︽煡璇�")
+ public R<List<FinLedgerRowVo>> detail(FinDetailLedgerQueryDto queryDto) {
+ return R.ok(finLedgerService.queryDetailLedger(queryDto));
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java b/src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java
new file mode 100644
index 0000000..beeaafa
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java
@@ -0,0 +1,69 @@
+package com.ruoyi.account.controller.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherStatusDto;
+import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import com.ruoyi.account.service.financial.FinVoucherService;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.domain.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 鍑瘉鎺у埗鍣ㄣ��
+ */
+@RestController
+@RequestMapping("/financial/voucher")
+@RequiredArgsConstructor
+@Tag(name = "璐㈠姟绠$悊-鍑瘉")
+public class FinVoucherController {
+
+ private final FinVoucherService finVoucherService;
+
+ @GetMapping("/page")
+ @Operation(summary = "鍑瘉鍒嗛〉鏌ヨ")
+ public R<IPage<FinVoucher>> page(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
+ return R.ok(finVoucherService.pageList(page, queryDto));
+ }
+
+ @PostMapping("/add")
+ @Log(title = "鍑瘉", businessType = BusinessType.INSERT)
+ @Operation(summary = "鏂板鍑瘉")
+ public R<Boolean> add(@RequestBody FinVoucherDto dto) {
+ return R.ok(finVoucherService.addVoucher(dto));
+ }
+
+ @PutMapping("/update")
+ @Log(title = "鍑瘉", businessType = BusinessType.UPDATE)
+ @Operation(summary = "淇敼鍑瘉")
+ public R<Boolean> update(@RequestBody FinVoucherDto dto) {
+ return R.ok(finVoucherService.updateVoucher(dto));
+ }
+
+ @PostMapping("/post")
+ @Log(title = "鍑瘉杩囪处", businessType = BusinessType.UPDATE)
+ @Operation(summary = "鍑瘉杩囪处")
+ public R<Boolean> post(@RequestBody FinVoucherStatusDto dto) {
+ return R.ok(finVoucherService.postVoucher(dto.getId()));
+ }
+
+ @PostMapping("/cancel")
+ @Log(title = "鍑瘉浣滃簾", businessType = BusinessType.UPDATE)
+ @Operation(summary = "鍑瘉浣滃簾")
+ public R<Boolean> cancel(@RequestBody FinVoucherStatusDto dto) {
+ return R.ok(finVoucherService.cancelVoucher(dto.getId()));
+ }
+
+ @GetMapping("/detail/{id}")
+ @Operation(summary = "鍑瘉璇︽儏")
+ public R<FinVoucherDetailVo> detail(@PathVariable("id") Long id) {
+ return R.ok(finVoucherService.detail(id));
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java b/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
index 1fface5..46a4968 100644
--- a/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
+++ b/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
@@ -3,6 +3,9 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.AccountSubject;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
/**
* <p>
@@ -15,4 +18,6 @@
@Mapper
public interface AccountSubjectMapper extends BaseMapper<AccountSubject> {
+ Long countReferencedBySubjectCodes(@Param("subjectCodes") List<String> subjectCodes);
+
}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java
new file mode 100644
index 0000000..da2ae49
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鍥哄畾璧勪骇 Mapper銆�
+ */
+@Mapper
+public interface FinFixedAssetMapper extends BaseMapper<FinFixedAsset> {
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java
new file mode 100644
index 0000000..8a7bbb2
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鏃犲舰璧勪骇 Mapper銆�
+ */
+@Mapper
+public interface FinIntangibleAssetMapper extends BaseMapper<FinIntangibleAsset> {
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java
new file mode 100644
index 0000000..2fa2b73
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java
@@ -0,0 +1,28 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo;
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 鍑瘉鍒嗗綍 Mapper銆�
+ */
+@Mapper
+public interface FinVoucherEntryMapper extends BaseMapper<FinVoucherEntry> {
+
+ List<FinLedgerEntryRecordVo> listPostedEntries(@Param("subjectCode") String subjectCode,
+ @Param("startDate") LocalDate startDate,
+ @Param("endDate") LocalDate endDate,
+ @Param("auxiliaryType") String auxiliaryType,
+ @Param("auxiliaryId") String auxiliaryId);
+
+ List<FinLedgerEntryRecordVo> listPostedEntriesBefore(@Param("subjectCode") String subjectCode,
+ @Param("beforeDate") LocalDate beforeDate,
+ @Param("auxiliaryType") String auxiliaryType,
+ @Param("auxiliaryId") String auxiliaryId);
+}
diff --git a/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java
new file mode 100644
index 0000000..b528b7c
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.account.mapper.financial;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 鍑瘉涓昏〃 Mapper銆�
+ */
+@Mapper
+public interface FinVoucherMapper extends BaseMapper<FinVoucher> {
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/AccountSubject.java b/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
index 49ba3da..9616324 100644
--- a/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
+++ b/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
@@ -39,6 +39,12 @@
private Long id;
/**
+ * 鐖剁鐩甀D锛堜负绌鸿〃绀烘牴鑺傜偣锛�
+ */
+ @ApiModelProperty("鐖剁鐩甀D锛堜负绌鸿〃绀烘牴鑺傜偣锛�")
+ private Long parentId;
+
+ /**
* 绉戠洰缂栫爜(鍞竴鏍囪瘑)
*/
@ApiModelProperty("绉戠洰缂栫爜(鍞竴鏍囪瘑)")
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java b/src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java
new file mode 100644
index 0000000..f11f700
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java
@@ -0,0 +1,101 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 鍥哄畾璧勪骇瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_fixed_asset")
+@ApiModel(value = "FinFixedAsset瀵硅薄", description = "鍥哄畾璧勪骇")
+public class FinFixedAsset implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("涓婚敭ID")
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @ApiModelProperty("璧勪骇缂栧彿")
+ private String assetCode;
+
+ @ApiModelProperty("璧勪骇鍚嶇О")
+ private String assetName;
+
+ @ApiModelProperty("璧勪骇绫诲埆")
+ private String category;
+
+ @ApiModelProperty("瑙勬牸鍨嬪彿")
+ private String specification;
+
+ @ApiModelProperty("璐疆鏃ユ湡")
+ private LocalDate purchaseDate;
+
+ @ApiModelProperty("璧勪骇鍘熷��")
+ private BigDecimal originalValue;
+
+ @ApiModelProperty("浣跨敤骞撮檺(骞�)")
+ private Integer usefulLife;
+
+ @ApiModelProperty("娈嬪�肩巼(%)")
+ private BigDecimal residualRate;
+
+ @ApiModelProperty("绱鎶樻棫")
+ private BigDecimal accumulatedDepreciation;
+
+ @ApiModelProperty("鍑�鍊�")
+ private BigDecimal netValue;
+
+ @ApiModelProperty("瀛樻斁鍦扮偣")
+ private String location;
+
+ @ApiModelProperty("浣跨敤閮ㄩ棬")
+ private String department;
+
+ @ApiModelProperty("淇濈浜�")
+ private String keeper;
+
+ @ApiModelProperty("鐘舵��")
+ private String status;
+
+ @ApiModelProperty("澶囨敞")
+ private String remark;
+
+ @ApiModelProperty("鍒涘缓浜�")
+ @TableField(fill = FieldFill.INSERT)
+ private String createUser;
+
+ @ApiModelProperty("鍒涘缓鏃堕棿")
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ @ApiModelProperty("淇敼浜�")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateUser;
+
+ @ApiModelProperty("淇敼鏃堕棿")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ @ApiModelProperty("閮ㄩ棬ID")
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java b/src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java
new file mode 100644
index 0000000..e8ab4d3
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java
@@ -0,0 +1,98 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 鏃犲舰璧勪骇瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_intangible_asset")
+@ApiModel(value = "FinIntangibleAsset瀵硅薄", description = "鏃犲舰璧勪骇")
+public class FinIntangibleAsset implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("涓婚敭ID")
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @ApiModelProperty("璧勪骇缂栧彿")
+ private String assetCode;
+
+ @ApiModelProperty("璧勪骇鍚嶇О")
+ private String assetName;
+
+ @ApiModelProperty("璧勪骇绫诲埆")
+ private String category;
+
+ @ApiModelProperty("璇佷功缂栧彿")
+ private String certificateNo;
+
+ @ApiModelProperty("鍙栧緱鏃ユ湡")
+ private LocalDate acquisitionDate;
+
+ @ApiModelProperty("璧勪骇鍘熷��")
+ private BigDecimal originalValue;
+
+ @ApiModelProperty("鎽婇攢骞撮檺(骞�)")
+ private Integer amortizationPeriod;
+
+ @ApiModelProperty("娈嬪�肩巼(%)")
+ private BigDecimal residualRate;
+
+ @ApiModelProperty("绱鎽婇攢")
+ private BigDecimal accumulatedAmortization;
+
+ @ApiModelProperty("鍑�鍊�")
+ private BigDecimal netValue;
+
+ @ApiModelProperty("鏈夋晥鏈熻嚦")
+ private LocalDate validityDate;
+
+ @ApiModelProperty("鐘舵��")
+ private String status;
+
+ @ApiModelProperty("璧勪骇鎻忚堪")
+ private String description;
+
+ @ApiModelProperty("澶囨敞")
+ private String remark;
+
+ @ApiModelProperty("鍒涘缓浜�")
+ @TableField(fill = FieldFill.INSERT)
+ private String createUser;
+
+ @ApiModelProperty("鍒涘缓鏃堕棿")
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ @ApiModelProperty("淇敼浜�")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateUser;
+
+ @ApiModelProperty("淇敼鏃堕棿")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ @ApiModelProperty("閮ㄩ棬ID")
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java
new file mode 100644
index 0000000..0a2918c
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java
@@ -0,0 +1,83 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 鍑瘉涓昏〃瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_voucher")
+@ApiModel(value = "FinVoucher瀵硅薄", description = "鍑瘉涓昏〃")
+public class FinVoucher implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("涓婚敭ID")
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @ApiModelProperty("鍑瘉瀛楀彿")
+ private String voucherNo;
+
+ @ApiModelProperty("鍑瘉鏃ユ湡")
+ private LocalDate voucherDate;
+
+ @ApiModelProperty("鎽樿")
+ private String summary;
+
+ @ApiModelProperty("鍊熸柟鍚堣")
+ private BigDecimal debit;
+
+ @ApiModelProperty("璐锋柟鍚堣")
+ private BigDecimal credit;
+
+ @ApiModelProperty("鍒跺崟浜�")
+ private String creator;
+
+ @ApiModelProperty("鐘舵��: unposted/posted/cancelled")
+ private String status;
+
+ @ApiModelProperty("闄勪欢鏁伴噺")
+ private Integer attachmentCount;
+
+ @ApiModelProperty("澶囨敞")
+ private String remark;
+
+ @ApiModelProperty("鍒涘缓浜�")
+ @TableField(fill = FieldFill.INSERT)
+ private String createUser;
+
+ @ApiModelProperty("鍒涘缓鏃堕棿")
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ @ApiModelProperty("淇敼浜�")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateUser;
+
+ @ApiModelProperty("淇敼鏃堕棿")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ @ApiModelProperty("閮ㄩ棬ID")
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java
new file mode 100644
index 0000000..44ac56e
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java
@@ -0,0 +1,85 @@
+package com.ruoyi.account.pojo.financial;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 鍑瘉鍒嗗綍瀹炰綋銆�
+ */
+@Getter
+@Setter
+@ToString
+@TableName("fin_voucher_entry")
+@ApiModel(value = "FinVoucherEntry瀵硅薄", description = "鍑瘉鍒嗗綍")
+public class FinVoucherEntry implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("涓婚敭ID")
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @ApiModelProperty("鍑瘉ID")
+ private Long voucherId;
+
+ @ApiModelProperty("琛屽彿")
+ private Integer rowNo;
+
+ @ApiModelProperty("绉戠洰缂栫爜")
+ private String subjectCode;
+
+ @ApiModelProperty("绉戠洰鍚嶇О")
+ private String subjectName;
+
+ @ApiModelProperty("鎽樿")
+ private String summary;
+
+ @ApiModelProperty("鍊熸柟閲戦")
+ private BigDecimal debit;
+
+ @ApiModelProperty("璐锋柟閲戦")
+ private BigDecimal credit;
+
+ @ApiModelProperty("杈呭姪鏍哥畻绫诲瀷")
+ private String auxiliaryType;
+
+ @ApiModelProperty("杈呭姪鏍哥畻瀵硅薄ID")
+ private String auxiliaryId;
+
+ @ApiModelProperty("杈呭姪鏍哥畻瀵硅薄鍚嶇О")
+ private String auxiliaryName;
+
+ @ApiModelProperty("鍒涘缓浜�")
+ @TableField(fill = FieldFill.INSERT)
+ private String createUser;
+
+ @ApiModelProperty("鍒涘缓鏃堕棿")
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ @ApiModelProperty("淇敼浜�")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateUser;
+
+ @ApiModelProperty("淇敼鏃堕棿")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ @ApiModelProperty("閮ㄩ棬ID")
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+}
diff --git a/src/main/java/com/ruoyi/account/service/AccountSubjectService.java b/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
index a7dd670..bcbc57c 100644
--- a/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
+++ b/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
@@ -8,6 +8,8 @@
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
+import java.util.List;
+
/**
* <p>
* 鎬昏处绉戠洰琛� 鏈嶅姟绫�
@@ -20,5 +22,11 @@
IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto);
+ Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto);
+
+ Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto);
+
+ Boolean removeAccountSubjectByIds(List<Long> ids);
+
void exportAccountSubject(HttpServletResponse response);
}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java b/src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java
new file mode 100644
index 0000000..0b2c1cc
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java
@@ -0,0 +1,26 @@
+package com.ruoyi.account.service.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 鍥哄畾璧勪骇鏈嶅姟銆�
+ */
+public interface FinFixedAssetService extends IService<FinFixedAsset> {
+
+ IPage<FinFixedAsset> pageList(Page<FinFixedAsset> page, FinFixedAssetDto queryDto);
+
+ Boolean add(FinFixedAssetDto dto);
+
+ Boolean update(FinFixedAssetDto dto);
+
+ Boolean deleteByIds(List<Long> ids);
+
+ Map<String, Object> depreciate(List<Long> ids);
+}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java b/src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java
new file mode 100644
index 0000000..46d946a
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java
@@ -0,0 +1,26 @@
+package com.ruoyi.account.service.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 鏃犲舰璧勪骇鏈嶅姟銆�
+ */
+public interface FinIntangibleAssetService extends IService<FinIntangibleAsset> {
+
+ IPage<FinIntangibleAsset> pageList(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto);
+
+ Boolean add(FinIntangibleAssetDto dto);
+
+ Boolean update(FinIntangibleAssetDto dto);
+
+ Boolean deleteByIds(List<Long> ids);
+
+ Map<String, Object> amortize(List<Long> ids);
+}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java b/src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java
new file mode 100644
index 0000000..c4e9fa4
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java
@@ -0,0 +1,17 @@
+package com.ruoyi.account.service.financial;
+
+import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
+import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
+import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
+
+import java.util.List;
+
+/**
+ * 绉戠洰璐︽湇鍔°��
+ */
+public interface FinLedgerService {
+
+ List<FinLedgerRowVo> queryGeneralLedger(FinLedgerQueryDto queryDto);
+
+ List<FinLedgerRowVo> queryDetailLedger(FinDetailLedgerQueryDto queryDto);
+}
diff --git a/src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java b/src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java
new file mode 100644
index 0000000..078bac4
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java
@@ -0,0 +1,27 @@
+package com.ruoyi.account.service.financial;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
+import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+
+/**
+ * 鍑瘉鏈嶅姟銆�
+ */
+public interface FinVoucherService extends IService<FinVoucher> {
+
+ IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto);
+
+ Boolean addVoucher(FinVoucherDto dto);
+
+ Boolean updateVoucher(FinVoucherDto dto);
+
+ Boolean postVoucher(Long id);
+
+ Boolean cancelVoucher(Long id);
+
+ FinVoucherDetailVo detail(Long id);
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
index 152966a..37bf64b 100644
--- a/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
+++ b/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
@@ -10,6 +10,7 @@
import com.ruoyi.account.mapper.AccountSubjectMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.service.AccountSubjectService;
+import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
@@ -18,7 +19,16 @@
import org.springframework.stereotype.Service;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -37,30 +47,78 @@
@Override
public IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
- LambdaQueryWrapper<AccountSubject> queryWrapper = new LambdaQueryWrapper<>();
- if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectCode())) {
- queryWrapper.like(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
- }
- if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectName())) {
- queryWrapper.like(AccountSubject::getSubjectName, accountSubjectDto.getSubjectName());
- }
- if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectType())) {
- queryWrapper.eq(AccountSubject::getSubjectType, accountSubjectDto.getSubjectType());
- }
- queryWrapper.orderByDesc(AccountSubject::getId);
+ Page<AccountSubjectDto> requestPage = page == null ? new Page<>(1, 10) : page;
+ List<AccountSubject> allSubjects = list(loadBaseQueryWrapper(accountSubjectDto));
+ List<AccountSubject> filteredSubjects = applyTreeFilter(allSubjects, accountSubjectDto);
+ List<AccountSubjectVo> fullTree = buildTree(filteredSubjects);
- Page<AccountSubject> entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
- Page<AccountSubject> paramPage = page(entityPage, queryWrapper);
+ long current = requestPage.getCurrent() <= 0 ? 1 : requestPage.getCurrent();
+ long size = requestPage.getSize() <= 0 ? 10 : requestPage.getSize();
+ int fromIndex = (int) Math.min((current - 1) * size, fullTree.size());
+ int toIndex = (int) Math.min(fromIndex + size, fullTree.size());
+ List<AccountSubjectVo> pagedRoots = fromIndex >= toIndex
+ ? Collections.emptyList()
+ : fullTree.subList(fromIndex, toIndex);
- Page<AccountSubjectVo> resultPage = new Page<>(paramPage.getCurrent(), paramPage.getSize(), paramPage.getTotal());
- List<AccountSubjectVo> records = new ArrayList<>(paramPage.getRecords().size());
- for (AccountSubject item : paramPage.getRecords()) {
- AccountSubjectVo vo = new AccountSubjectVo();
- BeanUtils.copyProperties(item, vo);
- records.add(vo);
- }
- resultPage.setRecords(records);
+ Page<AccountSubjectVo> resultPage = new Page<>(current, size, fullTree.size());
+ resultPage.setRecords(pagedRoots);
return resultPage;
+ }
+
+ @Override
+ public Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto) {
+ validateSubjectRequiredFields(accountSubjectDto);
+ validateSubjectCodeUnique(accountSubjectDto, false);
+ validateParent(accountSubjectDto.getParentId(), null);
+ if (accountSubjectDto.getStatus() == null) {
+ accountSubjectDto.setStatus(0);
+ }
+ return save(accountSubjectDto);
+ }
+
+ @Override
+ public Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto) {
+ if (accountSubjectDto == null || accountSubjectDto.getId() == null) {
+ throw new ServiceException("淇敼澶辫触锛岀鐩甀D涓嶈兘涓虹┖");
+ }
+ if (getById(accountSubjectDto.getId()) == null) {
+ throw new ServiceException("淇敼澶辫触锛屾湭鎵惧埌瀵瑰簲绉戠洰");
+ }
+ validateParent(accountSubjectDto.getParentId(), accountSubjectDto.getId());
+ validateSubjectRequiredFields(accountSubjectDto);
+ validateSubjectCodeUnique(accountSubjectDto, true);
+ return updateById(accountSubjectDto);
+ }
+
+ @Override
+ public Boolean removeAccountSubjectByIds(List<Long> ids) {
+ if (ids == null || ids.isEmpty()) {
+ return true;
+ }
+ List<AccountSubject> allSubjects = list();
+ if (allSubjects == null || allSubjects.isEmpty()) {
+ return true;
+ }
+ Map<Long, List<Long>> childrenIdMap = buildChildrenIdMap(allSubjects);
+ Set<Long> removeIds = new LinkedHashSet<>();
+ for (Long id : ids) {
+ collectDescendantIds(id, childrenIdMap, removeIds);
+ }
+ if (removeIds.isEmpty()) {
+ return true;
+ }
+ List<String> subjectCodes = allSubjects.stream()
+ .filter(subject -> removeIds.contains(subject.getId()))
+ .map(AccountSubject::getSubjectCode)
+ .filter(StringUtils::isNotEmpty)
+ .collect(Collectors.toList());
+ if (!subjectCodes.isEmpty()) {
+ Long referencedCount = accountSubjectMapper.countReferencedBySubjectCodes(subjectCodes);
+ if (referencedCount != null && referencedCount > 0) {
+ throw new ServiceException("鍒犻櫎澶辫触锛岀鐩凡琚嚟璇佸垎褰曞紩鐢�");
+ }
+ }
+ return removeByIds(removeIds);
}
@Override
@@ -74,4 +132,266 @@
ExcelUtil<AccountSubjectImportDto> util = new ExcelUtil<>(AccountSubjectImportDto.class);
util.exportExcel(response, importDtos , "鎬昏处绉戠洰");
}
+
+ /**
+ * 鏍¢獙绉戠洰蹇呭~瀛楁锛岄伩鍏嶈剰鏁版嵁鍐欏叆銆�
+ */
+ private void validateSubjectRequiredFields(AccountSubjectDto accountSubjectDto) {
+ if (accountSubjectDto == null) {
+ throw new ServiceException("鎬昏处绉戠洰鏁版嵁涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(accountSubjectDto.getSubjectCode())) {
+ throw new ServiceException("绉戠洰缂栫爜涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(accountSubjectDto.getSubjectName())) {
+ throw new ServiceException("绉戠洰鍚嶇О涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(accountSubjectDto.getSubjectType())) {
+ throw new ServiceException("绉戠洰绫诲瀷涓嶈兘涓虹┖");
+ }
+ }
+
+ /**
+ * 鏍¢獙绉戠洰缂栫爜鍞竴锛屾柊澧炲拰淇敼閮借鎵ц銆�
+ */
+ private void validateSubjectCodeUnique(AccountSubjectDto accountSubjectDto, boolean isUpdate) {
+ LambdaQueryWrapper<AccountSubject> codeQueryWrapper = new LambdaQueryWrapper<>();
+ codeQueryWrapper.eq(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
+ if (isUpdate) {
+ codeQueryWrapper.ne(AccountSubject::getId, accountSubjectDto.getId());
+ }
+ AccountSubject exists = getOne(codeQueryWrapper, false);
+ if (Objects.nonNull(exists)) {
+ throw new ServiceException("绉戠洰缂栫爜宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+ }
+ }
+
+ /**
+ * 浠呮寜閫氱敤杩囨护鏉′欢鏌ヨ鍩虹鏁版嵁锛堟爲褰㈣繃婊ゅ悗缁啀鍋氾級銆�
+ */
+ private LambdaQueryWrapper<AccountSubject> loadBaseQueryWrapper(AccountSubjectDto accountSubjectDto) {
+ LambdaQueryWrapper<AccountSubject> queryWrapper = new LambdaQueryWrapper<>();
+ if (accountSubjectDto != null && accountSubjectDto.getStatus() != null) {
+ queryWrapper.eq(AccountSubject::getStatus, accountSubjectDto.getStatus());
+ }
+ queryWrapper.orderByAsc(AccountSubject::getSubjectCode).orderByAsc(AccountSubject::getId);
+ return queryWrapper;
+ }
+
+ /**
+ * 鏍戝舰杩囨护锛氬懡涓妭鐐瑰悗淇濈暀鍏剁埗閾句笌瀛愭爲锛屼繚璇侀�掑綊缁撴瀯瀹屾暣銆�
+ */
+ private List<AccountSubject> applyTreeFilter(List<AccountSubject> allSubjects, AccountSubjectDto queryDto) {
+ if (allSubjects == null || allSubjects.isEmpty()) {
+ return Collections.emptyList();
+ }
+ boolean hasFilter = queryDto != null && (
+ StringUtils.isNotEmpty(queryDto.getSubjectCode())
+ || StringUtils.isNotEmpty(queryDto.getSubjectName())
+ || StringUtils.isNotEmpty(queryDto.getSubjectType())
+ );
+ if (!hasFilter) {
+ return allSubjects;
+ }
+
+ Map<Long, AccountSubject> subjectMap = allSubjects.stream()
+ .filter(item -> item.getId() != null)
+ .collect(Collectors.toMap(AccountSubject::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
+ Map<Long, List<AccountSubject>> childrenMap = buildChildrenMap(allSubjects);
+
+ Set<Long> matchedIds = new LinkedHashSet<>();
+ for (AccountSubject subject : allSubjects) {
+ if (subject.getId() == null) {
+ continue;
+ }
+ if (matchesFilter(subject, queryDto)) {
+ matchedIds.add(subject.getId());
+ }
+ }
+ if (matchedIds.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ Set<Long> resultIds = new LinkedHashSet<>(matchedIds);
+ for (Long matchedId : matchedIds) {
+ addAncestors(matchedId, subjectMap, resultIds);
+ addDescendants(matchedId, childrenMap, resultIds);
+ }
+
+ return allSubjects.stream()
+ .filter(item -> item.getId() != null && resultIds.contains(item.getId()))
+ .collect(Collectors.toList());
+ }
+
+ private boolean matchesFilter(AccountSubject subject, AccountSubjectDto queryDto) {
+ if (queryDto == null) {
+ return true;
+ }
+ if (StringUtils.isNotEmpty(queryDto.getSubjectCode())
+ && (subject.getSubjectCode() == null || !subject.getSubjectCode().contains(queryDto.getSubjectCode()))) {
+ return false;
+ }
+ if (StringUtils.isNotEmpty(queryDto.getSubjectName())
+ && (subject.getSubjectName() == null || !subject.getSubjectName().contains(queryDto.getSubjectName()))) {
+ return false;
+ }
+ if (StringUtils.isNotEmpty(queryDto.getSubjectType())
+ && !queryDto.getSubjectType().equals(subject.getSubjectType())) {
+ return false;
+ }
+ return true;
+ }
+
+ private void addAncestors(Long subjectId, Map<Long, AccountSubject> subjectMap, Set<Long> resultIds) {
+ AccountSubject current = subjectMap.get(subjectId);
+ if (current == null) {
+ return;
+ }
+ Long parentId = current.getParentId();
+ while (parentId != null && parentId > 0) {
+ AccountSubject parent = subjectMap.get(parentId);
+ if (parent == null) {
+ break;
+ }
+ if (!resultIds.add(parent.getId())) {
+ break;
+ }
+ parentId = parent.getParentId();
+ }
+ }
+
+ private void addDescendants(Long subjectId, Map<Long, List<AccountSubject>> childrenMap, Set<Long> resultIds) {
+ List<AccountSubject> children = childrenMap.getOrDefault(subjectId, Collections.emptyList());
+ for (AccountSubject child : children) {
+ if (child.getId() == null) {
+ continue;
+ }
+ if (resultIds.add(child.getId())) {
+ addDescendants(child.getId(), childrenMap, resultIds);
+ }
+ }
+ }
+
+ private Map<Long, List<AccountSubject>> buildChildrenMap(List<AccountSubject> subjects) {
+ Map<Long, List<AccountSubject>> childrenMap = new HashMap<>();
+ for (AccountSubject subject : subjects) {
+ if (subject.getId() == null) {
+ continue;
+ }
+ Long parentId = subject.getParentId();
+ if (parentId == null || parentId <= 0) {
+ continue;
+ }
+ childrenMap.computeIfAbsent(parentId, key -> new ArrayList<>()).add(subject);
+ }
+ return childrenMap;
+ }
+
+ /**
+ * 鍩轰簬 parentId 閫掑綊鏋勫缓绉戠洰鏍戙��
+ */
+ private List<AccountSubjectVo> buildTree(List<AccountSubject> subjects) {
+ if (subjects == null || subjects.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<AccountSubject> sortedSubjects = new ArrayList<>(subjects);
+ sortedSubjects.sort(Comparator
+ .comparing(AccountSubject::getSubjectCode, Comparator.nullsLast(String::compareTo))
+ .thenComparing(AccountSubject::getId, Comparator.nullsLast(Long::compareTo)));
+
+ Map<Long, AccountSubjectVo> subjectVoMap = new LinkedHashMap<>();
+ for (AccountSubject subject : sortedSubjects) {
+ if (subject.getId() == null) {
+ continue;
+ }
+ AccountSubjectVo vo = new AccountSubjectVo();
+ BeanUtils.copyProperties(subject, vo);
+ subjectVoMap.put(subject.getId(), vo);
+ }
+
+ List<AccountSubjectVo> roots = new ArrayList<>();
+ for (AccountSubject subject : sortedSubjects) {
+ if (subject.getId() == null) {
+ continue;
+ }
+ AccountSubjectVo current = subjectVoMap.get(subject.getId());
+ Long parentId = subject.getParentId();
+ if (parentId != null && parentId > 0 && subjectVoMap.containsKey(parentId)) {
+ subjectVoMap.get(parentId).getChildren().add(current);
+ } else {
+ roots.add(current);
+ }
+ }
+
+ markLeafRecursively(roots);
+ return roots;
+ }
+
+ private void markLeafRecursively(List<AccountSubjectVo> nodes) {
+ for (AccountSubjectVo node : nodes) {
+ List<AccountSubjectVo> children = node.getChildren();
+ node.setLeaf(children == null || children.isEmpty());
+ if (children != null && !children.isEmpty()) {
+ markLeafRecursively(children);
+ }
+ }
+ }
+
+ /**
+ * 鏍¢獙鐖跺瓙鍏崇郴锛氱埗鑺傜偣蹇呴』瀛樺湪锛屼笖涓嶈兘褰㈡垚寰幆寮曠敤銆�
+ */
+ private void validateParent(Long parentId, Long currentId) {
+ if (parentId == null || parentId <= 0) {
+ return;
+ }
+ if (currentId != null && parentId.equals(currentId)) {
+ throw new ServiceException("鐖剁鐩笉鑳介�夋嫨鑷韩");
+ }
+ AccountSubject parent = getById(parentId);
+ if (parent == null) {
+ throw new ServiceException("鐖剁鐩笉瀛樺湪锛岃閲嶆柊閫夋嫨");
+ }
+ // 闃叉褰㈡垚鐜細鏇存柊鏃讹紝鐖惰妭鐐逛笉鑳芥槸褰撳墠鑺傜偣鐨勪换鎰忓瓙瀛欒妭鐐广��
+ if (currentId != null) {
+ Set<Long> visited = new HashSet<>();
+ Long traceParentId = parentId;
+ while (traceParentId != null && traceParentId > 0) {
+ if (!visited.add(traceParentId)) {
+ throw new ServiceException("绉戠洰灞傜骇瀛樺湪寰幆寮曠敤锛岃妫�鏌ョ埗绉戠洰璁剧疆");
+ }
+ if (traceParentId.equals(currentId)) {
+ throw new ServiceException("鐖剁鐩笉鑳芥槸褰撳墠绉戠洰鎴栧叾瀛愮鐩�");
+ }
+ AccountSubject traceNode = getById(traceParentId);
+ if (traceNode == null) {
+ break;
+ }
+ traceParentId = traceNode.getParentId();
+ }
+ }
+ }
+
+ private Map<Long, List<Long>> buildChildrenIdMap(List<AccountSubject> subjects) {
+ Map<Long, List<Long>> map = new HashMap<>();
+ for (AccountSubject subject : subjects) {
+ if (subject.getId() == null || subject.getParentId() == null || subject.getParentId() <= 0) {
+ continue;
+ }
+ map.computeIfAbsent(subject.getParentId(), key -> new ArrayList<>()).add(subject.getId());
+ }
+ return map;
+ }
+
+ /**
+ * 鏀堕泦寰呭垹闄よ妭鐐瑰強鍏舵墍鏈夊瓙瀛欒妭鐐广��
+ */
+ private void collectDescendantIds(Long id, Map<Long, List<Long>> childrenIdMap, Set<Long> result) {
+ if (id == null || !result.add(id)) {
+ return;
+ }
+ List<Long> children = childrenIdMap.getOrDefault(id, Collections.emptyList());
+ for (Long childId : children) {
+ collectDescendantIds(childId, childrenIdMap, result);
+ }
+ }
}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java
new file mode 100644
index 0000000..cb7a476
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java
@@ -0,0 +1,231 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
+import com.ruoyi.account.mapper.financial.FinFixedAssetMapper;
+import com.ruoyi.account.pojo.financial.FinFixedAsset;
+import com.ruoyi.account.service.financial.FinFixedAssetService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 鍥哄畾璧勪骇鏈嶅姟瀹炵幇銆�
+ */
+@Service
+@RequiredArgsConstructor
+public class FinFixedAssetServiceImpl extends ServiceImpl<FinFixedAssetMapper, FinFixedAsset> implements FinFixedAssetService {
+
+ private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
+ private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+ @Override
+ public IPage<FinFixedAsset> pageList(Page<FinFixedAsset> page, FinFixedAssetDto queryDto) {
+ LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetCode())) {
+ wrapper.like(FinFixedAsset::getAssetCode, queryDto.getAssetCode());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetName())) {
+ wrapper.like(FinFixedAsset::getAssetName, queryDto.getAssetName());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCategory())) {
+ wrapper.eq(FinFixedAsset::getCategory, queryDto.getCategory());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
+ wrapper.eq(FinFixedAsset::getStatus, queryDto.getStatus());
+ }
+ wrapper.orderByDesc(FinFixedAsset::getId);
+ return page(page, wrapper);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean add(FinFixedAssetDto dto) {
+ validateForSave(dto, false);
+ if (StringUtils.isEmpty(dto.getAssetCode())) {
+ dto.setAssetCode(generateAssetCode());
+ }
+ BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+ dto.setResidualRate(residualRate);
+ BigDecimal accumulatedDepreciation = defaultMoney(dto.getAccumulatedDepreciation());
+ dto.setAccumulatedDepreciation(accumulatedDepreciation);
+ dto.setNetValue(calculateNetValue(dto.getOriginalValue(), accumulatedDepreciation));
+ if (StringUtils.isEmpty(dto.getStatus())) {
+ dto.setStatus("in_use");
+ }
+ return save(dto);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean update(FinFixedAssetDto dto) {
+ if (dto == null || dto.getId() == null) {
+ throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+ }
+ FinFixedAsset existed = getById(dto.getId());
+ if (existed == null) {
+ throw new ServiceException("淇敼澶辫触锛屽浐瀹氳祫浜т笉瀛樺湪");
+ }
+ if (StringUtils.isEmpty(dto.getAssetCode())) {
+ dto.setAssetCode(existed.getAssetCode());
+ }
+ if (StringUtils.isEmpty(dto.getStatus())) {
+ dto.setStatus(existed.getStatus());
+ }
+ validateForSave(dto, true);
+ BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+ dto.setResidualRate(residualRate);
+ if (dto.getAccumulatedDepreciation() == null) {
+ dto.setAccumulatedDepreciation(defaultMoney(existed.getAccumulatedDepreciation()));
+ }
+ dto.setNetValue(calculateNetValue(dto.getOriginalValue(), dto.getAccumulatedDepreciation()));
+ return updateById(dto);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean deleteByIds(List<Long> ids) {
+ if (ids == null || ids.isEmpty()) {
+ throw new ServiceException("鍒犻櫎澶辫触锛岃閫夋嫨瑕佸垹闄ょ殑鏁版嵁");
+ }
+ return removeByIds(ids);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Map<String, Object> depreciate(List<Long> ids) {
+ LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
+ if (ids != null && !ids.isEmpty()) {
+ wrapper.in(FinFixedAsset::getId, ids);
+ } else {
+ wrapper.eq(FinFixedAsset::getStatus, "in_use");
+ }
+ List<FinFixedAsset> assets = list(wrapper);
+ BigDecimal totalMonthlyDepreciation = ZERO;
+ int processedCount = 0;
+ for (FinFixedAsset asset : assets) {
+ if (!"in_use".equals(asset.getStatus())) {
+ continue;
+ }
+ BigDecimal monthlyDepreciation = calculateMonthlyDepreciation(
+ asset.getOriginalValue(),
+ asset.getResidualRate(),
+ asset.getUsefulLife()
+ );
+ BigDecimal accumulatedDepreciation = defaultMoney(asset.getAccumulatedDepreciation()).add(monthlyDepreciation);
+ if (accumulatedDepreciation.compareTo(defaultMoney(asset.getOriginalValue())) > 0) {
+ accumulatedDepreciation = defaultMoney(asset.getOriginalValue());
+ }
+ asset.setAccumulatedDepreciation(roundMoney(accumulatedDepreciation));
+ asset.setNetValue(calculateNetValue(asset.getOriginalValue(), asset.getAccumulatedDepreciation()));
+ updateById(asset);
+ processedCount++;
+ totalMonthlyDepreciation = totalMonthlyDepreciation.add(monthlyDepreciation);
+ }
+ Map<String, Object> result = new HashMap<>(4);
+ result.put("processedCount", processedCount);
+ result.put("totalMonthlyDepreciation", roundMoney(totalMonthlyDepreciation));
+ result.put("executionTime", LocalDateTime.now());
+ return result;
+ }
+
+ /**
+ * 鎸夋枃妗h鍒欐牎楠屽浐瀹氳祫浜ф暟鎹��
+ */
+ private void validateForSave(FinFixedAssetDto dto, boolean isUpdate) {
+ if (dto == null) {
+ throw new ServiceException("鍥哄畾璧勪骇鏁版嵁涓嶈兘涓虹┖");
+ }
+ if (isUpdate && dto.getId() == null) {
+ throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(dto.getAssetName())) {
+ throw new ServiceException("璧勪骇鍚嶇О涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(dto.getCategory())) {
+ throw new ServiceException("璧勪骇绫诲埆涓嶈兘涓虹┖");
+ }
+ if (dto.getPurchaseDate() == null) {
+ throw new ServiceException("璐疆鏃ユ湡涓嶈兘涓虹┖");
+ }
+ if (dto.getOriginalValue() == null || dto.getOriginalValue().compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("璧勪骇鍘熷�间笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+ }
+ if (dto.getUsefulLife() == null || dto.getUsefulLife() <= 0) {
+ throw new ServiceException("浣跨敤骞撮檺蹇呴』澶т簬0");
+ }
+ if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("娈嬪�肩巼涓嶈兘灏忎簬0");
+ }
+ if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(ONE_HUNDRED) > 0) {
+ throw new ServiceException("娈嬪�肩巼涓嶈兘澶т簬100%");
+ }
+ if (StringUtils.isNotEmpty(dto.getAssetCode())) {
+ LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(FinFixedAsset::getAssetCode, dto.getAssetCode());
+ if (isUpdate) {
+ wrapper.ne(FinFixedAsset::getId, dto.getId());
+ }
+ if (count(wrapper) > 0) {
+ throw new ServiceException("璧勪骇缂栧彿宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+ }
+ }
+ }
+
+ /**
+ * 鍥哄畾璧勪骇鎶樻棫鍏紡锛�
+ * monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)
+ */
+ private BigDecimal calculateMonthlyDepreciation(BigDecimal originalValue, BigDecimal residualRate, Integer usefulLife) {
+ BigDecimal normalizedOriginalValue = defaultMoney(originalValue);
+ BigDecimal normalizedResidualRate = normalizeResidualRate(residualRate);
+ BigDecimal depreciableRatio = BigDecimal.ONE.subtract(normalizedResidualRate.divide(ONE_HUNDRED, 8, RoundingMode.HALF_UP));
+ BigDecimal months = BigDecimal.valueOf((long) usefulLife * 12L);
+ if (months.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new ServiceException("浣跨敤骞撮檺鏃犳晥锛屾棤娉曡鎻愭姌鏃�");
+ }
+ return roundMoney(normalizedOriginalValue.multiply(depreciableRatio).divide(months, 8, RoundingMode.HALF_UP));
+ }
+
+ /**
+ * 鍑�鍊� = 鍘熷�� - 绱鎶樻棫銆�
+ */
+ private BigDecimal calculateNetValue(BigDecimal originalValue, BigDecimal accumulatedDepreciation) {
+ BigDecimal value = defaultMoney(originalValue).subtract(defaultMoney(accumulatedDepreciation));
+ if (value.compareTo(BigDecimal.ZERO) < 0) {
+ value = BigDecimal.ZERO;
+ }
+ return roundMoney(value);
+ }
+
+ private BigDecimal normalizeResidualRate(BigDecimal residualRate) {
+ return residualRate == null ? BigDecimal.ZERO : residualRate;
+ }
+
+ private BigDecimal defaultMoney(BigDecimal value) {
+ return value == null ? ZERO : roundMoney(value);
+ }
+
+ private BigDecimal roundMoney(BigDecimal value) {
+ if (value == null) {
+ return ZERO;
+ }
+ return value.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private String generateAssetCode() {
+ return "GD" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java
new file mode 100644
index 0000000..72e5f06
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java
@@ -0,0 +1,250 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
+import com.ruoyi.account.mapper.financial.FinIntangibleAssetMapper;
+import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
+import com.ruoyi.account.service.financial.FinIntangibleAssetService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 鏃犲舰璧勪骇鏈嶅姟瀹炵幇銆�
+ */
+@Service
+@RequiredArgsConstructor
+public class FinIntangibleAssetServiceImpl extends ServiceImpl<FinIntangibleAssetMapper, FinIntangibleAsset> implements FinIntangibleAssetService {
+
+ private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
+ private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+ @Override
+ public IPage<FinIntangibleAsset> pageList(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto) {
+ LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetCode())) {
+ wrapper.like(FinIntangibleAsset::getAssetCode, queryDto.getAssetCode());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetName())) {
+ wrapper.like(FinIntangibleAsset::getAssetName, queryDto.getAssetName());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCategory())) {
+ wrapper.eq(FinIntangibleAsset::getCategory, queryDto.getCategory());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
+ wrapper.eq(FinIntangibleAsset::getStatus, queryDto.getStatus());
+ }
+ wrapper.orderByDesc(FinIntangibleAsset::getId);
+ return page(page, wrapper);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean add(FinIntangibleAssetDto dto) {
+ validateForSave(dto, false);
+ if (StringUtils.isEmpty(dto.getAssetCode())) {
+ dto.setAssetCode(generateAssetCode());
+ }
+ BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+ dto.setResidualRate(residualRate);
+ BigDecimal accumulatedAmortization = defaultMoney(dto.getAccumulatedAmortization());
+ dto.setAccumulatedAmortization(accumulatedAmortization);
+ dto.setNetValue(calculateNetValue(dto.getOriginalValue(), accumulatedAmortization));
+ if (StringUtils.isEmpty(dto.getStatus())) {
+ dto.setStatus("in_use");
+ }
+ return save(dto);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean update(FinIntangibleAssetDto dto) {
+ if (dto == null || dto.getId() == null) {
+ throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+ }
+ FinIntangibleAsset existed = getById(dto.getId());
+ if (existed == null) {
+ throw new ServiceException("淇敼澶辫触锛屾棤褰㈣祫浜т笉瀛樺湪");
+ }
+ if (StringUtils.isEmpty(dto.getAssetCode())) {
+ dto.setAssetCode(existed.getAssetCode());
+ }
+ if (StringUtils.isEmpty(dto.getStatus())) {
+ dto.setStatus(existed.getStatus());
+ }
+ validateForSave(dto, true);
+ BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
+ dto.setResidualRate(residualRate);
+ if (dto.getAccumulatedAmortization() == null) {
+ dto.setAccumulatedAmortization(defaultMoney(existed.getAccumulatedAmortization()));
+ }
+ dto.setNetValue(calculateNetValue(dto.getOriginalValue(), dto.getAccumulatedAmortization()));
+ if (dto.getNetValue().compareTo(BigDecimal.ZERO) <= 0) {
+ dto.setStatus("amortized");
+ } else if ("amortized".equals(dto.getStatus())) {
+ dto.setStatus("in_use");
+ }
+ if (dto.getValidityDate() != null
+ && dto.getValidityDate().isBefore(LocalDate.now())
+ && !"amortized".equals(dto.getStatus())) {
+ dto.setStatus("expired");
+ }
+ return updateById(dto);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean deleteByIds(List<Long> ids) {
+ if (ids == null || ids.isEmpty()) {
+ throw new ServiceException("鍒犻櫎澶辫触锛岃閫夋嫨瑕佸垹闄ょ殑鏁版嵁");
+ }
+ return removeByIds(ids);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Map<String, Object> amortize(List<Long> ids) {
+ LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
+ if (ids != null && !ids.isEmpty()) {
+ wrapper.in(FinIntangibleAsset::getId, ids);
+ } else {
+ wrapper.eq(FinIntangibleAsset::getStatus, "in_use");
+ }
+ List<FinIntangibleAsset> assets = list(wrapper);
+ BigDecimal totalMonthlyAmortization = ZERO;
+ int processedCount = 0;
+ for (FinIntangibleAsset asset : assets) {
+ if (!"in_use".equals(asset.getStatus())) {
+ continue;
+ }
+ BigDecimal monthlyAmortization = calculateMonthlyAmortization(
+ asset.getOriginalValue(),
+ asset.getResidualRate(),
+ asset.getAmortizationPeriod()
+ );
+ BigDecimal accumulatedAmortization = defaultMoney(asset.getAccumulatedAmortization()).add(monthlyAmortization);
+ if (accumulatedAmortization.compareTo(defaultMoney(asset.getOriginalValue())) > 0) {
+ accumulatedAmortization = defaultMoney(asset.getOriginalValue());
+ }
+ asset.setAccumulatedAmortization(roundMoney(accumulatedAmortization));
+ asset.setNetValue(calculateNetValue(asset.getOriginalValue(), asset.getAccumulatedAmortization()));
+
+ // 瑙勫垯锛氬綋鍑�鍊� <= 0 鏃讹紝鍑�鍊煎綊闆跺苟鏍囪涓哄凡鎽婇攢瀹屻��
+ if (asset.getNetValue().compareTo(BigDecimal.ZERO) <= 0) {
+ asset.setNetValue(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
+ asset.setStatus("amortized");
+ } else if (asset.getValidityDate() != null && asset.getValidityDate().isBefore(LocalDate.now())) {
+ asset.setStatus("expired");
+ }
+ updateById(asset);
+ processedCount++;
+ totalMonthlyAmortization = totalMonthlyAmortization.add(monthlyAmortization);
+ }
+ Map<String, Object> result = new HashMap<>(4);
+ result.put("processedCount", processedCount);
+ result.put("totalMonthlyAmortization", roundMoney(totalMonthlyAmortization));
+ result.put("executionTime", LocalDateTime.now());
+ return result;
+ }
+
+ /**
+ * 鎸夋枃妗h鍒欐牎楠屾棤褰㈣祫浜ф暟鎹��
+ */
+ private void validateForSave(FinIntangibleAssetDto dto, boolean isUpdate) {
+ if (dto == null) {
+ throw new ServiceException("鏃犲舰璧勪骇鏁版嵁涓嶈兘涓虹┖");
+ }
+ if (isUpdate && dto.getId() == null) {
+ throw new ServiceException("淇敼澶辫触锛岃祫浜D涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(dto.getAssetName())) {
+ throw new ServiceException("璧勪骇鍚嶇О涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(dto.getCategory())) {
+ throw new ServiceException("璧勪骇绫诲埆涓嶈兘涓虹┖");
+ }
+ if (dto.getAcquisitionDate() == null) {
+ throw new ServiceException("鍙栧緱鏃ユ湡涓嶈兘涓虹┖");
+ }
+ if (dto.getOriginalValue() == null || dto.getOriginalValue().compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("璧勪骇鍘熷�间笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+ }
+ if (dto.getAmortizationPeriod() == null || dto.getAmortizationPeriod() <= 0) {
+ throw new ServiceException("鎽婇攢骞撮檺蹇呴』澶т簬0");
+ }
+ if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("娈嬪�肩巼涓嶈兘灏忎簬0");
+ }
+ if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(ONE_HUNDRED) > 0) {
+ throw new ServiceException("娈嬪�肩巼涓嶈兘澶т簬100%");
+ }
+ if (StringUtils.isNotEmpty(dto.getAssetCode())) {
+ LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(FinIntangibleAsset::getAssetCode, dto.getAssetCode());
+ if (isUpdate) {
+ wrapper.ne(FinIntangibleAsset::getId, dto.getId());
+ }
+ if (count(wrapper) > 0) {
+ throw new ServiceException("璧勪骇缂栧彿宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+ }
+ }
+ }
+
+ /**
+ * 鏃犲舰璧勪骇鎽婇攢鍏紡锛�
+ * monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)
+ */
+ private BigDecimal calculateMonthlyAmortization(BigDecimal originalValue, BigDecimal residualRate, Integer amortizationPeriod) {
+ BigDecimal normalizedOriginalValue = defaultMoney(originalValue);
+ BigDecimal normalizedResidualRate = normalizeResidualRate(residualRate);
+ BigDecimal amortizableRatio = BigDecimal.ONE.subtract(normalizedResidualRate.divide(ONE_HUNDRED, 8, RoundingMode.HALF_UP));
+ BigDecimal months = BigDecimal.valueOf((long) amortizationPeriod * 12L);
+ if (months.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new ServiceException("鎽婇攢骞撮檺鏃犳晥锛屾棤娉曡鎻愭憡閿�");
+ }
+ return roundMoney(normalizedOriginalValue.multiply(amortizableRatio).divide(months, 8, RoundingMode.HALF_UP));
+ }
+
+ /**
+ * 鍑�鍊� = 鍘熷�� - 绱鎽婇攢銆�
+ */
+ private BigDecimal calculateNetValue(BigDecimal originalValue, BigDecimal accumulatedAmortization) {
+ BigDecimal value = defaultMoney(originalValue).subtract(defaultMoney(accumulatedAmortization));
+ if (value.compareTo(BigDecimal.ZERO) < 0) {
+ value = BigDecimal.ZERO;
+ }
+ return roundMoney(value);
+ }
+
+ private BigDecimal normalizeResidualRate(BigDecimal residualRate) {
+ return residualRate == null ? BigDecimal.ZERO : residualRate;
+ }
+
+ private BigDecimal defaultMoney(BigDecimal value) {
+ return value == null ? ZERO : roundMoney(value);
+ }
+
+ private BigDecimal roundMoney(BigDecimal value) {
+ if (value == null) {
+ return ZERO;
+ }
+ return value.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private String generateAssetCode() {
+ return "WX" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java
new file mode 100644
index 0000000..35360e3
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java
@@ -0,0 +1,206 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
+import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
+import com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo;
+import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
+import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
+import com.ruoyi.account.service.financial.FinLedgerService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.*;
+
+/**
+ * 绉戠洰鎬昏处/鏄庣粏璐︽湇鍔″疄鐜般��
+ */
+@Service
+@RequiredArgsConstructor
+public class FinLedgerServiceImpl implements FinLedgerService {
+
+ private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
+ private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+
+ private final FinVoucherEntryMapper finVoucherEntryMapper;
+
+ @Override
+ public List<FinLedgerRowVo> queryGeneralLedger(FinLedgerQueryDto queryDto) {
+ if (queryDto == null || StringUtils.isEmpty(queryDto.getSubjectCode())) {
+ return Collections.emptyList();
+ }
+ YearMonth startMonth = parseMonth(queryDto.getStartMonth(), "寮�濮嬫湀浠�");
+ YearMonth endMonth = parseMonth(queryDto.getEndMonth(), "缁撴潫鏈堜唤");
+ if (startMonth.isAfter(endMonth)) {
+ throw new ServiceException("寮�濮嬫湀浠戒笉鑳藉ぇ浜庣粨鏉熸湀浠�");
+ }
+ return buildLedgerRows(queryDto.getSubjectCode(), startMonth, endMonth, null, null);
+ }
+
+ @Override
+ public List<FinLedgerRowVo> queryDetailLedger(FinDetailLedgerQueryDto queryDto) {
+ if (queryDto == null || StringUtils.isEmpty(queryDto.getSubjectCode())) {
+ return Collections.emptyList();
+ }
+ YearMonth startMonth = parseMonth(queryDto.getStartMonth(), "寮�濮嬫湀浠�");
+ YearMonth endMonth = parseMonth(queryDto.getEndMonth(), "缁撴潫鏈堜唤");
+ if (startMonth.isAfter(endMonth)) {
+ throw new ServiceException("寮�濮嬫湀浠戒笉鑳藉ぇ浜庣粨鏉熸湀浠�");
+ }
+ return buildLedgerRows(queryDto.getSubjectCode(), startMonth, endMonth, queryDto.getAuxiliaryType(), queryDto.getAuxiliaryId());
+ }
+
+ /**
+ * 鏋勫缓璐︾翱琛屾暟鎹紝杈撳嚭鏈熷垵銆佸垎褰曘�佹湰鏈堝悎璁°�佹湰骞寸疮璁°��
+ */
+ private List<FinLedgerRowVo> buildLedgerRows(String subjectCode,
+ YearMonth startMonth,
+ YearMonth endMonth,
+ String auxiliaryType,
+ String auxiliaryId) {
+ LocalDate startDate = startMonth.atDay(1);
+ LocalDate endDate = endMonth.atEndOfMonth();
+
+ List<FinLedgerEntryRecordVo> openingEntries = finVoucherEntryMapper.listPostedEntriesBefore(
+ subjectCode, startDate, auxiliaryType, auxiliaryId
+ );
+ BigDecimal openingBalance = calculateBalance(openingEntries);
+
+ List<FinLedgerEntryRecordVo> currentPeriodEntries = finVoucherEntryMapper.listPostedEntries(
+ subjectCode, startDate, endDate, auxiliaryType, auxiliaryId
+ );
+ Map<YearMonth, List<FinLedgerEntryRecordVo>> monthEntriesMap = groupEntriesByMonth(currentPeriodEntries);
+
+ List<FinLedgerRowVo> rows = new ArrayList<>();
+ BigDecimal runningBalance = openingBalance;
+ BigDecimal yearDebit = ZERO;
+ BigDecimal yearCredit = ZERO;
+
+ for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
+ rows.add(buildOpeningRow(month.atDay(1), runningBalance));
+
+ List<FinLedgerEntryRecordVo> monthEntries = monthEntriesMap.getOrDefault(month, Collections.emptyList());
+ BigDecimal monthDebit = ZERO;
+ BigDecimal monthCredit = ZERO;
+ for (FinLedgerEntryRecordVo entry : monthEntries) {
+ BigDecimal debit = money(entry.getDebit());
+ BigDecimal credit = money(entry.getCredit());
+ runningBalance = runningBalance.add(debit).subtract(credit);
+ monthDebit = monthDebit.add(debit);
+ monthCredit = monthCredit.add(credit);
+
+ FinLedgerRowVo row = new FinLedgerRowVo();
+ row.setRowType("entry");
+ row.setDate(entry.getVoucherDate());
+ row.setVoucherNo(entry.getVoucherNo());
+ row.setSummary(StringUtils.isNotEmpty(entry.getSummary()) ? entry.getSummary() : "");
+ row.setDebit(debit);
+ row.setCredit(credit);
+ row.setBalance(money(runningBalance));
+ row.setDirection(resolveDirection(runningBalance));
+ rows.add(row);
+ }
+
+ rows.add(buildMonthlyTotalRow(month.atEndOfMonth(), monthDebit, monthCredit, runningBalance));
+ yearDebit = yearDebit.add(monthDebit);
+ yearCredit = yearCredit.add(monthCredit);
+ }
+
+ rows.add(buildYearlyTotalRow(endMonth.atEndOfMonth(), yearDebit, yearCredit, runningBalance));
+ return rows;
+ }
+
+ private Map<YearMonth, List<FinLedgerEntryRecordVo>> groupEntriesByMonth(List<FinLedgerEntryRecordVo> entries) {
+ Map<YearMonth, List<FinLedgerEntryRecordVo>> map = new LinkedHashMap<>();
+ for (FinLedgerEntryRecordVo entry : entries) {
+ if (entry.getVoucherDate() == null) {
+ continue;
+ }
+ YearMonth month = YearMonth.from(entry.getVoucherDate());
+ map.computeIfAbsent(month, key -> new ArrayList<>()).add(entry);
+ }
+ return map;
+ }
+
+ private FinLedgerRowVo buildOpeningRow(LocalDate date, BigDecimal openingBalance) {
+ FinLedgerRowVo row = new FinLedgerRowVo();
+ row.setRowType("opening");
+ row.setDate(date);
+ row.setVoucherNo("-");
+ row.setSummary("鏈熷垵浣欓");
+ row.setDebit(ZERO);
+ row.setCredit(ZERO);
+ row.setBalance(money(openingBalance));
+ row.setDirection(resolveDirection(openingBalance));
+ return row;
+ }
+
+ private FinLedgerRowVo buildMonthlyTotalRow(LocalDate date,
+ BigDecimal monthDebit,
+ BigDecimal monthCredit,
+ BigDecimal monthBalance) {
+ FinLedgerRowVo row = new FinLedgerRowVo();
+ row.setRowType("monthly_total");
+ row.setDate(date);
+ row.setVoucherNo("-");
+ row.setSummary("鏈湀鍚堣");
+ row.setDebit(money(monthDebit));
+ row.setCredit(money(monthCredit));
+ row.setBalance(money(monthBalance));
+ row.setDirection(resolveDirection(monthBalance));
+ return row;
+ }
+
+ private FinLedgerRowVo buildYearlyTotalRow(LocalDate date,
+ BigDecimal yearDebit,
+ BigDecimal yearCredit,
+ BigDecimal yearBalance) {
+ FinLedgerRowVo row = new FinLedgerRowVo();
+ row.setRowType("yearly_total");
+ row.setDate(date);
+ row.setVoucherNo("-");
+ row.setSummary("鏈勾绱");
+ row.setDebit(money(yearDebit));
+ row.setCredit(money(yearCredit));
+ row.setBalance(money(yearBalance));
+ row.setDirection(resolveDirection(yearBalance));
+ return row;
+ }
+
+ private BigDecimal calculateBalance(List<FinLedgerEntryRecordVo> entries) {
+ BigDecimal balance = ZERO;
+ for (FinLedgerEntryRecordVo entry : entries) {
+ balance = balance.add(money(entry.getDebit())).subtract(money(entry.getCredit()));
+ }
+ return money(balance);
+ }
+
+ private String resolveDirection(BigDecimal balance) {
+ return money(balance).compareTo(BigDecimal.ZERO) >= 0 ? "鍊�" : "璐�";
+ }
+
+ private YearMonth parseMonth(String value, String fieldLabel) {
+ if (StringUtils.isEmpty(value)) {
+ throw new ServiceException(fieldLabel + "涓嶈兘涓虹┖锛屾牸寮忓簲涓篩YYY-MM");
+ }
+ try {
+ return YearMonth.parse(value, MONTH_FORMATTER);
+ } catch (DateTimeParseException ex) {
+ throw new ServiceException(fieldLabel + "鏍煎紡閿欒锛屾牸寮忓簲涓篩YYY-MM");
+ }
+ }
+
+ private BigDecimal money(BigDecimal value) {
+ if (value == null) {
+ return ZERO;
+ }
+ return value.setScale(2, RoundingMode.HALF_UP);
+ }
+}
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
new file mode 100644
index 0000000..9e09020
--- /dev/null
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
@@ -0,0 +1,299 @@
+package com.ruoyi.account.service.impl.financial;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto;
+import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
+import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
+import com.ruoyi.account.mapper.AccountSubjectMapper;
+import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
+import com.ruoyi.account.mapper.financial.FinVoucherMapper;
+import com.ruoyi.account.pojo.AccountSubject;
+import com.ruoyi.account.pojo.financial.FinVoucher;
+import com.ruoyi.account.pojo.financial.FinVoucherEntry;
+import com.ruoyi.account.service.financial.FinVoucherService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 鍑瘉鏈嶅姟瀹炵幇銆�
+ */
+@Service
+@RequiredArgsConstructor
+public class FinVoucherServiceImpl extends ServiceImpl<FinVoucherMapper, FinVoucher> implements FinVoucherService {
+
+ private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+
+ private final FinVoucherEntryMapper finVoucherEntryMapper;
+ private final AccountSubjectMapper accountSubjectMapper;
+
+ @Override
+ public IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
+ LambdaQueryWrapper<FinVoucher> wrapper = new LambdaQueryWrapper<>();
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getVoucherNo())) {
+ wrapper.like(FinVoucher::getVoucherNo, queryDto.getVoucherNo());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCreator())) {
+ wrapper.eq(FinVoucher::getCreator, queryDto.getCreator());
+ }
+ if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
+ wrapper.eq(FinVoucher::getStatus, queryDto.getStatus());
+ }
+ if (queryDto != null && queryDto.getStartDate() != null) {
+ wrapper.ge(FinVoucher::getVoucherDate, queryDto.getStartDate());
+ }
+ if (queryDto != null && queryDto.getEndDate() != null) {
+ wrapper.le(FinVoucher::getVoucherDate, queryDto.getEndDate());
+ }
+ wrapper.orderByDesc(FinVoucher::getVoucherDate).orderByDesc(FinVoucher::getId);
+ return page(page, wrapper);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean addVoucher(FinVoucherDto dto) {
+ validateVoucherBasicInfo(dto, false);
+ List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
+
+ FinVoucher voucher = new FinVoucher();
+ BeanUtils.copyProperties(dto, voucher);
+ voucher.setStatus("unposted");
+ voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
+ BigDecimal totalDebit = calculateTotalDebit(validEntries);
+ BigDecimal totalCredit = calculateTotalCredit(validEntries);
+ voucher.setDebit(totalDebit);
+ voucher.setCredit(totalCredit);
+ if (StringUtils.isEmpty(voucher.getSummary())) {
+ voucher.setSummary(findDefaultSummary(validEntries));
+ }
+ save(voucher);
+ saveEntries(voucher.getId(), validEntries);
+ return true;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean updateVoucher(FinVoucherDto dto) {
+ validateVoucherBasicInfo(dto, true);
+ FinVoucher existed = getById(dto.getId());
+ if (existed == null) {
+ throw new ServiceException("淇敼澶辫触锛屽嚟璇佷笉瀛樺湪");
+ }
+ if (!"unposted".equals(existed.getStatus())) {
+ throw new ServiceException("浠呮湭杩囪处鍑瘉鍏佽淇敼");
+ }
+ List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
+
+ FinVoucher voucher = new FinVoucher();
+ BeanUtils.copyProperties(dto, voucher);
+ voucher.setStatus(existed.getStatus());
+ voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
+ BigDecimal totalDebit = calculateTotalDebit(validEntries);
+ BigDecimal totalCredit = calculateTotalCredit(validEntries);
+ voucher.setDebit(totalDebit);
+ voucher.setCredit(totalCredit);
+ if (StringUtils.isEmpty(voucher.getSummary())) {
+ voucher.setSummary(findDefaultSummary(validEntries));
+ }
+ updateById(voucher);
+
+ LambdaQueryWrapper<FinVoucherEntry> deleteWrapper = new LambdaQueryWrapper<>();
+ deleteWrapper.eq(FinVoucherEntry::getVoucherId, voucher.getId());
+ finVoucherEntryMapper.delete(deleteWrapper);
+ saveEntries(voucher.getId(), validEntries);
+ return true;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean postVoucher(Long id) {
+ FinVoucher voucher = getById(id);
+ if (voucher == null) {
+ throw new ServiceException("杩囪处澶辫触锛屽嚟璇佷笉瀛樺湪");
+ }
+ if (!"unposted".equals(voucher.getStatus())) {
+ throw new ServiceException("浠呮湭杩囪处鍑瘉鍏佽杩囪处");
+ }
+ voucher.setStatus("posted");
+ return updateById(voucher);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean cancelVoucher(Long id) {
+ FinVoucher voucher = getById(id);
+ if (voucher == null) {
+ throw new ServiceException("浣滃簾澶辫触锛屽嚟璇佷笉瀛樺湪");
+ }
+ if (!"unposted".equals(voucher.getStatus())) {
+ throw new ServiceException("浠呮湭杩囪处鍑瘉鍏佽浣滃簾");
+ }
+ voucher.setStatus("cancelled");
+ return updateById(voucher);
+ }
+
+ @Override
+ public FinVoucherDetailVo detail(Long id) {
+ FinVoucher voucher = getById(id);
+ if (voucher == null) {
+ throw new ServiceException("鏌ヨ澶辫触锛屽嚟璇佷笉瀛樺湪");
+ }
+ LambdaQueryWrapper<FinVoucherEntry> wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(FinVoucherEntry::getVoucherId, id)
+ .orderByAsc(FinVoucherEntry::getRowNo)
+ .orderByAsc(FinVoucherEntry::getId);
+ List<FinVoucherEntry> entries = finVoucherEntryMapper.selectList(wrapper);
+
+ FinVoucherDetailVo vo = new FinVoucherDetailVo();
+ BeanUtils.copyProperties(voucher, vo);
+ vo.setEntries(entries);
+ return vo;
+ }
+
+ /**
+ * 鏍¢獙鍑瘉涓昏〃瀛楁銆佺姸鎬佸瓧娈典笌鍞竴鎬с��
+ */
+ private void validateVoucherBasicInfo(FinVoucherDto dto, boolean isUpdate) {
+ if (dto == null) {
+ throw new ServiceException("鍑瘉鏁版嵁涓嶈兘涓虹┖");
+ }
+ if (isUpdate && dto.getId() == null) {
+ throw new ServiceException("淇敼澶辫触锛屽嚟璇両D涓嶈兘涓虹┖");
+ }
+ if (StringUtils.isEmpty(dto.getVoucherNo())) {
+ throw new ServiceException("鍑瘉瀛楀彿涓嶈兘涓虹┖");
+ }
+ if (dto.getVoucherDate() == null) {
+ throw new ServiceException("鍑瘉鏃ユ湡涓嶈兘涓虹┖");
+ }
+ LambdaQueryWrapper<FinVoucher> uniqueWrapper = new LambdaQueryWrapper<>();
+ uniqueWrapper.eq(FinVoucher::getVoucherNo, dto.getVoucherNo());
+ if (isUpdate) {
+ uniqueWrapper.ne(FinVoucher::getId, dto.getId());
+ }
+ if (count(uniqueWrapper) > 0) {
+ throw new ServiceException("鍑瘉瀛楀彿宸插瓨鍦紝璇峰嬁閲嶅鎻愪氦");
+ }
+ }
+
+ /**
+ * 杩囨护鏈夋晥鍒嗗綍骞舵墽琛屽�熻捶骞宠 鏍¢獙銆�
+ */
+ private List<FinVoucherEntry> buildAndValidateEntries(FinVoucherDto dto) {
+ List<FinVoucherEntryDto> rawEntries = dto.getEntries();
+ if (rawEntries == null || rawEntries.isEmpty()) {
+ throw new ServiceException("鍒嗗綍涓嶈兘涓虹┖锛岃嚦灏戦渶瑕佷竴鏉℃湁鏁堝垎褰�");
+ }
+ List<FinVoucherEntry> validEntries = new ArrayList<>();
+ int rowNo = 1;
+ for (FinVoucherEntryDto entryDto : rawEntries) {
+ if (entryDto == null || StringUtils.isEmpty(entryDto.getSubjectCode())) {
+ continue;
+ }
+ BigDecimal debit = defaultMoney(entryDto.getDebit());
+ BigDecimal credit = defaultMoney(entryDto.getCredit());
+ if (debit.compareTo(BigDecimal.ZERO) <= 0 && credit.compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ if (debit.compareTo(BigDecimal.ZERO) > 0 && credit.compareTo(BigDecimal.ZERO) > 0) {
+ throw new ServiceException("鍒嗗綍鍊熸柟鍜岃捶鏂逛笉鑳藉悓鏃跺ぇ浜�0");
+ }
+ FinVoucherEntry entry = new FinVoucherEntry();
+ BeanUtils.copyProperties(entryDto, entry);
+ entry.setDebit(debit);
+ entry.setCredit(credit);
+ entry.setRowNo(rowNo++);
+ validEntries.add(entry);
+ }
+ if (validEntries.isEmpty()) {
+ throw new ServiceException("鍒嗗綍鑷冲皯闇�瑕佷竴鏉℃湁鏁堣锛堢鐩笉绌猴紝涓斿�熸柟鎴栬捶鏂瑰ぇ浜�0锛�");
+ }
+
+ // 鍒嗗綍绉戠洰蹇呴』瀛樺湪锛岄伩鍏嶈剰绉戠洰缂栫爜鍏ヨ处銆�
+ Set<String> subjectCodes = validEntries.stream()
+ .map(FinVoucherEntry::getSubjectCode)
+ .filter(StringUtils::isNotEmpty)
+ .collect(Collectors.toSet());
+ if (subjectCodes.isEmpty()) {
+ throw new ServiceException("鍒嗗綍绉戠洰涓嶈兘涓虹┖");
+ }
+ LambdaQueryWrapper<AccountSubject> subjectWrapper = new LambdaQueryWrapper<>();
+ subjectWrapper.in(AccountSubject::getSubjectCode, subjectCodes);
+ List<AccountSubject> subjects = accountSubjectMapper.selectList(subjectWrapper);
+ Map<String, AccountSubject> subjectMap = subjects.stream()
+ .collect(Collectors.toMap(AccountSubject::getSubjectCode, it -> it, (a, b) -> a));
+ for (FinVoucherEntry entry : validEntries) {
+ AccountSubject accountSubject = subjectMap.get(entry.getSubjectCode());
+ if (accountSubject == null) {
+ throw new ServiceException("绉戠洰缂栫爜涓嶅瓨鍦細" + entry.getSubjectCode());
+ }
+ if (StringUtils.isEmpty(entry.getSubjectName())) {
+ entry.setSubjectName(accountSubject.getSubjectName());
+ }
+ }
+
+ BigDecimal totalDebit = calculateTotalDebit(validEntries);
+ BigDecimal totalCredit = calculateTotalCredit(validEntries);
+ if (totalDebit.compareTo(BigDecimal.ZERO) <= 0 || totalCredit.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new ServiceException("鍊熻捶閲戦蹇呴』澶т簬0");
+ }
+ if (totalDebit.compareTo(totalCredit) != 0) {
+ throw new ServiceException("鍊熻捶涓嶅钩琛★紝绂佹淇濆瓨");
+ }
+ return validEntries;
+ }
+
+ private void saveEntries(Long voucherId, List<FinVoucherEntry> entries) {
+ if (voucherId == null) {
+ throw new ServiceException("鍑瘉ID涓嶈兘涓虹┖");
+ }
+ for (FinVoucherEntry entry : entries) {
+ entry.setVoucherId(voucherId);
+ finVoucherEntryMapper.insert(entry);
+ }
+ }
+
+ private String findDefaultSummary(List<FinVoucherEntry> entries) {
+ for (FinVoucherEntry entry : entries) {
+ if (StringUtils.isNotEmpty(entry.getSummary())) {
+ return entry.getSummary();
+ }
+ }
+ return "";
+ }
+
+ private BigDecimal calculateTotalDebit(List<FinVoucherEntry> entries) {
+ BigDecimal total = BigDecimal.ZERO;
+ for (FinVoucherEntry entry : entries) {
+ total = total.add(defaultMoney(entry.getDebit()));
+ }
+ return total.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal calculateTotalCredit(List<FinVoucherEntry> entries) {
+ BigDecimal total = BigDecimal.ZERO;
+ for (FinVoucherEntry entry : entries) {
+ total = total.add(defaultMoney(entry.getCredit()));
+ }
+ return total.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal defaultMoney(BigDecimal value) {
+ if (value == null) {
+ return ZERO;
+ }
+ return value.setScale(2, RoundingMode.HALF_UP);
+ }
+}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 334e5d7..bda6bcf 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -28,7 +28,7 @@
# 寮�鍙戠幆澧冮厤缃�
server:
# 鏈嶅姟鍣ㄧ殑HTTP绔彛锛岄粯璁や负8080
- port: 7005
+ port: 7006
servlet:
# 搴旂敤鐨勮闂矾寰�
context-path: /
diff --git a/src/main/resources/mapper/account/AccountSubjectMapper.xml b/src/main/resources/mapper/account/AccountSubjectMapper.xml
index 179d858..95f450f 100644
--- a/src/main/resources/mapper/account/AccountSubjectMapper.xml
+++ b/src/main/resources/mapper/account/AccountSubjectMapper.xml
@@ -5,6 +5,7 @@
<!-- 閫氱敤鏌ヨ鏄犲皠缁撴灉 -->
<resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.AccountSubject">
<id column="id" property="id" />
+ <result column="parent_id" property="parentId" />
<result column="subject_code" property="subjectCode" />
<result column="subject_name" property="subjectName" />
<result column="subject_type" property="subjectType" />
@@ -18,4 +19,13 @@
<result column="dept_id" property="deptId" />
</resultMap>
+ <select id="countReferencedBySubjectCodes" resultType="java.lang.Long">
+ SELECT COUNT(1)
+ FROM fin_voucher_entry
+ WHERE subject_code IN
+ <foreach collection="subjectCodes" item="item" open="(" separator="," close=")">
+ #{item}
+ </foreach>
+ </select>
+
</mapper>
diff --git a/src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml b/src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml
new file mode 100644
index 0000000..56633ba
--- /dev/null
+++ b/src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.account.mapper.financial.FinVoucherEntryMapper">
+
+ <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.financial.FinVoucherEntry">
+ <id column="id" property="id"/>
+ <result column="voucher_id" property="voucherId"/>
+ <result column="row_no" property="rowNo"/>
+ <result column="subject_code" property="subjectCode"/>
+ <result column="subject_name" property="subjectName"/>
+ <result column="summary" property="summary"/>
+ <result column="debit" property="debit"/>
+ <result column="credit" property="credit"/>
+ <result column="auxiliary_type" property="auxiliaryType"/>
+ <result column="auxiliary_id" property="auxiliaryId"/>
+ <result column="auxiliary_name" property="auxiliaryName"/>
+ <result column="create_user" property="createUser"/>
+ <result column="create_time" property="createTime"/>
+ <result column="update_user" property="updateUser"/>
+ <result column="update_time" property="updateTime"/>
+ <result column="dept_id" property="deptId"/>
+ </resultMap>
+
+ <select id="listPostedEntries" resultType="com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo">
+ SELECT
+ v.voucher_date AS voucherDate,
+ v.voucher_no AS voucherNo,
+ CASE
+ WHEN e.summary IS NOT NULL AND e.summary != '' THEN e.summary
+ ELSE v.summary
+ END AS summary,
+ e.debit AS debit,
+ e.credit AS credit,
+ e.row_no AS rowNo
+ FROM fin_voucher_entry e
+ INNER JOIN fin_voucher v ON e.voucher_id = v.id
+ WHERE v.status = 'posted'
+ AND (e.subject_code = #{subjectCode} OR e.subject_code LIKE CONCAT(#{subjectCode}, '%'))
+ AND v.voucher_date <![CDATA[>=]]> #{startDate}
+ AND v.voucher_date <![CDATA[<=]]> #{endDate}
+ <if test="auxiliaryType != null and auxiliaryType != ''">
+ AND e.auxiliary_type = #{auxiliaryType}
+ </if>
+ <if test="auxiliaryId != null and auxiliaryId != ''">
+ AND e.auxiliary_id = #{auxiliaryId}
+ </if>
+ ORDER BY v.voucher_date ASC, v.id ASC, e.row_no ASC, e.id ASC
+ </select>
+
+ <select id="listPostedEntriesBefore" resultType="com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo">
+ SELECT
+ v.voucher_date AS voucherDate,
+ v.voucher_no AS voucherNo,
+ CASE
+ WHEN e.summary IS NOT NULL AND e.summary != '' THEN e.summary
+ ELSE v.summary
+ END AS summary,
+ e.debit AS debit,
+ e.credit AS credit,
+ e.row_no AS rowNo
+ FROM fin_voucher_entry e
+ INNER JOIN fin_voucher v ON e.voucher_id = v.id
+ WHERE v.status = 'posted'
+ AND (e.subject_code = #{subjectCode} OR e.subject_code LIKE CONCAT(#{subjectCode}, '%'))
+ AND v.voucher_date <![CDATA[<]]> #{beforeDate}
+ <if test="auxiliaryType != null and auxiliaryType != ''">
+ AND e.auxiliary_type = #{auxiliaryType}
+ </if>
+ <if test="auxiliaryId != null and auxiliaryId != ''">
+ AND e.auxiliary_id = #{auxiliaryId}
+ </if>
+ </select>
+
+</mapper>
--
Gitblit v1.9.3