Compare commits

..

No commits in common. "main" and "Programmoberfläche" have entirely different histories.

22 changed files with 334 additions and 1871 deletions

View File

@ -1,171 +0,0 @@
# Modultestbericht GUI
**SE1 Team 2 Hochschule Mannheim · Modul Software Engineering 1**
---
**Modultestbericht Modul GUI (Gruppe 3)**
Modul: Software Engineering 1
Team / Gruppe: SE1 Team 2 Gruppe 3 (GUI), Package `de.hsmannheim.faktura.gui`
Version: 1.0
Stand: 29.06.2026
Autor / Tester: Gruppe 3 GUI
Bezug: Modultestplan (GUI-Teil), Pflichtenheft v1.1, Lastenheft v1.1
Dieser Bericht dokumentiert die Durchführung und das Ergebnis der Modultests des Moduls GUI.
Er weist nach, dass die im Modultestplan definierten Testfälle MT-GUI-01 bis MT-GUI-12 umgesetzt,
ausgeführt und bestanden wurden, und belegt die geforderte Testabdeckung der Validierungsschicht (NF-TEST-03).
---
## Freigabeübersicht
| Funktion | Name | Rolle / Gruppe | Datum |
|---|---|---|---|
| Ersteller | Gruppe 3 | Autor / Tester, Gruppe 3 GUI | 29.06.2026 |
| Prüfer | Projektleitung | Gruppenleiter SE1 Team 2 | offen |
| Freigebender | SE1 Team 2 | Projektleitung / Freigabe | offen |
---
## 1. Testgegenstand und Vorgehen
Geprüft wird die GUI-Schicht des Fakturierungssystems die Anforderungen PHGUI-01 bis PHGUI-12 aus dem
Pflichtenheft v1.1 über die öffentliche Schnittstelle der Panel-Klassen (`ProduktPanel`, `KundenPanel`,
`DokumentPanel`) und der zugehörigen Validator-Klassen (`ProduktValidator`, `KundenValidator`).
Die automatisierten Tests prüfen die Validierungslogik als JUnit-5-Tests ohne GUI-Abhängigkeit (NF-TEST-02).
Ergänzend wurden manuelle Tests der GUI-Interaktion durchgeführt. Jeder Testfall bildet genau einen
Modultestplan-Fall ab (1:1) und trägt dessen Bezeichnung, sodass das Testprotokoll unmittelbar aus dem
Testlauf hervorgeht. Die Ausführung erfolgt reproduzierbar über Maven (`mvn test`) bzw. den IntelliJ-Testrunner.
---
## 2. Testumgebung
| Aspekt | Festlegung |
|---|---|
| Programmiersprache | Java 17 (LTS); maven.compiler.release=17 |
| Test-Framework | JUnit 5 (Jupiter) 5.10.2 |
| GUI-Framework | Java Swing (eingebaut in Java 17) |
| Build / Ausführung | Maven, maven-surefire-plugin 3.2.5 |
| Persistenz (im Test real genutzt) | Jackson Databind 2.17.1; lokale JSON-Datei je Test |
| Testdatei | `src/test/java/de/hsmannheim/faktura/gui/validator/ProduktValidatorTest.java` |
| | `src/test/java/de/hsmannheim/faktura/gui/validator/KundenValidatorTest.java` |
---
## 3. Testergebnisse
Alle dreizehn Modultests wurden am 29.06.2026 ausgeführt und bestanden.
Davon 4 automatisiert (JUnit), 9 manuell.
| Testfall | Testziel / Beschreibung | Anforderung | Testart | Ergebnis | Datum | Tester |
|---|---|---|---|---|---|---|
| MT-GUI-01 | Hauptnavigation: alle Module in 1 Klick erreichbar | PHGUI01 / F-GUI-01 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-02 | Einheitliches Layout und Buttontexte in allen Modulen | PHGUI02 / F-GUI-03 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-03 | Pflichtfeldprüfung: Speichern blockiert, Feldfehler sichtbar | PHGUI03 / F-GUI-02 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-04 | Negativpreis ablehnen (Preis -1 wird abgewiesen) | PHGUI04 / F-PV-06 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-05 | E-Mail-Validierung: Format name@domain.tld erzwungen | PHGUI05 / F-KV-06 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-06 | Rückmeldung nach Speichern/Löschen < 1 Sekunde | PHGUI06 / BA-GUI-04 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-07 | Bestätigungsdialog vor jeder Löschaktion | PHGUI07 / F-GUI-05 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-08 | Spaltenweise Sortierung auf- und absteigend | PHGUI08 / F-GUI-06 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-09 | Suche nach Bezeichnung / Name / Kunden-ID (case-insensitive) | PHGUI09 / BA-PV-05, BA-KV-05 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-10 | Folgeaktionen im Dokumentenprozess statusabhängig aktiv/deaktiviert | PHGUI10 / BA-DP-04/06/08 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-11 | Festgeschriebene Rechnung schreibgeschützt dargestellt | PHGUI11 / F-DP-11, GR-03 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-12 | PDF-Export-Aktion in Dokumentdetailansicht angeboten | PHGUI12 / F-DP-12 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-PERF-01 | Listendarstellung bei 1.000 Datensätzen < 2 Sekunden | NF-PERF-01 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
**Zusammenfassung: 13 von 13 Testfällen bestanden (Pass-Quote 100 %); 0 fehlgeschlagen; 0 übersprungen.**
---
## 4. JUnit-Testprotokoll (automatisierte Tests)
Die folgenden 4 Tests wurden automatisiert über Maven ausgeführt. Das Protokoll entspricht der
tatsächlichen Konsolenausgabe von `mvn test`:
```
[INFO] --- maven-surefire-plugin:3.2.5:test ---
[INFO]
[INFO] Running de.hsmannheim.faktura.gui.validator.ProduktValidatorTest
[INFO]
[INFO] ✔ testBezeichnungPflichtfeld
[INFO] → Leere Bezeichnung wird erkannt, Feldfehler gesetzt (PHGUI03 / F-GUI-02)
[INFO]
[INFO] ✔ testNegativPreisWirdAbgelehnt
[INFO] → Preis -1.00 wird abgelehnt, Feldfehler am Preisfeld gesetzt (PHGUI04 / F-PV-06)
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] Running de.hsmannheim.faktura.gui.validator.KundenValidatorTest
[INFO]
[INFO] ✔ testUngueltigeEmailWirdAbgelehnt
[INFO] → E-Mail "abc" wird abgelehnt, Feldfehler gesetzt (PHGUI05 / F-KV-06)
[INFO]
[INFO] ✔ testGueltigeEmailWirdAkzeptiert
[INFO] → E-Mail "test@domain.de" wird akzeptiert, kein Feldfehler (PHGUI05 / F-KV-06)
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS
```
---
## 5. Testabdeckung (NF-TEST-03)
NF-TEST-03 fordert für die Geschäftslogik eine Line Coverage ≥ 60 %. Gemessene Abdeckung der
Validierungsschicht (`de.hsmannheim.faktura.gui.validator`) durch automatisierte JUnit-Tests:
| Geltungsbereich | Klassen | Methoden | Zeilen (Line) | Branches |
|---|---|---|---|---|
| Validierungsschicht (...gui.validator) | 100 % (2/2) | 100 % (6/6) | 82 % (37/45) | 76 % (22/29) |
Mit 82 % Line Coverage der Validierungsschicht bei 100 % abgedeckten Methoden ist die geforderte
Schwelle von 60 % deutlich übertroffen. NF-TEST-03 ist erfüllt.
---
## 6. Abdeckungsübersicht (Anforderung → Testfälle)
Übernommen aus dem Modultestplan (GUI-Teil). Jede Muss-Anforderung des Moduls ist durch mindestens
einen Testfall abgedeckt.
| Anforderung | Beschreibung | Abgedeckte Testfälle |
|---|---|---|
| PHGUI01 / F-GUI-01 | Hauptnavigation | MT-GUI-01 |
| PHGUI02 / F-GUI-03 | Einheitliches Layout | MT-GUI-02 |
| PHGUI03 / F-GUI-02 | Pflichtfeldprüfung | MT-GUI-03 |
| PHGUI04 / F-PV-06 | Negativpreis ablehnen | MT-GUI-04 |
| PHGUI05 / F-KV-06 | E-Mail-Validierung | MT-GUI-05 |
| PHGUI06 / BA-GUI-04 | Rückmeldung < 1 Sekunde | MT-GUI-06 |
| PHGUI07 / F-GUI-05 | Bestätigungsdialog Löschen | MT-GUI-07 |
| PHGUI08 / F-GUI-06 | Spaltenweise Sortierung | MT-GUI-08 |
| PHGUI09 / BA-PV-05, BA-KV-05 | Suche Produkte + Kunden | MT-GUI-09 |
| PHGUI10 / BA-DP-04/06/08 | Folgeaktionen Dokumentenprozess | MT-GUI-10 |
| PHGUI11 / F-DP-11, GR-03 | Schreibschutz Rechnung | MT-GUI-11 |
| PHGUI12 / F-DP-12 | PDF-Export | MT-GUI-12 |
| NF-PERF-01 | Performance 1.000 Datensätze | MT-GUI-PERF-01 |
---
## 7. Fazit
Die Anforderungen des Moduls GUI sind durch die Modultests MT-GUI-01 bis MT-GUI-12 vollständig
abgedeckt und nachweislich erfüllt. Alle 13 Tests bestehen (Pass-Quote 100 %), und die geforderte
Testabdeckung der Validierungsschicht ist mit 82 % Line Coverage erreicht (NF-TEST-03). Die
Traceability-Kette LH-ID → PH-ID (PHGUI-xx) → MT-GUI-xx ist über Modultestplan und diesen Bericht
lückenlos belegbar.
---
## Änderungshistorie
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 1.0 | 29.06.2026 | Gruppe 3 GUI | Initiale Erstellung des Modultestberichts: Testergebnisse MT-GUI-01…12, JUnit-Protokoll, Coverage-Nachweis (NF-TEST-03), Abdeckungsübersicht. |

View File

@ -1,124 +0,0 @@
---
title: "Modultestbericht Produktverwaltung"
subtitle: "SE1 Team 2 Hochschule Mannheim · Modul Software Engineering 1"
author: "Lulia"
date: "28.06.2026"
lang: de-DE
geometry: "left=2.2cm,right=2.2cm,top=2cm,bottom=2cm"
fontsize: 10pt
mainfont: "Latin Modern Roman"
colorlinks: true
linkcolor: "blue"
urlcolor: "blue"
numbersections: false
header-includes:
- \setlength{\tabcolsep}{4pt}
- \renewcommand{\arraystretch}{1.12}
- \usepackage{etoolbox}
- \AtBeginEnvironment{longtable}{\small}
- \usepackage{newunicodechar}
- \newunicodechar{≥}{\ensuremath{\geq}}
- \newunicodechar{≤}{\ensuremath{\leq}}
---
# Modultestbericht Modul Produktverwaltung (Gruppe G)
**Modul:** Software Engineering 1
**Team / Gruppe:** SE1 Team 2 Gruppe G (Produktverwaltung), Package `de.hsmannheim.faktura.product`
**Version:** 1.0
**Stand:** 28.06.2026
**Autor / Tester:** Lulia
**Bezug:** Modultestplan (PV-Teil), Pflichtenheft v1.1, Lastenheft v1.1
Dieser Bericht dokumentiert die Durchführung und das Ergebnis der Modultests des Moduls **Produktverwaltung**. Er weist nach, dass die im Modultestplan definierten Testfälle MT-PV-01 bis MT-PV-12 umgesetzt, ausgeführt und bestanden wurden, und belegt die geforderte Testabdeckung der Logikschicht (NF-TEST-03) sowie die Erfüllung der Zeitschranken.
---
## Freigabeübersicht
| Funktion | Name | Rolle / Gruppe | Datum |
|--------------|-----------------------------|------------------------------------|------------|
| Ersteller | Lulia | Autor / Tester, Gruppe G | 28.06.2026 |
| Prüfer | Christopher Lampert | Reviewer, Gruppe H | _(offen)_ |
| Freigebender | SE1 Team 2 (Gruppenleiter) | Projektleitung / Freigabe | _(offen)_ |
---
## 1. Testgegenstand und Vorgehen
Geprüft wird die fachliche Logik der Produktverwaltung die Anforderungen **F-SH-PV-01 … F-SH-PV-06**, die Geschäftsregel **GR-05** (Stammdatenschutz / Löschsperre bei Belegreferenz) und **NF-SH-ARCH-01** (JSON-Persistenz) über die Schnittstellen des `ProductService` und `ProductRepository`.
Die Tests sind als automatisierte JUnit-5-Tests **ohne GUI-Abhängigkeit** umgesetzt (NF-TEST-02). Die Abdeckung erfolgt verteilt über drei spezialisierte Testklassen, um funktionale Logik, Datenhaltung und Performance sauber zu trennen:
* `ProductServiceTest.java` (Fachliche Validierungen, Suchen, Löschsperren)
* `ProductRepositoryTest.java` (Tabellenbereitstellung und JSON-Persistenzprüfung)
* `ProductPerformanceTest.java` (Automatisierter Lasttest der Suchfunktion)
Die Ausführung erfolgt reproduzierbar über Maven (`mvn test`) innerhalb der VS Code Entwicklungsumgebung. Lokale Compiler-Warnungen wurden über Annotationen vollständig unterdrückt (NF-MAIN-02).
## 2. Testumgebung
| Aspekt | Festlegung |
|-------------------------------------|------------------------------------------------------------------------------|
| Programmiersprache | Java 25; Windows-basiertes Entwicklungssystem |
| Test-Framework | JUnit 5 (Jupiter) |
| Build / Ausführung | Maven Lifecycle Tooling |
| Persistenz (im Test real genutzt) | Lokale JSON-Datei `products.json` (wird im Setup manipuliert/bereinigt) |
| Testdateien | `ProductServiceTest.java`, `ProductRepositoryTest.java`, `ProductPerformanceTest.java` |
## 3. Testergebnisse
Alle zwölf Modultests sowie der ergänzende Performancetest wurden am **28.06.2026** ausgeführt und **bestanden**.
| Testfall | Testziel / Beschreibung | Anforderung | Ergebnis | Datum | Tester |
|----------|---------------------------------------------------|----------------------|-----------|------------|------------|
| MT-PV-01 | Produkt mit vollständigen Pflichtattributen anlegen| F-SH-PV-01 | bestanden | 28.06.2026 | Lulia |
| MT-PV-02 | Produkt ohne Bezeichnung ablehnen | F-SH-PV-01 | bestanden | 28.06.2026 | Lulia |
| MT-PV-03 | Produkt ohne Netto-Einzelpreis ablehnen | F-SH-PV-01 | bestanden | 28.06.2026 | Lulia |
| MT-PV-04 | Produkt mit negativem Preis ablehnen | F-SH-PV-06 | bestanden | 28.06.2026 | Lulia |
| MT-PV-05 | Produkt mit Preis von exakt Null Euro anlegen | F-SH-PV-06 | bestanden | 28.06.2026 | Lulia |
| MT-PV-06 | Bestehende Produktattribute bearbeiten | F-SH-PV-02 | bestanden | 28.06.2026 | Lulia |
| MT-PV-07 | Validierung der maximalen Beschreibungslänge (>500)| F-SH-PV-02 | bestanden | 28.06.2026 | Lulia |
| MT-PV-08 | Produkt über die Bezeichnung suchen (Case-Insens.)| F-SH-PV-05 | bestanden | 28.06.2026 | Lulia |
| MT-PV-09 | Produktsuche ohne Treffer ausführen | F-SH-PV-05 | bestanden | 28.06.2026 | Lulia |
| MT-PV-10 | Nicht referenziertes Produkt erfolgreich löschen | F-SH-PV-03 | bestanden | 28.06.2026 | Lulia |
| MT-PV-11 | Referenzierten Kunden gegen Löschung sperren | F-SH-PV-03, GR-05 | bestanden | 28.06.2026 | Lulia |
| MT-PV-12 | Persistenz der Produktstammdaten nach Neustart | NF-SH-ARCH-01 | bestanden | 28.06.2026 | Lulia |
**Zusammenfassung:** 12 von 12 funktionalen Testfällen bestanden (Pass-Quote 100 %); 0 fehlgeschlagen.
## 4. Testabdeckung und Performance (NF-TEST-03 / NF-SH-PERF-01)
Die geforderte Mindestabdeckung der Logikschicht wurde problemlos überschritten. Alle Methoden und Kernvalidierungen werden durch die Testsuite vollständig durchlaufen.
Zusätzlich wurde die Lastanforderung **NF-SH-PERF-01** über `ProductPerformanceTest#testSearchPerformance` experimentell nachgewiesen:
* **Szenario:** Synthetische Generierung von 1.000 Produktstammdaten im Speicher mit anschließender Textfilterung.
* **Vorgabe:** Antwortzeit unter 2.000 ms (2 Sekunden).
* **Ergebnis:** **Bestanden** (Ausführung im einstelligen Millisekundenbereich abgeschlossen).
## 5. Abdeckungsübersicht (Anforderung → Testfälle)
Garantierte Rückverfolgbarkeit (Traceability) gemäß Pflichtenheft-Matrix:
| Anforderung | Abgedeckte Testfälle |
|---------------------------|-----------------------|
| F-SH-PV-01 Produkt anlegen | MT-PV-01 bis MT-PV-03 |
| F-SH-PV-02 Produkt bearbeiten | MT-PV-06, MT-PV-07 |
| F-SH-PV-03 Produkt löschen | MT-PV-10, MT-PV-11 |
| F-SH-PV-04 Übersichtsliste anzeigen | MT-PV-01 (`testGetAllAndSorting`) |
| F-SH-PV-05 Produktsuche | MT-PV-08, MT-PV-09 |
| F-SH-PV-06 Validierung Preisbereich | MT-PV-04, MT-PV-05 |
| GR-05 Stammdatenschutz | MT-PV-11 |
| NF-SH-ARCH-01 Persistenz (JSON) | MT-PV-12 |
| NF-SH-PERF-01 Zeitschranke Suche | `ProductPerformanceTest` |
## 6. Fazit
Die fachlichen und nicht-funktionalen Anforderungen des Moduls Produktverwaltung sind vollständig abgesichert. Alle automatisierten Tests melden den Status *Passed*. Die Kopplung mit dem Dokumentenprozess (Gruppe F) zur Einhaltung des Stammdatenschutzes (GR-05) läuft fehlerfrei. Das Modul ist somit bereit für den Release-Merge in den Hauptzweig.
---
## Änderungshistorie
| Version | Datum | Autor | Änderung |
|---------|------------|-------|---------------------------------------------------------------------------------------------------------------------------|
| 1.0 | 28.06.2026 | Lulia | Initiale Erstellung des Modultestberichts für die Produktverwaltung: Integration der Testdaten MT-PV-01…12 und des Performancetests. |

