【同步】BOOT 和 CLOUD 的功能(oa)

This commit is contained in:
YunaiV
2026-09-26 23:07:52 +08:00
parent 8cedced2a6
commit 088da8165b
96 changed files with 52994 additions and 24289 deletions
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.fms.service.closing;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.YearMonth;
@@ -57,6 +58,16 @@ public interface FmsClosingPeriodService {
*/
FmsClosingOverviewRespVO getClosingOverview(FmsClosingQueryReqVO queryReqVO, Long userId);
/**
* 获得指定会计期间的损益结转余额(末级损益科目本期借贷差额绝对值之和)
*
* @param accountSetId 账套编号
* @param month 会计期间
* @param userId 用户编号
* @return 损益余额
*/
BigDecimal getProfitLossBalance(Long accountSetId, String month, Long userId);
/**
* 结账
*
@@ -198,6 +198,11 @@ public class FmsClosingPeriodServiceImpl implements FmsClosingPeriodService {
.setCanClose(canClose);
}
@Override
public BigDecimal getProfitLossBalance(Long accountSetId, String month, Long userId) {
return calculateProfitLossBalance(accountSetId, LocalDateTimeUtils.parseYearMonth(month), userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
@LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_PERIOD_CLOSE_SUB_TYPE,
@@ -53,6 +53,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_INITIALIZED;
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CODE_DUPLICATE;
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CODE_RULE_INVALID;
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CURRENCY_INVALID;
@@ -161,6 +162,9 @@ public class FmsSubjectServiceImpl implements FmsSubjectService {
accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId);
// 1.2 查询财务参数
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(createReqVO.getAccountSetId());
if (financeParameter == null) {
throw exception(ACCOUNT_SET_NOT_INITIALIZED);
}
// 1.3 校验科目编码唯一
validateSubjectCodeUnique(null, createReqVO.getAccountSetId(), createReqVO.getCode());
// 1.4 校验上级科目和科目编码规则
@@ -209,6 +213,9 @@ public class FmsSubjectServiceImpl implements FmsSubjectService {
// 1.2 查询财务参数
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(
updateReqVO.getAccountSetId());
if (financeParameter == null) {
throw exception(ACCOUNT_SET_NOT_INITIALIZED);
}
// 1.3 校验科目存在
FmsSubjectDO subject = validateSubjectExists(updateReqVO.getAccountSetId(), updateReqVO.getId());
if (ObjUtil.notEqual(subject.getParentId(), updateReqVO.getParentId())) {
@@ -416,6 +423,9 @@ public class FmsSubjectServiceImpl implements FmsSubjectService {
accountSetService.validateAccountSetWritePermission(accountSetId, userId);
// 1.3 加载导入依赖
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(accountSetId);
if (financeParameter == null) {
throw exception(ACCOUNT_SET_NOT_INITIALIZED);
}
Map<String, FmsSubjectDO> subjectMap = convertMap(
subjectMapper.selectListByAccountSetIdAndType(accountSetId, null), FmsSubjectDO::getCode);
Map<String, Long> auxiliaryTypeIdMap = auxiliaryTypeService.getAuxiliaryTypeIdMap(accountSetId);
@@ -7,8 +7,6 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO;
@@ -292,7 +290,7 @@ public class FmsBalanceSheetServiceImpl implements FmsBalanceSheetService {
}
/**
* 判断查询期间内各月的损益是否均已结转,逐月检查结账概览的损益余额
* 判断查询期间内各月的损益是否均已结转
*
* @param listReqVO 查询条件
* @param userId 用户编号
@@ -302,12 +300,11 @@ public class FmsBalanceSheetServiceImpl implements FmsBalanceSheetService {
YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth());
for (YearMonth currentMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth());
!currentMonth.isAfter(endMonth); currentMonth = currentMonth.plusMonths(1)) {
FmsClosingQueryReqVO closingQueryReqVO = new FmsClosingQueryReqVO();
closingQueryReqVO.setAccountSetId(listReqVO.getAccountSetId());
closingQueryReqVO.setMonth(currentMonth.toString());
FmsClosingOverviewRespVO overview = closingPeriodService.getClosingOverview(closingQueryReqVO, userId);
// 直接查询损益余额,避免报表校验与结账概况循环调用
BigDecimal profitLossBalance = closingPeriodService.getProfitLossBalance(
listReqVO.getAccountSetId(), currentMonth.toString(), userId);
// 当月损益余额不为零时,说明存在尚未结转的损益
if (NumberUtils.zeroIfNull(overview.getProfitLossBalance()).signum() != 0) {
if (NumberUtils.zeroIfNull(profitLossBalance).signum() != 0) {
return false;
}
}
@@ -116,6 +116,41 @@ public class FmsSubjectServiceImplTest extends BaseDbUnitTest {
assertTrue(subjects.get(1).getAuxiliaryTypeIds().isEmpty());
}
@Test
public void testCreateSubject_accountSetNotInitialized() {
// 准备参数
FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(1L, "1001", "库存现金");
// 调用,并断言异常
assertServiceException(() -> subjectService.createSubject(reqVO, 10L), ACCOUNT_SET_NOT_INITIALIZED);
// 断言未写入科目
assertEquals(0, subjectMapper.selectCount());
}
@Test
public void testUpdateSubject_accountSetNotInitialized() {
// 准备参数
FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(1L, "1001", "库存现金");
reqVO.setId(1L);
// 调用,并断言异常
assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), ACCOUNT_SET_NOT_INITIALIZED);
}
@Test
public void testImportSubjectList_accountSetNotInitialized() {
// 准备参数
FmsSubjectImportExcelVO importVO = new FmsSubjectImportExcelVO();
importVO.setCode("1001");
importVO.setName("库存现金");
// 调用,并断言异常
assertServiceException(() -> subjectService.importSubjectList(1L, Collections.singletonList(importVO), 10L),
ACCOUNT_SET_NOT_INITIALIZED);
// 断言未写入科目
assertEquals(0, subjectMapper.selectCount());
}
@Test
public void testCreateSubject_success() {
// 准备参数
@@ -21,7 +21,6 @@ import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetConfi
import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetReportMapper;
import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum;
import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService;
@@ -46,6 +45,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@@ -206,8 +206,8 @@ public class FmsBalanceSheetServiceImplTest extends BaseDbUnitTest {
FmsTrialBalanceRespVO trialBalance = new FmsTrialBalanceRespVO();
trialBalance.setBalanced(false);
when(initialBalanceService.getTrialBalance(1L, 10L)).thenReturn(trialBalance);
when(closingPeriodService.getClosingOverview(any(), eq(10L)))
.thenReturn(new FmsClosingOverviewRespVO().setProfitLossBalance(new BigDecimal("20.00")));
when(closingPeriodService.getProfitLossBalance(1L, "2026-08", 10L))
.thenReturn(new BigDecimal("20.00"));
// 调用
FmsBalanceSheetCheckRespVO result = balanceSheetService.checkBalanceSheet(buildQueryReqVO(), 10L);
@@ -241,14 +241,15 @@ public class FmsBalanceSheetServiceImplTest extends BaseDbUnitTest {
.setChildren(Collections.emptyList())));
when(initialBalanceService.getTrialBalance(1L, 10L))
.thenReturn(new FmsTrialBalanceRespVO().setBalanced(true));
when(closingPeriodService.getClosingOverview(any(), eq(10L)))
.thenReturn(new FmsClosingOverviewRespVO().setProfitLossBalance(BigDecimal.ZERO));
when(closingPeriodService.getProfitLossBalance(1L, "2026-08", 10L))
.thenReturn(BigDecimal.ZERO);
// 调用
FmsBalanceSheetCheckRespVO result = balanceSheetService.checkBalanceSheet(buildQueryReqVO(), 10L);
// 断言
assertEquals(0, result.getUnmappedSubjects().size());
assertTrue(result.getProfitLossTransferred());
}
// ========== 随机对象 ==========
@@ -0,0 +1,381 @@
package cn.iocoder.yudao.module.fms.service.report;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO;
import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO;
import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper;
import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService;
import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService;
import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService;
import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService;
import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService;
import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import javax.sql.DataSource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@Import({FmsBalanceSheetServiceImpl.class, FmsIncomeStatementServiceImpl.class,
FmsCashFlowStatementServiceImpl.class, FmsReportCommonServiceImpl.class})
public class FmsReportTemplateServiceTest extends BaseDbUnitTest {
@Resource
private DataSource dataSource;
@Resource
private FmsReportTemplateMapper reportTemplateMapper;
@Resource
private FmsBalanceSheetServiceImpl balanceSheetService;
@Resource
private FmsIncomeStatementServiceImpl incomeStatementService;
@Resource
private FmsCashFlowStatementServiceImpl cashFlowStatementService;
@MockitoBean
private FmsAccountSetService accountSetService;
@MockitoBean
private FmsSubjectService subjectService;
@MockitoBean
private FmsLedgerService ledgerService;
@MockitoBean
private FmsFinanceParameterService financeParameterService;
@MockitoBean
private FmsInitialBalanceService initialBalanceService;
@MockitoBean
private FmsClosingPeriodService closingPeriodService;
@MockitoBean
private FmsClosingSchemeService closingSchemeService;
@MockitoBean
private FmsClosingVoucherService closingVoucherService;
@MockitoBean
private FmsVoucherService voucherService;
private final List<FmsSubjectDO> subjects = new ArrayList<>();
private final List<FmsLedgerSubjectBalanceRespVO> balances = new ArrayList<>();
@BeforeEach
public void before() {
// 初始化报表模板
initializeTemplates();
// mock 账套、科目和账簿余额
FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L).setInitialized(true)
.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn(accountSet);
when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(subjects);
when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(balances);
}
@Test
public void testInitializeTemplates_repeatExecution() {
// mock 数据:模拟用户修改已有报表公式
FmsReportTemplateDO template = CollUtil.findOne(
reportTemplateMapper.selectListByType(FmsReportTypeEnum.INCOME_STATEMENT.getType()),
item -> Integer.valueOf(21).equals(item.getRowNo()));
reportTemplateMapper.updateById(new FmsReportTemplateDO().setId(template.getId()).setFormula("[\"L1\"]"));
// 调用
initializeTemplates();
// 断言
assertEquals(58, reportTemplateMapper.selectListByType(FmsReportTypeEnum.BALANCE_SHEET.getType()).size());
assertEquals(31, reportTemplateMapper.selectListByType(FmsReportTypeEnum.INCOME_STATEMENT.getType()).size());
assertEquals(25, reportTemplateMapper.selectListByType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).size());
assertEquals(21, reportTemplateMapper.selectListByType(FmsReportTypeEnum.CASH_FLOW_ADJUSTMENT.getType()).size());
assertEquals(135, reportTemplateMapper.selectCount());
assertEquals("[\"L1\"]", reportTemplateMapper.selectById(template.getId()).getFormula());
}
@Test
public void testInitializeTemplates_preserveExistingFormulas() {
// mock 数据:模拟旧版报表公式
List<FmsReportTemplateDO> templates = new ArrayList<>();
for (FmsReportTemplateDO template : reportTemplateMapper.selectList()) {
String oldFormula;
if (FmsReportTypeEnum.BALANCE_SHEET.getType().equals(template.getType()) && (template.getRowNo() == 9
|| template.getRowNo() == 24 || template.getRowNo() == 25)) {
oldFormula = template.getFormula().replace("\"operator\":\"-\"", "\"operator\":\"+\"");
} else if (FmsReportTypeEnum.INCOME_STATEMENT.getType().equals(template.getType())
&& template.getRowNo() == 21) {
oldFormula = "[\"L1-L2-L3-L11-L14-L18\"]";
} else if (FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType().equals(template.getType())
&& template.getRowNo() == 19) {
oldFormula = "[\"L14+L15-L17-L18\"]";
} else {
continue;
}
templates.add(template);
reportTemplateMapper.updateById(new FmsReportTemplateDO().setId(template.getId()).setFormula(oldFormula));
}
// 调用
initializeTemplates();
// 断言
assertEquals(5, templates.size());
for (FmsReportTemplateDO template : templates) {
assertEquals(template.getFormula(), reportTemplateMapper.selectById(template.getId()).getFormula());
}
assertEquals(135, reportTemplateMapper.selectCount());
}
@Test
public void testInitializeTemplates_preserveLegacySubjectIds() {
// mock 数据:模拟使用历史科目编号的公式
FmsReportTemplateDO template = CollUtil.findOne(
reportTemplateMapper.selectListByType(FmsReportTypeEnum.BALANCE_SHEET.getType()),
item -> Integer.valueOf(25).equals(item.getRowNo()));
String oldFormula = "[{\"operator\":\"+\",\"rules\":0,\"subjectId\":1371,"
+ "\"subjectName\":\"无形资产\",\"subjectNumber\":\"1701\"},"
+ "{\"operator\":\"+\",\"rules\":0,\"subjectId\":1372,"
+ "\"subjectName\":\"累计摊销\",\"subjectNumber\":\"1702\"}]";
reportTemplateMapper.updateById(new FmsReportTemplateDO().setId(template.getId()).setFormula(oldFormula));
// 调用
initializeTemplates();
// 断言
assertEquals(template.getFormula(), reportTemplateMapper.selectById(template.getId()).getFormula());
}
@Test
public void testGetBalanceSheet_capitalContribution() {
// mock 数据:投入资本 100 元
mockSubjectBalance("1001", 1, 0, 100, 100, 0);
mockSubjectBalance("3001", 2, 0, -100, 0, 100);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsBalanceSheetRowRespVO> balanceSheet = balanceSheetService.getBalanceSheet(reqVO, 10L);
List<FmsReportItemRespVO> cashFlow = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
// 断言
assertAssetAmount(balanceSheet, 30, 100);
FmsBalanceSheetRowRespVO total = CollUtil.findOne(balanceSheet,
row -> Integer.valueOf(53).equals(row.getLiabilityRowNo()));
assertNotNull(total);
assertAmount(100, total.getLiabilityClosingAmount());
assertItemAmount(cashFlow, 15, 100);
assertItemAmount(cashFlow, 22, 100);
}
@Test
public void testGetBalanceSheet_inventoryAndContraAccounts() {
// mock 数据:存货、固定资产、无形资产及其备抵科目
mockSubjectBalance("1403", 1, 0, 30, 30, 0);
mockSubjectBalance("1407", 2, 0, -5, 0, 5);
mockSubjectBalance("1621", 1, 0, 100, 100, 0);
mockSubjectBalance("1622", 2, 0, -20, 0, 20);
mockSubjectBalance("1701", 1, 0, 100, 100, 0);
mockSubjectBalance("1702", 2, 0, -10, 0, 10);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsBalanceSheetRowRespVO> result = balanceSheetService.getBalanceSheet(reqVO, 10L);
// 断言
assertAssetAmount(result, 9, 25);
assertAssetAmount(result, 24, 80);
assertAssetAmount(result, 25, 90);
assertAssetAmount(result, 30, 195);
}
@Test
public void testGetCashFlowStatement_creditSale() {
// mock 数据:赊销 100 元并结转利润,尚未收款
mockSubjectBalance("1001", 1, 0, 0, 0, 0);
mockSubjectBalance("1122", 1, 0, 100, 100, 0);
mockSubjectBalance("5001", 2, 0, 0, 0, 100);
mockSubjectBalance("3103", 2, 0, -100, 0, 100);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsReportItemRespVO> incomeItems = incomeStatementService.getIncomeStatement(reqVO, 10L);
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
// 断言
assertItemAmount(incomeItems, 1, 100);
assertItemAmount(result, 1, 0);
assertItemAmount(result, 6, 0);
assertItemAmount(result, 20, 0);
assertItemAmount(result, 22, 0);
}
@Test
public void testGetCashFlowStatement_purchaseInventory() {
// mock 数据:期初现金 100 元,支付 40 元购入原材料
mockSubjectBalance("1001", 1, 100, 60, 0, 40);
mockSubjectBalance("1403", 1, 0, 40, 40, 0);
mockSubjectBalance("3001", 2, -100, -100, 0, 0);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
// 断言
assertItemAmount(result, 3, 40);
assertItemAmount(result, 6, 0);
assertItemAmount(result, 20, -40);
assertItemAmount(result, 22, 60);
}
@Test
public void testGetCashFlowStatement_payWagesAndIncomeTax() {
// mock 数据:计提并支付工资 30 元、所得税 10 元,期末应付款为零
mockSubjectBalance("1001", 1, 100, 60, 0, 40);
mockSubjectBalance("3001", 2, -100, -100, 0, 0);
mockSubjectBalance("3103", 2, 0, 40, 40, 0);
mockSubjectBalance("5602", 1, 0, 0, 30, 0);
mockSubjectBalance("5801", 1, 0, 0, 10, 0);
mockSubjectBalance("2211", 2, 0, 0, 30, 30);
mockSubjectBalance("2221", 2, 0, 0, 10, 10);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
// 断言
assertItemAmount(result, 4, 30);
assertItemAmount(result, 5, 10);
assertItemAmount(result, 6, 0);
assertItemAmount(result, 20, -40);
assertItemAmount(result, 22, 60);
}
@Test
public void testGetCashFlowStatement_repayLoan() {
// mock 数据:期初现金 100 元,偿还借款 30 元
mockSubjectBalance("1001", 1, 100, 70, 0, 30);
mockSubjectBalance("2001", 2, -100, -70, 30, 0);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
// 断言
assertItemAmount(result, 16, 30);
assertItemAmount(result, 19, -30);
assertItemAmount(result, 6, 0);
assertItemAmount(result, 22, 70);
}
@Test
public void testGetIncomeStatement_investmentIncomeAndNonOperatingExpense() {
// mock 数据:投资收益 20 元,营业外支出 5 元
mockSubjectBalance("5111", 2, 0, -20, 0, 20);
mockSubjectBalance("5711", 1, 0, 5, 5, 0);
// 准备参数
FmsReportListReqVO reqVO = buildQueryReqVO();
// 调用
List<FmsReportItemRespVO> result = incomeStatementService.getIncomeStatement(reqVO, 10L);
// 断言
assertItemAmount(result, 20, 20);
assertItemAmount(result, 21, 20);
assertItemAmount(result, 24, 5);
assertItemAmount(result, 32, 15);
}
/**
* 执行报表模板升级脚本
*/
private void initializeTemplates() {
new ResourceDatabasePopulator(new FileSystemResource("../../sql/mysql/upgrade/20260926-fms-report-template.sql"))
.execute(dataSource);
}
/**
* 模拟科目余额和发生额:余额借方为正、贷方为负,发生额已扣除损益结转数据
*/
private void mockSubjectBalance(String code, int direction, int opening, int closing, int debit, int credit) {
Long id = Long.valueOf(code);
subjects.add(new FmsSubjectDO().setId(id).setAccountSetId(1L).setCode(code).setName(code)
.setLevel(1).setBalanceDirection(direction));
balances.add(new FmsLedgerSubjectBalanceRespVO().setSubjectId(id).setSubjectCode(code)
.setOpeningDebitAmount(BigDecimal.valueOf(Math.max(opening, 0)))
.setOpeningCreditAmount(BigDecimal.valueOf(Math.max(-opening, 0)))
.setEndingDebitAmount(BigDecimal.valueOf(Math.max(closing, 0)))
.setEndingCreditAmount(BigDecimal.valueOf(Math.max(-closing, 0)))
.setPeriodDebitAmount(BigDecimal.valueOf(debit)).setPeriodCreditAmount(BigDecimal.valueOf(credit))
.setYearDebitAmount(BigDecimal.valueOf(debit)).setYearCreditAmount(BigDecimal.valueOf(credit))
.setChildren(Collections.emptyList()));
}
/**
* 构造 2026 年 8 月的报表查询参数
*/
private FmsReportListReqVO buildQueryReqVO() {
FmsReportListReqVO reqVO = new FmsReportListReqVO();
reqVO.setAccountSetId(1L);
reqVO.setStartMonth("2026-08");
reqVO.setEndMonth("2026-08");
return reqVO;
}
/**
* 断言报表指定行的本期和本年金额
*/
private void assertItemAmount(List<FmsReportItemRespVO> rows, int rowNo, int expected) {
FmsReportItemRespVO item = CollUtil.findOne(rows, row -> Integer.valueOf(rowNo).equals(row.getRowNo()));
assertNotNull(item);
assertAmount(expected, item.getCurrentAmount());
assertAmount(expected, item.getYearAmount());
}
/**
* 断言资产负债表指定行的资产期末余额
*/
private void assertAssetAmount(List<FmsBalanceSheetRowRespVO> rows, int rowNo, int expected) {
FmsBalanceSheetRowRespVO item = CollUtil.findOne(rows,
row -> Integer.valueOf(rowNo).equals(row.getAssetRowNo()));
assertNotNull(item);
assertAmount(expected, item.getAssetClosingAmount());
}
/**
* 按数值比较金额,忽略 BigDecimal 的小数位数差异
*/
private void assertAmount(int expected, BigDecimal actual) {
assertNotNull(actual);
assertEquals(0, BigDecimal.valueOf(expected).compareTo(actual), "实际金额:" + actual);
}
}