package cn.iocoder.yudao.module.crm.service.finance;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmCustomerStatementReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmCustomerStatementRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmCustomerStatementSummaryReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmCustomerStatementSummaryRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmFinanceTrendRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmShippedNotInvoicedRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.finance.vo.CrmUnwrittenOffRespVO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableItemDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
|
import cn.iocoder.yudao.module.crm.dal.mysql.invoice.CrmInvoiceMapper;
|
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableItemMapper;
|
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
|
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
|
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
|
import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableBizTypeEnum;
|
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
|
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
|
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
|
import cn.iocoder.yudao.module.mes.api.productsales.MesWmProductSalesApi;
|
import cn.iocoder.yudao.module.mes.api.productsales.dto.MesWmProductSalesRespDTO;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
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.time.format.DateTimeFormatter;
|
import java.time.temporal.ChronoUnit;
|
import java.util.*;
|
|
/**
|
* CRM 财务对账与报表 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class CrmFinanceReportServiceImpl implements CrmFinanceReportService {
|
|
private static final Integer ERP_STATUS_APPROVE = 20;
|
private static final Integer MES_STATUS_FINISHED = 4;
|
private static final Integer AUDIT_APPROVE = CrmAuditStatusEnum.APPROVE.getStatus();
|
private static final Integer PLAN_STATUS_RECEIPTED = 20;
|
|
@Resource
|
private ErpSaleOrderApi saleOrderApi;
|
@Resource
|
private MesWmProductSalesApi productSalesApi;
|
|
@Resource
|
private CrmInvoiceMapper invoiceMapper;
|
@Resource
|
private CrmReceivableMapper receivableMapper;
|
@Resource
|
private CrmReceivableItemMapper receivableItemMapper;
|
@Resource
|
private CrmReceivablePlanMapper receivablePlanMapper;
|
@Resource
|
private CrmCustomerService customerService;
|
|
@Override
|
public CrmCustomerStatementRespVO getCustomerStatement(CrmCustomerStatementReqVO reqVO) {
|
Long customerId = reqVO.getCustomerId();
|
LocalDateTime beginTime = reqVO.getBeginTime();
|
LocalDateTime endTime = reqVO.getEndTime();
|
List<ErpSaleOrderRespDTO> orders = getStatementOrders(customerId, beginTime, endTime,
|
reqVO.getIncludeUnapprovedOrder());
|
List<CrmInvoiceDO> invoices = getStatementInvoices(customerId, beginTime, endTime);
|
List<CrmReceivableDO> receipts = getStatementReceipts(customerId, beginTime, endTime);
|
|
Map<Long, BigDecimal> shippedByOrder = getShippedAmountByOrder(orders.stream()
|
.map(ErpSaleOrderRespDTO::getId).filter(Objects::nonNull)
|
.collect(java.util.stream.Collectors.toSet()));
|
Map<Long, BigDecimal> invoicedByOrder = new HashMap<>();
|
invoices.stream().filter(invoice -> invoice.getSaleOrderId() != null)
|
.forEach(invoice -> mergeAmount(invoicedByOrder, invoice.getSaleOrderId(), zeroIfNull(invoice.getPrice())));
|
Map<Long, BigDecimal> collectedByOrder = getCollectedByOrder(receipts, customerId);
|
Map<Long, BigDecimal> collectedByInvoice = getCollectedByInvoice(receipts);
|
|
CrmCustomerStatementRespVO result = new CrmCustomerStatementRespVO();
|
result.setCustomerId(customerId);
|
result.setOrderCount(orders.size());
|
result.setOrderTotalAmount(BigDecimal.ZERO);
|
result.setShippedAmount(BigDecimal.ZERO);
|
result.setInvoicedAmount(BigDecimal.ZERO);
|
result.setCollectedAmount(BigDecimal.ZERO);
|
result.setShippedNotInvoicedAmount(BigDecimal.ZERO);
|
result.setOutstandingAmount(BigDecimal.ZERO);
|
result.setNextMonthPlanAmount(getNextMonthPlanAmount(customerId));
|
|
// 1. 销售订单流水
|
List<CrmCustomerStatementRespVO.OrderRow> orderRows = new ArrayList<>(orders.size());
|
for (ErpSaleOrderRespDTO order : orders) {
|
CrmCustomerStatementRespVO.OrderRow row = new CrmCustomerStatementRespVO.OrderRow();
|
row.setSaleOrderId(order.getId());
|
row.setSaleOrderNo(order.getNo());
|
row.setContractId(order.getContractId());
|
row.setOrderTime(order.getOrderTime());
|
row.setOrderAmount(zeroIfNull(order.getTotalPrice()));
|
row.setShippedAmount(shippedByOrder.getOrDefault(order.getId(), BigDecimal.ZERO));
|
row.setInvoicedAmount(invoicedByOrder.getOrDefault(order.getId(), BigDecimal.ZERO));
|
row.setCollectedAmount(collectedByOrder.getOrDefault(order.getId(), BigDecimal.ZERO));
|
row.setShippedNotInvoicedAmount(maxZero(row.getShippedAmount().subtract(row.getInvoicedAmount())));
|
row.setUninvoicedAmount(maxZero(row.getOrderAmount().subtract(row.getInvoicedAmount())));
|
row.setOutstandingAmount(maxZero(row.getOrderAmount().subtract(row.getCollectedAmount())));
|
row.setStatus(order.getStatus());
|
orderRows.add(row);
|
result.setOrderTotalAmount(result.getOrderTotalAmount().add(row.getOrderAmount()));
|
result.setShippedAmount(result.getShippedAmount().add(row.getShippedAmount()));
|
result.setShippedNotInvoicedAmount(result.getShippedNotInvoicedAmount().add(row.getShippedNotInvoicedAmount()));
|
}
|
result.setOrders(orderRows);
|
|
// 2. 开票流水
|
Map<Long, String> orderNoMap = new HashMap<>();
|
orders.forEach(order -> orderNoMap.put(order.getId(), order.getNo()));
|
List<CrmCustomerStatementRespVO.InvoiceRow> invoiceRows = new ArrayList<>(invoices.size());
|
for (CrmInvoiceDO invoice : invoices) {
|
CrmCustomerStatementRespVO.InvoiceRow row = new CrmCustomerStatementRespVO.InvoiceRow();
|
row.setId(invoice.getId());
|
row.setNo(invoice.getNo());
|
row.setInvoiceNo(invoice.getInvoiceNo());
|
row.setInvoiceTime(invoice.getInvoiceTime());
|
row.setInvoiceAmount(zeroIfNull(invoice.getPrice()));
|
row.setCollectedAmount(collectedByInvoice.getOrDefault(invoice.getId(), BigDecimal.ZERO));
|
row.setRemainingReceivable(maxZero(row.getInvoiceAmount().subtract(row.getCollectedAmount())));
|
row.setSaleOrderId(invoice.getSaleOrderId());
|
row.setSaleOrderNo(orderNoMap.get(invoice.getSaleOrderId()));
|
row.setStatus(invoice.getAuditStatus());
|
invoiceRows.add(row);
|
result.setInvoicedAmount(result.getInvoicedAmount().add(row.getInvoiceAmount()));
|
}
|
result.setInvoices(invoiceRows);
|
|
// 3. 回款流水
|
Map<Long, String> invoiceNoMap = new HashMap<>();
|
invoices.forEach(invoice -> invoiceNoMap.put(invoice.getId(), invoice.getInvoiceNo()));
|
List<CrmCustomerStatementRespVO.ReceiptRow> receiptRows = new ArrayList<>(receipts.size());
|
for (CrmReceivableDO receipt : receipts) {
|
CrmCustomerStatementRespVO.ReceiptRow row = new CrmCustomerStatementRespVO.ReceiptRow();
|
row.setId(receipt.getId());
|
row.setNo(receipt.getNo());
|
row.setReturnTime(receipt.getReturnTime());
|
row.setAmount(zeroIfNull(receipt.getPrice()));
|
row.setInvoiceId(receipt.getInvoiceId());
|
row.setInvoiceNo(invoiceNoMap.get(receipt.getInvoiceId()));
|
row.setStatus(receipt.getAuditStatus());
|
receiptRows.add(row);
|
result.setCollectedAmount(result.getCollectedAmount().add(row.getAmount()));
|
}
|
result.setReceipts(receiptRows);
|
|
// 未收余额 = 累计开票 - 累计回款(与供应商往来的未付余额口径一致)
|
result.setOutstandingAmount(maxZero(result.getInvoicedAmount().subtract(result.getCollectedAmount())));
|
|
CrmCustomerDO customer = customerService.getCustomer(customerId);
|
result.setCustomerName(customer == null ? null : customer.getName());
|
return result;
|
}
|
|
@Override
|
public List<CrmCustomerStatementSummaryRespVO> getCustomerSummaryList(CrmCustomerStatementSummaryReqVO reqVO) {
|
LocalDateTime beginTime = reqVO.getBeginTime();
|
LocalDateTime endTime = reqVO.getEndTime();
|
List<ErpSaleOrderRespDTO> orders = getStatementOrders(null, beginTime, endTime,
|
reqVO.getIncludeUnapprovedOrder());
|
List<CrmInvoiceDO> invoices = getStatementInvoices(null, beginTime, endTime);
|
List<CrmReceivableDO> receipts = getStatementReceipts(null, beginTime, endTime);
|
|
Map<Long, CrmCustomerStatementSummaryRespVO> summaryMap = new HashMap<>();
|
for (ErpSaleOrderRespDTO order : orders) {
|
if (order.getCustomerId() == null) {
|
continue;
|
}
|
CrmCustomerStatementSummaryRespVO item = summaryMap.computeIfAbsent(order.getCustomerId(),
|
this::newSummary);
|
item.setOrderCount(item.getOrderCount() + 1);
|
item.setTotalOrderAmount(item.getTotalOrderAmount().add(zeroIfNull(order.getTotalPrice())));
|
}
|
for (CrmInvoiceDO invoice : invoices) {
|
if (invoice.getCustomerId() == null) {
|
continue;
|
}
|
CrmCustomerStatementSummaryRespVO item = summaryMap.computeIfAbsent(invoice.getCustomerId(),
|
this::newSummary);
|
item.setInvoiceCount(item.getInvoiceCount() + 1);
|
item.setTotalInvoiceAmount(item.getTotalInvoiceAmount().add(zeroIfNull(invoice.getPrice())));
|
}
|
for (CrmReceivableDO receipt : receipts) {
|
if (receipt.getCustomerId() == null) {
|
continue;
|
}
|
CrmCustomerStatementSummaryRespVO item = summaryMap.computeIfAbsent(receipt.getCustomerId(),
|
this::newSummary);
|
item.setReceiptCount(item.getReceiptCount() + 1);
|
item.setTotalCollectedAmount(item.getTotalCollectedAmount().add(zeroIfNull(receipt.getPrice())));
|
}
|
if (summaryMap.isEmpty()) {
|
return Collections.emptyList();
|
}
|
Map<Long, String> customerNameMap = getCustomerNameMap(summaryMap.keySet());
|
List<CrmCustomerStatementSummaryRespVO> result = new ArrayList<>(summaryMap.values());
|
result.forEach(item -> {
|
item.setCustomerName(customerNameMap.get(item.getCustomerId()));
|
item.setUncollectedAmount(maxZero(item.getTotalInvoiceAmount().subtract(item.getTotalCollectedAmount())));
|
});
|
result.sort(Comparator.comparing(item -> Objects.requireNonNullElse(item.getCustomerName(), "")));
|
return result;
|
}
|
|
private CrmCustomerStatementSummaryRespVO newSummary(Long customerId) {
|
CrmCustomerStatementSummaryRespVO item = new CrmCustomerStatementSummaryRespVO();
|
item.setCustomerId(customerId);
|
item.setOrderCount(0);
|
item.setInvoiceCount(0);
|
item.setReceiptCount(0);
|
item.setTotalOrderAmount(BigDecimal.ZERO);
|
item.setTotalInvoiceAmount(BigDecimal.ZERO);
|
item.setTotalCollectedAmount(BigDecimal.ZERO);
|
item.setUncollectedAmount(BigDecimal.ZERO);
|
return item;
|
}
|
|
@Override
|
public List<CrmShippedNotInvoicedRespVO> getShippedNotInvoicedList(Long customerId) {
|
List<ErpSaleOrderRespDTO> orders = getApprovedOrders(customerId);
|
if (CollUtil.isEmpty(orders)) {
|
return Collections.emptyList();
|
}
|
Map<Long, ErpSaleOrderRespDTO> orderMap = new HashMap<>();
|
orders.forEach(order -> orderMap.put(order.getId(), order));
|
Map<Long, String> customerNameMap = getCustomerNameMap(orders.stream()
|
.map(ErpSaleOrderRespDTO::getCustomerId).filter(Objects::nonNull).toList());
|
|
// 一次性批量查出库单,避免按订单逐条查询(订单量大时会导致接口超时)
|
List<MesWmProductSalesRespDTO> saleOuts = productSalesApi
|
.getProductSalesListBySaleOrderIds(orderMap.keySet()).stream()
|
.filter(saleOut -> ObjectUtilEqual(saleOut.getStatus(), MES_STATUS_FINISHED))
|
.toList();
|
if (CollUtil.isEmpty(saleOuts)) {
|
return Collections.emptyList();
|
}
|
// 一次性批量查开票金额,按出库单汇总
|
Map<Long, BigDecimal> invoicedBySaleOut = new HashMap<>();
|
invoiceMapper.selectListBySaleOutIdsAndStatus(
|
saleOuts.stream().map(MesWmProductSalesRespDTO::getId).toList(), List.of(AUDIT_APPROVE))
|
.forEach(invoice -> mergeAmount(invoicedBySaleOut, invoice.getSaleOutId(), zeroIfNull(invoice.getPrice())));
|
|
List<CrmShippedNotInvoicedRespVO> result = new ArrayList<>();
|
for (MesWmProductSalesRespDTO saleOut : saleOuts) {
|
ErpSaleOrderRespDTO order = orderMap.get(saleOut.getSaleOrderId());
|
if (order == null) {
|
continue;
|
}
|
BigDecimal outAmount = zeroIfNull(saleOut.getTotalPrice());
|
BigDecimal invoicedAmount = invoicedBySaleOut.getOrDefault(saleOut.getId(), BigDecimal.ZERO);
|
BigDecimal remainAmount = outAmount.subtract(invoicedAmount);
|
if (remainAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
continue;
|
}
|
CrmShippedNotInvoicedRespVO vo = new CrmShippedNotInvoicedRespVO();
|
vo.setSaleOutId(saleOut.getId());
|
vo.setSaleOutCode(saleOut.getCode());
|
vo.setSalesDate(saleOut.getSalesDate());
|
vo.setSaleOrderId(order.getId());
|
vo.setSaleOrderNo(order.getNo());
|
vo.setCustomerId(order.getCustomerId());
|
vo.setCustomerName(customerNameMap.get(order.getCustomerId()));
|
vo.setOutAmount(outAmount);
|
vo.setInvoicedAmount(invoicedAmount);
|
vo.setRemainInvoiceAmount(remainAmount);
|
result.add(vo);
|
}
|
return result;
|
}
|
|
@Override
|
public List<CrmUnwrittenOffRespVO> getUnwrittenOffList(Long customerId) {
|
List<CrmInvoiceDO> invoices = invoiceMapper.selectList(new LambdaQueryWrapperX<CrmInvoiceDO>()
|
.eq(CrmInvoiceDO::getAuditStatus, AUDIT_APPROVE)
|
.eqIfPresent(CrmInvoiceDO::getCustomerId, customerId)
|
.orderByAsc(CrmInvoiceDO::getInvoiceTime));
|
if (CollUtil.isEmpty(invoices)) {
|
return Collections.emptyList();
|
}
|
Map<Long, BigDecimal> collectedMap = getCollectedAmountByInvoice(customerId);
|
Map<Long, String> customerNameMap = getCustomerNameMap(invoices.stream()
|
.map(CrmInvoiceDO::getCustomerId).filter(Objects::nonNull).toList());
|
|
List<CrmUnwrittenOffRespVO> result = new ArrayList<>();
|
LocalDate today = LocalDate.now();
|
for (CrmInvoiceDO invoice : invoices) {
|
BigDecimal invoiceAmount = zeroIfNull(invoice.getPrice());
|
BigDecimal collectedAmount = collectedMap.getOrDefault(invoice.getId(), BigDecimal.ZERO);
|
BigDecimal remainAmount = invoiceAmount.subtract(collectedAmount);
|
if (remainAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
continue;
|
}
|
CrmUnwrittenOffRespVO vo = new CrmUnwrittenOffRespVO();
|
vo.setInvoiceId(invoice.getId());
|
vo.setInvoiceNo(invoice.getNo());
|
vo.setTaxInvoiceNo(invoice.getInvoiceNo());
|
vo.setCustomerId(invoice.getCustomerId());
|
vo.setCustomerName(customerNameMap.get(invoice.getCustomerId()));
|
vo.setContractId(invoice.getContractId());
|
vo.setSaleOrderId(invoice.getSaleOrderId());
|
vo.setInvoiceTime(invoice.getInvoiceTime());
|
vo.setInvoiceAmount(invoiceAmount);
|
vo.setCollectedAmount(collectedAmount);
|
vo.setRemainAmount(remainAmount);
|
long agingDays = invoice.getInvoiceTime() == null ? 0
|
: Math.max(0, ChronoUnit.DAYS.between(invoice.getInvoiceTime().toLocalDate(), today));
|
vo.setAgingDays(agingDays);
|
vo.setAgingBucket(agingBucket(agingDays));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
@Override
|
public List<CrmFinanceTrendRespVO> getMonthlyTrend(Integer months) {
|
int monthCount = months == null ? 6 : Math.min(Math.max(months, 1), 36);
|
YearMonth startMonth = YearMonth.now().minusMonths(monthCount - 1L);
|
LocalDateTime beginTime = startMonth.atDay(1).atStartOfDay();
|
LocalDateTime endTime = YearMonth.now().plusMonths(1).atDay(1).atStartOfDay();
|
|
List<CrmInvoiceDO> invoices = invoiceMapper.selectList(new LambdaQueryWrapperX<CrmInvoiceDO>()
|
.eq(CrmInvoiceDO::getAuditStatus, AUDIT_APPROVE)
|
.between(CrmInvoiceDO::getInvoiceTime, beginTime, endTime));
|
List<CrmReceivableDO> receivables = receivableMapper.selectList(new LambdaQueryWrapperX<CrmReceivableDO>()
|
.eq(CrmReceivableDO::getAuditStatus, AUDIT_APPROVE)
|
.between(CrmReceivableDO::getReturnTime, beginTime, endTime));
|
|
Map<String, BigDecimal> invoicedMap = new HashMap<>();
|
Map<String, BigDecimal> collectedMap = new HashMap<>();
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
|
invoices.forEach(invoice -> {
|
if (invoice.getInvoiceTime() != null) {
|
mergeAmount(invoicedMap, invoice.getInvoiceTime().format(formatter), zeroIfNull(invoice.getPrice()));
|
}
|
});
|
receivables.forEach(receivable -> {
|
if (receivable.getReturnTime() != null) {
|
mergeAmount(collectedMap, receivable.getReturnTime().format(formatter), zeroIfNull(receivable.getPrice()));
|
}
|
});
|
|
List<CrmFinanceTrendRespVO> result = new ArrayList<>(monthCount);
|
for (int i = 0; i < monthCount; i++) {
|
String month = startMonth.plusMonths(i).format(formatter);
|
CrmFinanceTrendRespVO vo = new CrmFinanceTrendRespVO();
|
vo.setMonth(month);
|
vo.setInvoicedAmount(invoicedMap.getOrDefault(month, BigDecimal.ZERO));
|
vo.setCollectedAmount(collectedMap.getOrDefault(month, BigDecimal.ZERO));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
// ==================== 数据查询与计算 ====================
|
|
private List<ErpSaleOrderRespDTO> getApprovedOrders(Long customerId) {
|
List<ErpSaleOrderRespDTO> orders = customerId == null
|
? saleOrderApi.getSaleOrderList().getCheckedData()
|
: saleOrderApi.getSaleOrderListByCustomerId(customerId).getCheckedData();
|
if (CollUtil.isEmpty(orders)) {
|
return Collections.emptyList();
|
}
|
return orders.stream()
|
.filter(order -> ObjectUtilEqual(order.getStatus(), ERP_STATUS_APPROVE))
|
.toList();
|
}
|
|
private Map<Long, BigDecimal> getShippedAmountByOrder(Collection<Long> saleOrderIds) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
productSalesApi.getProductSalesListBySaleOrderIds(saleOrderIds).stream()
|
.filter(saleOut -> ObjectUtilEqual(saleOut.getStatus(), MES_STATUS_FINISHED))
|
.forEach(saleOut -> mergeAmount(result, saleOut.getSaleOrderId(), zeroIfNull(saleOut.getTotalPrice())));
|
return result;
|
}
|
|
private Map<Long, BigDecimal> getInvoicedAmountByOrder(Collection<Long> saleOrderIds) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
invoiceMapper.selectListBySaleOrderIdsAndStatus(saleOrderIds, List.of(AUDIT_APPROVE))
|
.forEach(invoice -> mergeAmount(result, invoice.getSaleOrderId(), zeroIfNull(invoice.getPrice())));
|
return result;
|
}
|
|
/**
|
* 计算每张销售订单已回款金额
|
*
|
* 优先取核销明细(销售订单 / 开票),历史回款没有核销明细时按回款关联的开票折算到订单。
|
*/
|
private Map<Long, BigDecimal> getCollectedAmountByOrder(List<ErpSaleOrderRespDTO> orders, Long customerId) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
Map<Long, ErpSaleOrderRespDTO> orderMap = new HashMap<>();
|
orders.forEach(order -> orderMap.put(order.getId(), order));
|
if (orderMap.isEmpty()) {
|
return result;
|
}
|
// 1. 已审核回款
|
List<CrmReceivableDO> receivables = customerId == null
|
? receivableMapper.selectList(new LambdaQueryWrapperX<CrmReceivableDO>()
|
.eq(CrmReceivableDO::getAuditStatus, AUDIT_APPROVE))
|
: receivableMapper.selectListByCustomerId(customerId).stream()
|
.filter(receivable -> ObjectUtilEqual(receivable.getAuditStatus(), AUDIT_APPROVE)).toList();
|
if (CollUtil.isEmpty(receivables)) {
|
return result;
|
}
|
Map<Long, CrmReceivableDO> receivableMap = new HashMap<>();
|
receivables.forEach(receivable -> receivableMap.put(receivable.getId(), receivable));
|
// 2. 核销明细
|
List<CrmReceivableItemDO> items = receivableItemMapper.selectListByReceivableIds(receivableMap.keySet());
|
Set<Long> receivablesWithItems = new HashSet<>();
|
Set<Long> invoiceIds = new HashSet<>();
|
items.forEach(item -> {
|
receivablesWithItems.add(item.getReceivableId());
|
if (CrmReceivableBizTypeEnum.SALE_INVOICE.getType().equals(item.getBizType()) && item.getBizId() != null) {
|
invoiceIds.add(item.getBizId());
|
}
|
});
|
receivables.forEach(receivable -> {
|
if (receivable.getInvoiceId() != null) {
|
invoiceIds.add(receivable.getInvoiceId());
|
}
|
});
|
Map<Long, Long> invoiceOrderMap = new HashMap<>();
|
if (CollUtil.isNotEmpty(invoiceIds)) {
|
invoiceMapper.selectByIds(invoiceIds).forEach(invoice -> {
|
if (invoice.getSaleOrderId() != null) {
|
invoiceOrderMap.put(invoice.getId(), invoice.getSaleOrderId());
|
}
|
});
|
}
|
for (CrmReceivableItemDO item : items) {
|
if (!receivableMap.containsKey(item.getReceivableId())) {
|
continue;
|
}
|
Long orderId = null;
|
if (CrmReceivableBizTypeEnum.SALE_ORDER.getType().equals(item.getBizType())) {
|
orderId = item.getBizId();
|
} else if (CrmReceivableBizTypeEnum.SALE_INVOICE.getType().equals(item.getBizType())) {
|
orderId = invoiceOrderMap.get(item.getBizId());
|
}
|
if (orderId != null && orderMap.containsKey(orderId)) {
|
result.merge(orderId, zeroIfNull(item.getReceiptPrice()), BigDecimal::add);
|
}
|
}
|
// 3. 历史回款:没有核销明细时,按回款关联开票折算到订单(只处理订单范围内的开票)
|
for (CrmReceivableDO receivable : receivables) {
|
if (receivablesWithItems.contains(receivable.getId()) || receivable.getInvoiceId() == null) {
|
continue;
|
}
|
Long orderId = invoiceOrderMap.get(receivable.getInvoiceId());
|
if (orderId != null && orderMap.containsKey(orderId)) {
|
result.merge(orderId, zeroIfNull(receivable.getPrice()), BigDecimal::add);
|
}
|
}
|
return result;
|
}
|
|
/**
|
* 计算每张开票已回款金额(仅已审核回款)
|
*
|
* 有核销明细时按明细归集到开票;历史回款按 invoiceId 直接归集。
|
*/
|
private Map<Long, BigDecimal> getCollectedAmountByInvoice(Long customerId) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
List<CrmReceivableDO> receivables = customerId == null
|
? receivableMapper.selectList(new LambdaQueryWrapperX<CrmReceivableDO>()
|
.eq(CrmReceivableDO::getAuditStatus, AUDIT_APPROVE))
|
: receivableMapper.selectListByCustomerId(customerId).stream()
|
.filter(receivable -> ObjectUtilEqual(receivable.getAuditStatus(), AUDIT_APPROVE)).toList();
|
if (CollUtil.isEmpty(receivables)) {
|
return result;
|
}
|
Map<Long, CrmReceivableDO> receivableMap = new HashMap<>();
|
receivables.forEach(receivable -> receivableMap.put(receivable.getId(), receivable));
|
List<CrmReceivableItemDO> items = receivableItemMapper.selectListByReceivableIds(receivableMap.keySet());
|
Set<Long> receivablesWithItems = new HashSet<>();
|
for (CrmReceivableItemDO item : items) {
|
receivablesWithItems.add(item.getReceivableId());
|
if (CrmReceivableBizTypeEnum.SALE_INVOICE.getType().equals(item.getBizType()) && item.getBizId() != null) {
|
result.merge(item.getBizId(), zeroIfNull(item.getReceiptPrice()), BigDecimal::add);
|
}
|
}
|
for (CrmReceivableDO receivable : receivables) {
|
if (receivablesWithItems.contains(receivable.getId()) || receivable.getInvoiceId() == null) {
|
continue;
|
}
|
result.merge(receivable.getInvoiceId(), zeroIfNull(receivable.getPrice()), BigDecimal::add);
|
}
|
return result;
|
}
|
|
private BigDecimal getNextMonthPlanAmount(Long customerId) {
|
YearMonth nextMonth = YearMonth.now().plusMonths(1);
|
LocalDateTime beginTime = nextMonth.atDay(1).atStartOfDay();
|
LocalDateTime endTime = nextMonth.plusMonths(1).atDay(1).atStartOfDay();
|
List<CrmReceivablePlanDO> plans = receivablePlanMapper.selectList(
|
new LambdaQueryWrapperX<CrmReceivablePlanDO>()
|
.eqIfPresent(CrmReceivablePlanDO::getCustomerId, customerId)
|
.ge(CrmReceivablePlanDO::getReturnTime, beginTime)
|
.lt(CrmReceivablePlanDO::getReturnTime, endTime));
|
return plans.stream()
|
.filter(plan -> !PLAN_STATUS_RECEIPTED.equals(plan.getStatus()))
|
.map(plan -> zeroIfNull(plan.getPrice()))
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
}
|
|
private List<ErpSaleOrderRespDTO> getStatementOrders(Long customerId, LocalDateTime beginTime,
|
LocalDateTime endTime, Boolean includeUnapprovedOrder) {
|
List<ErpSaleOrderRespDTO> orders = customerId == null
|
? saleOrderApi.getSaleOrderList().getCheckedData()
|
: saleOrderApi.getSaleOrderListByCustomerId(customerId).getCheckedData();
|
if (CollUtil.isEmpty(orders)) {
|
return Collections.emptyList();
|
}
|
boolean includeUnapproved = Boolean.TRUE.equals(includeUnapprovedOrder);
|
return orders.stream()
|
.filter(order -> includeUnapproved || ObjectUtilEqual(order.getStatus(), ERP_STATUS_APPROVE))
|
.filter(order -> inTimeRange(order.getOrderTime(), beginTime, endTime))
|
.toList();
|
}
|
|
private List<CrmInvoiceDO> getStatementInvoices(Long customerId, LocalDateTime beginTime,
|
LocalDateTime endTime) {
|
return invoiceMapper.selectList(new LambdaQueryWrapperX<CrmInvoiceDO>()
|
.eq(CrmInvoiceDO::getAuditStatus, AUDIT_APPROVE)
|
.eqIfPresent(CrmInvoiceDO::getCustomerId, customerId)
|
.betweenIfPresent(CrmInvoiceDO::getInvoiceTime, beginTime, endTime)
|
.orderByAsc(CrmInvoiceDO::getInvoiceTime));
|
}
|
|
private List<CrmReceivableDO> getStatementReceipts(Long customerId, LocalDateTime beginTime,
|
LocalDateTime endTime) {
|
return receivableMapper.selectList(new LambdaQueryWrapperX<CrmReceivableDO>()
|
.eq(CrmReceivableDO::getAuditStatus, AUDIT_APPROVE)
|
.eqIfPresent(CrmReceivableDO::getCustomerId, customerId)
|
.betweenIfPresent(CrmReceivableDO::getReturnTime, beginTime, endTime)
|
.orderByAsc(CrmReceivableDO::getReturnTime));
|
}
|
|
private static boolean inTimeRange(LocalDateTime time, LocalDateTime beginTime, LocalDateTime endTime) {
|
if (beginTime != null && (time == null || time.isBefore(beginTime))) {
|
return false;
|
}
|
return endTime == null || (time != null && !time.isAfter(endTime));
|
}
|
|
/**
|
* 按回款单计算每张开票的已回款金额
|
*/
|
private Map<Long, BigDecimal> getCollectedByInvoice(List<CrmReceivableDO> receipts) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
if (CollUtil.isEmpty(receipts)) {
|
return result;
|
}
|
Map<Long, CrmReceivableDO> receiptMap = new HashMap<>();
|
receipts.forEach(receipt -> receiptMap.put(receipt.getId(), receipt));
|
Set<Long> withItems = new HashSet<>();
|
for (CrmReceivableItemDO item : receivableItemMapper.selectListByReceivableIds(receiptMap.keySet())) {
|
withItems.add(item.getReceivableId());
|
if (CrmReceivableBizTypeEnum.SALE_INVOICE.getType().equals(item.getBizType()) && item.getBizId() != null) {
|
result.merge(item.getBizId(), zeroIfNull(item.getReceiptPrice()), BigDecimal::add);
|
}
|
}
|
for (CrmReceivableDO receipt : receipts) {
|
if (!withItems.contains(receipt.getId()) && receipt.getInvoiceId() != null) {
|
result.merge(receipt.getInvoiceId(), zeroIfNull(receipt.getPrice()), BigDecimal::add);
|
}
|
}
|
return result;
|
}
|
|
/**
|
* 按回款单计算每张销售订单的已回款金额
|
*/
|
private Map<Long, BigDecimal> getCollectedByOrder(List<CrmReceivableDO> receipts, Long customerId) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
if (CollUtil.isEmpty(receipts)) {
|
return result;
|
}
|
Map<Long, CrmReceivableDO> receiptMap = new HashMap<>();
|
receipts.forEach(receipt -> receiptMap.put(receipt.getId(), receipt));
|
// 回款可能关联到时间范围外的开票,这里取客户全部开票做映射
|
List<CrmInvoiceDO> allInvoices = invoiceMapper.selectList(new LambdaQueryWrapperX<CrmInvoiceDO>()
|
.eqIfPresent(CrmInvoiceDO::getCustomerId, customerId));
|
Map<Long, Long> invoiceOrderMap = new HashMap<>();
|
allInvoices.forEach(invoice -> {
|
if (invoice.getSaleOrderId() != null) {
|
invoiceOrderMap.put(invoice.getId(), invoice.getSaleOrderId());
|
}
|
});
|
Set<Long> withItems = new HashSet<>();
|
for (CrmReceivableItemDO item : receivableItemMapper.selectListByReceivableIds(receiptMap.keySet())) {
|
withItems.add(item.getReceivableId());
|
if (CrmReceivableBizTypeEnum.SALE_ORDER.getType().equals(item.getBizType()) && item.getBizId() != null) {
|
result.merge(item.getBizId(), zeroIfNull(item.getReceiptPrice()), BigDecimal::add);
|
} else if (CrmReceivableBizTypeEnum.SALE_INVOICE.getType().equals(item.getBizType())
|
&& item.getBizId() != null && invoiceOrderMap.containsKey(item.getBizId())) {
|
result.merge(invoiceOrderMap.get(item.getBizId()), zeroIfNull(item.getReceiptPrice()), BigDecimal::add);
|
}
|
}
|
for (CrmReceivableDO receipt : receipts) {
|
if (!withItems.contains(receipt.getId()) && receipt.getInvoiceId() != null
|
&& invoiceOrderMap.containsKey(receipt.getInvoiceId())) {
|
result.merge(invoiceOrderMap.get(receipt.getInvoiceId()), zeroIfNull(receipt.getPrice()), BigDecimal::add);
|
}
|
}
|
return result;
|
}
|
|
private Map<Long, String> getCustomerNameMap(Collection<Long> customerIds) {
|
if (CollUtil.isEmpty(customerIds)) {
|
return Collections.emptyMap();
|
}
|
Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(customerIds);
|
Map<Long, String> result = new HashMap<>();
|
customerMap.forEach((id, customer) -> result.put(id, customer.getName()));
|
return result;
|
}
|
|
private static void mergeAmount(Map<String, BigDecimal> map, String key, BigDecimal amount) {
|
map.merge(key, amount, BigDecimal::add);
|
}
|
|
private static void mergeAmount(Map<Long, BigDecimal> map, Long key, BigDecimal amount) {
|
if (key == null) {
|
return;
|
}
|
map.merge(key, amount, BigDecimal::add);
|
}
|
|
private static String agingBucket(long days) {
|
if (days <= 30) {
|
return "0-30天";
|
}
|
if (days <= 60) {
|
return "31-60天";
|
}
|
if (days <= 90) {
|
return "61-90天";
|
}
|
return "90天以上";
|
}
|
|
private static boolean ObjectUtilEqual(Object left, Object right) {
|
return Objects.equals(left, right);
|
}
|
|
private static BigDecimal zeroIfNull(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
private static BigDecimal maxZero(BigDecimal value) {
|
return value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
|
}
|
|
}
|