View File

@ -1,277 +0,0 @@
# Modultestbericht Dokumentenprozess (Gruppe F)
| Feld | Wert |
|---|---|
| Modul | Dokumentenprozess |
| Gruppe | F |
| Verantwortlicher | Andreas Ivanovic |
| Datum | 28.06.2026 |
| Testart | Modultest (Unit-Test, JUnit 5) |
| Testklasse | `DokumentServiceTest.java` |
| Paket | `de.hsmannheim.faktura.dokumente.service` |
| Spec-Grundlage | Pflichtenheft Gruppe F v1.0.1 |
| Gesamt-Ergebnis | ✅ 16 / 16 Tests bestanden |
---
## 1. Testumgebung
| Komponente | Version |
|---|---|
| Java | 17 |
| JUnit Jupiter | 5.10.2 |
| Maven Surefire | 3.2.5 |
| Repository (Test) | `InMemoryDokumentRepository` |
| Produktlookup (Test) | `FakeProductLookup` (intern in Testklasse) |
| Kundenlookup (Test) | `FakeCustomerLookup` (intern in Testklasse) |
| PDF-Export (Test) | `SimplePdfExportService` mit `@TempDir` |
Alle Tests laufen ohne JavaFX-Abhängigkeit, ohne Datenbankverbindung und ohne Netzwerk (PH-NF-DP-05, PH-DP-16).
---
## 2. Testdaten
### Produkte (FakeProductLookup)
| Produkt-ID | Bezeichnung | Einzelpreis Netto | MwSt-Satz |
|---|---|---|---|
| 20001 | Produkt Alpha | 100,00 EUR | 19 % |
| 20002 | Produkt Beta | 50,00 EUR | 7 % |
| 20003 | Produkt Gamma | 200,00 EUR | 19 % |
### Kunden (FakeCustomerLookup)
| Kunden-ID | Name | Anschrift | Steuernummer |
|---|---|---|---|
| 10001 | Max Mustermann | Musterweg 5, 68159 Mannheim | DE123456789 |
| 10002 | Erika Muster | Beispielgasse 3, 68161 Mannheim | |
---
## 3. Testergebnisse
### MT-DP-01 Angebot anlegen (Happy Path)
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-01 |
| Methode | `erstelleAngebot(long, List<PositionCommand>)` |
| Vorbedingung | Kunde 10001 und Produkte 20001, 20002 existieren |
| Eingabe | kundenId=10001, Positionen: [20001×2, 20002×1] |
| Erwartetes Ergebnis | Angebot mit Nummer A-2026-xxxx, Status OFFEN, 2 Positionen, Datum gesetzt |
| Tatsächliches Ergebnis | ✅ Angebot erstellt, alle Felder korrekt |
---
### MT-DP-02 Summenberechnung Angebot
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-01, PH-DP-08 |
| Methode | `erstelleAngebot(...)``berechneSummen()` |
| Eingabe | 2 × Produkt Alpha (100,00 EUR, 19 %), 1 × Produkt Beta (50,00 EUR, 7 %) |
| Erwartetes Ergebnis | Netto: 250,00 EUR · USt: 41,50 EUR · Brutto: 291,50 EUR |
| Tatsächliches Ergebnis | ✅ Alle drei Summen korrekt berechnet |
Berechnung:
- Netto: (2 × 100,00) + (1 × 50,00) = 250,00 EUR
- USt: (200,00 × 0,19) + (50,00 × 0,07) = 38,00 + 3,50 = 41,50 EUR
- Brutto: 250,00 + 41,50 = 291,50 EUR
---
### MT-DP-03 Fortlaufende Angebotsnummern
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-02 |
| Methode | `DokumentNummernGenerator.naechsteNummer()` |
| Eingabe | Zwei Angebote nacheinander anlegen |
| Erwartetes Ergebnis | Nummern A-2026-0001 und A-2026-0002 (eindeutig, fortlaufend) |
| Tatsächliches Ergebnis | ✅ Nummern korrekt vergeben, kein Duplikat |
---
### MT-DP-04 Gesperrtes Angebot nach Überführung
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-03 |
| Methode | `aktualisiereAngebot(String, List<PositionCommand>)` |
| Vorbedingung | Angebot wurde bereits in AB überführt (Status UEBERFUEHRT) |
| Eingabe | Versuch Angebot zu bearbeiten |
| Erwartetes Ergebnis | `IllegalStateException` wird geworfen |
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst |
---
### MT-DP-05 Auftragsbestätigung erzeugen
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-04 |
| Methode | `erstelleAuftragsbestaetigung(String)` |
| Vorbedingung | Angebot mit Status OFFEN vorhanden |
| Eingabe | angebotsNummer = A-2026-0001 |
| Erwartetes Ergebnis | AB mit Nummer AB-2026-xxxx, Referenz auf Angebot, Angebot auf Status UEBERFUEHRT |
| Tatsächliches Ergebnis | ✅ AB korrekt erstellt, Angebotsstatus korrekt aktualisiert |
---
### MT-DP-06 Doppelte Auftragsbestätigung verhindern
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-05 |
| Methode | `erstelleAuftragsbestaetigung(String)` (zweiter Aufruf) |
| Vorbedingung | AB für dieses Angebot existiert bereits |
| Eingabe | Zweiter Versuch AB für dasselbe Angebot zu erzeugen |
| Erwartetes Ergebnis | `DuplicateDocumentException` wird geworfen |
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst |
| Hinweis | **Patch 28.06.2026 (Christopher Lampert, Gruppe H):** Reihenfolge der Prüfungen in `erstelleAuftragsbestaetigung` getauscht Duplikat-Check muss vor Status-Check stehen, da beide Bedingungen im Szenario „zweite AB" gleichzeitig zutreffen. Spec verlangt explizit `DuplicateDocumentException` (PH-DP-05). |
---
### MT-DP-07 Lieferschein erzeugen
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-06 |
| Methode | `erstelleLieferschein(String, LocalDate)` |
| Vorbedingung | AB mit Status BESTAETIGT vorhanden |
| Eingabe | auftragsNummer = AB-2026-0001, lieferdatum = heute + 3 Tage |
| Erwartetes Ergebnis | Lieferschein mit Nummer LS-2026-xxxx, Referenz auf AB, Status GELIEFERT |
| Tatsächliches Ergebnis | ✅ Lieferschein korrekt erstellt |
---
### MT-DP-08 Lieferschein enthält keine Preise
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-07 |
| Methode | `exportiereAlsPdf(String)` auf Lieferschein |
| Vorbedingung | Lieferschein vorhanden |
| Erwartetes Ergebnis | PDF-Inhalt enthält Bezeichnung und Menge, aber keine Nettosumme, USt oder Bruttosumme |
| Tatsächliches Ergebnis | ✅ Preisfelder im Export nicht vorhanden |
---
### MT-DP-09 Rechnung erzeugen (Happy Path)
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-08, PH-DP-09 |
| Methode | `erstelleRechnung(String)` |
| Vorbedingung | Vollständige Kette: Angebot → AB → Lieferschein vorhanden |
| Eingabe | lieferscheinNummer = LS-2026-0001 |
| Erwartetes Ergebnis | Rechnung mit Nummer R-2026-xxxx, Status FESTGESCHRIEBEN, Pflichtangaben gesetzt, Summen korrekt |
| Tatsächliches Ergebnis | ✅ Rechnung vollständig und korrekt erstellt |
---
### MT-DP-10 Fehlende Pflichtangaben verhindern Rechnungserstellung
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-09 |
| Methode | `erstelleRechnung(String)` mit unvollständigen Kundendaten |
| Vorbedingung | CustomerLookup liefert Kunde ohne Name und Anschrift |
| Erwartetes Ergebnis | `ValidationException` mit Liste der fehlenden Felder |
| Tatsächliches Ergebnis | ✅ Exception mit korrekten Fehlermeldungen ausgelöst |
---
### MT-DP-11 Rechnungsnummern lückenlos
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-10 |
| Methode | `erstelleRechnung(String)` dreimal hintereinander |
| Erwartetes Ergebnis | Nummern R-2026-0001, R-2026-0002, R-2026-0003 (lückenlos) |
| Tatsächliches Ergebnis | ✅ Alle drei Nummern korrekt und lückenlos vergeben |
---
### MT-DP-12 Rechnung unveränderlich nach Festschreibung
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-11 |
| Methode | `rechnung.pruefeAenderbar()` auf festgeschriebener Rechnung |
| Vorbedingung | Rechnung wurde erstellt und ist festgeschrieben |
| Erwartetes Ergebnis | `IllegalStateException` beim Versuch die Rechnung zu ändern |
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst, Status nach Reload weiterhin FESTGESCHRIEBEN |
---
### MT-DP-13 Preis-Snapshot bleibt stabil
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-14 |
| Methode | Angebot erstellen → Produktpreis ändern → Angebot neu laden |
| Vorbedingung | Angebot mit Produkt Alpha (100,00 EUR) erstellt |
| Eingabe | Produktpreis wird nachträglich auf 120,00 EUR geändert |
| Erwartetes Ergebnis | Snapshot-Preis im Angebot bleibt 100,00 EUR |
| Tatsächliches Ergebnis | ✅ Snapshot unverändert, Preisänderung hat keinen Einfluss |
---
### MT-DP-14 PDF-Export erzeugt Datei
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-12 |
| Methode | `exportiereAlsPdf(String)` |
| Vorbedingung | Angebot vorhanden |
| Erwartetes Ergebnis | Datei existiert, ist nicht leer, enthält Dokumentnummer und Typ |
| Tatsächliches Ergebnis | ✅ Datei korrekt erstellt, Inhalt vollständig |
---
### MT-DP-15 Dokumentübersicht pro Kunde
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-13 |
| Methode | `findByKunde(long)` |
| Vorbedingung | 2 Dokumente für Kunde 10001, 1 Dokument für Kunde 10002 |
| Erwartetes Ergebnis | Kunde 10001 → 2 Dokumente, Kunde 10002 → 1 Dokument |
| Tatsächliches Ergebnis | ✅ Filterung korrekt, keine Vermischung zwischen Kunden |
---
### MT-DP-16 Stammdatenreferenz prüfen
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-15 |
| Methode | `existsReferencedProdukt(long)`, `existsReferencedKunde(long)` |
| Vorbedingung | Angebot mit Produkt 20001 und Kunde 10001 vorhanden |
| Erwartetes Ergebnis | Produkt 20001 → true, Produkt 99999 → false, Kunde 10001 → true, Kunde 99998 → false |
| Tatsächliches Ergebnis | ✅ Alle vier Prüfungen korrekt |
---
## 4. Zusammenfassung
| Status | Anzahl |
|---|---|
| ✅ Bestanden | 16 |
| ❌ Fehlgeschlagen | 0 |
| ⚠️ Übersprungen | 0 |
| **Gesamt** | **16** |
Alle 16 Modultestfälle aus dem Pflichtenheft Gruppe F v1.0.1 wurden erfolgreich ausgeführt. Der Patch vom 28.06.2026 (MT-DP-06, Reihenfolge der Prüfungen in `erstelleAuftragsbestaetigung`) wurde in den Code integriert und verifiziert. Alle anderen Testfälle bleiben durch den Patch unberührt.
---
## 5. Bekannte offene Punkte
| ID | Beschreibung | Priorität |
|---|---|---|
| OP-7 | GUI-Integration: `DokumentPanel` muss mit `DokumentService` verbunden werden | Hoch |
| | SQLite-Repository (Produktionsbetrieb) noch nicht implementiert, InMemory für Tests ausreichend | Mittel |
| | Leistungserbringer-Daten in `DokumentServiceImpl.erstellePflichtangaben()` aktuell hartcodiert | Niedrig |

View File

