Anforderungsabgleich
parent
6b57a7b216
commit
082fee2923
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
title: "Anforderungsabgleich & Traceability-Matrix"
|
||||
subtitle: "Desktop-Fakturierungsanwendung"
|
||||
author:
|
||||
- Team 1
|
||||
version: "1.0"
|
||||
lang: de-DE
|
||||
---
|
||||
|
||||
# Anforderungsabgleich Lastenheft ↔ Implementierung
|
||||
|
||||
Dieses Dokument weist nach, dass die Implementierung alle Anforderungen des
|
||||
[Lastenheft.md](Lastenheft.md) (v1.3) erfüllt. Es dient als Traceability-Matrix
|
||||
(Anforderung ↔ Testfall ↔ Codebeleg) gemäß Abnahmebedingung Kap. 7.2 des Lastenhefts.
|
||||
|
||||
Status-Legende: ✅ erfüllt · 📋 organisatorischer Nachweis erforderlich
|
||||
|
||||
## 1. Fachliche Anforderungen (BA)
|
||||
|
||||
| Anf. | Inhalt | Codebeleg | Testfall | Status |
|
||||
|---|---|---|---|---|
|
||||
| BA-01 | Kunde anlegen (Pflicht-/Optionalfelder, eindeutige Nr.) | `KundenVerwaltungsService.legeAn`, `EinfacherKundennummernGenerator` | KundenVerwaltungTest TC-01 | ✅ |
|
||||
| BA-02 | Kundendaten ändern | `KundenVerwaltungsService.aendere` | KundenVerwaltungTest TC-07 | ✅ |
|
||||
| BA-03 | Kunden löschen (mit Löschsperre) | `KundenVerwaltungsService.loescheKunde` | KundenVerwaltungTest TC-09/TC-10 | ✅ |
|
||||
| BA-04 | Kunden suchen/auflisten (Name + Nr.) | `JsonKundenRepository.suche/alleSortiertNachName` | KundenVerwaltungTest TC-12/TC-13 | ✅ |
|
||||
| BA-05 | Produkt anlegen (Preis, Steuersatz, eindeutige Nr.) | `ProduktVerwaltungsService.legeAn` | ProduktVerwaltungTest TC-01 | ✅ |
|
||||
| BA-06 | Produktdaten ändern (Snapshot in Belegen) | `ProduktVerwaltungsService.aendere` | ProduktVerwaltungTest TC-06, DokumentzyklusTest TC-11 | ✅ |
|
||||
| BA-07 | Produkte löschen (mit Löschsperre) | `ProduktVerwaltungsService.loescheProdukt` | ProduktVerwaltungTest TC-08/TC-09 | ✅ |
|
||||
| BA-08 | Produkte suchen/auflisten (Bezeichnung + Nr.) | `JsonProduktRepository.suche/alleSortiertNachBezeichnung` | ProduktVerwaltungTest TC-11/TC-12 | ✅ |
|
||||
| BA-09 | Angebot erstellen + PDF | `StandardDokumentService.erstelleAngebot`, `PdfBoxPdfExporter` | DokumentzyklusTest TC-13 (analog) | ✅ |
|
||||
| BA-10 | Auftragsbestätigung erstellen + PDF | `StandardDokumentService.erstelleAuftragsbestaetigung` | DokumentzyklusTest TC-10 | ✅ |
|
||||
| BA-11 | Lieferschein erstellen + PDF | `StandardDokumentService.erstelleLieferschein` | DokumentzyklusTest TC-10 (Folgebeleg) | ✅ |
|
||||
| BA-12 | Rechnung erstellen (Nr., §14 UStG, Summen, Ziel) | `StandardDokumentService.erstelleRechnung` | DokumentzyklusTest TC-13 | ✅ |
|
||||
| BA-13 | Geführte Rechnungserstellung (Wizard) | `RechnungsWizardController/-Dialog/-Model` | OberflaechenControllerTest TC-01…TC-08 | ✅ |
|
||||
| BA-14 | Rechnung stornieren (Datum **und Benutzer**) | `Rechnung.storniere(datum, benutzer)`, `StandardDokumentService.SYSTEM_BENUTZER` | DokumentzyklusTest TC-09, OberflaechenControllerTest TC-10/TC-11 | ✅ |
|
||||
|
||||
## 2. Geschäftsregeln (GR)
|
||||
|
||||
| Anf. | Inhalt | Codebeleg | Testfall | Status |
|
||||
|---|---|---|---|---|
|
||||
| GR-01 | Lückenlose Rechnungsnummern | `EinfacherBelegnummernGenerator.ausRepository` | DokumentzyklusTest TC-04/TC-05 | ✅ |
|
||||
| GR-02 | Unveränderlichkeit versendeter Dokumente | `Dokument.pruefeAenderbar` | DokumentzyklusTest TC-08 | ✅ |
|
||||
| GR-03 | Steuerberechnung (Snapshot) | `Dokumentposition`, `Dokument.berechneSummen` | DokumentzyklusTest TC-01…TC-03, TC-11 | ✅ |
|
||||
| GR-04 | Referenzielle Integrität Kunden | `KundenVerwaltungsService.loescheKunde` | KundenVerwaltungTest TC-10 | ✅ |
|
||||
| GR-05 | Dokumentenzyklus-Konsistenz (Rückreferenz) | `StandardDokumentService.erzeugeFolgebeleg` | DokumentzyklusTest TC-10 | ✅ |
|
||||
| GR-06 | Standard-Zahlungsziel 14 Tage | `StandardDokumentService.STANDARD_ZAHLUNGSZIEL_TAGE` | DokumentzyklusTest TC-06/TC-07 | ✅ |
|
||||
|
||||
## 3. Qualitätsanforderungen (Q)
|
||||
|
||||
| Anf. | Inhalt | Codebeleg / Nachweis | Testfall | Status |
|
||||
|---|---|---|---|---|
|
||||
| Q-01 | Referenzgröße 5.000 Kunden/Produkte | In-Memory-Repositories, Seeding im Test | PerformanceTest (Seeding) | ✅ |
|
||||
| Q-02 | Suche/Auflistung ≤ 1 s | `JsonKundenRepository`/`JsonProduktRepository.suche` | PerformanceTest `q02Suche` | ✅ |
|
||||
| Q-03 | PDF-Erstellung ≤ 2 s (50 Positionen) | `PdfBoxPdfExporter` | PerformanceTest `q03PdfErstellung` | ✅ |
|
||||
| Q-04 | Anwendungsstart ≤ 5 s | Laden der drei JSON-Repositories | PerformanceTest `q04Anwendungsstart` | ✅ |
|
||||
| Q-05 | Ersterstellung Rechnung < 10 min (≥5 Personen) | Wizard `RechnungsWizardController` | Usability-Test (organisatorisch) | 📋 |
|
||||
| Q-06 | 100 % lokale Datenhaltung | nur lokales Dateisystem; `Desktop.mail()` = optionale IF-03 | Code-Inspektion / Netzwerk-Monitoring | ✅ |
|
||||
| Q-07 | Unveränderlichkeit versendeter Rechnungen | `Dokument.pruefeAenderbar` | DokumentzyklusTest TC-08 | ✅ |
|
||||
| Q-08 | Vollexport Stamm- **und Bewegungsdaten** ≤ 30 s | `KundenCsvExport`, `ProduktCsvExport`, **`DokumentCsvExport`** | DokumentCsvExportTest, PerformanceTest `q08Datenexport` | ✅ |
|
||||
| Q-09 | Pflichtfeldhinweis (≥80 % Korrektur) | `ValidierungsException`, `MeldungsAnzeige`, Wizard-Validierung | OberflaechenControllerTest TC-03…TC-05; Usability-Test | ✅ (Code) / 📋 (Usability) |
|
||||
|
||||
## 4. Akzeptanzkriterien (AC)
|
||||
|
||||
| Anf. | bezogen auf | Testfall | Status |
|
||||
|---|---|---|---|
|
||||
| AC-01 | BA-01, BA-04 | KundenVerwaltungTest TC-01, TC-12 | ✅ |
|
||||
| AC-02 | BA-02, BA-03, GR-04 | KundenVerwaltungTest TC-07, TC-10 | ✅ |
|
||||
| AC-03 | BA-05, BA-06, GR-02 | ProduktVerwaltungTest TC-06, DokumentzyklusTest TC-11 | ✅ |
|
||||
| AC-04 | BA-07, BA-08 | ProduktVerwaltungTest TC-08/TC-09, TC-11/TC-12; PerformanceTest `q02Suche` | ✅ |
|
||||
| AC-05 | BA-09, Q-03 | DokumentzyklusTest (Angebot), PerformanceTest `q03PdfErstellung` | ✅ |
|
||||
| AC-06 | BA-10 | DokumentzyklusTest TC-10 | ✅ |
|
||||
| AC-07 | BA-11 | DokumentzyklusTest TC-10 (Folgebeleg) | ✅ |
|
||||
| AC-08 | BA-12, GR-01, GR-06 | DokumentzyklusTest TC-04, TC-06, TC-13 | ✅ |
|
||||
| AC-09 | BA-13 | OberflaechenControllerTest TC-02, TC-07, TC-08 | ✅ |
|
||||
| AC-10 | BA-14 | DokumentzyklusTest TC-09, OberflaechenControllerTest TC-10/TC-11 | ✅ |
|
||||
| AC-11 | Q-09 | OberflaechenControllerTest TC-03…TC-05; Usability-Test | ✅ (Code) / 📋 (Usability) |
|
||||
|
||||
## 5. Offene organisatorische Punkte (Abnahmebedingung Kap. 7.2)
|
||||
|
||||
Folgende Punkte sind nicht durch Code/automatisierte Tests abdeckbar und vor der
|
||||
Endabnahme organisatorisch durchzuführen bzw. zu dokumentieren:
|
||||
|
||||
- **Q-05 / AC-11 – Usability-Tests** mit mindestens 5 Testpersonen
|
||||
(Ersterstellung einer Rechnung < 10 min; Pflichtfeldkorrektur ≥ 80 %).
|
||||
- **M-07 – Abschlusspräsentation** und Abnahme durch den Auftraggeber.
|
||||
|
||||
Alle übrigen Anforderungen (BA-01…BA-14, GR-01…GR-06, Q-01…Q-04, Q-06…Q-08
|
||||
sowie AC-01…AC-10) sind durch Code und automatisierte JUnit-Tests belegt.
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package de.team1.faktura;
|
||||
|
||||
import de.team1.faktura.dokumente.DokumentCsvExport;
|
||||
import de.team1.faktura.dokumente.DokumentReferenzPruefung;
|
||||
import de.team1.faktura.dokumente.DokumentService;
|
||||
import de.team1.faktura.dokumente.EinfacherBelegnummernGenerator;
|
||||
|
|
@ -78,7 +79,8 @@ public final class Main {
|
|||
HauptFenster fenster = new HauptFenster(
|
||||
new KundenPanel(kundenService, new KundenCsvExport(kundenRepository)),
|
||||
new ProduktPanel(produktService, new ProduktCsvExport(produktRepository)),
|
||||
new DokumentListenPanel(dokumentService, kundenService, produktService));
|
||||
new DokumentListenPanel(dokumentService, kundenService, produktService,
|
||||
new DokumentCsvExport(dokumentRepository)));
|
||||
fenster.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
package de.team1.faktura.dokumente;
|
||||
|
||||
import de.team1.faktura.gemeinsam.Csv;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static de.team1.faktura.gemeinsam.Csv.TRENNZEICHEN;
|
||||
import static de.team1.faktura.gemeinsam.Csv.feld;
|
||||
|
||||
/**
|
||||
* Export aller Belege (Bewegungsdaten) als CSV (Q-08, IF-04): UTF-8,
|
||||
* Semikolon-getrennt, mit Kopfzeile. Es wird eine Zeile je Dokumentposition
|
||||
* (denormalisiert) geschrieben, sodass der vollständige Datenbestand inklusive
|
||||
* Kopf- und Positionsdaten enthalten ist. Belege ohne Positionen erscheinen mit
|
||||
* leeren Positionsfeldern, damit kein Beleg verloren geht.
|
||||
*/
|
||||
public class DokumentCsvExport {
|
||||
|
||||
private final DokumentRepository repository;
|
||||
|
||||
public DokumentCsvExport(DokumentRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void exportiereCsv(Path zielDatei) {
|
||||
List<String> zeilen = new ArrayList<>();
|
||||
zeilen.add(String.join(TRENNZEICHEN,
|
||||
"belegnummer", "belegtyp", "datum", "status", "vorgaengerNr",
|
||||
"kundenNr", "kundeName", "kundeAnschrift",
|
||||
"summeNetto", "summeSteuer", "summeBrutto",
|
||||
"zahlungsziel", "storniertAm", "storniertVon",
|
||||
"produktnummer", "bezeichnung", "menge",
|
||||
"einzelpreisNetto", "steuersatz", "positionssummeNetto", "positionssummeBrutto"));
|
||||
for (Dokument dokument : repository.alle()) {
|
||||
if (dokument.getPositionen().isEmpty()) {
|
||||
zeilen.add(belegFelder(dokument) + TRENNZEICHEN + leerePositionsFelder());
|
||||
} else {
|
||||
for (Dokumentposition position : dokument.getPositionen()) {
|
||||
zeilen.add(belegFelder(dokument) + TRENNZEICHEN + positionsFelder(position));
|
||||
}
|
||||
}
|
||||
}
|
||||
Csv.schreibe(zielDatei, zeilen);
|
||||
}
|
||||
|
||||
private static String belegFelder(Dokument dokument) {
|
||||
Rechnung rechnung = dokument instanceof Rechnung r ? r : null;
|
||||
return String.join(TRENNZEICHEN,
|
||||
feld(dokument.getBelegnummer()),
|
||||
feld(dokument.belegtyp().name()),
|
||||
feld(datum(dokument.getDatum())),
|
||||
feld(dokument.getStatus() == null ? null : dokument.getStatus().name()),
|
||||
feld(dokument.getVorgaengerNr()),
|
||||
feld(dokument.getKundenReferenz()),
|
||||
feld(dokument.getKundeName()),
|
||||
feld(dokument.getKundeAnschrift()),
|
||||
feld(betrag(dokument.getSummeNetto())),
|
||||
feld(betrag(dokument.getSummeSteuer())),
|
||||
feld(betrag(dokument.getSummeBrutto())),
|
||||
feld(rechnung == null ? null : datum(rechnung.getZahlungsziel())),
|
||||
feld(rechnung == null ? null : datum(rechnung.getStorniertAm())),
|
||||
feld(rechnung == null ? null : rechnung.getStorniertVon()));
|
||||
}
|
||||
|
||||
private static String positionsFelder(Dokumentposition position) {
|
||||
return String.join(TRENNZEICHEN,
|
||||
feld(position.getProduktReferenz()),
|
||||
feld(position.getBezeichnung()),
|
||||
feld(Integer.toString(position.getMenge())),
|
||||
feld(betrag(position.getEinzelpreisNetto())),
|
||||
feld(betrag(position.getSteuersatz())),
|
||||
feld(betrag(position.getPositionssummeNetto())),
|
||||
feld(betrag(position.getPositionssummeBrutto())));
|
||||
}
|
||||
|
||||
private static String leerePositionsFelder() {
|
||||
return TRENNZEICHEN.repeat(6);
|
||||
}
|
||||
|
||||
private static String datum(LocalDate datum) {
|
||||
return datum == null ? null : datum.toString();
|
||||
}
|
||||
|
||||
private static String betrag(BigDecimal wert) {
|
||||
return wert == null ? null : wert.toPlainString();
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,9 @@ public class JsonDokumentRepository implements DokumentRepository {
|
|||
if (datei.getParent() != null) {
|
||||
Files.createDirectories(datei.getParent());
|
||||
}
|
||||
mapper.writeValue(datei.toFile(), dokumente);
|
||||
// Über den Basistyp schreiben, damit die polymorphe Typ-ID ('typ')
|
||||
// erhalten bleibt und Belege beim Neustart wieder geladen werden (IF-01).
|
||||
mapper.writerFor(new TypeReference<List<Dokument>>() { }).writeValue(datei.toFile(), dokumente);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Belegbestand konnte nicht gespeichert werden: " + datei, e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public class Rechnung extends Dokument {
|
|||
private LocalDate leistungsdatum;
|
||||
private LocalDate zahlungsziel;
|
||||
private LocalDate storniertAm;
|
||||
private String storniertVon;
|
||||
|
||||
@Override
|
||||
public Belegtyp belegtyp() {
|
||||
|
|
@ -18,10 +19,10 @@ public class Rechnung extends Dokument {
|
|||
}
|
||||
|
||||
/**
|
||||
* Storniert eine offene Rechnung (F-19, F-20): Status wird
|
||||
* {@code STORNIERT}, der Vorgang wird mit Datum protokolliert.
|
||||
* Storniert eine offene Rechnung (BA-14, F-19, F-20): Status wird
|
||||
* {@code STORNIERT}, der Vorgang wird mit Datum und Benutzer protokolliert.
|
||||
*/
|
||||
public void storniere(LocalDate datum) {
|
||||
public void storniere(LocalDate datum, String benutzer) {
|
||||
if (getStatus() != DokumentStatus.OFFEN) {
|
||||
throw new IllegalStateException(
|
||||
"Nur Rechnungen im Status OFFEN können storniert werden (F-19), "
|
||||
|
|
@ -29,10 +30,16 @@ public class Rechnung extends Dokument {
|
|||
}
|
||||
setzeStatus(DokumentStatus.STORNIERT);
|
||||
this.storniertAm = datum;
|
||||
this.storniertVon = benutzer;
|
||||
}
|
||||
|
||||
/** Storniert mit Datum, ohne Benutzerangabe (Rückwärtskompatibilität). */
|
||||
public void storniere(LocalDate datum) {
|
||||
storniere(datum, null);
|
||||
}
|
||||
|
||||
public void storniere() {
|
||||
storniere(LocalDate.now());
|
||||
storniere(LocalDate.now(), null);
|
||||
}
|
||||
|
||||
public LocalDate getLeistungsdatum() {
|
||||
|
|
@ -56,4 +63,8 @@ public class Rechnung extends Dokument {
|
|||
public LocalDate getStorniertAm() {
|
||||
return storniertAm;
|
||||
}
|
||||
|
||||
public String getStorniertVon() {
|
||||
return storniertVon;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ public class StandardDokumentService implements DokumentService {
|
|||
/** Standard-Gültigkeit eines Angebots in Kalendertagen ab Erstelldatum (F-02). */
|
||||
public static final int STANDARD_GUELTIGKEIT_TAGE = 30;
|
||||
|
||||
/** Protokollierter Benutzer einer Stornierung (BA-14); Einzelplatzbetrieb. */
|
||||
public static final String SYSTEM_BENUTZER = "Anwender";
|
||||
|
||||
private final DokumentRepository repository;
|
||||
private final BelegnummernGenerator nummernGenerator;
|
||||
private final KundenService kundenService;
|
||||
|
|
@ -164,7 +167,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
throw new ValidierungsException("Beleg",
|
||||
"Nur Rechnungen können storniert werden (F-19).");
|
||||
}
|
||||
rechnung.storniere();
|
||||
rechnung.storniere(LocalDate.now(), SYSTEM_BENUTZER);
|
||||
repository.speichere(rechnung);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,11 +51,27 @@ public class DokumentListenController {
|
|||
}
|
||||
try {
|
||||
dokumentService.storniere(rechnungsnummer);
|
||||
return Meldung.erfolg("Die Rechnung " + rechnungsnummer + " wurde storniert.");
|
||||
return Meldung.erfolg("Die Rechnung " + rechnungsnummer + " wurde storniert"
|
||||
+ protokoll(rechnungsnummer) + ".");
|
||||
} catch (ValidierungsException e) {
|
||||
return Meldung.fehler(e.getFeldname(), e.getMessage());
|
||||
} catch (IllegalStateException e) {
|
||||
return Meldung.fehler(null, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest das Storno-Protokoll (Datum, Benutzer) aus dem gespeicherten Beleg
|
||||
* für die Erfolgsmeldung (BA-14); leer, falls nicht ermittelbar.
|
||||
*/
|
||||
private String protokoll(String rechnungsnummer) {
|
||||
return dokumentService.alleDokumente().stream()
|
||||
.filter(d -> d instanceof Rechnung && d.getBelegnummer().equals(rechnungsnummer))
|
||||
.map(d -> (Rechnung) d)
|
||||
.filter(r -> r.getStorniertAm() != null)
|
||||
.findFirst()
|
||||
.map(r -> " am " + r.getStorniertAm()
|
||||
+ (r.getStorniertVon() == null ? "" : " durch " + r.getStorniertVon()))
|
||||
.orElse("");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.dokumente.Dokument;
|
||||
import de.team1.faktura.dokumente.DokumentCsvExport;
|
||||
import de.team1.faktura.dokumente.DokumentService;
|
||||
import de.team1.faktura.dokumente.DokumentStatus;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
|
|
@ -44,6 +45,7 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
private final DokumentService dokumentService;
|
||||
private final KundenService kundenService;
|
||||
private final ProduktService produktService;
|
||||
private final DokumentCsvExport datenExport;
|
||||
private final DokumentListenController controller;
|
||||
|
||||
private final JComboBox<String> statusFilter = new JComboBox<>(
|
||||
|
|
@ -60,10 +62,12 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
public DokumentListenPanel(DokumentService dokumentService,
|
||||
KundenService kundenService,
|
||||
ProduktService produktService) {
|
||||
ProduktService produktService,
|
||||
DokumentCsvExport datenExport) {
|
||||
this.dokumentService = dokumentService;
|
||||
this.kundenService = kundenService;
|
||||
this.produktService = produktService;
|
||||
this.datenExport = datenExport;
|
||||
this.controller = new DokumentListenController(dokumentService);
|
||||
baueOberflaeche();
|
||||
aktualisiere();
|
||||
|
|
@ -81,8 +85,11 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
neueRechnung.addActionListener(e -> oeffneWizard());
|
||||
JButton neuerBeleg = new JButton("Neuer Beleg…");
|
||||
neuerBeleg.addActionListener(e -> oeffneBelegDialog());
|
||||
JButton datenExportKnopf = new JButton("Daten exportieren (CSV)…");
|
||||
datenExportKnopf.addActionListener(e -> exportiereDaten());
|
||||
kopf.add(neueRechnung);
|
||||
kopf.add(neuerBeleg);
|
||||
kopf.add(datenExportKnopf);
|
||||
add(kopf, BorderLayout.NORTH);
|
||||
|
||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
|
|
@ -215,6 +222,18 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
}
|
||||
}
|
||||
|
||||
/** Vollständiger Datenexport aller Belege als CSV (Q-08, IF-04). */
|
||||
private void exportiereDaten() {
|
||||
JFileChooser auswahlDialog = new JFileChooser();
|
||||
auswahlDialog.setSelectedFile(new java.io.File("dokumente.csv"));
|
||||
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
||||
datenExport.exportiereCsv(ziel);
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg(
|
||||
"Die Belegdaten wurden exportiert nach " + ziel), null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Optionaler Druck über das Betriebssystem (IF-02). */
|
||||
private void drucke() {
|
||||
Dokument dokument = auswahl();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
package de.team1.faktura;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.team1.faktura.dokumente.Dokument;
|
||||
import de.team1.faktura.dokumente.DokumentCsvExport;
|
||||
import de.team1.faktura.dokumente.Dokumentposition;
|
||||
import de.team1.faktura.dokumente.JsonDokumentRepository;
|
||||
import de.team1.faktura.dokumente.PdfBoxPdfExporter;
|
||||
import de.team1.faktura.dokumente.Rechnung;
|
||||
import de.team1.faktura.gemeinsam.JsonPersistenz;
|
||||
import de.team1.faktura.kunden.JsonKundenRepository;
|
||||
import de.team1.faktura.kunden.Kunde;
|
||||
import de.team1.faktura.kunden.KundenCsvExport;
|
||||
import de.team1.faktura.produkte.JsonProduktRepository;
|
||||
import de.team1.faktura.produkte.Produkt;
|
||||
import de.team1.faktura.produkte.ProduktCsvExport;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||
|
||||
/**
|
||||
* Performance-Nachweise (Q-01 bis Q-04, Q-08) gemäß Lastenheft.
|
||||
*
|
||||
* <p>Referenzgröße Q-01: 5.000 Kunden und 5.000 Produkte. Die Grenzwerte sind
|
||||
* laufzeitabhängig; gemessen werden ausschließlich die Fachoperationen, das
|
||||
* Befüllen der Testdateien erfolgt vorab in {@link #seed(Path)} und fließt nicht
|
||||
* in die Messung ein.
|
||||
*/
|
||||
class PerformanceTest {
|
||||
|
||||
private static final int ANZAHL_STAMMDATEN = 5_000;
|
||||
private static final int ANZAHL_DOKUMENTE = 1_000;
|
||||
|
||||
@TempDir
|
||||
static Path datenVerzeichnis;
|
||||
|
||||
private static Path kundenDatei;
|
||||
private static Path produkteDatei;
|
||||
private static Path dokumenteDatei;
|
||||
|
||||
@BeforeAll
|
||||
static void seed() throws IOException {
|
||||
kundenDatei = datenVerzeichnis.resolve("kunden.json");
|
||||
produkteDatei = datenVerzeichnis.resolve("produkte.json");
|
||||
dokumenteDatei = datenVerzeichnis.resolve("dokumente.json");
|
||||
ObjectMapper mapper = JsonPersistenz.mapper();
|
||||
|
||||
List<Kunde> kunden = new ArrayList<>(ANZAHL_STAMMDATEN);
|
||||
List<Produkt> produkte = new ArrayList<>(ANZAHL_STAMMDATEN);
|
||||
for (int i = 1; i <= ANZAHL_STAMMDATEN; i++) {
|
||||
Kunde kunde = new Kunde("Kunde " + i + " GmbH", "Hauptstr. " + i, "68163", "Mannheim");
|
||||
kunde.setKundennummer(String.format("K-%06d", i));
|
||||
kunden.add(kunde);
|
||||
|
||||
Produkt produkt = new Produkt("Produkt " + i, new BigDecimal("49.99"), new BigDecimal("0.19"));
|
||||
produkt.setProduktnummer(String.format("P-%06d", i));
|
||||
produkte.add(produkt);
|
||||
}
|
||||
|
||||
List<Dokument> dokumente = new ArrayList<>(ANZAHL_DOKUMENTE);
|
||||
for (int i = 1; i <= ANZAHL_DOKUMENTE; i++) {
|
||||
dokumente.add(rechnungMitPositionen(String.format("R-2026-%06d", i), 5));
|
||||
}
|
||||
|
||||
mapper.writeValue(kundenDatei.toFile(), kunden);
|
||||
mapper.writeValue(produkteDatei.toFile(), produkte);
|
||||
mapper.writerFor(new TypeReference<List<Dokument>>() { }).writeValue(dokumenteDatei.toFile(), dokumente);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Q-04: Laden der drei Repositories (5.000/5.000/1.000) in ≤ 5 s")
|
||||
void q04Anwendungsstart() {
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
|
||||
new JsonKundenRepository(kundenDatei);
|
||||
new JsonProduktRepository(produkteDatei);
|
||||
new JsonDokumentRepository(dokumenteDatei);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Q-02: Suche/Auflistung in Kunden- und Produktverwaltung in ≤ 1 s")
|
||||
void q02Suche() {
|
||||
JsonKundenRepository kundenRepository = new JsonKundenRepository(kundenDatei);
|
||||
JsonProduktRepository produktRepository = new JsonProduktRepository(produkteDatei);
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
|
||||
assertFalse(kundenRepository.suche("Kunde 4999").isEmpty());
|
||||
assertFalse(kundenRepository.alleSortiertNachName().isEmpty());
|
||||
assertFalse(produktRepository.suche("Produkt 4999").isEmpty());
|
||||
assertFalse(produktRepository.alleSortiertNachBezeichnung().isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Q-03: PDF-Export einer Rechnung mit 50 Positionen in ≤ 2 s")
|
||||
void q03PdfErstellung() {
|
||||
Rechnung rechnung = rechnungMitPositionen("R-2026-999999", 50);
|
||||
PdfBoxPdfExporter exporter = new PdfBoxPdfExporter();
|
||||
Path ziel = datenVerzeichnis.resolve("pdf/R-2026-999999.pdf");
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(2), () -> exporter.exportiere(rechnung, ziel));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Q-08: Vollexport (Kunden, Produkte, Belege) in ≤ 30 s")
|
||||
void q08Datenexport() {
|
||||
JsonKundenRepository kundenRepository = new JsonKundenRepository(kundenDatei);
|
||||
JsonProduktRepository produktRepository = new JsonProduktRepository(produkteDatei);
|
||||
JsonDokumentRepository dokumentRepository = new JsonDokumentRepository(dokumenteDatei);
|
||||
|
||||
assertTimeoutPreemptively(Duration.ofSeconds(30), () -> {
|
||||
new KundenCsvExport(kundenRepository).exportiereCsv(datenVerzeichnis.resolve("export/kunden.csv"));
|
||||
new ProduktCsvExport(produktRepository).exportiereCsv(datenVerzeichnis.resolve("export/produkte.csv"));
|
||||
new DokumentCsvExport(dokumentRepository).exportiereCsv(datenVerzeichnis.resolve("export/dokumente.csv"));
|
||||
});
|
||||
}
|
||||
|
||||
private static Rechnung rechnungMitPositionen(String belegnummer, int anzahlPositionen) {
|
||||
Rechnung rechnung = new Rechnung();
|
||||
rechnung.setBelegnummer(belegnummer);
|
||||
rechnung.setDatum(LocalDate.of(2026, 6, 10));
|
||||
rechnung.setLeistungsdatum(LocalDate.of(2026, 6, 10));
|
||||
rechnung.setZahlungsziel(LocalDate.of(2026, 6, 24));
|
||||
rechnung.setzeKunde("K-000001", "Muster GmbH", "Hauptstr. 1, 68163 Mannheim");
|
||||
List<Dokumentposition> positionen = new ArrayList<>(anzahlPositionen);
|
||||
for (int i = 1; i <= anzahlPositionen; i++) {
|
||||
positionen.add(new Dokumentposition("P-" + String.format("%06d", i), "Produkt " + i,
|
||||
i, new BigDecimal("49.99"), new BigDecimal("0.19")));
|
||||
}
|
||||
rechnung.setzePositionen(positionen);
|
||||
return rechnung;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package de.team1.faktura.dokumente;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Datenexport der Bewegungsdaten (Q-08, IF-04): vollständiger CSV-Export
|
||||
* aller Belege mit einer Zeile je Position.
|
||||
*/
|
||||
class DokumentCsvExportTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
@DisplayName("Q-08: Export schreibt Kopfzeile und je eine Zeile pro Dokumentposition")
|
||||
void exportiertAlleBelegeMitPositionen() throws IOException {
|
||||
JsonDokumentRepository repository = new JsonDokumentRepository(tempDir.resolve("dokumente.json"));
|
||||
repository.speichere(TestBelege.rechnung("R-2026-000001", DokumentStatus.OFFEN));
|
||||
Rechnung storniert = TestBelege.rechnung("R-2026-000002", DokumentStatus.OFFEN);
|
||||
storniert.storniere(java.time.LocalDate.of(2026, 6, 10), "Anwender");
|
||||
repository.speichere(storniert);
|
||||
|
||||
Path ziel = tempDir.resolve("export/dokumente.csv");
|
||||
new DokumentCsvExport(repository).exportiereCsv(ziel);
|
||||
|
||||
List<String> zeilen = Files.readAllLines(ziel, StandardCharsets.UTF_8);
|
||||
assertEquals(3, zeilen.size()); // Kopfzeile + 2 Rechnungen mit je 1 Position
|
||||
assertTrue(zeilen.get(0).startsWith("belegnummer;belegtyp;datum;status"));
|
||||
assertTrue(zeilen.stream().anyMatch(z -> z.contains("R-2026-000001")));
|
||||
assertTrue(zeilen.stream().anyMatch(z -> z.contains("STORNIERT") && z.contains("Anwender")));
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ class DokumentzyklusTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-09: Storno einer offenen Rechnung -> STORNIERT, nicht mehr offen, storniertAm gesetzt")
|
||||
@DisplayName("TC-09: Storno einer offenen Rechnung -> STORNIERT, nicht mehr offen, mit Datum und Benutzer protokolliert (BA-14)")
|
||||
void tc09Storno() {
|
||||
Rechnung rechnung = service.erstelleRechnung(KUNDE_NR,
|
||||
List.of(new Positionsangabe(PRODUKT_NR, 1)), LocalDate.of(2026, 6, 9), null);
|
||||
|
|
@ -172,6 +172,7 @@ class DokumentzyklusTest {
|
|||
assertTrue(service.offeneRechnungen().stream()
|
||||
.noneMatch(r -> r.getBelegnummer().equals(rechnung.getBelegnummer())));
|
||||
assertNotNull(storniert.getStorniertAm());
|
||||
assertEquals(StandardDokumentService.SYSTEM_BENUTZER, storniert.getStorniertVon());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package de.team1.faktura.dokumente;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* Persistenz der Belege (IF-01): gespeicherte Belege müssen nach einem
|
||||
* Neustart (Neuinstanziierung des Repositories) wieder geladen werden,
|
||||
* inklusive der polymorphen Belegtypen (GoBD: lückenlose Erfassung).
|
||||
*/
|
||||
class JsonDokumentRepositoryTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
@DisplayName("Belege überleben den Neustart und behalten ihren Belegtyp")
|
||||
void belegeWerdenNachNeustartGeladen() {
|
||||
Path datei = tempDir.resolve("dokumente.json");
|
||||
|
||||
JsonDokumentRepository ersteInstanz = new JsonDokumentRepository(datei);
|
||||
ersteInstanz.speichere(TestBelege.rechnung("R-2026-000001", DokumentStatus.OFFEN));
|
||||
ersteInstanz.speichere(TestBelege.angebot("AN-2026-000001", DokumentStatus.ENTWURF));
|
||||
|
||||
JsonDokumentRepository neustart = new JsonDokumentRepository(datei);
|
||||
assertEquals(2, neustart.alle().size());
|
||||
assertInstanceOf(Rechnung.class, neustart.findeNachNummer("R-2026-000001"));
|
||||
assertInstanceOf(Angebot.class, neustart.findeNachNummer("AN-2026-000001"));
|
||||
assertNotNull(neustart.findeNachNummer("R-2026-000001").getPositionen());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue