Compare commits
3 commits
1dcccf89b1
...
e6d28a6e85
| Author | SHA1 | Date | |
|---|---|---|---|
| e6d28a6e85 | |||
| 40105189f5 | |||
| d819a836da |
6 changed files with 122 additions and 32 deletions
|
|
@ -33,5 +33,15 @@ namespace Schaad.Accounting.Datasets
|
||||||
else
|
else
|
||||||
return Value;
|
return Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public decimal GetValue(bool withFxRate, string accountId)
|
||||||
|
{
|
||||||
|
if (accountId == OriginAccountId)
|
||||||
|
{
|
||||||
|
return -1 * GetValue(withFxRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetValue(withFxRate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +112,7 @@ namespace Schaad.Accounting.Services
|
||||||
{
|
{
|
||||||
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.GetValue(account.IsFxAccount)));
|
.ToDictionary(g => g.Key, g => g.Sum(t => t.GetValue(account.IsFxAccount, accountId)));
|
||||||
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
|
EnsureEntryForEveryMonth(groupedByMonth, maxMonth);
|
||||||
|
|
||||||
list.Add(new DataSerie(
|
list.Add(new DataSerie(
|
||||||
|
|
@ -131,8 +131,9 @@ namespace Schaad.Accounting.Services
|
||||||
var transactions = await viewService.GetTransactionViewListAsync(accountId);
|
var transactions = await viewService.GetTransactionViewListAsync(accountId);
|
||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
|
.Where(t => t.TargetAccountId == accountId)
|
||||||
.GroupBy(t => t.Text)
|
.GroupBy(t => t.Text)
|
||||||
.Select(g => new CategoryTotal(g.Key, g.Sum(t => t.GetValue(account.IsFxAccount))))
|
.Select(g => new CategoryTotal(g.Key, g.Sum(t => t.GetValue(account.IsFxAccount, accountId))))
|
||||||
.Where(c => c.Value != 0)
|
.Where(c => c.Value != 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,9 @@ namespace Schaad.Accounting.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get transaction list filtered to a single account, with values sign-flipped
|
/// Get transaction list filtered to a single account. Sign is not baked in here -
|
||||||
/// when the requested account is the origin of an activa booking.
|
/// callers use <see cref="TransactionDataset.GetValue(bool, string)"/> with this
|
||||||
|
/// account's id to get a value that's negative when the account is paying out.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<List<TransactionDataset>> GetTransactionViewListAsync(string accountId)
|
public async Task<List<TransactionDataset>> GetTransactionViewListAsync(string accountId)
|
||||||
{
|
{
|
||||||
|
|
@ -143,22 +144,10 @@ namespace Schaad.Accounting.Services
|
||||||
|
|
||||||
return transactionList.Select(t =>
|
return transactionList.Select(t =>
|
||||||
new TransactionDataset(
|
new TransactionDataset(
|
||||||
WithDisplaySign(t),
|
t,
|
||||||
accountsById[t.OriginAccountId],
|
accountsById[t.OriginAccountId],
|
||||||
accountsById[t.TargetAccountId]))
|
accountsById[t.TargetAccountId]))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
Transaction WithDisplaySign(Transaction t)
|
|
||||||
{
|
|
||||||
var originAccount = accountsById[t.OriginAccountId];
|
|
||||||
if (originAccount.Class == ClassIds.Activa && accountId == t.OriginAccountId)
|
|
||||||
{
|
|
||||||
var copy = t.Clone();
|
|
||||||
copy.Value *= -1;
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -186,11 +186,27 @@ public class ChartServiceTestShould
|
||||||
|
|
||||||
series.Count.ShouldBe(2);
|
series.Count.ShouldBe(2);
|
||||||
var migros = series.Single(s => s.Name == "Migros");
|
var migros = series.Single(s => s.Name == "Migros");
|
||||||
migros.Y[0].ShouldBe(50m);
|
migros.Y[0].ShouldBe(-50m);
|
||||||
migros.Y[1].ShouldBe(0m);
|
migros.Y[1].ShouldBe(0m);
|
||||||
var coop = series.Single(s => s.Name == "Coop");
|
var coop = series.Single(s => s.Name == "Coop");
|
||||||
coop.Y[0].ShouldBe(0m);
|
coop.Y[0].ShouldBe(0m);
|
||||||
coop.Y[1].ShouldBe(20m);
|
coop.Y[1].ShouldBe(-20m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShowPositiveValueWhenAccountIsTargetOfBookingTextTransaction()
|
||||||
|
{
|
||||||
|
var checking = MakeAccount("chk", 1010);
|
||||||
|
var salary = MakeAccount("sal", 3000);
|
||||||
|
viewService.GetAccountViewAsync("chk").Returns(AsDataset(checking));
|
||||||
|
viewService.GetTransactionViewListAsync("chk").Returns(new List<TransactionDataset>
|
||||||
|
{
|
||||||
|
MakeTransaction(salary, checking, value: 5000m, valueDate: new DateTime(2026, 1, 5), text: "Lohn")
|
||||||
|
});
|
||||||
|
|
||||||
|
var series = await BuildService().GetBookingTextExpensesPerMonthAsync("chk");
|
||||||
|
|
||||||
|
series.Single().Y.Single().ShouldBe(5000m);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -206,7 +222,7 @@ public class ChartServiceTestShould
|
||||||
|
|
||||||
var series = await BuildService().GetBookingTextExpensesPerMonthAsync("chk");
|
var series = await BuildService().GetBookingTextExpensesPerMonthAsync("chk");
|
||||||
|
|
||||||
series.Single().Y.Single().ShouldBe(200m);
|
series.Single().Y.Single().ShouldBe(-200m);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -225,8 +241,25 @@ public class ChartServiceTestShould
|
||||||
var totals = await BuildService().GetBookingTextTotalsAsync("chk");
|
var totals = await BuildService().GetBookingTextTotalsAsync("chk");
|
||||||
|
|
||||||
totals.Count.ShouldBe(2);
|
totals.Count.ShouldBe(2);
|
||||||
totals.ShouldContain(t => t.Name == "Migros" && t.Value == 80m);
|
totals.ShouldContain(t => t.Name == "Migros" && t.Value == -80m);
|
||||||
totals.ShouldContain(t => t.Name == "Coop" && t.Value == 20m);
|
totals.ShouldContain(t => t.Name == "Coop" && t.Value == -20m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NetOutgoingAndIncomingBookingsWithTheSameTextWhenBuildingTotalsForAccount()
|
||||||
|
{
|
||||||
|
var checking = MakeAccount("chk", 1010);
|
||||||
|
var groceries = MakeAccount("groc", 4200);
|
||||||
|
viewService.GetAccountViewAsync("chk").Returns(AsDataset(checking));
|
||||||
|
viewService.GetTransactionViewListAsync("chk").Returns(new List<TransactionDataset>
|
||||||
|
{
|
||||||
|
MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Migros"),
|
||||||
|
MakeTransaction(groceries, checking, value: 20m, valueDate: new DateTime(2026, 1, 6), text: "Migros")
|
||||||
|
});
|
||||||
|
|
||||||
|
var totals = await BuildService().GetBookingTextTotalsAsync("chk");
|
||||||
|
|
||||||
|
totals.Single().Value.ShouldBe(-30m);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
54
Schaad.Accounting.Tests/TransactionDatasetTestShould.cs
Normal file
54
Schaad.Accounting.Tests/TransactionDatasetTestShould.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using Schaad.Accounting.Datasets;
|
||||||
|
using Schaad.Accounting.Models;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace Schaad.Accounting.Tests;
|
||||||
|
|
||||||
|
public class TransactionDatasetTestShould
|
||||||
|
{
|
||||||
|
private static Account MakeAccount(string id) => new() { Id = id, Name = id, Currency = "CHF" };
|
||||||
|
|
||||||
|
private static TransactionDataset MakeTransaction(Account origin, Account target, decimal value, decimal? fxRate = null) =>
|
||||||
|
new(
|
||||||
|
new Transaction
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
OriginAccountId = origin.Id,
|
||||||
|
TargetAccountId = target.Id,
|
||||||
|
Value = value,
|
||||||
|
FxRate = fxRate,
|
||||||
|
ValueDate = DateTime.Now,
|
||||||
|
BookingDate = DateTime.Now
|
||||||
|
},
|
||||||
|
origin, target);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnNegativeValueWhenAccountIsOrigin()
|
||||||
|
{
|
||||||
|
var checking = MakeAccount("chk");
|
||||||
|
var groceries = MakeAccount("groc");
|
||||||
|
var transaction = MakeTransaction(checking, groceries, value: 50m);
|
||||||
|
|
||||||
|
transaction.GetValue(withFxRate: false, accountId: "chk").ShouldBe(-50m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnPositiveValueWhenAccountIsTarget()
|
||||||
|
{
|
||||||
|
var checking = MakeAccount("chk");
|
||||||
|
var groceries = MakeAccount("groc");
|
||||||
|
var transaction = MakeTransaction(groceries, checking, value: 50m);
|
||||||
|
|
||||||
|
transaction.GetValue(withFxRate: false, accountId: "chk").ShouldBe(50m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyFxConversionBeforeApplyingSignWhenAccountIsOrigin()
|
||||||
|
{
|
||||||
|
var checking = MakeAccount("chk");
|
||||||
|
var groceries = MakeAccount("groc");
|
||||||
|
var transaction = MakeTransaction(checking, groceries, value: 100m, fxRate: 0.5m);
|
||||||
|
|
||||||
|
transaction.GetValue(withFxRate: true, accountId: "chk").ShouldBe(-200m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,14 +44,17 @@
|
||||||
</FluentGridItem>
|
</FluentGridItem>
|
||||||
</FluentGrid>
|
</FluentGrid>
|
||||||
|
|
||||||
<FluentGrid Justify="JustifyContent.FlexStart">
|
@if (selectedAccount.Class == ClassIds.Expenses)
|
||||||
<FluentGridItem xs="6">
|
{
|
||||||
<BookingTextOverTime @key="selectedAccount.Id" AccountId="@selectedAccount.Id"/>
|
<FluentGrid Justify="JustifyContent.FlexStart">
|
||||||
</FluentGridItem>
|
<FluentGridItem xs="6">
|
||||||
<FluentGridItem xs="6">
|
<BookingTextOverTime @key="selectedAccount.Id" AccountId="@selectedAccount.Id"/>
|
||||||
<BookingTextBreakdown @key="selectedAccount.Id" AccountId="@selectedAccount.Id"/>
|
</FluentGridItem>
|
||||||
</FluentGridItem>
|
<FluentGridItem xs="6">
|
||||||
</FluentGrid>
|
<BookingTextBreakdown @key="selectedAccount.Id" AccountId="@selectedAccount.Id"/>
|
||||||
|
</FluentGridItem>
|
||||||
|
</FluentGrid>
|
||||||
|
}
|
||||||
|
|
||||||
<FluentPaginator State="@pagination" SummaryTemplate="@template"/>
|
<FluentPaginator State="@pagination" SummaryTemplate="@template"/>
|
||||||
<FluentDataGrid Items="@transactionList" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
|
<FluentDataGrid Items="@transactionList" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
|
||||||
|
|
@ -62,7 +65,7 @@
|
||||||
@GetAccountName(context)
|
@GetAccountName(context)
|
||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
<TemplateColumn Title="Betrag" Align="Align.End">
|
<TemplateColumn Title="Betrag" Align="Align.End">
|
||||||
@context.GetValue(selectedAccount.IsFxAccount).ToString("N2") @selectedAccount.Currency
|
@context.GetValue(selectedAccount.IsFxAccount, selectedAccountId).ToString("N2") @selectedAccount.Currency
|
||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
<TemplateColumn Align="Align.Center" Width="50px">
|
<TemplateColumn Align="Align.Center" Width="50px">
|
||||||
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" title="Löschen" aria-label="Löschen" OnClick="@(() => DeleteAsync(context.Id))"/>
|
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" title="Löschen" aria-label="Löschen" OnClick="@(() => DeleteAsync(context.Id))"/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue