Usability U01–U16: sichere Abläufe und Bedienkorrekturen #1
@@ -23,3 +23,7 @@ Prüfung U01: 16/16 Debug-Tests bestanden, darunter WAL-Rundlauf mit exaktem Buc
|
|||||||
## U02 – Dateiaktionen
|
## U02 – Dateiaktionen
|
||||||
|
|
||||||
Strukturierte Ergebnisse und gemeinsame Statusanzeige mit optionalen technischen Details, Speicherpfad und „Ordner öffnen“. Abbruch ist neutral. Dateiaktionen sperren erneutes Auslösen; Fehler beim Erzeugen oder Öffnen einer PDF bleiben in der aktuellen Ansicht sichtbar. Native Dateidialoge melden Start-/Prozessfehler und erhalten Unicode-Pfade. Gespeichert wird über eine temporäre Datei, damit Schreibfehler eine bestehende Datei nicht verstümmeln. Prüfung: 18/18 Debug-Tests bestanden.
|
Strukturierte Ergebnisse und gemeinsame Statusanzeige mit optionalen technischen Details, Speicherpfad und „Ordner öffnen“. Abbruch ist neutral. Dateiaktionen sperren erneutes Auslösen; Fehler beim Erzeugen oder Öffnen einer PDF bleiben in der aktuellen Ansicht sichtbar. Native Dateidialoge melden Start-/Prozessfehler und erhalten Unicode-Pfade. Gespeichert wird über eine temporäre Datei, damit Schreibfehler eine bestehende Datei nicht verstümmeln. Prüfung: 18/18 Debug-Tests bestanden.
|
||||||
|
|
||||||
|
## U03 – Gleiche Bedeutung in PDF und Oberfläche
|
||||||
|
|
||||||
|
Einzelkonto und Übersicht enthalten nur wirksame Buchungen. Zeitraum und Bedeutung des Endstands sind ausdrücklich benannt, auch bei „Alle Jahre“ mit Anfangsbestand. Im Einzelkonto zählen Umbuchungen als Zu-/Abgang; die Übersicht zeigt beide Konten in einer Zeile und erklärt die unveränderte Vereinssumme. Prüfung: 19/19 Debug-Tests. PDF-Textextraktion prüft 58,30 € Kontostand, 108,30 € Vereinssumme, ausgeschlossene gelöschte/spätere Buchungen und Richtung. Einzelkonto, gemeinsame Übersicht und Seiten 2/3 eines dreiseitigen Auszugs gerendert und visuell auf Lesbarkeit, wiederholte Kopfzeilen und Abschluss geprüft.
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ public class PdfStatementService : IPdfStatementService
|
|||||||
{
|
{
|
||||||
col.Item().Text(clubName).Bold().FontSize(20);
|
col.Item().Text(clubName).Bold().FontSize(20);
|
||||||
col.Item().Text(title).FontSize(14).FontColor(Colors.Grey.Darken1);
|
col.Item().Text(title).FontSize(14).FontColor(Colors.Grey.Darken1);
|
||||||
|
col.Item().Text(year.HasValue ? $"Zeitraum: 01.01.{year} bis 31.12.{year}" : "Zeitraum: Alle Jahre").FontSize(9);
|
||||||
|
col.Item().Text("Nur wirksame Buchungen. Umbuchungen zählen im einzelnen Konto als Zu- oder Abgang.").FontSize(8);
|
||||||
col.Item().PaddingTop(5).Text($"Erstellt am: {DateTime.Now:dd.MM.yyyy}").FontSize(8).FontColor(Colors.Grey.Medium);
|
col.Item().PaddingTop(5).Text($"Erstellt am: {DateTime.Now:dd.MM.yyyy}").FontSize(8).FontColor(Colors.Grey.Medium);
|
||||||
col.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
col.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
||||||
});
|
});
|
||||||
@@ -94,7 +96,7 @@ public class PdfStatementService : IPdfStatementService
|
|||||||
.PaddingBottom(5).AlignRight().Text("Betrag").SemiBold();
|
.PaddingBottom(5).AlignRight().Text("Betrag").SemiBold();
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries.Where(e => !e.IsDeleted))
|
||||||
{
|
{
|
||||||
var titleText = entry.Title;
|
var titleText = entry.Title;
|
||||||
if (entry.IsTransfer && !string.IsNullOrEmpty(entry.LinkedAccountName))
|
if (entry.IsTransfer && !string.IsNullOrEmpty(entry.LinkedAccountName))
|
||||||
@@ -127,11 +129,16 @@ public class PdfStatementService : IPdfStatementService
|
|||||||
SummaryRow(summaryCol, $"Übertrag von {year.Value - 1}:", balance.CarryoverBalance);
|
SummaryRow(summaryCol, $"Übertrag von {year.Value - 1}:", balance.CarryoverBalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SummaryRow(summaryCol, "Anfangsbestand:", balance.CarryoverBalance);
|
||||||
|
}
|
||||||
|
|
||||||
SummaryRow(summaryCol, year.HasValue ? "Einnahmen:" : "Einnahmen gesamt:", incomeValue);
|
SummaryRow(summaryCol, year.HasValue ? "Einnahmen:" : "Einnahmen gesamt:", incomeValue);
|
||||||
SummaryRow(summaryCol, year.HasValue ? "Ausgaben:" : "Ausgaben gesamt:", -expenseValue);
|
SummaryRow(summaryCol, year.HasValue ? "Ausgaben:" : "Ausgaben gesamt:", -expenseValue);
|
||||||
summaryCol.Item().PaddingTop(5).Row(row =>
|
summaryCol.Item().PaddingTop(5).Row(row =>
|
||||||
{
|
{
|
||||||
row.RelativeItem().Text("Saldo:").Bold().FontSize(12);
|
row.RelativeItem().Text(year.HasValue ? $"Kontostand am 31.12.{year}:" : "Kontostand nach allen Buchungen:").Bold().FontSize(12);
|
||||||
row.AutoItem().Text(FormatCurrency(balance.TotalBalance)).Bold().FontSize(12)
|
row.AutoItem().Text(FormatCurrency(balance.TotalBalance)).Bold().FontSize(12)
|
||||||
.FontColor(balance.TotalBalance >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1);
|
.FontColor(balance.TotalBalance >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1);
|
||||||
});
|
});
|
||||||
@@ -302,6 +309,8 @@ public class PdfStatementService : IPdfStatementService
|
|||||||
? $"Übersicht aller Konten ({year.Value})"
|
? $"Übersicht aller Konten ({year.Value})"
|
||||||
: "Übersicht aller Konten";
|
: "Übersicht aller Konten";
|
||||||
col.Item().Text(headerTitle).FontSize(13).FontColor(Colors.Grey.Darken1);
|
col.Item().Text(headerTitle).FontSize(13).FontColor(Colors.Grey.Darken1);
|
||||||
|
col.Item().Text(year.HasValue ? $"Zeitraum: 01.01.{year} bis 31.12.{year}; Endstände am 31.12.{year}." : "Zeitraum: Alle Jahre; Endstände nach allen Buchungen.").FontSize(9);
|
||||||
|
col.Item().Text("Nur wirksame Buchungen. Interne Umbuchungen erscheinen einmal mit Minus und Plus; die Vereinssumme bleibt unverändert.").FontSize(8);
|
||||||
col.Item().PaddingTop(2).Text($"Gesamtvermögen: {FormatCurrency(totalClubBalance)}")
|
col.Item().PaddingTop(2).Text($"Gesamtvermögen: {FormatCurrency(totalClubBalance)}")
|
||||||
.FontSize(10)
|
.FontSize(10)
|
||||||
.SemiBold()
|
.SemiBold()
|
||||||
@@ -338,15 +347,14 @@ public class PdfStatementService : IPdfStatementService
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (year.HasValue && carryoverByAccountId is not null)
|
|
||||||
{
|
{
|
||||||
BodyCell(table, string.Empty);
|
BodyCell(table, string.Empty);
|
||||||
BodyCell(table, string.Empty);
|
BodyCell(table, string.Empty);
|
||||||
BodyCell(table, $"Übertrag von {year.Value - 1}", alignRight: true);
|
BodyCell(table, year.HasValue ? $"Übertrag von {year.Value - 1}" : "Anfangsbestand", alignRight: true);
|
||||||
|
|
||||||
foreach (var account in accounts)
|
foreach (var account in accounts)
|
||||||
{
|
{
|
||||||
BodyAmountCell(table, carryoverByAccountId.GetValueOrDefault(account.Id));
|
BodyAmountCell(table, carryoverByAccountId?.GetValueOrDefault(account.Id) ?? account.CarryoverBalance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,62 @@ public class DashboardPdfStatementServiceTests : IDisposable
|
|||||||
System.Text.Encoding.ASCII.GetString(pdf.Take(4).ToArray()).Should().Be("%PDF");
|
System.Text.Encoding.ASCII.GetString(pdf.Take(4).ToArray()).Should().Be("%PDF");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Exports_ExcludeDeleted_ExplainTransfers_AndMatchBalances()
|
||||||
|
{
|
||||||
|
var source = new Account { Name = "Barkasse", CarryoverBalance = 100m };
|
||||||
|
var target = new Account { Name = "Girokonto" };
|
||||||
|
_db.Accounts.AddRange(source, target);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<FinanceDbContext>().UseSqlite(_connectionString).Options;
|
||||||
|
var factory = new TestDbContextFactory(options);
|
||||||
|
var entries = new EntryService(factory);
|
||||||
|
await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2026, 1, 1), "Mitgliedsbeitrag", 12.50m);
|
||||||
|
await entries.CreateEntryAsync(source.Id, EntryType.Expense, new(2026, 1, 2), "Porto", 4.20m);
|
||||||
|
await entries.CreateTransferAsync(source.Id, target.Id, new(2026, 1, 3), "Zur Bank", 50m);
|
||||||
|
var deleted = await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2026, 1, 4), "GELOESCHTE_TESTBUCHUNG", 999m);
|
||||||
|
await entries.DeleteEntryAsync(deleted.Id);
|
||||||
|
await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2027, 1, 1), "ZUKUNFT_TESTBUCHUNG", 1m);
|
||||||
|
var balance = await new BalanceQueryService(factory).GetAccountBalanceAsync(source.Id, 2026);
|
||||||
|
Assert.Equal(58.30m, balance.TotalBalance);
|
||||||
|
var single = await _pdfService.GenerateStatementAsync(source.Id, 2026);
|
||||||
|
var combined = await _pdfService.GenerateDashboardStatementAsync(2026);
|
||||||
|
foreach (var pdf in new[] { single, combined })
|
||||||
|
{
|
||||||
|
var text = Extract(pdf);
|
||||||
|
Assert.Contains("Mitgliedsbeitrag", text);
|
||||||
|
Assert.Contains("Porto", text);
|
||||||
|
Assert.Contains("Zur Bank", text);
|
||||||
|
Assert.DoesNotContain("GELOESCHTE_TESTBUCHUNG", text);
|
||||||
|
Assert.DoesNotContain("ZUKUNFT_TESTBUCHUNG", text);
|
||||||
|
Assert.Contains("58,30", text);
|
||||||
|
Assert.Contains("31.12.2026", text);
|
||||||
|
}
|
||||||
|
Assert.Contains("108,30", Extract(combined));
|
||||||
|
Assert.Contains("an Girokonto", Extract(single));
|
||||||
|
var all = Extract(await _pdfService.GenerateStatementAsync(source.Id, (int?)null));
|
||||||
|
Assert.Contains("Alle Jahre", all);
|
||||||
|
Assert.Contains("Anfangsbestand", all);
|
||||||
|
Assert.Contains("59,30", all);
|
||||||
|
var evidence = Environment.GetEnvironmentVariable("DUEMPELKAS_PDF_EVIDENCE");
|
||||||
|
if (evidence != null)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(evidence);
|
||||||
|
await File.WriteAllBytesAsync(Path.Combine(evidence, "single.pdf"), single);
|
||||||
|
await File.WriteAllBytesAsync(Path.Combine(evidence, "combined.pdf"), combined);
|
||||||
|
// Additional pages exercise repeated headers and the final summary.
|
||||||
|
for (var i = 0; i < 90; i++)
|
||||||
|
await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2026, 2, 1), $"Lesbarkeitsprüfung Buchung {i + 1}", 1m);
|
||||||
|
await File.WriteAllBytesAsync(Path.Combine(evidence, "multipage.pdf"), await _pdfService.GenerateStatementAsync(source.Id, 2026));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Extract(byte[] pdf)
|
||||||
|
{
|
||||||
|
using var document = UglyToad.PdfPig.PdfDocument.Open(pdf);
|
||||||
|
return string.Join("\n", document.GetPages().Select(page => UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor.ContentOrderTextExtractor.GetText(page)));
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_db.Database.CloseConnection();
|
_db.Database.CloseConnection();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
|
||||||
|
<PackageReference Include="PdfPig" Version="0.1.16" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1">
|
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
Reference in New Issue
Block a user