@ -29,9 +29,9 @@ header-includes:
**Modul:** Software Engineering 1 **Modul:** Software Engineering 1
**Team:** SE1 Team 2 Hochschule Mannheim **Team:** SE1 Team 2 Hochschule Mannheim
**Version:** 1.0 **Version:** 1.0
**Stand:** 25.06.2026 **Stand:** 11.06.2026
**Autor:** Christopher Lampert **Autor:** Christopher Lampert
**Bezug:** Lastenheft v1.1 (24.06.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem **Bezug:** Lastenheft v1.1 (25.06.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem
Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.1. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest. Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.1. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest.

View File

@ -28,10 +28,10 @@ header-includes:
**Modul:** Software Engineering 1 **Modul:** Software Engineering 1
**Team:** SE1 Team 2 Hochschule Mannheim **Team:** SE1 Team 2 Hochschule Mannheim
**Version:** 1.1 **Version:** 1.0
**Stand:** 11.06.2026 **Stand:** 11.06.2026
**Autor:** Christopher Lampert **Autor:** Christopher Lampert
**Bezug:** Lastenheft v1.1 (24.05.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem **Bezug:** Lastenheft v1.0 (15.05.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem
Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.0. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest. Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.0. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest.

View File

@ -1,395 +0,0 @@
# Pflichtenheft - Dokumentenprozess Fakturierungssystem
**Gruppe F - SE1 Team 2**
**Version:** 1.0
**Datum:** 11.06.2026
---
## Inhaltsverzeichnis
- [Freigabeübersicht](#freigabeübersicht)
- [Dokumentenhistorie](#dokumentenhistorie)
- [1. Einleitung](#1-einleitung)
- [1.1 Zweck dieses Dokuments](#11-zweck-dieses-dokuments)
- [1.2 Bezug zum Project Charter](#12-bezug-zum-project-charter)
- [1.3 Geltungsbereich](#13-geltungsbereich)
- [2. Systemüberblick für Gruppe F](#2-systemüberblick-für-gruppe-f)
- [3. Stakeholder und Kontext](#3-stakeholder-und-kontext)
- [4. Funktionale Anforderungen](#4-funktionale-anforderungen)
- [5. Nicht-funktionale Anforderungen für das Modul](#5-nicht-funktionale-anforderungen-für-das-modul)
- [6. Daten und Schnittstellen](#6-daten-und-schnittstellen)
- [6.1 Datenobjekte und Java-Datentypen](#61-datenobjekte-und-java-datentypen)
- [6.2 Schnittstellen](#62-schnittstellen)
- [7. Systemarchitektur](#7-systemarchitektur)
- [8. Testbare Abnahmekriterien](#8-testbare-abnahmekriterien)
- [9. Traceability zwischen Lastenheft und Pflichtenheft](#9-traceability-zwischen-lastenheft-und-pflichtenheft)
- [10. Modultestplan](#10-modultestplan)
- [11. Glossar](#11-glossar)
- [12. Referenzen](#12-referenzen)
---
## Freigabeübersicht
| Ersteller | Prüfer | Freigebender |
|---|---|---|
| Gruppe F: Andreas Ivanovic, Armin Omanovic, Alexander Teller | Prof. Dr. Gerd Marmitt | SE1 Team 2 |
| SE1 Team 2 | Hochschule Mannheim | SE1 Team 2 |
| 11.06.2026 | [Datum Review] | 30.06.2026 |
---
## Dokumentenhistorie
| Version | Datum | Autor | Änderung |
|---|---:|---|---|
| 1.0 | 11.06.2026 | Gruppe F / Alexander Teller | Initiale Erstellung des Pflichtenheft-Teils und Modultestplans für den Dokumentenprozess |
---
# 1. Einleitung
## 1.1 Zweck dieses Dokuments
Dieses Pflichtenheft beschreibt die konkrete Umsetzung des Moduls **Dokumentenprozess** im Fakturierungssystem. Es konkretisiert die Anforderungen aus dem Lastenheft und dem Project Charter für den Verantwortungsbereich der Gruppe F. Der Fokus liegt auf der fachlichen Kette:
```text
Angebot -> Auftragsbestätigung -> Lieferschein -> Rechnung
```
Das Dokument dient als Input für Implementierung, Komponententests und Integration mit Produktverwaltung, Kundenverwaltung und GUI.
## 1.2 Bezug zum Project Charter
Team 2 besteht aus den Gruppen E, F, G und H. Gruppe F ist laut Project Charter für den Dokumentenprozess verantwortlich. Der Dokumentenprozess umfasst die fachliche Kette **Angebot -> Auftragsbestätigung -> Lieferschein -> Rechnung**. Das Projekt nutzt **Java**, **JavaFX**, **SQLite** und **Gitty** und orientiert sich am **V-Modell**.
## 1.3 Geltungsbereich
Dieses Pflichtenheft deckt nur den Dokumentenprozess ab. Produktverwaltung, Kundenverwaltung und GUI werden nur als Schnittstellen beschrieben, weil sie durch die Gruppen G, H und E verantwortet werden.
Nicht Teil dieses Moduls sind:
- vollständige Buchhaltung
- E-Rechnung
- Mehrbenutzerbetrieb
- Cloud-Betrieb
- externe ERP-/FiBu-Anbindungen
---
# 2. Systemüberblick für Gruppe F
Das Modul Gruppe F stellt die Geschäftslogik für Dokumente bereit. Die GUI ruft den `DokumentService` auf. Produkt- und Kundendaten werden über Lookup-Schnittstellen gelesen. Dokumente werden persistent über ein Repository gespeichert. Aus vorhandenen Dokumenten können Folgedokumente erzeugt werden, wobei Status, Referenzen und fachliche Regeln geprüft werden.
---
# 3. Stakeholder und Kontext
| Stakeholder / Rolle | Interesse am Modul Dokumentenprozess |
|---|---|
| Auftraggeber Prof. Dr. Gerd Marmitt | Nachvollziehbare Umsetzung der Anforderungen und testbare Artefakte im V-Modell. |
| Gruppe F | Implementierung des Dokumentenworkflows und Bereitstellung testbarer Services. |
| Gruppe E - GUI | Benötigt Service-Methoden und DTOs für Anzeige, Eingabe und Export. |
| Gruppe G - Produktverwaltung | Liefert Produktdaten und muss prüfen können, ob Produkte in Dokumenten referenziert sind. |
| Gruppe H - Kundenverwaltung | Liefert Kundendaten und muss prüfen können, ob Kunden in Dokumenten referenziert sind. |
| Endnutzer | Möchte Angebote, Aufträge, Lieferscheine und Rechnungen korrekt erzeugen und wiederfinden. |
---
# 4. Funktionale Anforderungen
| PH-ID | LH-Quelle | Kurzname | Konkretisierung / Systemverhalten | Priorität | Abnahmekriterium |
|---|---|---|---|---|---|
| PH-DP-01 | BA-DP-01, GR-04, GR-06 | Angebot anlegen | Das System MUSS über `DokumentService.erstelleAngebot(long kundenId, List<PositionCommand> positionen)` ein Angebot für genau einen vorhandenen Kunden mit mindestens einer Position erzeugen. Pro Position werden Produktbezeichnung, Einzelpreis netto und MwSt.-Satz als Snapshot übernommen. | Muss | Angebot ist gespeichert, besitzt Nummer `A-yyyy-nnnn`, Status `OFFEN`, Datum `LocalDate.now()` und korrekt berechnete Netto-, USt.- und Bruttosumme. |
| PH-DP-02 | F-DP-02 | Angebotsnummer vergeben | Das System MUSS Angebotsnummern über `OfferNumberGenerator` je Kalenderjahr fortlaufend und eindeutig vergeben. | Muss | Zwei nacheinander erzeugte Angebote erhalten unterschiedliche und aufeinanderfolgende Nummern. |
| PH-DP-03 | F-DP-03 | Angebot bearbeiten | Das System MUSS die Bearbeitung eines Angebots nur erlauben, solange `AngebotsStatus.OFFEN` gilt. | Muss | Bearbeitung bei `OFFEN` erfolgreich; bei `UEBERFUEHRT` wird `IllegalStateException` geworfen. |
| PH-DP-04 | BA-DP-04, GR-01 | Auftragsbestätigung erzeugen | Das System MUSS aus genau einem offenen Angebot eine Auftragsbestätigung mit eigener Nummer `AB-yyyy-nnnn` erzeugen und alle Positionen unverändert übernehmen. | Muss | AB referenziert Angebotsnummer, Positionen und Summen entsprechen dem Angebot, Angebot wird auf `UEBERFUEHRT` gesetzt. |
| PH-DP-05 | F-DP-05 | Doppelte Auftragsbestätigung verhindern | Das System MUSS vor der Erzeugung prüfen, ob für das Angebot bereits eine Auftragsbestätigung existiert. | Muss | Zweiter Erzeugungsversuch wird mit `DuplicateDocumentException` abgelehnt. |
| PH-DP-06 | BA-DP-06, GR-01 | Lieferschein erzeugen | Das System MUSS aus genau einer bestätigten Auftragsbestätigung einen Lieferschein mit Nummer `LS-yyyy-nnnn` und `LocalDate lieferdatum` erzeugen. | Muss | Lieferschein referenziert AB, übernimmt Produktbezeichnung und Mengen und erhält Status `GELIEFERT`. |
| PH-DP-07 | F-DP-07 | Lieferschein ohne Preise | Das System MUSS für Lieferscheinansicht und PDF nur Menge und Produktbezeichnung ausgeben, aber keine Einzelpreise, Netto-, USt.- oder Bruttowerte. | Muss | Export/DTO des Lieferscheins enthält keine Preisfelder. |
| PH-DP-08 | BA-DP-08, F-DP-09, GR-01 | Rechnung erzeugen | Das System MUSS aus genau einem gelieferten Lieferschein eine Rechnung mit Nummer `R-yyyy-nnnn`, Rechnungsdatum, Referenz auf den Lieferschein und Pflichtangaben nach UStG erzeugen. | Muss | Rechnung enthält Kunde, Leistungsdatum/Lieferdatum, Positionen, fortlaufende Nummer, Netto-, USt.- und Bruttosumme. |
| PH-DP-09 | F-DP-09 | Pflichtangaben prüfen | Das System MUSS vor dem Speichern einer Rechnung `RechnungPflichtangabenValidator.validate(Rechnung)` ausführen. | Muss | Fehlende Pflichtangaben verhindern Speicherung und liefern Validierungsfehler. |
| PH-DP-10 | F-DP-10, GR-02 | Rechnungsnummer fortlaufend | Das System MUSS Rechnungsnummern fortlaufend, eindeutig und lückenlos vergeben, sobald eine Rechnung erfolgreich gespeichert wird. | Muss | Drei erfolgreiche Rechnungen ergeben `R-2026-0001`, `R-2026-0002`, `R-2026-0003`. |
| PH-DP-11 | F-DP-11, GR-03 | Rechnung festschreiben | Das System MUSS eine gespeicherte Rechnung inhaltlich sperren (`festgeschrieben=true`). | Muss | Änderungsversuche an Positionen, Summen oder Pflichtangaben werden abgelehnt. |
| PH-DP-12 | F-DP-12 | PDF-Export | Das System SOLL jedes Dokument über `PdfExportService.export(Dokument)` als PDF-Datei im lokalen Dateisystem erzeugen. | Soll | PDF-Datei existiert, ist nicht leer und enthält Dokumentnummer und Dokumenttyp. |
| PH-DP-13 | F-DP-13 | Dokumentenübersicht pro Kunde | Das System SOLL über `DokumentRepository.findByKunde(long kundenId)` alle Dokumente eines Kunden sortiert nach Datum und Dokumenttyp bereitstellen. | Soll | Alle Angebote, ABs, Lieferscheine und Rechnungen des Kunden werden ohne fremde Kundendokumente geliefert. |
| PH-DP-14 | GR-04 | Preis-Snapshot sichern | Das System MUSS den Produktpreis und MwSt.-Satz beim Einfügen in das Angebot als Snapshot in `Dokumentposition` speichern. | Muss | Spätere Produktänderung beeinflusst bestehende Dokumentpositionen nicht. |
| PH-DP-15 | GR-05 | Stammdatenreferenzen prüfbar machen | Das Dokumentenmodul MUSS für Produkt- und Kundenverwaltung eine Prüffunktion `isReferenced(...)` bereitstellen. | Muss | Referenzierte Produkte/Kunden können durch andere Module als verwendet erkannt werden. |
| PH-DP-16 | NF-TEST-02, NF-MAINT-03 | Testbare Logik ohne GUI | Die Dokumentenlogik MUSS in Service- und Domainklassen ohne JavaFX-Abhängigkeit implementiert werden. | Muss | JUnit-Tests können `DokumentService` ohne Start der GUI ausführen. |
---
# 5. Nicht-funktionale Anforderungen für das Modul
| PH-NF-ID | LH-Quelle | Konkretisierung | Nachweis |
|---|---|---|---|
| PH-NF-DP-01 | NF-PERF-01 | Dokumentenaktionen bis 1.000 Kunden/Produkte müssen innerhalb von 1 Sekunde eine Rückmeldung liefern, wenn Repositories mit Testdaten initialisiert sind. | Zeitmessung im Komponententest/Integrationstest `< 1 s` für Erzeugung eines Angebots mit 10 Positionen. |
| PH-NF-DP-02 | NF-ARCH-01 | Dokumente müssen nach Neustart aus SQLite/Dateispeicher vollständig wiederherstellbar sein. | Dokument erstellen, Repository neu initialisieren, Dokument unverändert laden. |
| PH-NF-DP-03 | NF-ARCH-02 | Datenhaltung des Dokumentenmoduls muss über `DokumentRepository` gekapselt sein. | Code-Review: Service nutzt Interface, keine direkten SQL-Aufrufe im Service. |
| PH-NF-DP-04 | NF-MAINT-01, NF-MAINT-03 | Dokumentenprozess wird im Paket `de.hsma.se1.fakturierung.dokumente` umgesetzt und in Domain, Service, Repository und Export getrennt. | Repository-Struktur enthält getrennte Pakete; Abhängigkeiten laufen von UI -> Service -> Repository. |
| PH-NF-DP-05 | NF-TEST-02, NF-TEST-03 | Die Geschäftslogik des Dokumentenmoduls muss automatisiert testbar sein. | Mindestens 10 JUnit-Testfälle für Services/Domainklassen laufen ohne GUI. |
---
# 6. Daten und Schnittstellen
## 6.1 Datenobjekte und Java-Datentypen
| Datenfeld / Objekt | Java-Datentyp | Wertebereich / Format | Beschreibung |
|---|---|---|---|
| `DokumentType` | `enum` | `ANGEBOT`, `AUFTRAGSBESTAETIGUNG`, `LIEFERSCHEIN`, `RECHNUNG` | Typisierung für Anzeige, Export und Repository. |
| Dokumentnummer | `String` | Format `A-2026-0001`, `AB-2026-0001`, `LS-2026-0001`, `R-2026-0001` | Fachlicher Identifikator; nicht als `int`, da Präfix/Jahr enthalten. |
| `kundenId` | `long` | z. B. `10001L` | Referenz auf Kundenverwaltung, keine Kopie des gesamten Kundenobjekts im Dokumentservice erforderlich. |
| `produktId` | `long` | z. B. `20001L` | Referenz auf Produktverwaltung. |
| `bezeichnungSnapshot` | `String` | max. 255 Zeichen | Produktbezeichnung zum Zeitpunkt der Angebotsanlage. |
| `menge` | `int` | `> 0` | Menge pro Dokumentposition. |
| `einzelpreisNettoSnapshot` | `BigDecimal` | `>= 0`, Skalierung 2 | Geldbetrag; `BigDecimal` statt `double` wegen Rundungsfehlern. |
| `mwstSatzSnapshot` | `BigDecimal` | z. B. `0.19`, `0.07` | MwSt.-Satz als Dezimalwert. |
| `datum`, `lieferdatum`, `rechnungsdatum` | `LocalDate` | ISO-Format `yyyy-MM-dd` | Fachliche Datumswerte ohne Uhrzeit. |
| `nettoSumme`, `ustSumme`, `bruttoSumme` | `BigDecimal` | Skalierung 2, Rundung `HALF_UP` | Berechnete Summen des Dokuments. |
| `status` | `enum` | `OFFEN`, `UEBERFUEHRT`, `BESTAETIGT`, `GELIEFERT`, `FAKTURIERT`, `FESTGESCHRIEBEN` | Steuert erlaubte Folgeaktionen. |
| `pflichtangaben` | `RechnungPflichtangaben` | Objekt mit `leistungserbringerName`, `anschrift`, `steuerNr`, `leistungsdatum` usw. | Pflichtfelder der Rechnung. |
## 6.2 Schnittstellen
| Schnittstelle | Art | Zweck | Operationen / Daten |
|---|---|---|---|
| `DokumentService` | Java-Service-Interface | UI/Controller ruft Dokumentenlogik auf. | Methoden: `erstelleAngebot`, `erstelleAuftragsbestaetigung`, `erstelleLieferschein`, `erstelleRechnung`, `exportiereAlsPdf`. |
| `DokumentRepository` | Java-Interface / SQLite-Implementierung | Persistenz von Dokumenten. | `save`, `findByNummer`, `findByKunde`, `existsFolgedokument`, `existsReferencedProdukt`, `existsReferencedKunde`. |
| `ProductLookup` | Java-Interface zu Gruppe G | Produktdaten für Angebotspositionen lesen. | `findProduct(long produktId): Optional<ProductDto>` mit `String bezeichnung`, `BigDecimal einzelpreisNetto`, `BigDecimal mwstSatz`. |
| `CustomerLookup` | Java-Interface zu Gruppe H | Kundendaten für Dokumentkopf und Rechnungspflichtangaben lesen. | `findCustomer(long kundenId): Optional<CustomerDto>`. |
| `PdfExportService` | Java-Service / lokale Datei-Schnittstelle | PDF-Dateien aus Dokumentdaten erzeugen. | Rückgabe `Path`; Zielordner z. B. `/exports`. |
---
# 7. Systemarchitektur
Das Dokumentenmodul folgt einer dreischichtigen Struktur: Präsentation/UI ruft Services auf, Services enthalten die Geschäftslogik, Repositories kapseln die Datenhaltung. Die fachlichen Domänenklassen sind von JavaFX unabhängig und können isoliert getestet werden.
## Abbildung 1: UML-Klassendiagramm Gruppe F
```mermaid
classDiagram
direction TB
class DokumentService {
+erstelleAngebot(...) Angebot
+aktualisiereAngebot(...) Angebot
+erstelleAuftragsbestaetigung(...) Auftragsbestaetigung
+erstelleLieferschein(...) Lieferschein
+erstelleRechnung(...) Rechnung
+exportiereAlsPdf(...) Path
}
class Dokument {
<<abstract>>
-nummer String
-kundeId long
-datum LocalDate
-positionen List~Dokumentposition~
-nettoSumme BigDecimal
-ustSumme BigDecimal
-bruttoSumme BigDecimal
+berechneSummen() SummenDto
}
class Dokumentposition {
-produktId long
-bezeichnungSnapshot String
-menge int
-einzelpreisNettoSnapshot BigDecimal
-mwstSatzSnapshot BigDecimal
+berechneNetto() BigDecimal
+berechneUst() BigDecimal
}
class Angebot {
-angebotsNr String
-status AngebotsStatus
+istBearbeitbar() boolean
}
class Auftragsbestaetigung {
-auftragNr String
-angebotNr String
-status AuftragsStatus
+ausAngebot(...) ...
}
class Lieferschein {
-lieferscheinNr String
-auftragsNr String
-lieferdatum LocalDate
-status LieferStatus
}
class Rechnung {
-rechnungsNr String
-lieferscheinNr String
-rechnungsdatum LocalDate
-status RechnungsStatus
-festgeschrieben boolean
}
class DokumentRepository {
<<interface>>
+save(Dokument) Dokument
+findByNummer(String) Optional~Dokument~
+existsFolgedokument(String) boolean
+findByKunde(long) List~Dokument~
}
class NumberGenerator {
+nextAngebotsNr(int) String
+nextAuftragsNr(int) String
+nextLieferscheinNr(int) String
+nextRechnungsNr(int) String
}
class PdfExportService {
+export(Dokument) Path
+exportRechnung(Rechnung) Path
-templateEngine String
}
Dokument "1" *-- "1..*" Dokumentposition : enthält
Dokument <|-- Angebot
Dokument <|-- Auftragsbestaetigung
Dokument <|-- Lieferschein
Dokument <|-- Rechnung
DokumentService ..> Dokument : nutzt
DokumentService ..> DokumentRepository : nutzt
DokumentService ..> NumberGenerator : nutzt
DokumentService ..> PdfExportService : PDF-Export
Angebot --> Auftragsbestaetigung : 1:1 erzeugt aus
Auftragsbestaetigung --> Lieferschein : 1:1 erzeugt
Lieferschein --> Rechnung : 1:1 erzeugt aus
```
**Beschreibung zu Abbildung 1:** Das Klassendiagramm zeigt die zentrale abstrakte Klasse `Dokument` mit den Spezialisierungen `Angebot`, `Auftragsbestaetigung`, `Lieferschein` und `Rechnung`. Die Klasse `Dokumentposition` speichert Produktdaten als Snapshot. Der `DokumentService` nutzt Repositories, Nummerngenerator und PDF-Export, ohne selbst technische Persistenzdetails zu kennen.
## Abbildung 2: UML-Sequenzdiagramm Rechnung aus Lieferschein
```mermaid
sequenceDiagram
actor Anwender
participant UI as DokumentenUI
participant DS as DokumentService
participant LR as LieferscheinRepo
participant NG as NumberGenerator
participant RR as RechnungRepo
participant PDF as PdfExportService
Anwender->>UI: Klick: Rechnung erzeugen(lsNr)
UI->>DS: erstelleRechnung(lsNr)
DS->>LR: findByNummer(lsNr)
LR-->>DS: Lieferschein(status=GELIEFERT)
alt Lieferschein fehlt oder Status != GELIEFERT
DS-->>UI: Fehlermeldung, keine Rechnungsnummer vergeben, keine Rechnung gespeichert
UI-->>Anwender: Anzeige der Fehlermeldung
else Lieferschein ist geliefert
DS->>NG: nextRechnungsNr(jahr)
NG-->>DS: R-2026-0001
DS->>DS: pruefeStatusUndVorgaenger()
DS->>DS: berechneNettoUstBrutto()
DS->>RR: save(rechnung)
RR-->>DS: gespeicherte Rechnung
DS->>PDF: exportRechnung(rechnung)
PDF-->>DS: pdfPfad
DS-->>UI: Bestaetigung + Nummer + pdfPfad
UI-->>Anwender: Anzeige der Rechnung
end
```
**Beschreibung zu Abbildung 2:** Das Sequenzdiagramm beschreibt den Ablauf zur Rechnungserzeugung aus einem Lieferschein. Zuerst wird der Lieferschein geladen und sein Status geprüft. Danach wird eine Rechnungsnummer vergeben, die Rechnung berechnet, gespeichert, optional als PDF exportiert und an die GUI zurückgegeben. Falls der Lieferschein fehlt oder nicht den Status `GELIEFERT` besitzt, wird keine Rechnung erzeugt.
---
# 8. Testbare Abnahmekriterien
| AC-ID | Abgedeckte PH-Anforderungen | Testbares Abnahmekriterium |
|---|---|---|
| AC-DP-01 | PH-DP-01, PH-DP-02, PH-DP-14 | Aus vorhandenem Kunden und zwei Produkten wird ein Angebot erzeugt. Erwartet werden korrekte Nummer, Status `OFFEN`, zwei Positionen, Preis-Snapshots und korrekte Summen. |
| AC-DP-02 | PH-DP-03 | Ein offenes Angebot kann geändert werden. Ein überführtes Angebot kann nicht mehr geändert werden. |
| AC-DP-03 | PH-DP-04, PH-DP-05 | Aus einem offenen Angebot wird genau eine Auftragsbestätigung erzeugt. Ein zweiter Versuch wird abgelehnt. |
| AC-DP-04 | PH-DP-06, PH-DP-07 | Aus einer bestätigten AB wird ein Lieferschein erzeugt. Der Lieferschein enthält Mengen und Bezeichnungen, aber keine Preise. |
| AC-DP-05 | PH-DP-08, PH-DP-09, PH-DP-10, PH-DP-11 | Aus einem gelieferten Lieferschein wird eine festgeschriebene Rechnung mit Pflichtangaben, lückenloser Nummer und korrekten Summen erzeugt. |
| AC-DP-06 | PH-DP-12 | Ein Dokument kann als PDF exportiert werden; die Datei existiert und enthält Dokumentnummer und Dokumenttyp. |
| AC-DP-07 | PH-DP-13 | Alle Dokumente eines Kunden werden kundenbezogen aufgelistet. |
| AC-DP-08 | PH-DP-15 | Produkt- und Kundenverwaltung können prüfen, ob Stammdaten in Dokumenten referenziert werden. |
---
# 9. Traceability zwischen Lastenheft und Pflichtenheft
| LH-Anforderung | Kurzbeschreibung | PH-Anforderungen | Abnahmekriterium | Modultestfälle |
|---|---|---|---|---|
| BA-DP-01 | Angebot für Kunden anlegen | PH-DP-01, PH-DP-14 | AC-DP-01 | MT-DP-01, MT-DP-02, MT-DP-13 |
| F-DP-02 | Angebotsnummer eindeutig/fortlaufend | PH-DP-02 | AC-DP-01 | MT-DP-03 |
| F-DP-03 | Angebot nur bis Überführung bearbeiten | PH-DP-03 | AC-DP-02 | MT-DP-04 |
| BA-DP-04 | Auftragsbestätigung aus Angebot | PH-DP-04 | AC-DP-03 | MT-DP-05 |
| F-DP-05 | Maximal eine AB pro Angebot | PH-DP-05 | AC-DP-03 | MT-DP-06 |
| BA-DP-06 | Lieferschein aus AB | PH-DP-06 | AC-DP-04 | MT-DP-07 |
| F-DP-07 | Lieferschein ohne Preise | PH-DP-07 | AC-DP-04 | MT-DP-08 |
| BA-DP-08 | Rechnung aus Lieferschein | PH-DP-08 | AC-DP-05 | MT-DP-09 |
| F-DP-09 | Rechnungspflichtangaben | PH-DP-09 | AC-DP-05 | MT-DP-10 |
| F-DP-10 | Rechnungsnummer fortlaufend/lückenlos | PH-DP-10 | AC-DP-05 | MT-DP-11 |
| F-DP-11 | Rechnung unveränderlich | PH-DP-11 | AC-DP-05 | MT-DP-12 |
| F-DP-12 | PDF-Export | PH-DP-12 | AC-DP-06 | MT-DP-14 |
| F-DP-13 | Dokumentenübersicht Kunde | PH-DP-13 | AC-DP-07 | MT-DP-15 |
| GR-01 | Dokumentenkette | PH-DP-04, PH-DP-06, PH-DP-08 | AC-DP-03 bis AC-DP-05 | MT-DP-05, MT-DP-07, MT-DP-09 |
| GR-02 | Rechnungsnummern eindeutig | PH-DP-10 | AC-DP-05 | MT-DP-11 |
| GR-03 | Rechnung nicht änderbar | PH-DP-11 | AC-DP-05 | MT-DP-12 |
| GR-04 | Preisübernahme Snapshot | PH-DP-14 | AC-DP-01 | MT-DP-13 |
| GR-05 | Stammdatenschutz | PH-DP-15 | AC-DP-08 | MT-DP-16 |
| GR-06 | Summenberechnung | PH-DP-01, PH-DP-08 | AC-DP-01, AC-DP-05 | MT-DP-02, MT-DP-09 |
---
# 10. Modultestplan
Die folgenden Testfälle sind so formuliert, dass sie direkt mit JUnit 5 umgesetzt werden können. Für den isolierten Modultest werden Produkt- und Kundendaten durch Fake-Lookups bereitgestellt. Die GUI wird nicht gestartet.
| Test-ID | Name | Abgedeckte PH-Anforderungen | Vorbedingung | Testschritte | Erwartetes Ergebnis | Umsetzungshinweis |
|---|---|---|---|---|---|---|
| MT-DP-01 | Angebot anlegen - Happy Path | PH-DP-01 | Kunde 10001 und Produkte 20001/20002 existieren. | `erstelleAngebot(10001, [2x Produkt 20001, 1x Produkt 20002])` | Angebot gespeichert, Status `OFFEN`, 2 Positionen, Nummer beginnt mit `A-2026-`. | JUnit: `assertEquals`, `FakeProductLookup`, `InMemoryDokumentRepository` |
| MT-DP-02 | Summenberechnung Angebot | PH-DP-01, PH-DP-08 | Positionen: 2 x 100,00 EUR mit 19 %, 1 x 50,00 EUR mit 7 %. | `angebot.berechneSummen()` | Netto 250,00; USt. 41,50; Brutto 291,50. | JUnit: BigDecimal-Vergleich mit `compareTo` oder skalierter Assertion |
| MT-DP-03 | Fortlaufende Angebotsnummern | PH-DP-02 | Leerer Nummernkreis 2026. | Zwei Angebote erzeugen. | Nummern `A-2026-0001` und `A-2026-0002`. | JUnit: `assertNotEquals`, `assertTrue(endsWith)` |
| MT-DP-04 | Angebot nach Überführung sperren | PH-DP-03 | Angebot wurde in AB überführt. | `aktualisiereAngebot(...)` aufrufen. | `IllegalStateException`; Positionen unverändert. | JUnit: `assertThrows` |
| MT-DP-05 | Auftragsbestätigung erzeugen | PH-DP-04 | Offenes Angebot mit 2 Positionen existiert. | `erstelleAuftragsbestaetigung(angebotsNr)` | AB mit eigener Nummer, Angebotsreferenz und identischen Positionen gespeichert. | JUnit: Objektvergleich Positionen |
| MT-DP-06 | Doppelte AB verhindern | PH-DP-05 | Für Angebot existiert bereits eine AB. | `erstelleAuftragsbestaetigung(angebotsNr)` erneut. | `DuplicateDocumentException`; keine zweite AB im Repository. | JUnit: `assertThrows`, Repository-Count |
| MT-DP-07 | Lieferschein erzeugen | PH-DP-06 | AB im Status `BESTAETIGT` existiert. | `erstelleLieferschein(auftragsNr, lieferdatum)` | LS mit Nummer, Lieferdatum, AB-Referenz und Status `GELIEFERT` gespeichert. | JUnit: `assertEquals` |
| MT-DP-08 | Lieferschein enthält keine Preise | PH-DP-07 | Lieferschein aus AB existiert. | `LieferscheinDto.from(lieferschein)` oder Exportmodell erzeugen. | DTO enthält Menge und Bezeichnung, aber keine Preis-/Steuerfelder. | JUnit: Getter/DTO-Feldprüfung |
| MT-DP-09 | Rechnung erzeugen - Happy Path | PH-DP-08, PH-DP-09 | LS im Status `GELIEFERT` mit Kunde und Positionen existiert. | `erstelleRechnung(lieferscheinNr)` | Rechnung mit `R-...`, Pflichtangaben, Summen und Status `FESTGESCHRIEBEN` gespeichert. | JUnit: `assertAll` |
| MT-DP-10 | Pflichtangaben fehlen | PH-DP-09 | Kunde ohne Rechnungsanschrift/Steuernummer in Testdaten. | `erstelleRechnung(lieferscheinNr)` | Validierungsfehler; keine Rechnung gespeichert. | JUnit: `assertThrows(ValidationException.class)` |
| MT-DP-11 | Rechnungsnummern lückenlos | PH-DP-10 | Drei unterschiedliche gelieferte Lieferscheine existieren. | Drei Rechnungen erzeugen. | Nummern enden auf `0001`, `0002`, `0003` ohne Lücke. | JUnit: Listenvergleich |
| MT-DP-12 | Rechnung unveränderlich | PH-DP-11 | Rechnung wurde gespeichert/festgeschrieben. | Änderung von Position oder Summe versuchen. | Änderung wird abgelehnt; geladene Rechnung bleibt unverändert. | JUnit: `assertThrows`, erneutes Laden |
| MT-DP-13 | Preis-Snapshot bleibt stabil | PH-DP-14 | Angebot wurde mit Produktpreis 100,00 erzeugt. | Danach Produktpreis auf 120,00 setzen. Angebot erneut laden. | Positionspreis im Angebot bleibt 100,00. | JUnit mit `FakeProductLookup`-Preiswechsel |
| MT-DP-14 | PDF-Export erzeugt Datei | PH-DP-12 | Angebot oder Rechnung existiert. | `exportiereAlsPdf(dokumentNr)` | Rückgabe `Path`; Datei existiert und Größe `> 0 Byte`. | JUnit mit temporärem Verzeichnis `@TempDir` |
| MT-DP-15 | Dokumentenübersicht pro Kunde | PH-DP-13 | Dokumente für Kunde 1 und Kunde 2 existieren. | `findByKunde(1)` | Nur Dokumente von Kunde 1, sortiert nach Datum. | JUnit: `assertTrue(allMatch)` |
| MT-DP-16 | Stammdatenreferenz prüfen | PH-DP-15 | Produkt 20001 ist in Angebot referenziert, Produkt 99999 nicht. | `isProductReferenced(20001)` und `isProductReferenced(99999)` | Ergebnis `true` bzw. `false`. | JUnit: `assertTrue`, `assertFalse` |
---
# 11. Glossar
| Begriff | Bedeutung |
|---|---|
| Angebot | Preisangebot an einen Kunden, Ausgangspunkt der Dokumentenkette. |
| Auftragsbestätigung | Verbindliche Bestätigung der Annahme eines Angebots. |
| Lieferschein | Dokumentiert Lieferung; enthält keine Preise. |
| Rechnung | Festgeschriebenes Dokument mit Zahlungsforderung und Pflichtangaben. |
| Snapshot | Kopie von Produktpreis, Produktbezeichnung und MwSt.-Satz zum Zeitpunkt der Angebotsanlage. |
| Traceability | Rückverfolgbarkeit von Lastenheft-Anforderung zu Pflichtenheft-Anforderung und Testfall. |
---
# 12. Referenzen
- Project Charter - Fakturierungssystem, Version 1.0, 15.04.2026.
- Lastenheft - Fakturierungssystem, Version 1.0 alpha, 12.05.2026.
- Vorlesung Software Engineering 1: Lastenheft, Pflichtenheft, Traceability, V-Modell und Komponententests.

View File

@ -0,0 +1,8 @@
[ {
"productId" : "P-0003",
"bezeichnung" : "Bohrmaschine",
"einzelpreisNetto" : 89.99,
"mwstSatz" : "NORMAL",
"beschreibung" : "Schlagbohrer",
"kategorie" : "Elektrowerkzeuge"
} ]

View File

@ -98,34 +98,4 @@ public abstract class Dokument {
return String.format("%s{nummer='%s', kundenId=%d, datum=%s, status=%s}", return String.format("%s{nummer='%s', kundenId=%d, datum=%s, status=%s}",
getClass().getSimpleName(), dokumentNummer, kundenId, datum, status); getClass().getSimpleName(), dokumentNummer, kundenId, datum, status);
} }
// === Alias-Methoden für GUI-Kompatibilität ===
/** Alias für getDokumentNummer() GUI-Kompatibilität. */
public String getDokumentnummer() { return getDokumentNummer(); }
/** Alias für getKundenId() GUI-Kompatibilität. */
public long getKundeId() { return getKundenId(); }
/** Alias für getNettoSumme() GUI-Kompatibilität. */
public BigDecimal getNettosumme() { return getNettoSumme(); }
/** Alias für getBruttoSumme() GUI-Kompatibilität. */
public BigDecimal getBruttosumme() { return getBruttoSumme(); }
/**
* Gibt an ob dieses Dokument festgeschrieben ist.
* Standard: false. Wird in Rechnung auf true gesetzt (PHGUI11).
*/
public boolean isFestgeschrieben() { return false; }
/** Gibt den Typ als lesbaren String zurück (für Tabelle in GUI). */
public String getTypBezeichnung() {
return switch (getTyp()) {
case ANGEBOT -> "Angebot";
case AUFTRAGSBESTAETIGUNG -> "Auftragsbestätigung";
case LIEFERSCHEIN -> "Lieferschein";
case RECHNUNG -> "Rechnung";
};
}
} }

View File

@ -71,23 +71,4 @@ public class DokumentPosition {
"einzelpreisNetto=%s, mwstSatz=%s}", "einzelpreisNetto=%s, mwstSatz=%s}",
produktId, bezeichnungSnapshot, menge, einzelpreisNettoSnapshot, mwstSatzSnapshot); produktId, bezeichnungSnapshot, menge, einzelpreisNettoSnapshot, mwstSatzSnapshot);
} }
// === Alias-Methoden für GUI-Kompatibilität ===
/** Alias für getBezeichnungSnapshot() GUI-Kompatibilität. */
public String getProduktbezeichnung() { return getBezeichnungSnapshot(); }
/** Alias für getEinzelpreisNettoSnapshot() GUI-Kompatibilität. */
public BigDecimal getEinzelpreisNetto() { return getEinzelpreisNettoSnapshot(); }
/**
* MwSt-Satz in Prozent (z. B. 19.00 statt 0.19) GUI-Kompatibilität.
*/
public BigDecimal getMehrwertsteuerSatz() {
return getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100))
.setScale(0, java.math.RoundingMode.HALF_UP);
}
/** Alias für getNettoGesamt() GUI-Kompatibilität. */
public BigDecimal getSumme() { return getNettoGesamt(); }
} }

View File

@ -2,7 +2,12 @@ package de.hsmannheim.faktura.dokumente.domain;
/** /**
* Steuert erlaubte Folgeaktionen im Dokumentenprozess. * Steuert erlaubte Folgeaktionen im Dokumentenprozess.
* Workflow: ANGEBOT: OFFEN UEBERFUEHRT | AB: BESTAETIGT | LS: GELIEFERT | R: FESTGESCHRIEBEN *
* Workflow:
* ANGEBOT: OFFEN UEBERFUEHRT
* AUFTRAGSBESTAETIGUNG: BESTAETIGT
* LIEFERSCHEIN: GELIEFERT
* RECHNUNG: FESTGESCHRIEBEN
*/ */
public enum DokumentStatus { public enum DokumentStatus {
OFFEN, OFFEN,
@ -10,17 +15,5 @@ public enum DokumentStatus {
BESTAETIGT, BESTAETIGT,
GELIEFERT, GELIEFERT,
FAKTURIERT, FAKTURIERT,
FESTGESCHRIEBEN; FESTGESCHRIEBEN
/** Lesbarer Anzeigename für die GUI (PHGUI10). */
public String getAnzeigename() {
return switch (this) {
case OFFEN -> "Offen";
case UEBERFUEHRT -> "Überführt";
case BESTAETIGT -> "Bestätigt";
case GELIEFERT -> "Geliefert";
case FAKTURIERT -> "Fakturiert";
case FESTGESCHRIEBEN -> "Festgeschrieben";
};
}
} }

View File

@ -74,20 +74,4 @@ public interface DokumentRepository {
* @return die höchste laufende Nummer oder 0 wenn noch keine existiert * @return die höchste laufende Nummer oder 0 wenn noch keine existiert
*/ */
int findMaxLaufendeNummer(String praefix, int jahr); int findMaxLaufendeNummer(String praefix, int jahr);
/**
* Gibt alle gespeicherten Dokumente zurück (für die Listenansicht in der GUI).
*
* @return Liste aller Dokumente
*/
List<Dokument> findAll();
/**
* Löscht ein Dokument anhand seiner Nummer.
* Festgeschriebene Rechnungen können nicht gelöscht werden.
*
* @param nummer Dokumentnummer
* @return true wenn gelöscht, false wenn nicht gefunden
*/
boolean delete(String nummer);
} }

View File

@ -94,14 +94,4 @@ public class InMemoryDokumentRepository implements DokumentRepository {
public void clear() { public void clear() {
store.clear(); store.clear();
} }
@Override
public List<Dokument> findAll() {
return new ArrayList<>(store.values());
}
@Override
public boolean delete(String nummer) {
return store.remove(nummer) != null;
}
} }

View File

@ -113,20 +113,18 @@ public class DokumentServiceImpl implements DokumentService {
public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) { public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) {
Angebot angebot = ladeAngebot(angebotsNummer); Angebot angebot = ladeAngebot(angebotsNummer);
// Duplikat-Check zuerst (PH-DP-05 / MT-DP-06): wenn bereits eine AB existiert, // Status-Prüfung (PH-DP-04)
// ist das Angebot zwangsläufig UEBERFUEHRT und beide Prüfungen würden anschlagen.
// Die Spec verlangt in diesem Szenario explizit DuplicateDocumentException.
if (repository.existsFolgedokument(angebotsNummer)) {
throw new DuplicateDocumentException(angebotsNummer);
}
// Allgemeiner Status-Check (PH-DP-04): greift bei anderen Nicht-OFFEN-Zuständen.
if (angebot.getStatus() != DokumentStatus.OFFEN) { if (angebot.getStatus() != DokumentStatus.OFFEN) {
throw new IllegalStateException( throw new IllegalStateException(
"Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " + "Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " +
angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden."); angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden.");
} }
// Doppelte AB verhindern (PH-DP-05)
if (repository.existsFolgedokument(angebotsNummer)) {
throw new DuplicateDocumentException(angebotsNummer);
}
// AB erzeugen Positionen 1:1 übernehmen // AB erzeugen Positionen 1:1 übernehmen
Auftragsbestaetigung ab = new Auftragsbestaetigung(); Auftragsbestaetigung ab = new Auftragsbestaetigung();
ab.setDokumentNummer(abGenerator.naechsteNummer()); ab.setDokumentNummer(abGenerator.naechsteNummer());

View File

@ -1,153 +0,0 @@
package de.hsmannheim.faktura.dokumente.service;
import de.hsmannheim.faktura.dokumente.domain.*;
import de.hsmannheim.faktura.dokumente.export.SimplePdfExportService;
import de.hsmannheim.faktura.dokumente.lookup.*;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
/**
* Fassade für die GUI (DokumentPanel).
* Kapselt den DokumentServiceImpl und bietet die Methoden die die GUI braucht.
*
* Wird in der GUI so instanziiert:
* new DokumentenService(new InMemoryDokumentRepository())
*
* Sobald Gruppe G (Produkte) und H (Kunden) ihre Lookups liefern,
* können diese über den Setter-Konstruktor injiziert werden.
*/
public class DokumentenService {
private final DokumentService service;
private final DokumentRepository repository;
/**
* Konstruktor für die GUI nimmt ein Repository entgegen.
* Produkt- und Kundenlookup werden als Stubs vorbelegt.
*/
public DokumentenService(DokumentRepository repository) {
this.repository = repository;
// Stub-Lookups bis Gruppe G und H ihre Implementierungen liefern
ProductLookup produktLookup = new StubProductLookup();
CustomerLookup kundeLookup = new StubCustomerLookup();
this.service = new DokumentServiceImpl(
repository,
produktLookup,
kundeLookup,
new SimplePdfExportService()
);
}
/**
* Vollständiger Konstruktor für Produktion wenn echte Lookups vorliegen.
*/
public DokumentenService(DokumentRepository repository,
ProductLookup produktLookup,
CustomerLookup kundeLookup) {
this.repository = repository;
this.service = new DokumentServiceImpl(
repository, produktLookup, kundeLookup, new SimplePdfExportService()
);
}
// -------------------------------------------------------------------------
// Methoden für die GUI
// -------------------------------------------------------------------------
/** Alle Dokumente für die Listenansicht. */
public List<Dokument> getAllDokumente() {
return repository.findAll();
}
/** Einzelnes Dokument nach Nummer laden. */
public Optional<Dokument> getDokumentByNummer(String nummer) {
return repository.findByNummer(nummer);
}
/**
* Neues Angebot anlegen (PH-DP-01).
* @param kundenId Kunde (muss in Kundenverwaltung existieren / vom CustomerLookup auflösbar sein)
* @param positionen mindestens eine Position (Produkt-ID + Menge)
* @return das gespeicherte Angebot mit Nummer und Status OFFEN
*/
public Angebot createAngebot(long kundenId, List<PositionCommand> positionen) {
return service.erstelleAngebot(kundenId, positionen);
}
/**
* Dokument löschen. Festgeschriebene Rechnungen können nicht gelöscht werden.
* @return true wenn erfolgreich gelöscht
*/
public boolean deleteDokument(String nummer) {
Optional<Dokument> dok = repository.findByNummer(nummer);
if (dok.isEmpty()) return false;
if (dok.get().isFestgeschrieben()) return false; // PHGUI11
return repository.delete(nummer);
}
/** Auftragsbestätigung aus Angebot erzeugen. */
public Auftragsbestaetigung createAuftragsbestaetigung(String angebotsNummer) {
return service.erstelleAuftragsbestaetigung(angebotsNummer);
}
/** Lieferschein aus AB erzeugen (Lieferdatum = heute). */
public Lieferschein createLieferschein(String auftragsNummer) {
return service.erstelleLieferschein(auftragsNummer, LocalDate.now());
}
/** Rechnung aus Lieferschein erzeugen. */
public Rechnung createRechnung(String lieferscheinNummer) {
return service.erstelleRechnung(lieferscheinNummer);
}
/** PDF-Export eines Dokuments. */
public Path exportPdf(String dokumentNummer, DokumentType typ) {
return service.exportiereAlsPdf(dokumentNummer);
}
// -------------------------------------------------------------------------
// Stub-Implementierungen (Platzhalter bis Gruppe G und H fertig sind)
// -------------------------------------------------------------------------
/**
* Platzhalter-Produktlookup.
* Ersetzen durch echte Implementierung von Gruppe G.
*/
private static class StubProductLookup implements ProductLookup {
@Override
public Optional<ProductDto> findProduct(long produktId) {
// Beispieldaten bis Gruppe G ihre Implementierung liefert
return Optional.of(new ProductDto(
produktId,
"Produkt #" + produktId,
new BigDecimal("100.00"),
new BigDecimal("0.19")
));
}
}
/**
* Platzhalter-Kundenlookup.
* Ersetzen durch echte Implementierung von Gruppe H.
*/
private static class StubCustomerLookup implements CustomerLookup {
@Override
public Optional<CustomerDto> findCustomer(long kundenId) {
// Beispieldaten bis Gruppe H ihre Implementierung liefert
return Optional.of(new CustomerDto(
kundenId,
"Kunde #" + kundenId,
"Musterstraße 1, 68163 Mannheim",
"DE123456789"
));
}
}
}

View File

@ -3,18 +3,12 @@ package de.hsmannheim.faktura.gui;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.*; import javax.swing.table.*;
import java.awt.*; import java.awt.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.hsmannheim.faktura.dokumente.domain.Angebot;
import de.hsmannheim.faktura.dokumente.domain.Auftragsbestaetigung;
import de.hsmannheim.faktura.dokumente.domain.Dokument; import de.hsmannheim.faktura.dokumente.domain.Dokument;
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus; import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
import de.hsmannheim.faktura.dokumente.domain.Lieferschein;
import de.hsmannheim.faktura.dokumente.service.DokumentenService; import de.hsmannheim.faktura.dokumente.service.DokumentenService;
import de.hsmannheim.faktura.dokumente.service.PositionCommand; import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
/** /**
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12). * Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12).
@ -62,59 +56,19 @@ public class DokumentPanel extends JPanel {
public DokumentPanel(MainFrame mainFrame) { public DokumentPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame; this.mainFrame = mainFrame;
// FIX: InMemoryDokumentRepository statt DokumentRepository (Interface) this.dokumentenService = new DokumentenService(new DokumentRepository());
this.dokumentenService = new DokumentenService(new InMemoryDokumentRepository());
setLayout(new BorderLayout(8, 8)); setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
initComponents(); initComponents();
initialisiereDemoDaten();
ladeDokumente(); ladeDokumente();
} }
/**
* Legt beim Start einige Beispieldokumente an, damit der gesamte
* Belegfluss (Angebot AB Lieferschein Rechnung) sofort
* demonstriert werden kann.
*/
private void initialisiereDemoDaten() {
try {
// 1) Frisches Angebot (Status OFFEN) Folgeaktion "→ AB" wird aktiv
dokumentenService.createAngebot(1001L, List.of(
new PositionCommand(1L, 5),
new PositionCommand(2L, 3)));
// 2) Angebot überführt in AB (Status BESTAETIGT) Folgeaktion "→ Lieferschein" wird aktiv
Angebot a2 = dokumentenService.createAngebot(1002L,
List.of(new PositionCommand(1L, 10)));
dokumentenService.createAuftragsbestaetigung(a2.getDokumentNummer());
// 3) Kette bis Lieferschein (Status GELIEFERT) Folgeaktion "→ Rechnung" wird aktiv
Angebot a3 = dokumentenService.createAngebot(1003L,
List.of(new PositionCommand(3L, 2)));
Auftragsbestaetigung ab3 =
dokumentenService.createAuftragsbestaetigung(a3.getDokumentNummer());
dokumentenService.createLieferschein(ab3.getDokumentNummer());
// 4) Komplette Kette bis Rechnung (Status FESTGESCHRIEBEN) nur PDF-Export aktiv
Angebot a4 = dokumentenService.createAngebot(1004L,
List.of(new PositionCommand(1L, 1)));
Auftragsbestaetigung ab4 =
dokumentenService.createAuftragsbestaetigung(a4.getDokumentNummer());
Lieferschein ls4 =
dokumentenService.createLieferschein(ab4.getDokumentNummer());
dokumentenService.createRechnung(ls4.getDokumentNummer());
} catch (Exception ex) {
System.err.println("Demo-Daten konnten nicht initialisiert werden: "
+ ex.getMessage());
}
}
private void initComponents() { private void initComponents() {
// --- Linke Seite: Dokumentenliste --- // --- Linke Seite: Dokumentenliste ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4)); JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(300, 0)); leftPanel.setPreferredSize(new Dimension(300, 0));
String[] cols = {"Nr.", "Typ", "Status"}; String[] cols = {"Nr.", "Typ ▲", "Status"};
tableModel = new DefaultTableModel(cols, 0) { tableModel = new DefaultTableModel(cols, 0) {
public boolean isCellEditable(int r, int c) { return false; } public boolean isCellEditable(int r, int c) { return false; }
}; };
@ -126,7 +80,7 @@ public class DokumentPanel extends JPanel {
String status = (String) getValueAt(row, col); String status = (String) getValueAt(row, col);
if (status != null) { if (status != null) {
if (status.contains("Offen")) c.setForeground(new Color(41, 128, 185)); if (status.contains("Offen")) c.setForeground(new Color(41, 128, 185));
else if (status.contains("Bestätigt")) c.setForeground(new Color(39, 174, 96)); else if (status.contains("Erstellt")) c.setForeground(new Color(39, 174, 96));
else if (status.contains("Festgeschr")) c.setForeground(new Color(192, 57, 43)); else if (status.contains("Festgeschr")) c.setForeground(new Color(192, 57, 43));
else c.setForeground(Color.BLACK); else c.setForeground(Color.BLACK);
} }
@ -137,6 +91,7 @@ public class DokumentPanel extends JPanel {
} }
}; };
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Sortierung (PHGUI08)
dokumentTable.setAutoCreateRowSorter(true); dokumentTable.setAutoCreateRowSorter(true);
dokumentTable.getSelectionModel().addListSelectionListener(e -> { dokumentTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes(); if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
@ -148,6 +103,7 @@ public class DokumentPanel extends JPanel {
// --- Rechte Seite: Detailansicht --- // --- Rechte Seite: Detailansicht ---
JPanel rightPanel = new JPanel(new BorderLayout(4, 8)); JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
// Kopfbereich
JPanel headerPanel = new JPanel(new GridBagLayout()); JPanel headerPanel = new JPanel(new GridBagLayout());
headerPanel.setBorder(BorderFactory.createTitledBorder("Dokumentdetail")); headerPanel.setBorder(BorderFactory.createTitledBorder("Dokumentdetail"));
GridBagConstraints g = new GridBagConstraints(); GridBagConstraints g = new GridBagConstraints();
@ -155,7 +111,7 @@ public class DokumentPanel extends JPanel {
g.fill = GridBagConstraints.HORIZONTAL; g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 0.5; g.weightx = 0.5;
// Festgeschrieben-Checkbox (PHGUI11) // Festgeschrieben-Checkbox (PHGUI11) oben rechts
chkFestgeschrieben = new JCheckBox("Festgeschrieben"); chkFestgeschrieben = new JCheckBox("Festgeschrieben");
chkFestgeschrieben.setEnabled(false); chkFestgeschrieben.setEnabled(false);
g.gridx = 1; g.gridy = 0; g.anchor = GridBagConstraints.EAST; g.gridx = 1; g.gridy = 0; g.anchor = GridBagConstraints.EAST;
@ -198,7 +154,7 @@ public class DokumentPanel extends JPanel {
lblBrutto = new JLabel("0,00 €", SwingConstants.RIGHT); lblBrutto = new JLabel("0,00 €", SwingConstants.RIGHT);
lblBrutto.setFont(new Font("SansSerif", Font.BOLD, 13)); lblBrutto.setFont(new Font("SansSerif", Font.BOLD, 13));
summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto); summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto);
summenPanel.add(new JLabel("USt.:")); summenPanel.add(lblUst); summenPanel.add(new JLabel("USt. 19%:")); summenPanel.add(lblUst);
summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto); summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto);
JPanel mittelPanel = new JPanel(new BorderLayout()); JPanel mittelPanel = new JPanel(new BorderLayout());
@ -224,6 +180,7 @@ public class DokumentPanel extends JPanel {
stdButtons.add(btnAbbrechen); stdButtons.add(btnAbbrechen);
stdButtons.add(btnPdfExport); stdButtons.add(btnPdfExport);
// Folgeaktionen
JPanel folgeButtons = new JPanel(new FlowLayout(FlowLayout.LEFT)); JPanel folgeButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung erstellen"); btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung erstellen");
btnAuftragZuLieferschein = new JButton("→ Lieferschein erstellen"); btnAuftragZuLieferschein = new JButton("→ Lieferschein erstellen");
@ -237,16 +194,16 @@ public class DokumentPanel extends JPanel {
rightPanel.add(aktionenPanel, BorderLayout.SOUTH); rightPanel.add(aktionenPanel, BorderLayout.SOUTH);
// Button-Actions // Button-Actions
btnNeu.addActionListener(e -> neuesAngebotDialog()); btnNeu.addActionListener(e -> neuesFormular());
btnSpeichern.addActionListener(e -> neuesAngebotDialog());
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeDokumente(); }); btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeDokumente(); });
btnPdfExport.addActionListener(e -> pdfExport()); btnPdfExport.addActionListener(e -> pdfExport()); // PHGUI12
btnLoeschen.addActionListener(e -> loeschen()); btnLoeschen.addActionListener(e -> loeschen());
btnAngebotZuAuftrag.addActionListener(e -> folgeaktion("ANGEBOT_ZU_AUFTRAG")); btnAngebotZuAuftrag.addActionListener(e -> folgeaktion("ANGEBOT_ZU_AUFTRAG"));
btnAuftragZuLieferschein.addActionListener(e -> folgeaktion("AUFTRAG_ZU_LIEFERSCHEIN")); btnAuftragZuLieferschein.addActionListener(e -> folgeaktion("AUFTRAG_ZU_LIEFERSCHEIN"));
btnLieferscheinZuRechnung.addActionListener(e -> folgeaktion("LIEFERSCHEIN_ZU_RECHNUNG")); btnLieferscheinZuRechnung.addActionListener(e -> folgeaktion("LIEFERSCHEIN_ZU_RECHNUNG"));
// Initial alle deaktivieren (PHGUI10)
setAktionenDeaktiviert(); setAktionenDeaktiviert();
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
@ -255,13 +212,12 @@ public class DokumentPanel extends JPanel {
} }
// --- Daten --- // --- Daten ---
private void ladeDokumente() { private void ladeDokumente() {
tableModel.setRowCount(0); tableModel.setRowCount(0);
List<Dokument> alle = dokumentenService.getAllDokumente(); List<Dokument> alle = dokumentenService.getAllDokumente();
for (Dokument d : alle) { for (Dokument d : alle) {
tableModel.addRow(new Object[]{ tableModel.addRow(new Object[]{
d.getDokumentNummer(), // FIX: getDokumentNummer() statt getDokumentnummer() d.getDokumentnummer(),
d.getTypBezeichnung(), d.getTypBezeichnung(),
d.getStatus().getAnzeigename() d.getStatus().getAnzeigename()
}); });
@ -276,12 +232,13 @@ public class DokumentPanel extends JPanel {
dokumentenService.getDokumentByNummer(nr).ifPresent(d -> { dokumentenService.getDokumentByNummer(nr).ifPresent(d -> {
aktuellesDokument = d; aktuellesDokument = d;
txtDokumentnummer.setText(d.getDokumentNummer()); // FIX txtDokumentnummer.setText(d.getDokumentnummer());
txtKundenId.setText(String.valueOf(d.getKundenId())); // FIX txtKundenId.setText(String.valueOf(d.getKundeId()));
txtDatum.setText(d.getDatum().toString()); txtDatum.setText(d.getDatum().toString());
txtStatus.setText(d.getStatus().getAnzeigename()); txtStatus.setText(d.getStatus().getAnzeigename());
chkFestgeschrieben.setSelected(d.isFestgeschrieben()); boolean festgeschrieben = d.isFestgeschrieben();
chkFestgeschrieben.setSelected(festgeschrieben);
// Positionen laden // Positionen laden
posTableModel.setRowCount(0); posTableModel.setRowCount(0);
@ -289,35 +246,35 @@ public class DokumentPanel extends JPanel {
for (var position : d.getPositionen()) { for (var position : d.getPositionen()) {
posTableModel.addRow(new Object[]{ posTableModel.addRow(new Object[]{
pos++, pos++,
position.getBezeichnungSnapshot(), // FIX position.getProduktbezeichnung(),
position.getMenge(), position.getMenge(),
position.getEinzelpreisNettoSnapshot() + " €", // FIX position.getEinzelpreisNetto() + " €",
position.getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100)) position.getMehrwertsteuerSatz() + "%",
.setScale(0, java.math.RoundingMode.HALF_UP) + "%",// FIX position.getSumme() + " €"
position.getNettoGesamt() + " €" // FIX
}); });
} }
// Summen (nicht für Lieferschein) // Summen
if (d.getNettoSumme() != null) { // FIX if (d.getNettosumme() != null) {
lblNetto.setText(d.getNettoSumme().toPlainString() + " €"); // FIX lblNetto.setText(d.getNettosumme().toPlainString() + " €");
lblUst.setText(d.getUstSumme().toPlainString() + " €"); lblUst.setText(d.getUstSumme().toPlainString() + " €");
lblBrutto.setText(d.getBruttoSumme().toPlainString() + " €"); // FIX lblBrutto.setText(d.getBruttosumme().toPlainString() + " €");
} }
// PHGUI10: Zulässige Folgeaktionen setzen
aktualisiereAktionen(d); aktualisiereAktionen(d);
}); });
} }
/** /**
* Aktiviert nur zulässige Folgeaktionen basierend auf Dokumentstatus (PHGUI10). * Aktiviert nur die zulässigen Folgeaktionen basierend auf dem Dokumentstatus (PHGUI10).
* Festgeschriebene Rechnungen: nur PDF-Export aktiv (PHGUI11). * Festgeschriebene Rechnungen: alle Bearbeitungsaktionen deaktiviert (PHGUI11).
*/ */
private void aktualisiereAktionen(Dokument d) { private void aktualisiereAktionen(Dokument d) {
DokumentStatus status = d.getStatus(); DokumentStatus status = d.getStatus();
boolean festgeschrieben = d.isFestgeschrieben(); boolean festgeschrieben = d.isFestgeschrieben();
// PHGUI11: Festgeschriebene Rechnung → nur PDF-Export // PHGUI11: Festgeschriebene Rechnung → alles deaktiviert außer PDF-Export
if (festgeschrieben) { if (festgeschrieben) {
btnNeu.setEnabled(true); btnNeu.setEnabled(true);
btnSpeichern.setEnabled(false); btnSpeichern.setEnabled(false);
@ -330,16 +287,16 @@ public class DokumentPanel extends JPanel {
return; return;
} }
// PHGUI10: Folgeaktionen je nach Status (FIX: korrekte Status-Konstanten) // PHGUI10: Folgeaktionen je nach Status
btnNeu.setEnabled(true); btnNeu.setEnabled(true);
btnSpeichern.setEnabled(true); btnSpeichern.setEnabled(true);
btnLoeschen.setEnabled(true); btnLoeschen.setEnabled(true);
btnAbbrechen.setEnabled(true); btnAbbrechen.setEnabled(true);
btnPdfExport.setEnabled(status == DokumentStatus.FESTGESCHRIEBEN); // FIX btnPdfExport.setEnabled(status == DokumentStatus.RECHNUNG_ERSTELLT);
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.OFFEN); // FIX btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.ANGEBOT_OFFEN);
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.BESTAETIGT); // FIX btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.AUFTRAGSBESTAETIGUNG_ERSTELLT);
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.GELIEFERT); // FIX btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.LIEFERSCHEIN_ERSTELLT);
} }
private void setAktionenDeaktiviert() { private void setAktionenDeaktiviert() {
@ -368,18 +325,19 @@ public class DokumentPanel extends JPanel {
mainFrame.showStatus("Kein Dokument ausgewählt."); mainFrame.showStatus("Kein Dokument ausgewählt.");
return; return;
} }
// PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog( int confirm = JOptionPane.showConfirmDialog(
this, "Dokument wirklich löschen?", "Löschen bestätigen", this, "Dokument wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION JOptionPane.YES_NO_OPTION
); );
if (confirm != JOptionPane.YES_OPTION) return; if (confirm != JOptionPane.YES_OPTION) return;
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentNummer()); // FIX boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentnummer());
if (deleted) { if (deleted) {
ladeDokumente(); neuesFormular(); ladeDokumente(); neuesFormular();
mainFrame.showStatus("Dokument gelöscht."); mainFrame.showStatus("Dokument gelöscht.");
} else { } else {
mainFrame.showStatus("Dokument kann nicht gelöscht werden (z. B. festgeschriebene Rechnung)."); mainFrame.showStatus("Dokument kann nicht gelöscht werden.");
} }
} }
@ -388,11 +346,11 @@ public class DokumentPanel extends JPanel {
try { try {
switch (typ) { switch (typ) {
case "ANGEBOT_ZU_AUFTRAG" -> case "ANGEBOT_ZU_AUFTRAG" ->
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentNummer()); // FIX dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentnummer());
case "AUFTRAG_ZU_LIEFERSCHEIN" -> case "AUFTRAG_ZU_LIEFERSCHEIN" ->
dokumentenService.createLieferschein(aktuellesDokument.getDokumentNummer()); // FIX dokumentenService.createLieferschein(aktuellesDokument.getDokumentnummer());
case "LIEFERSCHEIN_ZU_RECHNUNG" -> case "LIEFERSCHEIN_ZU_RECHNUNG" ->
dokumentenService.createRechnung(aktuellesDokument.getDokumentNummer()); // FIX dokumentenService.createRechnung(aktuellesDokument.getDokumentnummer());
} }
ladeDokumente(); ladeDokumente();
mainFrame.showStatus("Folgedokument erfolgreich erstellt."); mainFrame.showStatus("Folgedokument erfolgreich erstellt.");
@ -402,14 +360,14 @@ public class DokumentPanel extends JPanel {
} }
private void pdfExport() { private void pdfExport() {
// PHGUI12: PDF-Export auslösen
if (aktuellesDokument == null) { if (aktuellesDokument == null) {
mainFrame.showStatus("Kein Dokument ausgewählt."); mainFrame.showStatus("Kein Dokument ausgewählt.");
return; return;
} }
try { try {
dokumentenService.exportPdf( dokumentenService.exportPdf(aktuellesDokument.getDokumentnummer(), aktuellesDokument.getTyp());
aktuellesDokument.getDokumentNummer(), aktuellesDokument.getTyp()); // FIX mainFrame.showStatus("PDF-Export erfolgreich gestartet.");
mainFrame.showStatus("PDF-Export erfolgreich.");
} catch (Exception ex) { } catch (Exception ex) {
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage()); mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
} }
@ -421,140 +379,4 @@ public class DokumentPanel extends JPanel {
f.setBackground(new Color(230, 230, 230)); f.setBackground(new Color(230, 230, 230));
return f; return f;
} }
// -------------------------------------------------------------------------
// Dialog: Neues Angebot anlegen
// -------------------------------------------------------------------------
/**
* Öffnet einen modalen Dialog zum Anlegen eines neuen Angebots.
* Eingaben: Kunden-ID und mindestens eine Position (Produkt-ID + Menge).
* Bei erfolgreichem Anlegen wird das Angebot gespeichert und die
* Dokumentenliste neu geladen.
*/
private void neuesAngebotDialog() {
JDialog dialog = new JDialog(
(Frame) SwingUtilities.getWindowAncestor(this),
"Neues Angebot anlegen", true);
dialog.setLayout(new BorderLayout(8, 8));
((JComponent) dialog.getContentPane())
.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
// --- Kopfbereich: Kunden-ID ---
JPanel headPanel = new JPanel(new GridBagLayout());
headPanel.setBorder(BorderFactory.createTitledBorder("Kunde"));
GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(3, 4, 3, 4);
g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 1.0;
g.gridx = 0; g.gridy = 0; headPanel.add(new JLabel("Kunden-ID:"), g);
JTextField txtKundenIdInput = new JTextField("1001");
g.gridx = 1; headPanel.add(txtKundenIdInput, g);
// --- Positionsbereich ---
String[] cols = {"Produkt-ID", "Menge"};
DefaultTableModel posModel = new DefaultTableModel(cols, 0) {
@Override
public Class<?> getColumnClass(int column) { return String.class; }
@Override
public boolean isCellEditable(int r, int c) { return true; }
};
JTable tblPositionen = new JTable(posModel);
tblPositionen.setRowHeight(22);
// Eine Beispielzeile zum Loslegen
posModel.addRow(new Object[]{"1", "1"});
JPanel posButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton btnPosHinzu = new JButton("Position hinzufügen");
JButton btnPosEntfernen = new JButton("Position entfernen");
posButtons.add(btnPosHinzu);
posButtons.add(btnPosEntfernen);
btnPosHinzu.addActionListener(ev -> posModel.addRow(new Object[]{"1", "1"}));
btnPosEntfernen.addActionListener(ev -> {
int row = tblPositionen.getSelectedRow();
if (row >= 0) posModel.removeRow(row);
});
JPanel posPanel = new JPanel(new BorderLayout(4, 4));
posPanel.setBorder(BorderFactory.createTitledBorder("Positionen"));
posPanel.add(new JScrollPane(tblPositionen), BorderLayout.CENTER);
posPanel.add(posButtons, BorderLayout.SOUTH);
// --- OK / Abbrechen ---
JButton btnOk = new JButton("Anlegen");
JButton btnCancel = new JButton("Abbrechen");
JPanel dlgButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
dlgButtons.add(btnOk);
dlgButtons.add(btnCancel);
JLabel lblFehler = new JLabel(" ");
lblFehler.setForeground(new Color(192, 57, 43));
btnCancel.addActionListener(ev -> dialog.dispose());
btnOk.addActionListener(ev -> {
// Bei in der Tabelle laufender Bearbeitung: Wert übernehmen
if (tblPositionen.isEditing()) tblPositionen.getCellEditor().stopCellEditing();
// Kunden-ID parsen
long kundenId;
try {
kundenId = Long.parseLong(txtKundenIdInput.getText().trim());
} catch (NumberFormatException ex) {
lblFehler.setText("Kunden-ID muss eine Zahl sein.");
return;
}
// Positionen aufbauen
List<PositionCommand> positionen = new ArrayList<>();
for (int r = 0; r < posModel.getRowCount(); r++) {
Object pidObj = posModel.getValueAt(r, 0);
Object mengeObj = posModel.getValueAt(r, 1);
if (pidObj == null || mengeObj == null
|| pidObj.toString().isBlank() || mengeObj.toString().isBlank()) {
continue;
}
try {
long pid = Long.parseLong(pidObj.toString().trim());
int menge = Integer.parseInt(mengeObj.toString().trim());
positionen.add(new PositionCommand(pid, menge));
} catch (NumberFormatException ex) {
lblFehler.setText("Position " + (r + 1)
+ ": Produkt-ID und Menge müssen Zahlen sein.");
return;
} catch (IllegalArgumentException ex) {
lblFehler.setText("Position " + (r + 1) + ": " + ex.getMessage());
return;
}
}
if (positionen.isEmpty()) {
lblFehler.setText("Mindestens eine Position erforderlich.");
return;
}
// Angebot anlegen
try {
Angebot a = dokumentenService.createAngebot(kundenId, positionen);
ladeDokumente();
mainFrame.showStatus("Angebot " + a.getDokumentNummer()
+ " angelegt (Netto " + a.getNettoSumme() + " €).");
dialog.dispose();
} catch (Exception ex) {
lblFehler.setText("Fehler: " + ex.getMessage());
}
});
JPanel southPanel = new JPanel(new BorderLayout());
southPanel.add(lblFehler, BorderLayout.NORTH);
southPanel.add(dlgButtons, BorderLayout.SOUTH);
dialog.add(headPanel, BorderLayout.NORTH);
dialog.add(posPanel, BorderLayout.CENTER);
dialog.add(southPanel, BorderLayout.SOUTH);
dialog.setSize(480, 360);
dialog.setLocationRelativeTo(this);
dialog.setVisible(true);
}
} }

View File

@ -3,46 +3,29 @@ package de.hsmannheim.faktura.gui;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.*; import javax.swing.table.*;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.util.List; import java.util.List;
import de.hsmannheim.faktura.kunde.model.Kunde; import de.hsmannheim.faktura.kunde.Kunde;
import de.hsmannheim.faktura.kunde.repository.KundeRepository; import de.hsmannheim.faktura.kunde.service.KundenService;
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson; import de.hsmannheim.faktura.kunde.repository.KundenRepository;
import de.hsmannheim.faktura.kunde.service.BelegReferenzPruefer; import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.kunde.service.KundeService;
import de.hsmannheim.faktura.kunde.service.KundeValidator;
import de.hsmannheim.faktura.kunde.service.LoeschsperreException;
import de.hsmannheim.faktura.kunde.service.ValidierungsException;
/** /**
* Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05PHGUI09). * Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05PHGUI09).
* * Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
* Verdrahtung gegen die spec-konforme Schnittstelle K1 aus dem
* gemeinsamen Pflichtenheft v1.1 (Kap. 6.1.2 / Kap. 7.2):
* - Klassennamen: {@link KundeService}, {@link KundeRepository}, {@link Kunde}
* - Methoden: anlegen / bearbeiten / suchen / loeschen / finde / alle
* - Typ kundeId: long
*
* Das ursprüngliche GUI-Modell (KundenService / KundenRepository /
* 8-arg-Konstruktor / getKundenId():int) ist eine Erfindung des
* Gruppen-E-Pflichtenhefts und wird hier auf die gemeinsame Spec
* gemappt. Eintrag im Fahrplan: OP-6 (GUI vs. PH v1.1).
*/ */
public class KundenPanel extends JPanel { public class KundenPanel extends JPanel {
private final MainFrame mainFrame; private final MainFrame mainFrame;
private final KundeService kundeService; private final KundenService kundenService;
// Tabelle // Tabelle
private DefaultTableModel tableModel; private DefaultTableModel tableModel;
private JTable kundenTable; private JTable kundenTable;
// Formularfelder // Formularfelder
private JTextField txtKundeId; private JTextField txtKundenId;
private JTextField txtFirmenname; private JTextField txtFirmenname;
private JTextField txtNachname;
private JTextField txtVorname;
private JTextField txtStrasse; private JTextField txtStrasse;
private JTextField txtPlz; private JTextField txtPlz;
private JTextField txtOrt; private JTextField txtOrt;
@ -51,7 +34,7 @@ public class KundenPanel extends JPanel {
private JTextField txtUstIdNr; private JTextField txtUstIdNr;
// Fehlerhinweise (PHGUI03, PHGUI05) // Fehlerhinweise (PHGUI03, PHGUI05)
private JLabel lblNameFehler; private JLabel lblFirmenfehler;
private JLabel lblStrasseFehler; private JLabel lblStrasseFehler;
private JLabel lblPlzFehler; private JLabel lblPlzFehler;
private JLabel lblOrtFehler; private JLabel lblOrtFehler;
@ -68,10 +51,9 @@ public class KundenPanel extends JPanel {
public KundenPanel(MainFrame mainFrame) { public KundenPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame; this.mainFrame = mainFrame;
this.kundeService = new KundeService( this.kundenService = new KundenService(
new KundeRepositoryJson(new File("data/kunden.json")), new KundenRepository(),
new KundeValidator(), createDummyDokumentRepository()
stubBelegPruefer()
); );
setLayout(new BorderLayout(8, 8)); setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@ -79,183 +61,153 @@ public class KundenPanel extends JPanel {
ladeKunden(); ladeKunden();
} }
/**
* GR-05-Löschsperre lokal deaktiviert: das DokumentPanel hat eine
* eigene Repository-Instanz, eine geteilte Belegsicht erfordert ein
* zentrales Wiring (über MainFrame) und ist Bestandteil von OP-6.
*/
private BelegReferenzPruefer stubBelegPruefer() {
return kundeId -> false;
}
private void initComponents() { private void initComponents() {
// --- Linke Seite: Liste + Suche (PHGUI09) --- // --- Linke Seite: Liste + Suche ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4)); JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(320, 0)); leftPanel.setPreferredSize(new Dimension(320, 0));
String[] columns = {"ID", "Name", "Ort"}; String[] columns = {"ID", "Name ▲", "Ort"};
tableModel = new DefaultTableModel(columns, 0) { tableModel = new DefaultTableModel(columns, 0) {
public boolean isCellEditable(int r, int c) { return false; } public boolean isCellEditable(int r, int c) { return false; }
}; };
kundenTable = new JTable(tableModel); kundenTable = new JTable(tableModel);
kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
kundenTable.getTableHeader().setReorderingAllowed(false); // Sortierung (PHGUI08)
kundenTable.setAutoCreateRowSorter(true); // PHGUI08 kundenTable.setAutoCreateRowSorter(true);
kundenTable.getSelectionModel().addListSelectionListener(e -> { kundenTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehlten(); if (!e.getValueIsAdjusting()) zeigeAusgewaehlten();
}); });
JPanel suchPanel = new JPanel(new BorderLayout(4, 0)); leftPanel.add(new JLabel("Kundenliste"), BorderLayout.NORTH);
suchPanel.add(new JLabel("Suche: "), BorderLayout.WEST); leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER);
// Suche (PHGUI09: nach Name oder Kunden-ID)
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche (Name / Kunden-ID)"));
txtSuche = new JTextField(); txtSuche = new JTextField();
txtSuche.setToolTipText("Name oder ID eingeben...");
txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
}); });
suchPanel.add(txtSuche, BorderLayout.CENTER); suchePanel.add(txtSuche, BorderLayout.CENTER);
leftPanel.add(suchePanel, BorderLayout.SOUTH);
leftPanel.add(suchPanel, BorderLayout.NORTH); // --- Rechte Seite: Formular ---
leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER); JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
rightPanel.setBorder(BorderFactory.createTitledBorder("Kundendetails Pflichtfelder sind mit * markiert"));
// --- Rechte Seite: Formular + Buttons (PHGUI02) --- JPanel formPanel = new JPanel(new GridBagLayout());
JPanel rightPanel = new JPanel(new BorderLayout(0, 8)); GridBagConstraints gbc = new GridBagConstraints();
rightPanel.add(formularPanel(), BorderLayout.CENTER); gbc.insets = new Insets(3, 4, 1, 4);
rightPanel.add(buttonPanel(), BorderLayout.SOUTH); gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 0.5;
// Zeile 0: KundenId + Telefon
addLabel(formPanel, gbc, "Kunden-ID (autom.)", 0, 0);
addLabel(formPanel, gbc, "Telefonnummer", 1, 0);
txtKundenId = new JTextField(); txtKundenId.setEditable(false);
txtKundenId.setBackground(new Color(230, 230, 230));
addField(formPanel, gbc, txtKundenId, 0, 1);
txtTelefon = new JTextField();
addField(formPanel, gbc, txtTelefon, 1, 1);
// Zeile 2: Firmenname + E-Mail
addLabel(formPanel, gbc, "Firmenname / Nachname *", 0, 2);
addLabel(formPanel, gbc, "E-Mail", 1, 2);
txtFirmenname = new JTextField();
addField(formPanel, gbc, txtFirmenname, 0, 3);
txtEmail = new JTextField();
addField(formPanel, gbc, txtEmail, 1, 3);
// Fehler Firmenname + E-Mail
lblFirmenfehler = createFehlerLabel();
addField(formPanel, gbc, lblFirmenfehler, 0, 4);
lblEmailFehler = createFehlerLabel();
lblEmailFehler.setText("▲ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120));
addField(formPanel, gbc, lblEmailFehler, 1, 4);
// Zeile 5: Straße + UStIdNr
addLabel(formPanel, gbc, "Straße *", 0, 5);
addLabel(formPanel, gbc, "UStIdNr.", 1, 5);
txtStrasse = new JTextField();
addField(formPanel, gbc, txtStrasse, 0, 6);
txtUstIdNr = new JTextField();
addField(formPanel, gbc, txtUstIdNr, 1, 6);
lblStrasseFehler = createFehlerLabel();
addField(formPanel, gbc, lblStrasseFehler, 0, 7);
// Zeile 8: PLZ
addLabel(formPanel, gbc, "PLZ *", 0, 8);
txtPlz = new JTextField();
addField(formPanel, gbc, txtPlz, 0, 9);
lblPlzFehler = createFehlerLabel();
addField(formPanel, gbc, lblPlzFehler, 0, 10);
// Zeile 11: Ort
addLabel(formPanel, gbc, "Ort *", 0, 11);
txtOrt = new JTextField();
addField(formPanel, gbc, txtOrt, 0, 12);
lblOrtFehler = createFehlerLabel();
addField(formPanel, gbc, lblOrtFehler, 0, 13);
rightPanel.add(new JScrollPane(formPanel), BorderLayout.CENTER);
// --- Buttons (PHGUI02) ---
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
btnNeu = new JButton("Neu");
btnSpeichern = new JButton("Speichern");
btnLoeschen = new JButton("Löschen");
btnAbbrechen = new JButton("Abbrechen");
btnPanel.add(btnNeu);
btnPanel.add(btnSpeichern);
btnPanel.add(btnLoeschen);
btnPanel.add(btnAbbrechen);
rightPanel.add(btnPanel, BorderLayout.SOUTH);
btnNeu.addActionListener(e -> neuesFormular());
btnSpeichern.addActionListener(e -> speichern());
btnLoeschen.addActionListener(e -> loeschen());
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeKunden(); });
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
split.setDividerLocation(320); split.setDividerLocation(320);
add(split, BorderLayout.CENTER); add(split, BorderLayout.CENTER);
} }
private JPanel formularPanel() { // --- Hilfsmethoden Layout ---
JPanel p = new JPanel(new GridBagLayout()); private void addLabel(JPanel p, GridBagConstraints g, String text, int col, int row) {
p.setBorder(BorderFactory.createTitledBorder("Kundendaten")); g.gridx = col; g.gridy = row; g.weighty = 0;
GridBagConstraints g = new GridBagConstraints(); p.add(new JLabel(text), g);
g.insets = new Insets(4, 6, 2, 6);
g.anchor = GridBagConstraints.WEST;
g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 1.0;
txtKundeId = createReadOnlyField();
txtFirmenname = new JTextField();
txtNachname = new JTextField();
txtVorname = new JTextField();
txtStrasse = new JTextField();
txtPlz = new JTextField();
txtOrt = new JTextField();
txtTelefon = new JTextField();
txtEmail = new JTextField();
txtUstIdNr = new JTextField();
lblNameFehler = createFehlerLabel();
lblStrasseFehler = createFehlerLabel();
lblPlzFehler = createFehlerLabel();
lblOrtFehler = createFehlerLabel();
lblEmailFehler = createFehlerLabel();
lblEmailFehler.setText("ⓘ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120));
int row = 0;
row = addLabelField(p, g, "Kunden-ID:", txtKundeId, null, row);
row = addLabelField(p, g, "Firmenname *:", txtFirmenname, lblNameFehler, row);
row = addLabelField(p, g, "Nachname *:", txtNachname, null, row);
row = addLabelField(p, g, "Vorname:", txtVorname, null, row);
row = addLabelField(p, g, "Straße + Nr. *:", txtStrasse, lblStrasseFehler, row);
row = addLabelField(p, g, "PLZ *:", txtPlz, lblPlzFehler, row);
row = addLabelField(p, g, "Ort *:", txtOrt, lblOrtFehler, row);
row = addLabelField(p, g, "Telefon:", txtTelefon, null, row);
row = addLabelField(p, g, "E-Mail:", txtEmail, lblEmailFehler, row);
row = addLabelField(p, g, "USt-IdNr.:", txtUstIdNr, null, row);
// Fülldummy, damit obere Felder oben bleiben
g.gridx = 0; g.gridy = row; g.weighty = 1.0;
p.add(new JLabel(" "), g);
JLabel hint = new JLabel("* Pflichtfeld Firmenname ODER Nachname ist Pflicht (PH v1.1 Kap. 6.1.2)");
hint.setFont(new Font("SansSerif", Font.ITALIC, 11));
hint.setForeground(new Color(120, 120, 120));
JPanel wrap = new JPanel(new BorderLayout());
wrap.add(p, BorderLayout.CENTER);
wrap.add(hint, BorderLayout.SOUTH);
return wrap;
} }
private void addField(JPanel p, GridBagConstraints g, JComponent c, int col, int row) {
private int addLabelField(JPanel p, GridBagConstraints g, g.gridx = col; g.gridy = row; g.weighty = 0;
String label, JComponent field, JLabel fehlerLabel, int row) { p.add(c, g);
g.weighty = 0;
g.gridx = 0; g.gridy = row; g.weightx = 0; p.add(new JLabel(label), g);
g.gridx = 1; g.gridy = row; g.weightx = 1.0; p.add(field, g);
row++;
if (fehlerLabel != null) {
g.gridx = 1; g.gridy = row; g.weightx = 1.0; p.add(fehlerLabel, g);
row++;
}
return row;
}
private JPanel buttonPanel() {
JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnNeu = new JButton("Neu");
btnSpeichern = new JButton("Speichern");
btnLoeschen = new JButton("Löschen");
btnAbbrechen = new JButton("Abbrechen");
btnNeu.addActionListener(e -> neuesFormular());
btnSpeichern.addActionListener(e -> speichern());
btnLoeschen.addActionListener(e -> loeschen());
btnAbbrechen.addActionListener(e -> { neuesFormular(); kundenTable.clearSelection(); });
p.add(btnNeu); p.add(btnSpeichern); p.add(btnLoeschen); p.add(btnAbbrechen);
return p;
} }
// --- Daten --- // --- Daten ---
private void ladeKunden() { private void ladeKunden() {
ladeKundenListe(kundeService.alle()); ladeKundenListe(kundenService.getAllKunden());
} }
private void ladeKundenListe(List<Kunde> liste) { private void ladeKundenListe(List<Kunde> liste) {
tableModel.setRowCount(0); tableModel.setRowCount(0);
for (Kunde k : liste) { for (Kunde k : liste) {
tableModel.addRow(new Object[]{ tableModel.addRow(new Object[]{
Long.toString(k.getKundeId()), String.valueOf(k.getKundenId()),
nameVon(k), k.getFirmennameNachname(),
nullAls(k.getOrt()) k.getOrt()
}); });
} }
} }
/**
* Anzeigename gemäß PH v1.1 Kap. 6.1.2: firmenname ODER nachname ist
* Pflicht. Liefert firmenname, sonst nachname[, vorname].
*/
private static String nameVon(Kunde k) {
if (k.getFirmenname() != null && !k.getFirmenname().isBlank()) {
return k.getFirmenname();
}
String n = nullAls(k.getNachname());
if (k.getVorname() != null && !k.getVorname().isBlank()) {
return n + ", " + k.getVorname();
}
return n;
}
private static String nullAls(String s) { return s == null ? "" : s; }
private void sucheAktualisieren() { private void sucheAktualisieren() {
// PHGUI09: Treffer über Name UND Kundennummer. // PHGUI09: Groß-/Kleinschreibung ignorieren, nach Name oder ID suchen
// KundeService.suchen("") liefert bewusst eine leere Liste (P4-3),
// deshalb hier explizit auf .alle() umschalten.
String query = txtSuche.getText(); String query = txtSuche.getText();
if (query == null || query.isBlank()) { ladeKundenListe(kundenService.searchKunden(query));
ladeKundenListe(kundeService.alle());
} else {
ladeKundenListe(kundeService.suchen(query));
}
} }
private void zeigeAusgewaehlten() { private void zeigeAusgewaehlten() {
@ -263,144 +215,118 @@ public class KundenPanel extends JPanel {
if (row < 0) return; if (row < 0) return;
int modelRow = kundenTable.convertRowIndexToModel(row); int modelRow = kundenTable.convertRowIndexToModel(row);
String idStr = (String) tableModel.getValueAt(modelRow, 0); String idStr = (String) tableModel.getValueAt(modelRow, 0);
try { kundenService.getKundeById(Integer.parseInt(idStr)).ifPresent(k -> {
long kundeId = Long.parseLong(idStr); txtKundenId.setText(String.valueOf(k.getKundenId()));
kundeService.finde(kundeId).ifPresent(this::fuelleFormular); txtFirmenname.setText(k.getFirmennameNachname());
} catch (NumberFormatException ignored) { txtStrasse.setText(k.getStrasse());
// Tabelle in inkonsistentem Zustand; einfach ignorieren txtPlz.setText(k.getPlz());
} txtOrt.setText(k.getOrt());
} txtTelefon.setText(k.getTelefonnummer());
txtEmail.setText(k.getEmail());
private void fuelleFormular(Kunde k) { txtUstIdNr.setText(k.getUstIdNr());
txtKundeId.setText(Long.toString(k.getKundeId()));
txtFirmenname.setText(nullAls(k.getFirmenname()));
txtNachname.setText(nullAls(k.getNachname()));
txtVorname.setText(nullAls(k.getVorname()));
txtStrasse.setText(nullAls(k.getStrasse()));
txtPlz.setText(nullAls(k.getPlz()));
txtOrt.setText(nullAls(k.getOrt()));
txtTelefon.setText(nullAls(k.getTelefon()));
txtEmail.setText(nullAls(k.getEmail()));
txtUstIdNr.setText(nullAls(k.getUstIdNr()));
clearFehler(); clearFehler();
});
} }
// --- Aktionen --- // --- Aktionen ---
private void neuesFormular() { private void neuesFormular() {
txtKundeId.setText(""); txtKundenId.setText(""); txtFirmenname.setText(""); txtStrasse.setText("");
txtFirmenname.setText(""); txtNachname.setText(""); txtVorname.setText(""); txtPlz.setText(""); txtOrt.setText(""); txtTelefon.setText("");
txtStrasse.setText(""); txtPlz.setText(""); txtOrt.setText(""); txtEmail.setText(""); txtUstIdNr.setText("");
txtTelefon.setText(""); txtEmail.setText(""); txtUstIdNr.setText("");
clearFehler(); clearFehler();
kundenTable.clearSelection(); kundenTable.clearSelection();
} }
private void speichern() { private void speichern() {
clearFehler(); clearFehler();
boolean valid = true;
// GUI-seitige Vorprüfung der Pflichtfelder (PHGUI03) - die fachliche // PHGUI03: Pflichtfelder
// Validierung gegen PH v1.1 macht der KundeValidator beim Service-Aufruf. if (txtFirmenname.getText().trim().isEmpty()) {
boolean clientValid = true; lblFirmenfehler.setText("⚠ Pflichtfeld darf nicht leer sein");
if (istLeer(txtFirmenname) && istLeer(txtNachname)) { valid = false;
lblNameFehler.setText("⚠ Firmenname ODER Nachname erforderlich"); }
clientValid = false; if (txtStrasse.getText().trim().isEmpty()) {
lblStrasseFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
}
if (txtPlz.getText().trim().isEmpty()) {
lblPlzFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
}
if (txtOrt.getText().trim().isEmpty()) {
lblOrtFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
} }
if (istLeer(txtStrasse)) { lblStrasseFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
if (istLeer(txtPlz)) { lblPlzFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
if (istLeer(txtOrt)) { lblOrtFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
if (!clientValid) return;
// Kunde-Objekt aufbauen (no-arg-Konstruktor + Setter; Jackson-kompatibel) // PHGUI05: E-Mail-Validierung (Format: name@domain.tld)
Kunde k = new Kunde(); String email = txtEmail.getText().trim();
if (!txtKundeId.getText().isBlank()) { if (!email.isEmpty() && !email.matches("^[^@]+@[^@]+\\.[^@]+$")) {
try { k.setKundeId(Long.parseLong(txtKundeId.getText().trim())); } lblEmailFehler.setText("⚠ Format: name@domain.tld");
catch (NumberFormatException ignored) { /* bleibt 0 = Neuanlage */ } lblEmailFehler.setForeground(new Color(192, 57, 43));
valid = false;
} }
k.setFirmenname(leerAlsNull(txtFirmenname));
k.setNachname(leerAlsNull(txtNachname)); if (!valid) return;
k.setVorname(leerAlsNull(txtVorname));
k.setStrasse(leerAlsNull(txtStrasse)); int kundenId = txtKundenId.getText().trim().isEmpty() ? 0
k.setPlz(leerAlsNull(txtPlz)); : Integer.parseInt(txtKundenId.getText().trim());
k.setOrt(leerAlsNull(txtOrt));
k.setTelefon(leerAlsNull(txtTelefon)); Kunde k = new Kunde(
k.setEmail(leerAlsNull(txtEmail)); kundenId,
k.setUstIdNr(leerAlsNull(txtUstIdNr)); txtFirmenname.getText().trim(),
txtStrasse.getText().trim(),
txtPlz.getText().trim(),
txtOrt.getText().trim(),
txtTelefon.getText().trim(),
email,
txtUstIdNr.getText().trim()
);
try { try {
if (k.getKundeId() == 0) { kundenService.saveKunde(k);
long neueId = kundeService.anlegen(k); // F-KV-01
k.setKundeId(neueId);
} else {
kundeService.bearbeiten(k); // F-KV-02
}
ladeKunden(); ladeKunden();
mainFrame.showStatus("Kunde gespeichert (ID " + k.getKundeId() + ")."); // PHGUI06
neuesFormular(); neuesFormular();
} catch (ValidierungsException ex) { // PHGUI06: Bestätigung < 1 Sekunde
// Fehler aus KundeValidator (PH v1.1 Kap. 6.1.2) sichtbar machen mainFrame.showStatus("Kunde erfolgreich gespeichert.");
String msg = ex.getMessage(); } catch (IllegalArgumentException ex) {
if (msg.toLowerCase().contains("email") || msg.toLowerCase().contains("e-mail")) { mainFrame.showStatus("Fehler: " + ex.getMessage());
lblEmailFehler.setText("⚠ " + msg);
lblEmailFehler.setForeground(new Color(192, 57, 43));
} else if (msg.toLowerCase().contains("firmen") || msg.toLowerCase().contains("nach")) {
lblNameFehler.setText("⚠ " + msg);
} else if (msg.toLowerCase().contains("plz")) {
lblPlzFehler.setText("⚠ " + msg);
} else if (msg.toLowerCase().contains("straße") || msg.toLowerCase().contains("strasse")) {
lblStrasseFehler.setText("⚠ " + msg);
} else if (msg.toLowerCase().contains("ort")) {
lblOrtFehler.setText("⚠ " + msg);
} else {
mainFrame.showStatus("Validierung: " + msg);
}
} }
} }
private void loeschen() { private void loeschen() {
int row = kundenTable.getSelectedRow(); int row = kundenTable.getSelectedRow();
if (row < 0) { mainFrame.showStatus("Kein Kunde ausgewählt."); return; } if (row < 0) {
mainFrame.showStatus("Kein Kunde ausgewählt.");
return;
}
// PHGUI07: Bestätigungsdialog // PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog( int confirm = JOptionPane.showConfirmDialog(
this, "Kunden wirklich löschen?", "Löschen bestätigen", this, "Kunden wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION); JOptionPane.YES_NO_OPTION
);
if (confirm != JOptionPane.YES_OPTION) return; if (confirm != JOptionPane.YES_OPTION) return;
int modelRow = kundenTable.convertRowIndexToModel(row); int modelRow = kundenTable.convertRowIndexToModel(row);
String idStr = (String) tableModel.getValueAt(modelRow, 0); String idStr = (String) tableModel.getValueAt(modelRow, 0);
try { boolean deleted = kundenService.deleteKunde(Integer.parseInt(idStr));
long kundeId = Long.parseLong(idStr);
kundeService.loeschen(kundeId); // F-KV-04 if (deleted) {
ladeKunden(); ladeKunden();
neuesFormular(); neuesFormular();
mainFrame.showStatus("Kunde gelöscht."); mainFrame.showStatus("Kunde gelöscht.");
} catch (LoeschsperreException ex) { } else {
mainFrame.showStatus("Löschen nicht möglich " + ex.getMessage()); // GR-05 mainFrame.showStatus("Löschen nicht möglich Kunde wird in Dokumenten verwendet.");
} catch (ValidierungsException ex) {
mainFrame.showStatus("Fehler: " + ex.getMessage());
} catch (NumberFormatException ex) {
mainFrame.showStatus("Ungültige Kunden-ID.");
} }
} }
// --- Hilfsmethoden ---
private boolean istLeer(JTextField f) {
return f.getText() == null || f.getText().trim().isEmpty();
}
private String leerAlsNull(JTextField f) {
String s = f.getText() == null ? null : f.getText().trim();
return (s == null || s.isEmpty()) ? null : s;
}
private void clearFehler() { private void clearFehler() {
lblNameFehler.setText(""); lblFirmenfehler.setText("");
lblStrasseFehler.setText(""); lblStrasseFehler.setText("");
lblPlzFehler.setText(""); lblPlzFehler.setText("");
lblOrtFehler.setText(""); lblOrtFehler.setText("");
lblEmailFehler.setText(" Format: name@domain.tld"); lblEmailFehler.setText("▲ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120)); lblEmailFehler.setForeground(new Color(120, 120, 120));
} }
@ -411,10 +337,15 @@ public class KundenPanel extends JPanel {
return lbl; return lbl;
} }
private JTextField createReadOnlyField() { private DokumentRepository createDummyDokumentRepository() {
JTextField f = new JTextField(); return new DokumentRepository() {
f.setEditable(false); public boolean existsReferencedProdukt(long id) { return false; }
f.setBackground(new Color(230, 230, 230)); public boolean existsReferencedKunde(long id) { return false; }
return f; public int findMaxLaufendeNummer(String p, int j) { return 0; }
public void save(de.hsmannheim.faktura.dokumente.domain.Dokument d) {}
public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); }
public boolean existsFolgedokument(String q) { return false; }
};
} }
} }

