fix: MT-DP-06 Merge-Konflikt geloest - Duplikat-Check vor Status-Check
commit
54c1a482b4
|
|
@ -0,0 +1,171 @@
|
|||
# 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. |
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
---
|
||||
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. |
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,183 @@
|
|||
git add .---
|
||||
title: "Pflichtenheft – Fakturierungssystem (Modul: Produktverwaltung)"
|
||||
author: "Gruppe G (Produktverwaltung)"
|
||||
date: "12.06.2026"
|
||||
version: "1.0"
|
||||
header-includes:
|
||||
- \usepackage{fancyhdr}
|
||||
- \pagestyle{fancy}
|
||||
- \fancyfoot[CO,CE]{Hochschule Mannheim - SE1 Team 2}
|
||||
---
|
||||
|
||||
# Freigabeübersicht
|
||||
|
||||
| Ersteller | Prüfer | Freigebender |
|
||||
| :--- | :--- | :--- |
|
||||
| Gruppe G (Entwicklungsteam) | Prof. Dr. Gerd Marmitt | SE1 Team 2 (Gruppenleiter) |
|
||||
| 12.06.2026 | 15.06.2026 | 15.06.2026 |
|
||||
|
||||
---
|
||||
|
||||
# Dokumentenhistorie
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 1.0 | 12.06.2026 | Gruppe G | Initiale Erstellung des Pflichtenhefts für das Modul Produktverwaltung auf Basis des Lastenhefts v1.0_alpha. |
|
||||
|
||||
---
|
||||
|
||||
# 1. Einleitung und Geltungsbereich
|
||||
|
||||
Dieses Dokument spezifiziert das Pflichtenheft für das Teilmodul **Produktverwaltung (Gruppe G)** des lokalen Fakturierungssystems. Während das Lastenheft die fachlichen Anforderungen aus Sicht des Auftraggebers beschreibt („Was“), definiert dieses Pflichtenheft die konkrete technische Umsetzung („Wie“) für die Entwicklungs- und Testphase im V-Modell.
|
||||
|
||||
Der Geltungsbereich (Scope) beschränkt sich auf die Verwaltung der Produktstammdaten (Anlegen, Bearbeiten, Löschen, Suchen, Listenansicht) sowie die Bereitstellung der entsprechenden Datenschnittstellen für den Dokumentenprozess (Gruppe F) und die Programmoberfläche (Gruppe E).
|
||||
|
||||
---
|
||||
|
||||
# 2. Systemüberblick und Kontext
|
||||
|
||||
Das Modul Produktverwaltung wird als integraler Bestandteil einer lokalen Java-Einbenutzer-Anwendung realisiert. Es läuft ohne externe Netzanbindungen oder Datenbankserver und speichert die Daten persistent im lokalen Dateisystem.
|
||||
|
||||
## 2.1 Typischer Kontrollfluss (Aktivitätsdiagramm)
|
||||
|
||||
1. **Aufruf:** Der Anwender öffnet die Produktverwaltung über die Hauptnavigation.
|
||||
2. **Laden:** Das System liest alle Einträge aus der lokalen Datei `products.json`.
|
||||
3. **Interaktion:** Der Anwender kann neue Produkte erfassen, bestehende ändern oder unreferenzierte Einträge entfernen.
|
||||
|
||||
---
|
||||
|
||||
# 3. Stakeholder und Benutzerrollen
|
||||
|
||||
Die Stakeholder und Benutzerrollen werden direkt aus dem Lastenheft übernommen:
|
||||
* **S1 (Auftraggeber):** Prof. Dr. Gerd Marmitt (TH Mannheim).
|
||||
* **S2 (Auftragnehmer):** SE1 Team 2, speziell Gruppe G (Produktverwaltung).
|
||||
* **Anwender:** Der einzige Nutzer der lokalen Anwendung (Stammdatenpflege, Belege erzeugen).
|
||||
|
||||
---
|
||||
|
||||
# 4. Funktionale Anforderungen
|
||||
|
||||
Alle funktionalen Systemanforderungen (F-SH-) sind direkt aus den Benutzeranforderungen (BA-) und funktionalen Lastenheft-Anforderungen (F-PV-) abgeleitet und nach den formalen Satzschablonen definiert.
|
||||
|
||||
* **F-SH-PV-01 (Abgeleitet von F-PV-01):** Das System MUSS ein Datenformular bereitstellen, das es dem Anwender ERMÖGLICHEN, ein neues Produkt mit den Attributen `produktId`, `bezeichnung`, `einzelpreisNetto` und `mwstSatz` anzulegen.
|
||||
* **F-SH-PV-02 (Abgeleitet von F-PV-02):** Das System MUSS es dem Anwender ERMÖGLICHEN, einen bestehenden Produktdatensatz über die GUI auszuwählen, die Werte von `bezeichnung`, `einzelpreisNetto`, `mwstSatz`, `beschreibung` und `kategorie` zu modifizieren und diese permanent zu überschreiben.
|
||||
* **F-SH-PV-03 (Abgeleitet von F-PV-03 / GR-05):** Das System MUSS vor dem Löschen eines Produktdatensatzes prüfen, ob die `produktId` in den Dokumenten des Dokumentenprozesses vorhanden ist. Wenn eine Referenz existiert, MUSS das System den Löschvorgang blockieren und einen Hinweis anzeigen.
|
||||
* **F-SH-PV-04 (Abgeleitet von F-PV-04):** Das System MUSS beim Initialisieren des Moduls eine tabellarische Übersichtsliste aller gespeicherten Produkte generieren, welche die Spalten Produkt-ID, Bezeichnung und Einzelpreis anzeigt.
|
||||
* **F-SH-PV-05 (Abgeleitet von BA-PV-05):** Das System MUSS eine Filterung der Produktliste bereitstellen, sobald der Anwender ein Zeichen in das Suchfeld eingibt, und alle Produkte anzeigen, bei denen der Suchbegriff eine Teilmenge der `bezeichnung` (Groß-/Kleinschreibung ignorierend) bildet.
|
||||
* **F-SH-PV-06 (Abgeleitet von F-PV-06):** Das System MUSS die Eingabe im Feld `einzelpreisNetto` blockieren und eine sichtbare Fehlermeldung anzeigen, falls der eingegebene Wert numerisch kleiner als 0.00 ist.
|
||||
|
||||
---
|
||||
|
||||
# 5. Nicht-funktionale Anforderungen (Qualitätsanforderungen)
|
||||
|
||||
## 5.1 Usability (NF-USE)
|
||||
* **NF-SH-USE-01 (Abgeleitet von NF-USE-03 / F-GUI-02):** Das System MUSS visuelle Fehlermeldungen bei einer Falscheingabe direkt am betroffenen Feld innerhalb von maximal 1 Sekunde anzeigen.
|
||||
|
||||
## 5.2 Performance (NF-PERF)
|
||||
* **NF-SH-PERF-01 (Abgeleitet von NF-PERF-01 / NF-PERF-02):** Das System MUSS das Laden und Filtern der Produktliste bei einer Datenmenge von bis zu 1.000 Produktdatensätzen in weniger als 2 Sekunden vollständig abschließen.
|
||||
|
||||
## 5.3 Wartbarkeit und Architektur (NF-MAINT / NF-ARCH)
|
||||
* **NF-SH-ARCH-01 (Abgeleitet von NF-ARCH-01):** Das System MUSS die Produktdaten persistent so speichern, dass sie nach einem Neustart der Anwendung verlustfrei wiederhergestellt werden.
|
||||
* **NF-SH-MAINT-01 (Abgeleitet von NF-MAINT-01 / NF-MAINT-03):** Der Quellcode des Moduls MUSS vollständig im Paket `de.hs_mannheim.sel.fakturierung.produktverwaltung` gekapselt sein und einer strikten dreischichtigen Architektur folgen.
|
||||
|
||||
---
|
||||
|
||||
# 6. Daten und Schnittstellen
|
||||
|
||||
## 6.1 Datenobjekt Beschreibung (Klasse `Product`)
|
||||
|
||||
| Attributname | Technischer Datentyp (Java) | Pflichtfeld | Validierung / Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `produktId` | `java.lang.String` | Ja | Eindeutiger Identifikator. Format: `P-` + fortlaufende Nummer (z. B. `P-0001`). |
|
||||
| `bezeichnung` | `java.lang.String` | Ja | Alphanumerischer String, min. 1 Zeichen, max. 100 Zeichen. |
|
||||
| `einzelpreisNetto` | `java.math.BigDecimal` | Ja | Skalierung auf exakt 2 Nachkommastellen. Wert muss $\ge 0.00$ sein (Verwendung von `BigDecimal` zur Vermeidung von Rundungsfehlern). |
|
||||
| `mwstSatz` | `enum MwstSatz` | Ja | Festgelegte Auswahlwerte über eine Enumeration: `NORMAL` (19%) oder `ERMAESSIGT` (7%). |
|
||||
| `beschreibung` | `java.lang.String` | Nein | Optionaler Freitext, maximal 500 Zeichen. |
|
||||
| `kategorie` | `java.lang.String` | Nein | Optionale Gruppierung der Produkte. |
|
||||
|
||||
## 6.2 Schnittstellen (APIs und Persistenz)
|
||||
* **Programminterne API (`ProductService`):** Bietet Methoden wie `getAllProducts()`, `saveProduct(Product p)` und `deleteProduct(String produktId)` an, um Daten für die GUI (Gruppe E) oder Belege (Gruppe F) bereitzustellen.
|
||||
* **Datei-Schnittstelle:** Die Daten werden im JSON-Format lokal in der Datei `./data/products.json` persistiert.
|
||||
|
||||
---
|
||||
|
||||
# 7. Systemarchitektur
|
||||
|
||||
Das Modul folgt einer klassischen Schichtenarchitektur (Präsentation -> Logik -> Datenhaltung). Die Datenhaltung wird hinter einer abstrakten Schnittstelle gekapselt, um die Austauschbarkeit der Speichertechnologie zu gewährleisten.
|
||||
|
||||
## 7.1 Struktur (UML-Klassendiagramm)
|
||||
|
||||
Die statische Systemstruktur des Moduls Produktverwaltung ist in Abbildung 1 dargestellt. Das Diagramm zeigt die dreischichtige Architektur der Anwendung. Der `ProductController` bildet die Präsentationsschicht (gekoppelt mit Gruppe E) und leitet Interaktionen an den `ProductService` (Geschäftslogik) weiter. Die datenbanklose Persistenz wird über das `ProductRepository` gekapselt, welches die `Product`-Entitäten direkt verwaltet und in JSON-Dateien speichert.
|
||||
|
||||
### Abbildung 1: UML-Klassendiagramm der Produktverwaltung
|
||||
|
||||

