Display transactions and balances in account currency

- Transaction amounts, totals, and sidebar badges in the Transactions page now show in the account's native currency (EUR, USD, etc.) instead of always CHF
- Fixed FX conversion in SaveTransaction/GetTransaction to fire when either the origin or target account is a foreign-currency account, not only the origin side

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-05-23 16:08:01 +02:00 committed by GitHub
parent 2c91ac7082
commit a16aedcfd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 11 deletions

64
CLAUDE.md Normal file
View file

@ -0,0 +1,64 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Build
dotnet build
# Run (dev server at http://localhost:5225)
cd Schaad.Accounting.UI && dotnet run
# Publish
dotnet publish -c Release
```
There are no automated tests or lint commands.
## Architecture
**AccountingNext** is a Swiss personal accounting app built with ASP.NET Core 9 + Blazor (interactive server-side rendering) and Microsoft Fluent UI. Data is stored as XML files on disk — no database.
### Projects
| Project | Role |
|---|---|
| `Schaad.Accounting.UI` | Blazor web app — pages, dialogs, layout |
| `Schaad.Accounting.Services` | Business logic — balances, FX conversion, file import, charts |
| `Schaad.Accounting.Db` | Repository layer — XML serialization/deserialization |
| `Schaad.Accounting.Common` | Shared models, DTOs, interfaces |
### Data flow
```
Blazor Pages/Dialogs (UI)
→ Services (ViewService, FileService, ChartService)
→ Repositories (AccountRepository, TransactionRepository, …)
→ XML files: {DataPath}/{year}/{mandator}/Data/*.xml
```
Data is multi-year and multi-mandator. Each combination has its own directory under `DataPath` (configured in `appsettings.Development.json`). When a new year is opened, the previous year's data is copied as the starting point.
### Key services
- **ViewService** — aggregates account balances, applies FX rates, builds datasets for the UI
- **FileService** — imports bank statements (MT940, CAMT053 via external `Schaad.Finance.dll`), parses PDFs, manages backups
- **ChartService** — produces data for Plotly charts (assets, spendings over time)
- **SettingsService** — manages the active year, mandator, and file paths
### UI structure
Pages live in `Schaad.Accounting.UI/Components/Pages/`. Each page typically has a companion `Dialogs/` subfolder with Fluent UI dialog components for CRUD operations. The app is hardcoded to the `de-CH` culture.
### Key external dependencies
- `Microsoft.FluentUI.AspNetCore.Components` — UI components (FluentDataGrid, FluentDialog, etc.)
- `Plotly.Blazor` — charts
- `FreeSpire.PDF` — PDF parsing for bank statement imports
- `Schaad.Finance.dll` / `Schaad.Finance.Api.dll` — proprietary DLLs for MT940/CAMT053 parsing and FX rate lookups (FixerIo API key in `appsettings.Development.json`)
### Domain constants
`ClassIds` in `Schaad.Accounting.Common` defines the Swiss accounting chart-of-accounts classes: Activa=1000, Passiva=2000, Income=3000, Expenses=4000.

View file

@ -31,18 +31,18 @@ namespace Schaad.Accounting.Repositories
public void SaveTransaction(Transaction transaction)
{
transaction.Value = Math.Abs(transaction.Value);
// prevent saving fxrate for chf account
var isFxAccount = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount;
// if source account is fx account, we pass a foreign value -> calc to chf
var originIsFx = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount;
var targetIsFx = accountRepository.GetAccount(transaction.TargetAccountId).IsFxAccount;
var isFxAccount = originIsFx || targetIsFx;
// user enters amount in the foreign account's currency -> convert to CHF for storage
if (isFxAccount && transaction.FxRate != 0)
{
transaction.Value = transaction.Value * transaction.FxRate.Value;
}
isFxAccount |= accountRepository.GetAccount(transaction.TargetAccountId).IsFxAccount;
if (isFxAccount == false)
if (!isFxAccount)
{
transaction.FxRate = null;
}
@ -69,8 +69,9 @@ namespace Schaad.Accounting.Repositories
var transactions = GetTransactionList();
var transaction = transactions.FirstOrDefault(t => t.Id == id);
// if source account is fx account, we save in chf currency -> calc to foreign currency for display
var isFxAccount = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount;
// value is stored in CHF -> convert back to foreign currency for display/editing
var isFxAccount = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount
|| accountRepository.GetAccount(transaction.TargetAccountId).IsFxAccount;
if (isFxAccount && transaction.FxRate != 0)
{
transaction.Value = transaction.Value / transaction.FxRate.Value;

View file

@ -17,7 +17,7 @@
</div>
<div style="margin-left: auto;">
<FluentBadge Appearance="Appearance.Neutral">
@Math.Abs(account.BalanceCHF).ToString("C2")
@Math.Abs(account.Balance).ToString("N2") @account.Currency
</FluentBadge>
</div>
</FluentStack>
@ -34,7 +34,7 @@
@selectedAccount.Name
</FluentGridItem>
<FluentGridItem xs="4" Class="align-content-center">
Total @Math.Abs(selectedAccount.Balance).ToString("C2") in @transactionList?.Count() Transaktionen
Total @Math.Abs(selectedAccount.Balance).ToString("N2") @selectedAccount.Currency in @transactionList?.Count() Transaktionen
</FluentGridItem>
<FluentGridItem xs="4" Class="align-content-center" Justify="JustifyContent.FlexEnd">
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neue Transaktion hinzufügen</FluentButton>
@ -49,7 +49,9 @@
<TemplateColumn Title="Gegenkonto">
@GetAccountName(context)
</TemplateColumn>
<PropertyColumn Property="@(p => p.Value)" Format="C2" Align="Align.End"/>
<TemplateColumn Title="Betrag" Align="Align.End">
@context.GetValue(selectedAccount.IsFxAccount).ToString("N2") @selectedAccount.Currency
</TemplateColumn>
<TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))"/>
</TemplateColumn>