View File

@ -337,8 +337,6 @@ public class ProduktPanel extends JPanel {
public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); } public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); } public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); }
public boolean existsFolgedokument(String q) { return false; } public boolean existsFolgedokument(String q) { return false; }
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findAll() { return List.of(); }
public boolean delete(String n) { return false; }
}; };
} }
} }

View File

@ -9,9 +9,6 @@ import java.math.BigDecimal;
*/ */
public class Product { public class Product {
// Ein globaler Zähler, der für jedes neue Produkt hochgezählt wird
private static int idCounter = 1;
private String productId; private String productId;
private String bezeichnung; private String bezeichnung;
private BigDecimal einzelpreisNetto; private BigDecimal einzelpreisNetto;
@ -21,33 +18,24 @@ public class Product {
/** /**
* Standardkonstruktor. * Standardkonstruktor.
* Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt. * Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt[cite: 71].
* Generiert automatisch die nächste ID im Format P-0001.
*/ */
public Product() { public Product() {
// String.format sorgt dafür, dass die Zahl immer 4 Stellen hat (mit führenden Nullen)
this.productId = String.format("P-%04d", idCounter++);
} }
/** /**
* All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts. * All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts[cite: 55].
* *
* @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001) * @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001) [cite: 55, 392]
* @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen) * @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen) [cite: 55, 456]
* @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein) * @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein) [cite: 55]
* @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT) * @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT) [cite: 55]
* @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen) * @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen) [cite: 55]
* @param kategorie Optionale Warengruppe oder hierarchische Zuordnung * @param kategorie Optionale Warengruppe oder hierarchische Zuordnung [cite: 55]
*/ */
public Product(String productId, String bezeichnung, BigDecimal einzelpreisNetto, public Product(String productId, String bezeichnung, BigDecimal einzelpreisNetto,
MwstSatz mwstSatz, String beschreibung, String kategorie) { MwstSatz mwstSatz, String beschreibung, String kategorie) {
// Falls keine ID übergeben wird (null oder leer), generieren wir die fortlaufende ID
if (productId == null || productId.trim().isEmpty()) {
this.productId = String.format("P-%04d", idCounter++);
} else {
this.productId = productId; this.productId = productId;
}
this.bezeichnung = bezeichnung; this.bezeichnung = bezeichnung;
this.einzelpreisNetto = einzelpreisNetto; this.einzelpreisNetto = einzelpreisNetto;
this.mwstSatz = mwstSatz; this.mwstSatz = mwstSatz;

View File

@ -1,60 +0,0 @@
package de.hsmannheim.faktura.product;
public class ProduktverwaltungDemo {
public static void main(String[] args) {
System.out.println("==================================================");
System.out.println(" DEMO: MODUL PRODUKTVERWALTUNG (GRUPPE G) ");
System.out.println("==================================================\n");
// 1. System initialisieren (Beispielhaftes Setup)
System.out.println("[Schritt 1] Initialisiere ProductService und Repository...");
// Hier simulieren wir die Erstellung deines Services
// ProductRepository repository = new ProductRepository();
// ProductService productService = new ProductService(repository);
System.out.println("=> System ist betriebsbereit (JSON-Dateianbindung aktiv).\n");
System.out.println("--------------------------------------------------");
// 2. Produkt erfolgreich anlegen (F-SH-PV-01)
System.out.println("[Schritt 2] Lege valides Produkt an (Anforderung F-SH-PV-01)...");
System.out.println("-> Erstelle: 'Schraube M6', Preis: 0.15 EUR, MwSt: 19%");
System.out.println("=> ERFOLG: Produkt erfolgreich gespeichert und ID vergeben.\n");
System.out.println("--------------------------------------------------");
// 3. Preisvalidierung live zeigen (F-SH-PV-06 / MT-PV-04 & MT-PV-05)
System.out.println("[Schritt 3] Teste Preisvalidierungen (Anforderung F-SH-PV-06)...");
// Fall A: Negativer Preis
System.out.println("-> Versuch: Produkt mit NEGATIVEM Preis (-2.50 EUR) anlegen...");
System.out.println("=> ABGEWIESEN: java.lang.IllegalArgumentException: Preis darf nicht negativ sein!");
System.out.println(" (Robuster Schutz der Geschaeftslogik greift!)");
// Fall B: Exakt Null Euro
System.out.println("-> Versuch: Produkt mit EXAKT 0.00 EUR (Gratis-Beilage) anlegen...");
System.out.println("=> ERFOLG: Kostenlose Testartikel werden korrekt akzeptiert.\n");
System.out.println("--------------------------------------------------");
// 4. Fehlertolerante Suche zeigen (F-SH-PV-05 / MT-PV-08)
System.out.println("[Schritt 4] Zeige fehlertolerante Suche (Anforderung F-SH-PV-05)...");
System.out.println("-> Suche im System nach dem Begriff: 'schraube' (KLEINGESCHRIEBEN)");
System.out.println("=> GEFUNDEN: 2 Treffer unabhaengig von Gross-/Kleinschreibung:");
System.out.println(" - [ID: 20001] Schraube M6 (0.15 EUR)");
System.out.println(" - [ID: 20002] SCHRAUBE M8 (0.45 EUR)\n");
System.out.println("--------------------------------------------------");
// 5. Stammdatenschutz / Löschsperre (F-SH-PV-03 / GR-05 / MT-PV-11)
System.out.println("[Schritt 5] Demonstriere Loeschsperre (Geschaeftsregel GR-05)...");
System.out.println("-> Szenario: Produkt 'Schraube M6' wird in aktivem Angebot #4 verwendet.");
System.out.println("-> Versuch: Loeschen dieses referenzierten Produkts...");
System.out.println("=> SPERRE AKTIV: ProductDeleteException: Produkt kann nicht geloescht werden!");
System.out.println(" (Verhindert erfolgreich Dateninkonsistenz im Dokumentenprozess!)\n");
System.out.println("==================================================");
System.out.println(" DEMO ERFOLGREICH BEENDET - ALLE TESTS PASSED ");
System.out.println("==================================================");
}
}

View File

@ -34,8 +34,6 @@ public class ProductPerformanceTest {
@Override public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); } @Override public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
@Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return java.util.List.of(); } @Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return java.util.List.of(); }
@Override public boolean existsFolgedokument(String q) { return false; } @Override public boolean existsFolgedokument(String q) { return false; }
@Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findAll() { return java.util.List.of(); }
@Override public boolean delete(String n) { return false; }
}; };
ProductService service = new ProductService(repo, dummyDocRepo); ProductService service = new ProductService(repo, dummyDocRepo);

View File

@ -68,16 +68,6 @@ public class ProductServiceTest {
public boolean existsFolgedokument(String quellDokumentNummer) { public boolean existsFolgedokument(String quellDokumentNummer) {
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'"); throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
} }
@Override
public List<Dokument> findAll() {
throw new UnsupportedOperationException("Unimplemented method 'findAll'");
}
@Override
public boolean delete(String nummer) {
throw new UnsupportedOperationException("Unimplemented method 'delete'");
}
}; };
productService = new ProductService(productRepository, dokumentRepository); productService = new ProductService(productRepository, dokumentRepository);
@ -215,14 +205,6 @@ public class ProductServiceTest {
public boolean existsFolgedokument(String quellDokumentNummer) { public boolean existsFolgedokument(String quellDokumentNummer) {
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'"); throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
} }
@Override
public List<Dokument> findAll() {
throw new UnsupportedOperationException("Unimplemented method 'findAll'");
}
@Override
public boolean delete(String nummer) {
throw new UnsupportedOperationException("Unimplemented method 'delete'");
}
}; };
ProductService serviceWithSperre = new ProductService(productRepository, mockRepo); ProductService serviceWithSperre = new ProductService(productRepository, mockRepo);