Fix SpendingsOverTime chart: apply FX conversion and use subclass names
ChartService summed raw transaction Value instead of converting via GetValue(account.IsFxAccount), so FX expense accounts charted in their foreign currency labeled as CHF. Subclass series also took their Name from an arbitrary account in the group instead of the actual subclass name.
This commit is contained in:
parent
90660177f2
commit
a19bdf4558
2 changed files with 56 additions and 13 deletions
|
|
@ -31,28 +31,32 @@ namespace Schaad.Accounting.Services
|
||||||
.Count();
|
.Count();
|
||||||
|
|
||||||
return distinctSubClasses > 1
|
return distinctSubClasses > 1
|
||||||
? GetSubClassExpensesPerMonth(expenseTransactions)
|
? await GetSubClassExpensesPerMonthAsync(expenseTransactions)
|
||||||
: await GetAccountExpensesPerMonthAsync();
|
: await GetAccountExpensesPerMonthAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DataSerie> GetSubClassExpensesPerMonth(List<TransactionDataset> expenseTransactions)
|
private async Task<List<DataSerie>> GetSubClassExpensesPerMonthAsync(List<TransactionDataset> expenseTransactions)
|
||||||
{
|
{
|
||||||
var year = settingsService.GetYear();
|
var year = settingsService.GetYear();
|
||||||
var maxMonth = expenseTransactions
|
var maxMonth = expenseTransactions
|
||||||
.OrderByDescending(t => t.ValueDate)
|
.OrderByDescending(t => t.ValueDate)
|
||||||
.FirstOrDefault()?.ValueDate.Month ?? 12;
|
.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>();
|
var list = new List<DataSerie>();
|
||||||
foreach (var grp in expenseTransactions.GroupBy(t => t.TargetAccount.SubClass))
|
foreach (var grp in expenseTransactions.GroupBy(t => t.TargetAccount.SubClass))
|
||||||
{
|
{
|
||||||
var groupedByMonth = grp
|
var groupedByMonth = grp
|
||||||
.GroupBy(t => t.ValueDate.Month)
|
.GroupBy(t => t.ValueDate.Month)
|
||||||
.ToDictionary(g => g.Key, g => g.Sum(t => t.Value));
|
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(t.TargetAccount.IsFxAccount)));
|
||||||
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
|
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
|
||||||
|
|
||||||
list.Add(new DataSerie(
|
list.Add(new DataSerie(
|
||||||
Id: grp.Key.ToString(),
|
Id: grp.Key.ToString(),
|
||||||
Name: grp.First().TargetAccount.Name,
|
Name: subClassNameByNumber[grp.Key],
|
||||||
X: groupedByMonth.OrderBy(g => g.Key).Select(g => new DateOnly(year, g.Key, 1)).ToList(),
|
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()
|
Y: groupedByMonth.OrderBy(g => g.Key).Select(g => g.Value).ToList()
|
||||||
));
|
));
|
||||||
|
|
@ -78,7 +82,7 @@ namespace Schaad.Accounting.Services
|
||||||
var maxMonth = transactions.Max(t => t.ValueDate.Month);
|
var maxMonth = transactions.Max(t => t.ValueDate.Month);
|
||||||
var groupedByMonth = transactions
|
var groupedByMonth = transactions
|
||||||
.GroupBy(t => t.ValueDate.Month)
|
.GroupBy(t => t.ValueDate.Month)
|
||||||
.ToDictionary(g => g.Key, g => g.Sum(t => t.Value));
|
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(account.IsFxAccount)));
|
||||||
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
|
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
|
||||||
|
|
||||||
list.Add(new DataSerie(
|
list.Add(new DataSerie(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Linq;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Schaad.Accounting.Datasets;
|
using Schaad.Accounting.Datasets;
|
||||||
using Schaad.Accounting.Interfaces;
|
using Schaad.Accounting.Interfaces;
|
||||||
|
|
@ -70,7 +71,7 @@ public class ChartServiceTestShould
|
||||||
var checking = MakeAccount("chk", 1010);
|
var checking = MakeAccount("chk", 1010);
|
||||||
viewService.GetAccountViewListAsync().Returns(new List<AccountDataset>
|
viewService.GetAccountViewListAsync().Returns(new List<AccountDataset>
|
||||||
{
|
{
|
||||||
AsDataset(groceries), AsDataset(utilities), AsDataset(checking)
|
AsDataset(groceries, subClassName: "Groceries"), AsDataset(utilities, subClassName: "Utilities"), AsDataset(checking)
|
||||||
});
|
});
|
||||||
viewService.GetTransactionViewListAsync().Returns(new List<TransactionDataset>
|
viewService.GetTransactionViewListAsync().Returns(new List<TransactionDataset>
|
||||||
{
|
{
|
||||||
|
|
@ -81,8 +82,8 @@ public class ChartServiceTestShould
|
||||||
var series = await BuildService().GetExpensesPerMonthAsync();
|
var series = await BuildService().GetExpensesPerMonthAsync();
|
||||||
|
|
||||||
series.Count.ShouldBe(2);
|
series.Count.ShouldBe(2);
|
||||||
series.ShouldContain(s => s.Id == "42");
|
series.ShouldContain(s => s.Id == "42" && s.Name == "Groceries");
|
||||||
series.ShouldContain(s => s.Id == "43");
|
series.ShouldContain(s => s.Id == "43" && s.Name == "Utilities");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -106,18 +107,18 @@ public class ChartServiceTestShould
|
||||||
series[0].Id.ShouldBe("groc");
|
series[0].Id.ShouldBe("groc");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Account MakeAccount(string id, int number) => new()
|
private static Account MakeAccount(string id, int number, string currency = "CHF") => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Number = number,
|
Number = number,
|
||||||
Name = id,
|
Name = id,
|
||||||
Currency = "CHF"
|
Currency = currency
|
||||||
};
|
};
|
||||||
|
|
||||||
private static AccountDataset AsDataset(Account a) =>
|
private static AccountDataset AsDataset(Account a, string subClassName = "") =>
|
||||||
new(a, balance: 0, balanceCHF: 0, startBalanceCHF: 0, className: "", subClassName: "");
|
new(a, balance: 0, balanceCHF: 0, startBalanceCHF: 0, className: "", subClassName: subClassName);
|
||||||
|
|
||||||
private static TransactionDataset MakeTransaction(Account origin, Account target, decimal value, DateTime valueDate) =>
|
private static TransactionDataset MakeTransaction(Account origin, Account target, decimal value, DateTime valueDate, decimal? fxRate = null) =>
|
||||||
new(
|
new(
|
||||||
new Transaction
|
new Transaction
|
||||||
{
|
{
|
||||||
|
|
@ -125,8 +126,46 @@ public class ChartServiceTestShould
|
||||||
OriginAccountId = origin.Id,
|
OriginAccountId = origin.Id,
|
||||||
TargetAccountId = target.Id,
|
TargetAccountId = target.Id,
|
||||||
Value = value,
|
Value = value,
|
||||||
|
FxRate = fxRate,
|
||||||
ValueDate = valueDate,
|
ValueDate = valueDate,
|
||||||
BookingDate = valueDate
|
BookingDate = valueDate
|
||||||
},
|
},
|
||||||
origin, target);
|
origin, target);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConvertFxAccountValuesWhenBuildingAccountSeries()
|
||||||
|
{
|
||||||
|
var usSubscription = MakeAccount("us", 4000, currency: "USD");
|
||||||
|
var checking = MakeAccount("chk", 1010);
|
||||||
|
viewService.GetAccountViewListAsync().Returns(new List<AccountDataset> { AsDataset(usSubscription), AsDataset(checking) });
|
||||||
|
viewService.GetTransactionViewListAsync().Returns(new List<TransactionDataset>
|
||||||
|
{
|
||||||
|
MakeTransaction(checking, usSubscription, value: 100m, valueDate: new DateTime(2026, 1, 1), fxRate: 0.5m)
|
||||||
|
});
|
||||||
|
|
||||||
|
var series = await BuildService().GetExpensesPerMonthAsync();
|
||||||
|
|
||||||
|
series.Single().Y.Single().ShouldBe(200m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConvertFxAccountValuesWhenBuildingSubClassSeries()
|
||||||
|
{
|
||||||
|
var usSubscription = MakeAccount("us", 4200, currency: "USD");
|
||||||
|
var utilities = MakeAccount("util", 4300);
|
||||||
|
var checking = MakeAccount("chk", 1010);
|
||||||
|
viewService.GetAccountViewListAsync().Returns(new List<AccountDataset>
|
||||||
|
{
|
||||||
|
AsDataset(usSubscription), AsDataset(utilities), AsDataset(checking)
|
||||||
|
});
|
||||||
|
viewService.GetTransactionViewListAsync().Returns(new List<TransactionDataset>
|
||||||
|
{
|
||||||
|
MakeTransaction(checking, usSubscription, value: 100m, valueDate: new DateTime(2026, 1, 1), fxRate: 0.5m),
|
||||||
|
MakeTransaction(checking, utilities, value: 30m, valueDate: new DateTime(2026, 2, 1))
|
||||||
|
});
|
||||||
|
|
||||||
|
var series = await BuildService().GetExpensesPerMonthAsync();
|
||||||
|
|
||||||
|
series.Single(s => s.Id == "42").Y[0].ShouldBe(200m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue