package com.ruoyi.account.service.impl.financial;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
|
import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto;
|
import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
|
import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
|
import com.ruoyi.account.mapper.AccountSubjectMapper;
|
import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
|
import com.ruoyi.account.mapper.financial.FinVoucherMapper;
|
import com.ruoyi.account.pojo.AccountSubject;
|
import com.ruoyi.account.pojo.financial.FinVoucher;
|
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
|
import com.ruoyi.account.service.financial.FinVoucherService;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.utils.StringUtils;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.util.*;
|
import java.util.stream.Collectors;
|
|
/**
|
* 凭证服务实现。
|
*/
|
@Service
|
@RequiredArgsConstructor
|
public class FinVoucherServiceImpl extends ServiceImpl<FinVoucherMapper, FinVoucher> implements FinVoucherService {
|
|
private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
|
private final FinVoucherEntryMapper finVoucherEntryMapper;
|
private final AccountSubjectMapper accountSubjectMapper;
|
|
@Override
|
public IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
|
LambdaQueryWrapper<FinVoucher> wrapper = new LambdaQueryWrapper<>();
|
if (queryDto != null && StringUtils.isNotEmpty(queryDto.getVoucherNo())) {
|
wrapper.like(FinVoucher::getVoucherNo, queryDto.getVoucherNo());
|
}
|
if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCreator())) {
|
wrapper.eq(FinVoucher::getCreator, queryDto.getCreator());
|
}
|
if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
|
wrapper.eq(FinVoucher::getStatus, queryDto.getStatus());
|
}
|
if (queryDto != null && queryDto.getStartDate() != null) {
|
wrapper.ge(FinVoucher::getVoucherDate, queryDto.getStartDate());
|
}
|
if (queryDto != null && queryDto.getEndDate() != null) {
|
wrapper.le(FinVoucher::getVoucherDate, queryDto.getEndDate());
|
}
|
wrapper.orderByDesc(FinVoucher::getVoucherDate).orderByDesc(FinVoucher::getId);
|
return page(page, wrapper);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Boolean addVoucher(FinVoucherDto dto) {
|
validateVoucherBasicInfo(dto, false);
|
List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
|
|
FinVoucher voucher = new FinVoucher();
|
BeanUtils.copyProperties(dto, voucher);
|
voucher.setStatus("unposted");
|
voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
|
BigDecimal totalDebit = calculateTotalDebit(validEntries);
|
BigDecimal totalCredit = calculateTotalCredit(validEntries);
|
voucher.setDebit(totalDebit);
|
voucher.setCredit(totalCredit);
|
if (StringUtils.isEmpty(voucher.getSummary())) {
|
voucher.setSummary(findDefaultSummary(validEntries));
|
}
|
save(voucher);
|
saveEntries(voucher.getId(), validEntries);
|
return true;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Boolean updateVoucher(FinVoucherDto dto) {
|
validateVoucherBasicInfo(dto, true);
|
FinVoucher existed = getById(dto.getId());
|
if (existed == null) {
|
throw new ServiceException("修改失败,凭证不存在");
|
}
|
if (!"unposted".equals(existed.getStatus())) {
|
throw new ServiceException("仅未过账凭证允许修改");
|
}
|
List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
|
|
FinVoucher voucher = new FinVoucher();
|
BeanUtils.copyProperties(dto, voucher);
|
voucher.setStatus(existed.getStatus());
|
voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
|
BigDecimal totalDebit = calculateTotalDebit(validEntries);
|
BigDecimal totalCredit = calculateTotalCredit(validEntries);
|
voucher.setDebit(totalDebit);
|
voucher.setCredit(totalCredit);
|
if (StringUtils.isEmpty(voucher.getSummary())) {
|
voucher.setSummary(findDefaultSummary(validEntries));
|
}
|
updateById(voucher);
|
|
LambdaQueryWrapper<FinVoucherEntry> deleteWrapper = new LambdaQueryWrapper<>();
|
deleteWrapper.eq(FinVoucherEntry::getVoucherId, voucher.getId());
|
finVoucherEntryMapper.delete(deleteWrapper);
|
saveEntries(voucher.getId(), validEntries);
|
return true;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Boolean postVoucher(Long id) {
|
FinVoucher voucher = getById(id);
|
if (voucher == null) {
|
throw new ServiceException("过账失败,凭证不存在");
|
}
|
if (!"unposted".equals(voucher.getStatus())) {
|
throw new ServiceException("仅未过账凭证允许过账");
|
}
|
voucher.setStatus("posted");
|
return updateById(voucher);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Boolean cancelVoucher(Long id) {
|
FinVoucher voucher = getById(id);
|
if (voucher == null) {
|
throw new ServiceException("作废失败,凭证不存在");
|
}
|
if (!"unposted".equals(voucher.getStatus())) {
|
throw new ServiceException("仅未过账凭证允许作废");
|
}
|
voucher.setStatus("cancelled");
|
return updateById(voucher);
|
}
|
|
@Override
|
public FinVoucherDetailVo detail(Long id) {
|
FinVoucher voucher = getById(id);
|
if (voucher == null) {
|
throw new ServiceException("查询失败,凭证不存在");
|
}
|
LambdaQueryWrapper<FinVoucherEntry> wrapper = new LambdaQueryWrapper<>();
|
wrapper.eq(FinVoucherEntry::getVoucherId, id)
|
.orderByAsc(FinVoucherEntry::getRowNo)
|
.orderByAsc(FinVoucherEntry::getId);
|
List<FinVoucherEntry> entries = finVoucherEntryMapper.selectList(wrapper);
|
|
FinVoucherDetailVo vo = new FinVoucherDetailVo();
|
BeanUtils.copyProperties(voucher, vo);
|
vo.setEntries(entries);
|
return vo;
|
}
|
|
/**
|
* 校验凭证主表字段、状态字段与唯一性。
|
*/
|
private void validateVoucherBasicInfo(FinVoucherDto dto, boolean isUpdate) {
|
if (dto == null) {
|
throw new ServiceException("凭证数据不能为空");
|
}
|
if (isUpdate && dto.getId() == null) {
|
throw new ServiceException("修改失败,凭证ID不能为空");
|
}
|
if (StringUtils.isEmpty(dto.getVoucherNo())) {
|
throw new ServiceException("凭证字号不能为空");
|
}
|
if (dto.getVoucherDate() == null) {
|
throw new ServiceException("凭证日期不能为空");
|
}
|
LambdaQueryWrapper<FinVoucher> uniqueWrapper = new LambdaQueryWrapper<>();
|
uniqueWrapper.eq(FinVoucher::getVoucherNo, dto.getVoucherNo());
|
if (isUpdate) {
|
uniqueWrapper.ne(FinVoucher::getId, dto.getId());
|
}
|
if (count(uniqueWrapper) > 0) {
|
throw new ServiceException("凭证字号已存在,请勿重复提交");
|
}
|
}
|
|
/**
|
* 过滤有效分录并执行借贷平衡校验。
|
*/
|
private List<FinVoucherEntry> buildAndValidateEntries(FinVoucherDto dto) {
|
List<FinVoucherEntryDto> rawEntries = dto.getEntries();
|
if (rawEntries == null || rawEntries.isEmpty()) {
|
throw new ServiceException("分录不能为空,至少需要一条有效分录");
|
}
|
List<FinVoucherEntry> validEntries = new ArrayList<>();
|
int rowNo = 1;
|
for (FinVoucherEntryDto entryDto : rawEntries) {
|
if (entryDto == null || StringUtils.isEmpty(entryDto.getSubjectCode())) {
|
continue;
|
}
|
BigDecimal debit = defaultMoney(entryDto.getDebit());
|
BigDecimal credit = defaultMoney(entryDto.getCredit());
|
if (debit.compareTo(BigDecimal.ZERO) <= 0 && credit.compareTo(BigDecimal.ZERO) <= 0) {
|
continue;
|
}
|
if (debit.compareTo(BigDecimal.ZERO) > 0 && credit.compareTo(BigDecimal.ZERO) > 0) {
|
throw new ServiceException("分录借方和贷方不能同时大于0");
|
}
|
FinVoucherEntry entry = new FinVoucherEntry();
|
BeanUtils.copyProperties(entryDto, entry);
|
entry.setDebit(debit);
|
entry.setCredit(credit);
|
entry.setRowNo(rowNo++);
|
validEntries.add(entry);
|
}
|
if (validEntries.isEmpty()) {
|
throw new ServiceException("分录至少需要一条有效行(科目不空,且借方或贷方大于0)");
|
}
|
|
// 分录科目必须存在,避免脏科目编码入账。
|
Set<String> subjectCodes = validEntries.stream()
|
.map(FinVoucherEntry::getSubjectCode)
|
.filter(StringUtils::isNotEmpty)
|
.collect(Collectors.toSet());
|
if (subjectCodes.isEmpty()) {
|
throw new ServiceException("分录科目不能为空");
|
}
|
LambdaQueryWrapper<AccountSubject> subjectWrapper = new LambdaQueryWrapper<>();
|
subjectWrapper.in(AccountSubject::getSubjectCode, subjectCodes);
|
List<AccountSubject> subjects = accountSubjectMapper.selectList(subjectWrapper);
|
Map<String, AccountSubject> subjectMap = subjects.stream()
|
.collect(Collectors.toMap(AccountSubject::getSubjectCode, it -> it, (a, b) -> a));
|
for (FinVoucherEntry entry : validEntries) {
|
AccountSubject accountSubject = subjectMap.get(entry.getSubjectCode());
|
if (accountSubject == null) {
|
throw new ServiceException("科目编码不存在:" + entry.getSubjectCode());
|
}
|
if (StringUtils.isEmpty(entry.getSubjectName())) {
|
entry.setSubjectName(accountSubject.getSubjectName());
|
}
|
}
|
|
BigDecimal totalDebit = calculateTotalDebit(validEntries);
|
BigDecimal totalCredit = calculateTotalCredit(validEntries);
|
if (totalDebit.compareTo(BigDecimal.ZERO) <= 0 || totalCredit.compareTo(BigDecimal.ZERO) <= 0) {
|
throw new ServiceException("借贷金额必须大于0");
|
}
|
if (totalDebit.compareTo(totalCredit) != 0) {
|
throw new ServiceException("借贷不平衡,禁止保存");
|
}
|
return validEntries;
|
}
|
|
private void saveEntries(Long voucherId, List<FinVoucherEntry> entries) {
|
if (voucherId == null) {
|
throw new ServiceException("凭证ID不能为空");
|
}
|
for (FinVoucherEntry entry : entries) {
|
entry.setVoucherId(voucherId);
|
finVoucherEntryMapper.insert(entry);
|
}
|
}
|
|
private String findDefaultSummary(List<FinVoucherEntry> entries) {
|
for (FinVoucherEntry entry : entries) {
|
if (StringUtils.isNotEmpty(entry.getSummary())) {
|
return entry.getSummary();
|
}
|
}
|
return "";
|
}
|
|
private BigDecimal calculateTotalDebit(List<FinVoucherEntry> entries) {
|
BigDecimal total = BigDecimal.ZERO;
|
for (FinVoucherEntry entry : entries) {
|
total = total.add(defaultMoney(entry.getDebit()));
|
}
|
return total.setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private BigDecimal calculateTotalCredit(List<FinVoucherEntry> entries) {
|
BigDecimal total = BigDecimal.ZERO;
|
for (FinVoucherEntry entry : entries) {
|
total = total.add(defaultMoney(entry.getCredit()));
|
}
|
return total.setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private BigDecimal defaultMoney(BigDecimal value) {
|
if (value == null) {
|
return ZERO;
|
}
|
return value.setScale(2, RoundingMode.HALF_UP);
|
}
|
}
|