|
||||
|
||||
## 7.2 Verhalten (UML-Sequenzdiagramm)
|
||||
|
||||
In Abbildung 2 wird der dynamische Ablauf einer Produktlöschung inklusive der systemübergreifenden Referenzprüfung nach der Geschäftsregel `GR-05` (Stammdaten-Schutz) visualisiert.
|
||||
|
||||
Wenn der Anwender in der Benutzeroberfläche ein Produkt löschen möchte, fängt der `ProductController` das Event ab und leitet die ID an den `ProductService` weiter. Dieser fragt vorab den `DocumentService` (Gruppe F) ab, ob das Produkt in aktiven Belegen referenziert wird. Liegt keine Referenz vor, wird das Produkt über das `ProductRepository` aus dem JSON-Speicher entfernt.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
title Abbildung 2: UML-Sequenzdiagramm für den Löschvorgang eines Produkts
|
||||
actor Anwender
|
||||
participant GUI as ProductController (Gruppe E)
|
||||
participant Service as ProductService (Gruppe G)
|
||||
participant DocRef as DocumentService (Gruppe F)
|
||||
participant Repo as ProductRepository (Gruppe G)
|
||||
|
||||
Anwender->>GUI: Klickt auf "Löschen" für Produkt ID "P-0001"
|
||||
GUI->>Service: deleteProduct("P-0001")
|
||||
activate Service
|
||||
|
||||
Service->>DocRef: isProductReferenced("P-0001")
|
||||
DocRef-->>Service: Rückgabe: false (Keine aktiven Belege)
|
||||
|
||||
Service->>Repo: remove("P-0001")
|
||||
activate Repo
|
||||
Repo->>Repo: Aus Cache entfernen & JSON neu schreiben
|
||||
Repo-->>Service: Bestätigung (void)
|
||||
deactivate Repo
|
||||
|
||||
Service-->>GUI: Rückgabe: true (Erfolgreich gelöscht)
|
||||
deactivate Service
|
||||
GUI-->>Anwender: Zeigt Erfolgsmeldung via Status-Popup (binnen 1s)
|
||||
```
|
||||
---
|
||||
|
||||
# 8. Testbare Abnahmekriterien
|
||||
|
||||
Diese softwareseitigen Abnahmekriterien dienen als direkte Vorlage für die JUnit-Komponententests im Projekt.
|
||||
|
||||
* **AC-SH-PV-01 (Zu Anforderung F-SH-PV-01 & 06):**
|
||||
* *Vorbedingung:* Der `ProductService` ist ordnungsgemäß initialisiert.
|
||||
* *Test-Aktion:* Aufruf von `saveProduct(new Product(null, "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL))`.
|
||||
* *Erwartetes Ergebnis:* Das System wirft eine `IllegalArgumentException` und bricht das Speichern ab.
|
||||
* **AC-SH-PV-02 (Zu Anforderung F-SH-PV-03):**
|
||||
* *Vorbedingung:* Produkt mit ID `P-0001` ist in einem aktiven Angebot (Gruppe F) hinterlegt.
|
||||
* *Test-Aktion:* Aufruf von `deleteProduct("P-0001")`.
|
||||
* *Erwartetes Ergebnis:* Die Methode liefert `false` zurück, der Eintrag bleibt unberührt im Datenbestand.
|
||||
* **AC-SH-PV-03 (Zu Anforderung F-SH-PV-05):**
|
||||
* *Vorbedingung:* Das Repository enthält die Produkte "M6 Schraube" und "M8 SCHRAUBE".
|
||||
* *Test-Aktion:* Aufruf von `searchProducts("schraube")`.
|
||||
* *Erwartetes Ergebnis:* Die zurückgegebene Liste enthält exakt beide Objekte (Ignorierung der Groß-/Kleinschreibung).
|
||||
|
||||
---
|
||||
|
||||
# 9. Traceability-Matrix (Lastenheft zu Pflichtenheft)
|
||||
|
||||
| Lastenheft-ID (LH) | Pflichtenheft-ID (PH) | Zugeordneter Komponententest (JUnit) | Status / Abdeckung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **F-PV-01** | F-SH-PV-01 | `ProductServiceTest#testSaveValidProduct` | Vollständig abgedeckt |
|
||||
| **F-PV-02** | F-SH-PV-02 | `ProductServiceTest#testUpdateProductAttributes` | Vollständig abgedeckt |
|
||||
| **F-PV-03** | F-SH-PV-03 | `ProductServiceTest#testDeleteReferencedProduct` | Vollständig abgedeckt |
|
||||
|**F-PV-04** | F-SH-PV-04 | `ProductRepositoryTest#testGetAllAndSorting` | Vollständig abgedeckt |
|
||||
| **BA-PV-05** | F-SH-PV-05 | `ProductServiceTest#testSearchCaseInsensitive` | Vollständig abgedeckt |
|
||||
| **F-PV-06** | F-SH-PV-06 | `ProductServiceTest#testNegativePriceRejection` | Vollständig abgedeckt |
|
||||
| **F-PV-07** | F-SH-PV-02 | `ProductServiceTest#testDescriptionLengthBounds` | Vollständig abgedeckt |
|
||||
| **F-PV-08** | F-SH-PV-02 | `ProductServiceTest#testCategoryAssignment` | Vollständig abgedeckt |
|
||||
| **GR-05** | F-SH-PV-03 | `ProductServiceTest#testDeleteReferencedProduct` | Vollständig abgedeckt |
|
||||
| **NF-PERF-01** | NF-SH-PERF-01 | `ProductPerformanceTest#testSearchPerformance` | Vollständig abgedeckt |
|
||||
| **NF-ARCH-01** | NF-SH-ARCH-01 | `ProductRepositoryTest#testPersistenceOnRestart` | Vollständig abgedeckt |
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,395 @@
|
|||
# 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.
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
||||
import de.hsmannheim.faktura.dokumente.domain.DokumentPosition;
|
||||
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
|
||||
import de.hsmannheim.faktura.dokumente.domain.DokumentType;
|
||||
import de.hsmannheim.faktura.dokumente.export.PdfExportService;
|
||||
import de.hsmannheim.faktura.dokumente.export.SimplePdfExportService;
|
||||
import de.hsmannheim.faktura.dokumente.lookup.CustomerDto;
|
||||
import de.hsmannheim.faktura.dokumente.lookup.CustomerLookup;
|
||||
import de.hsmannheim.faktura.dokumente.lookup.ProductDto;
|
||||
import de.hsmannheim.faktura.dokumente.lookup.ProductLookup;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
|
||||
import de.hsmannheim.faktura.dokumente.service.DokumentService;
|
||||
import de.hsmannheim.faktura.dokumente.service.DokumentServiceImpl;
|
||||
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
|
||||
import de.hsmannheim.faktura.kunde.service.BelegReferenzPruefer;
|
||||
import de.hsmannheim.faktura.kunde.service.KundeService;
|
||||
import de.hsmannheim.faktura.kunde.service.KundeValidator;
|
||||
import de.hsmannheim.faktura.kunde.model.Kunde;
|
||||
|
||||
/**
|
||||
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12).
|
||||
*
|
||||
* Verdrahtung gegen die spec-konforme Schnittstelle aus dem
|
||||
* Pflichtenheft Gruppe F v1.0 (Kap. 6.2):
|
||||
* - Service: {@link DokumentService} / {@link DokumentServiceImpl}
|
||||
* - Repository: {@link DokumentRepository} / {@link InMemoryDokumentRepository}
|
||||
* - Methoden: erstelleAuftragsbestaetigung / erstelleLieferschein /
|
||||
* erstelleRechnung / exportiereAlsPdf
|
||||
* - Status: OFFEN, UEBERFUEHRT, BESTAETIGT, GELIEFERT, FAKTURIERT,
|
||||
* FESTGESCHRIEBEN
|
||||
*
|
||||
* Die ursprüngliche GUI-Verdrahtung (DokumentenService, getDokumentnummer,
|
||||
* RECHNUNG_ERSTELLT, deleteDokument, ...) findet sich in keinem Pflichtenheft
|
||||
* wieder und wurde hier auf die gemeinsame Spec gemappt. OP-6 (GUI vs.
|
||||
* PH v1.1 / Gruppe F PH v1.0).
|
||||
*/
|
||||
public class DokumentPanel extends JPanel {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final DokumentService dokumentService;
|
||||
private final TrackingDokumentRepository tracking;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable dokumentTable;
|
||||
|
||||
// Detailfelder (schreibgeschützt)
|
||||
private JTextField txtDokumentNummer;
|
||||
private JTextField txtKundenId;
|
||||
private JTextField txtDatum;
|
||||
private JTextField txtStatus;
|
||||
private JCheckBox chkFestgeschrieben;
|
||||
|
||||
// Positionstabelle
|
||||
private DefaultTableModel posTableModel;
|
||||
private JTable posTable;
|
||||
|
||||
// Summenfelder
|
||||
private JLabel lblNetto;
|
||||
private JLabel lblUst;
|
||||
private JLabel lblBrutto;
|
||||
|
||||
// Aktions-Buttons (PHGUI10)
|
||||
private JButton btnNeu;
|
||||
private JButton btnAbbrechen;
|
||||
private JButton btnPdfExport;
|
||||
|
||||
// Folgeaktions-Buttons
|
||||
private JButton btnAngebotZuAuftrag;
|
||||
private JButton btnAuftragZuLieferschein;
|
||||
private JButton btnLieferscheinZuRechnung;
|
||||
|
||||
private Dokument aktuellesDokument = null;
|
||||
|
||||
public DokumentPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
|
||||
// DokumentServiceImpl braucht 4 Abhängigkeiten (DI laut Gruppe F):
|
||||
// Repository, ProductLookup, CustomerLookup, PdfExportService.
|
||||
this.tracking = new TrackingDokumentRepository(new InMemoryDokumentRepository());
|
||||
PdfExportService pdfExport = new SimplePdfExportService(Path.of("exports"));
|
||||
CustomerLookup customerLookup = new KundeCustomerLookup(buildKundeService());
|
||||
ProductLookup productLookup = stubProductLookup();
|
||||
|
||||
this.dokumentService = new DokumentServiceImpl(
|
||||
tracking, productLookup, customerLookup, pdfExport);
|
||||
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
ladeDokumente();
|
||||
}
|
||||
|
||||
/**
|
||||
* KundeService nur als Datenquelle für den CustomerLookup-Adapter
|
||||
* (K3 aus PH v1.1). Eigener Aufbau, da das Panel self-contained ist –
|
||||
* geteiltes Wiring zwischen Panels ist Bestandteil von OP-6.
|
||||
*/
|
||||
private KundeService buildKundeService() {
|
||||
return new KundeService(
|
||||
new KundeRepositoryJson(new File("data/kunden.json")),
|
||||
new KundeValidator(),
|
||||
(BelegReferenzPruefer) (kundeId -> false));
|
||||
}
|
||||
|
||||
/**
|
||||
* DokumentPanel kann derzeit keine neuen Angebote anlegen
|
||||
* (Eingabemaske fehlt im UI), deshalb wird der ProductLookup nicht
|
||||
* aktiv genutzt. Stub ist ausreichend, damit DokumentServiceImpl
|
||||
* verdrahtet werden kann.
|
||||
*/
|
||||
private ProductLookup stubProductLookup() {
|
||||
return produktId -> Optional.<ProductDto>empty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// UI-Aufbau
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void initComponents() {
|
||||
// --- Linke Seite: Dokumentenliste ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(320, 0));
|
||||
|
||||
String[] cols = {"Nr.", "Typ", "Status"};
|
||||
tableModel = new DefaultTableModel(cols, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
dokumentTable = new JTable(tableModel) {
|
||||
// Status-Spalte farbig (PHGUI10)
|
||||
public Component prepareRenderer(TableCellRenderer r, int row, int col) {
|
||||
Component c = super.prepareRenderer(r, row, col);
|
||||
if (col == 2 && !isRowSelected(row)) {
|
||||
String status = String.valueOf(getValueAt(row, col));
|
||||
c.setForeground(farbeFuerStatus(status));
|
||||
} else {
|
||||
c.setForeground(Color.BLACK);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
};
|
||||
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
dokumentTable.setAutoCreateRowSorter(true);
|
||||
dokumentTable.getTableHeader().setReorderingAllowed(false);
|
||||
dokumentTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
|
||||
});
|
||||
|
||||
leftPanel.add(new JScrollPane(dokumentTable), BorderLayout.CENTER);
|
||||
|
||||
// --- Rechte Seite: Detailbereich ---
|
||||
JPanel rightPanel = new JPanel(new BorderLayout(0, 8));
|
||||
rightPanel.add(kopfPanel(), BorderLayout.NORTH);
|
||||
rightPanel.add(positionsBereich(), BorderLayout.CENTER);
|
||||
rightPanel.add(buttonPanel(), BorderLayout.SOUTH);
|
||||
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
|
||||
split.setDividerLocation(320);
|
||||
add(split, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private JPanel kopfPanel() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
p.setBorder(BorderFactory.createTitledBorder("Dokumentkopf"));
|
||||
GridBagConstraints g = new GridBagConstraints();
|
||||
g.insets = new Insets(4, 6, 2, 6);
|
||||
g.anchor = GridBagConstraints.WEST;
|
||||
g.fill = GridBagConstraints.HORIZONTAL;
|
||||
g.weightx = 1.0;
|
||||
|
||||
txtDokumentNummer = createReadOnlyField();
|
||||
txtKundenId = createReadOnlyField();
|
||||
txtDatum = createReadOnlyField();
|
||||
txtStatus = createReadOnlyField();
|
||||
chkFestgeschrieben = new JCheckBox("Festgeschrieben (PHGUI11)");
|
||||
chkFestgeschrieben.setEnabled(false);
|
||||
|
||||
int row = 0;
|
||||
row = addLabelField(p, g, "Dokumentnummer:", txtDokumentNummer, row);
|
||||
row = addLabelField(p, g, "Kunden-ID:", txtKundenId, row);
|
||||
row = addLabelField(p, g, "Datum:", txtDatum, row);
|
||||
row = addLabelField(p, g, "Status:", txtStatus, row);
|
||||
|
||||
g.gridx = 0; g.gridy = row; g.gridwidth = 2; p.add(chkFestgeschrieben, g);
|
||||
g.gridwidth = 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
private JPanel positionsBereich() {
|
||||
JPanel p = new JPanel(new BorderLayout(0, 4));
|
||||
p.setBorder(BorderFactory.createTitledBorder("Positionen / Summen"));
|
||||
|
||||
String[] posCols = {"Pos.", "Bezeichnung", "Menge", "Einzelpreis", "MwSt.", "Gesamt"};
|
||||
posTableModel = new DefaultTableModel(posCols, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
posTable = new JTable(posTableModel);
|
||||
posTable.getTableHeader().setReorderingAllowed(false);
|
||||
p.add(new JScrollPane(posTable), BorderLayout.CENTER);
|
||||
|
||||
JPanel summenPanel = new JPanel(new GridLayout(1, 6, 8, 0));
|
||||
summenPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||
summenPanel.add(new JLabel("Netto:"));
|
||||
lblNetto = new JLabel("0.00 €");
|
||||
summenPanel.add(lblNetto);
|
||||
summenPanel.add(new JLabel("USt.:"));
|
||||
lblUst = new JLabel("0.00 €");
|
||||
summenPanel.add(lblUst);
|
||||
summenPanel.add(new JLabel("Brutto:"));
|
||||
lblBrutto = new JLabel("0.00 €");
|
||||
summenPanel.add(lblBrutto);
|
||||
p.add(summenPanel, BorderLayout.SOUTH);
|
||||
return p;
|
||||
}
|
||||
|
||||
private JPanel buttonPanel() {
|
||||
JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
btnNeu = new JButton("Auswahl zurücksetzen");
|
||||
btnAbbrechen = new JButton("Abbrechen");
|
||||
btnPdfExport = new JButton("PDF-Export");
|
||||
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung");
|
||||
btnAuftragZuLieferschein = new JButton("→ Lieferschein");
|
||||
btnLieferscheinZuRechnung = new JButton("→ Rechnung");
|
||||
|
||||
btnNeu.addActionListener(e -> neuesFormular());
|
||||
btnAbbrechen.addActionListener(e -> neuesFormular());
|
||||
btnPdfExport.addActionListener(e -> pdfExport());
|
||||
btnAngebotZuAuftrag.addActionListener(e ->
|
||||
folgeaktion(DokumentType.ANGEBOT));
|
||||
btnAuftragZuLieferschein.addActionListener(e ->
|
||||
folgeaktion(DokumentType.AUFTRAGSBESTAETIGUNG));
|
||||
btnLieferscheinZuRechnung.addActionListener(e ->
|
||||
folgeaktion(DokumentType.LIEFERSCHEIN));
|
||||
|
||||
p.add(btnAngebotZuAuftrag);
|
||||
p.add(btnAuftragZuLieferschein);
|
||||
p.add(btnLieferscheinZuRechnung);
|
||||
p.add(btnPdfExport);
|
||||
p.add(btnNeu);
|
||||
p.add(btnAbbrechen);
|
||||
|
||||
// Initial alles deaktiviert (es ist nichts ausgewählt)
|
||||
setAktionenDeaktiviert();
|
||||
return p;
|
||||
}
|
||||
|
||||
private int addLabelField(JPanel p, GridBagConstraints g,
|
||||
String label, JComponent field, int row) {
|
||||
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);
|
||||
return row + 1;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Daten laden / anzeigen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void ladeDokumente() {
|
||||
tableModel.setRowCount(0);
|
||||
for (Dokument d : tracking.findAll()) {
|
||||
tableModel.addRow(new Object[]{
|
||||
d.getDokumentNummer(),
|
||||
typBezeichnung(d.getTyp()),
|
||||
d.getStatus().name()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehltes() {
|
||||
int row = dokumentTable.getSelectedRow();
|
||||
if (row < 0) { aktuellesDokument = null; return; }
|
||||
int modelRow = dokumentTable.convertRowIndexToModel(row);
|
||||
String nr = (String) tableModel.getValueAt(modelRow, 0);
|
||||
|
||||
tracking.findByNummer(nr).ifPresent(this::zeigeDokument);
|
||||
}
|
||||
|
||||
private void zeigeDokument(Dokument d) {
|
||||
aktuellesDokument = d;
|
||||
txtDokumentNummer.setText(d.getDokumentNummer());
|
||||
txtKundenId.setText(Long.toString(d.getKundenId()));
|
||||
txtDatum.setText(d.getDatum() == null ? "" : d.getDatum().toString());
|
||||
txtStatus.setText(d.getStatus().name());
|
||||
|
||||
boolean festgeschrieben = d.getStatus() == DokumentStatus.FESTGESCHRIEBEN;
|
||||
chkFestgeschrieben.setSelected(festgeschrieben);
|
||||
|
||||
// Positionen
|
||||
posTableModel.setRowCount(0);
|
||||
int pos = 1;
|
||||
boolean lieferschein = d.getTyp() == DokumentType.LIEFERSCHEIN; // PH-DP-07
|
||||
for (DokumentPosition p : d.getPositionen()) {
|
||||
posTableModel.addRow(new Object[]{
|
||||
pos++,
|
||||
p.getBezeichnungSnapshot(),
|
||||
p.getMenge(),
|
||||
lieferschein ? "—" : (p.getEinzelpreisNettoSnapshot() + " €"),
|
||||
lieferschein ? "—" : (formatProzent(p.getMwstSatzSnapshot())),
|
||||
lieferschein ? "—" : (p.getNettoGesamt() + " €")
|
||||
});
|
||||
}
|
||||
|
||||
// Summen (Lieferschein zeigt keine Preise – PH-DP-07)
|
||||
if (lieferschein) {
|
||||
lblNetto.setText("—");
|
||||
lblUst.setText("—");
|
||||
lblBrutto.setText("—");
|
||||
} else {
|
||||
lblNetto.setText(nullSichBd(d.getNettoSumme()) + " €");
|
||||
lblUst.setText( nullSichBd(d.getUstSumme()) + " €");
|
||||
lblBrutto.setText(nullSichBd(d.getBruttoSumme())+ " €");
|
||||
}
|
||||
|
||||
aktualisiereAktionen(d);
|
||||
}
|
||||
|
||||
private String nullSichBd(BigDecimal v) { return v == null ? "0.00" : v.toPlainString(); }
|
||||
|
||||
/**
|
||||
* Aktiviert nur die zulässigen Folgeaktionen basierend auf
|
||||
* Dokumenttyp und -status (PHGUI10).
|
||||
* Festgeschriebene Rechnungen: alle Aktionen außer PDF-Export
|
||||
* deaktiviert (PHGUI11).
|
||||
*/
|
||||
private void aktualisiereAktionen(Dokument d) {
|
||||
DokumentType typ = d.getTyp();
|
||||
DokumentStatus status = d.getStatus();
|
||||
|
||||
if (status == DokumentStatus.FESTGESCHRIEBEN) {
|
||||
btnAngebotZuAuftrag.setEnabled(false);
|
||||
btnAuftragZuLieferschein.setEnabled(false);
|
||||
btnLieferscheinZuRechnung.setEnabled(false);
|
||||
btnPdfExport.setEnabled(true); // PHGUI12
|
||||
return;
|
||||
}
|
||||
|
||||
btnPdfExport.setEnabled(true);
|
||||
btnAngebotZuAuftrag.setEnabled(
|
||||
typ == DokumentType.ANGEBOT && status == DokumentStatus.OFFEN);
|
||||
btnAuftragZuLieferschein.setEnabled(
|
||||
typ == DokumentType.AUFTRAGSBESTAETIGUNG && status == DokumentStatus.BESTAETIGT);
|
||||
btnLieferscheinZuRechnung.setEnabled(
|
||||
typ == DokumentType.LIEFERSCHEIN && status == DokumentStatus.GELIEFERT);
|
||||
}
|
||||
|
||||
private void setAktionenDeaktiviert() {
|
||||
btnPdfExport.setEnabled(false);
|
||||
btnAngebotZuAuftrag.setEnabled(false);
|
||||
btnAuftragZuLieferschein.setEnabled(false);
|
||||
btnLieferscheinZuRechnung.setEnabled(false);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Aktionen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void neuesFormular() {
|
||||
aktuellesDokument = null;
|
||||
dokumentTable.clearSelection();
|
||||
txtDokumentNummer.setText("");
|
||||
txtKundenId.setText("");
|
||||
txtDatum.setText("");
|
||||
txtStatus.setText("");
|
||||
chkFestgeschrieben.setSelected(false);
|
||||
posTableModel.setRowCount(0);
|
||||
lblNetto.setText("0.00 €");
|
||||
lblUst.setText("0.00 €");
|
||||
lblBrutto.setText("0.00 €");
|
||||
setAktionenDeaktiviert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt das spec-konforme Folgedokument zum aktuell ausgewählten
|
||||
* Dokument. PH-DP-04 / PH-DP-06 / PH-DP-08.
|
||||
*/
|
||||
private void folgeaktion(DokumentType erwartet) {
|
||||
if (aktuellesDokument == null) {
|
||||
mainFrame.showStatus("Kein Dokument ausgewählt.");
|
||||
return;
|
||||
}
|
||||
if (aktuellesDokument.getTyp() != erwartet) {
|
||||
mainFrame.showStatus("Aktion passt nicht zum Dokumenttyp.");
|
||||
return;
|
||||
}
|
||||
String quellNr = aktuellesDokument.getDokumentNummer();
|
||||
try {
|
||||
Dokument folge;
|
||||
switch (erwartet) {
|
||||
case ANGEBOT ->
|
||||
folge = dokumentService.erstelleAuftragsbestaetigung(quellNr); // PH-DP-04
|
||||
case AUFTRAGSBESTAETIGUNG ->
|
||||
folge = dokumentService.erstelleLieferschein(quellNr, LocalDate.now()); // PH-DP-06
|
||||
case LIEFERSCHEIN ->
|
||||
folge = dokumentService.erstelleRechnung(quellNr); // PH-DP-08
|
||||
default -> { mainFrame.showStatus("Kein Folgedokument vorgesehen."); return; }
|
||||
}
|
||||
ladeDokumente();
|
||||
mainFrame.showStatus("Folgedokument erstellt: " + folge.getDokumentNummer());
|
||||
} catch (RuntimeException ex) {
|
||||
// Fängt IllegalStateException, DuplicateDocumentException,
|
||||
// ValidationException, DokumentNotFoundException
|
||||
mainFrame.showStatus("Fehler: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void pdfExport() {
|
||||
if (aktuellesDokument == null) {
|
||||
mainFrame.showStatus("Kein Dokument ausgewählt.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Path pfad = dokumentService.exportiereAlsPdf(
|
||||
aktuellesDokument.getDokumentNummer()); // PHGUI12 / PH-DP-12
|
||||
mainFrame.showStatus("PDF erzeugt: " + pfad);
|
||||
} catch (RuntimeException ex) {
|
||||
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helfer
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static String typBezeichnung(DokumentType t) {
|
||||
if (t == null) return "";
|
||||
return switch (t) {
|
||||
case ANGEBOT -> "Angebot";
|
||||
case AUFTRAGSBESTAETIGUNG -> "Auftragsbestätigung";
|
||||
case LIEFERSCHEIN -> "Lieferschein";
|
||||
case RECHNUNG -> "Rechnung";
|
||||
};
|
||||
}
|
||||
|
||||
private static Color farbeFuerStatus(String status) {
|
||||
return switch (status) {
|
||||
case "OFFEN", "BESTAETIGT" -> new Color(41, 128, 185);
|
||||
case "GELIEFERT" -> new Color(39, 174, 96);
|
||||
case "FESTGESCHRIEBEN" -> new Color(155, 89, 182);
|
||||
case "UEBERFUEHRT", "FAKTURIERT" -> new Color(127, 140, 141);
|
||||
default -> Color.BLACK;
|
||||
};
|
||||
}
|
||||
|
||||
private static String formatProzent(BigDecimal mwstSatz) {
|
||||
if (mwstSatz == null) return "";
|
||||
return mwstSatz.multiply(BigDecimal.valueOf(100)) + " %";
|
||||
}
|
||||
|
||||
private JTextField createReadOnlyField() {
|
||||
JTextField f = new JTextField();
|
||||
f.setEditable(false);
|
||||
f.setBackground(new Color(230, 230, 230));
|
||||
return f;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Adapter / Wrapper – GUI-intern, keine Eingriffe in fremde Module
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Adapter K3 (PH v1.1 Kap. 6.2): Bedient die von Gruppe F geforderte
|
||||
* {@link CustomerLookup}-Schnittstelle über das spec-konforme
|
||||
* {@link KundeService} der Kundenverwaltung.
|
||||
*/
|
||||
private static final class KundeCustomerLookup implements CustomerLookup {
|
||||
private final KundeService kundeService;
|
||||
KundeCustomerLookup(KundeService kundeService) {
|
||||
this.kundeService = kundeService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CustomerDto> findCustomer(long kundenId) {
|
||||
return kundeService.finde(kundenId).map(this::toDto);
|
||||
}
|
||||
|
||||
private CustomerDto toDto(Kunde k) {
|
||||
String name;
|
||||
if (k.getFirmenname() != null && !k.getFirmenname().isBlank()) {
|
||||
name = k.getFirmenname();
|
||||
} else {
|
||||
String n = k.getNachname() == null ? "" : k.getNachname();
|
||||
name = (k.getVorname() == null || k.getVorname().isBlank())
|
||||
? n : (k.getVorname() + " " + n);
|
||||
}
|
||||
String anschrift = String.format("%s, %s %s",
|
||||
nullAls(k.getStrasse()),
|
||||
nullAls(k.getPlz()),
|
||||
nullAls(k.getOrt()));
|
||||
return new CustomerDto(k.getKundeId(), name, anschrift, nullAls(k.getUstIdNr()));
|
||||
}
|
||||
|
||||
private static String nullAls(String s) { return s == null ? "" : s; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator über {@link DokumentRepository}: trackt zusätzlich alle
|
||||
* gespeicherten Dokumente, damit das Panel eine vollständige Liste
|
||||
* anzeigen kann. {@code findByNummer/findByKunde/...} werden an das
|
||||
* gekapselte Repository delegiert.
|
||||
*
|
||||
* Notwendig, weil das Schnittstellen-Interface kein {@code findAll()}
|
||||
* vorsieht (Gruppe F PH v1.0 Kap. 6.2) – ein Tracking in der GUI ist
|
||||
* spec-neutral und greift nicht in Gruppe F's Modul ein.
|
||||
*/
|
||||
private static final class TrackingDokumentRepository implements DokumentRepository {
|
||||
private final DokumentRepository delegate;
|
||||
private final List<Dokument> alle = new ArrayList<>();
|
||||
|
||||
TrackingDokumentRepository(DokumentRepository delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
List<Dokument> findAll() {
|
||||
List<Dokument> kopie = new ArrayList<>(alle);
|
||||
kopie.sort(Comparator
|
||||
.comparing(Dokument::getDatum, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(Dokument::getDokumentNummer));
|
||||
return Collections.unmodifiableList(kopie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Dokument dokument) {
|
||||
delegate.save(dokument);
|
||||
if (!alle.contains(dokument)) {
|
||||
alle.add(dokument);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Optional<Dokument> findByNummer(String nummer) { return delegate.findByNummer(nummer); }
|
||||
@Override public List<Dokument> findByKunde(long kundenId) { return delegate.findByKunde(kundenId); }
|
||||
@Override public boolean existsFolgedokument(String quellDokumentNr) { return delegate.existsFolgedokument(quellDokumentNr); }
|
||||
@Override public boolean existsReferencedProdukt(long produktId) { return delegate.existsReferencedProdukt(produktId); }
|
||||
@Override public boolean existsReferencedKunde(long kundenId) { return delegate.existsReferencedKunde(kundenId); }
|
||||
@Override public int findMaxLaufendeNummer(String praefix, int j) { return delegate.findMaxLaufendeNummer(praefix, j); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import de.hsmannheim.faktura.kunde.model.Kunde;
|
||||
import de.hsmannheim.faktura.kunde.repository.KundeRepository;
|
||||
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
|
||||
import de.hsmannheim.faktura.kunde.service.BelegReferenzPruefer;
|
||||
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, PHGUI05–PHGUI09).
|
||||
*
|
||||
* 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 {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final KundeService kundeService;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable kundenTable;
|
||||
|
||||
// Formularfelder
|
||||
private JTextField txtKundeId;
|
||||
private JTextField txtFirmenname;
|
||||
private JTextField txtNachname;
|
||||
private JTextField txtVorname;
|
||||
private JTextField txtStrasse;
|
||||
private JTextField txtPlz;
|
||||
private JTextField txtOrt;
|
||||
private JTextField txtTelefon;
|
||||
private JTextField txtEmail;
|
||||
private JTextField txtUstIdNr;
|
||||
|
||||
// Fehlerhinweise (PHGUI03, PHGUI05)
|
||||
private JLabel lblNameFehler;
|
||||
private JLabel lblStrasseFehler;
|
||||
private JLabel lblPlzFehler;
|
||||
private JLabel lblOrtFehler;
|
||||
private JLabel lblEmailFehler;
|
||||
|
||||
// Suche (PHGUI09)
|
||||
private JTextField txtSuche;
|
||||
|
||||
// Buttons (PHGUI02)
|
||||
private JButton btnNeu;
|
||||
private JButton btnSpeichern;
|
||||
private JButton btnLoeschen;
|
||||
private JButton btnAbbrechen;
|
||||
|
||||
public KundenPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.kundeService = new KundeService(
|
||||
new KundeRepositoryJson(new File("data/kunden.json")),
|
||||
new KundeValidator(),
|
||||
stubBelegPruefer()
|
||||
);
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
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() {
|
||||
// --- Linke Seite: Liste + Suche (PHGUI09) ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(320, 0));
|
||||
|
||||
String[] columns = {"ID", "Name", "Ort"};
|
||||
tableModel = new DefaultTableModel(columns, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
kundenTable = new JTable(tableModel);
|
||||
kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
kundenTable.getTableHeader().setReorderingAllowed(false);
|
||||
kundenTable.setAutoCreateRowSorter(true); // PHGUI08
|
||||
|
||||
kundenTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehlten();
|
||||
});
|
||||
|
||||
JPanel suchPanel = new JPanel(new BorderLayout(4, 0));
|
||||
suchPanel.add(new JLabel("Suche: "), BorderLayout.WEST);
|
||||
txtSuche = new JTextField();
|
||||
txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
|
||||
public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
});
|
||||
suchPanel.add(txtSuche, BorderLayout.CENTER);
|
||||
|
||||
leftPanel.add(suchPanel, BorderLayout.NORTH);
|
||||
leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER);
|
||||
|
||||
// --- Rechte Seite: Formular + Buttons (PHGUI02) ---
|
||||
JPanel rightPanel = new JPanel(new BorderLayout(0, 8));
|
||||
rightPanel.add(formularPanel(), BorderLayout.CENTER);
|
||||
rightPanel.add(buttonPanel(), BorderLayout.SOUTH);
|
||||
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
|
||||
split.setDividerLocation(320);
|
||||
add(split, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private JPanel formularPanel() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
p.setBorder(BorderFactory.createTitledBorder("Kundendaten"));
|
||||
GridBagConstraints g = new GridBagConstraints();
|
||||
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 int addLabelField(JPanel p, GridBagConstraints g,
|
||||
String label, JComponent field, JLabel fehlerLabel, int row) {
|
||||
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 ---
|
||||
|
||||
private void ladeKunden() {
|
||||
ladeKundenListe(kundeService.alle());
|
||||
}
|
||||
|
||||
private void ladeKundenListe(List<Kunde> liste) {
|
||||
tableModel.setRowCount(0);
|
||||
for (Kunde k : liste) {
|
||||
tableModel.addRow(new Object[]{
|
||||
Long.toString(k.getKundeId()),
|
||||
nameVon(k),
|
||||
nullAls(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() {
|
||||
// PHGUI09: Treffer über Name UND Kundennummer.
|
||||
// KundeService.suchen("") liefert bewusst eine leere Liste (P4-3),
|
||||
// deshalb hier explizit auf .alle() umschalten.
|
||||
String query = txtSuche.getText();
|
||||
if (query == null || query.isBlank()) {
|
||||
ladeKundenListe(kundeService.alle());
|
||||
} else {
|
||||
ladeKundenListe(kundeService.suchen(query));
|
||||
}
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehlten() {
|
||||
int row = kundenTable.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
int modelRow = kundenTable.convertRowIndexToModel(row);
|
||||
String idStr = (String) tableModel.getValueAt(modelRow, 0);
|
||||
try {
|
||||
long kundeId = Long.parseLong(idStr);
|
||||
kundeService.finde(kundeId).ifPresent(this::fuelleFormular);
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Tabelle in inkonsistentem Zustand; einfach ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
private void fuelleFormular(Kunde k) {
|
||||
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();
|
||||
}
|
||||
|
||||
// --- Aktionen ---
|
||||
|
||||
private void neuesFormular() {
|
||||
txtKundeId.setText("");
|
||||
txtFirmenname.setText(""); txtNachname.setText(""); txtVorname.setText("");
|
||||
txtStrasse.setText(""); txtPlz.setText(""); txtOrt.setText("");
|
||||
txtTelefon.setText(""); txtEmail.setText(""); txtUstIdNr.setText("");
|
||||
clearFehler();
|
||||
kundenTable.clearSelection();
|
||||
}
|
||||
|
||||
private void speichern() {
|
||||
clearFehler();
|
||||
|
||||
// GUI-seitige Vorprüfung der Pflichtfelder (PHGUI03) - die fachliche
|
||||
// Validierung gegen PH v1.1 macht der KundeValidator beim Service-Aufruf.
|
||||
boolean clientValid = true;
|
||||
if (istLeer(txtFirmenname) && istLeer(txtNachname)) {
|
||||
lblNameFehler.setText("⚠ Firmenname ODER Nachname erforderlich");
|
||||
clientValid = 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)
|
||||
Kunde k = new Kunde();
|
||||
if (!txtKundeId.getText().isBlank()) {
|
||||
try { k.setKundeId(Long.parseLong(txtKundeId.getText().trim())); }
|
||||
catch (NumberFormatException ignored) { /* bleibt 0 = Neuanlage */ }
|
||||
}
|
||||
k.setFirmenname(leerAlsNull(txtFirmenname));
|
||||
k.setNachname(leerAlsNull(txtNachname));
|
||||
k.setVorname(leerAlsNull(txtVorname));
|
||||
k.setStrasse(leerAlsNull(txtStrasse));
|
||||
k.setPlz(leerAlsNull(txtPlz));
|
||||
k.setOrt(leerAlsNull(txtOrt));
|
||||
k.setTelefon(leerAlsNull(txtTelefon));
|
||||
k.setEmail(leerAlsNull(txtEmail));
|
||||
k.setUstIdNr(leerAlsNull(txtUstIdNr));
|
||||
|
||||
try {
|
||||
if (k.getKundeId() == 0) {
|
||||
long neueId = kundeService.anlegen(k); // F-KV-01
|
||||
k.setKundeId(neueId);
|
||||
} else {
|
||||
kundeService.bearbeiten(k); // F-KV-02
|
||||
}
|
||||
ladeKunden();
|
||||
mainFrame.showStatus("Kunde gespeichert (ID " + k.getKundeId() + ")."); // PHGUI06
|
||||
neuesFormular();
|
||||
} catch (ValidierungsException ex) {
|
||||
// Fehler aus KundeValidator (PH v1.1 Kap. 6.1.2) sichtbar machen
|
||||
String msg = ex.getMessage();
|
||||
if (msg.toLowerCase().contains("email") || msg.toLowerCase().contains("e-mail")) {
|
||||
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() {
|
||||
int row = kundenTable.getSelectedRow();
|
||||
if (row < 0) { mainFrame.showStatus("Kein Kunde ausgewählt."); return; }
|
||||
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this, "Kunden wirklich löschen?", "Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
int modelRow = kundenTable.convertRowIndexToModel(row);
|
||||
String idStr = (String) tableModel.getValueAt(modelRow, 0);
|
||||
try {
|
||||
long kundeId = Long.parseLong(idStr);
|
||||
kundeService.loeschen(kundeId); // F-KV-04
|
||||
ladeKunden();
|
||||
neuesFormular();
|
||||
mainFrame.showStatus("Kunde gelöscht.");
|
||||
} catch (LoeschsperreException ex) {
|
||||
mainFrame.showStatus("Löschen nicht möglich – " + ex.getMessage()); // GR-05
|
||||
} 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() {
|
||||
lblNameFehler.setText("");
|
||||
lblStrasseFehler.setText("");
|
||||
lblPlzFehler.setText("");
|
||||
lblOrtFehler.setText("");
|
||||
lblEmailFehler.setText("ⓘ Format: name@domain.tld");
|
||||
lblEmailFehler.setForeground(new Color(120, 120, 120));
|
||||
}
|
||||
|
||||
private JLabel createFehlerLabel() {
|
||||
JLabel lbl = new JLabel("");
|
||||
lbl.setForeground(new Color(192, 57, 43));
|
||||
lbl.setFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private JTextField createReadOnlyField() {
|
||||
JTextField f = new JTextField();
|
||||
f.setEditable(false);
|
||||
f.setBackground(new Color(230, 230, 230));
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Hauptfenster der Anwendung (PHGUI01, PHGUI02).
|
||||
* Stellt die dauerhaft sichtbare Hauptnavigation bereit und lädt die Modulpanels.
|
||||
*/
|
||||
public class MainFrame extends JFrame {
|
||||
|
||||
private final ProduktPanel produktPanel;
|
||||
private final KundenPanel kundenPanel;
|
||||
private final DokumentPanel dokumentPanel;
|
||||
private final JLabel statusLabel;
|
||||
|
||||
public MainFrame() {
|
||||
setTitle("Fakturierungssystem");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setSize(1100, 700);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
// Hauptlayout
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
// --- Navigation (PHGUI01) ---
|
||||
JPanel navPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
navPanel.setBackground(new Color(52, 73, 94));
|
||||
|
||||
JLabel title = new JLabel(" Fakturierungssystem ");
|
||||
title.setForeground(Color.WHITE);
|
||||
title.setFont(new Font("SansSerif", Font.BOLD, 14));
|
||||
navPanel.add(title);
|
||||
|
||||
JButton btnProdukt = createNavButton("Produktverwaltung");
|
||||
JButton btnKunde = createNavButton("Kundenverwaltung");
|
||||
JButton btnDokument = createNavButton("Dokumentenprozess");
|
||||
navPanel.add(btnProdukt);
|
||||
navPanel.add(btnKunde);
|
||||
navPanel.add(btnDokument);
|
||||
add(navPanel, BorderLayout.NORTH);
|
||||
|
||||
// --- Inhaltsbereich ---
|
||||
JPanel contentArea = new JPanel(new CardLayout());
|
||||
produktPanel = new ProduktPanel(this);
|
||||
kundenPanel = new KundenPanel(this);
|
||||
dokumentPanel = new DokumentPanel(this);
|
||||
|
||||
contentArea.add(produktPanel, "produkt");
|
||||
contentArea.add(kundenPanel, "kunde");
|
||||
contentArea.add(dokumentPanel, "dokument");
|
||||
add(contentArea, BorderLayout.CENTER);
|
||||
|
||||
// --- Statusbereich (PHGUI06) ---
|
||||
statusLabel = new JLabel("Status: Bereit");
|
||||
statusLabel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
|
||||
statusLabel.setFont(new Font("SansSerif", Font.PLAIN, 12));
|
||||
add(statusLabel, BorderLayout.SOUTH);
|
||||
|
||||
// Navigation-Actions (PHGUI01: 1 Klick)
|
||||
CardLayout cl = (CardLayout) contentArea.getLayout();
|
||||
btnProdukt.addActionListener(e -> cl.show(contentArea, "produkt"));
|
||||
btnKunde.addActionListener(e -> cl.show(contentArea, "kunde"));
|
||||
btnDokument.addActionListener(e -> cl.show(contentArea, "dokument"));
|
||||
|
||||
// Standard: Produktverwaltung
|
||||
cl.show(contentArea, "produkt");
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Statusmeldung im Statusbereich an (PHGUI06: innerhalb 1 Sekunde).
|
||||
*/
|
||||
public void showStatus(String message) {
|
||||
statusLabel.setText("Status: " + message);
|
||||
// Reset nach 3 Sekunden
|
||||
Timer timer = new Timer(3000, e -> statusLabel.setText("Status: Bereit"));
|
||||
timer.setRepeats(false);
|
||||
timer.start();
|
||||
}
|
||||
|
||||
private JButton createNavButton(String text) {
|
||||
JButton btn = new JButton(text);
|
||||
btn.setBackground(new Color(52, 73, 94));
|
||||
btn.setForeground(Color.WHITE);
|
||||
btn.setFocusPainted(false);
|
||||
btn.setBorderPainted(false);
|
||||
btn.setFont(new Font("SansSerif", Font.BOLD, 13));
|
||||
btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
btn.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
public void mouseEntered(java.awt.event.MouseEvent e) {
|
||||
btn.setBackground(new Color(41, 128, 185));
|
||||
}
|
||||
public void mouseExited(java.awt.event.MouseEvent e) {
|
||||
btn.setBackground(new Color(52, 73, 94));
|
||||
}
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(MainFrame::new);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import de.hsmannheim.faktura.product.MwstSatz;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
import de.hsmannheim.faktura.product.service.ProductService;
|
||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
|
||||
/**
|
||||
* Panel für die Produktverwaltung (PHGUI01–PHGUI04, PHGUI06–PHGUI09).
|
||||
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
|
||||
*/
|
||||
public class ProduktPanel extends JPanel {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final ProductService productService;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable produktTable;
|
||||
|
||||
// Formularfelder
|
||||
private JTextField txtProduktId;
|
||||
private JTextField txtBezeichnung;
|
||||
private JTextField txtPreis;
|
||||
private JComboBox<MwstSatz> cmbMwst;
|
||||
private JTextArea txtBeschreibung;
|
||||
private JTextField txtKategorie;
|
||||
|
||||
// Fehlerhinweise (PHGUI03)
|
||||
private JLabel lblBezeichnungFehler;
|
||||
private JLabel lblPreisFehler;
|
||||
|
||||
// Suche (PHGUI09)
|
||||
private JTextField txtSuche;
|
||||
|
||||
// Buttons (PHGUI02: einheitlich beschriftet)
|
||||
private JButton btnNeu;
|
||||
private JButton btnSpeichern;
|
||||
private JButton btnLoeschen;
|
||||
private JButton btnAbbrechen;
|
||||
|
||||
public ProduktPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.productService = new ProductService(
|
||||
new ProductRepository(),
|
||||
createDummyDokumentRepository()
|
||||
);
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
ladeProdukte();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
// --- Linke Seite: Liste + Suche ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(320, 0));
|
||||
|
||||
String[] columns = {"ID", "Bezeichnung ▲", "Preis"};
|
||||
tableModel = new DefaultTableModel(columns, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
produktTable = new JTable(tableModel);
|
||||
produktTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
produktTable.getTableHeader().setReorderingAllowed(false);
|
||||
|
||||
// Sortierung (PHGUI08)
|
||||
produktTable.setAutoCreateRowSorter(true);
|
||||
|
||||
produktTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
|
||||
});
|
||||
|
||||
leftPanel.add(new JLabel("Produktliste"), BorderLayout.NORTH);
|
||||
leftPanel.add(new JScrollPane(produktTable), BorderLayout.CENTER);
|
||||
|
||||
// Suche (PHGUI09)
|
||||
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
|
||||
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche"));
|
||||
txtSuche = new JTextField();
|
||||
txtSuche.setToolTipText("Bezeichnung eingeben...");
|
||||
txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
|
||||
public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
});
|
||||
suchePanel.add(new JLabel("Bezeichnung:"), BorderLayout.WEST);
|
||||
suchePanel.add(txtSuche, BorderLayout.CENTER);
|
||||
leftPanel.add(suchePanel, BorderLayout.SOUTH);
|
||||
|
||||
// --- Rechte Seite: Formular ---
|
||||
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
|
||||
rightPanel.setBorder(BorderFactory.createTitledBorder("Produktdetails Pflichtfelder sind mit * markiert"));
|
||||
|
||||
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 4, 2, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
// Zeile 0: ProduktId + Einzelpreis
|
||||
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0.5;
|
||||
formPanel.add(new JLabel("Produkt-ID (autom. vergeben)"), gbc);
|
||||
gbc.gridx = 1;
|
||||
formPanel.add(new JLabel("Einzelpreis Netto (€) *"), gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 1;
|
||||
txtProduktId = new JTextField();
|
||||
txtProduktId.setEditable(false);
|
||||
txtProduktId.setBackground(new Color(230, 230, 230));
|
||||
formPanel.add(txtProduktId, gbc);
|
||||
gbc.gridx = 1;
|
||||
txtPreis = new JTextField();
|
||||
formPanel.add(txtPreis, gbc);
|
||||
|
||||
// Fehlerhinweis Preis
|
||||
gbc.gridx = 1; gbc.gridy = 2;
|
||||
lblPreisFehler = createFehlerLabel();
|
||||
formPanel.add(lblPreisFehler, gbc);
|
||||
|
||||
// Zeile 3: Bezeichnung + MwSt
|
||||
gbc.gridx = 0; gbc.gridy = 3;
|
||||
formPanel.add(new JLabel("Bezeichnung *"), gbc);
|
||||
gbc.gridx = 1;
|
||||
formPanel.add(new JLabel("MwSt.-Satz (%) *"), gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 4;
|
||||
txtBezeichnung = new JTextField();
|
||||
formPanel.add(txtBezeichnung, gbc);
|
||||
gbc.gridx = 1;
|
||||
cmbMwst = new JComboBox<>(MwstSatz.values());
|
||||
formPanel.add(cmbMwst, gbc);
|
||||
|
||||
// Fehlerhinweis Bezeichnung
|
||||
gbc.gridx = 0; gbc.gridy = 5;
|
||||
lblBezeichnungFehler = createFehlerLabel();
|
||||
formPanel.add(lblBezeichnungFehler, gbc);
|
||||
|
||||
// Zeile 6: Kategorie
|
||||
gbc.gridx = 0; gbc.gridy = 6; gbc.gridwidth = 2;
|
||||
formPanel.add(new JLabel("Kategorie"), gbc);
|
||||
gbc.gridy = 7;
|
||||
txtKategorie = new JTextField();
|
||||
formPanel.add(txtKategorie, gbc);
|
||||
|
||||
// Zeile 8: Beschreibung
|
||||
gbc.gridy = 8;
|
||||
formPanel.add(new JLabel("Beschreibung"), gbc);
|
||||
gbc.gridy = 9; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH;
|
||||
txtBeschreibung = new JTextArea(4, 20);
|
||||
txtBeschreibung.setLineWrap(true);
|
||||
formPanel.add(new JScrollPane(txtBeschreibung), gbc);
|
||||
|
||||
rightPanel.add(formPanel, BorderLayout.CENTER);
|
||||
|
||||
// --- Buttons (PHGUI02: einheitlich) ---
|
||||
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);
|
||||
|
||||
// Button-Actions
|
||||
btnNeu.addActionListener(e -> neuesFormular());
|
||||
btnSpeichern.addActionListener(e -> speichern());
|
||||
btnLoeschen.addActionListener(e -> loeschen());
|
||||
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeProdukte(); });
|
||||
|
||||
// Split
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
|
||||
split.setDividerLocation(320);
|
||||
add(split, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// --- Daten laden ---
|
||||
private void ladeProdukte() {
|
||||
ladeProdukteListe(productService.getAllProducts());
|
||||
}
|
||||
|
||||
private void ladeProdukteListe(List<Product> liste) {
|
||||
tableModel.setRowCount(0);
|
||||
for (Product p : liste) {
|
||||
tableModel.addRow(new Object[]{
|
||||
p.getProductId(),
|
||||
p.getBezeichnung(),
|
||||
p.getEinzelpreisNetto() + " €"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sucheAktualisieren() {
|
||||
String query = txtSuche.getText();
|
||||
// PHGUI09: Groß-/Kleinschreibung ignorieren
|
||||
ladeProdukteListe(productService.searchProducts(query));
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehltes() {
|
||||
int row = produktTable.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
int modelRow = produktTable.convertRowIndexToModel(row);
|
||||
String id = (String) tableModel.getValueAt(modelRow, 0);
|
||||
productService.getProductById(id).ifPresent(p -> {
|
||||
txtProduktId.setText(p.getProductId());
|
||||
txtBezeichnung.setText(p.getBezeichnung());
|
||||
txtPreis.setText(p.getEinzelpreisNetto().toPlainString());
|
||||
cmbMwst.setSelectedItem(p.getMwstSatz());
|
||||
txtBeschreibung.setText(p.getBeschreibung());
|
||||
txtKategorie.setText(p.getKategorie());
|
||||
clearFehler();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Aktionen ---
|
||||
private void neuesFormular() {
|
||||
txtProduktId.setText("");
|
||||
txtBezeichnung.setText("");
|
||||
txtPreis.setText("");
|
||||
txtBeschreibung.setText("");
|
||||
txtKategorie.setText("");
|
||||
cmbMwst.setSelectedIndex(0);
|
||||
clearFehler();
|
||||
produktTable.clearSelection();
|
||||
}
|
||||
|
||||
private void speichern() {
|
||||
clearFehler();
|
||||
boolean valid = true;
|
||||
|
||||
String bezeichnung = txtBezeichnung.getText().trim();
|
||||
String preisText = txtPreis.getText().trim();
|
||||
|
||||
// PHGUI03: Pflichtfeldprüfung
|
||||
if (bezeichnung.isEmpty()) {
|
||||
lblBezeichnungFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
BigDecimal preis = null;
|
||||
if (preisText.isEmpty()) {
|
||||
lblPreisFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
} else {
|
||||
try {
|
||||
preis = new BigDecimal(preisText.replace(",", "."));
|
||||
// PHGUI04: Negativpreis ablehnen
|
||||
if (preis.compareTo(BigDecimal.ZERO) < 0) {
|
||||
lblPreisFehler.setText("⚠ Preis darf nicht negativ sein");
|
||||
valid = false;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
lblPreisFehler.setText("⚠ Ungültiger Zahlenwert");
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
Product p = new Product(
|
||||
txtProduktId.getText().trim(),
|
||||
bezeichnung,
|
||||
preis,
|
||||
(MwstSatz) cmbMwst.getSelectedItem(),
|
||||
txtBeschreibung.getText().trim(),
|
||||
txtKategorie.getText().trim()
|
||||
);
|
||||
|
||||
try {
|
||||
productService.saveProduct(p);
|
||||
ladeProdukte();
|
||||
neuesFormular();
|
||||
// PHGUI06: Sichtbare Bestätigung < 1 Sekunde
|
||||
mainFrame.showStatus("Produkt erfolgreich gespeichert.");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
mainFrame.showStatus("Fehler: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loeschen() {
|
||||
int row = produktTable.getSelectedRow();
|
||||
if (row < 0) {
|
||||
mainFrame.showStatus("Kein Produkt ausgewählt.");
|
||||
return;
|
||||
}
|
||||
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
"Produkt wirklich löschen?",
|
||||
"Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
int modelRow = produktTable.convertRowIndexToModel(row);
|
||||
String id = (String) tableModel.getValueAt(modelRow, 0);
|
||||
boolean deleted = productService.deleteProduct(id);
|
||||
|
||||
if (deleted) {
|
||||
ladeProdukte();
|
||||
neuesFormular();
|
||||
// PHGUI06
|
||||
mainFrame.showStatus("Produkt gelöscht.");
|
||||
} else {
|
||||
mainFrame.showStatus("Löschen nicht möglich – Produkt wird in Dokumenten verwendet.");
|
||||
}
|
||||
}
|
||||
|
||||
private void clearFehler() {
|
||||
lblBezeichnungFehler.setText("");
|
||||
lblPreisFehler.setText("");
|
||||
}
|
||||
|
||||
private JLabel createFehlerLabel() {
|
||||
JLabel lbl = new JLabel("");
|
||||
lbl.setForeground(new Color(192, 57, 43));
|
||||
lbl.setFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private DokumentRepository createDummyDokumentRepository() {
|
||||
return new DokumentRepository() {
|
||||
public boolean existsReferencedProdukt(long id) { return false; }
|
||||
public boolean existsReferencedKunde(long id) { return false; }
|
||||
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; }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.hsmannheim.faktura.product;
|
||||
|
||||
public enum MwstSatz {
|
||||
/**
|
||||
* Enumeration zur Repräsentation der Mehrwertsteuersätze in Deutschland.
|
||||
*/
|
||||
NORMAL,
|
||||
ERMAESSIGT
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package de.hsmannheim.faktura.product;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Entitätsklasse zur Repräsentation eines Produkts in der Produktverwaltung (Gruppe G)[cite: 54, 71].
|
||||
* Bildet die Stammdaten eines Artikels mit Netto-Einzelpreis, Bezeichnung und Mehrwertsteuersatz ab[cite: 55].
|
||||
* Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation.
|
||||
*/
|
||||
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 bezeichnung;
|
||||
private BigDecimal einzelpreisNetto;
|
||||
private MwstSatz mwstSatz;
|
||||
private String beschreibung;
|
||||
private String kategorie;
|
||||
|
||||
/**
|
||||
* Standardkonstruktor.
|
||||
* Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt.
|
||||
* Generiert automatisch die nächste ID im Format P-0001.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001)
|
||||
* @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen)
|
||||
* @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein)
|
||||
* @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT)
|
||||
* @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen)
|
||||
* @param kategorie Optionale Warengruppe oder hierarchische Zuordnung
|
||||
*/
|
||||
public Product(String productId, String bezeichnung, BigDecimal einzelpreisNetto,
|
||||
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.bezeichnung = bezeichnung;
|
||||
this.einzelpreisNetto = einzelpreisNetto;
|
||||
this.mwstSatz = mwstSatz;
|
||||
this.beschreibung = beschreibung;
|
||||
this.kategorie = kategorie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die eindeutige Produkt-ID zurück[cite: 55, 392].
|
||||
* * @return Die productId als String [cite: 55]
|
||||
*/
|
||||
public String getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die eindeutige Produkt-ID[cite: 55, 392].
|
||||
* * @param productId Die neu zu setzende ID [cite: 55]
|
||||
*/
|
||||
public void setProductId(String productId) {
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Anzeigenamen (die Bezeichnung) des Produkts zurück[cite: 55, 456].
|
||||
* * @return Die Produktbezeichnung als String [cite: 55]
|
||||
*/
|
||||
public String getBezeichnung() {
|
||||
return bezeichnung;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Produktbezeichnung[cite: 55, 456].
|
||||
* * @param bezeichnung Der neue Produktname (Pflichtfeld) [cite: 55, 393]
|
||||
*/
|
||||
public void setBezeichnung(String bezeichnung) {
|
||||
this.bezeichnung = bezeichnung;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Netto-Einzelpreis des Produkts zurück[cite: 55, 393].
|
||||
* * @return Der einzelpreisNetto als BigDecimal zur Vermeidung von Rundungsfehlern [cite: 55]
|
||||
*/
|
||||
public BigDecimal getEinzelpreisNetto() {
|
||||
return einzelpreisNetto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Netto-Einzelpreis des Produkts[cite: 55, 393].
|
||||
* * @param einzelpreisNetto Der neue Netto-Preis (muss numerisch >= 0.00 sein) [cite: 55]
|
||||
*/
|
||||
public void setEinzelpreisNetto(BigDecimal einzelpreisNetto) {
|
||||
this.einzelpreisNetto = einzelpreisNetto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den hinterlegten Mehrwertsteuersatz des Artikels zurück[cite: 55, 393].
|
||||
* * @return Der mwstSatz als Enum-Konstante (NORMAL oder ERMAESSIGT) [cite: 55]
|
||||
*/
|
||||
public MwstSatz getMwstSatz() {
|
||||
return mwstSatz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Mehrwertsteuersatz des Artikels[cite: 55, 393].
|
||||
* * @param mwstSatz Der anzuwendende Steuersatz [cite: 55]
|
||||
*/
|
||||
public void setMwstSatz(MwstSatz mwstSatz) {
|
||||
this.mwstSatz = mwstSatz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die optionale Beschreibung des Artikels zurück[cite: 55, 394].
|
||||
* * @return Der Beschreibungstext oder null, falls nicht angegeben [cite: 55]
|
||||
*/
|
||||
public String getBeschreibung() {
|
||||
return beschreibung;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den optionalen Beschreibungstext[cite: 55, 394].
|
||||
* * @param beschreibung Freitext zur Produktbeschreibung (max. 500 Zeichen) [cite: 55]
|
||||
*/
|
||||
public void setBeschreibung(String beschreibung) {
|
||||
this.beschreibung = beschreibung;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die optionale Produktkategorie zurück[cite: 55, 394].
|
||||
* * @return Die Kategoriebezeichnung als String [cite: 55]
|
||||
*/
|
||||
public String getKategorie() {
|
||||
return kategorie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die optionale Produktkategorie[cite: 55, 394].
|
||||
* * @param kategorie Name der Kategorie zur Gruppierung [cite: 55]
|
||||
*/
|
||||
public void setKategorie(String kategorie) {
|
||||
this.kategorie = kategorie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine String-Repräsentation des Produkt-Objekts für Debugging- und Testzwecke.
|
||||
* * @return Ein String mit den wichtigsten Attributwerten des Produkts
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Product{" +
|
||||
"productId='" + productId + '\'' +
|
||||
", bezeichnung='" + bezeichnung + '\'' +
|
||||
", einzelpreisNetto=" + einzelpreisNetto +
|
||||
", mwstSatz=" + mwstSatz +
|
||||
", kategorie='" + kategorie + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
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("==================================================");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package de.hsmannheim.faktura.product.repository;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Datenhaltungsschicht (Persistenz) für das Modul Produktverwaltung (Gruppe G)[cite: 51, 71].
|
||||
* Realisiert die datenbanklose Speicherung von Produkt-Entitäten im lokalen Dateisystem[cite: 13, 71].
|
||||
* Um die Antwortzeiten (NF-SH-PERF-01) zu minimieren, werden alle Lesezugriffe über einen
|
||||
* In-Memory-Arbeitsspeicher-Cache abgewickelt, welcher synchron zur JSON-Datei gehalten wird[cite: 16, 44, 71].
|
||||
* Folgt den Architekturvorgaben NF-SH-MAINT-01 (Schichtentrennung) und NF-SH-ARCH-01 (Verlustfreie Speicherung)[cite: 50, 51].
|
||||
*/
|
||||
public class ProductRepository {
|
||||
|
||||
/**
|
||||
* Relativer Systempfad zur lokalen JSON-Datei im Projektverzeichnis[cite: 16, 61].
|
||||
*/
|
||||
private static final String FILE_PATH = "./data/products.json";
|
||||
|
||||
/**
|
||||
* Zentraler Serialisierungs-Dienst des Jackson-Frameworks für die JSON-Objekt-Abbildung.
|
||||
*/
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Lokaler Cache im Arbeitsspeicher zur Reduzierung von I/O-Festplattenzugriffen[cite: 16, 71].
|
||||
*/
|
||||
private List<Product> cache;
|
||||
|
||||
/**
|
||||
* Initialisiert das Repository, konfiguriert das JSON-Ausgabeformat und
|
||||
* lädt den bestehenden Datenbestand direkt beim Anwendungsstart in den Cache[cite: 16, 36].
|
||||
*/
|
||||
public ProductRepository() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
|
||||
// Aktiviert das Pretty-Printing für eine menschenlesbare Strukturierung der JSON-Datei
|
||||
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
this.cache = new ArrayList<>();
|
||||
|
||||
loadData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine entkoppelte Kopie des aktuellen Zustands aller Produkte zurück.
|
||||
* Schützt den internen Cache vor unkontrollierten Modifikationen von außen.
|
||||
*
|
||||
* @return Eine {@link List} aller im System registrierten {@link Product}-Objekte[cite: 36, 87].
|
||||
*/
|
||||
public List<Product> findAll() {
|
||||
return new ArrayList<>(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht ein Produkt anhand des eindeutigen Identifikators im Cache[cite: 34, 103].
|
||||
*
|
||||
* @param productId Der eindeutige Primärschlüssel des Produkts [cite: 34, 392]
|
||||
* @return Ein {@link Optional}, das die Produkt-Entität umschließt, sofern gefunden [cite: 88, 99]
|
||||
*/
|
||||
public Optional<Product> findById(String productId) {
|
||||
return cache.stream()
|
||||
.filter(p -> p.getProductId().equals(productId))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert oder aktualisiert ein Produkt im System (Idempotente Operation).
|
||||
* Existiert die ID bereits, wird der alte Datensatz im Cache überschrieben[cite: 29].
|
||||
* Andernfalls wird ein neuer Eintrag angelegt[cite: 17, 28].
|
||||
* Nach der Cache-Modifikation wird der Zustand sofort atomar in der JSON-Datei persistiert[cite: 29, 61].
|
||||
*
|
||||
* @param product Die zu speichernde oder zu aktualisierende Produkt-Entität [cite: 28, 29, 89]
|
||||
*/
|
||||
public void save(Product product) {
|
||||
Optional<Product> existingProduct = findById(product.getProductId());
|
||||
|
||||
if (existingProduct.isPresent()) {
|
||||
int index = cache.indexOf(existingProduct.get());
|
||||
cache.set(index, product);
|
||||
} else {
|
||||
cache.add(product);
|
||||
}
|
||||
|
||||
saveData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt ein Produkt anhand seiner ID aus dem lokalen Cache und der Datei[cite: 17, 61].
|
||||
* Der Aufruf sollte erst nach erfolgreicher Referenzprüfung (F-SH-PV-03) erfolgen[cite: 33, 34].
|
||||
*
|
||||
* @param productId Der eindeutige Identifikator des zu löschenden Produkts [cite: 34, 98]
|
||||
* @return {@code true}, wenn das Produkt im Cache existierte und entfernt wurde; andernfalls {@code false} [cite: 90]
|
||||
*/
|
||||
public boolean deleteById(String productId) {
|
||||
boolean removed = cache.removeIf(p -> p.getProductId().equals(productId));
|
||||
if (removed) {
|
||||
saveData();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialisiert den Inhalt der lokalen JSON-Datei in den Arbeitsspeicher[cite: 16, 61].
|
||||
* Erstellt eine leere Struktur, falls die Datei noch nicht existiert oder korrupt ist,
|
||||
* um Ausfälle während des Programmstarts (Kaltstart-Performance) zu verhindern[cite: 375].
|
||||
*/
|
||||
private void loadData() {
|
||||
File file = new File(FILE_PATH);
|
||||
|
||||
if (!file.exists()) {
|
||||
this.cache = new ArrayList<>();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.cache = objectMapper.readValue(file, new TypeReference<List<Product>>() {});
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Laden der Produktdaten: " + e.getMessage());
|
||||
this.cache = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialisiert den gesamten Arbeitsspeicher-Cache synchron zurück in das Dateisystem[cite: 61, 71].
|
||||
* Stellt automatisch sicher, dass die übergeordneten Verzeichnisstrukturen (`./data/`)
|
||||
* existieren, bevor der Schreibvorgang initiiert wird.
|
||||
*/
|
||||
private void saveData() {
|
||||
File file = new File(FILE_PATH);
|
||||
|
||||
File directory = file.getParentFile();
|
||||
if (directory != null && !directory.exists()) {
|
||||
directory.mkdirs();
|
||||
}
|
||||
|
||||
try {
|
||||
objectMapper.writeValue(file, cache);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Speichern der Produktdaten: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package de.hsmannheim.faktura.product.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||
|
||||
/**
|
||||
* Service-Klasse zur Bereitstellung der Geschäftslogik für die Produktverwaltung (Gruppe G).
|
||||
* Dient als zentrale programminterne API für die Schnittstellen zur Programmoberfläche (Gruppe E)
|
||||
* und den Dokumentenprozess (Gruppe F).
|
||||
* Folgt der Vorgabe NF-MAIN-02 für die Quellcodedokumentation.
|
||||
*/
|
||||
public class ProductService {
|
||||
|
||||
/**
|
||||
* Referenz auf das Datenhaltungs-Repository zur JSON-Persistierung.
|
||||
*/
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
/**
|
||||
* Referenz auf das Dokumenten-Repository zur Durchführung der Referenzprüfung.
|
||||
*/
|
||||
private final DokumentRepository dokumentRepository;
|
||||
|
||||
/**
|
||||
* Konstruktor zur Erstellung des ProductService mittels Dependency Injection.
|
||||
*
|
||||
* @param productRepository Das zu verwendende Repository für Datenzugriffe
|
||||
* @param dokumentRepository Das zu verwendende Repository für Dokumentendaten
|
||||
*/
|
||||
public ProductService(ProductRepository productRepository, DokumentRepository dokumentRepository) {
|
||||
this.productRepository = productRepository;
|
||||
this.dokumentRepository = dokumentRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine tabellarische Liste aller im System hinterlegten Produkte zurück.
|
||||
* Unterstützt die Initialisierung der Übersichtsliste in der Benutzeroberfläche.
|
||||
*
|
||||
* @return Eine Liste von {@link Product}-Objekten.
|
||||
*/
|
||||
public List<Product> getAllProducts() {
|
||||
return productRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach einem spezifischen Produkt anhand seiner eindeutigen ID.
|
||||
*
|
||||
* @param productId Die eindeutige Kennung des gesuchten Produkts
|
||||
* @return Ein {@link Optional}, das das gefundene Produkt enthält, oder leer ist
|
||||
*/
|
||||
public Optional<Product> getProductById(String productId) {
|
||||
return productRepository.findById(productId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert ein neues Produkt oder aktualisiert einen bestehenden Datensatz.
|
||||
* * @param product Das zu speichernde oder zu aktualisierende {@link Product}-Objekt
|
||||
* @throws IllegalArgumentException Wenn Validierungsregeln aus Kapitel 6.1.1 verletzt werden
|
||||
*/
|
||||
public void saveProduct(Product product) {
|
||||
if (product == null) {
|
||||
throw new IllegalArgumentException("Das Produkt darf nicht null sein.");
|
||||
}
|
||||
|
||||
if (product.getBezeichnung() == null || product.getBezeichnung().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Anforderung F-SH-PV-01 verletzt: Die Bezeichnung ist ein Pflichtfeld.");
|
||||
}
|
||||
|
||||
if (product.getBezeichnung().length() > 100) {
|
||||
throw new IllegalArgumentException("Anforderung F-SH-PV-01 verletzt: Die Bezeichnung darf maximal 100 Zeichen lang sein.");
|
||||
}
|
||||
|
||||
if (product.getEinzelpreisNetto() == null || product.getEinzelpreisNetto().compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new IllegalArgumentException("Anforderung F-SH-PV-06 verletzt: Der Einzelpreis Netto darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
if (product.getBeschreibung() != null && product.getBeschreibung().length() > 500) {
|
||||
throw new IllegalArgumentException("Anforderung F-PV-07 verletzt: Die Beschreibung darf maximal 500 Zeichen lang sein.");
|
||||
}
|
||||
|
||||
if (product.getProductId() == null || product.getProductId().trim().isEmpty()) {
|
||||
product.setProductId("PROD-" + java.util.UUID.randomUUID().toString().substring(0, 8));
|
||||
}
|
||||
|
||||
productRepository.save(product);
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht ein Produkt permanent aus dem Datenbestand anhand seiner ID.
|
||||
* * @param productId Die eindeutige ID des zu löschenden Produkts
|
||||
* @return {@code true}, wenn das Produkt erfolgreich gelöscht wurde; {@code false}, wenn die ID leer war
|
||||
* oder der Löschvorgang blockiert wurde
|
||||
*/
|
||||
public boolean deleteProduct(String productId) {
|
||||
if (productId == null || productId.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String cleanId = productId.replace("PROD-", "").replace("P-", "").trim();
|
||||
long idAsLong = Long.parseLong(cleanId);
|
||||
|
||||
boolean isReferenced = dokumentRepository.existsReferencedProdukt(idAsLong);
|
||||
|
||||
if (isReferenced) {
|
||||
return false;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("ID-Format ungültig für Referenzprüfung: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return productRepository.deleteById(productId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtert die Produktliste in Echtzeit basierend auf einem Suchbegriff.
|
||||
* * @param query Der eingegebene Suchbegriff für die Live-Filterung
|
||||
* @return Eine Liste der gefilterten Produkte, die dem Suchkriterium entsprechen
|
||||
*/
|
||||
public List<Product> searchProducts(String query) {
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
return getAllProducts();
|
||||
}
|
||||
String lowerQuery = query.toLowerCase();
|
||||
return getAllProducts().stream()
|
||||
.filter(p -> p.getBezeichnung() != null && p.getBezeichnung().toLowerCase().contains(lowerQuery))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package de.hsmannheim.faktura.product.repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.hsmannheim.faktura.product.MwstSatz;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
|
||||
/**
|
||||
* Testklasse für das ProductRepository (Gruppe G).
|
||||
* Überprüft die korrekte Tabelleninitialisierung und die verlustfreie
|
||||
* dateibasierte Persistenz der Stammdaten (NF-SH-ARCH-01).
|
||||
*/
|
||||
public class ProductRepositoryTest {
|
||||
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
productRepository = new ProductRepository();
|
||||
|
||||
for (Product p : productRepository.findAll()) {
|
||||
productRepository.deleteById(p.getProductId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet die tabellarische Bereitstellung aller Produkte für die Übersichtsliste.
|
||||
* Abdeckung von Anforderung: F-SH-PV-04.
|
||||
*/
|
||||
@Test
|
||||
public void testGetAllAndSorting() {
|
||||
Product p1 = new Product("P-0001", "A-Schraube", new BigDecimal("1.50"), MwstSatz.NORMAL, "", "");
|
||||
Product p2 = new Product("P-0002", "B-Mutter", new BigDecimal("0.80"), MwstSatz.NORMAL, "", "");
|
||||
|
||||
productRepository.save(p1);
|
||||
productRepository.save(p2);
|
||||
|
||||
List<Product> allProducts = productRepository.findAll();
|
||||
assertEquals(2, allProducts.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet die verlustfreie Wiederherstellung der Produktdaten aus dem JSON-Speicher nach einem Systemneustart.
|
||||
* Abdeckung von Anforderung: NF-SH-ARCH-01.
|
||||
*/
|
||||
@Test
|
||||
public void testPersistenceOnRestart() {
|
||||
Product product = new Product("P-1000", "Persistenz-Test", new BigDecimal("49.99"), MwstSatz.NORMAL, "Sicherung auf Dateiebene", "Test");
|
||||
productRepository.save(product);
|
||||
|
||||
// Simulation des Anwendungs-Neustarts durch Neuinitialisierung des Repositories
|
||||
ProductRepository newRepositoryInstance = new ProductRepository();
|
||||
|
||||
assertTrue(newRepositoryInstance.findById("P-1000").isPresent());
|
||||
assertEquals("Persistenz-Test", newRepositoryInstance.findById("P-1000").get().getBezeichnung());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package de.hsmannheim.faktura.product.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
import de.hsmannheim.faktura.product.MwstSatz;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||
|
||||
/**
|
||||
* Performancetest-Klasse für die Produktverwaltung (Gruppe G).
|
||||
* Verifiziert die Zeitschranken beim Laden und Filtern von großen Datenmengen (NF-SH-PERF-01).
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
public class ProductPerformanceTest {
|
||||
|
||||
/**
|
||||
* Überprüft, ob das Durchsuchen und Echtzeit-Filtern bei einem Datenbestand von 1.000
|
||||
* Produkten in weniger als 2 Sekunden vollständig abgeschlossen wird.
|
||||
* Abdeckung von Anforderung: NF-SH-PERF-01.
|
||||
*/
|
||||
@Test
|
||||
public void testSearchPerformance() {
|
||||
ProductRepository repo = new ProductRepository();
|
||||
DokumentRepository dummyDocRepo = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
|
||||
@Override public boolean existsReferencedProdukt(long id) { return false; }
|
||||
@Override public boolean existsReferencedKunde(long id) { return false; }
|
||||
@Override public int findMaxLaufendeNummer(String p, int j) { return 0; }
|
||||
@Override public void save(de.hsmannheim.faktura.dokumente.domain.Dokument d) {}
|
||||
@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 boolean existsFolgedokument(String q) { return false; }
|
||||
};
|
||||
|
||||
ProductService service = new ProductService(repo, dummyDocRepo);
|
||||
|
||||
// Bereinigen und künstliches Befüllen mit exakt 1.000 Produkten
|
||||
for (Product p : repo.findAll()) {
|
||||
repo.deleteById(p.getProductId());
|
||||
}
|
||||
|
||||
for (int i = 1; i <= 1000; i++) {
|
||||
repo.save(new Product("P-" + i, "Produkt Nummer " + i, new BigDecimal("10.00"), MwstSatz.NORMAL, "", "Kategorie"));
|
||||
}
|
||||
|
||||
// Zeitmessung starten
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
List<Product> results = service.searchProducts("Nummer 999");
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
long duration = endTime - startTime;
|
||||
|
||||
// Überprüfung der Zeitschranke (2000 Millisekunden = 2 Sekunden)
|
||||
assertTrue(duration < 2000, "Der Lasttest wurde nicht in der geforderten Zeit von 2 Sekunden erfüllt. Benötigt: " + duration + "ms");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
package de.hsmannheim.faktura.product.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
||||
import de.hsmannheim.faktura.product.MwstSatz;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||
|
||||
/**
|
||||
* Komponententest-Klasse für den ProductService (Gruppe G).
|
||||
* Überprüft die Einhaltung der funktionalen Anforderungen und Geschäftsregeln
|
||||
* aus dem Pflichtenheft vollständig ohne GUI-Abhängigkeiten (NF-TEST-02).
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
public class ProductServiceTest {
|
||||
|
||||
private ProductService productService;
|
||||
private ProductRepository productRepository;
|
||||
private de.hsmannheim.faktura.dokumente.repository.DokumentRepository dokumentRepository;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
productRepository = new ProductRepository();
|
||||
|
||||
dokumentRepository = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
|
||||
@Override
|
||||
public boolean existsReferencedProdukt(long produktId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsReferencedKunde(long kundenId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findMaxLaufendeNummer(String praefix, int jahr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Dokument dokument) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'save'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Dokument> findByNummer(String nummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'findByNummer'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Dokument> findByKunde(long kundenId) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'findByKunde'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsFolgedokument(String quellDokumentNummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
|
||||
}
|
||||
};
|
||||
|
||||
productService = new ProductService(productRepository, dokumentRepository);
|
||||
|
||||
for (Product p : productRepository.findAll()) {
|
||||
productRepository.deleteById(p.getProductId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet das erfolgreiche Anlegen eines validen Produkts mit allen Pflichtattributen.
|
||||
* Abdeckung von Anforderung: F-SH-PV-01.
|
||||
*/
|
||||
@Test
|
||||
public void testSaveValidProduct() {
|
||||
Product valid = new Product("P-0001", "M6 Schraube", new BigDecimal("10.50"), MwstSatz.NORMAL, "Beschreibung", "Werkzeug");
|
||||
|
||||
assertDoesNotThrow(() -> productService.saveProduct(valid));
|
||||
assertTrue(productService.getProductById("P-0001").isPresent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet das erfolgreiche Modifizieren und Überschreiben bestehender Produktdatensätze.
|
||||
* Abdeckung von Anforderung: F-SH-PV-02.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateProductAttributes() {
|
||||
Product product = new Product("P-0001", "Alte Bezeichnung", new BigDecimal("5.00"), MwstSatz.NORMAL, "Alt", "Alt");
|
||||
productService.saveProduct(product);
|
||||
|
||||
Product updatedProduct = new Product("P-0001", "Neue Bezeichnung", new BigDecimal("12.50"), MwstSatz.ERMAESSIGT, "Neu", "Neu");
|
||||
productService.saveProduct(updatedProduct);
|
||||
|
||||
Optional<Product> result = productService.getProductById("P-0001");
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("Neue Bezeichnung", result.get().getBezeichnung());
|
||||
assertEquals(new BigDecimal("12.50"), result.get().getEinzelpreisNetto());
|
||||
assertEquals(MwstSatz.ERMAESSIGT, result.get().getMwstSatz());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob Preise kleiner als 0.00 wie gefordert blockiert und mit einer Exception abgewiesen werden.
|
||||
* Abdeckung von Anforderung: F-SH-PV-06 / AC-SH-PV-01.
|
||||
*/
|
||||
@Test
|
||||
public void testNegativePriceRejection() {
|
||||
Product invalidProduct = new Product("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
productService.saveProduct(invalidProduct);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob Beschreibungen mit mehr als 500 Zeichen blockiert und mit einer Exception abgewiesen werden.
|
||||
* Abdeckung von Anforderung: F-PV-07.
|
||||
*/
|
||||
@Test
|
||||
public void testDescriptionLengthBounds() {
|
||||
StringBuilder longDescription = new StringBuilder();
|
||||
for (int i = 0; i < 501; i++) {
|
||||
longDescription.append("A");
|
||||
}
|
||||
|
||||
Product invalidProduct = new Product("P-0002", "Produkt mit langer Beschreibung", new BigDecimal("10.00"), MwstSatz.NORMAL, longDescription.toString(), "Test");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
productService.saveProduct(invalidProduct);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die optionale Kategoriezuweisung korrekt verarbeitet und persistent im Objekt hinterlegt wird.
|
||||
* Abdeckung von Anforderung: F-PV-08.
|
||||
*/
|
||||
@Test
|
||||
public void testCategoryAssignment() {
|
||||
Product product = new Product("P-0003", "Bohrmaschine", new BigDecimal("89.99"), MwstSatz.NORMAL, "Schlagbohrer", "Elektrowerkzeuge");
|
||||
productService.saveProduct(product);
|
||||
|
||||
Optional<Product> result = productService.getProductById("P-0003");
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("Elektrowerkzeuge", result.get().getKategorie());
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft die Echtzeit-Filterung der Produktliste anhand einer Teilmenge des Namens,
|
||||
* wobei Groß- und Kleinschreibung ignoriert werden muss.
|
||||
* Abdeckung von Anforderung: F-SH-PV-05 / AC-SH-PV-03.
|
||||
*/
|
||||
@Test
|
||||
public void testSearchCaseInsensitive() {
|
||||
Product p1 = new Product("P-0001", "M6 Schraube", new BigDecimal("1.00"), MwstSatz.NORMAL, "", "");
|
||||
Product p2 = new Product("P-0002", "M8 SCHRAUBE", new BigDecimal("2.00"), MwstSatz.NORMAL, "", "");
|
||||
|
||||
productService.saveProduct(p1);
|
||||
productService.saveProduct(p2);
|
||||
|
||||
List<Product> results = productService.searchProducts("schraube");
|
||||
assertEquals(2, results.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft die Blockierung des Löschvorgangs, falls das Produkt in Belegen referenziert wird.
|
||||
* Abdeckung von Geschäftsregel: GR-05 / AC-SH-PV-02 / F-SH-PV-03.
|
||||
*/
|
||||
@Test
|
||||
public void testDeleteReferencedProduct() {
|
||||
de.hsmannheim.faktura.dokumente.repository.DokumentRepository mockRepo = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
|
||||
@Override
|
||||
public boolean existsReferencedProdukt(long produktId) {
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public boolean existsReferencedKunde(long kundenId) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int findMaxLaufendeNummer(String praefix, int jahr) {
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public void save(Dokument dokument) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'save'");
|
||||
}
|
||||
@Override
|
||||
public Optional<Dokument> findByNummer(String nummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'findByNummer'");
|
||||
}
|
||||
@Override
|
||||
public List<Dokument> findByKunde(long kundenId) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'findByKunde'");
|
||||
}
|
||||
@Override
|
||||
public boolean existsFolgedokument(String quellDokumentNummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
|
||||
}
|
||||
};
|
||||
|
||||
ProductService serviceWithSperre = new ProductService(productRepository, mockRepo);
|
||||
Product product = new Product("P-0001", "Gesperrtes Produkt", new BigDecimal("10.00"), MwstSatz.NORMAL, "", "");
|
||||
productRepository.save(product);
|
||||
|
||||
boolean result = serviceWithSperre.deleteProduct("P-0001");
|
||||
assertFalse(result, "Das Löschen hätte blockiert werden müssen, da das Produkt referenziert wird.");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue