gaoluyang
昨天 8c65f898ed6e5b0c7a94c792c4562f6ec831dccd
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/finance/CrmFinanceReportServiceImpl.java
@@ -2,7 +2,10 @@
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;
@@ -67,9 +70,23 @@
    private CrmCustomerService customerService;
    @Override
    public CrmCustomerStatementRespVO getCustomerStatement(Long customerId) {
        List<ErpSaleOrderRespDTO> orders = getApprovedOrders(customerId);
        Map<Long, BigDecimal> collectedByOrder = getCollectedAmountByOrder(orders, customerId);
    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);
@@ -81,33 +98,138 @@
        result.setShippedNotInvoicedAmount(BigDecimal.ZERO);
        result.setOutstandingAmount(BigDecimal.ZERO);
        result.setNextMonthPlanAmount(getNextMonthPlanAmount(customerId));
        List<CrmCustomerStatementRespVO.OrderRow> rows = new ArrayList<>(orders.size());
        // 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(getShippedAmount(order.getId()));
            row.setInvoicedAmount(getInvoicedAmount(order.getId()));
            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())));
            rows.add(row);
            row.setStatus(order.getStatus());
            orderRows.add(row);
            result.setOrderTotalAmount(result.getOrderTotalAmount().add(row.getOrderAmount()));
            result.setShippedAmount(result.getShippedAmount().add(row.getShippedAmount()));
            result.setInvoicedAmount(result.getInvoicedAmount().add(row.getInvoicedAmount()));
            result.setCollectedAmount(result.getCollectedAmount().add(row.getCollectedAmount()));
            result.setShippedNotInvoicedAmount(result.getShippedNotInvoicedAmount().add(row.getShippedNotInvoicedAmount()));
            result.setOutstandingAmount(result.getOutstandingAmount().add(row.getOutstandingAmount()));
        }
        result.setOrders(rows);
        if (customerId != null) {
            CrmCustomerDO customer = customerService.getCustomer(customerId);
            result.setCustomerName(customer == null ? null : customer.getName());
        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
@@ -121,29 +243,44 @@
        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 (ErpSaleOrderRespDTO order : orders) {
            for (MesWmProductSalesRespDTO saleOut : getFinishedSaleOuts(order.getId())) {
                BigDecimal outAmount = zeroIfNull(saleOut.getTotalPrice());
                BigDecimal invoicedAmount = sumInvoicePrice(invoiceMapper.selectListBySaleOutIdAndStatus(
                        saleOut.getId(), List.of(AUDIT_APPROVE)));
                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);
        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;
    }
@@ -245,22 +382,19 @@
                .toList();
    }
    private List<MesWmProductSalesRespDTO> getFinishedSaleOuts(Long saleOrderId) {
        List<MesWmProductSalesRespDTO> saleOuts = productSalesApi.getProductSalesListBySaleOrderId(saleOrderId);
        if (CollUtil.isEmpty(saleOuts)) {
            return Collections.emptyList();
        }
        return saleOuts.stream().filter(saleOut -> ObjectUtilEqual(saleOut.getStatus(), MES_STATUS_FINISHED)).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 BigDecimal getShippedAmount(Long saleOrderId) {
        return getFinishedSaleOuts(saleOrderId).stream()
                .map(saleOut -> zeroIfNull(saleOut.getTotalPrice()))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    private BigDecimal getInvoicedAmount(Long saleOrderId) {
        return sumInvoicePrice(invoiceMapper.selectListBySaleOrderIdAndStatus(saleOrderId, List.of(AUDIT_APPROVE)));
    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;
    }
    /**
@@ -385,6 +519,109 @@
                .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();
@@ -395,14 +632,14 @@
        return result;
    }
    private static BigDecimal sumInvoicePrice(List<CrmInvoiceDO> invoices) {
        if (CollUtil.isEmpty(invoices)) {
            return BigDecimal.ZERO;
        }
        return invoices.stream().map(invoice -> zeroIfNull(invoice.getPrice())).reduce(BigDecimal.ZERO, BigDecimal::add);
    private static void mergeAmount(Map<String, BigDecimal> map, String key, BigDecimal amount) {
        map.merge(key, amount, BigDecimal::add);
    }
    private static void mergeAmount(Map<String, BigDecimal> map, String key, BigDecimal amount) {
    private static void mergeAmount(Map<Long, BigDecimal> map, Long key, BigDecimal amount) {
        if (key == null) {
            return;
        }
        map.merge(key, amount, BigDecimal::add);
    }