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;
|
}
|
|
}
|