| | |
| | | private static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); |
| | | private static final int DEFAULT_LIMIT = 10; |
| | | private static final int MAX_LIMIT = 50; |
| | | private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.65"); |
| | | |
| | | private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.60"); |
| | | private static final BigDecimal DEFAULT_LABOR_COST_RATE = new BigDecimal("0.15"); |
| | | private static final BigDecimal DEFAULT_OVERHEAD_COST_RATE = new BigDecimal("0.10"); |
| | | |
| | | private static final BigDecimal SME_RECEIVABLE_RISK_THRESHOLD = new BigDecimal("500000"); |
| | | private static final BigDecimal SME_INVENTORY_RISK_THRESHOLD = new BigDecimal("1000000"); |
| | | private static final BigDecimal SME_PROFIT_WARNING_RATE = new BigDecimal("0.08"); |
| | | |
| | | private final SalesLedgerMapper salesLedgerMapper; |
| | | private final SalesLedgerProductMapper salesLedgerProductMapper; |
| | |
| | | @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword, |
| | | @P(value = "返回条数,默认10,最大50", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | if (loginUser == null) { |
| | | return jsonResponse(false, "financial_cost_accounting", "用户信息获取失败", Map.of(), Map.of(), Map.of()); |
| | | } |
| | | |
| | | DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天"); |
| | | AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit); |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("orderCount", bundle.orderMetrics().size()); |
| | | summary.put("totalRevenue", bundle.totalRevenue()); |
| | | summary.put("totalMaterialCost", bundle.totalMaterialCost()); |
| | |
| | | @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword, |
| | | @P(value = "返回条数,默认10,最大50", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | if (loginUser == null) { |
| | | return jsonResponse(false, "financial_order_profit_analysis", "用户信息获取失败", Map.of(), Map.of(), Map.of()); |
| | | } |
| | | |
| | | DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天"); |
| | | AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit); |
| | | List<OrderProfitMetric> metrics = bundle.orderMetrics(); |
| | | |
| | | List<OrderProfitMetric> riskyOrders = metrics.stream() |
| | | .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(new BigDecimal("0.08")) < 0) |
| | | .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(SME_PROFIT_WARNING_RATE) < 0) |
| | | .sorted(Comparator.comparing(OrderProfitMetric::profitRate) |
| | | .thenComparing(OrderProfitMetric::profit)) |
| | | .toList(); |
| | |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("orderCount", metrics.size()); |
| | | summary.put("lossOrderCount", metrics.stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count()); |
| | | summary.put("lowProfitOrderCount", riskyOrders.size()); |
| | |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("actualIncomeTotal", collections.stream().map(AccountSalesCollection::getCollectionAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); |
| | | summary.put("actualExpenseTotal", payments.stream().map(AccountPurchasePayment::getPaymentAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); |
| | | summary.put("receivableBalance", receivableTotal); |
| | |
| | | List<Map<String, Object>> topAnomalies = anomalyItems.stream().limit(finalLimit).toList(); |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("anomalyCount", topAnomalies.size()); |
| | | summary.put("highRiskCount", topAnomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count()); |
| | | summary.put("mediumRiskCount", topAnomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count()); |
| | |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("outputValue", outputValue); |
| | | summary.put("profit", profitBundle.totalProfit()); |
| | | summary.put("profitRate", toPercent(profitRate)); |
| | |
| | | if (lossCount > 0) { |
| | | riskSuggestions.add(riskSuggestion("利润风险", "高", "复核亏损订单BOM和工序工资定额,必要时调整报价与交付节奏。")); |
| | | } |
| | | if (snapshot.receivableTotal().compareTo(new BigDecimal("1000000")) > 0) { |
| | | if (snapshot.receivableTotal().compareTo(SME_RECEIVABLE_RISK_THRESHOLD) > 0) { |
| | | riskSuggestions.add(riskSuggestion("回款风险", "中", "对应收TOP客户建立周度回款计划,并设置预警阈值。")); |
| | | } |
| | | if (inventoryValue.compareTo(new BigDecimal("3000000")) > 0) { |
| | | if (inventoryValue.compareTo(SME_INVENTORY_RISK_THRESHOLD) > 0) { |
| | | riskSuggestions.add(riskSuggestion("库存风险", "中", "对高金额呆滞库存执行降价、替代和生产消耗策略。")); |
| | | } |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("reportType", type); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("orderCount", bundle.orderMetrics().size()); |
| | | summary.put("lossOrderCount", lossCount); |
| | | summary.put("riskSuggestionCount", riskSuggestions.size()); |
| | |
| | | applyTenantFilter(outWrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId); |
| | | applyDeptFilter(outWrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId); |
| | | outWrapper.eq(ProcurementRecordOut::getType, 2) |
| | | .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds) |
| | | .ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay()) |
| | | .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay()); |
| | | .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds); |
| | | if (range.hasDateFilter()) { |
| | | outWrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay()) |
| | | .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay()); |
| | | } |
| | | List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(outWrapper)); |
| | | |
| | | Set<Integer> storageIds = outList.stream() |
| | |
| | | |
| | | LambdaQueryWrapper<ProductionOrder> orderWrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(orderWrapper, loginUser.getCurrentDeptId(), ProductionOrder::getDeptId); |
| | | orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2)) |
| | | .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1)); |
| | | if (range.hasDateFilter()) { |
| | | orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2)) |
| | | .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1)); |
| | | } |
| | | List<ProductionOrder> orders = defaultList(productionOrderMapper.selectList(orderWrapper)); |
| | | |
| | | Map<Long, Set<Long>> orderIdToLedgerIds = new HashMap<>(); |
| | |
| | | |
| | | LambdaQueryWrapper<ProductionProductMain> mainWrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(mainWrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId); |
| | | mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet()) |
| | | .ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2)) |
| | | .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1)); |
| | | mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet()); |
| | | if (range.hasDateFilter()) { |
| | | mainWrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2)) |
| | | .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1)); |
| | | } |
| | | List<ProductionProductMain> mainList = defaultList(productionProductMainMapper.selectList(mainWrapper)); |
| | | Map<Long, Set<Long>> mainIdToLedgers = new HashMap<>(); |
| | | for (ProductionProductMain main : mainList) { |
| | |
| | | |
| | | LambdaQueryWrapper<ProductionAccount> accountWrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(accountWrapper, loginUser.getCurrentDeptId(), ProductionAccount::getDeptId); |
| | | accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet()) |
| | | .ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay()) |
| | | .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay()); |
| | | accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet()); |
| | | if (range.hasDateFilter()) { |
| | | accountWrapper.ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay()) |
| | | .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay()); |
| | | } |
| | | List<ProductionAccount> accountList = defaultList(productionAccountMapper.selectList(accountWrapper)); |
| | | |
| | | Map<String, BigDecimal> salaryQuotaByOperation = defaultList(technologyOperationMapper.selectList(new LambdaQueryWrapper<TechnologyOperation>() |
| | |
| | | |
| | | LambdaQueryWrapper<ProductionProductOutput> outputWrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(outputWrapper, loginUser.getCurrentDeptId(), ProductionProductOutput::getDeptId); |
| | | outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet()) |
| | | .ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay()) |
| | | .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay()); |
| | | outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet()); |
| | | if (range.hasDateFilter()) { |
| | | outputWrapper.ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay()) |
| | | .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay()); |
| | | } |
| | | List<ProductionProductOutput> outputList = defaultList(productionProductOutputMapper.selectList(outputWrapper)); |
| | | Map<Long, BigDecimal> scrapCostByLedger = new HashMap<>(); |
| | | for (ProductionProductOutput output : outputList) { |
| | |
| | | .or().like(SalesLedger::getProjectName, keyword) |
| | | .or().like(SalesLedger::getSalesman, keyword)); |
| | | } |
| | | wrapper.ge(SalesLedger::getEntryDate, toDate(range.start())) |
| | | .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end())) |
| | | .orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId) |
| | | .last("limit " + normalizeLimit(limit)); |
| | | if (range.hasDateFilter()) { |
| | | wrapper.ge(SalesLedger::getEntryDate, toDate(range.start())) |
| | | .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end())); |
| | | } |
| | | wrapper.orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId); |
| | | if (limit != null && limit > 0) { |
| | | wrapper.last("limit " + normalizeLimit(limit)); |
| | | } |
| | | return defaultList(salesLedgerMapper.selectList(wrapper)); |
| | | } |
| | | |
| | |
| | | if (productModelIds != null && !productModelIds.isEmpty()) { |
| | | wrapper.in(ProcurementRecordOut::getProductModelId, productModelIds); |
| | | } |
| | | wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay()) |
| | | .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay()); |
| | | if (range.hasDateFilter()) { |
| | | wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay()) |
| | | .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay()); |
| | | } |
| | | List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(wrapper)); |
| | | if (outList.isEmpty()) { |
| | | return OutboundStats.empty(); |
| | |
| | | private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId); |
| | | wrapper.ge(AccountSalesCollection::getCollectionDate, range.start()) |
| | | .le(AccountSalesCollection::getCollectionDate, range.end()) |
| | | .orderByAsc(AccountSalesCollection::getCollectionDate); |
| | | if (range.hasDateFilter()) { |
| | | wrapper.ge(AccountSalesCollection::getCollectionDate, range.start()) |
| | | .le(AccountSalesCollection::getCollectionDate, range.end()); |
| | | } |
| | | wrapper.orderByAsc(AccountSalesCollection::getCollectionDate); |
| | | return defaultList(accountSalesCollectionMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId); |
| | | wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start()) |
| | | .le(AccountPurchasePayment::getPaymentDate, range.end()) |
| | | .orderByAsc(AccountPurchasePayment::getPaymentDate); |
| | | if (range.hasDateFilter()) { |
| | | wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start()) |
| | | .le(AccountPurchasePayment::getPaymentDate, range.end()); |
| | | } |
| | | wrapper.orderByAsc(AccountPurchasePayment::getPaymentDate); |
| | | return defaultList(accountPurchasePaymentMapper.selectList(wrapper)); |
| | | } |
| | | |
| | |
| | | List<AccountPurchasePayment> payments) { |
| | | Map<YearMonth, BigDecimal> incomeByMonth = new LinkedHashMap<>(); |
| | | Map<YearMonth, BigDecimal> expenseByMonth = new LinkedHashMap<>(); |
| | | YearMonth startMonth = YearMonth.from(range.start()); |
| | | YearMonth endMonth = YearMonth.from(range.end()); |
| | | DateRange monthlyRange = range.hasDateFilter() ? range : inferCashFlowRange(collections, payments); |
| | | if (!monthlyRange.hasDateFilter()) { |
| | | return List.of(); |
| | | } |
| | | YearMonth startMonth = YearMonth.from(monthlyRange.start()); |
| | | YearMonth endMonth = YearMonth.from(monthlyRange.end()); |
| | | for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) { |
| | | incomeByMonth.put(month, BigDecimal.ZERO); |
| | | expenseByMonth.put(month, BigDecimal.ZERO); |
| | |
| | | result.add(new MonthlyCashFlow(month.toString(), income, expense, income.subtract(expense))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private DateRange inferCashFlowRange(List<AccountSalesCollection> collections, |
| | | List<AccountPurchasePayment> payments) { |
| | | LocalDate min = null; |
| | | LocalDate max = null; |
| | | for (AccountSalesCollection row : defaultList(collections)) { |
| | | if (row.getCollectionDate() == null) { |
| | | continue; |
| | | } |
| | | min = min == null || row.getCollectionDate().isBefore(min) ? row.getCollectionDate() : min; |
| | | max = max == null || row.getCollectionDate().isAfter(max) ? row.getCollectionDate() : max; |
| | | } |
| | | for (AccountPurchasePayment row : defaultList(payments)) { |
| | | if (row.getPaymentDate() == null) { |
| | | continue; |
| | | } |
| | | min = min == null || row.getPaymentDate().isBefore(min) ? row.getPaymentDate() : min; |
| | | max = max == null || row.getPaymentDate().isAfter(max) ? row.getPaymentDate() : max; |
| | | } |
| | | return min == null || max == null ? new DateRange(null, null, "全部") : new DateRange(min, max, "全部"); |
| | | } |
| | | |
| | | private List<MonthlyCashFlow> forecastMonthlyCashFlow(List<MonthlyCashFlow> actual, int forecastMonths) { |
| | |
| | | return productAmount; |
| | | } |
| | | } |
| | | return revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE); |
| | | |
| | | BigDecimal materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE); |
| | | BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE); |
| | | BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE); |
| | | |
| | | return materialCost.add(laborCost).add(overheadCost); |
| | | } |
| | | |
| | | private BigDecimal estimateTotalCost(BigDecimal revenue, List<SalesLedgerProduct> products) { |
| | | if (revenue == null || revenue.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | BigDecimal materialCost = BigDecimal.ZERO; |
| | | if (products != null && !products.isEmpty()) { |
| | | materialCost = products.stream() |
| | | .map(SalesLedgerProduct::getTaxExclusiveTotalPrice) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | } |
| | | |
| | | if (materialCost.compareTo(BigDecimal.ZERO) <= 0) { |
| | | materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE); |
| | | } |
| | | |
| | | BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE); |
| | | BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE); |
| | | |
| | | return materialCost.add(laborCost).add(overheadCost); |
| | | } |
| | | |
| | | private Map<String, String> queryCustomerNameMap(Set<String> idSet) { |
| | |
| | | } |
| | | |
| | | private DateRange previousSameLengthRange(DateRange range) { |
| | | if (!range.hasDateFilter()) { |
| | | return new DateRange(null, null, "全部"); |
| | | } |
| | | long days = daysBetween(range.start(), range.end()) + 1L; |
| | | LocalDate prevEnd = range.start().minusDays(1); |
| | | LocalDate prevStart = prevEnd.minusDays(days - 1L); |
| | |
| | | } |
| | | |
| | | if (!StringUtils.hasText(timeRange)) { |
| | | if ("今天".equals(defaultLabel)) { |
| | | return new DateRange(today, today, "今天"); |
| | | } |
| | | if ("本周".equals(defaultLabel)) { |
| | | LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L); |
| | | return new DateRange(start, today, "本周"); |
| | | } |
| | | if ("本月".equals(defaultLabel)) { |
| | | return new DateRange(today.withDayOfMonth(1), today, "本月"); |
| | | } |
| | | if ("近90天".equals(defaultLabel)) { |
| | | return new DateRange(today.minusDays(89), today, "近90天"); |
| | | } |
| | | return new DateRange(today.minusDays(29), today, defaultLabel); |
| | | return new DateRange(null, null, "全部"); |
| | | } |
| | | |
| | | String text = timeRange.trim(); |
| | |
| | | return new DateRange(start, end, start + "至" + end); |
| | | } |
| | | } |
| | | return new DateRange(today.minusDays(29), today, "近30天"); |
| | | return new DateRange(null, null, "全部"); |
| | | } |
| | | |
| | | private LocalDate parseLocalDate(String text) { |
| | |
| | | } |
| | | |
| | | private String formatDate(LocalDate date) { |
| | | return date == null ? "" : date.format(DATE_FMT); |
| | | } |
| | | |
| | | private String displayDate(LocalDate date) { |
| | | return date == null ? "" : date.format(DATE_FMT); |
| | | } |
| | | |
| | |
| | | private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) { |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("startDate", displayDate(range.start())); |
| | | summary.put("endDate", displayDate(range.end())); |
| | | summary.put("count", count); |
| | | summary.put("keyword", safe(keyword)); |
| | | return summary; |
| | |
| | | } |
| | | |
| | | private record DateRange(LocalDate start, LocalDate end, String label) { |
| | | private boolean hasDateFilter() { |
| | | return start != null && end != null; |
| | | } |
| | | } |
| | | |
| | | private record OrderProfitMetric(Long ledgerId, |