gaoluyang
2 天以前 90b3ad820eea45d021835197eac5bc3ad55b12ed
feat(finance): 新增财务核算模块及 ERP 统计接口

- 新增会计科目树形管理(CRUD、编码唯一校验、父子循环检测)
- 新增记账凭证与分录,含借贷平衡校验与过账/作废流程
- 新增总账与明细账查询,按期间逐月汇总与逐条结转
- 新增固定资产与无形资产卡片,支持按月计提折旧与摊销
- 新增 ERP 财务统计 API,按月聚合已审核的收付款与购销金额
- 新增财务核算看板与财务报表,消费 ERP 统计数据
已修改20个文件
已添加73个文件
6008 ■■■■■ 文件已修改
docs/project-business/README.md 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/business.json 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/flows.json 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/modules.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/objects.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/roles.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/index.html 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/01-business-mindmap.mmd 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/08-module-relation.mmd 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/09-data-flow.mmd 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/10-business-object.mmd 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/finance_menu_init.sql 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/finance_module_init.sql 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinanceStatisticsApi.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/dto/ErpFinanceMonthlyStatisticsRespDTO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinanceStatisticsApiImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinanceStatisticsMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/resources/mapper/finance/ErpFinanceStatisticsMapper.xml 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/pom.xml 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/FinFixedAssetController.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/FinIntangibleAssetController.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetDepreciateRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetPageReqVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetRespVO.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetSaveReqVO.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetAmortizeRespVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetPageReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetRespVO.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetSaveReqVO.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/ledger/FinLedgerController.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/ledger/vo/FinLedgerReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/ledger/vo/FinLedgerRespVO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/FinStatisticsController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/vo/FinStatisticsOverviewRespVO.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/vo/FinStatisticsReqVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/vo/FinStatisticsTrendRespVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/FinAccountSubjectController.java 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/vo/FinAccountSubjectPageReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/vo/FinAccountSubjectRespVO.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/vo/FinAccountSubjectSaveReqVO.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/FinVoucherController.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherEntryRespVO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherEntrySaveReqVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherRespVO.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherSaveReqVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/asset/FinFixedAssetDO.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/asset/FinIntangibleAssetDO.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/ledger/FinLedgerEntryDO.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/subject/FinAccountSubjectDO.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/voucher/FinVoucherDO.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/voucher/FinVoucherEntryDO.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/asset/FinFixedAssetMapper.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/asset/FinIntangibleAssetMapper.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/ledger/FinLedgerMapper.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/subject/FinAccountSubjectMapper.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/voucher/FinVoucherEntryMapper.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/voucher/FinVoucherMapper.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/redis/RedisKeyConstants.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/redis/no/FinNoRedisDAO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/DictTypeConstants.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/ErrorCodeConstants.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinAuxiliaryTypeEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinBalanceDirectionEnum.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinFixedAssetStatusEnum.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinIntangibleAssetStatusEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinLedgerRowTypeEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinSubjectTypeEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinVoucherStatusEnum.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/framework/web/config/FinanceWebConfiguration.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinAssetAmountUtils.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinFixedAssetService.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinFixedAssetServiceImpl.java 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinIntangibleAssetService.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinIntangibleAssetServiceImpl.java 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/ledger/FinLedgerService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/ledger/FinLedgerServiceImpl.java 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/statistics/FinStatisticsService.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/statistics/FinStatisticsServiceImpl.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/subject/FinAccountSubjectService.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/subject/FinAccountSubjectServiceImpl.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/voucher/FinVoucherService.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/voucher/FinVoucherServiceImpl.java 389 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-finance/src/main/resources/mapper/ledger/FinLedgerMapper.xml 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-trt.yaml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/README.md
@@ -14,12 +14,12 @@
```text
docs/project-business/
├── index.html      # å¯è§†åŒ–页面:双击直接打开(file:// å³å¯ï¼Œæ— éœ€ä»»ä½•服务)
├── mermaid/        # 11 å¼  Mermaid ä¸šåŠ¡å›¾ï¼ˆæƒå¨æºï¼Œæ”¹è¿™é‡Œï¼‰
├── mermaid/        # 12 å¼  Mermaid ä¸šåŠ¡å›¾ï¼ˆæƒå¨æºï¼Œæ”¹è¿™é‡Œï¼‰
├── data/           # ä¸šåŠ¡æ•°æ®ï¼ˆæƒå¨æºï¼Œæ”¹è¿™é‡Œï¼‰
│   â”œâ”€â”€ business.json  # é¡¹ç›®å…ƒä¿¡æ¯/业务十问/AI能力/待确认事项
│   â”œâ”€â”€ modules.json   # 12 ä¸ªä¸šåŠ¡æ¨¡å—å®šä¹‰
│   â”œâ”€â”€ modules.json   # 14 ä¸ªä¸šåŠ¡æ¨¡å—å®šä¹‰
│   â”œâ”€â”€ objects.json   # æ ¸å¿ƒä¸šåŠ¡å¯¹è±¡ï¼ˆä½œç”¨/上游/下游/表名)
│   â”œâ”€â”€ flows.json     # 7 æ¡æµç¨‹ï¼ˆæ‘˜è¦+步骤+角色)
│   â”œâ”€â”€ flows.json     # 8 æ¡æµç¨‹ï¼ˆæ‘˜è¦+步骤+角色)
│   â””── roles.json     # è§’色权限
└── README.md
```
@@ -45,6 +45,7 @@
| 09-data-flow | è·¨æ¨¡å—数据流(单据生成、字段带入、数量回写、事件) |
| 10-business-object | æ ¸å¿ƒä¸šåŠ¡å¯¹è±¡ ER(只含业务对象,非全库表) |
| 11-ai-business-flow | AI ä¸šåŠ¡é“¾è·¯ï¼ˆOCR/质检判定/四类预测/RAG,含 API ä¾èµ–) |
| 12-finance-flow | ä¼šè®¡æ ¸ç®—:科目建账→凭证录入(借贷平衡校验)→过账/作废→总账明细账→资产折旧摊销→看板取数 ERP å·²å®¡æ ¸å•据 |
图中状态数字均为源码枚举真实值(如工单 0草稿/1已确认/2完成/3取消;ERP å®¡æ‰¹ 15审批中/20已审核)。
docs/project-business/data/business.json
@@ -21,9 +21,9 @@
    "frontend": "mom-pro2-before(Vue3 ç®¡ç†åŽå°ï¼ŒåŒä»“库存放,只读参考)"
  },
  "stats": {
    "businessModules": 13,
    "coreObjects": 48,
    "documentFlows": 6,
    "businessModules": 14,
    "coreObjects": 53,
    "documentFlows": 7,
    "approvalListeners": 13,
    "aiCapabilities": 8,
    "wmBillTypes": 15
@@ -143,7 +143,8 @@
    "MesWmSnDO æ–°å¢žçš„ uuid å­—段生成/消费逻辑未在核心 Service ä¸­ç¡®è®¤",
    "转正申请(hrm_regularize_application)、培训证书等仅有 SQL ä¸Žèœå•初始化,Java ä¾§ Service/Controller æœªå®žçް",
    "NCR å¤„置结果(返工/退货)未回写触发源单据,仅台账记录",
    "MPS çŠ¶æ€æžšä¸¾ç‹¬ç«‹ç¼–å·(0/1/2/3),与通用单据常量(0/1/4/5)不一致"
    "MPS çŠ¶æ€æžšä¸¾ç‹¬ç«‹ç¼–å·(0/1/2/3),与通用单据常量(0/1/4/5)不一致",
    "财务管理(会计核算)模块 yudao-module-finance ä»£ç å·²è½åœ°ï¼Œä½†â‘ èœå•/权限未配置(需自行在菜单管理建 finance:* æƒé™ï¼‰â‘¡å»ºè¡¨ä¸Žå­—典脚本未同步进 docs/sql/config_export_all_<日期>.sql é˜¶æ®µä¸€ï¼ŒæŒ‰çŽ°æœ‰åˆå¹¶è„šæœ¬åˆå§‹åŒ–æ–°åº“ä¼šç¼º fin_* è¡¨ä¸Ž 6 ç»„ finance_* å­—典【待确认】"
  ],
  "generatedAt": "2026-09-10",
  "analysisBasis": "基于 dev_business åˆ†æ”¯ 87bf4a99 æºç ï¼Œ6 ç»„并行源码走读(MES ç”Ÿäº§è´¨é‡ / MES ä»“储 / ERP / CRM+SRM / å”®åŽ+HRM / AI+BI+MDM+BPM+底座)"
docs/project-business/data/flows.json
@@ -54,6 +54,15 @@
       {"stage": "判定", "action": "问题判定: ä¸€èˆ¬/ç»´ä¿®/退货,严重度 0-3", "roles": ["售后工程师"]},
       {"stage": "ç»´ä¿®", "action": "现场/返厂/远程维修,费用与结果记录,完成联动工单完结", "roles": ["维修工程师"]},
       {"stage": "退货", "action": "退货申请审核→自动建 MES é”€å”®é€€è´§å•(RQC)→质检/入库回调→退款回调→关闭", "roles": ["售后", "质检", "财务"]}]},
    {"id": "finance", "name": "会计核算流程", "mermaid": "mermaid/12-finance-flow.mmd", "color": "#3ec9a7",
     "summary": "独立核算模块 yudao-module-finance:会计科目建账 â†’ è®°è´¦å‡­è¯å½•å…¥(借贷平衡校验) â†’ è¿‡è´¦ â†’ æ€»è´¦/明细账 â†’ èµ„产计提折旧与摊销 â†’ è´¢åŠ¡æŠ¥è¡¨çœ‹æ¿å–æ•° ERP å·²å®¡æ ¸å•据。",
     "steps": [
       {"stage": "建账", "action": "维护会计科目 tree(subjectCode å”¯ä¸€ï¼Œå¯å»ºä¸‹çº§),首次可导入约 68 æ¡ä¸€çº§ç§‘目种子", "roles": ["会计"]},
       {"stage": "è®°è´¦", "action": "录入凭证(字号留空自动生成)+分录,分录逐行选科目并填借/贷;校验至少一行、借贷都不得同时大于0、合计相等且大于0", "roles": ["会计"]},
       {"stage": "过账", "action": "未过账0 â†’ post è¿‡è´¦ â†’ å·²è¿‡è´¦10(不可再修改);未过账0 æˆ– å·²è¿‡è´¦10 å‡å¯ cancel ä½œåºŸ â†’ å·²ä½œåºŸ20", "roles": ["会计", "财务主管"]},
       {"stage": "查账", "action": "总账按期间逐月汇总、明细账逐条分录并结转余额,两者只统计已过账10的凭证,并支持导出", "roles": ["会计", "财务主管"]},
       {"stage": "资产", "action": "固定资产(1在用/2闲置/3维修中/4已报废)与无形资产(1在用/2已失效/3已摊销完)计提:传 ids æŒ‰é€‰ä¸­è®¡æï¼Œids ä¸ºç©ºå¯¹å…¨éƒ¨æœªæŠ¥åºŸ/未失效资产计提一个月", "roles": ["资产会计"]},
       {"stage": "报表", "action": "看板汇总资产与凭证时点数 + ç» ErpFinanceStatisticsApi å– ERP å·²å®¡æ ¸æ”¶è´§ä»˜æ¬¾ä¸Žè´­é”€é‡‘额,输出月度收支趋势", "roles": ["财务主管", "管理层"]}]},
    {"id": "ai", "name": "AI ä¸šåŠ¡æµç¨‹", "mermaid": "mermaid/11-ai-business-flow.mmd", "color": "#56c8d8",
     "summary": "统一模式:业务侧收集上下文(必要时调 MDM/SRM/CRM API å–名称)→组装 Prompt→chat/chatWithImage→解析 JSON→前端预填或建议;无 Function Calling;RAG çŸ¥è¯†åº“已建成但暂无业务消费。",
     "steps": [
docs/project-business/data/modules.json
@@ -32,6 +32,14 @@
     "roles": ["采购员", "销售员", "出纳", "会计"],
     "upstream": ["CRM(合同)", "SRM(定标)", "主数据"], "downstream": ["生产(MPS)", "仓储(收发)"],
     "coreTables": ["erp_purchase_request", "erp_purchase_order", "erp_purchase_invoice", "erp_finance_payment", "erp_sale_order", "erp_finance_receipt", "erp_account"]},
    {"key": "finance", "name": "财务管理(会计核算)", "color": "#3ec9a7", "tag": "FIN", "flow": "finance",
     "positioning": "独立核算模块:会计核算 + èµ„产核算 + è´¢åŠ¡æŠ¥è¡¨çœ‹æ¿ï¼ˆä¸Ž ERP è¿›é”€å­˜è´¢åŠ¡å•æ®åˆ†ç¦»ï¼Œyudao-module-finance)",
     "problem": "进销存只管单据不管账,缺少会计科目/凭证/账簿与资产折旧摊销沉淀,无法出账表与资产净值",
     "features": ["会计科目树(约68条一级科目种子·可建下级)", "记账凭证(字号自动生成·分录借贷平衡前后端双重校验)", "凭证状态机 æœªè¿‡è´¦0→过账10/作废20,已过账不可改", "总账按期间逐月汇总", "明细账逐条分录并结转余额", "固定资产+计提折旧", "无形资产+计提摊销", "会计核算看板(收支/资产/凭证/月度趋势)", "附件走系统附件中心 recordType=fin_voucher"],
     "objects": ["会计科目", "记账凭证", "凭证分录", "固定资产", "无形资产"],
     "roles": ["会计", "财务主管", "资产会计", "管理层"],
     "upstream": ["进销存(ERP å·²å®¡æ ¸æ”¶è´§/付款/购销单据)"], "downstream": ["财务报表看板", "BI ç®¡ç†é©¾é©¶èˆ±"],
     "coreTables": ["fin_account_subject", "fin_voucher", "fin_voucher_entry", "fin_fixed_asset", "fin_intangible_asset"]},
    {"key": "pro", "name": "生产管理", "color": "#e0b34f", "tag": "MES-PRO", "flow": "production",
     "positioning": "生产执行核心:计划-工单-任务-报工",
     "problem": "订单到车间执行的逐层拆解与过程可控",
docs/project-business/data/objects.json
@@ -19,6 +19,11 @@
    {"name": "采购来票", "module": "进销存", "table": "erp_purchase_invoice", "doc": "FPD编号,AI OCR é¢„填,累计≤订单总价,付款单的前置", "upstream": ["采购订单"], "downstream": ["付款单"], "keywords": "来票 å‘票 é‡‡è´­å‘票"},
    {"name": "付款单", "module": "财务", "table": "erp_finance_payment", "doc": "FKD编号,必须关联来票+附件,累计≤来票金额,BPM å®¡æ‰¹", "upstream": ["采购来票"], "downstream": ["结算账户"], "keywords": "付款 åº”付 payment"},
    {"name": "收款单", "module": "财务", "table": "erp_finance_receipt", "doc": "SKD编号,关联 CRM å®¢æˆ·æ ¡éªŒï¼Œæ—  BPM(手动审核)", "upstream": ["销售(弱关联)"], "downstream": ["结算账户"], "keywords": "收款 åº”æ”¶ receipt"},
    {"name": "会计科目", "module": "会计核算", "table": "fin_account_subject", "doc": "科目树,parent_id è‡ªå…³è”;科目类型1资产2负债3权益4成本5损益,余额方向1借2贷,状态0启用1禁用;账表与分录统一按 subject_code å–æ•°", "upstream": ["会计科目种子约68条一级科目"], "downstream": ["记账凭证分录", "总账", "明细账"], "keywords": "会计科目 ç§‘ç›® subject account èµ„产类 è´Ÿå€ºç±» æŸç›Šç±»"},
    {"name": "记账凭证", "module": "会计核算", "table": "fin_voucher", "doc": "凭证字号可留空由后端自动生成;状态 æœªè¿‡è´¦0/已过账10/已作废20;已过账不可改,未过账与已过账均可作废,未过账与已作废可删除;附件按 recordType=fin_voucher æŒ‚系统附件中心", "upstream": ["会计科目"], "downstream": ["总账", "明细账", "财务报表看板"], "keywords": "记账凭证 å‡­è¯ voucher è¿‡è´¦ ä½œåºŸ å€Ÿè´·å¹³è¡¡"},
    {"name": "凭证分录", "module": "会计核算", "table": "fin_voucher_entry", "doc": "凭证下逐行分录:科目编码+借/贷金额+辅助核算(1客户2供应商3部门4员工5项目);一行不可借贷同时大于0,全表借贷合计必须相等且大于0", "upstream": ["记账凭证", "会计科目"], "downstream": ["总账", "明细账"], "keywords": "凭证分录 åˆ†å½• entry å€Ÿæ–¹ è´·æ–¹ è¾…助核算"},
    {"name": "固定资产", "module": "会计核算", "table": "fin_fixed_asset", "doc": "资产卡片,状态 1在用/2闲置/3维修中/4已报废;月折旧额=(原值×(1-残值率))÷(年限×12),累计折旧与净值由计提累加,不接收前端传值", "upstream": [], "downstream": ["财务报表看板", "总账"], "keywords": "固定资产 æŠ˜æ—§ èµ„产卡片 fixed asset æ®‹å€¼çއ"},
    {"name": "无形资产", "module": "会计核算", "table": "fin_intangible_asset", "doc": "无形资产卡片(证书编号/有效期),状态 1在用/2已失效/3已摊销完;月摊销额=(原值×(1-残值率))÷(摊销年限×12),累计摊销与净值由计提累加", "upstream": [], "downstream": ["财务报表看板", "总账"], "keywords": "无形资产 æ‘Šé”€ è½¯ä»¶è‘—作权 ä¸“利 intangible æœ‰æ•ˆæœŸ"},
    {"name": "销售订单", "module": "进销存", "table": "erp_sale_order", "doc": "XSDD编号,BPM å®¡æ‰¹ï¼Œéœ€ç”Ÿäº§è¡Œè‡ªåŠ¨å»º MPS,出库状态:未0已预留1部分2全部3,生产完成事件回写", "upstream": ["合同"], "downstream": ["MPS", "发货通知", "销售出库", "售后工单"], "keywords": "销售订单 sale"},
    {"name": "主生产计划", "module": "生产", "table": "mes_pro_mps", "doc": "MPS计划,草稿0→已下发1→完成2/取消3,可合并下发", "upstream": ["销售订单"], "downstream": ["生产工单"], "keywords": "MPS ä¸»è®¡åˆ’ ç”Ÿäº§è®¡åˆ’"},
    {"name": "生产工单", "module": "生产", "table": "mes_pro_work_order", "doc": "草稿0→已确认1→完成2/取消3,确认时按工艺路线生成任务,最后一道工序报工回写已生产数量", "upstream": ["MPS"], "downstream": ["生产任务", "领料", "完工入库"], "keywords": "工单 ç”Ÿäº§å·¥å• work order"},
docs/project-business/data/roles.json
@@ -17,7 +17,7 @@
    {"role": "质检员", "modules": ["MES-QC"], "operations": ["IQC/IPQC/OQC/RQC æ£€éªŒ", "NCR è¯„审处置"], "data": ["mes_qc_*"], "scope": "inspectorUserId å½’属"},
    {"role": "设备员/维修工", "modules": ["MES-DV"], "operations": ["点检保养", "维修工单", "工装领用"], "data": ["mes_dv_*", "mes_tm_*"], "scope": "chargeUserId å½’属"},
    {"role": "客服/售后工程师", "modules": ["售后"], "operations": ["工单受理派工", "ç»´ä¿®", "退货申请"], "data": ["after_sale_*"], "scope": "行级数据权限四视图:我负责/我参与/下属负责/全部(aftersales:view-all)"},
    {"role": "财务", "modules": ["ERP", "CRM"], "operations": ["收款单", "付款单", "开票审核", "结算账户"], "data": ["erp_finance_*", "crm_invoice", "crm_receivable"], "scope": "financeUserId å½’属"},
    {"role": "财务", "modules": ["ERP", "CRM", "FIN"], "operations": ["收款单", "付款单", "开票审核", "结算账户", "会计科目维护", "记账凭证录入与过账/作废", "总账与明细账查询", "固定资产折旧计提", "无形资产摊销计提", "会计核算看板"], "data": ["erp_finance_*", "crm_invoice", "crm_receivable", "fin_account_subject", "fin_voucher", "fin_voucher_entry", "fin_fixed_asset", "fin_intangible_asset"], "scope": "financeUserId å½’属;财务核算模块按 finance:* æŽ¥å£æƒé™ + system_menu æŽˆæƒï¼Œæš‚无数据权限字段"},
    {"role": "HR", "modules": ["HRM"], "operations": ["员工档案入转调离", "考勤规则排班", "薪酬核算发放", "离职交接"], "data": ["hrm_*"], "scope": "部门范围 dataScope"},
    {"role": "管理层", "modules": ["BI"], "operations": ["查看采购销售/仓库/生产设备/质量/人力五大主题大屏"], "data": ["bi_dashboard"], "scope": "只读"}
  ]
docs/project-business/index.html
@@ -111,6 +111,7 @@
  <a data-t="flow-quality"><span class="dot" style="background:var(--c-qc)"></span>质量流程</a>
  <a data-t="flow-warehouse"><span class="dot" style="background:var(--c-wm)"></span>仓储流程</a>
  <a data-t="flow-aftersales"><span class="dot" style="background:var(--c-as)"></span>售后流程</a>
  <a data-t="flow-finance"><span class="dot" style="background:var(--c-fin)"></span>会计核算</a>
  <div class="group">结构与数据</div>
  <a data-t="relation"><span class="dot" style="background:var(--c-bpm)"></span>系统关系</a>
  <a data-t="dataflow"><span class="dot" style="background:var(--c-fin)"></span>数据流</a>
@@ -137,6 +138,7 @@
<section class="page" id="flow-quality"></section>
<section class="page" id="flow-warehouse"></section>
<section class="page" id="flow-aftersales"></section>
<section class="page" id="flow-finance"></section>
<section class="page" id="relation">
  <h2 class="pt">系统关系</h2>
  <p class="desc">模块之间的调用与数据依赖:实线=单据/数据流向,虚线=审批回调与辅助能力。</p>
@@ -201,9 +203,9 @@
    "frontend": "mom-pro2-before(Vue3 ç®¡ç†åŽå°ï¼ŒåŒä»“库存放,只读参考)"
  },
  "stats": {
    "businessModules": 13,
    "coreObjects": 48,
    "documentFlows": 6,
    "businessModules": 14,
    "coreObjects": 53,
    "documentFlows": 7,
    "approvalListeners": 13,
    "aiCapabilities": 8,
    "wmBillTypes": 15
@@ -323,7 +325,8 @@
    "MesWmSnDO æ–°å¢žçš„ uuid å­—段生成/消费逻辑未在核心 Service ä¸­ç¡®è®¤",
    "转正申请(hrm_regularize_application)、培训证书等仅有 SQL ä¸Žèœå•初始化,Java ä¾§ Service/Controller æœªå®žçް",
    "NCR å¤„置结果(返工/退货)未回写触发源单据,仅台账记录",
    "MPS çŠ¶æ€æžšä¸¾ç‹¬ç«‹ç¼–å·(0/1/2/3),与通用单据常量(0/1/4/5)不一致"
    "MPS çŠ¶æ€æžšä¸¾ç‹¬ç«‹ç¼–å·(0/1/2/3),与通用单据常量(0/1/4/5)不一致",
    "财务管理(会计核算)模块 yudao-module-finance ä»£ç å·²è½åœ°ï¼Œä½†â‘ èœå•/权限未配置(需自行在菜单管理建 finance:* æƒé™ï¼‰â‘¡å»ºè¡¨ä¸Žå­—典脚本未同步进 docs/sql/config_export_all_<日期>.sql é˜¶æ®µä¸€ï¼ŒæŒ‰çŽ°æœ‰åˆå¹¶è„šæœ¬åˆå§‹åŒ–æ–°åº“ä¼šç¼º fin_* è¡¨ä¸Ž 6 ç»„ finance_* å­—典【待确认】"
  ],
  "generatedAt": "2026-09-10",
  "analysisBasis": "基于 dev_business åˆ†æ”¯ 87bf4a99 æºç ï¼Œ6 ç»„并行源码走读(MES ç”Ÿäº§è´¨é‡ / MES ä»“储 / ERP / CRM+SRM / å”®åŽ+HRM / AI+BI+MDM+BPM+底座)"
