AccountingNext/Schaad.Accounting.Services/ChartService.cs
Claudio Schaad d819a836da Show signed amounts on the transactions page and in its charts
Adds TransactionDataset.GetValue(bool, string accountId), which
negates the value when the given account is the transaction's origin
(paying out) so the account's own transaction list and charts show
a minus sign for outgoing money, regardless of account class. Removes
the old Activa-only sign flip baked into
ViewService.GetTransactionViewListAsync(accountId), which would have
double-negated Activa bookings against the new accountId-aware
GetValue. ChartService's booking-text totals now sum through the same
accountId-aware GetValue instead of filtering to inbound transactions
only, so a booking text nets its in/out transactions correctly.
2026-07-22 14:33:54 +02:00

151 lines
6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Datasets.Charts;
using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.Services
{
public class ChartService : IChartService
{
private readonly ISettingsService settingsService;
private readonly IViewService viewService;
public ChartService(ISettingsService settingsService, IViewService viewService)
{
this.settingsService = settingsService;
this.viewService = viewService;
}
public async Task<IReadOnlyList<DataSerie>> GetExpensesPerMonthAsync()
{
var expenseTransactions = (await viewService.GetTransactionViewListAsync())
.Where(t => t.TargetAccount.Class == ClassIds.Expenses)
.ToList();
var distinctSubClasses = expenseTransactions
.Select(t => t.TargetAccount.SubClass)
.Distinct()
.Count();
return distinctSubClasses > 1
? await GetSubClassExpensesPerMonthAsync(expenseTransactions)
: await GetAccountExpensesPerMonthAsync();
}
private async Task<List<DataSerie>> GetSubClassExpensesPerMonthAsync(List<TransactionDataset> expenseTransactions)
{
var year = settingsService.GetYear();
var maxMonth = expenseTransactions
.OrderByDescending(t => t.ValueDate)
.FirstOrDefault()?.ValueDate.Month ?? 12;
var subClassNameByNumber = (await viewService.GetAccountViewListAsync())
.GroupBy(a => a.SubClass)
.ToDictionary(g => g.Key, g => g.First().SubClassName);
var list = new List<DataSerie>();
foreach (var grp in expenseTransactions.GroupBy(t => t.TargetAccount.SubClass))
{
var groupedByMonth = grp
.GroupBy(t => t.ValueDate.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(t.TargetAccount.IsFxAccount)));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
list.Add(new DataSerie(
Id: grp.Key.ToString(),
Name: subClassNameByNumber[grp.Key],
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
));
}
return list;
}
private async Task<List<DataSerie>> GetAccountExpensesPerMonthAsync()
{
var year = settingsService.GetYear();
var allTransactions = await viewService.GetTransactionViewListAsync();
var expenseAccounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses);
var list = new List<DataSerie>();
foreach (var account in expenseAccounts)
{
var transactions = allTransactions.Where(t => t.TargetAccountId == account.Id).ToList();
if (transactions.Count == 0)
{
continue;
}
var maxMonth = transactions.Max(t => t.ValueDate.Month);
var groupedByMonth = transactions
.GroupBy(t => t.ValueDate.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(account.IsFxAccount)));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
list.Add(new DataSerie(
Id: account.Id,
Name: account.Name,
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
));
}
return list;
}
public async Task<IReadOnlyList<DataSerie>> GetBookingTextExpensesPerMonthAsync(string accountId)
{
var account = await viewService.GetAccountViewAsync(accountId);
var transactions = await viewService.GetTransactionViewListAsync(accountId);
if (transactions.Count == 0)
{
return new List<DataSerie>();
}
var year = settingsService.GetYear();
var maxMonth = transactions.Max(t => t.ValueDate.Month);
var list = new List<DataSerie>();
foreach (var grp in transactions.GroupBy(t => t.Text))
{
var groupedByMonth = grp
.GroupBy(t => t.ValueDate.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(account.IsFxAccount, accountId)));
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
list.Add(new DataSerie(
Id: grp.Key,
Name: grp.Key,
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
));
}
return list;
}
public async Task<IReadOnlyList<CategoryTotal>> GetBookingTextTotalsAsync(string accountId)
{
var account = await viewService.GetAccountViewAsync(accountId);
var transactions = await viewService.GetTransactionViewListAsync(accountId);
return transactions
.GroupBy(t => t.Text)
.Select(g => new CategoryTotal(g.Key, g.Sum(t => t.GetValue(account.IsFxAccount, accountId))))
.Where(c => c.Value != 0)
.ToList();
}
private static void EnsureEntryForEveryMonth(Dictionary<int, decimal> values, int maxMonth = 12)
{
for (int i = 1; i <= maxMonth; i++)
{
if (values.ContainsKey(i) == false)
{
values.Add(i, 0);
}
}
}
}
}