@@ -362,6 +365,14 @@
     "roles": ["采购员", "销售员", "出纳", "会计"],
     "upstream": ["CRM(合同)", "SRM(定标)", "主数据"], "downstream": ["生产(MPS)", "仓储(收发)"],
     "coreTables": ["erp_purchase_request", "erp_purchase_order", "erp_purchase_invoice", "erp_finance_payment", "erp_sale_order", "erp_finance_receipt", "erp_account"]},
    {"key": "finance", "name": "财务管理(会计核算)", "color": "#3ec9a7", "tag": "FIN", "flow": "finance",
     "positioning": "独立核算模块:会计核算 + èµ„产核算 + è´¢åŠ¡æŠ¥è¡¨çœ‹æ¿ï¼ˆä¸Ž ERP è¿›é”€å­˜è´¢åŠ¡å•æ®åˆ†ç¦»ï¼Œyudao-module-finance)",
     "problem": "进销存只管单据不管账,缺少会计科目/凭证/账簿与资产折旧摊销沉淀,无法出账表与资产净值",
     "features": ["会计科目树(约68条一级科目种子·可建下级)", "记账凭证(字号自动生成·分录借贷平衡前后端双重校验)", "凭证状态机 æœªè¿‡è´¦0→过账10/作废20,已过账不可改", "总账按期间逐月汇总", "明细账逐条分录并结转余额", "固定资产+计提折旧", "无形资产+计提摊销", "会计核算看板(收支/资产/凭证/月度趋势)", "附件走系统附件中心 recordType=fin_voucher"],
     "objects": ["会计科目", "记账凭证", "凭证分录", "固定资产", "无形资产"],
     "roles": ["会计", "财务主管", "资产会计", "管理层"],
     "upstream": ["进销存(ERP å·²å®¡æ ¸æ”¶è´§/付款/购销单据)"], "downstream": ["财务报表看板", "BI ç®¡ç†é©¾é©¶èˆ±"],
     "coreTables": ["fin_account_subject", "fin_voucher", "fin_voucher_entry", "fin_fixed_asset", "fin_intangible_asset"]},
    {"key": "pro", "name": "生产管理", "color": "#e0b34f", "tag": "MES-PRO", "flow": "production",
     "positioning": "生产执行核心:计划-工单-任务-报工",
     "problem": "订单到车间执行的逐层拆解与过程可控",
@@ -462,6 +473,11 @@
    {"name": "采购来票", "module": "进销存", "table": "erp_purchase_invoice", "doc": "FPD编号,AI OCR é¢„填,累计≤订单总价,付款单的前置", "upstream": ["采购订单"], "downstream": ["付款单"], "keywords": "来票 å‘票 é‡‡è´­å‘票"},
    {"name": "付款单", "module": "财务", "table": "erp_finance_payment", "doc": "FKD编号,必须关联来票+附件,累计≤来票金额,BPM å®¡æ‰¹", "upstream": ["采购来票"], "downstream": ["结算账户"], "keywords": "付款 åº”付 payment"},
    {"name": "收款单", "module": "财务", "table": "erp_finance_receipt", "doc": "SKD编号,关联 CRM å®¢æˆ·æ ¡éªŒï¼Œæ—  BPM(手动审核)", "upstream": ["销售(弱关联)"], "downstream": ["结算账户"], "keywords": "收款 åº”æ”¶ receipt"},
    {"name": "会计科目", "module": "会计核算", "table": "fin_account_subject", "doc": "科目树,parent_id è‡ªå…³è”;科目类型1资产2负债3权益4成本5损益,余额方向1借2贷,状态0启用1禁用;账表与分录统一按 subject_code å–æ•°", "upstream": ["会计科目种子约68条一级科目"], "downstream": ["记账凭证分录", "总账", "明细账"], "keywords": "会计科目 ç§‘ç›® subject account èµ„产类 è´Ÿå€ºç±» æŸç›Šç±»"},
    {"name": "记账凭证", "module": "会计核算", "table": "fin_voucher", "doc": "凭证字号可留空由后端自动生成;状态 æœªè¿‡è´¦0/已过账10/已作废20;已过账不可改,未过账与已过账均可作废,未过账与已作废可删除;附件按 recordType=fin_voucher æŒ‚系统附件中心", "upstream": ["会计科目"], "downstream": ["总账", "明细账", "财务报表看板"], "keywords": "记账凭证 å‡­è¯ voucher è¿‡è´¦ ä½œåºŸ å€Ÿè´·å¹³è¡¡"},
    {"name": "凭证分录", "module": "会计核算", "table": "fin_voucher_entry", "doc": "凭证下逐行分录:科目编码+借/贷金额+辅助核算(1客户2供应商3部门4员工5项目);一行不可借贷同时大于0,全表借贷合计必须相等且大于0", "upstream": ["记账凭证", "会计科目"], "downstream": ["总账", "明细账"], "keywords": "凭证分录 åˆ†å½• entry å€Ÿæ–¹ è´·æ–¹ è¾…助核算"},
    {"name": "固定资产", "module": "会计核算", "table": "fin_fixed_asset", "doc": "资产卡片,状态 1在用/2闲置/3维修中/4已报废;月折旧额=(原值×(1-残值率))÷(年限×12),累计折旧与净值由计提累加,不接收前端传值", "upstream": [], "downstream": ["财务报表看板", "总账"], "keywords": "固定资产 æŠ˜æ—§ èµ„产卡片 fixed asset æ®‹å€¼çއ"},
    {"name": "无形资产", "module": "会计核算", "table": "fin_intangible_asset", "doc": "无形资产卡片(证书编号/有效期),状态 1在用/2已失效/3已摊销完;月摊销额=(原值×(1-残值率))÷(摊销年限×12),累计摊销与净值由计提累加", "upstream": [], "downstream": ["财务报表看板", "总账"], "keywords": "无形资产 æ‘Šé”€ è½¯ä»¶è‘—作权 ä¸“利 intangible æœ‰æ•ˆæœŸ"},
    {"name": "销售订单", "module": "进销存", "table": "erp_sale_order", "doc": "XSDD编号,BPM å®¡æ‰¹ï¼Œéœ€ç”Ÿäº§è¡Œè‡ªåŠ¨å»º MPS,出库状态:未0已预留1部分2全部3,生产完成事件回写", "upstream": ["合同"], "downstream": ["MPS", "发货通知", "销售出库", "售后工单"], "keywords": "销售订单 sale"},
    {"name": "主生产计划", "module": "生产", "table": "mes_pro_mps", "doc": "MPS计划,草稿0→已下发1→完成2/取消3,可合并下发", "upstream": ["销售订单"], "downstream": ["生产工单"], "keywords": "MPS ä¸»è®¡åˆ’ ç”Ÿäº§è®¡åˆ’"},
    {"name": "生产工单", "module": "生产", "table": "mes_pro_work_order", "doc": "草稿0→已确认1→完成2/取消3,确认时按工艺路线生成任务,最后一道工序报工回写已生产数量", "upstream": ["MPS"], "downstream": ["生产任务", "领料", "完工入库"], "keywords": "工单 ç”Ÿäº§å·¥å• work order"},
@@ -549,6 +565,15 @@
       {"stage": "判定", "action": "问题判定: ä¸€èˆ¬/ç»´ä¿®/退货,严重度 0-3", "roles": ["售后工程师"]},
       {"stage": "ç»´ä¿®", "action": "现场/返厂/远程维修,费用与结果记录,完成联动工单完结", "roles": ["维修工程师"]},
       {"stage": "退货", "action": "退货申请审核→自动建 MES é”€å”®é€€è´§å•(RQC)→质检/入库回调→退款回调→关闭", "roles": ["售后", "质检", "财务"]}]},
    {"id": "finance", "name": "会计核算流程", "mermaid": "mermaid/12-finance-flow.mmd", "color": "#3ec9a7",
     "summary": "独立核算模块 yudao-module-finance:会计科目建账 â†’ è®°è´¦å‡­è¯å½•å…¥(借贷平衡校验) â†’ è¿‡è´¦ â†’ æ€»è´¦/明细账 â†’ èµ„产计提折旧与摊销 â†’ è´¢åŠ¡æŠ¥è¡¨çœ‹æ¿å–æ•° ERP å·²å®¡æ ¸å•据。",
     "steps": [
       {"stage": "建账", "action": "维护会计科目 tree(subjectCode å”¯ä¸€ï¼Œå¯å»ºä¸‹çº§),首次可导入约 68 æ¡ä¸€çº§ç§‘目种子", "roles": ["会计"]},
       {"stage": "è®°è´¦", "action": "录入凭证(字号留空自动生成)+分录,分录逐行选科目并填借/贷;校验至少一行、借贷都不得同时大于0、合计相等且大于0", "roles": ["会计"]},
       {"stage": "过账", "action": "未过账0 â†’ post è¿‡è´¦ â†’ å·²è¿‡è´¦10(不可再修改);未过账0 æˆ– å·²è¿‡è´¦10 å‡å¯ cancel ä½œåºŸ â†’ å·²ä½œåºŸ20", "roles": ["会计", "财务主管"]},
       {"stage": "查账", "action": "总账按期间逐月汇总、明细账逐条分录并结转余额,两者只统计已过账10的凭证,并支持导出", "roles": ["会计", "财务主管"]},
       {"stage": "资产", "action": "固定资产(1在用/2闲置/3维修中/4已报废)与无形资产(1在用/2已失效/3已摊销完)计提:传 ids æŒ‰é€‰ä¸­è®¡æï¼Œids ä¸ºç©ºå¯¹å…¨éƒ¨æœªæŠ¥åºŸ/未失效资产计提一个月", "roles": ["资产会计"]},
       {"stage": "报表", "action": "看板汇总资产与凭证时点数 + ç» ErpFinanceStatisticsApi å– ERP å·²å®¡æ ¸æ”¶è´§ä»˜æ¬¾ä¸Žè´­é”€é‡‘额,输出月度收支趋势", "roles": ["财务主管", "管理层"]}]},
    {"id": "ai", "name": "AI ä¸šåŠ¡æµç¨‹", "mermaid": "mermaid/11-ai-business-flow.mmd", "color": "#56c8d8",
     "summary": "统一模式:业务侧收集上下文(必要时调 MDM/SRM/CRM API å–名称)→组装 Prompt→chat/chatWithImage→解析 JSON→前端预填或建议;无 Function Calling;RAG çŸ¥è¯†åº“已建成但暂无业务消费。",
     "steps": [
@@ -578,7 +603,7 @@
    {"role": "质检员", "modules": ["MES-QC"], "operations": ["IQC/IPQC/OQC/RQC æ£€éªŒ", "NCR è¯„审处置"], "data": ["mes_qc_*"], "scope": "inspectorUserId å½’属"},
    {"role": "设备员/维修工", "modules": ["MES-DV"], "operations": ["点检保养", "维修工单", "工装领用"], "data": ["mes_dv_*", "mes_tm_*"], "scope": "chargeUserId å½’属"},
    {"role": "客服/售后工程师", "modules": ["售后"], "operations": ["工单受理派工", "ç»´ä¿®", "退货申请"], "data": ["after_sale_*"], "scope": "行级数据权限四视图:我负责/我参与/下属负责/全部(aftersales:view-all)"},
    {"role": "财务", "modules": ["ERP", "CRM"], "operations": ["收款单", "付款单", "开票审核", "结算账户"], "data": ["erp_finance_*", "crm_invoice", "crm_receivable"], "scope": "financeUserId å½’属"},
    {"role": "财务", "modules": ["ERP", "CRM", "FIN"], "operations": ["收款单", "付款单", "开票审核", "结算账户", "会计科目维护", "记账凭证录入与过账/作废", "总账与明细账查询", "固定资产折旧计提", "无形资产摊销计提", "会计核算看板"], "data": ["erp_finance_*", "crm_invoice", "crm_receivable", "fin_account_subject", "fin_voucher", "fin_voucher_entry", "fin_fixed_asset", "fin_intangible_asset"], "scope": "financeUserId å½’属;财务核算模块按 finance:* æŽ¥å£æƒé™ + system_menu æŽˆæƒï¼Œæš‚无数据权限字段"},
    {"role": "HR", "modules": ["HRM"], "operations": ["员工档案入转调离", "考勤规则排班", "薪酬核算发放", "离职交接"], "data": ["hrm_*"], "scope": "部门范围 dataScope"},
    {"role": "管理层", "modules": ["BI"], "operations": ["查看采购销售/仓库/生产设备/质量/人力五大主题大屏"], "data": ["bi_dashboard"], "scope": "只读"}
  ]
@@ -614,6 +639,14 @@
      ä»˜æ¬¾å•
      æ”¶æ¬¾å•
      ç»“算账户
    è´¢åŠ¡ç®¡ç† ä¼šè®¡æ ¸ç®— FIN
      ä¼šè®¡ç§‘目树
      è®°è´¦å‡­è¯ä¸Žåˆ†å½•
      å‡­è¯è¿‡è´¦ä¸Žä½œåºŸ
      æ€»è´¦ä¸Žæ˜Žç»†è´¦
      å›ºå®šèµ„产与折旧
      æ— å½¢èµ„产与摊销
      è´¢åŠ¡æŠ¥è¡¨çœ‹æ¿
    ç”Ÿäº§ç®¡ç† MES
      ä¸»ç”Ÿäº§è®¡åˆ’ MPS
      ç”Ÿäº§å·¥å•
@@ -910,6 +943,7 @@
  ERP -->|销售订单审批生成MPS| MES
  MES -->|入库出库回写单据进度| ERP
  MES -->|ProductionFinishedEvent| ERP
  ERP -->|ErpFinanceStatisticsApi.getMonthlyStatistics·只取已审核 status=20 çš„æ”¶æ¬¾ä»˜æ¬¾ä¸Žè´­é”€é‡‘额| FIN[财务核算 FIN ç§‘目·凭证·总账明细账·资产折旧摊销·报表看板]
  MES -->|退货质检入库回调| AS[售后工单维修退货]
  CRM -->|客户主数据| AS
  HRM[HRM å‘˜å·¥å…¥è½¬è°ƒç¦»äº¤æŽ¥è€ƒå‹¤è–ªé…¬] -.->|expandUserIds æ•°æ®å¯è§æ€§æ‰©å±•·接口已就绪待各模块接入| CRM
@@ -927,9 +961,11 @@
  ERP --> BI
  CRM --> BI
  HRM --> BI
  FIN --> BI
  SYS[系统底座 è®¤è¯Â·RBAC·字典·附件中心·定时任务] --- MDM
  SYS --- MES
  STG[附件中心 storage-blob+attachment å…¨æ¨¡å—统一挂附件] --- ERP
  FIN -.->|凭证附件 recordType=fin_voucher| STG
</pre>
<pre style="display:none" class="mmd-src" data-mmd="09-data-flow">
flowchart LR
@@ -958,6 +994,19 @@
    RC[收款单]
    PI[采购来票] --> PA[付款单]
  end
  subgraph S5 [财务核算 FIN ç‹¬ç«‹æ¨¡å—]
    FA[会计科目 fin_account_subject] --> FV[记账凭证 fin_voucher]
    FV --> FE[凭证分录 fin_voucher_entry]
    FE -->|借贷平衡校验·不平衡或合计为0则拦下| FVPOST{凭证过账}
    FVPOST -->|已过账10| GL[总账·按期间逐月汇总]
    FVPOST -->|已过账10| DL[明细账·逐条分录并结转余额]
    GL --> FDASH[会计核算看板]
    DL --> FDASH
    FIX[固定资产·1在用2闲置3维修中4已报废] -->|计提折旧·传ids按选中 ç©ºids全量| FDEP[累计折旧与净值]
    FINT[无形资产·1在用2已失效3已摊销完] -->|计提摊销·传ids按选中 ç©ºids全量| FAMR[累计摊销与净值]
    FDEP --> FDASH
    FAMR --> FDASH
  end
  CT -->|generateSaleOrder å¸¦å…¥å®¢æˆ·äº§å“æŠ˜æ‰£å®šé‡‘·回写orderId| SO
  SO -->|审批通过带入 saleOrderId è¡ŒID æ•°é‡ å®¢æˆ· äº¤æœŸ| MP
  PO -->|确认收货带入订单行物料供应商数量| AN
@@ -971,6 +1020,11 @@
  IV -->|回款强制先开票| RK
  RC -->|关联CRM客户校验| CU
  PA -->|关联来票与供应商| PI
  RC -->|已审核收款单金额 ErpFinanceStatisticsApi| FDASH
  PA -->|已审核付款单金额 ErpFinanceStatisticsApi| FDASH
  SO -->|已审核销售订单金额 ErpFinanceStatisticsApi| FDASH
  PO -->|已审核采购订单金额 ErpFinanceStatisticsApi| FDASH
  FV -.->|凭证附件 recordType=fin_voucher| PDFARCH
  subgraph S4 [智能质检报告 qcreport]
    RPT[报告模板] --> RPTV[模板版本]
    RPTVSAVE[设计器保存 create æˆ– update] -->|写入侧闸门 è§„则101_006 ç”»å¸ƒå®‰å…¨101_009 ä¸åˆæ ¼ä¸è½åº“| RPTV
@@ -1044,6 +1098,9 @@
  hrm_employee ||--o| hrm_user_handover : "离职交接映射"
  qc_report_template ||--o{ qc_report_template_version : "多版本"
  qc_report_template_version ||--o{ qc_report_instance : "冻结出件版本"
  fin_account_subject |o--o{ fin_voucher_entry : "分录按 subject_code å¯¹åº”ç§‘ç›®"
  fin_voucher ||--o{ fin_voucher_entry : "凭证含分录"
  %% fin_fixed_asset å›ºå®šèµ„产与 fin_intangible_asset æ— å½¢èµ„产为独立资产卡片,靠资产编号自管理,无外键关联,故不上 ER
</pre>
<pre style="display:none" class="mmd-src" data-mmd="11-ai-business-flow">
flowchart TD
@@ -1088,6 +1145,32 @@
  MODE[模型层:通义千问为主·预留DeepSeek/OpenAI/Ollama未实现] --- O2
  NOTE[全项目无Function Calling·AI只输出结构化JSON由业务侧解析] --- J2
</pre>
<pre style="display:none" class="mmd-src" data-mmd="12-finance-flow">
flowchart TD
  A[会计科目 fin_account_subject·一级科目种子约68条] -->|建下级科目 parent_id| B[科目树·list ç»„装 children]
  B -->|科目取值用 subjectCode è€Œéž id| C[记账凭证 fin_voucher·留空则自动生成字号]
  C --> D[凭证分录 fin_voucher_entry·逐行科目+借/è´·+辅助核算]
  D -->|前端校验·后端复核| E{借贷平衡校验}
  E -->|不平衡或合计为0| E1[拦下·提示出错行与借贷差额]
  E -->|平衡| F[保存·未过账0]
  F -->|可编辑 å¯è¿‡è´¦ å¯ä½œåºŸ å¯åˆ é™¤| F
  F -->|过账 post| G[已过账10·不可再修改]
  F -->|作废 cancel| H[已作废20]
  G -->|作废 cancel| H
  H -->|删除 delete| X[删除]
  G -->|只统计已过账凭证明细| I[总账 finance/ledger/general·按期间逐月汇总]
  G -->|只统计已过账凭证明细| J[明细账 finance/ledger/detail·逐条分录并结转余额]
  I --> K[财务报表 / ä¼šè®¡æ ¸ç®—看板]
  J --> K
  L[固定资产 fin_fixed_asset·状态 1在用 2闲置 3维修中 4已报废] -->|计提折旧 depreciate·空ids=全量| M[累计折旧递增·净值=原值-累计折旧]
  N[无形资产 fin_intangible_asset·状态 1在用 2已失效 3已摊销完] -->|计提摊销 amortize·空ids=全量| O[累计摊销递增·净值=原值-累计摊销]
  M --> K
  O --> K
  P[ERP å·²å®¡æ ¸å•据 status=20 æ”¶æ¬¾å• ä»˜æ¬¾å• é”€å”®è®¢å• é‡‡è´­è®¢å•] -->|ErpFinanceStatisticsApi.getMonthlyStatistics æŒ‰æœˆ| K
  K -.->|科目数 å‡­è¯æ•° æœªè¿‡è´¦ å·²è¿‡è´¦ å·²ä½œåºŸ èµ„产净值 æœˆåº¦è¶‹åŠ¿| Q[看板卡片+月度趋势图]
  V[附件中心 recordType=fin_voucher] --- C
  V -.->|不落 file_url åˆ—| C
</pre>
<script type="module">
const DATA = {};
@@ -1107,7 +1190,7 @@
const mmdSrc = {};
document.querySelectorAll('pre.mmd-src').forEach(p=>{ mmdSrc[p.dataset.mmd]=p.textContent.trim(); });
const flowColor = {sales:'var(--c-sale)',purchase:'var(--c-erp)',production:'var(--c-prod)',quality:'var(--c-qc)',warehouse:'var(--c-wm)',aftersales:'var(--c-as)',ai:'var(--c-ai)'};
const flowColor = {sales:'var(--c-sale)',purchase:'var(--c-erp)',production:'var(--c-prod)',quality:'var(--c-qc)',warehouse:'var(--c-wm)',aftersales:'var(--c-as)',finance:'var(--c-fin)',ai:'var(--c-ai)'};
async function boot(){
  DATA.business = await loadJson('business');
@@ -1144,8 +1227,8 @@
  </div>
  <h3 class="sec">业务模块</h3>
  <div class="grid cards">`;
  const targets = {crm:'flow-sales',erp:'flow-purchase',pro:'flow-production',wm:'flow-warehouse',qc:'flow-quality',aftersales:'flow-aftersales',ai:'ai',srm:'flow-purchase',mdm:'objects',dv:'objects',hrm:'objects',bpm:'relation'};
  const accs = {mdm:'var(--c-base)',crm:'var(--c-sale)',srm:'var(--c-sup)',erp:'var(--c-erp)',pro:'var(--c-prod)',wm:'var(--c-wm)',qc:'var(--c-qc)',dv:'var(--c-dv)',aftersales:'var(--c-as)',hrm:'var(--c-hrm)',bpm:'var(--c-bpm)',ai:'var(--c-ai)'};
  const targets = {crm:'flow-sales',erp:'flow-purchase',pro:'flow-production',wm:'flow-warehouse',qc:'flow-quality',aftersales:'flow-aftersales',ai:'ai',srm:'flow-purchase',mdm:'objects',dv:'objects',hrm:'objects',bpm:'relation',finance:'flow-finance'};
  const accs = {mdm:'var(--c-base)',crm:'var(--c-sale)',srm:'var(--c-sup)',erp:'var(--c-erp)',pro:'var(--c-prod)',wm:'var(--c-wm)',qc:'var(--c-qc)',dv:'var(--c-dv)',aftersales:'var(--c-as)',hrm:'var(--c-hrm)',bpm:'var(--c-bpm)',ai:'var(--c-ai)',finance:'var(--c-fin)'};
  DATA.modules.modules.forEach(m=>{
    html += `<div class="card" style="--acc:${accs[m.key]||'#2c5cff'}" data-goto="${targets[m.key]||'panorama'}">
      <h3>${esc(m.name)}<span class="tag">${esc(m.tag)}</span></h3>
docs/project-business/mermaid/01-business-mindmap.mmd
@@ -28,6 +28,14 @@
      ä»˜æ¬¾å•
      æ”¶æ¬¾å•
      ç»“算账户
    è´¢åŠ¡ç®¡ç† ä¼šè®¡æ ¸ç®— FIN
      ä¼šè®¡ç§‘目树
      è®°è´¦å‡­è¯ä¸Žåˆ†å½•
      å‡­è¯è¿‡è´¦ä¸Žä½œåºŸ
      æ€»è´¦ä¸Žæ˜Žç»†è´¦
      å›ºå®šèµ„产与折旧
      æ— å½¢èµ„产与摊销
      è´¢åŠ¡æŠ¥è¡¨çœ‹æ¿
    ç”Ÿäº§ç®¡ç† MES
      ä¸»ç”Ÿäº§è®¡åˆ’ MPS
      ç”Ÿäº§å·¥å•
docs/project-business/mermaid/08-module-relation.mmd
@@ -8,6 +8,7 @@
  ERP -->|销售订单审批生成MPS| MES
  MES -->|入库出库回写单据进度| ERP
  MES -->|ProductionFinishedEvent| ERP
  ERP -->|ErpFinanceStatisticsApi.getMonthlyStatistics·只取已审核 status=20 çš„æ”¶æ¬¾ä»˜æ¬¾ä¸Žè´­é”€é‡‘额| FIN[财务核算 FIN ç§‘目·凭证·总账明细账·资产折旧摊销·报表看板]
  MES -->|退货质检入库回调| AS[售后工单维修退货]
  CRM -->|客户主数据| AS
  HRM[HRM å‘˜å·¥å…¥è½¬è°ƒç¦»äº¤æŽ¥è€ƒå‹¤è–ªé…¬] -.->|expandUserIds æ•°æ®å¯è§æ€§æ‰©å±•·接口已就绪待各模块接入| CRM
@@ -25,6 +26,8 @@
  ERP --> BI
  CRM --> BI
  HRM --> BI
  FIN --> BI
  SYS[系统底座 è®¤è¯Â·RBAC·字典·附件中心·定时任务] --- MDM
  SYS --- MES
  STG[附件中心 storage-blob+attachment å…¨æ¨¡å—统一挂附件] --- ERP
  FIN -.->|凭证附件 recordType=fin_voucher| STG
docs/project-business/mermaid/09-data-flow.mmd
@@ -24,6 +24,19 @@
    RC[收款单]
    PI[采购来票] --> PA[付款单]
  end
  subgraph S5 [财务核算 FIN ç‹¬ç«‹æ¨¡å—]
    FA[会计科目 fin_account_subject] --> FV[记账凭证 fin_voucher]
    FV --> FE[凭证分录 fin_voucher_entry]
    FE -->|借贷平衡校验·不平衡或合计为0则拦下| FVPOST{凭证过账}
    FVPOST -->|已过账10| GL[总账·按期间逐月汇总]
    FVPOST -->|已过账10| DL[明细账·逐条分录并结转余额]
    GL --> FDASH[会计核算看板]
    DL --> FDASH
    FIX[固定资产·1在用2闲置3维修中4已报废] -->|计提折旧·传ids按选中 ç©ºids全量| FDEP[累计折旧与净值]
    FINT[无形资产·1在用2已失效3已摊销完] -->|计提摊销·传ids按选中 ç©ºids全量| FAMR[累计摊销与净值]
    FDEP --> FDASH
    FAMR --> FDASH
  end
  CT -->|generateSaleOrder å¸¦å…¥å®¢æˆ·äº§å“æŠ˜æ‰£å®šé‡‘·回写orderId| SO
  SO -->|审批通过带入 saleOrderId è¡ŒID æ•°é‡ å®¢æˆ· äº¤æœŸ| MP
  PO -->|确认收货带入订单行物料供应商数量| AN
@@ -37,6 +50,11 @@
  IV -->|回款强制先开票| RK
  RC -->|关联CRM客户校验| CU
  PA -->|关联来票与供应商| PI
  RC -->|已审核收款单金额 ErpFinanceStatisticsApi| FDASH
  PA -->|已审核付款单金额 ErpFinanceStatisticsApi| FDASH
  SO -->|已审核销售订单金额 ErpFinanceStatisticsApi| FDASH
  PO -->|已审核采购订单金额 ErpFinanceStatisticsApi| FDASH
  FV -.->|凭证附件 recordType=fin_voucher| PDFARCH
  subgraph S4 [智能质检报告 qcreport]
    RPT[报告模板] --> RPTV[模板版本]
    RPTVSAVE[设计器保存 create æˆ– update] -->|写入侧闸门 è§„则101_006 ç”»å¸ƒå®‰å…¨101_009 ä¸åˆæ ¼ä¸è½åº“| RPTV
docs/project-business/mermaid/10-business-object.mmd
@@ -36,3 +36,7 @@
  hrm_employee ||--o| hrm_user_handover : "离职交接映射"
  qc_report_template ||--o{ qc_report_template_version : "多版本"
  qc_report_template_version ||--o{ qc_report_instance : "冻结出件版本"
  fin_account_subject |o--o{ fin_voucher_entry : "分录按 subject_code å¯¹åº”ç§‘ç›®"
  fin_voucher ||--o{ fin_voucher_entry : "凭证含分录"
  %% fin_fixed_asset å›ºå®šèµ„产与 fin_intangible_asset æ— å½¢èµ„产为独立资产卡片,靠资产编号自管理,无外键关联,故不上 ER
pom.xml
@@ -25,6 +25,8 @@
        <module>yudao-module-crm-api</module>
        <module>yudao-module-erp</module>
        <module>yudao-module-erp-api</module>
        <!-- è´¢åŠ¡ç®¡ç†æ¨¡å—ï¼ˆä¼šè®¡æ ¸ç®—ï¼šç§‘ç›®ã€å‡­è¯ã€æ€»è´¦ã€èµ„äº§ã€æŠ¥è¡¨ï¼‰ -->
        <module>yudao-module-finance</module>
<!--        <module>yudao-module-iot</module>-->
        <module>yudao-module-mes</module>
        <module>yudao-module-mes-api</module>
sql/mysql/finance_menu_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
-- =============================================
-- è´¢åŠ¡æ ¸ç®—æ¨¡å—èœå•åˆå§‹åŒ–è„šæœ¬
-- =============================================
-- æ¨¡å—:yudao-module-finance
--      è®°è´¦å‡­è¯ / æ€»è´¦ / æ˜Žç»†è´¦ / ä¼šè®¡ç§‘ç›® / å›ºå®šèµ„产 / æ— å½¢èµ„产 / è´¢åŠ¡æŠ¥è¡¨
-- å‰ç«¯é¡µé¢ï¼šmom-pro2-before/src/views/erp/finance/**
-- åŽç«¯æƒé™ç ï¼šè§å„ Controller çš„ @PreAuthorize
--
-- è¯´æ˜Žï¼š
--   1. æ–°å»ºä¸€çº§èœå•「财务核算」(/finance-core),不触碰原有 ERP ä¸‹çš„「财务管理」(id=2645)
--      åŠå…¶ ç»“算账户/付款单/收款单 ä¸‰ä¸ªèœå•。
--   2. é¡µé¢èœå•(type=2) å„自携带 :query æƒé™ï¼›å¢žåˆ æ”¹/导出/折旧摊销等操作权限建成按钮(type=3)。
--   3. æ€»è´¦ä¸Žæ˜Žç»†è´¦å…±ç”¨ finance:ledger:query、finance:ledger:export ä¸¤ä¸ªæƒé™ç ï¼Œ
--      å› æ­¤ã€Œå¯¼å‡ºè´¦ç°¿ã€æŒ‰é’®åªæŒ‚在「总账」下 â€”— åŒä¸€æƒé™ç åœ¨è§’色授权树里出现一次即可,
--      æŽˆäºˆåŽã€Œæ˜Žç»†è´¦ã€çš„导出按钮同样生效。
--   4. å¹‚等:先删除本脚本占用的 id åŒºé—´ (1075500~1075549) å†æ’入,可重复执行。
--      æ³¨æ„ï¼šè‹¥ä¹‹åŽå·²åœ¨ã€Œè§’色管理」里给这些菜单授权,重跑本脚本会丢失对应 system_role_menu è®°å½•。
--   5. ä¸å« tenant_id(本项目非多租户)。
-- =============================================
DELETE FROM system_menu WHERE id BETWEEN 1075500 AND 1075549;
-- ---------------------------------------------------------------
-- ä¸€çº§èœå•
-- ---------------------------------------------------------------
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES
(1075500, '财务核算', '', 1, 4, 0, '/finance-core', 'ep:money', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- ---------------------------------------------------------------
-- é¡µé¢èœå• (type=2)
-- ---------------------------------------------------------------
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES
(1075501, '记账凭证', 'finance:voucher:query',           2, 1, 1075500, 'voucher',         'ep:document',        'erp/finance/voucher/index',           'FinVoucher',         0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075502, '总账',     'finance:ledger:query',            2, 2, 1075500, 'general-ledger',  'ep:notebook',        'erp/finance/ledger/general/index',    'FinGeneralLedger',   0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075503, '明细账',   'finance:ledger:query',            2, 3, 1075500, 'detail-ledger',   'ep:list',            'erp/finance/ledger/detail/index',     'FinDetailLedger',    0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075504, '会计科目', 'finance:account-subject:query',   2, 4, 1075500, 'subject',         'ep:collection-tag',  'erp/finance/subject/index',           'FinAccountSubject',  0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075505, '固定资产', 'finance:fixed-asset:query',       2, 5, 1075500, 'fixed-asset',     'ep:office-building', 'erp/finance/fixed-asset/index',       'FinFixedAsset',      0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075506, '无形资产', 'finance:intangible-asset:query',  2, 6, 1075500, 'intangible-asset','ep:medal',           'erp/finance/intangible-asset/index',  'FinIntangibleAsset', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075507, '财务报表', 'finance:statistics:query',        2, 7, 1075500, 'statistics',      'ep:data-line',       'erp/finance/statistics/index',        'FinStatistics',      0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0');
-- ---------------------------------------------------------------
-- æŒ‰é’®æƒé™ (type=3)
-- ---------------------------------------------------------------
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES
-- è®°è´¦å‡­è¯
(1075510, '凭证新增',     'finance:voucher:create',        3, 1, 1075501, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075511, '凭证修改',     'finance:voucher:update',        3, 2, 1075501, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075512, '凭证删除',     'finance:voucher:delete',        3, 3, 1075501, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075513, '凭证状态变更', 'finance:voucher:update-status', 3, 4, 1075501, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075514, '凭证导出',     'finance:voucher:export',        3, 5, 1075501, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
-- æ€»è´¦ / æ˜Žç»†è´¦ï¼ˆä¸¤é¡µå…±ç”¨ finance:ledger:query,导出权限只在此处挂一次,见文件头说明 3)
(1075520, '账簿导出',     'finance:ledger:export',         3, 1, 1075502, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
-- ä¼šè®¡ç§‘ç›®
(1075521, '科目新增',     'finance:account-subject:create',3, 1, 1075504, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075522, '科目修改',     'finance:account-subject:update',3, 2, 1075504, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075523, '科目删除',     'finance:account-subject:delete',3, 3, 1075504, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075524, '科目导出',     'finance:account-subject:export',3, 4, 1075504, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
-- å›ºå®šèµ„产
(1075530, '资产新增',     'finance:fixed-asset:create',    3, 1, 1075505, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075531, '资产修改',     'finance:fixed-asset:update',    3, 2, 1075505, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075532, '资产删除',     'finance:fixed-asset:delete',    3, 3, 1075505, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075533, '计提折旧',     'finance:fixed-asset:depreciate',3, 4, 1075505, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075534, '资产导出',     'finance:fixed-asset:export',    3, 5, 1075505, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
-- æ— å½¢èµ„产
(1075540, '资产新增',     'finance:intangible-asset:create',   3, 1, 1075506, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075541, '资产修改',     'finance:intangible-asset:update',   3, 2, 1075506, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075542, '资产删除',     'finance:intangible-asset:delete',   3, 3, 1075506, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075543, '无形资产摊销', 'finance:intangible-asset:amortize', 3, 4, 1075506, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0'),
(1075544, '资产导出',     'finance:intangible-asset:export',   3, 5, 1075506, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0');
-- ---------------------------------------------------------------
-- è§’色授权(可选)
-- è¶…级管理员 (super_admin) ç”±ä»£ç ç›´æŽ¥è¿”回全部菜单,无需授权。
-- è‹¥è¦æŠŠè´¢åŠ¡æ ¸ç®—æ”¾ç»™æŸä¸ªè§’è‰²ï¼Œåœ¨ã€Œç³»ç»Ÿç®¡ç† â†’ è§’色管理」里勾选即可;
-- ä¹Ÿå¯ç”¨ä¸‹é¢çš„语句直接授权(把 <roleId> æ¢æˆç›®æ ‡è§’色 id,参考:163 è®¾å¤‡ / 164 è´¨é‡ / 165 é”€å”®ï¼‰ï¼š
--
-- INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
-- SELECT 165, id, '1', NOW(), '1', NOW(), b'0' FROM system_menu WHERE id BETWEEN 1075500 AND 1075549;
-- ---------------------------------------------------------------
-- ---------------------------------------------------------------
-- å›žæ»šè„šæœ¬
-- DELETE FROM system_menu WHERE id BETWEEN 1075500 AND 1075549;
-- DELETE FROM system_role_menu WHERE menu_id BETWEEN 1075500 AND 1075549;
-- ---------------------------------------------------------------
sql/mysql/finance_module_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
-- =====================================================================================
-- è´¢åŠ¡ç®¡ç†æ¨¡å—ï¼ˆä¼šè®¡æ ¸ç®—ï¼‰åˆå§‹åŒ–è„šæœ¬
-- æ¨¡å—:yudao-module-finance   æŽ¥å£å‰ç¼€ï¼š/finance/**   è¡¨å‰ç¼€ï¼šfin_
--
-- å†…容:
--   1. 5 å¼ ä¸šåŠ¡è¡¨ï¼šä¼šè®¡ç§‘ç›®ã€è®°è´¦å‡­è¯ã€å‡­è¯åˆ†å½•ã€å›ºå®šèµ„äº§ã€æ— å½¢èµ„äº§
--   2. 6 ç»„字典(system_dict_type / system_dict_data)
--   3. ä¸€å¥—企业会计准则常用会计科目种子数据(仅在会计科目表为空时写入)
--
-- è¯´æ˜Žï¼š
--   * æœ¬è„šæœ¬ä¸å«èœå•/权限配置,菜单请自行在「系统管理 - èœå•管理」中配置,
--     æŽ¥å£æƒé™ä¸²è§ä¸‹æ–¹å„表的接口注释(finance:xxx:yyy)。
--   * é¡¹ç›®ä¸ºå•租户,表结构不含 tenant_id å­—段。
--   * è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼šå»ºè¡¨ç”¨ CREATE TABLE IF NOT EXISTS,字典/科目均带存在性判断。
--   * æŒ‰ã€Šæ•°æ®åº“初始化与配置导出维护规则》,本脚本内容需同步进 docs/sql/config_export_all_<日期>.sql
--     çš„阶段一(结构 + å­—典配置),否则新库初始化会缺表。
-- =====================================================================================
-- -------------------------------------------------------------------------------------
-- 1.1 ä¼šè®¡ç§‘ç›®
-- æŽ¥å£ï¼š/finance/account-subject  æƒé™ï¼šfinance:account-subject:{query|create|update|delete|export}
-- -------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `fin_account_subject`
(
    `id`                bigint       NOT NULL AUTO_INCREMENT COMMENT '科目编号',
    `parent_id`         bigint       NULL DEFAULT NULL COMMENT '上级科目编号,NULL è¡¨ç¤ºä¸€çº§ç§‘ç›®',
    `subject_code`      varchar(64)  NOT NULL COMMENT '科目编码',
    `subject_name`      varchar(128) NOT NULL COMMENT '科目名称',
    `subject_type`      tinyint      NOT NULL COMMENT '科目类型:1资产类 2负债类 3所有者权益类 4成本类 5损益类',
    `balance_direction` tinyint      NOT NULL COMMENT '余额方向:1借 2è´·',
    `status`            tinyint      NOT NULL DEFAULT 0 COMMENT '状态:0启用 1禁用',
    `remark`            varchar(500) NULL DEFAULT '' COMMENT '备注',
    `creator`           varchar(64)  NULL DEFAULT '' COMMENT '创建者',
    `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`           varchar(64)  NULL DEFAULT '' COMMENT '更新者',
    `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`           bit(1)       NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    KEY `idx_fin_subject_code` (`deleted`, `subject_code`),
    KEY `idx_fin_subject_parent` (`deleted`, `parent_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '会计科目';
-- -------------------------------------------------------------------------------------
-- 1.2 è®°è´¦å‡­è¯
-- æŽ¥å£ï¼š/finance/voucher  æƒé™ï¼šfinance:voucher:{query|create|update|delete|update-status|export}
-- -------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `fin_voucher`
(
    `id`                 bigint        NOT NULL AUTO_INCREMENT COMMENT '凭证编号',
    `voucher_no`         varchar(64)   NOT NULL COMMENT '凭证字号,例如:记2026090001',
    `voucher_date`       date          NOT NULL COMMENT '凭证日期',
    `summary`            varchar(500)  NULL DEFAULT '' COMMENT '摘要',
    `debit`              decimal(18,2) NOT NULL DEFAULT 0 COMMENT '借方合计',
    `credit`             decimal(18,2) NOT NULL DEFAULT 0 COMMENT '贷方合计',
    `status`             tinyint       NOT NULL DEFAULT 0 COMMENT '凭证状态:0未过账 10已过账 20已作废',
    `attachment_count`   int           NOT NULL DEFAULT 0 COMMENT '附件张数',
    `remark`             varchar(500)  NULL DEFAULT '' COMMENT '备注',
    `creator`            varchar(64)   NULL DEFAULT '' COMMENT '制单人',
    `create_time`        datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`            varchar(64)   NULL DEFAULT '' COMMENT '更新者',
    `update_time`        datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`            bit(1)        NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    KEY `idx_fin_voucher_no` (`deleted`, `voucher_no`),
    KEY `idx_fin_voucher_date` (`deleted`, `status`, `voucher_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '记账凭证';
-- -------------------------------------------------------------------------------------
-- 1.3 å‡­è¯åˆ†å½•
-- -------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `fin_voucher_entry`
(
    `id`             bigint        NOT NULL AUTO_INCREMENT COMMENT '分录编号',
    `voucher_id`     bigint        NOT NULL COMMENT '凭证编号,关联 fin_voucher.id',
    `row_no`         int           NOT NULL DEFAULT 1 COMMENT '行号,从 1 å¼€å§‹',
    `subject_code`   varchar(64)   NOT NULL COMMENT '科目编码,关联 fin_account_subject.subject_code',
    `subject_name`   varchar(128)  NULL DEFAULT '' COMMENT '科目名称(冗余)',
    `summary`        varchar(500)  NULL DEFAULT '' COMMENT '摘要',
    `debit`          decimal(18,2) NOT NULL DEFAULT 0 COMMENT '借方金额',
    `credit`         decimal(18,2) NOT NULL DEFAULT 0 COMMENT '贷方金额',
    `auxiliary_type` tinyint       NULL DEFAULT NULL COMMENT '辅助核算类型:1客户 2供应商 3部门 4员工 5项目',
    `auxiliary_id`   varchar(64)   NULL DEFAULT '' COMMENT '辅助核算对象编号',
    `auxiliary_name` varchar(128)  NULL DEFAULT '' COMMENT '辅助核算对象名称',
    `creator`        varchar(64)   NULL DEFAULT '' COMMENT '创建者',
    `create_time`    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`        varchar(64)   NULL DEFAULT '' COMMENT '更新者',
    `update_time`    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`        bit(1)        NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    KEY `idx_fin_entry_voucher` (`deleted`, `voucher_id`),
    KEY `idx_fin_entry_subject` (`deleted`, `subject_code`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '凭证分录';
-- -------------------------------------------------------------------------------------
-- 1.4 å›ºå®šèµ„产
-- æŽ¥å£ï¼š/finance/fixed-asset  æƒé™ï¼šfinance:fixed-asset:{query|create|update|delete|depreciate|export}
-- -------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `fin_fixed_asset`
(
    `id`                         bigint        NOT NULL AUTO_INCREMENT COMMENT '资产编号',
    `asset_code`                 varchar(64)   NOT NULL COMMENT '资产编号',
    `asset_name`                 varchar(128)  NOT NULL COMMENT '资产名称',
    `category`                   varchar(64)   NULL DEFAULT '' COMMENT '资产类别',
    `specification`              varchar(255)  NULL DEFAULT '' COMMENT '规格型号',
    `purchase_date`              date          NOT NULL COMMENT '购置日期',
    `original_value`             decimal(24,6) NOT NULL DEFAULT 0 COMMENT '资产原值',
    `useful_life`                int           NOT NULL DEFAULT 1 COMMENT '使用年限(年)',
    `residual_rate`              decimal(5,2)  NOT NULL DEFAULT 0 COMMENT '残值率(%)',
    `accumulated_depreciation`   decimal(24,6) NOT NULL DEFAULT 0 COMMENT '累计折旧',
    `net_value`                  decimal(24,6) NOT NULL DEFAULT 0 COMMENT '净值 = åŽŸå€¼ - ç´¯è®¡æŠ˜æ—§',
    `location`                   varchar(255)  NULL DEFAULT '' COMMENT '存放地点',
    `department`                 varchar(128)  NULL DEFAULT '' COMMENT '使用部门',
    `keeper`                     varchar(64)   NULL DEFAULT '' COMMENT '保管人',
    `status`                     tinyint       NOT NULL DEFAULT 1 COMMENT '状态:1在用 2闲置 3维修中 4已报废',
    `remark`                     varchar(500)  NULL DEFAULT '' COMMENT '备注',
    `creator`                    varchar(64)   NULL DEFAULT '' COMMENT '创建者',
    `create_time`                datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`                    varchar(64)   NULL DEFAULT '' COMMENT '更新者',
    `update_time`                datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`                    bit(1)        NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    KEY `idx_fin_fixed_asset_code` (`deleted`, `asset_code`),
    KEY `idx_fin_fixed_asset_status` (`deleted`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '固定资产';
-- -------------------------------------------------------------------------------------
-- 1.5 æ— å½¢èµ„产
-- æŽ¥å£ï¼š/finance/intangible-asset  æƒé™ï¼šfinance:intangible-asset:{query|create|update|delete|amortize|export}
-- -------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `fin_intangible_asset`
(
    `id`                          bigint        NOT NULL AUTO_INCREMENT COMMENT '资产编号',
    `asset_code`                  varchar(64)   NOT NULL COMMENT '资产编号',
    `asset_name`                  varchar(128)  NOT NULL COMMENT '资产名称',
    `category`                    varchar(64)   NULL DEFAULT '' COMMENT '资产类别',
    `certificate_no`              varchar(128)  NULL DEFAULT '' COMMENT '证书编号',
    `acquisition_date`            date          NOT NULL COMMENT '取得日期',
    `original_value`              decimal(24,6) NOT NULL DEFAULT 0 COMMENT '资产原值',
    `amortization_period`         int           NOT NULL DEFAULT 1 COMMENT '摊销年限(年)',
    `residual_rate`               decimal(5,2)  NOT NULL DEFAULT 0 COMMENT '残值率(%)',
    `accumulated_amortization`    decimal(24,6) NOT NULL DEFAULT 0 COMMENT '累计摊销',
    `net_value`                   decimal(24,6) NOT NULL DEFAULT 0 COMMENT '净值 = åŽŸå€¼ - ç´¯è®¡æ‘Šé”€',
    `validity_date`               date          NULL DEFAULT NULL COMMENT '有效期至',
    `status`                      tinyint       NOT NULL DEFAULT 1 COMMENT '状态:1在用 2已失效 3已摊销完',
    `description`                 varchar(500)  NULL DEFAULT '' COMMENT '资产描述',
    `remark`                      varchar(500)  NULL DEFAULT '' COMMENT '备注',
    `creator`                     varchar(64)   NULL DEFAULT '' COMMENT '创建者',
    `create_time`                 datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater`                     varchar(64)   NULL DEFAULT '' COMMENT '更新者',
    `update_time`                 datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted`                     bit(1)        NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    KEY `idx_fin_intangible_asset_code` (`deleted`, `asset_code`),
    KEY `idx_fin_intangible_asset_status` (`deleted`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '无形资产';
-- -------------------------------------------------------------------------------------
-- 2. å­—å…¸
-- -------------------------------------------------------------------------------------
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '会计科目类型', 'finance_subject_type', 0, '财务管理 - ç§‘目类型', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_type` WHERE `type` = 'finance_subject_type' AND `deleted` = b'0');
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '会计余额方向', 'finance_balance_direction', 0, '财务管理 - ç§‘目余额方向', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_type` WHERE `type` = 'finance_balance_direction' AND `deleted` = b'0');
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '辅助核算类型', 'finance_auxiliary_type', 0, '财务管理 - å‡­è¯åˆ†å½•辅助核算类型', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_type` WHERE `type` = 'finance_auxiliary_type' AND `deleted` = b'0');
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '凭证状态', 'finance_voucher_status', 0, '财务管理 - è®°è´¦å‡­è¯çŠ¶æ€', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_type` WHERE `type` = 'finance_voucher_status' AND `deleted` = b'0');
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '固定资产状态', 'finance_fixed_asset_status', 0, '财务管理 - å›ºå®šèµ„产状态', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_type` WHERE `type` = 'finance_fixed_asset_status' AND `deleted` = b'0');
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '无形资产状态', 'finance_intangible_asset_status', 0, '财务管理 - æ— å½¢èµ„产状态', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_type` WHERE `type` = 'finance_intangible_asset_status' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, '资产类', '1', 'finance_subject_type', 0, 'primary', '', '会计科目 - èµ„产类', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_subject_type' AND `value` = '1' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 2, '负债类', '2', 'finance_subject_type', 0, 'warning', '', '会计科目 - è´Ÿå€ºç±»', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_subject_type' AND `value` = '2' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 3, '所有者权益类', '3', 'finance_subject_type', 0, 'success', '', '会计科目 - æ‰€æœ‰è€…权益类', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_subject_type' AND `value` = '3' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 4, '成本类', '4', 'finance_subject_type', 0, 'info', '', '会计科目 - æˆæœ¬ç±»', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_subject_type' AND `value` = '4' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 5, '损益类', '5', 'finance_subject_type', 0, 'danger', '', '会计科目 - æŸç›Šç±»', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_subject_type' AND `value` = '5' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, '借', '1', 'finance_balance_direction', 0, 'primary', '', '余额方向 - å€Ÿ', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_balance_direction' AND `value` = '1' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 2, 'è´·', '2', 'finance_balance_direction', 0, 'warning', '', '余额方向 - è´·', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_balance_direction' AND `value` = '2' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, '客户', '1', 'finance_auxiliary_type', 0, 'primary', '', '辅助核算 - å®¢æˆ·', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_auxiliary_type' AND `value` = '1' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 2, '供应商', '2', 'finance_auxiliary_type', 0, 'warning', '', '辅助核算 - ä¾›åº”商', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_auxiliary_type' AND `value` = '2' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 3, '部门', '3', 'finance_auxiliary_type', 0, 'info', '', '辅助核算 - éƒ¨é—¨', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_auxiliary_type' AND `value` = '3' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 4, '员工', '4', 'finance_auxiliary_type', 0, 'success', '', '辅助核算 - å‘˜å·¥', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_auxiliary_type' AND `value` = '4' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 5, '项目', '5', 'finance_auxiliary_type', 0, 'default', '', '辅助核算 - é¡¹ç›®', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_auxiliary_type' AND `value` = '5' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, '未过账', '0', 'finance_voucher_status', 0, 'info', '', '凭证状态 - æœªè¿‡è´¦', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_voucher_status' AND `value` = '0' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 2, '已过账', '10', 'finance_voucher_status', 0, 'success', '', '凭证状态 - å·²è¿‡è´¦', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_voucher_status' AND `value` = '10' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 3, '已作废', '20', 'finance_voucher_status', 0, 'danger', '', '凭证状态 - å·²ä½œåºŸ', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_voucher_status' AND `value` = '20' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, '在用', '1', 'finance_fixed_asset_status', 0, 'success', '', '固定资产 - åœ¨ç”¨', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_fixed_asset_status' AND `value` = '1' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 2, '闲置', '2', 'finance_fixed_asset_status', 0, 'info', '', '固定资产 - é—²ç½®', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_fixed_asset_status' AND `value` = '2' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 3, '维修中', '3', 'finance_fixed_asset_status', 0, 'warning', '', '固定资产 - ç»´ä¿®ä¸­', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_fixed_asset_status' AND `value` = '3' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 4, '已报废', '4', 'finance_fixed_asset_status', 0, 'danger', '', '固定资产 - å·²æŠ¥åºŸ', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_fixed_asset_status' AND `value` = '4' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, '在用', '1', 'finance_intangible_asset_status', 0, 'success', '', '无形资产 - åœ¨ç”¨', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_intangible_asset_status' AND `value` = '1' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 2, '已失效', '2', 'finance_intangible_asset_status', 0, 'danger', '', '无形资产 - å·²å¤±æ•ˆ', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_intangible_asset_status' AND `value` = '2' AND `deleted` = b'0');
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 3, '已摊销完', '3', 'finance_intangible_asset_status', 0, 'info', '', '无形资产 - å·²æ‘Šé”€å®Œ', '1', NOW(), '1', NOW(), b'0'
WHERE NOT EXISTS (SELECT 1 FROM `system_dict_data` WHERE `dict_type` = 'finance_intangible_asset_status' AND `value` = '3' AND `deleted` = b'0');
-- -------------------------------------------------------------------------------------
-- 3. ä¼šè®¡ç§‘目种子数据(企业会计准则常用科目)
-- ä»…当 fin_account_subject ä¸ºç©ºæ—¶å†™å…¥ï¼Œé¿å…è¦†ç›–用户已维护的科目表
-- parent_id ä¸º NULL è¡¨ç¤ºä¸€çº§ç§‘目;科目编码遵循「4 ä½ä¸€çº§ + 2 ä½äºŒçº§ã€çš„层级规则
-- -------------------------------------------------------------------------------------
INSERT INTO `fin_account_subject` (`parent_id`, `subject_code`, `subject_name`, `subject_type`, `balance_direction`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT *
FROM (SELECT NULL AS parent_id, '1001' AS subject_code, '库存现金' AS subject_name, 1 AS subject_type, 1 AS balance_direction, 0 AS status, '' AS remark, '1' AS creator, NOW() AS create_time, '1' AS updater, NOW() AS update_time, b'0' AS deleted
      UNION ALL SELECT NULL, '1002', '银行存款', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1012', '其他货币资金', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1101', '交易性金融资产', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1121', '应收票据', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1122', '应收账款', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1123', '预付账款', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1131', '应收股利', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1132', '应收利息', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1221', '其他应收款', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1231', '坏账准备', 1, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1401', '材料采购', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1402', '在途物资', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1403', '原材料', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1405', '库存商品', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1406', '发出商品', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1408', '委托加工物资', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1411', '周转材料', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1471', '存货跌价准备', 1, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1501', '债权投资', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1601', '固定资产', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1602', '累计折旧', 1, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1603', '固定资产减值准备', 1, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1604', '在建工程', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1605', '工程物资', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1606', '固定资产清理', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1701', '无形资产', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1702', '累计摊销', 1, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1703', '无形资产减值准备', 1, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1801', '长期待摊费用', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1811', '递延所得税资产', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '1901', '待处理财产损溢', 1, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2001', '短期借款', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2201', '应付票据', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2202', '应付账款', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2203', '预收账款', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2211', '应付职工薪酬', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2221', '应交税费', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2231', '应付利息', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2232', '应付股利', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2241', '其他应付款', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2501', '长期借款', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2801', '预计负债', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '2901', '递延所得税负债', 2, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '4001', '实收资本', 3, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '4002', '资本公积', 3, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '4101', '盈余公积', 3, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '4103', '本年利润', 3, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '4104', '利润分配', 3, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '5001', '生产成本', 4, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '5101', '制造费用', 4, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '5201', '劳务成本', 4, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '5301', '研发支出', 4, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6001', '主营业务收入', 5, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6051', '其他业务收入', 5, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6101', '公允价值变动损益', 5, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6111', '投资收益', 5, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6301', '营业外收入', 5, 2, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6401', '主营业务成本', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6402', '其他业务成本', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6403', '税金及附加', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6601', '销售费用', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6602', '管理费用', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6603', '财务费用', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6701', '资产减值损失', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6711', '营业外支出', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6801', '所得税费用', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0'
      UNION ALL SELECT NULL, '6901', '以前年度损益调整', 5, 1, 0, '', '1', NOW(), '1', NOW(), b'0') AS seed
WHERE NOT EXISTS (SELECT 1 FROM `fin_account_subject` WHERE `deleted` = b'0');
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java
@@ -41,6 +41,14 @@
        if (columnWidth < 0) {
            return;
        }
        // å­—段上声明了 @ColumnWidth æ—¶ï¼Œä»¥å£°æ˜Žå€¼ä¸ºä¸‹é™ã€‚
        // å¦åˆ™æ³¨è§£å€¼ä¼šè¢«æœ¬ç­–略覆盖:EasyExcel è‡ªå·±çš„ AbstractHeadColumnWidthStyleStrategy(负责把
        // @ColumnWidth å†™è¿›åˆ—宽)与本策略 order ç›¸åŒï¼ˆéƒ½æ˜¯ 0,按注册顺序排在前面),
        // æœ¬ç­–略按内容长度后写,直接把声明的宽度冲掉 â€”— ä¾‹å¦‚「2026-10-13 10:10:20」只有 19 ä¸ªå­—符,
        // åˆ—宽就停在 19,声明再大也没用。取最大值后,注解成了内容长度的下限,只放宽不收窄。
        if (isHead && head != null && head.getColumnWidthProperty() != null) {
            columnWidth = Math.max(columnWidth, head.getColumnWidthProperty().getWidth());
        }
        if (columnWidth > MAX_COLUMN_WIDTH) {
            columnWidth = MAX_COLUMN_WIDTH;
        }
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinanceStatisticsApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.erp.api.finance;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinanceMonthlyStatisticsRespDTO;
import java.time.LocalDateTime;
import java.util.List;
/**
 * ERP è´¢åŠ¡ç»Ÿè®¡ API æŽ¥å£
 *
 * ä¾›è´¢åŠ¡æ¨¡å—ï¼ˆyudao-module-finance)的财务报表 / ä¼šè®¡æ ¸ç®—看板取数使用,
 * é¿å…è·¨æ¨¡å—直接查 erp çš„库表
 *
 * @author è¶…级管理员
 */
public interface ErpFinanceStatisticsApi {
    /**
     * æŒ‰æœˆç»Ÿè®¡ ERP çš„æ”¶æ¬¾ã€ä»˜æ¬¾ã€é”€å”®è®¢å•、采购订单金额
     *
     * ç»Ÿè®¡å£å¾„:只统计已审核(status = 20)的单据,已作废、审批中、草稿的单据不计入
     *
     * @param startTime å¼€å§‹æ—¶é—´ï¼ˆå«ï¼‰
     * @param endTime   ç»“束时间(不含)
     * @return æŒ‰æœˆç»Ÿè®¡ç»“果,按月份升序;没有数据的月份不会返回
     */
    CommonResult<List<ErpFinanceMonthlyStatisticsRespDTO>> getMonthlyStatistics(LocalDateTime startTime,
                                                                               LocalDateTime endTime);
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/dto/ErpFinanceMonthlyStatisticsRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.erp.api.finance.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - ERP è´¢åŠ¡æœˆåº¦ç»Ÿè®¡ Response DTO")
@Data
public class ErpFinanceMonthlyStatisticsRespDTO {
    @Schema(description = "月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10")
    private String month;
    @Schema(description = "收款金额(收款单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100000.00")
    private BigDecimal receiptPrice;
    @Schema(description = "付款金额(付款单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "60000.00")
    private BigDecimal paymentPrice;
    @Schema(description = "销售订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "150000.00")
    private BigDecimal saleOrderPrice;
    @Schema(description = "采购订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "80000.00")
    private BigDecimal purchaseOrderPrice;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinanceStatisticsApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.erp.api.finance;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinanceMonthlyStatisticsRespDTO;
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinanceStatisticsMapper;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * ERP è´¢åŠ¡ç»Ÿè®¡ API å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class ErpFinanceStatisticsApiImpl implements ErpFinanceStatisticsApi {
    @Resource
    private ErpFinanceStatisticsMapper financeStatisticsMapper;
    @Override
    public CommonResult<List<ErpFinanceMonthlyStatisticsRespDTO>> getMonthlyStatistics(LocalDateTime startTime,
                                                                                       LocalDateTime endTime) {
        return success(financeStatisticsMapper.selectMonthlyStatistics(ErpAuditStatus.APPROVE.getStatus(),
                startTime, endTime));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinanceStatisticsMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.erp.dal.mysql.finance;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinanceMonthlyStatisticsRespDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
/**
 * ERP è´¢åŠ¡ç»Ÿè®¡ Mapper
 *
 * ä¸ºè´¢åŠ¡æ¨¡å—çš„æŠ¥è¡¨ã€çœ‹æ¿æä¾›æŒ‰æœˆèšåˆçš„æ”¶æ¬¾ / ä»˜æ¬¾ / é”€å”®è®¢å• / é‡‡è´­è®¢å•金额
 *
 * @author è¶…级管理员
 */
@Mapper
public interface ErpFinanceStatisticsMapper {
    /**
     * æŒ‰æœˆç»Ÿè®¡æ”¶æ¬¾ã€ä»˜æ¬¾ã€é”€å”®è®¢å•、采购订单金额
     *
     * @param approveStatus å·²å®¡æ ¸çŠ¶æ€å€¼
     * @param startTime     å¼€å§‹æ—¶é—´ï¼ˆå«ï¼‰
     * @param endTime       ç»“束时间(不含)
     * @return æœˆåº¦ç»Ÿè®¡åˆ—表,按月份升序
     */
    List<ErpFinanceMonthlyStatisticsRespDTO> selectMonthlyStatistics(@Param("approveStatus") Integer approveStatus,
                                                                     @Param("startTime") LocalDateTime startTime,
                                                                     @Param("endTime") LocalDateTime endTime);
}
yudao-module-erp/src/main/resources/mapper/finance/ErpFinanceStatisticsMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
<?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="cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinanceStatisticsMapper">
    <select id="selectMonthlyStatistics"
            resultType="cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinanceMonthlyStatisticsRespDTO">
        SELECT t.month                                                    AS month,
               IFNULL(SUM(t.receiptPrice), 0)                             AS receiptPrice,
               IFNULL(SUM(t.paymentPrice), 0)                             AS paymentPrice,
               IFNULL(SUM(t.saleOrderPrice), 0)                           AS saleOrderPrice,
               IFNULL(SUM(t.purchaseOrderPrice), 0)                       AS purchaseOrderPrice
        FROM (
            SELECT DATE_FORMAT(receipt_time, '%Y-%m')  AS month,
                   IFNULL(SUM(receipt_price), 0)       AS receiptPrice,
                   0                                   AS paymentPrice,
                   0                                   AS saleOrderPrice,
                   0                                   AS purchaseOrderPrice
            FROM erp_finance_receipt
            WHERE deleted = 0
              AND status = #{approveStatus}
              AND receipt_time &gt;= #{startTime}
              AND receipt_time &lt; #{endTime}
            GROUP BY DATE_FORMAT(receipt_time, '%Y-%m')
            UNION ALL
            SELECT DATE_FORMAT(payment_time, '%Y-%m'),
                   IFNULL(SUM(payment_price), 0),
                   0,
                   0,
                   0
            FROM erp_finance_payment
            WHERE deleted = 0
              AND status = #{approveStatus}
              AND payment_time &gt;= #{startTime}
              AND payment_time &lt; #{endTime}
            GROUP BY DATE_FORMAT(payment_time, '%Y-%m')
            UNION ALL
            SELECT DATE_FORMAT(order_time, '%Y-%m'),
                   IFNULL(SUM(total_price), 0),
                   0,
                   0,
                   0
            FROM erp_sale_order
            WHERE deleted = 0
              AND status = #{approveStatus}
              AND order_time &gt;= #{startTime}
              AND order_time &lt; #{endTime}
            GROUP BY DATE_FORMAT(order_time, '%Y-%m')
            UNION ALL
            SELECT DATE_FORMAT(order_time, '%Y-%m'),
                   IFNULL(SUM(total_price), 0),
                   0,
                   0,
                   0
            FROM erp_purchase_order
            WHERE deleted = 0
              AND status = #{approveStatus}
              AND order_time &gt;= #{startTime}
              AND order_time &lt; #{endTime}
            GROUP BY DATE_FORMAT(order_time, '%Y-%m')
        ) t
        GROUP BY t.month
        ORDER BY t.month
    </select>
</mapper>
yudao-module-finance/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-finance</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        finance åŒ…下,财务管理(Financial Management)。
        ä¾‹å¦‚说:会计科目、凭证、总账、明细账、固定资产、无形资产、财务报表等等
    </description>
    <dependencies>
        <!-- System æ¨¡å—(文件上传走 system æ¨¡å—çš„ Storage é™„件机制) -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ERP æ¨¡å— API(财务报表需要读取收付款单、采购销售订单数据) -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-erp-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- Web ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-security</artifactId>
        </dependency>
        <!-- DB ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-redis</artifactId>
        </dependency>
        <!-- å·¥å…·ç±»ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-excel</artifactId>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/FinFixedAssetController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetDepreciateRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinFixedAssetDO;
import cn.iocoder.yudao.module.finance.service.asset.FinAssetAmountUtils;
import cn.iocoder.yudao.module.finance.service.asset.FinFixedAssetService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - å›ºå®šèµ„产")
@RestController
@RequestMapping("/finance/fixed-asset")
@Validated
public class FinFixedAssetController {
    @Resource
    private FinFixedAssetService fixedAssetService;
    @Resource
    private DeptApi deptApi;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建固定资产")
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:create')")
    public CommonResult<Long> createFixedAsset(@Valid @RequestBody FinFixedAssetSaveReqVO createReqVO) {
        return success(fixedAssetService.createFixedAsset(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新固定资产")
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:update')")
    public CommonResult<Boolean> updateFixedAsset(@Valid @RequestBody FinFixedAssetSaveReqVO updateReqVO) {
        fixedAssetService.updateFixedAsset(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除固定资产")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:delete')")
    public CommonResult<Boolean> deleteFixedAsset(@RequestParam("id") Long id) {
        fixedAssetService.deleteFixedAsset(id);
        return success(true);
    }
    @DeleteMapping("/delete-list")
    @Operation(summary = "批量删除固定资产")
    @Parameter(name = "ids", description = "编号列表", required = true)
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:delete')")
    public CommonResult<Boolean> deleteFixedAssetList(@RequestParam("ids") List<Long> ids) {
        fixedAssetService.deleteFixedAssetList(ids);
        return success(true);
    }
    @PostMapping("/depreciate")
    @Operation(summary = "计提折旧", description = "按资产逐个计提一个月的折旧;ids ä¸ºç©ºæ—¶å¯¹æ‰€æœ‰æœªæŠ¥åºŸèµ„产计提")
    @Parameter(name = "ids", description = "资产编号列表,为空表示全部", required = false)
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:depreciate')")
    public CommonResult<FinFixedAssetDepreciateRespVO> depreciateFixedAssetList(
            @RequestParam(value = "ids", required = false) List<Long> ids) {
        return success(fixedAssetService.depreciateFixedAssetList(ids));
    }
    @GetMapping("/get")
    @Operation(summary = "获得固定资产")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:query')")
    public CommonResult<FinFixedAssetRespVO> getFixedAsset(@RequestParam("id") Long id) {
        FinFixedAssetDO asset = fixedAssetService.getFixedAsset(id);
        if (asset == null) {
            return success(null);
        }
        FinFixedAssetRespVO respVO = toRespVO(asset);
        fillDepartmentAndKeeper(List.of(respVO));
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获得固定资产分页")
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:query')")
    public CommonResult<PageResult<FinFixedAssetRespVO>> getFixedAssetPage(@Valid FinFixedAssetPageReqVO pageReqVO) {
        PageResult<FinFixedAssetDO> pageResult = fixedAssetService.getFixedAssetPage(pageReqVO);
        PageResult<FinFixedAssetRespVO> result = BeanUtils.toBean(pageResult, FinFixedAssetRespVO.class,
                this::fillMonthlyDepreciation);
        fillDepartmentAndKeeper(result.getList());
        return success(result);
    }
    @GetMapping("/list")
    @Operation(summary = "获得固定资产列表")
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:query')")
    public CommonResult<List<FinFixedAssetRespVO>> getFixedAssetList() {
        List<FinFixedAssetDO> list = fixedAssetService.getFixedAssetList();
        List<FinFixedAssetRespVO> voList = convertList(list, this::toRespVO);
        fillDepartmentAndKeeper(voList);
        return success(voList);
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出固定资产 Excel")
    @PreAuthorize("@ss.hasPermission('finance:fixed-asset:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportFixedAssetExcel(@Valid FinFixedAssetPageReqVO pageReqVO,
                                      HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<FinFixedAssetDO> list = fixedAssetService.getFixedAssetPage(pageReqVO).getList();
        List<FinFixedAssetRespVO> voList = BeanUtils.toBean(list, FinFixedAssetRespVO.class,
                this::fillMonthlyDepreciation);
        fillDepartmentAndKeeper(voList);
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "固定资产.xls", "数据", FinFixedAssetRespVO.class, voList);
    }
    private FinFixedAssetRespVO toRespVO(FinFixedAssetDO asset) {
        return fillMonthlyDepreciation(BeanUtils.toBean(asset, FinFixedAssetRespVO.class));
    }
    /**
     * å›žå¡«æœˆæŠ˜æ—§é¢ï¼ˆä¸è½åº“,仅用于展示,口径与计提折旧一致)
     */
    private FinFixedAssetRespVO fillMonthlyDepreciation(FinFixedAssetRespVO respVO) {
        if (respVO.getUsefulLife() == null || respVO.getUsefulLife() <= 0
                || respVO.getOriginalValue() == null) {
            return respVO;
        }
        return respVO.setMonthlyDepreciation(FinAssetAmountUtils.calcMonthlyAmount(
                respVO.getOriginalValue(), respVO.getResidualRate(), respVO.getUsefulLife()));
    }
    /**
     * æ‰¹é‡å›žå¡«ã€Œä½¿ç”¨éƒ¨é—¨åç§°ã€ä¸Žã€Œä¿ç®¡äººæ˜µç§°ã€ã€‚
     *
     * DO ä¸Šå­˜çš„æ˜¯ departmentId / keeperId,而列表与导出要展示名称,
     * æ‰€ä»¥æŒ‰ ERP æ”¶æ¬¾å•的既有口径,在这里把部门、用户各批量查一次再回填,避免逐行查库。
     * è§£æžä¸åˆ°çš„(部门/用户被删除、编号为空)就保持为空,不抛异常。
     */
    private void fillDepartmentAndKeeper(List<FinFixedAssetRespVO> list) {
        if (CollUtil.isEmpty(list)) {
            return;
        }
        Set<Long> deptIds = convertSet(list, FinFixedAssetRespVO::getDepartmentId,
                respVO -> respVO.getDepartmentId() != null);
        Set<Long> keeperIds = convertSet(list, FinFixedAssetRespVO::getKeeperId,
                respVO -> respVO.getKeeperId() != null);
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(deptIds);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(keeperIds);
        list.forEach(respVO -> {
            MapUtils.findAndThen(deptMap, respVO.getDepartmentId(),
                    dept -> respVO.setDepartmentName(dept.getName()));
            MapUtils.findAndThen(userMap, respVO.getKeeperId(),
                    user -> respVO.setKeeperName(user.getNickname()));
        });
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/FinIntangibleAssetController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetAmortizeRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinIntangibleAssetDO;
import cn.iocoder.yudao.module.finance.service.asset.FinAssetAmountUtils;
import cn.iocoder.yudao.module.finance.service.asset.FinIntangibleAssetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - æ— å½¢èµ„产")
@RestController
@RequestMapping("/finance/intangible-asset")
@Validated
public class FinIntangibleAssetController {
    @Resource
    private FinIntangibleAssetService intangibleAssetService;
    @PostMapping("/create")
    @Operation(summary = "创建无形资产")
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:create')")
    public CommonResult<Long> createIntangibleAsset(@Valid @RequestBody FinIntangibleAssetSaveReqVO createReqVO) {
        return success(intangibleAssetService.createIntangibleAsset(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新无形资产")
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:update')")
    public CommonResult<Boolean> updateIntangibleAsset(@Valid @RequestBody FinIntangibleAssetSaveReqVO updateReqVO) {
        intangibleAssetService.updateIntangibleAsset(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除无形资产")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:delete')")
    public CommonResult<Boolean> deleteIntangibleAsset(@RequestParam("id") Long id) {
        intangibleAssetService.deleteIntangibleAsset(id);
        return success(true);
    }
    @DeleteMapping("/delete-list")
    @Operation(summary = "批量删除无形资产")
    @Parameter(name = "ids", description = "编号列表", required = true)
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:delete')")
    public CommonResult<Boolean> deleteIntangibleAssetList(@RequestParam("ids") List<Long> ids) {
        intangibleAssetService.deleteIntangibleAssetList(ids);
        return success(true);
    }
    @PostMapping("/amortize")
    @Operation(summary = "计提摊销", description = "按资产逐个计提一个月的摊销;ids ä¸ºç©ºæ—¶å¯¹æ‰€æœ‰æœªå¤±æ•ˆèµ„产计提")
    @Parameter(name = "ids", description = "资产编号列表,为空表示全部", required = false)
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:amortize')")
    public CommonResult<FinIntangibleAssetAmortizeRespVO> amortizeIntangibleAssetList(
            @RequestParam(value = "ids", required = false) List<Long> ids) {
        return success(intangibleAssetService.amortizeIntangibleAssetList(ids));
    }
    @GetMapping("/get")
    @Operation(summary = "获得无形资产")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:query')")
    public CommonResult<FinIntangibleAssetRespVO> getIntangibleAsset(@RequestParam("id") Long id) {
        FinIntangibleAssetDO asset = intangibleAssetService.getIntangibleAsset(id);
        return success(asset == null ? null : toRespVO(asset));
    }
    @GetMapping("/page")
    @Operation(summary = "获得无形资产分页")
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:query')")
    public CommonResult<PageResult<FinIntangibleAssetRespVO>> getIntangibleAssetPage(
            @Valid FinIntangibleAssetPageReqVO pageReqVO) {
        PageResult<FinIntangibleAssetDO> pageResult = intangibleAssetService.getIntangibleAssetPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, FinIntangibleAssetRespVO.class, this::fillMonthlyAmortization));
    }
    @GetMapping("/list")
    @Operation(summary = "获得无形资产列表")
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:query')")
    public CommonResult<List<FinIntangibleAssetRespVO>> getIntangibleAssetList() {
        List<FinIntangibleAssetDO> list = intangibleAssetService.getIntangibleAssetList();
        return success(convertList(list, this::toRespVO));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出无形资产 Excel")
    @PreAuthorize("@ss.hasPermission('finance:intangible-asset:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportIntangibleAssetExcel(@Valid FinIntangibleAssetPageReqVO pageReqVO,
                                           HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<FinIntangibleAssetDO> list = intangibleAssetService.getIntangibleAssetPage(pageReqVO).getList();
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "无形资产.xls", "数据", FinIntangibleAssetRespVO.class,
                BeanUtils.toBean(list, FinIntangibleAssetRespVO.class, this::fillMonthlyAmortization));
    }
    private FinIntangibleAssetRespVO toRespVO(FinIntangibleAssetDO asset) {
        return fillMonthlyAmortization(BeanUtils.toBean(asset, FinIntangibleAssetRespVO.class));
    }
    /**
     * å›žå¡«æœˆæ‘Šé”€é¢ï¼ˆä¸è½åº“,仅用于展示,口径与计提摊销一致)
     */
    private FinIntangibleAssetRespVO fillMonthlyAmortization(FinIntangibleAssetRespVO respVO) {
        if (respVO.getAmortizationPeriod() == null || respVO.getAmortizationPeriod() <= 0
                || respVO.getOriginalValue() == null) {
            return respVO;
        }
        return respVO.setMonthlyAmortization(FinAssetAmountUtils.calcMonthlyAmount(
                respVO.getOriginalValue(), respVO.getResidualRate(), respVO.getAmortizationPeriod()));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetDepreciateRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - å›ºå®šèµ„产计提折旧 Response VO")
@Data
public class FinFixedAssetDepreciateRespVO {
    @Schema(description = "本次计提折旧的资产数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
    private Integer processedCount;
    @Schema(description = "本次计提的月折旧额合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "2850.00")
    private BigDecimal totalMonthlyDepreciation;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - å›ºå®šèµ„产分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class FinFixedAssetPageReqVO extends PageParam {
    @Schema(description = "资产编号", example = "GD202610120001")
    private String assetCode;
    @Schema(description = "资产名称", example = "数控车床")
    private String assetName;
    @Schema(description = "资产类别", example = "生产设备")
    private String category;
    @Schema(description = "使用部门编号", example = "100")
    private Long departmentId;
    @Schema(description = "保管人编号(用户编号)", example = "1")
    private Long keeperId;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "购置日期区间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate[] purchaseDate;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.finance.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å›ºå®šèµ„产 Response VO")
@Data
@ExcelIgnoreUnannotated
public class FinFixedAssetRespVO {
    @Schema(description = "资产编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "资产编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "GD202610120001")
    @ExcelProperty("资产编号")
    private String assetCode;
    @Schema(description = "资产名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数控车床")
    @ExcelProperty("资产名称")
    private String assetName;
    @Schema(description = "资产类别", example = "生产设备")
    @ExcelProperty("资产类别")
    private String category;
    @Schema(description = "规格型号", example = "CK6140")
    @ExcelProperty("规格型号")
    private String specification;
    @Schema(description = "购置日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("购置日期")
    private LocalDate purchaseDate;
    @Schema(description = "资产原值", requiredMode = Schema.RequiredMode.REQUIRED, example = "120000.00")
    @ExcelProperty("资产原值")
    private BigDecimal originalValue;
    @Schema(description = "使用年限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @ExcelProperty("使用年限(å¹´)")
    private Integer usefulLife;
    @Schema(description = "残值率(%)", example = "5.00")
    @ExcelProperty("残值率(%)")
    private BigDecimal residualRate;
    @Schema(description = "月折旧额", example = "950.00")
    @ExcelProperty("月折旧额")
    private BigDecimal monthlyDepreciation;
    @Schema(description = "累计折旧", requiredMode = Schema.RequiredMode.REQUIRED, example = "11400.00")
    @ExcelProperty("累计折旧")
    private BigDecimal accumulatedDepreciation;
    @Schema(description = "净值", requiredMode = Schema.RequiredMode.REQUIRED, example = "108600.00")
    @ExcelProperty("净值")
    private BigDecimal netValue;
    @Schema(description = "存放地点", example = "一号车间")
    @ExcelProperty("存放地点")
    private String location;
    @Schema(description = "使用部门编号", example = "100")
    private Long departmentId;
    /**
     * ä½¿ç”¨éƒ¨é—¨åç§°ï¼Œåˆ—表与 Excel å¯¼å‡ºç”¨ã€‚
     * departmentId å­˜çš„æ˜¯éƒ¨é—¨ç¼–号,直接展示就是一串数字,所以另用这个字段承载名称;
     * ä¸Žå‡­è¯çš„制单人昵称 creatorName åŒä¸€å¥—口径,由 Controller æ‰¹é‡è§£æžåŽå›žå¡«ã€‚
     */
    @Schema(description = "使用部门名称", example = "生产部")
    @ExcelProperty("使用部门")
    private String departmentName;
    @Schema(description = "保管人编号(用户编号)", example = "1")
    private Long keeperId;
    @Schema(description = "保管人昵称", example = "张三")
    @ExcelProperty("保管人")
    private String keeperName;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.FIXED_ASSET_STATUS)
    private Integer status;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    // ã€Œyyyy-MM-dd HH:mm:ss」正好 19 ä¸ªå­—符,自适应列宽就只给 19,贴着边很难看,这里放宽到 22
    @ColumnWidth(22)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinFixedAssetSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.finance.enums.FinFixedAssetStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - å›ºå®šèµ„产新增/修改 Request VO")
@Data
public class FinFixedAssetSaveReqVO {
    @Schema(description = "资产编号", example = "1024")
    private Long id;
    @Schema(description = "资产编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "GD202610120001")
    @NotEmpty(message = "资产编号不能为空")
    @Size(max = 64, message = "资产编号长度不能超过 64 ä¸ªå­—符")
    private String assetCode;
    @Schema(description = "资产名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数控车床")
    @NotEmpty(message = "资产名称不能为空")
    @Size(max = 128, message = "资产名称长度不能超过 128 ä¸ªå­—符")
    private String assetName;
    @Schema(description = "资产类别", example = "生产设备")
    @Size(max = 64, message = "资产类别长度不能超过 64 ä¸ªå­—符")
    private String category;
    @Schema(description = "规格型号", example = "CK6140")
    @Size(max = 255, message = "规格型号长度不能超过 255 ä¸ªå­—符")
    private String specification;
    @Schema(description = "购置日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "购置日期不能为空")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate purchaseDate;
    @Schema(description = "资产原值", requiredMode = Schema.RequiredMode.REQUIRED, example = "120000.00")
    @NotNull(message = "资产原值不能为空")
    @DecimalMin(value = "0", message = "资产原值不能小于 0")
    private BigDecimal originalValue;
    @Schema(description = "使用年限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "使用年限不能为空")
    @Min(value = 1, message = "使用年限必须大于等于 1 å¹´")
    private Integer usefulLife;
    @Schema(description = "残值率(%)", example = "5.00")
    @DecimalMin(value = "0", message = "残值率不能小于 0")
    @DecimalMax(value = "100", message = "残值率不能大于 100")
    private BigDecimal residualRate;
    @Schema(description = "存放地点", example = "一号车间")
    @Size(max = 255, message = "存放地点长度不能超过 255 ä¸ªå­—符")
    private String location;
    @Schema(description = "使用部门编号", example = "100")
    private Long departmentId;
    @Schema(description = "保管人编号(用户编号)", example = "1")
    private Long keeperId;
    @Schema(description = "状态", example = "1")
    @InEnum(value = FinFixedAssetStatusEnum.class, message = "状态必须是 {value}")
    private Integer status;
    @Schema(description = "备注", example = "备注")
    @Size(max = 500, message = "备注长度不能超过 500 ä¸ªå­—符")
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetAmortizeRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - æ— å½¢èµ„产计提摊销 Response VO")
@Data
public class FinIntangibleAssetAmortizeRespVO {
    @Schema(description = "本次计提摊销的资产数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
    private Integer processedCount;
    @Schema(description = "本次计提的月摊销额合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "3000.00")
    private BigDecimal totalMonthlyAmortization;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - æ— å½¢èµ„产分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class FinIntangibleAssetPageReqVO extends PageParam {
    @Schema(description = "资产编号", example = "WX202610120001")
    private String assetCode;
    @Schema(description = "资产名称", example = "ERP è½¯ä»¶è‘—作权")
    private String assetName;
    @Schema(description = "资产类别", example = "软件")
    private String category;
    @Schema(description = "证书编号", example = "软著登字第 1234567 å·")
    private String certificateNo;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "取得日期区间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate[] acquisitionDate;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.finance.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - æ— å½¢èµ„产 Response VO")
@Data
@ExcelIgnoreUnannotated
public class FinIntangibleAssetRespVO {
    @Schema(description = "资产编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "资产编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "WX202610120001")
    @ExcelProperty("资产编号")
    private String assetCode;
    @Schema(description = "资产名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERP è½¯ä»¶è‘—作权")
    @ExcelProperty("资产名称")
    private String assetName;
    @Schema(description = "资产类别", example = "软件")
    @ExcelProperty("资产类别")
    private String category;
    @Schema(description = "证书编号", example = "软著登字第 1234567 å·")
    @ExcelProperty("证书编号")
    private String certificateNo;
    @Schema(description = "取得日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("取得日期")
    private LocalDate acquisitionDate;
    @Schema(description = "资产原值", requiredMode = Schema.RequiredMode.REQUIRED, example = "60000.00")
    @ExcelProperty("资产原值")
    private BigDecimal originalValue;
    @Schema(description = "摊销年限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
    @ExcelProperty("摊销年限(å¹´)")
    private Integer amortizationPeriod;
    @Schema(description = "残值率(%)", example = "0.00")
    @ExcelProperty("残值率(%)")
    private BigDecimal residualRate;
    @Schema(description = "月摊销额", example = "1000.00")
    @ExcelProperty("月摊销额")
    private BigDecimal monthlyAmortization;
    @Schema(description = "累计摊销", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000.00")
    @ExcelProperty("累计摊销")
    private BigDecimal accumulatedAmortization;
    @Schema(description = "净值", requiredMode = Schema.RequiredMode.REQUIRED, example = "40000.00")
    @ExcelProperty("净值")
    private BigDecimal netValue;
    @Schema(description = "有效期至")
    @ExcelProperty("有效期至")
    private LocalDate validityDate;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.INTANGIBLE_ASSET_STATUS)
    private Integer status;
    @Schema(description = "资产描述", example = "描述")
    @ExcelProperty("资产描述")
    private String description;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    // ã€Œyyyy-MM-dd HH:mm:ss」正好 19 ä¸ªå­—符,自适应列宽就只给 19,贴着边很难看,这里放宽到 22
    @ColumnWidth(22)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/asset/vo/FinIntangibleAssetSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.finance.controller.admin.asset.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.finance.enums.FinIntangibleAssetStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - æ— å½¢èµ„产新增/修改 Request VO")
@Data
public class FinIntangibleAssetSaveReqVO {
    @Schema(description = "资产编号", example = "1024")
    private Long id;
    @Schema(description = "资产编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "WX202610120001")
    @NotEmpty(message = "资产编号不能为空")
    @Size(max = 64, message = "资产编号长度不能超过 64 ä¸ªå­—符")
    private String assetCode;
    @Schema(description = "资产名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERP è½¯ä»¶è‘—作权")
    @NotEmpty(message = "资产名称不能为空")
    @Size(max = 128, message = "资产名称长度不能超过 128 ä¸ªå­—符")
    private String assetName;
    @Schema(description = "资产类别", example = "软件")
    @Size(max = 64, message = "资产类别长度不能超过 64 ä¸ªå­—符")
    private String category;
    @Schema(description = "证书编号", example = "软著登字第 1234567 å·")
    @Size(max = 128, message = "证书编号长度不能超过 128 ä¸ªå­—符")
    private String certificateNo;
    @Schema(description = "取得日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "取得日期不能为空")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate acquisitionDate;
    @Schema(description = "资产原值", requiredMode = Schema.RequiredMode.REQUIRED, example = "60000.00")
    @NotNull(message = "资产原值不能为空")
    @DecimalMin(value = "0", message = "资产原值不能小于 0")
    private BigDecimal originalValue;
    @Schema(description = "摊销年限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
    @NotNull(message = "摊销年限不能为空")
    @Min(value = 1, message = "摊销年限必须大于等于 1 å¹´")
    private Integer amortizationPeriod;
    @Schema(description = "残值率(%)", example = "0.00")
    @DecimalMin(value = "0", message = "残值率不能小于 0")
    @DecimalMax(value = "100", message = "残值率不能大于 100")
    private BigDecimal residualRate;
    @Schema(description = "有效期至")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate validityDate;
    @Schema(description = "状态", example = "1")
    @InEnum(value = FinIntangibleAssetStatusEnum.class, message = "状态必须是 {value}")
    private Integer status;
    @Schema(description = "资产描述", example = "描述")
    @Size(max = 500, message = "资产描述长度不能超过 500 ä¸ªå­—符")
    private String description;
    @Schema(description = "备注", example = "备注")
    @Size(max = 500, message = "备注长度不能超过 500 ä¸ªå­—符")
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/ledger/FinLedgerController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.finance.controller.admin.ledger;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerRespVO;
import cn.iocoder.yudao.module.finance.service.ledger.FinLedgerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - æ€»è´¦ / æ˜Žç»†è´¦")
@RestController
@RequestMapping("/finance/ledger")
@Validated
public class FinLedgerController {
    @Resource
    private FinLedgerService ledgerService;
    @GetMapping("/general")
    @Operation(summary = "获得总账", description = "按期间逐月汇总,只统计已过账凭证")
    @PreAuthorize("@ss.hasPermission('finance:ledger:query')")
    public CommonResult<List<FinLedgerRespVO>> getGeneralLedgerList(@Valid FinLedgerReqVO reqVO) {
        return success(ledgerService.getGeneralLedgerList(reqVO));
    }
    @GetMapping("/detail")
    @Operation(summary = "获得明细账", description = "逐条分录展示并结转余额,只统计已过账凭证")
    @PreAuthorize("@ss.hasPermission('finance:ledger:query')")
    public CommonResult<List<FinLedgerRespVO>> getDetailLedgerList(@Valid FinLedgerReqVO reqVO) {
        return success(ledgerService.getDetailLedgerList(reqVO));
    }
    @GetMapping("/general/export-excel")
    @Operation(summary = "导出总账 Excel")
    @PreAuthorize("@ss.hasPermission('finance:ledger:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportGeneralLedgerExcel(@Valid FinLedgerReqVO reqVO,
                                         HttpServletResponse response) throws IOException {
        List<FinLedgerRespVO> list = ledgerService.getGeneralLedgerList(reqVO);
        ExcelUtils.write(response, "总账.xls", "数据", FinLedgerRespVO.class, list);
    }
    @GetMapping("/detail/export-excel")
    @Operation(summary = "导出明细账 Excel")
    @PreAuthorize("@ss.hasPermission('finance:ledger:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportDetailLedgerExcel(@Valid FinLedgerReqVO reqVO,
                                        HttpServletResponse response) throws IOException {
        List<FinLedgerRespVO> list = ledgerService.getDetailLedgerList(reqVO);
        ExcelUtils.write(response, "明细账.xls", "数据", FinLedgerRespVO.class, list);
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/ledger/vo/FinLedgerReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.finance.controller.admin.ledger.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.YearMonth;
@Schema(description = "管理后台 - æ€»è´¦/明细账查询 Request VO")
@Data
public class FinLedgerReqVO {
    @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
    @NotEmpty(message = "查询总账/明细账必须指定会计科目")
    private String subjectCode;
    // æ³¨æ„ï¼šè¿™é‡Œå¿…须是 YearMonth,不能是 LocalDate。
    // å‰ç«¯æœˆä»½é€‰æ‹©å™¨ä¼ çš„æ˜¯ "2026-01",LocalDate æ— æ³•从「年+月」还原出「日」,
    // ä¼šåœ¨å‚数绑定阶段就抛 "Failed to convert ... to java.time.LocalDate"(400),
    // å³ä½¿å†™äº† @DateTimeFormat(pattern = "yyyy-MM") ä¹Ÿæ•‘不了 â€”— æ˜¯å­—段类型错了,不是格式错了。
    @Schema(description = "开始月份,格式 yyyy-MM,取该月 1 å·ä¸ºè´¦æœŸèµ·ç‚¹", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-01")
    @NotNull(message = "开始月份不能为空")
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth startMonth;
    @Schema(description = "结束月份,格式 yyyy-MM,取该月最后一天为账期终点", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-12")
    @NotNull(message = "结束月份不能为空")
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth endMonth;
    @Schema(description = "是否包含下级科目", example = "true")
    private Boolean includeChildren;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/ledger/vo/FinLedgerRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.finance.controller.admin.ledger.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.finance.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - æ€»è´¦/明细账行 Response VO")
@Data
@ExcelIgnoreUnannotated
public class FinLedgerRespVO {
    @Schema(description = "行类型:opening æœŸåˆä½™é¢ã€entry æ˜Žç»†ã€monthly_total æœ¬æœˆåˆè®¡ã€yearly_total æœ¬å¹´ç´¯è®¡",
            requiredMode = Schema.RequiredMode.REQUIRED, example = "entry")
    private String rowType;
    /**
     * è¡Œç±»åž‹çš„中文名,专供 Excel å¯¼å‡ºã€‚
     * rowType å­˜çš„æ˜¯æžšä¸¾ç ï¼ˆopening/entry/monthly_total/yearly_total),EasyExcel ä¼šåŽŸæ ·å†™è¿›å•å…ƒæ ¼ï¼Œ
     * æ‰€ä»¥å¯¼å‡ºçš„「行类型」列必须由另一个字段承载中文;rowType æœ¬èº«ä»ä¿ç•™åœ¨ JSON é‡Œï¼Œå‰ç«¯é å®ƒåšæ ‡ç­¾æ˜ å°„。
     */
    @Schema(description = "行类型名称", example = "明细")
    @ExcelProperty("行类型")
    private String rowTypeName;
    @Schema(description = "所属期间,格式 yyyy-MM", example = "2026-10")
    @ExcelProperty("期间")
    private String period;
    @Schema(description = "日期", example = "2026-10-12")
    @ExcelProperty("日期")
    private LocalDate date;
    @Schema(description = "凭证字号", example = "è®°2026100001")
    @ExcelProperty("凭证字号")
    private String voucherNo;
    @Schema(description = "科目编码", example = "1001")
    @ExcelProperty("科目编码")
    private String subjectCode;
    @Schema(description = "科目名称", example = "库存现金")
    @ExcelProperty("科目名称")
    private String subjectName;
    @Schema(description = "摘要", example = "收到货款")
    @ExcelProperty("摘要")
    private String summary;
    @Schema(description = "借方金额", example = "1000.00")
    @ExcelProperty("借方金额")
    private BigDecimal debit;
    @Schema(description = "贷方金额", example = "0.00")
    @ExcelProperty("贷方金额")
    private BigDecimal credit;
    @Schema(description = "余额方向", example = "1")
    @ExcelProperty(value = "余额方向", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.BALANCE_DIRECTION)
    private Integer direction;
    @Schema(description = "余额,借方为正、贷方为负", example = "1000.00")
    @ExcelProperty("余额")
    private BigDecimal balance;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/FinStatisticsController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.finance.controller.admin.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsOverviewRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsTrendRespVO;
import cn.iocoder.yudao.module.finance.service.statistics.FinStatisticsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - è´¢åŠ¡æŠ¥è¡¨ / ä¼šè®¡æ ¸ç®—看板")
@RestController
@RequestMapping("/finance/statistics")
@Validated
public class FinStatisticsController {
    @Resource
    private FinStatisticsService statisticsService;
    @GetMapping("/overview")
    @Operation(summary = "获得会计核算看板总览",
            description = "收支、购销金额取自 ERP å·²å®¡æ ¸çš„æ”¶æ¬¾å•/付款单/销售订单/采购订单,资产与凭证取自本模块")
    @PreAuthorize("@ss.hasPermission('finance:statistics:query')")
    public CommonResult<FinStatisticsOverviewRespVO> getStatisticsOverview(@Valid FinStatisticsReqVO reqVO) {
        return success(statisticsService.getStatisticsOverview(reqVO));
    }
    @GetMapping("/trend")
    @Operation(summary = "获得月度收支趋势", description = "期间内没有数据的月份补 0,便于前端直接绘图")
    @PreAuthorize("@ss.hasPermission('finance:statistics:query')")
    public CommonResult<List<FinStatisticsTrendRespVO>> getStatisticsTrend(@Valid FinStatisticsReqVO reqVO) {
        return success(statisticsService.getStatisticsTrend(reqVO));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/vo/FinStatisticsOverviewRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.finance.controller.admin.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - ä¼šè®¡æ ¸ç®—看板总览 Response VO")
@Data
public class FinStatisticsOverviewRespVO {
    @Schema(description = "开始日期", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDate startDate;
    @Schema(description = "结束日期", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDate endDate;
    // ========== æ”¶æ”¯ï¼ˆæ¥æºï¼šERP æ”¶æ¬¾å• / ä»˜æ¬¾å•,只统计已审核单据) ==========
    @Schema(description = "总营收(收款单收款金额合计)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100000.00")
    private BigDecimal receiptPrice;
    @Schema(description = "总支出(付款单付款金额合计)", requiredMode = Schema.RequiredMode.REQUIRED, example = "60000.00")
    private BigDecimal paymentPrice;
    @Schema(description = "净利润(总营收 - æ€»æ”¯å‡ºï¼‰", requiredMode = Schema.RequiredMode.REQUIRED, example = "40000.00")
    private BigDecimal netProfit;
    // ========== è´­é”€ï¼ˆæ¥æºï¼šERP é”€å”®è®¢å• / é‡‡è´­è®¢å•,只统计已审核单据) ==========
    @Schema(description = "销售订单金额合计(应收口径)", requiredMode = Schema.RequiredMode.REQUIRED, example = "150000.00")
    private BigDecimal saleOrderPrice;
    @Schema(description = "采购订单金额合计(应付口径)", requiredMode = Schema.RequiredMode.REQUIRED, example = "80000.00")
    private BigDecimal purchaseOrderPrice;
    // ========== èµ„产(时点数,取当前全部资产) ==========
    @Schema(description = "固定资产原值合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "1200000.00")
    private BigDecimal fixedAssetOriginalValue;
    @Schema(description = "固定资产累计折旧合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "240000.00")
    private BigDecimal fixedAssetAccumulatedDepreciation;
    @Schema(description = "固定资产净值合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "960000.00")
    private BigDecimal fixedAssetNetValue;
    @Schema(description = "无形资产原值合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "300000.00")
    private BigDecimal intangibleAssetOriginalValue;
    @Schema(description = "无形资产累计摊销合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "60000.00")
    private BigDecimal intangibleAssetAccumulatedAmortization;
    @Schema(description = "无形资产净值合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "240000.00")
    private BigDecimal intangibleAssetNetValue;
    @Schema(description = "资产净值合计(固定资产 + æ— å½¢èµ„产)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1200000.00")
    private BigDecimal totalAssetNetValue;
    // ========== ä¼šè®¡æ ¸ç®—(时点数) ==========
    @Schema(description = "会计科目数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "120")
    private Long subjectCount;
    @Schema(description = "凭证总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "300")
    private Long voucherCount;
    @Schema(description = "未过账凭证数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Long voucherUnpostedCount;
    @Schema(description = "已过账凭证数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "280")
    private Long voucherPostedCount;
    @Schema(description = "已作废凭证数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Long voucherCancelledCount;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/vo/FinStatisticsReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.finance.controller.admin.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.YearMonth;
@Schema(description = "管理后台 - è´¢åŠ¡æŠ¥è¡¨ / ä¼šè®¡æ ¸ç®—看板查询 Request VO")
@Data
public class FinStatisticsReqVO {
    // åŒ FinLedgerReqVO:月份参数必须是 YearMonth,LocalDate æ— æ³•接收 "2026-01"。
    @Schema(description = "开始月份,格式 yyyy-MM,为空时取本年 1 æœˆ", example = "2026-01")
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth startMonth;
    @Schema(description = "结束月份,格式 yyyy-MM,为空时取当前月", example = "2026-12")
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth endMonth;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/statistics/vo/FinStatisticsTrendRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.finance.controller.admin.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - è´¢åŠ¡ä¼šè®¡æ ¸ç®—çœ‹æ¿æœˆåº¦è¶‹åŠ¿ Response VO")
@Data
public class FinStatisticsTrendRespVO {
    @Schema(description = "月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10")
    private String month;
    @Schema(description = "收款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100000.00")
    private BigDecimal receiptPrice;
    @Schema(description = "付款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "60000.00")
    private BigDecimal paymentPrice;
    @Schema(description = "净利润(收款 - ä»˜æ¬¾ï¼‰", requiredMode = Schema.RequiredMode.REQUIRED, example = "40000.00")
    private BigDecimal netProfit;
    @Schema(description = "销售订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "150000.00")
    private BigDecimal saleOrderPrice;
    @Schema(description = "采购订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "80000.00")
    private BigDecimal purchaseOrderPrice;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/FinAccountSubjectController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,148 @@
package cn.iocoder.yudao.module.finance.controller.admin.subject;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import cn.iocoder.yudao.module.finance.service.subject.FinAccountSubjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@Tag(name = "管理后台 - è´¢åŠ¡ä¼šè®¡ç§‘ç›®")
@RestController
@RequestMapping("/finance/account-subject")
@Validated
public class FinAccountSubjectController {
    @Resource
    private FinAccountSubjectService accountSubjectService;
    @PostMapping("/create")
    @Operation(summary = "创建会计科目")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:create')")
    public CommonResult<Long> createAccountSubject(@Valid @RequestBody FinAccountSubjectSaveReqVO createReqVO) {
        return success(accountSubjectService.createAccountSubject(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新会计科目")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:update')")
    public CommonResult<Boolean> updateAccountSubject(@Valid @RequestBody FinAccountSubjectSaveReqVO updateReqVO) {
        accountSubjectService.updateAccountSubject(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除会计科目", description = "存在下级科目或被凭证分录引用时不允许删除")
    @Parameter(name = "ids", description = "科目编号数组", required = true, example = "1,2,3")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:delete')")
    public CommonResult<Boolean> deleteAccountSubject(@RequestParam("ids") List<Long> ids) {
        accountSubjectService.deleteAccountSubjectList(ids);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得会计科目")
    @Parameter(name = "id", description = "科目编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:query')")
    public CommonResult<FinAccountSubjectRespVO> getAccountSubject(@RequestParam("id") Long id) {
        FinAccountSubjectDO subject = accountSubjectService.getAccountSubject(id);
        return success(BeanUtils.toBean(subject, FinAccountSubjectRespVO.class));
    }
    /**
     * ä¼šè®¡ç§‘目树形列表页的取数方式:一次拉全量命中的科目,再在前端内存里组树,分页对树没有意义,
     * æ‰€ä»¥å‰ç«¯ä¼  pageSize = -1(即框架的 PageParam.PAGE_SIZE_NONE,语义是「查询全部」)。
     *
     * ä½† PageParam ä¸ŠæŒ‚着 @Min(1) @Max(200),请求里的 -1 ä¼šè¢«å‚数校验直接拦成
     * 400「每页条数最小值为 1」。PAGE_SIZE_NONE åœ¨æ¡†æž¶é‡Œä¸€ç›´æ˜¯æœåŠ¡ç«¯å¯¼å‡ºæ–¹æ³•å†…éƒ¨ set çš„
     * ï¼ˆè§ä¸‹æ–¹ export-excel),从没走 HTTP å‚数校验,所以这里必须去掉 @Valid,
     * æ”¹ä¸ºåœ¨æ–¹æ³•内把 pageSize < 1 æ˜¾å¼å½’一为 PAGE_SIZE_NONE,让「查询全部」这条路走得通。
     */
    @GetMapping("/page")
    @Operation(summary = "获得会计科目分页", description = "pageSize ä¼  -1 è¡¨ç¤ºæŸ¥è¯¢å…¨éƒ¨ï¼ˆæ ‘形列表页组树用)")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:query')")
    public CommonResult<PageResult<FinAccountSubjectRespVO>> getAccountSubjectPage(FinAccountSubjectPageReqVO pageReqVO) {
        if (pageReqVO.getPageSize() == null || pageReqVO.getPageSize() < 1) {
            pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        }
        PageResult<FinAccountSubjectDO> pageResult = accountSubjectService.getAccountSubjectPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, FinAccountSubjectRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获得会计科目树列表", description = "返回全量科目,按父子层级组装 children")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:query')")
    public CommonResult<List<FinAccountSubjectRespVO>> getAccountSubjectList() {
        return success(buildTree(accountSubjectService.getAccountSubjectList()));
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得会计科目精简列表", description = "用于凭证分录的科目下拉/树选择")
    public CommonResult<List<FinAccountSubjectRespVO>> getAccountSubjectSimpleList() {
        List<FinAccountSubjectDO> list = accountSubjectService.getAccountSubjectList();
        return success(convertList(list, subject -> new FinAccountSubjectRespVO()
                .setId(subject.getId()).setParentId(subject.getParentId())
                .setSubjectCode(subject.getSubjectCode()).setSubjectName(subject.getSubjectName())
                .setSubjectType(subject.getSubjectType()).setBalanceDirection(subject.getBalanceDirection())
                .setStatus(subject.getStatus())));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出会计科目 Excel")
    @PreAuthorize("@ss.hasPermission('finance:account-subject:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportAccountSubjectExcel(@Valid FinAccountSubjectPageReqVO pageReqVO,
                                          HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<FinAccountSubjectDO> list = accountSubjectService.getAccountSubjectPage(pageReqVO).getList();
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "会计科目.xls", "数据", FinAccountSubjectRespVO.class,
                BeanUtils.toBean(list, FinAccountSubjectRespVO.class));
    }
    /**
     * å°†æ‰å¹³ç§‘目列表组装为树:上级科目不存在(已被删除)时,按根节点处理,避免节点丢失
     */
    private List<FinAccountSubjectRespVO> buildTree(List<FinAccountSubjectDO> list) {
        List<FinAccountSubjectRespVO> voList = BeanUtils.toBean(list, FinAccountSubjectRespVO.class);
        Map<Long, FinAccountSubjectRespVO> idMap = convertMap(voList, FinAccountSubjectRespVO::getId);
        List<FinAccountSubjectRespVO> roots = new ArrayList<>();
        voList.forEach(vo -> {
            FinAccountSubjectRespVO parent = vo.getParentId() == null ? null : idMap.get(vo.getParentId());
            if (parent == null) {
                roots.add(vo);
            } else {
                if (parent.getChildren() == null) {
                    parent.setChildren(new ArrayList<>());
                }
                parent.getChildren().add(vo);
            }
        });
        return roots;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/vo/FinAccountSubjectPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.finance.controller.admin.subject.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - ä¼šè®¡ç§‘目分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class FinAccountSubjectPageReqVO extends PageParam {
    @Schema(description = "科目编码", example = "1001")
    private String subjectCode;
    @Schema(description = "科目名称", example = "库存现金")
    private String subjectName;
    @Schema(description = "科目类型", example = "1")
    private Integer subjectType;
    @Schema(description = "状态", example = "0")
    private Integer status;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/vo/FinAccountSubjectRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.finance.controller.admin.subject.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.finance.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - ä¼šè®¡ç§‘ç›® Response VO")
@Data
@ExcelIgnoreUnannotated
public class FinAccountSubjectRespVO {
    @Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @ExcelProperty("科目编号")
    private Long id;
    @Schema(description = "父科目编号", example = "1024")
    private Long parentId;
    @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
    @ExcelProperty("科目编码")
    private String subjectCode;
    @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金")
    @ExcelProperty("科目名称")
    private String subjectName;
    @Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "科目类型", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.SUBJECT_TYPE)
    private Integer subjectType;
    @Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "余额方向", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.BALANCE_DIRECTION)
    private Integer balanceDirection;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(cn.iocoder.yudao.module.system.enums.DictTypeConstants.COMMON_STATUS)
    private Integer status;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    // ã€Œyyyy-MM-dd HH:mm:ss」正好 19 ä¸ªå­—符,自适应列宽就只给 19,贴着边很难看,这里放宽到 22
    @ColumnWidth(22)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    @Schema(description = "子科目", example = "[]")
    private List<FinAccountSubjectRespVO> children;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/subject/vo/FinAccountSubjectSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.finance.controller.admin.subject.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.finance.enums.FinBalanceDirectionEnum;
import cn.iocoder.yudao.module.finance.enums.FinSubjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - ä¼šè®¡ç§‘目新增/修改 Request VO")
@Data
public class FinAccountSubjectSaveReqVO {
    @Schema(description = "科目编号", example = "1024")
    private Long id;
    @Schema(description = "父科目编号", example = "1024")
    private Long parentId;
    @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
    @NotEmpty(message = "科目编码不能为空")
    @Size(max = 64, message = "科目编码长度不能超过 64 ä¸ªå­—符")
    private String subjectCode;
    @Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金")
    @NotEmpty(message = "科目名称不能为空")
    @Size(max = 128, message = "科目名称长度不能超过 128 ä¸ªå­—符")
    private String subjectName;
    @Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "科目类型不能为空")
    @InEnum(value = FinSubjectTypeEnum.class, message = "科目类型必须是 {value}")
    private Integer subjectType;
    @Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "余额方向不能为空")
    @InEnum(value = FinBalanceDirectionEnum.class, message = "余额方向必须是 {value}")
    private Integer balanceDirection;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
    private Integer status;
    @Schema(description = "备注", example = "备注")
    @Size(max = 500, message = "备注长度不能超过 500 ä¸ªå­—符")
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/FinVoucherController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,204 @@
package cn.iocoder.yudao.module.finance.controller.admin.voucher;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherEntryRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherEntryDO;
import cn.iocoder.yudao.module.finance.service.voucher.FinVoucherService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
@Tag(name = "管理后台 - è®°è´¦å‡­è¯")
@RestController
@RequestMapping("/finance/voucher")
@Validated
public class FinVoucherController {
    /**
     * é™„件的业务记录类型
     */
    private static final String ATTACHMENT_RECORD_TYPE = "fin_voucher";
    @Resource
    private FinVoucherService voucherService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建记账凭证")
    @PreAuthorize("@ss.hasPermission('finance:voucher:create')")
    public CommonResult<Long> createVoucher(@Valid @RequestBody FinVoucherSaveReqVO createReqVO) {
        return success(voucherService.createVoucher(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新记账凭证", description = "仅未过账的凭证允许修改")
    @PreAuthorize("@ss.hasPermission('finance:voucher:update')")
    public CommonResult<Boolean> updateVoucher(@Valid @RequestBody FinVoucherSaveReqVO updateReqVO) {
        voucherService.updateVoucher(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除记账凭证")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('finance:voucher:delete')")
    public CommonResult<Boolean> deleteVoucher(@RequestParam("id") Long id) {
        voucherService.deleteVoucher(id);
        return success(true);
    }
    @DeleteMapping("/delete-list")
    @Operation(summary = "批量删除记账凭证")
    @Parameter(name = "ids", description = "编号列表", required = true)
    @PreAuthorize("@ss.hasPermission('finance:voucher:delete')")
    public CommonResult<Boolean> deleteVoucherList(@RequestParam("ids") List<Long> ids) {
        voucherService.deleteVoucherList(ids);
        return success(true);
    }
    @PutMapping("/post")
    @Operation(summary = "凭证过账", description = "未过账 -> å·²è¿‡è´¦ï¼Œè¿‡è´¦åŽå‡­è¯ä¸å¯å†ä¿®æ”¹")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('finance:voucher:update-status')")
    public CommonResult<Boolean> postVoucher(@RequestParam("id") Long id) {
        voucherService.postVoucher(id);
        return success(true);
    }
    @PutMapping("/cancel")
    @Operation(summary = "凭证作废", description = "未过账 -> å·²ä½œåºŸ")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('finance:voucher:update-status')")
    public CommonResult<Boolean> cancelVoucher(@RequestParam("id") Long id) {
        voucherService.cancelVoucher(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得记账凭证", description = "包含凭证分录与附件列表")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('finance:voucher:query')")
    public CommonResult<FinVoucherRespVO> getVoucher(@RequestParam("id") Long id) {
        FinVoucherDO voucher = voucherService.getVoucher(id);
        if (voucher == null) {
            return success(null);
        }
        FinVoucherRespVO respVO = BeanUtils.toBean(voucher, FinVoucherRespVO.class);
        respVO.setEntries(BeanUtils.toBean(voucherService.getVoucherEntryListByVoucherId(id), FinVoucherEntryRespVO.class));
        // æ³¨æ„ï¼šStorageAttachmentApi.listAttachments åœ¨ã€Œè¯¥è®°å½•没有任何附件」时返回 null,而不是空集合。
        // ç›´æŽ¥ .isEmpty() ä¼š NPE:新增的凭证还没传附件时,一查详情就 500,必须先兜底成空列表。
        List<StorageBlobRespDTO> attachments = storageAttachmentApi.listAttachments(ATTACHMENT_RECORD_TYPE, id);
        if (attachments == null) {
            attachments = List.of();
        }
        respVO.setAttachmentList(attachments);
        respVO.setHasAttachment(!attachments.isEmpty());
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获得记账凭证分页")
    @PreAuthorize("@ss.hasPermission('finance:voucher:query')")
    public CommonResult<PageResult<FinVoucherRespVO>> getVoucherPage(@Valid FinVoucherPageReqVO pageReqVO) {
        PageResult<FinVoucherDO> pageResult = voucherService.getVoucherPage(pageReqVO);
        PageResult<FinVoucherRespVO> result = BeanUtils.toBean(pageResult, FinVoucherRespVO.class);
        fillCreatorName(result.getList());
        return success(result);
    }
    @GetMapping("/page-with-entry")
    @Operation(summary = "获得记账凭证分页(含分录)", description = "一次性返回本页所有凭证的分录,避免前端逐条查询")
    @PreAuthorize("@ss.hasPermission('finance:voucher:query')")
    public CommonResult<PageResult<FinVoucherRespVO>> getVoucherPageWithEntry(@Valid FinVoucherPageReqVO pageReqVO) {
        PageResult<FinVoucherDO> pageResult = voucherService.getVoucherPage(pageReqVO);
        PageResult<FinVoucherRespVO> result = BeanUtils.toBean(pageResult, FinVoucherRespVO.class);
        if (result.getList().isEmpty()) {
            return success(result);
        }
        fillCreatorName(result.getList());
        // æ‰¹é‡æŸ¥è¯¢æœ¬é¡µå‡­è¯çš„分录
        Map<Long, List<FinVoucherEntryDO>> entryMap = convertMultiMap(
                voucherService.getVoucherEntryListByVoucherIds(
                        convertList(result.getList(), FinVoucherRespVO::getId)),
                FinVoucherEntryDO::getVoucherId);
        result.getList().forEach(voucher -> {
            voucher.setEntries(BeanUtils.toBean(
                    entryMap.getOrDefault(voucher.getId(), List.of()), FinVoucherEntryRespVO.class));
            // åˆ—表页也需要展示附件标记;附件表按 recordId å•条查询,本页最多 20 æ¡ï¼Œæˆæœ¬å¯æŽ¥å—
            // åŒä¸Šï¼šæ²¡æœ‰é™„ä»¶æ—¶ listAttachments è¿”回 null,必须兜底,否则列表里只要有一条没附件的凭证就整页 500
            List<StorageBlobRespDTO> attachments = storageAttachmentApi.listAttachments(ATTACHMENT_RECORD_TYPE, voucher.getId());
            if (attachments == null) {
                attachments = List.of();
            }
            voucher.setAttachmentList(attachments);
            voucher.setHasAttachment(!attachments.isEmpty());
        });
        return success(result);
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出记账凭证 Excel")
    @PreAuthorize("@ss.hasPermission('finance:voucher:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportVoucherExcel(@Valid FinVoucherPageReqVO pageReqVO,
                                   HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<FinVoucherDO> list = voucherService.getVoucherPage(pageReqVO).getList();
        List<FinVoucherRespVO> voList = BeanUtils.toBean(list, FinVoucherRespVO.class);
        fillCreatorName(voList);
        // å¯¼å‡º Excel
        ExcelUtils.write(response, "记账凭证.xls", "数据", FinVoucherRespVO.class, voList);
    }
    /**
     * å¡«å……制单人昵称。
     * BaseDO.creator å­˜çš„æ˜¯ç”¨æˆ· ID,列表与导出表里直接展示就是一串数字,
     * æ‰€ä»¥æŒ‰ ERP æ”¶æ¬¾å•/付款单的既有口径,统一在这里把用户信息批量查一次再回填。
     */
    private void fillCreatorName(List<FinVoucherRespVO> list) {
        if (CollUtil.isEmpty(list)) {
            return;
        }
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(list,
                voucher -> Stream.of(NumberUtils.parseLong(voucher.getCreator()))));
        list.forEach(voucher -> MapUtils.findAndThen(userMap, NumberUtils.parseLong(voucher.getCreator()),
                user -> voucher.setCreatorName(user.getNickname())));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherEntryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.finance.controller.admin.voucher.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.finance.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - å‡­è¯åˆ†å½• Response VO")
@Data
@ExcelIgnoreUnannotated
public class FinVoucherEntryRespVO {
    @Schema(description = "分录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "行号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer rowNo;
    @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
    @ExcelProperty("科目编码")
    private String subjectCode;
    @Schema(description = "科目名称", example = "库存现金")
    @ExcelProperty("科目名称")
    private String subjectName;
    @Schema(description = "摘要", example = "收到货款")
    @ExcelProperty("摘要")
    private String summary;
    @Schema(description = "借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
    @ExcelProperty("借方金额")
    private BigDecimal debit;
    @Schema(description = "贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00")
    @ExcelProperty("贷方金额")
    private BigDecimal credit;
    @Schema(description = "辅助核算类型", example = "1")
    @ExcelProperty(value = "辅助核算类型", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.AUXILIARY_TYPE)
    private Integer auxiliaryType;
    @Schema(description = "辅助核算对象编号", example = "1024")
    private String auxiliaryId;
    @Schema(description = "辅助核算对象名称", example = "杭州某某公司")
    @ExcelProperty("辅助核算对象")
    private String auxiliaryName;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherEntrySaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.finance.controller.admin.voucher.vo;
import cn.iocoder.yudao.module.finance.enums.FinAuxiliaryTypeEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - å‡­è¯åˆ†å½•新增/修改 Request VO")
@Data
public class FinVoucherEntrySaveReqVO {
    @Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
    @NotEmpty(message = "分录的科目编码不能为空")
    private String subjectCode;
    @Schema(description = "科目名称", example = "库存现金")
    private String subjectName;
    @Schema(description = "摘要", example = "收到货款")
    @Size(max = 500, message = "分录摘要长度不能超过 500 ä¸ªå­—符")
    private String summary;
    @Schema(description = "借方金额", example = "1000.00")
    private BigDecimal debit;
    @Schema(description = "贷方金额", example = "0.00")
    private BigDecimal credit;
    @Schema(description = "辅助核算类型", example = "1")
    @InEnum(value = FinAuxiliaryTypeEnum.class, message = "辅助核算类型必须是 {value}")
    private Integer auxiliaryType;
    @Schema(description = "辅助核算对象编号", example = "1024")
    private String auxiliaryId;
    @Schema(description = "辅助核算对象名称", example = "杭州某某公司")
    private String auxiliaryName;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.finance.controller.admin.voucher.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - è®°è´¦å‡­è¯åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class FinVoucherPageReqVO extends PageParam {
    @Schema(description = "凭证字号", example = "è®°-0001")
    private String voucherNo;
    @Schema(description = "摘要")
    private String summary;
    @Schema(description = "制单人", example = "admin")
    private String creator;
    @Schema(description = "凭证状态", example = "0")
    private Integer status;
    @Schema(description = "凭证日期区间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate[] voucherDate;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.finance.controller.admin.voucher.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.finance.enums.DictTypeConstants;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è®°è´¦å‡­è¯ Response VO")
@Data
@ExcelIgnoreUnannotated
public class FinVoucherRespVO {
    @Schema(description = "凭证编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @ExcelProperty("凭证编号")
    private Long id;
    @Schema(description = "凭证字号", requiredMode = Schema.RequiredMode.REQUIRED, example = "è®°-0001")
    @ExcelProperty("凭证字号")
    private String voucherNo;
    @Schema(description = "凭证日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("凭证日期")
    private LocalDate voucherDate;
    @Schema(description = "摘要", example = "销售收入")
    @ExcelProperty("摘要")
    private String summary;
    @Schema(description = "借方合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
    @ExcelProperty("借方合计")
    private BigDecimal debit;
    @Schema(description = "贷方合计", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
    @ExcelProperty("贷方合计")
    private BigDecimal credit;
    @Schema(description = "凭证状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    // æ³¨æ„ï¼š@DictFormat åªè´Ÿè´£ã€Œä»Žå­—段上取字典类型」,真正做翻译的是 DictConvert,
    // å¿…须在这里显式挂上 converter,否则导出的是 0/10/20 è¿™æ ·çš„原始码
    @ExcelProperty(value = "凭证状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.VOUCHER_STATUS)
    private Integer status;
    @Schema(description = "制单人(用户编号)", example = "1")
    private String creator;
    /**
     * åˆ¶å•人昵称,专供列表与 Excel å¯¼å‡ºã€‚
     * BaseDO.creator å­˜çš„æ˜¯ç”¨æˆ· ID,直接展示就是一串数字,所以另用这个字段承载昵称;
     * creator æœ¬èº«ä»ä¿ç•™åœ¨ JSON é‡Œï¼ˆå‰ç«¯æœç´¢æ¡ä»¶ã€æƒé™åˆ¤æ–­é å®ƒï¼‰ã€‚
     */
    @Schema(description = "制单人昵称", example = "管理员")
    @ExcelProperty("制单人")
    private String creatorName;
    @Schema(description = "附件张数", example = "0")
    @ExcelProperty("附件张数")
    private Integer attachmentCount;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    // ã€Œyyyy-MM-dd HH:mm:ss」正好 19 ä¸ªå­—符,自适应列宽就只给 19,贴着边很难看,这里放宽到 22
    @ColumnWidth(22)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    @Schema(description = "凭证分录", example = "[]")
    private List<FinVoucherEntryRespVO> entries;
    @Schema(description = "附件列表", example = "[]")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "是否存在附件", example = "true")
    private Boolean hasAttachment;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/controller/admin/voucher/vo/FinVoucherSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.finance.controller.admin.voucher.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - è®°è´¦å‡­è¯æ–°å¢ž/修改 Request VO")
@Data
public class FinVoucherSaveReqVO {
    @Schema(description = "凭证编号", example = "1024")
    private Long id;
    @Schema(description = "凭证字号;为空时由系统按【凭证字+年月+序号】自动生成", example = "è®°2026090001")
    @Size(max = 64, message = "凭证字号长度不能超过 64 ä¸ªå­—符")
    private String voucherNo;
    @Schema(description = "凭证日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "凭证日期不能为空")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate voucherDate;
    @Schema(description = "摘要", example = "销售收入")
    @Size(max = 500, message = "摘要长度不能超过 500 ä¸ªå­—符")
    private String summary;
    @Schema(description = "附件张数", example = "0")
    private Integer attachmentCount;
    @Schema(description = "备注", example = "备注")
    @Size(max = 500, message = "备注长度不能超过 500 ä¸ªå­—符")
    private String remark;
    @Schema(description = "凭证分录", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "凭证分录不能为空")
    @Valid
    private List<FinVoucherEntrySaveReqVO> entries;
    @Schema(description = "附件编号数组", example = "[]")
    private List<Long> blobIds;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/asset/FinFixedAssetDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.finance.dal.dataobject.asset;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * å›ºå®šèµ„产 DO
 *
 * @author è¶…级管理员
 */
@TableName("fin_fixed_asset")
@KeySequence("fin_fixed_asset_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinFixedAssetDO extends BaseDO {
    /**
     * èµ„产编号
     */
    @TableId
    private Long id;
    /**
     * èµ„产编号
     *
     * ä¾‹å¦‚说:GD20260512001
     */
    private String assetCode;
    /**
     * èµ„产名称
     */
    private String assetName;
    /**
     * èµ„产类别
     */
    private String category;
    /**
     * è§„格型号
     */
    private String specification;
    /**
     * è´­ç½®æ—¥æœŸ
     */
    private LocalDate purchaseDate;
    /**
     * èµ„产原值
     */
    private BigDecimal originalValue;
    /**
     * ä½¿ç”¨å¹´é™ï¼ˆå¹´ï¼‰
     */
    private Integer usefulLife;
    /**
     * æ®‹å€¼çŽ‡ï¼ˆ%)
     */
    private BigDecimal residualRate;
    /**
     * ç´¯è®¡æŠ˜æ—§
     */
    private BigDecimal accumulatedDepreciation;
    /**
     * å‡€å€¼ = åŽŸå€¼ - ç´¯è®¡æŠ˜æ—§
     */
    private BigDecimal netValue;
    /**
     * å­˜æ”¾åœ°ç‚¹
     */
    private String location;
    /**
     * ä½¿ç”¨éƒ¨é—¨ï¼ˆå·²åºŸå¼ƒï¼‰
     *
     * åŽ†å²é—ç•™çš„è‡ªç”±æ–‡æœ¬åˆ—ï¼Œå·²ä¸å†å†™å…¥ä¸Žè¯»å–ï¼Œå±•ç¤ºç”¨çš„éƒ¨é—¨åç§°ç”± departmentId å®žæ—¶è§£æžï¼›
     * ä¿ç•™åˆ—是为了不丢历史数据,新逻辑一律用 departmentId。
     */
    private String department;
    /**
     * ä¿ç®¡äººï¼ˆå·²åºŸå¼ƒï¼‰
     *
     * åŒ department,历史遗留的自由文本列,已不再写入与读取,展示用昵称由 keeperId å®žæ—¶è§£æžã€‚
     */
    private String keeper;
    /**
     * ä½¿ç”¨éƒ¨é—¨ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO#getId()}
     */
    private Long departmentId;
    /**
     * ä¿ç®¡äººç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO#getId()}
     */
    private Long keeperId;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.finance.enums.FinFixedAssetStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/asset/FinIntangibleAssetDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.finance.dal.dataobject.asset;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * æ— å½¢èµ„产 DO
 *
 * @author è¶…级管理员
 */
@TableName("fin_intangible_asset")
@KeySequence("fin_intangible_asset_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinIntangibleAssetDO extends BaseDO {
    /**
     * èµ„产编号
     */
    @TableId
    private Long id;
    /**
     * èµ„产编号
     *
     * ä¾‹å¦‚说:WX20260512001
     */
    private String assetCode;
    /**
     * èµ„产名称
     */
    private String assetName;
    /**
     * èµ„产类别
     */
    private String category;
    /**
     * è¯ä¹¦ç¼–号
     */
    private String certificateNo;
    /**
     * å–得日期
     */
    private LocalDate acquisitionDate;
    /**
     * èµ„产原值
     */
    private BigDecimal originalValue;
    /**
     * æ‘Šé”€å¹´é™ï¼ˆå¹´ï¼‰
     */
    private Integer amortizationPeriod;
    /**
     * æ®‹å€¼çŽ‡ï¼ˆ%)
     */
    private BigDecimal residualRate;
    /**
     * ç´¯è®¡æ‘Šé”€
     */
    private BigDecimal accumulatedAmortization;
    /**
     * å‡€å€¼ = åŽŸå€¼ - ç´¯è®¡æ‘Šé”€
     */
    private BigDecimal netValue;
    /**
     * æœ‰æ•ˆæœŸè‡³
     */
    private LocalDate validityDate;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.finance.enums.FinIntangibleAssetStatusEnum}
     */
    private Integer status;
    /**
     * èµ„产描述
     */
    private String description;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/ledger/FinLedgerEntryDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.finance.dal.dataobject.ledger;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * æ€»è´¦ / æ˜Žç»†è´¦çš„分录投影
 *
 * è¯¥å¯¹è±¡ä¸æ˜¯æ•°æ®åº“表,仅用于承载 fin_voucher_entry ä¸Ž fin_voucher çš„联表查询结果,
 * ä¾›æ€»è´¦ã€æ˜Žç»†è´¦åœ¨å†…存中重算余额使用
 *
 * @author è¶…级管理员
 */
@Data
public class FinLedgerEntryDO {
    /**
     * å‡­è¯ç¼–号
     */
    private Long voucherId;
    /**
     * å‡­è¯å­—号
     */
    private String voucherNo;
    /**
     * å‡­è¯æ—¥æœŸ
     */
    private LocalDate voucherDate;
    /**
     * è¡Œå·
     */
    private Integer rowNo;
    /**
     * ç§‘目编码
     */
    private String subjectCode;
    /**
     * ç§‘目名称
     */
    private String subjectName;
    /**
     * æ‘˜è¦
     */
    private String summary;
    /**
     * å€Ÿæ–¹é‡‘额
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹é‡‘额
     */
    private BigDecimal credit;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/subject/FinAccountSubjectDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.finance.dal.dataobject.subject;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * ä¼šè®¡ç§‘ç›® DO
 *
 * @author è¶…级管理员
 */
@TableName("fin_account_subject")
@KeySequence("fin_account_subject_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinAccountSubjectDO extends BaseDO {
    /**
     * ç§‘目编号
     */
    @TableId
    private Long id;
    /**
     * çˆ¶ç§‘目编号
     *
     * å…³è” {@link #id};为空时表示根科目
     */
    private Long parentId;
    /**
     * ç§‘目编码
     */
    private String subjectCode;
    /**
     * ç§‘目名称
     */
    private String subjectName;
    /**
     * ç§‘目类型
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.finance.enums.FinSubjectTypeEnum}
     */
    private Integer subjectType;
    /**
     * ä½™é¢æ–¹å‘
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.finance.enums.FinBalanceDirectionEnum}
     */
    private Integer balanceDirection;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/voucher/FinVoucherDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.finance.dal.dataobject.voucher;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * è®°è´¦å‡­è¯ DO
 *
 * åˆ¶å•人使用基类 {@link BaseDO#getCreator()} å­—段,不再单独建列
 *
 * @author è¶…级管理员
 */
@TableName("fin_voucher")
@KeySequence("fin_voucher_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinVoucherDO extends BaseDO {
    /**
     * å‡­è¯ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å‡­è¯å­—号
     *
     * ä¾‹å¦‚说:记-0001
     */
    private String voucherNo;
    /**
     * å‡­è¯æ—¥æœŸ
     */
    private LocalDate voucherDate;
    /**
     * æ‘˜è¦
     */
    private String summary;
    /**
     * å€Ÿæ–¹åˆè®¡
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹åˆè®¡
     */
    private BigDecimal credit;
    /**
     * å‡­è¯çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.finance.enums.FinVoucherStatusEnum}
     */
    private Integer status;
    /**
     * é™„ä»¶å¼ æ•°
     */
    private Integer attachmentCount;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/dataobject/voucher/FinVoucherEntryDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.finance.dal.dataobject.voucher;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * å‡­è¯åˆ†å½• DO
 *
 * @author è¶…级管理员
 */
@TableName("fin_voucher_entry")
@KeySequence("fin_voucher_entry_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinVoucherEntryDO extends BaseDO {
    /**
     * åˆ†å½•编号
     */
    @TableId
    private Long id;
    /**
     * å‡­è¯ç¼–号
     *
     * å…³è” {@link FinVoucherDO#getId()}
     */
    private Long voucherId;
    /**
     * è¡Œå·ï¼Œä»Ž 1 å¼€å§‹
     */
    private Integer rowNo;
    /**
     * ç§‘目编码
     *
     * å…³è” {@link cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO#getSubjectCode()}
     */
    private String subjectCode;
    /**
     * ç§‘目名称
     *
     * å†—余字段,保存分录时从会计科目带出
     */
    private String subjectName;
    /**
     * æ‘˜è¦
     */
    private String summary;
    /**
     * å€Ÿæ–¹é‡‘额
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹é‡‘额
     */
    private BigDecimal credit;
    /**
     * è¾…助核算类型
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.finance.enums.FinAuxiliaryTypeEnum}
     */
    private Integer auxiliaryType;
    /**
     * è¾…助核算对象编号
     *
     * æ ¹æ® {@link #auxiliaryType} æŒ‡å‘客户、供应商、部门、员工、项目的主键
     */
    private String auxiliaryId;
    /**
     * è¾…助核算对象名称
     */
    private String auxiliaryName;
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/asset/FinFixedAssetMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.finance.dal.mysql.asset;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetPageReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinFixedAssetDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * å›ºå®šèµ„产 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface FinFixedAssetMapper extends BaseMapperX<FinFixedAssetDO> {
    default PageResult<FinFixedAssetDO> selectPage(FinFixedAssetPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<FinFixedAssetDO>()
                .likeIfPresent(FinFixedAssetDO::getAssetCode, reqVO.getAssetCode())
                .likeIfPresent(FinFixedAssetDO::getAssetName, reqVO.getAssetName())
                .eqIfPresent(FinFixedAssetDO::getCategory, reqVO.getCategory())
                .eqIfPresent(FinFixedAssetDO::getDepartmentId, reqVO.getDepartmentId())
                .eqIfPresent(FinFixedAssetDO::getKeeperId, reqVO.getKeeperId())
                .eqIfPresent(FinFixedAssetDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(FinFixedAssetDO::getPurchaseDate, reqVO.getPurchaseDate())
                .orderByDesc(FinFixedAssetDO::getId));
    }
    default FinFixedAssetDO selectByAssetCode(String assetCode) {
        return selectOne(FinFixedAssetDO::getAssetCode, assetCode);
    }
    default List<FinFixedAssetDO> selectListByIds(Collection<Long> ids) {
        return selectList(new LambdaQueryWrapperX<FinFixedAssetDO>()
                .in(FinFixedAssetDO::getId, ids));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/asset/FinIntangibleAssetMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.finance.dal.mysql.asset;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetPageReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinIntangibleAssetDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * æ— å½¢èµ„产 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface FinIntangibleAssetMapper extends BaseMapperX<FinIntangibleAssetDO> {
    default PageResult<FinIntangibleAssetDO> selectPage(FinIntangibleAssetPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<FinIntangibleAssetDO>()
                .likeIfPresent(FinIntangibleAssetDO::getAssetCode, reqVO.getAssetCode())
                .likeIfPresent(FinIntangibleAssetDO::getAssetName, reqVO.getAssetName())
                .eqIfPresent(FinIntangibleAssetDO::getCategory, reqVO.getCategory())
                .likeIfPresent(FinIntangibleAssetDO::getCertificateNo, reqVO.getCertificateNo())
                .eqIfPresent(FinIntangibleAssetDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(FinIntangibleAssetDO::getAcquisitionDate, reqVO.getAcquisitionDate())
                .orderByDesc(FinIntangibleAssetDO::getId));
    }
    default FinIntangibleAssetDO selectByAssetCode(String assetCode) {
        return selectOne(FinIntangibleAssetDO::getAssetCode, assetCode);
    }
    default List<FinIntangibleAssetDO> selectListByIds(Collection<Long> ids) {
        return selectList(new LambdaQueryWrapperX<FinIntangibleAssetDO>()
                .in(FinIntangibleAssetDO::getId, ids));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/ledger/FinLedgerMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.finance.dal.mysql.ledger;
import cn.iocoder.yudao.module.finance.dal.dataobject.ledger.FinLedgerEntryDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
/**
 * æ€»è´¦ / æ˜Žç»†è´¦ Mapper
 *
 * åªç»Ÿè®¡å·²è¿‡è´¦ï¼ˆstatus = posted)的凭证分录;总账、明细账的余额在 Service å±‚按期间重算,
 * ä¿è¯å£å¾„与凭证状态一致
 *
 * @author è¶…级管理员
 */
@Mapper
public interface FinLedgerMapper {
    /**
     * æŸ¥è¯¢æŒ‡å®šç§‘目(可多个)在指定日期区间内的已过账分录
     *
     * @param subjectCodes ç§‘目编码集合
     * @param startDate    å¼€å§‹æ—¥æœŸï¼ˆå«ï¼‰
     * @param endDate      ç»“束日期(含)
     * @param status       å‡­è¯çŠ¶æ€
     * @return åˆ†å½•列表(按凭证日期、凭证编号、行号升序)
     */
    List<FinLedgerEntryDO> selectEntryList(@Param("subjectCodes") Collection<String> subjectCodes,
                                           @Param("startDate") LocalDate startDate,
                                           @Param("endDate") LocalDate endDate,
                                           @Param("status") Integer status);
    /**
     * æŸ¥è¯¢æŒ‡å®šç§‘目(可多个)在指定日期之前的已过账分录净额,即期初余额
     *
     * @param subjectCodes ç§‘目编码集合
     * @param date         æ—¥æœŸï¼ˆä¸å«ï¼‰
     * @param status       å‡­è¯çŠ¶æ€
     * @return å‡€é¢ï¼Œå€Ÿæ–¹ä¸ºæ­£ã€è´·æ–¹ä¸ºè´Ÿ
     */
    BigDecimal selectBalanceBeforeDate(@Param("subjectCodes") Collection<String> subjectCodes,
                                       @Param("date") LocalDate date,
                                       @Param("status") Integer status);
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/subject/FinAccountSubjectMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.finance.dal.mysql.subject;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectPageReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * ä¼šè®¡ç§‘ç›® Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface FinAccountSubjectMapper extends BaseMapperX<FinAccountSubjectDO> {
    default PageResult<FinAccountSubjectDO> selectPage(FinAccountSubjectPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<FinAccountSubjectDO>()
                .likeIfPresent(FinAccountSubjectDO::getSubjectCode, reqVO.getSubjectCode())
                .likeIfPresent(FinAccountSubjectDO::getSubjectName, reqVO.getSubjectName())
                .eqIfPresent(FinAccountSubjectDO::getSubjectType, reqVO.getSubjectType())
                .eqIfPresent(FinAccountSubjectDO::getStatus, reqVO.getStatus())
                .orderByAsc(FinAccountSubjectDO::getSubjectCode));
    }
    default FinAccountSubjectDO selectBySubjectCode(String subjectCode) {
        return selectOne(FinAccountSubjectDO::getSubjectCode, subjectCode);
    }
    default List<FinAccountSubjectDO> selectListByParentId(Long parentId) {
        return selectList(FinAccountSubjectDO::getParentId, parentId);
    }
    default List<FinAccountSubjectDO> selectListBySubjectCodes(Collection<String> subjectCodes) {
        return selectList(new LambdaQueryWrapperX<FinAccountSubjectDO>()
                .in(FinAccountSubjectDO::getSubjectCode, subjectCodes));
    }
    default List<FinAccountSubjectDO> selectListAll() {
        return selectList(new LambdaQueryWrapperX<FinAccountSubjectDO>()
                .orderByAsc(FinAccountSubjectDO::getSubjectCode));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/voucher/FinVoucherEntryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.finance.dal.mysql.voucher;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherEntryDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * å‡­è¯åˆ†å½• Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface FinVoucherEntryMapper extends BaseMapperX<FinVoucherEntryDO> {
    default List<FinVoucherEntryDO> selectListByVoucherId(Long voucherId) {
        return selectList(new LambdaQueryWrapperX<FinVoucherEntryDO>()
                .eq(FinVoucherEntryDO::getVoucherId, voucherId)
                .orderByAsc(FinVoucherEntryDO::getRowNo));
    }
    default List<FinVoucherEntryDO> selectListByVoucherIds(Collection<Long> voucherIds) {
        return selectList(new LambdaQueryWrapperX<FinVoucherEntryDO>()
                .in(FinVoucherEntryDO::getVoucherId, voucherIds)
                .orderByAsc(FinVoucherEntryDO::getVoucherId)
                .orderByAsc(FinVoucherEntryDO::getRowNo));
    }
    default Long selectCountBySubjectCodes(Collection<String> subjectCodes) {
        return selectCount(new LambdaQueryWrapperX<FinVoucherEntryDO>()
                .in(FinVoucherEntryDO::getSubjectCode, subjectCodes));
    }
    default void deleteByVoucherId(Long voucherId) {
        delete(new LambdaQueryWrapperX<FinVoucherEntryDO>()
                .eq(FinVoucherEntryDO::getVoucherId, voucherId));
    }
    default void deleteByVoucherIds(Collection<Long> voucherIds) {
        delete(new LambdaQueryWrapperX<FinVoucherEntryDO>()
                .in(FinVoucherEntryDO::getVoucherId, voucherIds));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/mysql/voucher/FinVoucherMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.finance.dal.mysql.voucher;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherPageReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
/**
 * è®°è´¦å‡­è¯ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface FinVoucherMapper extends BaseMapperX<FinVoucherDO> {
    /**
     * @param creatorIds åˆ¶å•人编号集合。列表里展示的是昵称,所以 reqVO.getCreator() æ‹¿åˆ°çš„æ˜¯æ˜µç§°/编号这类
     *                   æœç´¢è¯ï¼Œç”± Service å…ˆè§£æžæˆç¼–号集合再传进来;传 null è¡¨ç¤ºä¸æŒ‰åˆ¶å•人过滤。
     */
    default PageResult<FinVoucherDO> selectPage(FinVoucherPageReqVO reqVO, Collection<String> creatorIds) {
        return selectPage(reqVO, new LambdaQueryWrapperX<FinVoucherDO>()
                .likeIfPresent(FinVoucherDO::getVoucherNo, reqVO.getVoucherNo())
                .likeIfPresent(FinVoucherDO::getSummary, reqVO.getSummary())
                // æ³¨æ„ï¼šcreator å­˜çš„æ˜¯ç”¨æˆ·ç¼–号,搜索词不是编号(是昵称)时不能直接 like,
                // å¦åˆ™ã€Œèµµç£Šã€è¿™ç§æ˜µç§°æ°¸è¿œåŒ¹é…ä¸ä¸Šï¼›Service å·²ç»æŠŠæœç´¢è¯è§£æžæˆç¼–号集合
                .inIfPresent(FinVoucherDO::getCreator, creatorIds)
                .eqIfPresent(FinVoucherDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(FinVoucherDO::getVoucherDate, reqVO.getVoucherDate())
                .orderByDesc(FinVoucherDO::getVoucherDate)
                .orderByDesc(FinVoucherDO::getId));
    }
    default FinVoucherDO selectByVoucherNo(String voucherNo) {
        return selectOne(FinVoucherDO::getVoucherNo, voucherNo);
    }
    default List<FinVoucherDO> selectListByIds(Collection<Long> ids) {
        return selectList(new LambdaQueryWrapperX<FinVoucherDO>()
                .in(FinVoucherDO::getId, ids));
    }
    default Long selectCountByStatus(Integer status) {
        return selectCount(FinVoucherDO::getStatus, status);
    }
    default List<FinVoucherDO> selectListByDateBetween(LocalDate startDate, LocalDate endDate) {
        return selectList(new LambdaQueryWrapperX<FinVoucherDO>()
                .geIfPresent(FinVoucherDO::getVoucherDate, startDate)
                .leIfPresent(FinVoucherDO::getVoucherDate, endDate));
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/redis/RedisKeyConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.finance.dal.redis;
/**
 * Finance Redis Key æžšä¸¾ç±»
 *
 * @author è¶…级管理员
 */
public interface RedisKeyConstants {
    /**
     * åºå·çš„缓存
     *
     * KEY æ ¼å¼ï¼šfinance:seq_no:{prefix}
     * VALUE æ•°æ®æ ¼å¼ï¼šç¼–号自增
     */
    String NO = "finance:seq_no:";
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/dal/redis/no/FinNoRedisDAO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.finance.dal.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.module.finance.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
/**
 * Finance åºå·çš„ Redis DAO
 *
 * @author è¶…级管理员
 */
@Repository
public class FinNoRedisDAO {
    /**
     * å‡­è¯å­—的默认字号
     */
    public static final String VOUCHER_NO_PREFIX = "è®°";
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”Ÿæˆåºå·ï¼Œä½¿ç”¨å½“前日期,格式为 {PREFIX} + yyyyMM + 4 ä½è‡ªå¢ž
     * ä¾‹å¦‚说:记 202609 0001 ï¼ˆæ²¡æœ‰ä¸­é—´ç©ºæ ¼ï¼‰
     *
     * @param prefix å‰ç¼€
     * @return åºå·
     */
    public String generate(String prefix) {
        // é€’增序号
        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.SIMPLE_MONTH_PATTERN);
        String key = RedisKeyConstants.NO + noPrefix;
        Long no = stringRedisTemplate.opsForValue().increment(key);
        // è®¾ç½®è¿‡æœŸæ—¶é—´
        stringRedisTemplate.expire(key, Duration.ofDays(1L));
        return noPrefix + String.format("%04d", no);
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/DictTypeConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.finance.enums;
/**
 * Finance å­—典类型的枚举类
 *
 * @author è¶…级管理员
 */
public interface DictTypeConstants {
    String SUBJECT_TYPE = "finance_subject_type"; // ç§‘目类型
    String BALANCE_DIRECTION = "finance_balance_direction"; // ä½™é¢æ–¹å‘
    String AUXILIARY_TYPE = "finance_auxiliary_type"; // è¾…助核算类型
    String VOUCHER_STATUS = "finance_voucher_status"; // å‡­è¯çŠ¶æ€
    String FIXED_ASSET_STATUS = "finance_fixed_asset_status"; // å›ºå®šèµ„产状态
    String INTANGIBLE_ASSET_STATUS = "finance_intangible_asset_status"; // æ— å½¢èµ„产状态
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/ErrorCodeConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
 * Finance æ¨¡å—错误码
 *
 * finance æ¨¡å—,使用 1-060-xxx-xxx æ®µ
 *
 * @author è¶…级管理员
 */
public interface ErrorCodeConstants {
    // ========== ä¼šè®¡ç§‘ç›® 1_060_100_xxx ==========
    ErrorCode ACCOUNT_SUBJECT_NOT_EXISTS = new ErrorCode(1_060_100_000, "会计科目不存在");
    ErrorCode ACCOUNT_SUBJECT_CODE_DUPLICATE = new ErrorCode(1_060_100_001, "科目编码【{}】已存在,请更换后重试");
    ErrorCode ACCOUNT_SUBJECT_PARENT_NOT_EXISTS = new ErrorCode(1_060_100_002, "父科目【{}】不存在,请重新选择上级科目");
    ErrorCode ACCOUNT_SUBJECT_PARENT_SELF = new ErrorCode(1_060_100_003, "上级科目不能选择自己,请重新选择");
    ErrorCode ACCOUNT_SUBJECT_PARENT_CYCLE = new ErrorCode(1_060_100_004, "上级科目【{}】是当前科目的下级,会形成循环,请重新选择");
    ErrorCode ACCOUNT_SUBJECT_HAS_CHILDREN = new ErrorCode(1_060_100_005, "会计科目【{}】下还有 {} ä¸ªä¸‹çº§ç§‘目,请先删除或转移下级科目");
    ErrorCode ACCOUNT_SUBJECT_REFERENCED = new ErrorCode(1_060_100_006, "会计科目【{}】已被 {} æ¡å‡­è¯åˆ†å½•引用,无法删除;请先处理对应凭证");
    // ========== å‡­è¯ 1_060_101_xxx ==========
    ErrorCode VOUCHER_NOT_EXISTS = new ErrorCode(1_060_101_000, "凭证不存在");
    ErrorCode VOUCHER_NO_DUPLICATE = new ErrorCode(1_060_101_001, "凭证字号【{}】已存在,请更换后重试");
    ErrorCode VOUCHER_ENTRY_EMPTY = new ErrorCode(1_060_101_002, "凭证至少需要一条有效分录(科目不为空,且借方或贷方金额大于 0)");
    ErrorCode VOUCHER_ENTRY_DIRECTION_BOTH = new ErrorCode(1_060_101_003, "第 {} è¡Œåˆ†å½•(科目【{}】)的借方金额【{}】与贷方金额【{}】不能同时大于 0,请只填写其中一方");
    ErrorCode VOUCHER_ENTRY_SUBJECT_NOT_EXISTS = new ErrorCode(1_060_101_004, "第 {} è¡Œåˆ†å½•的科目编码【{}】不存在,请重新选择科目");
    ErrorCode VOUCHER_ENTRY_UNBALANCED = new ErrorCode(1_060_101_005, "凭证借贷不平衡:借方合计【{}】元,贷方合计【{}】元,两者必须相等且大于 0");
    ErrorCode VOUCHER_STATUS_NOT_UNPOSTED = new ErrorCode(1_060_101_006, "凭证【{}】当前状态为【{}】,只有未过账的凭证允许修改或过账");
    ErrorCode VOUCHER_DELETE_NOT_ALLOWED = new ErrorCode(1_060_101_007, "凭证【{}】当前状态为【{}】,不允许删除;已过账凭证请编制红字凭证冲销,已作废凭证可直接删除");
    ErrorCode VOUCHER_ENTRY_SUBJECT_REQUIRED = new ErrorCode(1_060_101_008, "第 {} è¡Œåˆ†å½•已填写金额(借方【{}】元、贷方【{}】元),但未选择会计科目,请选择科目或删除该行");
    ErrorCode VOUCHER_ENTRY_AMOUNT_REQUIRED = new ErrorCode(1_060_101_009, "第 {} è¡Œåˆ†å½•(科目【{}】)未填写借方或贷方金额,请补全金额或删除该行");
    ErrorCode VOUCHER_STATUS_NOT_CANCELLABLE = new ErrorCode(1_060_101_010, "凭证【{}】当前状态为【{}】,已作废的凭证不能重复作废");
    // ========== æ€»è´¦ / æ˜Žç»†è´¦ 1_060_102_xxx ==========
    ErrorCode LEDGER_SUBJECT_REQUIRED = new ErrorCode(1_060_102_000, "查询总账/明细账必须指定会计科目");
    ErrorCode LEDGER_MONTH_RANGE_INVALID = new ErrorCode(1_060_102_001, "查询期间不合法:开始月份【{}】不能晚于结束月份【{}】");
    ErrorCode LEDGER_SUBJECT_NOT_EXISTS = new ErrorCode(1_060_102_002, "会计科目【{}】不存在,请重新选择科目");
    // ========== è´¢åŠ¡æŠ¥è¡¨ / ä¼šè®¡æ ¸ç®—看板 1_060_103_xxx ==========
    ErrorCode STATISTICS_PERIOD_INVALID = new ErrorCode(1_060_103_000, "统计期间不合法:开始月份【{}】不能晚于结束月份【{}】");
    // ========== å›ºå®šèµ„产 1_060_200_xxx ==========
    ErrorCode FIXED_ASSET_NOT_EXISTS = new ErrorCode(1_060_200_000, "固定资产不存在");
    ErrorCode FIXED_ASSET_CODE_DUPLICATE = new ErrorCode(1_060_200_001, "资产编号【{}】已存在,请更换后重试");
    ErrorCode FIXED_ASSET_DEPRECIATE_EMPTY = new ErrorCode(1_060_200_002, "没有可计提折旧的固定资产:所选资产的累计折旧均已提足,无需再计提");
    ErrorCode FIXED_ASSET_DEPRECIATE_NEGATIVE = new ErrorCode(1_060_200_003, "固定资产【{}】的原值为【{}】元、使用年限 {} å¹´ã€æ®‹å€¼çއ {}%,月折旧额计算结果小于等于 0,请检查原值、使用年限与残值率后重新保存该资产");
    // ========== æ— å½¢èµ„产 1_060_201_xxx ==========
    ErrorCode INTANGIBLE_ASSET_NOT_EXISTS = new ErrorCode(1_060_201_000, "无形资产不存在");
    ErrorCode INTANGIBLE_ASSET_CODE_DUPLICATE = new ErrorCode(1_060_201_001, "资产编号【{}】已存在,请更换后重试");
    ErrorCode INTANGIBLE_ASSET_AMORTIZE_EMPTY = new ErrorCode(1_060_201_002, "没有可计提摊销的无形资产:所选资产的累计摊销均已摊完,无需再计提");
    ErrorCode INTANGIBLE_ASSET_AMORTIZE_NEGATIVE = new ErrorCode(1_060_201_003, "无形资产【{}】的原值为【{}】元、摊销年限 {} å¹´ã€æ®‹å€¼çއ {}%,月摊销额计算结果小于等于 0,请检查原值、摊销年限与残值率后重新保存该资产");
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinAuxiliaryTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å‡­è¯åˆ†å½•取辅助核算类型枚举
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinAuxiliaryTypeEnum implements ArrayValuable<Integer> {
    CUSTOMER(1, "客户"),
    SUPPLIER(2, "供应商"),
    DEPARTMENT(3, "部门"),
    EMPLOYEE(4, "员工"),
    PROJECT(5, "项目");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(FinAuxiliaryTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinBalanceDirectionEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.math.BigDecimal;
import java.util.Arrays;
/**
 * ä¼šè®¡ç§‘目余额方向枚举
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinBalanceDirectionEnum implements ArrayValuable<Integer> {
    DEBIT(1, "借"),
    CREDIT(2, "è´·");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(FinBalanceDirectionEnum::getDirection).toArray(Integer[]::new);
    /**
     * æ–¹å‘
     */
    private final Integer direction;
    /**
     * æ–¹å‘名
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    /**
     * æ ¹æ®ä½™é¢ï¼ˆå€Ÿæ–¹ä¸ºæ­£ã€è´·æ–¹ä¸ºè´Ÿï¼‰åˆ¤æ–­ä½™é¢æ–¹å‘
     *
     * @param balance ä½™é¢ï¼Œå…è®¸ä¸ºç©ºï¼ˆæŒ‰ 0 å¤„理)
     * @return ä½™é¢æ–¹å‘
     */
    public static Integer getByBalance(BigDecimal balance) {
        return balance != null && balance.compareTo(BigDecimal.ZERO) < 0
                ? CREDIT.getDirection() : DEBIT.getDirection();
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinFixedAssetStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * å›ºå®šèµ„产状态枚举
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinFixedAssetStatusEnum implements ArrayValuable<Integer> {
    IN_USE(1, "在用"),
    IDLE(2, "闲置"),
    REPAIR(3, "维修中"),
    SCRAPPED(4, "已报废");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(FinFixedAssetStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinIntangibleAssetStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * æ— å½¢èµ„产状态枚举
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinIntangibleAssetStatusEnum implements ArrayValuable<Integer> {
    IN_USE(1, "在用"),
    EXPIRED(2, "已失效"),
    AMORTIZED(3, "已摊销完");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(FinIntangibleAssetStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinLedgerRowTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.finance.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
 * æ€»è´¦ / æ˜Žç»†è´¦è¡Œç±»åž‹æžšä¸¾
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinLedgerRowTypeEnum {
    OPENING("opening", "期初余额"),
    ENTRY("entry", "明细"),
    MONTHLY_TOTAL("monthly_total", "本月合计"),
    YEARLY_TOTAL("yearly_total", "本年累计");
    /**
     * è¡Œç±»åž‹
     */
    private final String type;
    /**
     * è¡Œç±»åž‹å
     */
    private final String name;
    public static FinLedgerRowTypeEnum valueOfType(String type) {
        return Arrays.stream(values()).filter(item -> Objects.equals(item.getType(), type))
                .findFirst().orElse(null);
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinSubjectTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * ä¼šè®¡ç§‘目类型枚举
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinSubjectTypeEnum implements ArrayValuable<Integer> {
    ASSET(1, "资产类"),
    LIABILITY(2, "负债类"),
    EQUITY(3, "所有者权益类"),
    COST(4, "成本类"),
    PROFIT_LOSS(5, "损益类");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(FinSubjectTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/enums/FinVoucherStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.finance.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
 * å‡­è¯çŠ¶æ€æžšä¸¾
 *
 * çŠ¶æ€æµè½¬ï¼šunposted(未过账)-> posted(已过账);unposted(未过账)-> cancelled(已作废)
 *
 * @author è¶…级管理员
 */
@RequiredArgsConstructor
@Getter
public enum FinVoucherStatusEnum implements ArrayValuable<Integer> {
    UNPOSTED(0, "未过账"),
    POSTED(10, "已过账"),
    CANCELLED(20, "已作废");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(FinVoucherStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static FinVoucherStatusEnum valueOf(Integer status) {
        return Arrays.stream(values()).filter(item -> Objects.equals(item.getStatus(), status))
                .findFirst().orElse(null);
    }
    public static String getName(Integer status) {
        FinVoucherStatusEnum statusEnum = valueOf(status);
        return statusEnum == null ? String.valueOf(status) : statusEnum.getName();
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/framework/web/config/FinanceWebConfiguration.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.finance.framework.web.config;
import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * finance æ¨¡å—çš„ web ç»„ä»¶çš„ Configuration
 *
 * @author è¶…级管理员
 */
@Configuration(proxyBeanMethods = false)
public class FinanceWebConfiguration {
    /**
     * finance æ¨¡å—çš„ API åˆ†ç»„
     */
    @Bean
    public GroupedOpenApi financeGroupedOpenApi() {
        return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("finance");
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinAssetAmountUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.finance.service.asset;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
 * å›ºå®šèµ„产折旧 / æ— å½¢èµ„产摊销的计算工具
 *
 * ç»Ÿä¸€å£å¾„:
 * 1. åº”计提总额(基数) = åŽŸå€¼ Ã— (1 - æ®‹å€¼çއ / 100)
 * 2. æœˆæŠ˜æ—§é¢ / æœˆæ‘Šé”€é¢ = åº”计提总额 / (年限 Ã— 12),四舍五入保留 2 ä½å°æ•°
 *
 * @author è¶…级管理员
 */
public class FinAssetAmountUtils {
    private FinAssetAmountUtils() {
    }
    /**
     * è®¡ç®—应计提总额(基数)
     *
     * @param originalValue åŽŸå€¼
     * @param residualRate  æ®‹å€¼çŽ‡ï¼ˆ%)
     * @return åº”计提总额,保留 2 ä½å°æ•°
     */
    public static BigDecimal calcDepreciableBase(BigDecimal originalValue, BigDecimal residualRate) {
        return originalValue.multiply(BigDecimal.ONE.subtract(toResidualRatio(residualRate)))
                .setScale(2, RoundingMode.HALF_UP);
    }
    /**
     * è®¡ç®—月折旧额 / æœˆæ‘Šé”€é¢
     *
     * @param originalValue åŽŸå€¼
     * @param residualRate  æ®‹å€¼çŽ‡ï¼ˆ%)
     * @param years         å¹´é™ï¼ˆå¹´ï¼‰ï¼Œå¿…须大于 0
     * @return æœˆæŠ˜æ—§é¢ / æœˆæ‘Šé”€é¢ï¼Œä¿ç•™ 2 ä½å°æ•°
     */
    public static BigDecimal calcMonthlyAmount(BigDecimal originalValue, BigDecimal residualRate, int years) {
        BigDecimal base = originalValue.multiply(BigDecimal.ONE.subtract(toResidualRatio(residualRate)));
        return base.divide(BigDecimal.valueOf(years * 12L), 2, RoundingMode.HALF_UP);
    }
    /**
     * æ®‹å€¼çŽ‡ï¼ˆ%)转成计算用的比例,例如 5 -> 0.05
     */
    private static BigDecimal toResidualRatio(BigDecimal residualRate) {
        BigDecimal rate = residualRate == null ? BigDecimal.ZERO : residualRate;
        return rate.divide(BigDecimal.valueOf(100), 10, RoundingMode.HALF_UP);
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinFixedAssetService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.finance.service.asset;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetDepreciateRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinFixedAssetDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * å›ºå®šèµ„产 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface FinFixedAssetService {
    /**
     * åˆ›å»ºå›ºå®šèµ„产
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return èµ„产编号
     */
    Long createFixedAsset(@Valid FinFixedAssetSaveReqVO createReqVO);
    /**
     * æ›´æ–°å›ºå®šèµ„产
     *
     * ä¿®æ”¹åŽŸå€¼ã€ä½¿ç”¨å¹´é™ã€æ®‹å€¼çŽ‡æ—¶ä¼šæŒ‰æ–°å‚æ•°é‡ç®—ç´¯è®¡æŠ˜æ—§ä¸Žå‡€å€¼
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateFixedAsset(@Valid FinFixedAssetSaveReqVO updateReqVO);
    /**
     * åˆ é™¤å›ºå®šèµ„产
     *
     * @param id èµ„产编号
     */
    void deleteFixedAsset(Long id);
    /**
     * æ‰¹é‡åˆ é™¤å›ºå®šèµ„产
     *
     * @param ids èµ„产编号数组
     */
    void deleteFixedAssetList(List<Long> ids);
    /**
     * è®¡ææŠ˜æ—§
     *
     * æ¯ä¸ªèµ„产计提一个月的折旧:月折旧额 = åŽŸå€¼ Ã— (1 - æ®‹å€¼çއ / 100) / (使用年限 Ã— 12)。
     * ç´¯è®¡æŠ˜æ—§ä»¥åº”计提总额封顶,最后一期按剩余可折旧额计提,净值不会被提成负数
     *
     * @param ids èµ„产编号数组;为空时对所有未报废的固定资产计提
     * @return è®¡æç»“æžœ
     */
    FinFixedAssetDepreciateRespVO depreciateFixedAssetList(List<Long> ids);
    /**
     * èŽ·å¾—å›ºå®šèµ„äº§
     *
     * @param id èµ„产编号
     * @return å›ºå®šèµ„产
     */
    FinFixedAssetDO getFixedAsset(Long id);
    /**
     * èŽ·å¾—å›ºå®šèµ„äº§åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return å›ºå®šèµ„产分页
     */
    PageResult<FinFixedAssetDO> getFixedAssetPage(FinFixedAssetPageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‡å®šç¼–å·çš„å›ºå®šèµ„äº§åˆ—è¡¨
     *
     * @param ids èµ„产编号数组
     * @return å›ºå®šèµ„产列表
     */
    List<FinFixedAssetDO> getFixedAssetList(Collection<Long> ids);
    /**
     * èŽ·å¾—å…¨éƒ¨å›ºå®šèµ„äº§åˆ—è¡¨
     *
     * @return å›ºå®šèµ„产列表
     */
    List<FinFixedAssetDO> getFixedAssetList();
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinFixedAssetServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,203 @@
package cn.iocoder.yudao.module.finance.service.asset;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetDepreciateRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinFixedAssetSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinFixedAssetDO;
import cn.iocoder.yudao.module.finance.dal.mysql.asset.FinFixedAssetMapper;
import cn.iocoder.yudao.module.finance.enums.FinFixedAssetStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.finance.service.asset.FinAssetAmountUtils.calcDepreciableBase;
import static cn.iocoder.yudao.module.finance.service.asset.FinAssetAmountUtils.calcMonthlyAmount;
/**
 * å›ºå®šèµ„产 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class FinFixedAssetServiceImpl implements FinFixedAssetService {
    @Resource
    private FinFixedAssetMapper fixedAssetMapper;
    @Override
    public Long createFixedAsset(FinFixedAssetSaveReqVO createReqVO) {
        // 1. æ ¡éªŒèµ„产编号唯一
        validateAssetCodeUnique(null, createReqVO.getAssetCode());
        // 2. æ’入:新资产尚未计提折旧
        FinFixedAssetDO asset = BeanUtils.toBean(createReqVO, FinFixedAssetDO.class);
        asset.setAssetCode(StrUtil.trim(asset.getAssetCode()));
        asset.setResidualRate(ObjectUtil.defaultIfNull(asset.getResidualRate(), BigDecimal.ZERO));
        asset.setStatus(ObjectUtil.defaultIfNull(asset.getStatus(), FinFixedAssetStatusEnum.IN_USE.getStatus()));
        asset.setAccumulatedDepreciation(BigDecimal.ZERO);
        fillNetValue(asset);
        fixedAssetMapper.insert(asset);
        return asset.getId();
    }
    @Override
    public void updateFixedAsset(FinFixedAssetSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        FinFixedAssetDO oldAsset = validateFixedAssetExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒèµ„产编号唯一
        validateAssetCodeUnique(updateReqVO.getId(), updateReqVO.getAssetCode());
        // 2.1 æ›´æ–°ï¼ˆç´¯è®¡æŠ˜æ—§ä¸ä»Žå‰ç«¯æŽ¥æ”¶ï¼Œæ²¿ç”¨åº“中值)
        FinFixedAssetDO updateObj = BeanUtils.toBean(updateReqVO, FinFixedAssetDO.class);
        updateObj.setAssetCode(StrUtil.trim(updateObj.getAssetCode()));
        updateObj.setResidualRate(ObjectUtil.defaultIfNull(updateObj.getResidualRate(), BigDecimal.ZERO));
        updateObj.setStatus(ObjectUtil.defaultIfNull(updateObj.getStatus(), oldAsset.getStatus()));
        updateObj.setAccumulatedDepreciation(ObjectUtil.defaultIfNull(oldAsset.getAccumulatedDepreciation(), BigDecimal.ZERO));
        // 2.2 åŽŸå€¼ã€æ®‹å€¼çŽ‡å˜åŒ–åŽï¼ŒæŒ‰æ–°çš„åº”è®¡ææ€»é¢å°é¡¶ç´¯è®¡æŠ˜æ—§ï¼Œå¹¶é‡ç®—å‡€å€¼
        BigDecimal depreciableBase = calcDepreciableBase(defaultIfNull(updateObj.getOriginalValue()),
                updateObj.getResidualRate());
        if (updateObj.getAccumulatedDepreciation().compareTo(depreciableBase) > 0) {
            updateObj.setAccumulatedDepreciation(depreciableBase);
        }
        fillNetValue(updateObj);
        fixedAssetMapper.updateById(updateObj);
    }
    @Override
    public void deleteFixedAsset(Long id) {
        validateFixedAssetExists(id);
        fixedAssetMapper.deleteById(id);
    }
    @Override
    public void deleteFixedAssetList(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return;
        }
        fixedAssetMapper.deleteByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FinFixedAssetDepreciateRespVO depreciateFixedAssetList(List<Long> ids) {
        // 1. æŸ¥è¯¢å¾…计提的资产:已报废的不再计提
        List<FinFixedAssetDO> assets = CollUtil.isEmpty(ids)
                ? fixedAssetMapper.selectList() : fixedAssetMapper.selectListByIds(ids);
        List<FinFixedAssetDO> updateList = new ArrayList<>();
        BigDecimal totalMonthlyDepreciation = BigDecimal.ZERO;
        for (FinFixedAssetDO asset : assets) {
            if (FinFixedAssetStatusEnum.SCRAPPED.getStatus().equals(asset.getStatus())) {
                continue;
            }
            BigDecimal originalValue = defaultIfNull(asset.getOriginalValue());
            BigDecimal residualRate = defaultIfNull(asset.getResidualRate());
            int usefulLife = ObjectUtil.defaultIfNull(asset.getUsefulLife(), 0);
            BigDecimal accumulated = defaultIfNull(asset.getAccumulatedDepreciation());
            // 1.1 å·²æè¶³çš„不再计提
            BigDecimal depreciableBase = calcDepreciableBase(originalValue, residualRate);
            BigDecimal remaining = depreciableBase.subtract(accumulated);
            if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            // 1.2 å‚数异常导致月折旧额非正
            BigDecimal monthlyDepreciation = usefulLife <= 0 ? BigDecimal.ZERO
                    : calcMonthlyAmount(originalValue, residualRate, usefulLife);
            if (monthlyDepreciation.compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(FIXED_ASSET_DEPRECIATE_NEGATIVE, asset.getAssetName(),
                        formatAmount(originalValue), usefulLife, formatAmount(residualRate));
            }
            // 1.3 æœ€åŽä¸€æœŸæŒ‰å‰©ä½™å¯æŠ˜æ—§é¢å°é¡¶ï¼Œå‡€å€¼ä¸ä¼šè¢«ææˆè´Ÿæ•°
            BigDecimal periodDepreciation = monthlyDepreciation.min(remaining);
            BigDecimal newAccumulated = accumulated.add(periodDepreciation);
            updateList.add(new FinFixedAssetDO().setId(asset.getId())
                    .setAccumulatedDepreciation(newAccumulated)
                    .setNetValue(originalValue.subtract(newAccumulated).max(BigDecimal.ZERO)));
            totalMonthlyDepreciation = totalMonthlyDepreciation.add(periodDepreciation);
        }
        // 2. æ²¡æœ‰å¯è®¡æçš„资产,明确告知
        if (updateList.isEmpty()) {
            throw exception(FIXED_ASSET_DEPRECIATE_EMPTY);
        }
        // 3. æ‰¹é‡æ›´æ–°
        fixedAssetMapper.updateBatch(updateList);
        return new FinFixedAssetDepreciateRespVO().setProcessedCount(updateList.size())
                .setTotalMonthlyDepreciation(totalMonthlyDepreciation);
    }
    @Override
    public FinFixedAssetDO getFixedAsset(Long id) {
        return fixedAssetMapper.selectById(id);
    }
    @Override
    public PageResult<FinFixedAssetDO> getFixedAssetPage(FinFixedAssetPageReqVO pageReqVO) {
        return fixedAssetMapper.selectPage(pageReqVO);
    }
    @Override
    public List<FinFixedAssetDO> getFixedAssetList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return fixedAssetMapper.selectListByIds(ids);
    }
    @Override
    public List<FinFixedAssetDO> getFixedAssetList() {
        return fixedAssetMapper.selectList();
    }
    private FinFixedAssetDO validateFixedAssetExists(Long id) {
        if (id == null) {
            throw exception(FIXED_ASSET_NOT_EXISTS);
        }
        FinFixedAssetDO asset = fixedAssetMapper.selectById(id);
        if (asset == null) {
            throw exception(FIXED_ASSET_NOT_EXISTS);
        }
        return asset;
    }
    private void validateAssetCodeUnique(Long id, String assetCode) {
        FinFixedAssetDO asset = fixedAssetMapper.selectByAssetCode(StrUtil.trim(assetCode));
        if (asset == null) {
            return;
        }
        if (id == null || !asset.getId().equals(id)) {
            throw exception(FIXED_ASSET_CODE_DUPLICATE, assetCode);
        }
    }
    /**
     * å‡€å€¼ = åŽŸå€¼ - ç´¯è®¡æŠ˜æ—§ï¼Œä¸”不小于 0
     */
    private void fillNetValue(FinFixedAssetDO asset) {
        BigDecimal netValue = defaultIfNull(asset.getOriginalValue())
                .subtract(defaultIfNull(asset.getAccumulatedDepreciation()));
        asset.setNetValue(netValue.max(BigDecimal.ZERO));
    }
    private static BigDecimal defaultIfNull(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount;
    }
    private static String formatAmount(BigDecimal amount) {
        return amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinIntangibleAssetService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
package cn.iocoder.yudao.module.finance.service.asset;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetAmortizeRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinIntangibleAssetDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * æ— å½¢èµ„产 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface FinIntangibleAssetService {
    /**
     * åˆ›å»ºæ— å½¢èµ„产
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return èµ„产编号
     */
    Long createIntangibleAsset(@Valid FinIntangibleAssetSaveReqVO createReqVO);
    /**
     * æ›´æ–°æ— å½¢èµ„产
     *
     * ä¿®æ”¹åŽŸå€¼ã€æ‘Šé”€å¹´é™ã€æ®‹å€¼çŽ‡æ—¶ä¼šæŒ‰æ–°å‚æ•°é‡ç®—ç´¯è®¡æ‘Šé”€ä¸Žå‡€å€¼
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateIntangibleAsset(@Valid FinIntangibleAssetSaveReqVO updateReqVO);
    /**
     * åˆ é™¤æ— å½¢èµ„产
     *
     * @param id èµ„产编号
     */
    void deleteIntangibleAsset(Long id);
    /**
     * æ‰¹é‡åˆ é™¤æ— å½¢èµ„产
     *
     * @param ids èµ„产编号数组
     */
    void deleteIntangibleAssetList(List<Long> ids);
    /**
     * è®¡ææ‘Šé”€
     *
     * æ¯ä¸ªèµ„产计提一个月的摊销:月摊销额 = åŽŸå€¼ Ã— (1 - æ®‹å€¼çއ / 100) / (摊销年限 Ã— 12)。
     * ç´¯è®¡æ‘Šé”€ä»¥åº”摊销总额封顶,最后一期按剩余可摊销额计提;
     * å‡€å€¼æ‘Šå®Œï¼ˆä¸º 0)时状态自动置为「已摊销完」
     *
     * @param ids èµ„产编号数组;为空时对所有未失效的无形资产计提
     * @return è®¡æç»“æžœ
     */
    FinIntangibleAssetAmortizeRespVO amortizeIntangibleAssetList(List<Long> ids);
    /**
     * èŽ·å¾—æ— å½¢èµ„äº§
     *
     * @param id èµ„产编号
     * @return æ— å½¢èµ„产
     */
    FinIntangibleAssetDO getIntangibleAsset(Long id);
    /**
     * èŽ·å¾—æ— å½¢èµ„äº§åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return æ— å½¢èµ„产分页
     */
    PageResult<FinIntangibleAssetDO> getIntangibleAssetPage(FinIntangibleAssetPageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‡å®šç¼–å·çš„æ— å½¢èµ„äº§åˆ—è¡¨
     *
     * @param ids èµ„产编号数组
     * @return æ— å½¢èµ„产列表
     */
    List<FinIntangibleAssetDO> getIntangibleAssetList(Collection<Long> ids);
    /**
     * èŽ·å¾—å…¨éƒ¨æ— å½¢èµ„äº§åˆ—è¡¨
     *
     * @return æ— å½¢èµ„产列表
     */
    List<FinIntangibleAssetDO> getIntangibleAssetList();
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/asset/FinIntangibleAssetServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,215 @@
package cn.iocoder.yudao.module.finance.service.asset;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetAmortizeRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.asset.vo.FinIntangibleAssetSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinIntangibleAssetDO;
import cn.iocoder.yudao.module.finance.dal.mysql.asset.FinIntangibleAssetMapper;
import cn.iocoder.yudao.module.finance.enums.FinIntangibleAssetStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.finance.service.asset.FinAssetAmountUtils.calcDepreciableBase;
import static cn.iocoder.yudao.module.finance.service.asset.FinAssetAmountUtils.calcMonthlyAmount;
/**
 * æ— å½¢èµ„产 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class FinIntangibleAssetServiceImpl implements FinIntangibleAssetService {
    @Resource
    private FinIntangibleAssetMapper intangibleAssetMapper;
    @Override
    public Long createIntangibleAsset(FinIntangibleAssetSaveReqVO createReqVO) {
        // 1. æ ¡éªŒèµ„产编号唯一
        validateAssetCodeUnique(null, createReqVO.getAssetCode());
        // 2. æ’入:新资产尚未计提摊销
        FinIntangibleAssetDO asset = BeanUtils.toBean(createReqVO, FinIntangibleAssetDO.class);
        asset.setAssetCode(StrUtil.trim(asset.getAssetCode()));
        asset.setResidualRate(ObjectUtil.defaultIfNull(asset.getResidualRate(), BigDecimal.ZERO));
        asset.setStatus(ObjectUtil.defaultIfNull(asset.getStatus(), FinIntangibleAssetStatusEnum.IN_USE.getStatus()));
        asset.setAccumulatedAmortization(BigDecimal.ZERO);
        fillNetValue(asset);
        intangibleAssetMapper.insert(asset);
        return asset.getId();
    }
    @Override
    public void updateIntangibleAsset(FinIntangibleAssetSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        FinIntangibleAssetDO oldAsset = validateIntangibleAssetExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒèµ„产编号唯一
        validateAssetCodeUnique(updateReqVO.getId(), updateReqVO.getAssetCode());
        // 2.1 æ›´æ–°ï¼ˆç´¯è®¡æ‘Šé”€ä¸ä»Žå‰ç«¯æŽ¥æ”¶ï¼Œæ²¿ç”¨åº“中值)
        FinIntangibleAssetDO updateObj = BeanUtils.toBean(updateReqVO, FinIntangibleAssetDO.class);
        updateObj.setAssetCode(StrUtil.trim(updateObj.getAssetCode()));
        updateObj.setResidualRate(ObjectUtil.defaultIfNull(updateObj.getResidualRate(), BigDecimal.ZERO));
        updateObj.setStatus(ObjectUtil.defaultIfNull(updateObj.getStatus(), oldAsset.getStatus()));
        updateObj.setAccumulatedAmortization(
                ObjectUtil.defaultIfNull(oldAsset.getAccumulatedAmortization(), BigDecimal.ZERO));
        // 2.2 åŽŸå€¼ã€æ®‹å€¼çŽ‡å˜åŒ–åŽï¼ŒæŒ‰æ–°çš„åº”æ‘Šé”€æ€»é¢å°é¡¶ç´¯è®¡æ‘Šé”€ï¼Œå¹¶é‡ç®—å‡€å€¼
        BigDecimal amortizableBase = calcDepreciableBase(defaultIfNull(updateObj.getOriginalValue()),
                updateObj.getResidualRate());
        if (updateObj.getAccumulatedAmortization().compareTo(amortizableBase) > 0) {
            updateObj.setAccumulatedAmortization(amortizableBase);
        }
        fillNetValue(updateObj);
        // 2.3 å·²æ‘Šå®Œä½†çŠ¶æ€è¿˜åœ¨ç”¨çš„ï¼ŒåŒæ­¥ä¸ºã€Œå·²æ‘Šé”€å®Œã€
        if (updateObj.getNetValue().compareTo(BigDecimal.ZERO) <= 0
                && FinIntangibleAssetStatusEnum.IN_USE.getStatus().equals(updateObj.getStatus())) {
            updateObj.setStatus(FinIntangibleAssetStatusEnum.AMORTIZED.getStatus());
        }
        intangibleAssetMapper.updateById(updateObj);
    }
    @Override
    public void deleteIntangibleAsset(Long id) {
        validateIntangibleAssetExists(id);
        intangibleAssetMapper.deleteById(id);
    }
    @Override
    public void deleteIntangibleAssetList(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return;
        }
        intangibleAssetMapper.deleteByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FinIntangibleAssetAmortizeRespVO amortizeIntangibleAssetList(List<Long> ids) {
        // 1. æŸ¥è¯¢å¾…计提的资产:已失效的不再计提
        List<FinIntangibleAssetDO> assets = CollUtil.isEmpty(ids)
                ? intangibleAssetMapper.selectList() : intangibleAssetMapper.selectListByIds(ids);
        List<FinIntangibleAssetDO> updateList = new ArrayList<>();
        BigDecimal totalMonthlyAmortization = BigDecimal.ZERO;
        for (FinIntangibleAssetDO asset : assets) {
            if (FinIntangibleAssetStatusEnum.EXPIRED.getStatus().equals(asset.getStatus())) {
                continue;
            }
            BigDecimal originalValue = defaultIfNull(asset.getOriginalValue());
            BigDecimal residualRate = defaultIfNull(asset.getResidualRate());
            int years = ObjectUtil.defaultIfNull(asset.getAmortizationPeriod(), 0);
            BigDecimal accumulated = defaultIfNull(asset.getAccumulatedAmortization());
            // 1.1 å·²æ‘Šå®Œçš„不再计提
            BigDecimal amortizableBase = calcDepreciableBase(originalValue, residualRate);
            BigDecimal remaining = amortizableBase.subtract(accumulated);
            if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            // 1.2 å‚数异常导致月摊销额非正
            BigDecimal monthlyAmortization = years <= 0 ? BigDecimal.ZERO
                    : calcMonthlyAmount(originalValue, residualRate, years);
            if (monthlyAmortization.compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(INTANGIBLE_ASSET_AMORTIZE_NEGATIVE, asset.getAssetName(),
                        formatAmount(originalValue), years, formatAmount(residualRate));
            }
            // 1.3 æœ€åŽä¸€æœŸæŒ‰å‰©ä½™å¯æ‘Šé”€é¢å°é¡¶ï¼Œå‡€å€¼ä¸ä¼šè¢«æ‘Šæˆè´Ÿæ•°
            BigDecimal periodAmortization = monthlyAmortization.min(remaining);
            BigDecimal newAccumulated = accumulated.add(periodAmortization);
            BigDecimal newNetValue = originalValue.subtract(newAccumulated).max(BigDecimal.ZERO);
            FinIntangibleAssetDO updateObj = new FinIntangibleAssetDO().setId(asset.getId())
                    .setAccumulatedAmortization(newAccumulated)
                    .setNetValue(newNetValue);
            // 1.4 æ‘Šå®Œæ—¶è‡ªåŠ¨ç½®ä¸ºã€Œå·²æ‘Šé”€å®Œã€
            if (newNetValue.compareTo(BigDecimal.ZERO) <= 0) {
                updateObj.setStatus(FinIntangibleAssetStatusEnum.AMORTIZED.getStatus());
            }
            updateList.add(updateObj);
            totalMonthlyAmortization = totalMonthlyAmortization.add(periodAmortization);
        }
        // 2. æ²¡æœ‰å¯è®¡æçš„资产,明确告知
        if (updateList.isEmpty()) {
            throw exception(INTANGIBLE_ASSET_AMORTIZE_EMPTY);
        }
        // 3. æ‰¹é‡æ›´æ–°
        intangibleAssetMapper.updateBatch(updateList);
        return new FinIntangibleAssetAmortizeRespVO().setProcessedCount(updateList.size())
                .setTotalMonthlyAmortization(totalMonthlyAmortization);
    }
    @Override
    public FinIntangibleAssetDO getIntangibleAsset(Long id) {
        return intangibleAssetMapper.selectById(id);
    }
    @Override
    public PageResult<FinIntangibleAssetDO> getIntangibleAssetPage(FinIntangibleAssetPageReqVO pageReqVO) {
        return intangibleAssetMapper.selectPage(pageReqVO);
    }
    @Override
    public List<FinIntangibleAssetDO> getIntangibleAssetList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return intangibleAssetMapper.selectListByIds(ids);
    }
    @Override
    public List<FinIntangibleAssetDO> getIntangibleAssetList() {
        return intangibleAssetMapper.selectList();
    }
    private FinIntangibleAssetDO validateIntangibleAssetExists(Long id) {
        if (id == null) {
            throw exception(INTANGIBLE_ASSET_NOT_EXISTS);
        }
        FinIntangibleAssetDO asset = intangibleAssetMapper.selectById(id);
        if (asset == null) {
            throw exception(INTANGIBLE_ASSET_NOT_EXISTS);
        }
        return asset;
    }
    private void validateAssetCodeUnique(Long id, String assetCode) {
        FinIntangibleAssetDO asset = intangibleAssetMapper.selectByAssetCode(StrUtil.trim(assetCode));
        if (asset == null) {
            return;
        }
        if (id == null || !asset.getId().equals(id)) {
            throw exception(INTANGIBLE_ASSET_CODE_DUPLICATE, assetCode);
        }
    }
    /**
     * å‡€å€¼ = åŽŸå€¼ - ç´¯è®¡æ‘Šé”€ï¼Œä¸”不小于 0
     */
    private void fillNetValue(FinIntangibleAssetDO asset) {
        BigDecimal netValue = defaultIfNull(asset.getOriginalValue())
                .subtract(defaultIfNull(asset.getAccumulatedAmortization()));
        asset.setNetValue(netValue.max(BigDecimal.ZERO));
    }
    private static BigDecimal defaultIfNull(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount;
    }
    private static String formatAmount(BigDecimal amount) {
        return amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/ledger/FinLedgerService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.finance.service.ledger;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerRespVO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * æ€»è´¦ / æ˜Žç»†è´¦ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface FinLedgerService {
    /**
     * èŽ·å¾—æ€»è´¦åˆ—è¡¨
     *
     * æŒ‰æœŸé—´é€æœˆæ±‡æ€»ï¼šæœŸåˆä½™é¢ -> å„月「本月合计」 -> æœ¬æœŸç´¯è®¡
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶
     * @return æ€»è´¦è¡Œåˆ—表
     */
    List<FinLedgerRespVO> getGeneralLedgerList(@Valid FinLedgerReqVO reqVO);
    /**
     * èŽ·å¾—æ˜Žç»†è´¦åˆ—è¡¨
     *
     * é€æ¡åˆ†å½•展示并结转余额:期初余额 -> é€æ¡æ˜Žç»†ï¼ˆæ¯é€¢æœˆæœ«æ’「本月合计」) -> æœ¬æœŸç´¯è®¡
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶
     * @return æ˜Žç»†è´¦è¡Œåˆ—表
     */
    List<FinLedgerRespVO> getDetailLedgerList(@Valid FinLedgerReqVO reqVO);
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/ledger/FinLedgerServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,227 @@
package cn.iocoder.yudao.module.finance.service.ledger;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerRespVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.ledger.FinLedgerEntryDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import cn.iocoder.yudao.module.finance.dal.mysql.ledger.FinLedgerMapper;
import cn.iocoder.yudao.module.finance.enums.FinBalanceDirectionEnum;
import cn.iocoder.yudao.module.finance.enums.FinLedgerRowTypeEnum;
import cn.iocoder.yudao.module.finance.enums.FinVoucherStatusEnum;
import cn.iocoder.yudao.module.finance.service.subject.FinAccountSubjectService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.LEDGER_MONTH_RANGE_INVALID;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.LEDGER_SUBJECT_NOT_EXISTS;
/**
 * æ€»è´¦ / æ˜Žç»†è´¦ Service å®žçŽ°ç±»
 *
 * å£å¾„说明:
 * 1. åªç»Ÿè®¡å·²è¿‡è´¦ï¼ˆposted)的凭证,未过账、已作废的凭证不计入账簿;
 * 2. ä½™é¢ä¸€å¾‹ã€Œå€Ÿæ–¹æ­£ã€è´·æ–¹è´Ÿã€ï¼ŒæœŸåˆä½™é¢ = èµ·å§‹æœˆä»½ä¹‹å‰æ‰€æœ‰å·²è¿‡è´¦åˆ†å½•的借贷净额;
 * 3. ç§‘目的下级科目由 parentId é€’归收集(不使用科目编码前缀匹配,避免把形近编码误算进来)。
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class FinLedgerServiceImpl implements FinLedgerService {
    @Resource
    private FinLedgerMapper ledgerMapper;
    @Resource
    private FinAccountSubjectService accountSubjectService;
    @Override
    public List<FinLedgerRespVO> getGeneralLedgerList(FinLedgerReqVO reqVO) {
        // 1. æ ¡éªŒç§‘目与期间
        FinAccountSubjectDO subject = validateSubject(reqVO);
        LocalDate startDate = reqVO.getStartMonth().atDay(1);
        LocalDate endDate = reqVO.getEndMonth().atEndOfMonth();
        validateMonthRange(startDate, endDate);
        Set<String> subjectCodes = buildSubjectCodeSet(subject, reqVO.getIncludeChildren());
        // 2. æœŸåˆä½™é¢
        List<FinLedgerRespVO> rows = new ArrayList<>();
        BigDecimal balance = defaultIfNull(ledgerMapper.selectBalanceBeforeDate(subjectCodes, startDate,
                FinVoucherStatusEnum.POSTED.getStatus()));
        rows.add(buildOpeningRow(subject, startDate, balance));
        // 3. æŒ‰æœˆæ±‡æ€»
        List<FinLedgerEntryDO> entries = ledgerMapper.selectEntryList(subjectCodes, startDate, endDate,
                FinVoucherStatusEnum.POSTED.getStatus());
        Map<String, List<FinLedgerEntryDO>> monthMap = entries.stream()
                .collect(Collectors.groupingBy(entry -> YearMonth.from(entry.getVoucherDate()).toString()));
        BigDecimal totalDebit = BigDecimal.ZERO;
        BigDecimal totalCredit = BigDecimal.ZERO;
        YearMonth endMonth = YearMonth.from(endDate);
        for (YearMonth month = YearMonth.from(startDate); !month.isAfter(endMonth); month = month.plusMonths(1)) {
            List<FinLedgerEntryDO> monthEntries = monthMap.getOrDefault(month.toString(), List.of());
            BigDecimal debit = sum(monthEntries, true);
            BigDecimal credit = sum(monthEntries, false);
            balance = balance.add(debit).subtract(credit);
            totalDebit = totalDebit.add(debit);
            totalCredit = totalCredit.add(credit);
            LocalDate monthEndDate = minDate(month.atEndOfMonth(), endDate);
            rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, month.toString(), monthEndDate, subject, null,
                    FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), debit, credit, balance));
        }
        // 4. æœ¬æœŸç´¯è®¡
        rows.add(buildRow(FinLedgerRowTypeEnum.YEARLY_TOTAL, null, endDate, subject, null,
                FinLedgerRowTypeEnum.YEARLY_TOTAL.getName(), totalDebit, totalCredit, balance));
        return rows;
    }
    @Override
    public List<FinLedgerRespVO> getDetailLedgerList(FinLedgerReqVO reqVO) {
        // 1. æ ¡éªŒç§‘目与期间
        FinAccountSubjectDO subject = validateSubject(reqVO);
        LocalDate startDate = reqVO.getStartMonth().atDay(1);
        LocalDate endDate = reqVO.getEndMonth().atEndOfMonth();
        validateMonthRange(startDate, endDate);
        Set<String> subjectCodes = buildSubjectCodeSet(subject, reqVO.getIncludeChildren());
        // 2. æœŸåˆä½™é¢
        List<FinLedgerRespVO> rows = new ArrayList<>();
        BigDecimal balance = defaultIfNull(ledgerMapper.selectBalanceBeforeDate(subjectCodes, startDate,
                FinVoucherStatusEnum.POSTED.getStatus()));
        rows.add(buildOpeningRow(subject, startDate, balance));
        // 3. é€æ¡æ˜Žç»†ï¼Œå¹¶å¯¹æ¯æ¡åˆ†å½•结转余额;每逢月末补一行「本月合计」
        List<FinLedgerEntryDO> entries = ledgerMapper.selectEntryList(subjectCodes, startDate, endDate,
                FinVoucherStatusEnum.POSTED.getStatus());
        BigDecimal totalDebit = BigDecimal.ZERO;
        BigDecimal totalCredit = BigDecimal.ZERO;
        BigDecimal monthDebit = BigDecimal.ZERO;
        BigDecimal monthCredit = BigDecimal.ZERO;
        YearMonth currentMonth = null;
        for (FinLedgerEntryDO entry : entries) {
            YearMonth month = YearMonth.from(entry.getVoucherDate());
            // 3.1 è·¨æœˆï¼šå…ˆè¡¥ä¸Šä¸ªæœˆçš„æœ¬æœˆåˆè®¡
            if (currentMonth != null && !currentMonth.equals(month)) {
                rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, currentMonth.toString(),
                        currentMonth.atEndOfMonth(), subject, null, FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(),
                        monthDebit, monthCredit, balance));
                monthDebit = BigDecimal.ZERO;
                monthCredit = BigDecimal.ZERO;
            }
            currentMonth = month;
            // 3.2 æ˜Žç»†è¡Œ
            BigDecimal debit = defaultIfNull(entry.getDebit());
            BigDecimal credit = defaultIfNull(entry.getCredit());
            balance = balance.add(debit).subtract(credit);
            monthDebit = monthDebit.add(debit);
            monthCredit = monthCredit.add(credit);
            totalDebit = totalDebit.add(debit);
            totalCredit = totalCredit.add(credit);
            rows.add(buildRow(FinLedgerRowTypeEnum.ENTRY, month.toString(), entry.getVoucherDate(), subject,
                    entry.getVoucherNo(), entry.getSummary(), debit, credit, balance)
                    .setSubjectCode(entry.getSubjectCode()).setSubjectName(entry.getSubjectName()));
        }
        // 4. æœ€åŽä¸€ä¸ªæœˆçš„æœ¬æœˆåˆè®¡
        if (currentMonth != null) {
            rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, currentMonth.toString(),
                    minDate(currentMonth.atEndOfMonth(), endDate), subject, null,
                    FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), monthDebit, monthCredit, balance));
        }
        // 5. æœ¬æœŸç´¯è®¡
        rows.add(buildRow(FinLedgerRowTypeEnum.YEARLY_TOTAL, null, endDate, subject, null,
                FinLedgerRowTypeEnum.YEARLY_TOTAL.getName(), totalDebit, totalCredit, balance));
        return rows;
    }
    private FinAccountSubjectDO validateSubject(FinLedgerReqVO reqVO) {
        FinAccountSubjectDO subject = accountSubjectService.getAccountSubjectByCode(reqVO.getSubjectCode());
        if (subject == null) {
            throw exception(LEDGER_SUBJECT_NOT_EXISTS, reqVO.getSubjectCode());
        }
        return subject;
    }
    private void validateMonthRange(LocalDate startDate, LocalDate endDate) {
        if (startDate.isAfter(endDate)) {
            throw exception(LEDGER_MONTH_RANGE_INVALID, YearMonth.from(startDate), YearMonth.from(endDate));
        }
    }
    /**
     * æ”¶é›†æŸ¥è¯¢ç”¨çš„科目编码集合:默认包含所有下级科目
     */
    private Set<String> buildSubjectCodeSet(FinAccountSubjectDO subject, Boolean includeChildren) {
        Set<String> subjectCodes = new HashSet<>();
        subjectCodes.add(subject.getSubjectCode());
        if (!Boolean.TRUE.equals(includeChildren)) {
            return subjectCodes;
        }
        // ç”¨ parentId é€’归收集下级,visited ç”± subjectCodes çš„ add ç»“果兼作防护,避免脏数据成环时死循环
        Map<Long, List<FinAccountSubjectDO>> childrenMap = accountSubjectService.getAccountSubjectList().stream()
                .filter(item -> item.getParentId() != null)
                .collect(Collectors.groupingBy(FinAccountSubjectDO::getParentId));
        Deque<Long> queue = new ArrayDeque<>();
        queue.add(subject.getId());
        while (!queue.isEmpty()) {
            for (FinAccountSubjectDO child : childrenMap.getOrDefault(queue.poll(), List.of())) {
                if (subjectCodes.add(child.getSubjectCode())) {
                    queue.add(child.getId());
                }
            }
        }
        return subjectCodes;
    }
    private FinLedgerRespVO buildOpeningRow(FinAccountSubjectDO subject, LocalDate date, BigDecimal balance) {
        return buildRow(FinLedgerRowTypeEnum.OPENING, YearMonth.from(date).toString(), date, subject, null,
                FinLedgerRowTypeEnum.OPENING.getName(), null, null, balance);
    }
    private FinLedgerRespVO buildRow(FinLedgerRowTypeEnum rowType, String period, LocalDate date,
                                     FinAccountSubjectDO subject, String voucherNo, String summary,
                                     BigDecimal debit, BigDecimal credit, BigDecimal balance) {
        return new FinLedgerRespVO()
                .setRowType(rowType.getType())
                .setRowTypeName(rowType.getName())
                .setPeriod(period)
                .setDate(date)
                .setVoucherNo(voucherNo)
                .setSubjectCode(subject.getSubjectCode())
                .setSubjectName(subject.getSubjectName())
                .setSummary(summary)
                .setDebit(debit)
                .setCredit(credit)
                .setDirection(FinBalanceDirectionEnum.getByBalance(balance))
                .setBalance(balance);
    }
    private static BigDecimal sum(List<FinLedgerEntryDO> entries, boolean debit) {
        return entries.stream()
                .map(entry -> debit ? defaultIfNull(entry.getDebit()) : defaultIfNull(entry.getCredit()))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    private static BigDecimal defaultIfNull(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount;
    }
    private static LocalDate minDate(LocalDate date, LocalDate maxDate) {
        return date.isAfter(maxDate) ? maxDate : date;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/statistics/FinStatisticsService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.finance.service.statistics;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsOverviewRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsTrendRespVO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * è´¢åŠ¡æŠ¥è¡¨ / ä¼šè®¡æ ¸ç®—看板 Service æŽ¥å£
 *
 * æ•°æ®æ¥æºï¼š
 * 1. æ”¶æ”¯ã€è´­é”€é‡‘额取自 ERP æ¨¡å—(收款单、付款单、销售订单、采购订单),
 *    é€šè¿‡ {@link cn.iocoder.yudao.module.erp.api.finance.ErpFinanceStatisticsApi} å–数,不跨模块查表;
 * 2. èµ„产数据取自本模块的固定资产、无形资产;
 * 3. å‡­è¯æ•°æ®å–自本模块的记账凭证。
 *
 * @author è¶…级管理员
 */
public interface FinStatisticsService {
    /**
     * èŽ·å¾—ä¼šè®¡æ ¸ç®—çœ‹æ¿æ€»è§ˆ
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶ï¼ŒæœŸé—´ç•™ç©ºæ—¶é»˜è®¤æœ¬å¹´ 1 æœˆè‡³å½“前月
     * @return çœ‹æ¿æ€»è§ˆ
     */
    FinStatisticsOverviewRespVO getStatisticsOverview(@Valid FinStatisticsReqVO reqVO);
    /**
     * èŽ·å¾—æœˆåº¦æ”¶æ”¯è¶‹åŠ¿
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶ï¼ŒæœŸé—´ç•™ç©ºæ—¶é»˜è®¤æœ¬å¹´ 1 æœˆè‡³å½“前月
     * @return æœˆåº¦è¶‹åŠ¿åˆ—è¡¨ï¼ŒæœŸé—´å†…æ²¡æœ‰æ•°æ®çš„æœˆä»½è¡¥ 0
     */
    List<FinStatisticsTrendRespVO> getStatisticsTrend(@Valid FinStatisticsReqVO reqVO);
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/statistics/FinStatisticsServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,182 @@
package cn.iocoder.yudao.module.finance.service.statistics;
import cn.iocoder.yudao.module.erp.api.finance.ErpFinanceStatisticsApi;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinanceMonthlyStatisticsRespDTO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsOverviewRespVO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.statistics.vo.FinStatisticsTrendRespVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinFixedAssetDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.asset.FinIntangibleAssetDO;
import cn.iocoder.yudao.module.finance.dal.mysql.subject.FinAccountSubjectMapper;
import cn.iocoder.yudao.module.finance.dal.mysql.voucher.FinVoucherMapper;
import cn.iocoder.yudao.module.finance.enums.FinVoucherStatusEnum;
import cn.iocoder.yudao.module.finance.service.asset.FinFixedAssetService;
import cn.iocoder.yudao.module.finance.service.asset.FinIntangibleAssetService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.STATISTICS_PERIOD_INVALID;
/**
 * è´¢åŠ¡æŠ¥è¡¨ / ä¼šè®¡æ ¸ç®—看板 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class FinStatisticsServiceImpl implements FinStatisticsService {
    @Resource
    private ErpFinanceStatisticsApi erpFinanceStatisticsApi;
    @Resource
    private FinVoucherMapper voucherMapper;
    @Resource
    private FinAccountSubjectMapper accountSubjectMapper;
    @Resource
    private FinFixedAssetService fixedAssetService;
    @Resource
    private FinIntangibleAssetService intangibleAssetService;
    @Override
    public FinStatisticsOverviewRespVO getStatisticsOverview(FinStatisticsReqVO reqVO) {
        // 1. æœŸé—´
        LocalDate startDate = resolveStartDate(reqVO);
        LocalDate endDate = resolveEndDate(reqVO);
        validatePeriod(startDate, endDate);
        // 2. ERP æ”¶æ”¯ã€è´­é”€é‡‘额
        List<ErpFinanceMonthlyStatisticsRespDTO> monthlyList = getErpMonthlyStatistics(startDate, endDate);
        BigDecimal receiptPrice = sumOf(monthlyList, ErpFinanceMonthlyStatisticsRespDTO::getReceiptPrice);
        BigDecimal paymentPrice = sumOf(monthlyList, ErpFinanceMonthlyStatisticsRespDTO::getPaymentPrice);
        BigDecimal saleOrderPrice = sumOf(monthlyList, ErpFinanceMonthlyStatisticsRespDTO::getSaleOrderPrice);
        BigDecimal purchaseOrderPrice = sumOf(monthlyList, ErpFinanceMonthlyStatisticsRespDTO::getPurchaseOrderPrice);
        // 3. å›ºå®šèµ„产、无形资产(时点数,取全部资产)
        BigDecimal fixedAssetOriginalValue = BigDecimal.ZERO;
        BigDecimal fixedAssetAccumulatedDepreciation = BigDecimal.ZERO;
        BigDecimal fixedAssetNetValue = BigDecimal.ZERO;
        for (FinFixedAssetDO asset : fixedAssetService.getFixedAssetList()) {
            fixedAssetOriginalValue = fixedAssetOriginalValue.add(defaultIfNull(asset.getOriginalValue()));
            fixedAssetAccumulatedDepreciation = fixedAssetAccumulatedDepreciation
                    .add(defaultIfNull(asset.getAccumulatedDepreciation()));
            fixedAssetNetValue = fixedAssetNetValue.add(defaultIfNull(asset.getNetValue()));
        }
        BigDecimal intangibleAssetOriginalValue = BigDecimal.ZERO;
        BigDecimal intangibleAssetAccumulatedAmortization = BigDecimal.ZERO;
        BigDecimal intangibleAssetNetValue = BigDecimal.ZERO;
        for (FinIntangibleAssetDO asset : intangibleAssetService.getIntangibleAssetList()) {
            intangibleAssetOriginalValue = intangibleAssetOriginalValue.add(defaultIfNull(asset.getOriginalValue()));
            intangibleAssetAccumulatedAmortization = intangibleAssetAccumulatedAmortization
                    .add(defaultIfNull(asset.getAccumulatedAmortization()));
            intangibleAssetNetValue = intangibleAssetNetValue.add(defaultIfNull(asset.getNetValue()));
        }
        // 4. å‡­è¯ç»Ÿè®¡
        Long voucherUnpostedCount = defaultIfNull(voucherMapper.selectCountByStatus(FinVoucherStatusEnum.UNPOSTED.getStatus()));
        Long voucherPostedCount = defaultIfNull(voucherMapper.selectCountByStatus(FinVoucherStatusEnum.POSTED.getStatus()));
        Long voucherCancelledCount = defaultIfNull(voucherMapper.selectCountByStatus(FinVoucherStatusEnum.CANCELLED.getStatus()));
        // 5. ç»„装
        return new FinStatisticsOverviewRespVO()
                .setStartDate(startDate).setEndDate(endDate)
                .setReceiptPrice(receiptPrice)
                .setPaymentPrice(paymentPrice)
                .setNetProfit(receiptPrice.subtract(paymentPrice))
                .setSaleOrderPrice(saleOrderPrice)
                .setPurchaseOrderPrice(purchaseOrderPrice)
                .setFixedAssetOriginalValue(fixedAssetOriginalValue)
                .setFixedAssetAccumulatedDepreciation(fixedAssetAccumulatedDepreciation)
                .setFixedAssetNetValue(fixedAssetNetValue)
                .setIntangibleAssetOriginalValue(intangibleAssetOriginalValue)
                .setIntangibleAssetAccumulatedAmortization(intangibleAssetAccumulatedAmortization)
                .setIntangibleAssetNetValue(intangibleAssetNetValue)
                .setTotalAssetNetValue(fixedAssetNetValue.add(intangibleAssetNetValue))
                .setSubjectCount(defaultIfNull(accountSubjectMapper.selectCount()))
                .setVoucherCount(voucherUnpostedCount + voucherPostedCount + voucherCancelledCount)
                .setVoucherUnpostedCount(voucherUnpostedCount)
                .setVoucherPostedCount(voucherPostedCount)
                .setVoucherCancelledCount(voucherCancelledCount);
    }
    @Override
    public List<FinStatisticsTrendRespVO> getStatisticsTrend(FinStatisticsReqVO reqVO) {
        // 1. æœŸé—´
        LocalDate startDate = resolveStartDate(reqVO);
        LocalDate endDate = resolveEndDate(reqVO);
        validatePeriod(startDate, endDate);
        // 2. ERP æœˆåº¦æ•°æ®è½¬ Map,便于按月份补 0
        Map<String, ErpFinanceMonthlyStatisticsRespDTO> monthMap = convertMap(
                getErpMonthlyStatistics(startDate, endDate), ErpFinanceMonthlyStatisticsRespDTO::getMonth);
        // 3. æœŸé—´å†…逐月输出,没有数据的月份补 0
        List<FinStatisticsTrendRespVO> list = new ArrayList<>();
        YearMonth endMonth = YearMonth.from(endDate);
        for (YearMonth month = YearMonth.from(startDate); !month.isAfter(endMonth); month = month.plusMonths(1)) {
            ErpFinanceMonthlyStatisticsRespDTO dto = monthMap.get(month.toString());
            BigDecimal receiptPrice = dto == null ? BigDecimal.ZERO : defaultIfNull(dto.getReceiptPrice());
            BigDecimal paymentPrice = dto == null ? BigDecimal.ZERO : defaultIfNull(dto.getPaymentPrice());
            list.add(new FinStatisticsTrendRespVO()
                    .setMonth(month.toString())
                    .setReceiptPrice(receiptPrice)
                    .setPaymentPrice(paymentPrice)
                    .setNetProfit(receiptPrice.subtract(paymentPrice))
                    .setSaleOrderPrice(dto == null ? BigDecimal.ZERO : defaultIfNull(dto.getSaleOrderPrice()))
                    .setPurchaseOrderPrice(dto == null ? BigDecimal.ZERO : defaultIfNull(dto.getPurchaseOrderPrice())));
        }
        return list;
    }
    /**
     * è°ƒç”¨ ERP æ¨¡å— API èŽ·å–æœˆåº¦ç»Ÿè®¡ï¼Œå¹¶åšå…œåº•è§£åŒ…
     */
    private List<ErpFinanceMonthlyStatisticsRespDTO> getErpMonthlyStatistics(LocalDate startDate, LocalDate endDate) {
        List<ErpFinanceMonthlyStatisticsRespDTO> list = erpFinanceStatisticsApi
                .getMonthlyStatistics(startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay())
                .getCheckedData();
        return list == null ? List.of() : list;
    }
    private LocalDate resolveStartDate(FinStatisticsReqVO reqVO) {
        return reqVO.getStartMonth() != null ? reqVO.getStartMonth().atDay(1)
                : LocalDate.now().withDayOfYear(1);
    }
    private LocalDate resolveEndDate(FinStatisticsReqVO reqVO) {
        return reqVO.getEndMonth() != null ? reqVO.getEndMonth().atEndOfMonth()
                : LocalDate.now();
    }
    private void validatePeriod(LocalDate startDate, LocalDate endDate) {
        if (startDate.isAfter(endDate)) {
            throw exception(STATISTICS_PERIOD_INVALID, YearMonth.from(startDate), YearMonth.from(endDate));
        }
    }
    private static BigDecimal sumOf(List<ErpFinanceMonthlyStatisticsRespDTO> list,
                                   Function<ErpFinanceMonthlyStatisticsRespDTO, BigDecimal> getter) {
        return list.stream().map(getter).map(FinStatisticsServiceImpl::defaultIfNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    private static BigDecimal defaultIfNull(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount;
    }
    private static Long defaultIfNull(Long value) {
        return value == null ? 0L : value;
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/subject/FinAccountSubjectService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
package cn.iocoder.yudao.module.finance.service.subject;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * ä¼šè®¡ç§‘ç›® Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface FinAccountSubjectService {
    /**
     * åˆ›å»ºä¼šè®¡ç§‘ç›®
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç§‘目编号
     */
    Long createAccountSubject(@Valid FinAccountSubjectSaveReqVO createReqVO);
    /**
     * æ›´æ–°ä¼šè®¡ç§‘ç›®
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateAccountSubject(@Valid FinAccountSubjectSaveReqVO updateReqVO);
    /**
     * åˆ é™¤ä¼šè®¡ç§‘ç›®
     *
     * @param id ç§‘目编号
     */
    void deleteAccountSubject(Long id);
    /**
     * æ‰¹é‡åˆ é™¤ä¼šè®¡ç§‘ç›®
     *
     * @param ids ç§‘目编号数组
     */
    void deleteAccountSubjectList(List<Long> ids);
    /**
     * èŽ·å¾—ä¼šè®¡ç§‘ç›®
     *
     * @param id ç§‘目编号
     * @return ä¼šè®¡ç§‘ç›®
     */
    FinAccountSubjectDO getAccountSubject(Long id);
    /**
     * èŽ·å¾—ä¼šè®¡ç§‘ç›®åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ä¼šè®¡ç§‘目分页
     */
    PageResult<FinAccountSubjectDO> getAccountSubjectPage(FinAccountSubjectPageReqVO pageReqVO);
    /**
     * èŽ·å¾—ä¼šè®¡ç§‘ç›®åˆ—è¡¨ï¼ˆå…¨é‡ï¼ŒæŒ‰ç§‘ç›®ç¼–ç å‡åºï¼Œå‰ç«¯æ®æ­¤æž„å»ºç§‘ç›®æ ‘ï¼‰
     *
     * @return ä¼šè®¡ç§‘目列表
     */
    List<FinAccountSubjectDO> getAccountSubjectList();
    /**
     * èŽ·å¾—æŒ‡å®šç§‘ç›®ç¼–ç çš„ä¼šè®¡ç§‘ç›®ï¼Œä¸å­˜åœ¨æ—¶è¿”å›ž null
     *
     * @param subjectCode ç§‘目编码
     * @return ä¼šè®¡ç§‘ç›®
     */
    FinAccountSubjectDO getAccountSubjectByCode(String subjectCode);
    /**
     * èŽ·å¾—æŒ‡å®šç§‘ç›®ç¼–ç çš„ä¼šè®¡ç§‘ç›® Map
     *
     * @param subjectCodes ç§‘目编码数组
     * @return ä¼šè®¡ç§‘ç›® Map
     */
    default Map<String, FinAccountSubjectDO> getAccountSubjectMap(Collection<String> subjectCodes) {
        List<FinAccountSubjectDO> list = getAccountSubjectListByCodes(subjectCodes);
        return convertMap(list, FinAccountSubjectDO::getSubjectCode);
    }
    /**
     * èŽ·å¾—æŒ‡å®šç§‘ç›®ç¼–ç çš„ä¼šè®¡ç§‘ç›®åˆ—è¡¨
     *
     * @param subjectCodes ç§‘目编码数组
     * @return ä¼šè®¡ç§‘目列表
     */
    List<FinAccountSubjectDO> getAccountSubjectListByCodes(Collection<String> subjectCodes);
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/subject/FinAccountSubjectServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package cn.iocoder.yudao.module.finance.service.subject;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.subject.vo.FinAccountSubjectSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import cn.iocoder.yudao.module.finance.dal.mysql.subject.FinAccountSubjectMapper;
import cn.iocoder.yudao.module.finance.dal.mysql.voucher.FinVoucherEntryMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.*;
/**
 * ä¼šè®¡ç§‘ç›® Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class FinAccountSubjectServiceImpl implements FinAccountSubjectService {
    @Resource
    private FinAccountSubjectMapper accountSubjectMapper;
    @Resource
    private FinVoucherEntryMapper voucherEntryMapper;
    @Override
    public Long createAccountSubject(FinAccountSubjectSaveReqVO createReqVO) {
        // 1. æ ¡éªŒç§‘目编码唯一
        validateSubjectCodeUnique(null, createReqVO.getSubjectCode());
        // 2. æ ¡éªŒä¸Šçº§ç§‘ç›®
        validateParent(createReqVO.getParentId(), null);
        // 3. æ’å…¥
        FinAccountSubjectDO subject = BeanUtils.toBean(createReqVO, FinAccountSubjectDO.class);
        accountSubjectMapper.insert(subject);
        return subject.getId();
    }
    @Override
    public void updateAccountSubject(FinAccountSubjectSaveReqVO updateReqVO) {
        // 1. æ ¡éªŒå­˜åœ¨
        validateAccountSubjectExists(updateReqVO.getId());
        // 2. æ ¡éªŒç§‘目编码唯一
        validateSubjectCodeUnique(updateReqVO.getId(), updateReqVO.getSubjectCode());
        // 3. æ ¡éªŒä¸Šçº§ç§‘目(不能选自己,也不能选自己的下级)
        validateParent(updateReqVO.getParentId(), updateReqVO.getId());
        // 4. æ›´æ–°
        accountSubjectMapper.updateById(BeanUtils.toBean(updateReqVO, FinAccountSubjectDO.class));
    }
    @Override
    public void deleteAccountSubject(Long id) {
        FinAccountSubjectDO subject = validateAccountSubjectExists(id);
        // 1. æ ¡éªŒæ˜¯å¦å­˜åœ¨ä¸‹çº§ç§‘ç›®
        List<FinAccountSubjectDO> children = accountSubjectMapper.selectListByParentId(id);
        if (!children.isEmpty()) {
            throw exception(ACCOUNT_SUBJECT_HAS_CHILDREN, subject.getSubjectName(), children.size());
        }
        // 2. æ ¡éªŒæ˜¯å¦è¢«å‡­è¯åˆ†å½•引用
        Long referencedCount = voucherEntryMapper.selectCountBySubjectCodes(List.of(subject.getSubjectCode()));
        if (referencedCount != null && referencedCount > 0) {
            throw exception(ACCOUNT_SUBJECT_REFERENCED, subject.getSubjectName(), referencedCount);
        }
        // 3. åˆ é™¤
        accountSubjectMapper.deleteById(id);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteAccountSubjectList(List<Long> ids) {
        ids.forEach(this::deleteAccountSubject);
    }
    private FinAccountSubjectDO validateAccountSubjectExists(Long id) {
        FinAccountSubjectDO subject = accountSubjectMapper.selectById(id);
        if (subject == null) {
            throw exception(ACCOUNT_SUBJECT_NOT_EXISTS);
        }
        return subject;
    }
    private void validateSubjectCodeUnique(Long id, String subjectCode) {
        FinAccountSubjectDO subject = accountSubjectMapper.selectBySubjectCode(subjectCode);
        if (subject == null) {
            return;
        }
        if (id == null || !Objects.equals(subject.getId(), id)) {
            throw exception(ACCOUNT_SUBJECT_CODE_DUPLICATE, subjectCode);
        }
    }
    /**
     * æ ¡éªŒä¸Šçº§ç§‘目:非空时必须存在,且不能是自己或自己的下级(避免形成环)
     *
     * @param parentId ä¸Šçº§ç§‘目编号
     * @param selfId   å½“前科目编号;新增时传 null
     */
    private void validateParent(Long parentId, Long selfId) {
        if (parentId == null) {
            return;
        }
        if (Objects.equals(parentId, selfId)) {
            throw exception(ACCOUNT_SUBJECT_PARENT_SELF);
        }
        FinAccountSubjectDO parent = accountSubjectMapper.selectById(parentId);
        if (parent == null) {
            throw exception(ACCOUNT_SUBJECT_PARENT_NOT_EXISTS, parentId);
        }
        // è‡ªä¸‹è€Œä¸Šå›žæº¯ï¼Œè‹¥é‡åˆ°è‡ªå·±ï¼Œè¯´æ˜Žé€‰æ‹©çš„上级是自己的下级
        Set<Long> visited = new HashSet<>();
        Long cursor = parentId;
        while (cursor != null && visited.add(cursor)) {
            if (Objects.equals(cursor, selfId)) {
                throw exception(ACCOUNT_SUBJECT_PARENT_CYCLE, parent.getSubjectName());
            }
            FinAccountSubjectDO node = accountSubjectMapper.selectById(cursor);
            cursor = node == null ? null : node.getParentId();
        }
    }
    @Override
    public FinAccountSubjectDO getAccountSubject(Long id) {
        return accountSubjectMapper.selectById(id);
    }
    @Override
    public PageResult<FinAccountSubjectDO> getAccountSubjectPage(FinAccountSubjectPageReqVO pageReqVO) {
        return accountSubjectMapper.selectPage(pageReqVO);
    }
    @Override
    public List<FinAccountSubjectDO> getAccountSubjectList() {
        return accountSubjectMapper.selectListAll();
    }
    @Override
    public FinAccountSubjectDO getAccountSubjectByCode(String subjectCode) {
        return accountSubjectMapper.selectBySubjectCode(subjectCode);
    }
    @Override
    public List<FinAccountSubjectDO> getAccountSubjectListByCodes(Collection<String> subjectCodes) {
        return accountSubjectMapper.selectListBySubjectCodes(subjectCodes);
    }
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/voucher/FinVoucherService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
package cn.iocoder.yudao.module.finance.service.voucher;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherEntryDO;
import jakarta.validation.Valid;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
/**
 * è®°è´¦å‡­è¯ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface FinVoucherService {
    /**
     * åˆ›å»ºè®°è´¦å‡­è¯
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return å‡­è¯ç¼–号
     */
    Long createVoucher(@Valid FinVoucherSaveReqVO createReqVO);
    /**
     * æ›´æ–°è®°è´¦å‡­è¯
     *
     * ä»…未过账的凭证允许修改;修改时会整表重建凭证分录
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateVoucher(@Valid FinVoucherSaveReqVO updateReqVO);
    /**
     * åˆ é™¤è®°è´¦å‡­è¯
     *
     * @param id å‡­è¯ç¼–号
     */
    void deleteVoucher(Long id);
    /**
     * æ‰¹é‡åˆ é™¤è®°è´¦å‡­è¯
     *
     * @param ids å‡­è¯ç¼–号数组
     */
    void deleteVoucherList(List<Long> ids);
    /**
     * å‡­è¯è¿‡è´¦ï¼šæœªè¿‡è´¦ -> å·²è¿‡è´¦
     *
     * @param id å‡­è¯ç¼–号
     */
    void postVoucher(Long id);
    /**
     * å‡­è¯ä½œåºŸï¼šæœªè¿‡è´¦ -> å·²ä½œåºŸ
     *
     * @param id å‡­è¯ç¼–号
     */
    void cancelVoucher(Long id);
    /**
     * èŽ·å¾—è®°è´¦å‡­è¯
     *
     * @param id å‡­è¯ç¼–号
     * @return è®°è´¦å‡­è¯
     */
    FinVoucherDO getVoucher(Long id);
    /**
     * èŽ·å¾—è®°è´¦å‡­è¯åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return è®°è´¦å‡­è¯åˆ†é¡µ
     */
    PageResult<FinVoucherDO> getVoucherPage(FinVoucherPageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‡å®šç¼–å·çš„è®°è´¦å‡­è¯åˆ—è¡¨
     *
     * @param ids å‡­è¯ç¼–号数组
     * @return è®°è´¦å‡­è¯åˆ—表
     */
    List<FinVoucherDO> getVoucherList(Collection<Long> ids);
    /**
     * èŽ·å¾—æŒ‡å®šæ—¥æœŸåŒºé—´çš„è®°è´¦å‡­è¯åˆ—è¡¨
     *
     * @param startDate å¼€å§‹æ—¥æœŸï¼ˆå«ï¼‰
     * @param endDate   ç»“束日期(含)
     * @return è®°è´¦å‡­è¯åˆ—表
     */
    List<FinVoucherDO> getVoucherListByDateBetween(LocalDate startDate, LocalDate endDate);
    /**
     * èŽ·å¾—æŒ‡å®šå‡­è¯çš„åˆ†å½•åˆ—è¡¨ï¼ˆæŒ‰è¡Œå·å‡åºï¼‰
     *
     * @param voucherId å‡­è¯ç¼–号
     * @return åˆ†å½•列表
     */
    List<FinVoucherEntryDO> getVoucherEntryListByVoucherId(Long voucherId);
    /**
     * èŽ·å¾—æŒ‡å®šå‡­è¯çš„åˆ†å½• Map(key ä¸ºå‡­è¯ç¼–号)
     *
     * @param voucherIds å‡­è¯ç¼–号数组
     * @return åˆ†å½• Map
     */
    default Map<Long, List<FinVoucherEntryDO>> getVoucherEntryMap(Collection<Long> voucherIds) {
        List<FinVoucherEntryDO> list = getVoucherEntryListByVoucherIds(voucherIds);
        return convertMultiMap(list, FinVoucherEntryDO::getVoucherId);
    }
    /**
     * èŽ·å¾—æŒ‡å®šå‡­è¯çš„åˆ†å½•åˆ—è¡¨
     *
     * @param voucherIds å‡­è¯ç¼–号数组
     * @return åˆ†å½•列表
     */
    List<FinVoucherEntryDO> getVoucherEntryListByVoucherIds(Collection<Long> voucherIds);
}
yudao-module-finance/src/main/java/cn/iocoder/yudao/module/finance/service/voucher/FinVoucherServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,389 @@
package cn.iocoder.yudao.module.finance.service.voucher;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherEntrySaveReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherPageReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherSaveReqVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherEntryDO;
import cn.iocoder.yudao.module.finance.dal.mysql.voucher.FinVoucherEntryMapper;
import cn.iocoder.yudao.module.finance.dal.mysql.voucher.FinVoucherMapper;
import cn.iocoder.yudao.module.finance.dal.redis.no.FinNoRedisDAO;
import cn.iocoder.yudao.module.finance.enums.FinVoucherStatusEnum;
import cn.iocoder.yudao.module.finance.service.subject.FinAccountSubjectService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.*;
/**
 * è®°è´¦å‡­è¯ Service å®žçŽ°ç±»
 *
 * å€Ÿè´·è®°è´¦è§„则:有借必有贷、借贷必相等
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class FinVoucherServiceImpl implements FinVoucherService {
    /**
     * é™„件:应用用途
     */
    private static final String ATTACHMENT_APPLICATION = "file";
    /**
     * é™„件:业务记录类型
     */
    private static final String ATTACHMENT_RECORD_TYPE = "fin_voucher";
    @Resource
    private FinVoucherMapper voucherMapper;
    @Resource
    private FinVoucherEntryMapper voucherEntryMapper;
    @Resource
    private FinAccountSubjectService accountSubjectService;
    @Resource
    private FinNoRedisDAO noRedisDAO;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createVoucher(FinVoucherSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒåˆ†å½•,并得到规范化后的分录明细
        List<FinVoucherEntryDO> entries = buildVoucherEntries(createReqVO.getEntries(), createReqVO.getSummary());
        // 1.2 å‡­è¯å­—号:留空时自动生成,并校验唯一
        String voucherNo = StrUtil.isBlank(createReqVO.getVoucherNo())
                ? noRedisDAO.generate(FinNoRedisDAO.VOUCHER_NO_PREFIX) : StrUtil.trim(createReqVO.getVoucherNo());
        validateVoucherNoUnique(null, voucherNo);
        // 2.1 æ’入凭证
        FinVoucherDO voucher = BeanUtils.toBean(createReqVO, FinVoucherDO.class);
        voucher.setVoucherNo(voucherNo);
        voucher.setStatus(FinVoucherStatusEnum.UNPOSTED.getStatus());
        voucher.setDebit(sumAmount(entries, true));
        voucher.setCredit(sumAmount(entries, false));
        voucher.setAttachmentCount(ObjectUtil.defaultIfNull(createReqVO.getAttachmentCount(), 0));
        voucherMapper.insert(voucher);
        // 2.2 æ’入分录
        insertVoucherEntryList(voucher.getId(), entries);
        // 2.3 ç»‘定附件
        storageAttachmentApi.bindAttachments(ATTACHMENT_APPLICATION, ATTACHMENT_RECORD_TYPE,
                voucher.getId(), createReqVO.getBlobIds());
        return voucher.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateVoucher(FinVoucherSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        FinVoucherDO voucher = validateVoucherExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒçŠ¶æ€ï¼šåªæœ‰æœªè¿‡è´¦çš„å‡­è¯å…è®¸ä¿®æ”¹
        validateVoucherUnposted(voucher);
        // 1.3 æ ¡éªŒåˆ†å½•,并得到规范化后的分录明细
        List<FinVoucherEntryDO> entries = buildVoucherEntries(updateReqVO.getEntries(), updateReqVO.getSummary());
        // 1.4 å‡­è¯å­—号:留空时沿用原字号,填写了则校验唯一
        String voucherNo = StrUtil.isBlank(updateReqVO.getVoucherNo())
                ? voucher.getVoucherNo() : StrUtil.trim(updateReqVO.getVoucherNo());
        validateVoucherNoUnique(voucher.getId(), voucherNo);
        // 2.1 æ›´æ–°å‡­è¯ï¼ˆçŠ¶æ€ã€åˆ¶å•äººä¸ç”±å‰ç«¯æ”¹åŠ¨ï¼‰
        FinVoucherDO updateObj = BeanUtils.toBean(updateReqVO, FinVoucherDO.class);
        updateObj.setId(voucher.getId());
        updateObj.setVoucherNo(voucherNo);
        updateObj.setDebit(sumAmount(entries, true));
        updateObj.setCredit(sumAmount(entries, false));
        updateObj.setAttachmentCount(ObjectUtil.defaultIfNull(updateReqVO.getAttachmentCount(), 0));
        voucherMapper.updateById(updateObj);
        // 2.2 æ•´è¡¨é‡å»ºåˆ†å½•
        voucherEntryMapper.deleteByVoucherId(voucher.getId());
        insertVoucherEntryList(voucher.getId(), entries);
        // 2.3 æ›´æ–°é™„ä»¶
        storageAttachmentApi.updateAttachments(ATTACHMENT_APPLICATION, ATTACHMENT_RECORD_TYPE,
                voucher.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteVoucher(Long id) {
        FinVoucherDO voucher = validateVoucherExists(id);
        // åªæœ‰æœªè¿‡è´¦ã€å·²ä½œåºŸçš„凭证允许删除
        validateVoucherDeletable(voucher);
        deleteVoucherList0(Collections.singletonList(id));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteVoucherList(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return;
        }
        // æ ¡éªŒçŠ¶æ€ï¼šåªè¦å­˜åœ¨å·²è¿‡è´¦çš„å‡­è¯ï¼Œæ•´æ‰¹æ‹’ç»ï¼Œé¿å…éƒ¨åˆ†æˆåŠŸ
        voucherMapper.selectListByIds(ids).forEach(this::validateVoucherDeletable);
        deleteVoucherList0(ids);
    }
    private void deleteVoucherList0(List<Long> ids) {
        voucherMapper.deleteByIds(ids);
        voucherEntryMapper.deleteByVoucherIds(ids);
        // åˆ é™¤é™„件关联
        ids.forEach(id -> storageAttachmentApi.deleteAttachmentsByRecord(ATTACHMENT_RECORD_TYPE, id));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void postVoucher(Long id) {
        FinVoucherDO voucher = validateVoucherExists(id);
        validateVoucherUnposted(voucher);
        voucherMapper.updateById(new FinVoucherDO().setId(id)
                .setStatus(FinVoucherStatusEnum.POSTED.getStatus()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelVoucher(Long id) {
        FinVoucherDO voucher = validateVoucherExists(id);
        validateVoucherCancellable(voucher);
        voucherMapper.updateById(new FinVoucherDO().setId(id)
                .setStatus(FinVoucherStatusEnum.CANCELLED.getStatus()));
    }
    @Override
    public FinVoucherDO getVoucher(Long id) {
        return voucherMapper.selectById(id);
    }
    @Override
    public PageResult<FinVoucherDO> getVoucherPage(FinVoucherPageReqVO pageReqVO) {
        // ã€Œåˆ¶å•人」搜索框里填的是昵称(列表展示的就是昵称),先解析成用户编号再过滤
        Set<String> creatorIds = resolveCreatorIds(pageReqVO.getCreator());
        if (creatorIds != null && creatorIds.isEmpty()) {
            // æœç´¢è¯æ²¡åŒ¹é…åˆ°ä»»ä½•用户:直接返回空页。
            // ä¸èƒ½æŠŠç©ºé›†åˆå½“成「不过滤」传下去,否则用户搜一个不存在的昵称会看到全部凭证,像是筛选失效
            return new PageResult<>(Collections.emptyList(), 0L);
        }
        return voucherMapper.selectPage(pageReqVO, creatorIds);
    }
    /**
     * æŠŠã€Œåˆ¶å•人」搜索词解析成用户编号集合。
     *
     * æ”¯æŒä¸‰ç§è¾“入:昵称、账号(都是模糊匹配)、纯数字的用户编号(兼容这个搜索框以前按编号模糊匹配的老习惯)。
     * æ˜µç§°æ˜¯åˆ—表展示的字段,账号是搜索框上写的提示词,所以两者都要认。
     *
     * @param creatorKeyword æœç´¢è¯
     * @return null è¡¨ç¤ºæœç´¢è¯ä¸ºç©ºã€ä¸æŒ‰åˆ¶å•人过滤;空集合表示搜索词没匹配到任何用户
     */
    private Set<String> resolveCreatorIds(String creatorKeyword) {
        if (StrUtil.isBlank(creatorKeyword)) {
            return null;
        }
        String keyword = creatorKeyword.trim();
        Set<Long> userIds = convertSet(adminUserApi.getUserListByNickname(keyword), AdminUserRespDTO::getId);
        userIds.addAll(convertSet(adminUserApi.getUserListByUsername(keyword), AdminUserRespDTO::getId));
        try {
            userIds.add(Long.parseLong(keyword));
        } catch (NumberFormatException ignored) {
            // ä¸æ˜¯çº¯æ•°å­—,只按昵称、账号匹配
        }
        return convertSet(userIds, String::valueOf);
    }
    @Override
    public List<FinVoucherDO> getVoucherList(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyList();
        }
        return voucherMapper.selectListByIds(ids);
    }
    @Override
    public List<FinVoucherDO> getVoucherListByDateBetween(LocalDate startDate, LocalDate endDate) {
        return voucherMapper.selectListByDateBetween(startDate, endDate);
    }
    @Override
    public List<FinVoucherEntryDO> getVoucherEntryListByVoucherId(Long voucherId) {
        return voucherEntryMapper.selectListByVoucherId(voucherId);
    }
    @Override
    public List<FinVoucherEntryDO> getVoucherEntryListByVoucherIds(Collection<Long> voucherIds) {
        if (voucherIds == null || voucherIds.isEmpty()) {
            return Collections.emptyList();
        }
        return voucherEntryMapper.selectListByVoucherIds(voucherIds);
    }
    private FinVoucherDO validateVoucherExists(Long id) {
        if (id == null) {
            throw exception(VOUCHER_NOT_EXISTS);
        }
        FinVoucherDO voucher = voucherMapper.selectById(id);
        if (voucher == null) {
            throw exception(VOUCHER_NOT_EXISTS);
        }
        return voucher;
    }
    private void validateVoucherNoUnique(Long id, String voucherNo) {
        FinVoucherDO voucher = voucherMapper.selectByVoucherNo(voucherNo);
        if (voucher == null) {
            return;
        }
        if (id == null || !voucher.getId().equals(id)) {
            throw exception(VOUCHER_NO_DUPLICATE, voucherNo);
        }
    }
    private void validateVoucherUnposted(FinVoucherDO voucher) {
        if (!FinVoucherStatusEnum.UNPOSTED.getStatus().equals(voucher.getStatus())) {
            throw exception(VOUCHER_STATUS_NOT_UNPOSTED, voucher.getVoucherNo(),
                    FinVoucherStatusEnum.getName(voucher.getStatus()));
        }
    }
    private void validateVoucherCancellable(FinVoucherDO voucher) {
        if (FinVoucherStatusEnum.UNPOSTED.getStatus().equals(voucher.getStatus())
                || FinVoucherStatusEnum.POSTED.getStatus().equals(voucher.getStatus())) {
            return;
        }
        throw exception(VOUCHER_STATUS_NOT_CANCELLABLE, voucher.getVoucherNo(),
                FinVoucherStatusEnum.getName(voucher.getStatus()));
    }
    private void validateVoucherDeletable(FinVoucherDO voucher) {
        if (FinVoucherStatusEnum.UNPOSTED.getStatus().equals(voucher.getStatus())
                || FinVoucherStatusEnum.CANCELLED.getStatus().equals(voucher.getStatus())) {
            return;
        }
        throw exception(VOUCHER_DELETE_NOT_ALLOWED, voucher.getVoucherNo(),
                FinVoucherStatusEnum.getName(voucher.getStatus()));
    }
    /**
     * æ ¡éªŒå‡­è¯åˆ†å½•,并转换为待落库的分录列表
     *
     * æ ¡éªŒé¡¹ï¼šâ‘  æ•´è¡Œç©ºç™½å¿½ç•¥ â‘¡ æœ‰é‡‘额必须有科目 â‘¢ æœ‰ç§‘目必须有金额
     * â‘£ å•行借贷不得同时大于 0 â‘¤ ç§‘目必须存在 â‘¥ å€Ÿè´·åˆè®¡ç›¸ç­‰ä¸”大于 0
     *
     * @param entryReqVOs    å‰ç«¯æäº¤çš„分录
     * @param voucherSummary å‡­è¯æ‘˜è¦ï¼Œç”¨äºŽå›žå¡«åˆ†å½•摘要
     * @return å¾…落库的分录列表(行号从 1 è¿žç»­ç¼–号)
     */
    private List<FinVoucherEntryDO> buildVoucherEntries(List<FinVoucherEntrySaveReqVO> entryReqVOs,
                                                        String voucherSummary) {
        // 1. ä¸€æ¬¡æ€§æŸ¥è¯¢æ‰€æœ‰æ¶‰åŠçš„会计科目,避免逐行查库
        Set<String> subjectCodes = convertSet(entryReqVOs,
                entry -> StrUtil.trim(entry.getSubjectCode()),
                entry -> StrUtil.isNotBlank(entry.getSubjectCode()));
        Map<String, FinAccountSubjectDO> subjectMap = accountSubjectService.getAccountSubjectMap(subjectCodes);
        // 2. é€è¡Œæ ¡éªŒ
        List<FinVoucherEntryDO> entries = new ArrayList<>(entryReqVOs.size());
        for (int i = 0; i < entryReqVOs.size(); i++) {
            int rowNo = i + 1; // æç¤ºä¸­çš„行号与前端表格行号保持一致
            FinVoucherEntrySaveReqVO entry = entryReqVOs.get(i);
            BigDecimal debit = normalizeAmount(entry.getDebit());
            BigDecimal credit = normalizeAmount(entry.getCredit());
            boolean hasAmount = isPositive(debit) || isPositive(credit);
            // 2.1 æ•´è¡Œç©ºç™½ï¼ˆæ— ç§‘目、无金额),直接忽略
            if (StrUtil.isBlank(entry.getSubjectCode())) {
                if (!hasAmount) {
                    continue;
                }
                throw exception(VOUCHER_ENTRY_SUBJECT_REQUIRED, rowNo, formatAmount(debit), formatAmount(credit));
            }
            String subjectCode = StrUtil.trim(entry.getSubjectCode());
            // 2.2 æœ‰ç§‘目但没填金额
            if (!hasAmount) {
                throw exception(VOUCHER_ENTRY_AMOUNT_REQUIRED, rowNo, subjectCode);
            }
            // 2.3 å•行借贷不得同时大于 0
            if (isPositive(debit) && isPositive(credit)) {
                throw exception(VOUCHER_ENTRY_DIRECTION_BOTH, rowNo, subjectCode,
                        formatAmount(debit), formatAmount(credit));
            }
            // 2.4 ç§‘目必须存在
            FinAccountSubjectDO subject = subjectMap.get(subjectCode);
            if (subject == null) {
                throw exception(VOUCHER_ENTRY_SUBJECT_NOT_EXISTS, rowNo, subjectCode);
            }
            // 3. ç»„装
            entries.add(FinVoucherEntryDO.builder()
                    .rowNo(entries.size() + 1)
                    .subjectCode(subjectCode)
                    .subjectName(subject.getSubjectName())
                    .summary(StrUtil.isBlank(entry.getSummary())
                            ? StrUtil.blankToDefault(voucherSummary, subject.getSubjectName()) : entry.getSummary())
                    .debit(debit)
                    .credit(credit)
                    .auxiliaryType(entry.getAuxiliaryType())
                    .auxiliaryId(entry.getAuxiliaryId())
                    .auxiliaryName(entry.getAuxiliaryName())
                    .build());
        }
        // 4. è‡³å°‘一条有效分录
        if (entries.isEmpty()) {
            throw exception(VOUCHER_ENTRY_EMPTY);
        }
        // 5. å€Ÿè´·å¹³è¡¡ï¼Œä¸”合计大于 0
        BigDecimal debitTotal = sumAmount(entries, true);
        BigDecimal creditTotal = sumAmount(entries, false);
        if (!isPositive(debitTotal) || debitTotal.compareTo(creditTotal) != 0) {
            throw exception(VOUCHER_ENTRY_UNBALANCED, formatAmount(debitTotal), formatAmount(creditTotal));
        }
        return entries;
    }
    private void insertVoucherEntryList(Long voucherId, List<FinVoucherEntryDO> entries) {
        entries.forEach(entry -> entry.setVoucherId(voucherId));
        voucherEntryMapper.insertBatch(entries);
    }
    private static BigDecimal sumAmount(List<FinVoucherEntryDO> entries, boolean debit) {
        return entries.stream()
                .map(entry -> debit ? entry.getDebit() : entry.getCredit())
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    /**
     * é‡‘额规范化:空值按 0 å¤„理,统一保留 2 ä½å°æ•°
     */
    private static BigDecimal normalizeAmount(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount.setScale(2, RoundingMode.HALF_UP);
    }
    private static boolean isPositive(BigDecimal amount) {
        return amount != null && amount.compareTo(BigDecimal.ZERO) > 0;
    }
    private static String formatAmount(BigDecimal amount) {
        return amount == null ? "0.00" : amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
    }
}
yudao-module-finance/src/main/resources/mapper/ledger/FinLedgerMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
<?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="cn.iocoder.yudao.module.finance.dal.mysql.ledger.FinLedgerMapper">
    <select id="selectEntryList"
            resultType="cn.iocoder.yudao.module.finance.dal.dataobject.ledger.FinLedgerEntryDO">
        SELECT e.voucher_id   AS voucherId,
               v.voucher_no   AS voucherNo,
               v.voucher_date AS voucherDate,
               e.row_no       AS rowNo,
               e.subject_code AS subjectCode,
               e.subject_name AS subjectName,
               e.summary      AS summary,
               e.debit        AS debit,
               e.credit       AS credit
        FROM fin_voucher_entry e
                 INNER JOIN fin_voucher v ON v.id = e.voucher_id AND v.deleted = 0
        WHERE e.deleted = 0
          AND v.status = #{status}
          AND v.voucher_date &gt;= #{startDate}
          AND v.voucher_date &lt;= #{endDate}
          AND e.subject_code IN
        <foreach collection="subjectCodes" item="subjectCode" open="(" separator="," close=")">
            #{subjectCode}
        </foreach>
        ORDER BY v.voucher_date ASC, v.id ASC, e.row_no ASC
    </select>
    <select id="selectBalanceBeforeDate" resultType="java.math.BigDecimal">
        SELECT COALESCE(SUM(e.debit - e.credit), 0)
        FROM fin_voucher_entry e
                 INNER JOIN fin_voucher v ON v.id = e.voucher_id AND v.deleted = 0
        WHERE e.deleted = 0
          AND v.status = #{status}
          AND v.voucher_date &lt; #{date}
          AND e.subject_code IN
        <foreach collection="subjectCodes" item="subjectCode" open="(" separator="," close=")">
            #{subjectCode}
        </foreach>
    </select>
</mapper>
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
@@ -81,6 +81,14 @@
    List<AdminUserRespDTO> getUserListByNickname(String nickname);
    /**
     * æ ¹æ®è´¦å·æ¨¡ç³Šæœç´¢ç”¨æˆ·
     *
     * @param username è´¦å·å…³é”®è¯
     * @return ç”¨æˆ·åˆ—表
     */
    List<AdminUserRespDTO> getUserListByUsername(String username);
    /**
     * èŽ·å¾—ç”¨æˆ· Map
     *
     * @param ids ç”¨æˆ·ç¼–号数组
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java
@@ -120,6 +120,12 @@
    }
    @Override
    public List<AdminUserRespDTO> getUserListByUsername(String username) {
        List<AdminUserDO> users = userService.getUserListByUsername(username);
        return BeanUtils.toBean(users, AdminUserRespDTO.class);
    }
    @Override
    public void validateUserList(Collection<Long> ids) {
        userService.validateUserList(ids);
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java
@@ -40,6 +40,10 @@
        return selectList(new LambdaQueryWrapperX<AdminUserDO>().like(AdminUserDO::getNickname, nickname));
    }
    default List<AdminUserDO> selectListByUsername(String username) {
        return selectList(new LambdaQueryWrapperX<AdminUserDO>().like(AdminUserDO::getUsername, username));
    }
    default List<AdminUserDO> selectListByStatus(Integer status) {
        return selectList(AdminUserDO::getStatus, status);
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java
@@ -197,6 +197,14 @@
    List<AdminUserDO> getUserListByNickname(String nickname);
    /**
     * èŽ·å¾—ç”¨æˆ·åˆ—è¡¨ï¼ŒåŸºäºŽè´¦å·æ¨¡ç³ŠåŒ¹é…
     *
     * @param username è´¦å·
     * @return ç”¨æˆ·åˆ—表
     */
    List<AdminUserDO> getUserListByUsername(String username);
    /**
     * æ‰¹é‡å¯¼å…¥ç”¨æˆ·
     *
     * @param importUsers     å¯¼å…¥ç”¨æˆ·åˆ—表
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
@@ -389,6 +389,11 @@
        return userMapper.selectListByNickname(nickname);
    }
    @Override
    public List<AdminUserDO> getUserListByUsername(String username) {
        return userMapper.selectListByUsername(username);
    }
    /**
     * èŽ·å¾—éƒ¨é—¨æ¡ä»¶ï¼šæŸ¥è¯¢æŒ‡å®šéƒ¨é—¨çš„å­éƒ¨é—¨ç¼–å·ä»¬ï¼ŒåŒ…æ‹¬è‡ªèº«
     *
yudao-server/pom.xml
@@ -101,6 +101,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- è´¢åŠ¡ç®¡ç†ç›¸å…³æ¨¡å— -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-finance</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- AI å¤§æ¨¡åž‹ç›¸å…³æ¨¡å—。默认注释,保证编译速度 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-server/src/main/resources/application-trt.yaml
@@ -65,7 +65,7 @@
      primary: master
      datasource:
        master:
          url: jdbc:mysql://172.17.0.1:10000/mom-hm?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          url: jdbc:mysql://172.17.0.1:10000/mom-trt?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: root
          password: xd@123456..