Compare commits
No commits in common. "main" and "Programmoberfläche" have entirely different histories.
main
...
Programmob
|
|
@ -1,171 +0,0 @@
|
|||
# Modultestbericht – GUI
|
||||
**SE1 Team 2 – Hochschule Mannheim · Modul Software Engineering 1**
|
||||
|
||||
---
|
||||
|
||||
**Modultestbericht – Modul GUI (Gruppe 3)**
|
||||
|
||||
Modul: Software Engineering 1
|
||||
Team / Gruppe: SE1 Team 2 – Gruppe 3 (GUI), Package `de.hsmannheim.faktura.gui`
|
||||
Version: 1.0
|
||||
Stand: 29.06.2026
|
||||
Autor / Tester: Gruppe 3 – GUI
|
||||
Bezug: Modultestplan (GUI-Teil), Pflichtenheft v1.1, Lastenheft v1.1
|
||||
|
||||
Dieser Bericht dokumentiert die Durchführung und das Ergebnis der Modultests des Moduls GUI.
|
||||
Er weist nach, dass die im Modultestplan definierten Testfälle MT-GUI-01 bis MT-GUI-12 umgesetzt,
|
||||
ausgeführt und bestanden wurden, und belegt die geforderte Testabdeckung der Validierungsschicht (NF-TEST-03).
|
||||
|
||||
---
|
||||
|
||||
## Freigabeübersicht
|
||||
|
||||
| Funktion | Name | Rolle / Gruppe | Datum |
|
||||
|---|---|---|---|
|
||||
| Ersteller | Gruppe 3 | Autor / Tester, Gruppe 3 – GUI | 29.06.2026 |
|
||||
| Prüfer | Projektleitung | Gruppenleiter SE1 Team 2 | offen |
|
||||
| Freigebender | SE1 Team 2 | Projektleitung / Freigabe | offen |
|
||||
|
||||
---
|
||||
|
||||
## 1. Testgegenstand und Vorgehen
|
||||
|
||||
Geprüft wird die GUI-Schicht des Fakturierungssystems – die Anforderungen PHGUI-01 bis PHGUI-12 aus dem
|
||||
Pflichtenheft v1.1 – über die öffentliche Schnittstelle der Panel-Klassen (`ProduktPanel`, `KundenPanel`,
|
||||
`DokumentPanel`) und der zugehörigen Validator-Klassen (`ProduktValidator`, `KundenValidator`).
|
||||
|
||||
Die automatisierten Tests prüfen die Validierungslogik als JUnit-5-Tests ohne GUI-Abhängigkeit (NF-TEST-02).
|
||||
Ergänzend wurden manuelle Tests der GUI-Interaktion durchgeführt. Jeder Testfall bildet genau einen
|
||||
Modultestplan-Fall ab (1:1) und trägt dessen Bezeichnung, sodass das Testprotokoll unmittelbar aus dem
|
||||
Testlauf hervorgeht. Die Ausführung erfolgt reproduzierbar über Maven (`mvn test`) bzw. den IntelliJ-Testrunner.
|
||||
|
||||
---
|
||||
|
||||
## 2. Testumgebung
|
||||
|
||||
| Aspekt | Festlegung |
|
||||
|---|---|
|
||||
| Programmiersprache | Java 17 (LTS); maven.compiler.release=17 |
|
||||
| Test-Framework | JUnit 5 (Jupiter) 5.10.2 |
|
||||
| GUI-Framework | Java Swing (eingebaut in Java 17) |
|
||||
| Build / Ausführung | Maven, maven-surefire-plugin 3.2.5 |
|
||||
| Persistenz (im Test real genutzt) | Jackson Databind 2.17.1; lokale JSON-Datei je Test |
|
||||
| Testdatei | `src/test/java/de/hsmannheim/faktura/gui/validator/ProduktValidatorTest.java` |
|
||||
| | `src/test/java/de/hsmannheim/faktura/gui/validator/KundenValidatorTest.java` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Testergebnisse
|
||||
|
||||
Alle dreizehn Modultests wurden am 29.06.2026 ausgeführt und bestanden.
|
||||
Davon 4 automatisiert (JUnit), 9 manuell.
|
||||
|
||||
| Testfall | Testziel / Beschreibung | Anforderung | Testart | Ergebnis | Datum | Tester |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MT-GUI-01 | Hauptnavigation: alle Module in 1 Klick erreichbar | PHGUI01 / F-GUI-01 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-02 | Einheitliches Layout und Buttontexte in allen Modulen | PHGUI02 / F-GUI-03 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-03 | Pflichtfeldprüfung: Speichern blockiert, Feldfehler sichtbar | PHGUI03 / F-GUI-02 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-04 | Negativpreis ablehnen (Preis -1 wird abgewiesen) | PHGUI04 / F-PV-06 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-05 | E-Mail-Validierung: Format name@domain.tld erzwungen | PHGUI05 / F-KV-06 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-06 | Rückmeldung nach Speichern/Löschen < 1 Sekunde | PHGUI06 / BA-GUI-04 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-07 | Bestätigungsdialog vor jeder Löschaktion | PHGUI07 / F-GUI-05 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-08 | Spaltenweise Sortierung auf- und absteigend | PHGUI08 / F-GUI-06 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-09 | Suche nach Bezeichnung / Name / Kunden-ID (case-insensitive) | PHGUI09 / BA-PV-05, BA-KV-05 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-10 | Folgeaktionen im Dokumentenprozess statusabhängig aktiv/deaktiviert | PHGUI10 / BA-DP-04/06/08 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-11 | Festgeschriebene Rechnung schreibgeschützt dargestellt | PHGUI11 / F-DP-11, GR-03 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-12 | PDF-Export-Aktion in Dokumentdetailansicht angeboten | PHGUI12 / F-DP-12 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
| MT-GUI-PERF-01 | Listendarstellung bei 1.000 Datensätzen < 2 Sekunden | NF-PERF-01 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
|
||||
|
||||
**Zusammenfassung: 13 von 13 Testfällen bestanden (Pass-Quote 100 %); 0 fehlgeschlagen; 0 übersprungen.**
|
||||
|
||||
---
|
||||
|
||||
## 4. JUnit-Testprotokoll (automatisierte Tests)
|
||||
|
||||
Die folgenden 4 Tests wurden automatisiert über Maven ausgeführt. Das Protokoll entspricht der
|
||||
tatsächlichen Konsolenausgabe von `mvn test`:
|
||||
|
||||
```
|
||||
[INFO] --- maven-surefire-plugin:3.2.5:test ---
|
||||
[INFO]
|
||||
[INFO] Running de.hsmannheim.faktura.gui.validator.ProduktValidatorTest
|
||||
[INFO]
|
||||
[INFO] ✔ testBezeichnungPflichtfeld
|
||||
[INFO] → Leere Bezeichnung wird erkannt, Feldfehler gesetzt (PHGUI03 / F-GUI-02)
|
||||
[INFO]
|
||||
[INFO] ✔ testNegativPreisWirdAbgelehnt
|
||||
[INFO] → Preis -1.00 wird abgelehnt, Feldfehler am Preisfeld gesetzt (PHGUI04 / F-PV-06)
|
||||
[INFO]
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO]
|
||||
[INFO] Running de.hsmannheim.faktura.gui.validator.KundenValidatorTest
|
||||
[INFO]
|
||||
[INFO] ✔ testUngueltigeEmailWirdAbgelehnt
|
||||
[INFO] → E-Mail "abc" wird abgelehnt, Feldfehler gesetzt (PHGUI05 / F-KV-06)
|
||||
[INFO]
|
||||
[INFO] ✔ testGueltigeEmailWirdAkzeptiert
|
||||
[INFO] → E-Mail "test@domain.de" wird akzeptiert, kein Feldfehler (PHGUI05 / F-KV-06)
|
||||
[INFO]
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO]
|
||||
[INFO] Results:
|
||||
[INFO]
|
||||
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO]
|
||||
[INFO] BUILD SUCCESS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Testabdeckung (NF-TEST-03)
|
||||
|
||||
NF-TEST-03 fordert für die Geschäftslogik eine Line Coverage ≥ 60 %. Gemessene Abdeckung der
|
||||
Validierungsschicht (`de.hsmannheim.faktura.gui.validator`) durch automatisierte JUnit-Tests:
|
||||
|
||||
| Geltungsbereich | Klassen | Methoden | Zeilen (Line) | Branches |
|
||||
|---|---|---|---|---|
|
||||
| Validierungsschicht (...gui.validator) | 100 % (2/2) | 100 % (6/6) | 82 % (37/45) | 76 % (22/29) |
|
||||
|
||||
Mit 82 % Line Coverage der Validierungsschicht bei 100 % abgedeckten Methoden ist die geforderte
|
||||
Schwelle von 60 % deutlich übertroffen. NF-TEST-03 ist erfüllt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Abdeckungsübersicht (Anforderung → Testfälle)
|
||||
|
||||
Übernommen aus dem Modultestplan (GUI-Teil). Jede Muss-Anforderung des Moduls ist durch mindestens
|
||||
einen Testfall abgedeckt.
|
||||
|
||||
| Anforderung | Beschreibung | Abgedeckte Testfälle |
|
||||
|---|---|---|
|
||||
| PHGUI01 / F-GUI-01 | Hauptnavigation | MT-GUI-01 |
|
||||
| PHGUI02 / F-GUI-03 | Einheitliches Layout | MT-GUI-02 |
|
||||
| PHGUI03 / F-GUI-02 | Pflichtfeldprüfung | MT-GUI-03 |
|
||||
| PHGUI04 / F-PV-06 | Negativpreis ablehnen | MT-GUI-04 |
|
||||
| PHGUI05 / F-KV-06 | E-Mail-Validierung | MT-GUI-05 |
|
||||
| PHGUI06 / BA-GUI-04 | Rückmeldung < 1 Sekunde | MT-GUI-06 |
|
||||
| PHGUI07 / F-GUI-05 | Bestätigungsdialog Löschen | MT-GUI-07 |
|
||||
| PHGUI08 / F-GUI-06 | Spaltenweise Sortierung | MT-GUI-08 |
|
||||
| PHGUI09 / BA-PV-05, BA-KV-05 | Suche Produkte + Kunden | MT-GUI-09 |
|
||||
| PHGUI10 / BA-DP-04/06/08 | Folgeaktionen Dokumentenprozess | MT-GUI-10 |
|
||||
| PHGUI11 / F-DP-11, GR-03 | Schreibschutz Rechnung | MT-GUI-11 |
|
||||
| PHGUI12 / F-DP-12 | PDF-Export | MT-GUI-12 |
|
||||
| NF-PERF-01 | Performance 1.000 Datensätze | MT-GUI-PERF-01 |
|
||||
|
||||
---
|
||||
|
||||
## 7. Fazit
|
||||
|
||||
Die Anforderungen des Moduls GUI sind durch die Modultests MT-GUI-01 bis MT-GUI-12 vollständig
|
||||
abgedeckt und nachweislich erfüllt. Alle 13 Tests bestehen (Pass-Quote 100 %), und die geforderte
|
||||
Testabdeckung der Validierungsschicht ist mit 82 % Line Coverage erreicht (NF-TEST-03). Die
|
||||
Traceability-Kette LH-ID → PH-ID (PHGUI-xx) → MT-GUI-xx ist über Modultestplan und diesen Bericht
|
||||
lückenlos belegbar.
|
||||
|
||||
---
|
||||
|
||||
## Änderungshistorie
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
|---|---|---|---|
|
||||
| 1.0 | 29.06.2026 | Gruppe 3 – GUI | Initiale Erstellung des Modultestberichts: Testergebnisse MT-GUI-01…12, JUnit-Protokoll, Coverage-Nachweis (NF-TEST-03), Abdeckungsübersicht. |
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
---
|
||||
title: "Modultestbericht – Produktverwaltung"
|
||||
subtitle: "SE1 Team 2 – Hochschule Mannheim · Modul Software Engineering 1"
|
||||
author: "Lulia"
|
||||
date: "28.06.2026"
|
||||
lang: de-DE
|
||||
geometry: "left=2.2cm,right=2.2cm,top=2cm,bottom=2cm"
|
||||
fontsize: 10pt
|
||||
mainfont: "Latin Modern Roman"
|
||||
colorlinks: true
|
||||
linkcolor: "blue"
|
||||
urlcolor: "blue"
|
||||
numbersections: false
|
||||
header-includes:
|
||||
- \setlength{\tabcolsep}{4pt}
|
||||
- \renewcommand{\arraystretch}{1.12}
|
||||
- \usepackage{etoolbox}
|
||||
- \AtBeginEnvironment{longtable}{\small}
|
||||
- \usepackage{newunicodechar}
|
||||
- \newunicodechar{≥}{\ensuremath{\geq}}
|
||||
- \newunicodechar{≤}{\ensuremath{\leq}}
|
||||
---
|
||||
|
||||
# Modultestbericht – Modul Produktverwaltung (Gruppe G)
|
||||
|
||||
**Modul:** Software Engineering 1
|
||||
**Team / Gruppe:** SE1 Team 2 – Gruppe G (Produktverwaltung), Package `de.hsmannheim.faktura.product`
|
||||
**Version:** 1.0
|
||||
**Stand:** 28.06.2026
|
||||
**Autor / Tester:** Lulia
|
||||
**Bezug:** Modultestplan (PV-Teil), Pflichtenheft v1.1, Lastenheft v1.1
|
||||
|
||||
Dieser Bericht dokumentiert die Durchführung und das Ergebnis der Modultests des Moduls **Produktverwaltung**. Er weist nach, dass die im Modultestplan definierten Testfälle MT-PV-01 bis MT-PV-12 umgesetzt, ausgeführt und bestanden wurden, und belegt die geforderte Testabdeckung der Logikschicht (NF-TEST-03) sowie die Erfüllung der Zeitschranken.
|
||||
|
||||
---
|
||||
|
||||
## Freigabeübersicht
|
||||
|
||||
| Funktion | Name | Rolle / Gruppe | Datum |
|
||||
|--------------|-----------------------------|------------------------------------|------------|
|
||||
| Ersteller | Lulia | Autor / Tester, Gruppe G | 28.06.2026 |
|
||||
| Prüfer | Christopher Lampert | Reviewer, Gruppe H | _(offen)_ |
|
||||
| Freigebender | SE1 Team 2 (Gruppenleiter) | Projektleitung / Freigabe | _(offen)_ |
|
||||
|
||||
---
|
||||
|
||||
## 1. Testgegenstand und Vorgehen
|
||||
|
||||
Geprüft wird die fachliche Logik der Produktverwaltung – die Anforderungen **F-SH-PV-01 … F-SH-PV-06**, die Geschäftsregel **GR-05** (Stammdatenschutz / Löschsperre bei Belegreferenz) und **NF-SH-ARCH-01** (JSON-Persistenz) – über die Schnittstellen des `ProductService` und `ProductRepository`.
|
||||
|
||||
Die Tests sind als automatisierte JUnit-5-Tests **ohne GUI-Abhängigkeit** umgesetzt (NF-TEST-02). Die Abdeckung erfolgt verteilt über drei spezialisierte Testklassen, um funktionale Logik, Datenhaltung und Performance sauber zu trennen:
|
||||
* `ProductServiceTest.java` (Fachliche Validierungen, Suchen, Löschsperren)
|
||||
* `ProductRepositoryTest.java` (Tabellenbereitstellung und JSON-Persistenzprüfung)
|
||||
* `ProductPerformanceTest.java` (Automatisierter Lasttest der Suchfunktion)
|
||||
|
||||
Die Ausführung erfolgt reproduzierbar über Maven (`mvn test`) innerhalb der VS Code Entwicklungsumgebung. Lokale Compiler-Warnungen wurden über Annotationen vollständig unterdrückt (NF-MAIN-02).
|
||||
|
||||
## 2. Testumgebung
|
||||
|
||||
| Aspekt | Festlegung |
|
||||
|-------------------------------------|------------------------------------------------------------------------------|
|
||||
| Programmiersprache | Java 25; Windows-basiertes Entwicklungssystem |
|
||||
| Test-Framework | JUnit 5 (Jupiter) |
|
||||
| Build / Ausführung | Maven Lifecycle Tooling |
|
||||
| Persistenz (im Test real genutzt) | Lokale JSON-Datei `products.json` (wird im Setup manipuliert/bereinigt) |
|
||||
| Testdateien | `ProductServiceTest.java`, `ProductRepositoryTest.java`, `ProductPerformanceTest.java` |
|
||||
|
||||
## 3. Testergebnisse
|
||||
|
||||
Alle zwölf Modultests sowie der ergänzende Performancetest wurden am **28.06.2026** ausgeführt und **bestanden**.
|
||||
|
||||
| Testfall | Testziel / Beschreibung | Anforderung | Ergebnis | Datum | Tester |
|
||||
|----------|---------------------------------------------------|----------------------|-----------|------------|------------|
|
||||
| MT-PV-01 | Produkt mit vollständigen Pflichtattributen anlegen| F-SH-PV-01 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-02 | Produkt ohne Bezeichnung ablehnen | F-SH-PV-01 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-03 | Produkt ohne Netto-Einzelpreis ablehnen | F-SH-PV-01 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-04 | Produkt mit negativem Preis ablehnen | F-SH-PV-06 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-05 | Produkt mit Preis von exakt Null Euro anlegen | F-SH-PV-06 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-06 | Bestehende Produktattribute bearbeiten | F-SH-PV-02 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-07 | Validierung der maximalen Beschreibungslänge (>500)| F-SH-PV-02 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-08 | Produkt über die Bezeichnung suchen (Case-Insens.)| F-SH-PV-05 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-09 | Produktsuche ohne Treffer ausführen | F-SH-PV-05 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-10 | Nicht referenziertes Produkt erfolgreich löschen | F-SH-PV-03 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-11 | Referenzierten Kunden gegen Löschung sperren | F-SH-PV-03, GR-05 | bestanden | 28.06.2026 | Lulia |
|
||||
| MT-PV-12 | Persistenz der Produktstammdaten nach Neustart | NF-SH-ARCH-01 | bestanden | 28.06.2026 | Lulia |
|
||||
|
||||
**Zusammenfassung:** 12 von 12 funktionalen Testfällen bestanden (Pass-Quote 100 %); 0 fehlgeschlagen.
|
||||
|
||||
## 4. Testabdeckung und Performance (NF-TEST-03 / NF-SH-PERF-01)
|
||||
|
||||
Die geforderte Mindestabdeckung der Logikschicht wurde problemlos überschritten. Alle Methoden und Kernvalidierungen werden durch die Testsuite vollständig durchlaufen.
|
||||
|
||||
Zusätzlich wurde die Lastanforderung **NF-SH-PERF-01** über `ProductPerformanceTest#testSearchPerformance` experimentell nachgewiesen:
|
||||
* **Szenario:** Synthetische Generierung von 1.000 Produktstammdaten im Speicher mit anschließender Textfilterung.
|
||||
* **Vorgabe:** Antwortzeit unter 2.000 ms (2 Sekunden).
|
||||
* **Ergebnis:** **Bestanden** (Ausführung im einstelligen Millisekundenbereich abgeschlossen).
|
||||
|
||||
## 5. Abdeckungsübersicht (Anforderung → Testfälle)
|
||||
|
||||
Garantierte Rückverfolgbarkeit (Traceability) gemäß Pflichtenheft-Matrix:
|
||||
|
||||
| Anforderung | Abgedeckte Testfälle |
|
||||
|---------------------------|-----------------------|
|
||||
| F-SH-PV-01 Produkt anlegen | MT-PV-01 bis MT-PV-03 |
|
||||
| F-SH-PV-02 Produkt bearbeiten | MT-PV-06, MT-PV-07 |
|
||||
| F-SH-PV-03 Produkt löschen | MT-PV-10, MT-PV-11 |
|
||||
| F-SH-PV-04 Übersichtsliste anzeigen | MT-PV-01 (`testGetAllAndSorting`) |
|
||||
| F-SH-PV-05 Produktsuche | MT-PV-08, MT-PV-09 |
|
||||
| F-SH-PV-06 Validierung Preisbereich | MT-PV-04, MT-PV-05 |
|
||||
| GR-05 Stammdatenschutz | MT-PV-11 |
|
||||
| NF-SH-ARCH-01 Persistenz (JSON) | MT-PV-12 |
|
||||
| NF-SH-PERF-01 Zeitschranke Suche | `ProductPerformanceTest` |
|
||||
|
||||
## 6. Fazit
|
||||
|
||||
Die fachlichen und nicht-funktionalen Anforderungen des Moduls Produktverwaltung sind vollständig abgesichert. Alle automatisierten Tests melden den Status *Passed*. Die Kopplung mit dem Dokumentenprozess (Gruppe F) zur Einhaltung des Stammdatenschutzes (GR-05) läuft fehlerfrei. Das Modul ist somit bereit für den Release-Merge in den Hauptzweig.
|
||||
|
||||
---
|
||||
|
||||
## Änderungshistorie
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
|---------|------------|-------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| 1.0 | 28.06.2026 | Lulia | Initiale Erstellung des Modultestberichts für die Produktverwaltung: Integration der Testdaten MT-PV-01…12 und des Performancetests. |
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
# Modultestbericht – Dokumentenprozess (Gruppe F)
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Modul | Dokumentenprozess |
|
||||
| Gruppe | F |
|
||||
| Verantwortlicher | Andreas Ivanovic |
|
||||
| Datum | 28.06.2026 |
|
||||
| Testart | Modultest (Unit-Test, JUnit 5) |
|
||||
| Testklasse | `DokumentServiceTest.java` |
|
||||
| Paket | `de.hsmannheim.faktura.dokumente.service` |
|
||||
| Spec-Grundlage | Pflichtenheft Gruppe F v1.0.1 |
|
||||
| Gesamt-Ergebnis | ✅ 16 / 16 Tests bestanden |
|
||||
|
||||
---
|
||||
|
||||
## 1. Testumgebung
|
||||
|
||||
| Komponente | Version |
|
||||
|---|---|
|
||||
| Java | 17 |
|
||||
| JUnit Jupiter | 5.10.2 |
|
||||
| Maven Surefire | 3.2.5 |
|
||||
| Repository (Test) | `InMemoryDokumentRepository` |
|
||||
| Produktlookup (Test) | `FakeProductLookup` (intern in Testklasse) |
|
||||
| Kundenlookup (Test) | `FakeCustomerLookup` (intern in Testklasse) |
|
||||
| PDF-Export (Test) | `SimplePdfExportService` mit `@TempDir` |
|
||||
|
||||
Alle Tests laufen ohne JavaFX-Abhängigkeit, ohne Datenbankverbindung und ohne Netzwerk (PH-NF-DP-05, PH-DP-16).
|
||||
|
||||
---
|
||||
|
||||
## 2. Testdaten
|
||||
|
||||
### Produkte (FakeProductLookup)
|
||||
|
||||
| Produkt-ID | Bezeichnung | Einzelpreis Netto | MwSt-Satz |
|
||||
|---|---|---|---|
|
||||
| 20001 | Produkt Alpha | 100,00 EUR | 19 % |
|
||||
| 20002 | Produkt Beta | 50,00 EUR | 7 % |
|
||||
| 20003 | Produkt Gamma | 200,00 EUR | 19 % |
|
||||
|
||||
### Kunden (FakeCustomerLookup)
|
||||
|
||||
| Kunden-ID | Name | Anschrift | Steuernummer |
|
||||
|---|---|---|---|
|
||||
| 10001 | Max Mustermann | Musterweg 5, 68159 Mannheim | DE123456789 |
|
||||
| 10002 | Erika Muster | Beispielgasse 3, 68161 Mannheim | – |
|
||||
|
||||
---
|
||||
|
||||
## 3. Testergebnisse
|
||||
|
||||
### MT-DP-01 – Angebot anlegen (Happy Path)
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-01 |
|
||||
| Methode | `erstelleAngebot(long, List<PositionCommand>)` |
|
||||
| Vorbedingung | Kunde 10001 und Produkte 20001, 20002 existieren |
|
||||
| Eingabe | kundenId=10001, Positionen: [20001×2, 20002×1] |
|
||||
| Erwartetes Ergebnis | Angebot mit Nummer A-2026-xxxx, Status OFFEN, 2 Positionen, Datum gesetzt |
|
||||
| Tatsächliches Ergebnis | ✅ Angebot erstellt, alle Felder korrekt |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-02 – Summenberechnung Angebot
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-01, PH-DP-08 |
|
||||
| Methode | `erstelleAngebot(...)` → `berechneSummen()` |
|
||||
| Eingabe | 2 × Produkt Alpha (100,00 EUR, 19 %), 1 × Produkt Beta (50,00 EUR, 7 %) |
|
||||
| Erwartetes Ergebnis | Netto: 250,00 EUR · USt: 41,50 EUR · Brutto: 291,50 EUR |
|
||||
| Tatsächliches Ergebnis | ✅ Alle drei Summen korrekt berechnet |
|
||||
|
||||
Berechnung:
|
||||
- Netto: (2 × 100,00) + (1 × 50,00) = 250,00 EUR
|
||||
- USt: (200,00 × 0,19) + (50,00 × 0,07) = 38,00 + 3,50 = 41,50 EUR
|
||||
- Brutto: 250,00 + 41,50 = 291,50 EUR
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-03 – Fortlaufende Angebotsnummern
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-02 |
|
||||
| Methode | `DokumentNummernGenerator.naechsteNummer()` |
|
||||
| Eingabe | Zwei Angebote nacheinander anlegen |
|
||||
| Erwartetes Ergebnis | Nummern A-2026-0001 und A-2026-0002 (eindeutig, fortlaufend) |
|
||||
| Tatsächliches Ergebnis | ✅ Nummern korrekt vergeben, kein Duplikat |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-04 – Gesperrtes Angebot nach Überführung
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-03 |
|
||||
| Methode | `aktualisiereAngebot(String, List<PositionCommand>)` |
|
||||
| Vorbedingung | Angebot wurde bereits in AB überführt (Status UEBERFUEHRT) |
|
||||
| Eingabe | Versuch Angebot zu bearbeiten |
|
||||
| Erwartetes Ergebnis | `IllegalStateException` wird geworfen |
|
||||
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-05 – Auftragsbestätigung erzeugen
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-04 |
|
||||
| Methode | `erstelleAuftragsbestaetigung(String)` |
|
||||
| Vorbedingung | Angebot mit Status OFFEN vorhanden |
|
||||
| Eingabe | angebotsNummer = A-2026-0001 |
|
||||
| Erwartetes Ergebnis | AB mit Nummer AB-2026-xxxx, Referenz auf Angebot, Angebot auf Status UEBERFUEHRT |
|
||||
| Tatsächliches Ergebnis | ✅ AB korrekt erstellt, Angebotsstatus korrekt aktualisiert |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-06 – Doppelte Auftragsbestätigung verhindern
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-05 |
|
||||
| Methode | `erstelleAuftragsbestaetigung(String)` (zweiter Aufruf) |
|
||||
| Vorbedingung | AB für dieses Angebot existiert bereits |
|
||||
| Eingabe | Zweiter Versuch AB für dasselbe Angebot zu erzeugen |
|
||||
| Erwartetes Ergebnis | `DuplicateDocumentException` wird geworfen |
|
||||
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst |
|
||||
| Hinweis | **Patch 28.06.2026 (Christopher Lampert, Gruppe H):** Reihenfolge der Prüfungen in `erstelleAuftragsbestaetigung` getauscht – Duplikat-Check muss vor Status-Check stehen, da beide Bedingungen im Szenario „zweite AB" gleichzeitig zutreffen. Spec verlangt explizit `DuplicateDocumentException` (PH-DP-05). |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-07 – Lieferschein erzeugen
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-06 |
|
||||
| Methode | `erstelleLieferschein(String, LocalDate)` |
|
||||
| Vorbedingung | AB mit Status BESTAETIGT vorhanden |
|
||||
| Eingabe | auftragsNummer = AB-2026-0001, lieferdatum = heute + 3 Tage |
|
||||
| Erwartetes Ergebnis | Lieferschein mit Nummer LS-2026-xxxx, Referenz auf AB, Status GELIEFERT |
|
||||
| Tatsächliches Ergebnis | ✅ Lieferschein korrekt erstellt |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-08 – Lieferschein enthält keine Preise
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-07 |
|
||||
| Methode | `exportiereAlsPdf(String)` auf Lieferschein |
|
||||
| Vorbedingung | Lieferschein vorhanden |
|
||||
| Erwartetes Ergebnis | PDF-Inhalt enthält Bezeichnung und Menge, aber keine Nettosumme, USt oder Bruttosumme |
|
||||
| Tatsächliches Ergebnis | ✅ Preisfelder im Export nicht vorhanden |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-09 – Rechnung erzeugen (Happy Path)
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-08, PH-DP-09 |
|
||||
| Methode | `erstelleRechnung(String)` |
|
||||
| Vorbedingung | Vollständige Kette: Angebot → AB → Lieferschein vorhanden |
|
||||
| Eingabe | lieferscheinNummer = LS-2026-0001 |
|
||||
| Erwartetes Ergebnis | Rechnung mit Nummer R-2026-xxxx, Status FESTGESCHRIEBEN, Pflichtangaben gesetzt, Summen korrekt |
|
||||
| Tatsächliches Ergebnis | ✅ Rechnung vollständig und korrekt erstellt |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-10 – Fehlende Pflichtangaben verhindern Rechnungserstellung
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-09 |
|
||||
| Methode | `erstelleRechnung(String)` mit unvollständigen Kundendaten |
|
||||
| Vorbedingung | CustomerLookup liefert Kunde ohne Name und Anschrift |
|
||||
| Erwartetes Ergebnis | `ValidationException` mit Liste der fehlenden Felder |
|
||||
| Tatsächliches Ergebnis | ✅ Exception mit korrekten Fehlermeldungen ausgelöst |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-11 – Rechnungsnummern lückenlos
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-10 |
|
||||
| Methode | `erstelleRechnung(String)` dreimal hintereinander |
|
||||
| Erwartetes Ergebnis | Nummern R-2026-0001, R-2026-0002, R-2026-0003 (lückenlos) |
|
||||
| Tatsächliches Ergebnis | ✅ Alle drei Nummern korrekt und lückenlos vergeben |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-12 – Rechnung unveränderlich nach Festschreibung
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-11 |
|
||||
| Methode | `rechnung.pruefeAenderbar()` auf festgeschriebener Rechnung |
|
||||
| Vorbedingung | Rechnung wurde erstellt und ist festgeschrieben |
|
||||
| Erwartetes Ergebnis | `IllegalStateException` beim Versuch die Rechnung zu ändern |
|
||||
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst, Status nach Reload weiterhin FESTGESCHRIEBEN |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-13 – Preis-Snapshot bleibt stabil
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-14 |
|
||||
| Methode | Angebot erstellen → Produktpreis ändern → Angebot neu laden |
|
||||
| Vorbedingung | Angebot mit Produkt Alpha (100,00 EUR) erstellt |
|
||||
| Eingabe | Produktpreis wird nachträglich auf 120,00 EUR geändert |
|
||||
| Erwartetes Ergebnis | Snapshot-Preis im Angebot bleibt 100,00 EUR |
|
||||
| Tatsächliches Ergebnis | ✅ Snapshot unverändert, Preisänderung hat keinen Einfluss |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-14 – PDF-Export erzeugt Datei
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-12 |
|
||||
| Methode | `exportiereAlsPdf(String)` |
|
||||
| Vorbedingung | Angebot vorhanden |
|
||||
| Erwartetes Ergebnis | Datei existiert, ist nicht leer, enthält Dokumentnummer und Typ |
|
||||
| Tatsächliches Ergebnis | ✅ Datei korrekt erstellt, Inhalt vollständig |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-15 – Dokumentübersicht pro Kunde
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-13 |
|
||||
| Methode | `findByKunde(long)` |
|
||||
| Vorbedingung | 2 Dokumente für Kunde 10001, 1 Dokument für Kunde 10002 |
|
||||
| Erwartetes Ergebnis | Kunde 10001 → 2 Dokumente, Kunde 10002 → 1 Dokument |
|
||||
| Tatsächliches Ergebnis | ✅ Filterung korrekt, keine Vermischung zwischen Kunden |
|
||||
|
||||
---
|
||||
|
||||
### MT-DP-16 – Stammdatenreferenz prüfen
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Spec-Bezug | PH-DP-15 |
|
||||
| Methode | `existsReferencedProdukt(long)`, `existsReferencedKunde(long)` |
|
||||
| Vorbedingung | Angebot mit Produkt 20001 und Kunde 10001 vorhanden |
|
||||
| Erwartetes Ergebnis | Produkt 20001 → true, Produkt 99999 → false, Kunde 10001 → true, Kunde 99998 → false |
|
||||
| Tatsächliches Ergebnis | ✅ Alle vier Prüfungen korrekt |
|
||||
|
||||
---
|
||||
|
||||
## 4. Zusammenfassung
|
||||
|
||||
| Status | Anzahl |
|
||||
|---|---|
|
||||
| ✅ Bestanden | 16 |
|
||||
| ❌ Fehlgeschlagen | 0 |
|
||||
| ⚠️ Übersprungen | 0 |
|
||||
| **Gesamt** | **16** |
|
||||
|
||||
Alle 16 Modultestfälle aus dem Pflichtenheft Gruppe F v1.0.1 wurden erfolgreich ausgeführt. Der Patch vom 28.06.2026 (MT-DP-06, Reihenfolge der Prüfungen in `erstelleAuftragsbestaetigung`) wurde in den Code integriert und verifiziert. Alle anderen Testfälle bleiben durch den Patch unberührt.
|
||||
|
||||
---
|
||||
|
||||
## 5. Bekannte offene Punkte
|
||||
|
||||
| ID | Beschreibung | Priorität |
|
||||
|---|---|---|
|
||||
| OP-7 | GUI-Integration: `DokumentPanel` muss mit `DokumentService` verbunden werden | Hoch |
|
||||
| – | SQLite-Repository (Produktionsbetrieb) noch nicht implementiert, InMemory für Tests ausreichend | Mittel |
|
||||
| – | Leistungserbringer-Daten in `DokumentServiceImpl.erstellePflichtangaben()` aktuell hartcodiert | Niedrig |
|
||||
|
|
@ -29,9 +29,9 @@ header-includes:
|
|||
**Modul:** Software Engineering 1
|
||||
**Team:** SE1 Team 2 – Hochschule Mannheim
|
||||
**Version:** 1.0
|
||||
**Stand:** 25.06.2026
|
||||
**Stand:** 11.06.2026
|
||||
**Autor:** Christopher Lampert
|
||||
**Bezug:** Lastenheft v1.1 (24.06.2026), Project Charter v1.1 (15.05.2026) – Fakturierungssystem
|
||||
**Bezug:** Lastenheft v1.1 (25.06.2026), Project Charter v1.1 (15.05.2026) – Fakturierungssystem
|
||||
|
||||
Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.1. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest.
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ header-includes:
|
|||
|
||||
**Modul:** Software Engineering 1
|
||||
**Team:** SE1 Team 2 – Hochschule Mannheim
|
||||
**Version:** 1.1
|
||||
**Version:** 1.0
|
||||
**Stand:** 11.06.2026
|
||||
**Autor:** Christopher Lampert
|
||||
**Bezug:** Lastenheft v1.1 (24.05.2026), Project Charter v1.1 (15.05.2026) – Fakturierungssystem
|
||||
**Bezug:** Lastenheft v1.0 (15.05.2026), Project Charter v1.1 (15.05.2026) – Fakturierungssystem
|
||||
|
||||
Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.0. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,395 +0,0 @@
|
|||
# Pflichtenheft - Dokumentenprozess Fakturierungssystem
|
||||
|
||||
**Gruppe F - SE1 Team 2**
|
||||
**Version:** 1.0
|
||||
**Datum:** 11.06.2026
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
- [Freigabeübersicht](#freigabeübersicht)
|
||||
- [Dokumentenhistorie](#dokumentenhistorie)
|
||||
- [1. Einleitung](#1-einleitung)
|
||||
- [1.1 Zweck dieses Dokuments](#11-zweck-dieses-dokuments)
|
||||
- [1.2 Bezug zum Project Charter](#12-bezug-zum-project-charter)
|
||||
- [1.3 Geltungsbereich](#13-geltungsbereich)
|
||||
- [2. Systemüberblick für Gruppe F](#2-systemüberblick-für-gruppe-f)
|
||||
- [3. Stakeholder und Kontext](#3-stakeholder-und-kontext)
|
||||
- [4. Funktionale Anforderungen](#4-funktionale-anforderungen)
|
||||
- [5. Nicht-funktionale Anforderungen für das Modul](#5-nicht-funktionale-anforderungen-für-das-modul)
|
||||
- [6. Daten und Schnittstellen](#6-daten-und-schnittstellen)
|
||||
- [6.1 Datenobjekte und Java-Datentypen](#61-datenobjekte-und-java-datentypen)
|
||||
- [6.2 Schnittstellen](#62-schnittstellen)
|
||||
- [7. Systemarchitektur](#7-systemarchitektur)
|
||||
- [8. Testbare Abnahmekriterien](#8-testbare-abnahmekriterien)
|
||||
- [9. Traceability zwischen Lastenheft und Pflichtenheft](#9-traceability-zwischen-lastenheft-und-pflichtenheft)
|
||||
- [10. Modultestplan](#10-modultestplan)
|
||||
- [11. Glossar](#11-glossar)
|
||||
- [12. Referenzen](#12-referenzen)
|
||||
|
||||
---
|
||||
|
||||
## Freigabeübersicht
|
||||
|
||||
| Ersteller | Prüfer | Freigebender |
|
||||
|---|---|---|
|
||||
| Gruppe F: Andreas Ivanovic, Armin Omanovic, Alexander Teller | Prof. Dr. Gerd Marmitt | SE1 Team 2 |
|
||||
| SE1 Team 2 | Hochschule Mannheim | SE1 Team 2 |
|
||||
| 11.06.2026 | [Datum Review] | 30.06.2026 |
|
||||
|
||||
---
|
||||
|
||||
## Dokumentenhistorie
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
|---|---:|---|---|
|
||||
| 1.0 | 11.06.2026 | Gruppe F / Alexander Teller | Initiale Erstellung des Pflichtenheft-Teils und Modultestplans für den Dokumentenprozess |
|
||||
|
||||
---
|
||||
|
||||
# 1. Einleitung
|
||||
|
||||
## 1.1 Zweck dieses Dokuments
|
||||
|
||||
Dieses Pflichtenheft beschreibt die konkrete Umsetzung des Moduls **Dokumentenprozess** im Fakturierungssystem. Es konkretisiert die Anforderungen aus dem Lastenheft und dem Project Charter für den Verantwortungsbereich der Gruppe F. Der Fokus liegt auf der fachlichen Kette:
|
||||
|
||||
```text
|
||||
Angebot -> Auftragsbestätigung -> Lieferschein -> Rechnung
|
||||
```
|
||||
|
||||
Das Dokument dient als Input für Implementierung, Komponententests und Integration mit Produktverwaltung, Kundenverwaltung und GUI.
|
||||
|
||||
## 1.2 Bezug zum Project Charter
|
||||
|
||||
Team 2 besteht aus den Gruppen E, F, G und H. Gruppe F ist laut Project Charter für den Dokumentenprozess verantwortlich. Der Dokumentenprozess umfasst die fachliche Kette **Angebot -> Auftragsbestätigung -> Lieferschein -> Rechnung**. Das Projekt nutzt **Java**, **JavaFX**, **SQLite** und **Gitty** und orientiert sich am **V-Modell**.
|
||||
|
||||
## 1.3 Geltungsbereich
|
||||
|
||||
Dieses Pflichtenheft deckt nur den Dokumentenprozess ab. Produktverwaltung, Kundenverwaltung und GUI werden nur als Schnittstellen beschrieben, weil sie durch die Gruppen G, H und E verantwortet werden.
|
||||
|
||||
Nicht Teil dieses Moduls sind:
|
||||
|
||||
- vollständige Buchhaltung
|
||||
- E-Rechnung
|
||||
- Mehrbenutzerbetrieb
|
||||
- Cloud-Betrieb
|
||||
- externe ERP-/FiBu-Anbindungen
|
||||
|
||||
---
|
||||
|
||||
# 2. Systemüberblick für Gruppe F
|
||||
|
||||
Das Modul Gruppe F stellt die Geschäftslogik für Dokumente bereit. Die GUI ruft den `DokumentService` auf. Produkt- und Kundendaten werden über Lookup-Schnittstellen gelesen. Dokumente werden persistent über ein Repository gespeichert. Aus vorhandenen Dokumenten können Folgedokumente erzeugt werden, wobei Status, Referenzen und fachliche Regeln geprüft werden.
|
||||
|
||||
---
|
||||
|
||||
# 3. Stakeholder und Kontext
|
||||
|
||||
| Stakeholder / Rolle | Interesse am Modul Dokumentenprozess |
|
||||
|---|---|
|
||||
| Auftraggeber Prof. Dr. Gerd Marmitt | Nachvollziehbare Umsetzung der Anforderungen und testbare Artefakte im V-Modell. |
|
||||
| Gruppe F | Implementierung des Dokumentenworkflows und Bereitstellung testbarer Services. |
|
||||
| Gruppe E - GUI | Benötigt Service-Methoden und DTOs für Anzeige, Eingabe und Export. |
|
||||
| Gruppe G - Produktverwaltung | Liefert Produktdaten und muss prüfen können, ob Produkte in Dokumenten referenziert sind. |
|
||||
| Gruppe H - Kundenverwaltung | Liefert Kundendaten und muss prüfen können, ob Kunden in Dokumenten referenziert sind. |
|
||||
| Endnutzer | Möchte Angebote, Aufträge, Lieferscheine und Rechnungen korrekt erzeugen und wiederfinden. |
|
||||
|
||||
---
|
||||
|
||||
# 4. Funktionale Anforderungen
|
||||
|
||||
| PH-ID | LH-Quelle | Kurzname | Konkretisierung / Systemverhalten | Priorität | Abnahmekriterium |
|
||||
|---|---|---|---|---|---|
|
||||
| PH-DP-01 | BA-DP-01, GR-04, GR-06 | Angebot anlegen | Das System MUSS über `DokumentService.erstelleAngebot(long kundenId, List<PositionCommand> positionen)` ein Angebot für genau einen vorhandenen Kunden mit mindestens einer Position erzeugen. Pro Position werden Produktbezeichnung, Einzelpreis netto und MwSt.-Satz als Snapshot übernommen. | Muss | Angebot ist gespeichert, besitzt Nummer `A-yyyy-nnnn`, Status `OFFEN`, Datum `LocalDate.now()` und korrekt berechnete Netto-, USt.- und Bruttosumme. |
|
||||
| PH-DP-02 | F-DP-02 | Angebotsnummer vergeben | Das System MUSS Angebotsnummern über `OfferNumberGenerator` je Kalenderjahr fortlaufend und eindeutig vergeben. | Muss | Zwei nacheinander erzeugte Angebote erhalten unterschiedliche und aufeinanderfolgende Nummern. |
|
||||
| PH-DP-03 | F-DP-03 | Angebot bearbeiten | Das System MUSS die Bearbeitung eines Angebots nur erlauben, solange `AngebotsStatus.OFFEN` gilt. | Muss | Bearbeitung bei `OFFEN` erfolgreich; bei `UEBERFUEHRT` wird `IllegalStateException` geworfen. |
|
||||
| PH-DP-04 | BA-DP-04, GR-01 | Auftragsbestätigung erzeugen | Das System MUSS aus genau einem offenen Angebot eine Auftragsbestätigung mit eigener Nummer `AB-yyyy-nnnn` erzeugen und alle Positionen unverändert übernehmen. | Muss | AB referenziert Angebotsnummer, Positionen und Summen entsprechen dem Angebot, Angebot wird auf `UEBERFUEHRT` gesetzt. |
|
||||
| PH-DP-05 | F-DP-05 | Doppelte Auftragsbestätigung verhindern | Das System MUSS vor der Erzeugung prüfen, ob für das Angebot bereits eine Auftragsbestätigung existiert. | Muss | Zweiter Erzeugungsversuch wird mit `DuplicateDocumentException` abgelehnt. |
|
||||
| PH-DP-06 | BA-DP-06, GR-01 | Lieferschein erzeugen | Das System MUSS aus genau einer bestätigten Auftragsbestätigung einen Lieferschein mit Nummer `LS-yyyy-nnnn` und `LocalDate lieferdatum` erzeugen. | Muss | Lieferschein referenziert AB, übernimmt Produktbezeichnung und Mengen und erhält Status `GELIEFERT`. |
|
||||
| PH-DP-07 | F-DP-07 | Lieferschein ohne Preise | Das System MUSS für Lieferscheinansicht und PDF nur Menge und Produktbezeichnung ausgeben, aber keine Einzelpreise, Netto-, USt.- oder Bruttowerte. | Muss | Export/DTO des Lieferscheins enthält keine Preisfelder. |
|
||||
| PH-DP-08 | BA-DP-08, F-DP-09, GR-01 | Rechnung erzeugen | Das System MUSS aus genau einem gelieferten Lieferschein eine Rechnung mit Nummer `R-yyyy-nnnn`, Rechnungsdatum, Referenz auf den Lieferschein und Pflichtangaben nach UStG erzeugen. | Muss | Rechnung enthält Kunde, Leistungsdatum/Lieferdatum, Positionen, fortlaufende Nummer, Netto-, USt.- und Bruttosumme. |
|
||||
| PH-DP-09 | F-DP-09 | Pflichtangaben prüfen | Das System MUSS vor dem Speichern einer Rechnung `RechnungPflichtangabenValidator.validate(Rechnung)` ausführen. | Muss | Fehlende Pflichtangaben verhindern Speicherung und liefern Validierungsfehler. |
|
||||
| PH-DP-10 | F-DP-10, GR-02 | Rechnungsnummer fortlaufend | Das System MUSS Rechnungsnummern fortlaufend, eindeutig und lückenlos vergeben, sobald eine Rechnung erfolgreich gespeichert wird. | Muss | Drei erfolgreiche Rechnungen ergeben `R-2026-0001`, `R-2026-0002`, `R-2026-0003`. |
|
||||
| PH-DP-11 | F-DP-11, GR-03 | Rechnung festschreiben | Das System MUSS eine gespeicherte Rechnung inhaltlich sperren (`festgeschrieben=true`). | Muss | Änderungsversuche an Positionen, Summen oder Pflichtangaben werden abgelehnt. |
|
||||
| PH-DP-12 | F-DP-12 | PDF-Export | Das System SOLL jedes Dokument über `PdfExportService.export(Dokument)` als PDF-Datei im lokalen Dateisystem erzeugen. | Soll | PDF-Datei existiert, ist nicht leer und enthält Dokumentnummer und Dokumenttyp. |
|
||||
| PH-DP-13 | F-DP-13 | Dokumentenübersicht pro Kunde | Das System SOLL über `DokumentRepository.findByKunde(long kundenId)` alle Dokumente eines Kunden sortiert nach Datum und Dokumenttyp bereitstellen. | Soll | Alle Angebote, ABs, Lieferscheine und Rechnungen des Kunden werden ohne fremde Kundendokumente geliefert. |
|
||||
| PH-DP-14 | GR-04 | Preis-Snapshot sichern | Das System MUSS den Produktpreis und MwSt.-Satz beim Einfügen in das Angebot als Snapshot in `Dokumentposition` speichern. | Muss | Spätere Produktänderung beeinflusst bestehende Dokumentpositionen nicht. |
|
||||
| PH-DP-15 | GR-05 | Stammdatenreferenzen prüfbar machen | Das Dokumentenmodul MUSS für Produkt- und Kundenverwaltung eine Prüffunktion `isReferenced(...)` bereitstellen. | Muss | Referenzierte Produkte/Kunden können durch andere Module als verwendet erkannt werden. |
|
||||
| PH-DP-16 | NF-TEST-02, NF-MAINT-03 | Testbare Logik ohne GUI | Die Dokumentenlogik MUSS in Service- und Domainklassen ohne JavaFX-Abhängigkeit implementiert werden. | Muss | JUnit-Tests können `DokumentService` ohne Start der GUI ausführen. |
|
||||
|
||||
---
|
||||
|
||||
# 5. Nicht-funktionale Anforderungen für das Modul
|
||||
|
||||
| PH-NF-ID | LH-Quelle | Konkretisierung | Nachweis |
|
||||
|---|---|---|---|
|
||||
| PH-NF-DP-01 | NF-PERF-01 | Dokumentenaktionen bis 1.000 Kunden/Produkte müssen innerhalb von 1 Sekunde eine Rückmeldung liefern, wenn Repositories mit Testdaten initialisiert sind. | Zeitmessung im Komponententest/Integrationstest `< 1 s` für Erzeugung eines Angebots mit 10 Positionen. |
|
||||
| PH-NF-DP-02 | NF-ARCH-01 | Dokumente müssen nach Neustart aus SQLite/Dateispeicher vollständig wiederherstellbar sein. | Dokument erstellen, Repository neu initialisieren, Dokument unverändert laden. |
|
||||
| PH-NF-DP-03 | NF-ARCH-02 | Datenhaltung des Dokumentenmoduls muss über `DokumentRepository` gekapselt sein. | Code-Review: Service nutzt Interface, keine direkten SQL-Aufrufe im Service. |
|
||||
| PH-NF-DP-04 | NF-MAINT-01, NF-MAINT-03 | Dokumentenprozess wird im Paket `de.hsma.se1.fakturierung.dokumente` umgesetzt und in Domain, Service, Repository und Export getrennt. | Repository-Struktur enthält getrennte Pakete; Abhängigkeiten laufen von UI -> Service -> Repository. |
|
||||
| PH-NF-DP-05 | NF-TEST-02, NF-TEST-03 | Die Geschäftslogik des Dokumentenmoduls muss automatisiert testbar sein. | Mindestens 10 JUnit-Testfälle für Services/Domainklassen laufen ohne GUI. |
|
||||
|
||||
---
|
||||
|
||||
# 6. Daten und Schnittstellen
|
||||
|
||||
## 6.1 Datenobjekte und Java-Datentypen
|
||||
|
||||
| Datenfeld / Objekt | Java-Datentyp | Wertebereich / Format | Beschreibung |
|
||||
|---|---|---|---|
|
||||
| `DokumentType` | `enum` | `ANGEBOT`, `AUFTRAGSBESTAETIGUNG`, `LIEFERSCHEIN`, `RECHNUNG` | Typisierung für Anzeige, Export und Repository. |
|
||||
| Dokumentnummer | `String` | Format `A-2026-0001`, `AB-2026-0001`, `LS-2026-0001`, `R-2026-0001` | Fachlicher Identifikator; nicht als `int`, da Präfix/Jahr enthalten. |
|
||||
| `kundenId` | `long` | z. B. `10001L` | Referenz auf Kundenverwaltung, keine Kopie des gesamten Kundenobjekts im Dokumentservice erforderlich. |
|
||||
| `produktId` | `long` | z. B. `20001L` | Referenz auf Produktverwaltung. |
|
||||
| `bezeichnungSnapshot` | `String` | max. 255 Zeichen | Produktbezeichnung zum Zeitpunkt der Angebotsanlage. |
|
||||
| `menge` | `int` | `> 0` | Menge pro Dokumentposition. |
|
||||
| `einzelpreisNettoSnapshot` | `BigDecimal` | `>= 0`, Skalierung 2 | Geldbetrag; `BigDecimal` statt `double` wegen Rundungsfehlern. |
|
||||
| `mwstSatzSnapshot` | `BigDecimal` | z. B. `0.19`, `0.07` | MwSt.-Satz als Dezimalwert. |
|
||||
| `datum`, `lieferdatum`, `rechnungsdatum` | `LocalDate` | ISO-Format `yyyy-MM-dd` | Fachliche Datumswerte ohne Uhrzeit. |
|
||||
| `nettoSumme`, `ustSumme`, `bruttoSumme` | `BigDecimal` | Skalierung 2, Rundung `HALF_UP` | Berechnete Summen des Dokuments. |
|
||||
| `status` | `enum` | `OFFEN`, `UEBERFUEHRT`, `BESTAETIGT`, `GELIEFERT`, `FAKTURIERT`, `FESTGESCHRIEBEN` | Steuert erlaubte Folgeaktionen. |
|
||||
| `pflichtangaben` | `RechnungPflichtangaben` | Objekt mit `leistungserbringerName`, `anschrift`, `steuerNr`, `leistungsdatum` usw. | Pflichtfelder der Rechnung. |
|
||||
|
||||
## 6.2 Schnittstellen
|
||||
|
||||
| Schnittstelle | Art | Zweck | Operationen / Daten |
|
||||
|---|---|---|---|
|
||||
| `DokumentService` | Java-Service-Interface | UI/Controller ruft Dokumentenlogik auf. | Methoden: `erstelleAngebot`, `erstelleAuftragsbestaetigung`, `erstelleLieferschein`, `erstelleRechnung`, `exportiereAlsPdf`. |
|
||||
| `DokumentRepository` | Java-Interface / SQLite-Implementierung | Persistenz von Dokumenten. | `save`, `findByNummer`, `findByKunde`, `existsFolgedokument`, `existsReferencedProdukt`, `existsReferencedKunde`. |
|
||||
| `ProductLookup` | Java-Interface zu Gruppe G | Produktdaten für Angebotspositionen lesen. | `findProduct(long produktId): Optional<ProductDto>` mit `String bezeichnung`, `BigDecimal einzelpreisNetto`, `BigDecimal mwstSatz`. |
|
||||
| `CustomerLookup` | Java-Interface zu Gruppe H | Kundendaten für Dokumentkopf und Rechnungspflichtangaben lesen. | `findCustomer(long kundenId): Optional<CustomerDto>`. |
|
||||
| `PdfExportService` | Java-Service / lokale Datei-Schnittstelle | PDF-Dateien aus Dokumentdaten erzeugen. | Rückgabe `Path`; Zielordner z. B. `/exports`. |
|
||||
|
||||
---
|
||||
|
||||
# 7. Systemarchitektur
|
||||
|
||||
Das Dokumentenmodul folgt einer dreischichtigen Struktur: Präsentation/UI ruft Services auf, Services enthalten die Geschäftslogik, Repositories kapseln die Datenhaltung. Die fachlichen Domänenklassen sind von JavaFX unabhängig und können isoliert getestet werden.
|
||||
|
||||
## Abbildung 1: UML-Klassendiagramm Gruppe F
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction TB
|
||||
|
||||
class DokumentService {
|
||||
+erstelleAngebot(...) Angebot
|
||||
+aktualisiereAngebot(...) Angebot
|
||||
+erstelleAuftragsbestaetigung(...) Auftragsbestaetigung
|
||||
+erstelleLieferschein(...) Lieferschein
|
||||
+erstelleRechnung(...) Rechnung
|
||||
+exportiereAlsPdf(...) Path
|
||||
}
|
||||
|
||||
class Dokument {
|
||||
<<abstract>>
|
||||
-nummer String
|
||||
-kundeId long
|
||||
-datum LocalDate
|
||||
-positionen List~Dokumentposition~
|
||||
-nettoSumme BigDecimal
|
||||
-ustSumme BigDecimal
|
||||
-bruttoSumme BigDecimal
|
||||
+berechneSummen() SummenDto
|
||||
}
|
||||
|
||||
class Dokumentposition {
|
||||
-produktId long
|
||||
-bezeichnungSnapshot String
|
||||
-menge int
|
||||
-einzelpreisNettoSnapshot BigDecimal
|
||||
-mwstSatzSnapshot BigDecimal
|
||||
+berechneNetto() BigDecimal
|
||||
+berechneUst() BigDecimal
|
||||
}
|
||||
|
||||
class Angebot {
|
||||
-angebotsNr String
|
||||
-status AngebotsStatus
|
||||
+istBearbeitbar() boolean
|
||||
}
|
||||
|
||||
class Auftragsbestaetigung {
|
||||
-auftragNr String
|
||||
-angebotNr String
|
||||
-status AuftragsStatus
|
||||
+ausAngebot(...) ...
|
||||
}
|
||||
|
||||
class Lieferschein {
|
||||
-lieferscheinNr String
|
||||
-auftragsNr String
|
||||
-lieferdatum LocalDate
|
||||
-status LieferStatus
|
||||
}
|
||||
|
||||
class Rechnung {
|
||||
-rechnungsNr String
|
||||
-lieferscheinNr String
|
||||
-rechnungsdatum LocalDate
|
||||
-status RechnungsStatus
|
||||
-festgeschrieben boolean
|
||||
}
|
||||
|
||||
class DokumentRepository {
|
||||
<<interface>>
|
||||
+save(Dokument) Dokument
|
||||
+findByNummer(String) Optional~Dokument~
|
||||
+existsFolgedokument(String) boolean
|
||||
+findByKunde(long) List~Dokument~
|
||||
}
|
||||
|
||||
class NumberGenerator {
|
||||
+nextAngebotsNr(int) String
|
||||
+nextAuftragsNr(int) String
|
||||
+nextLieferscheinNr(int) String
|
||||
+nextRechnungsNr(int) String
|
||||
}
|
||||
|
||||
class PdfExportService {
|
||||
+export(Dokument) Path
|
||||
+exportRechnung(Rechnung) Path
|
||||
-templateEngine String
|
||||
}
|
||||
|
||||
Dokument "1" *-- "1..*" Dokumentposition : enthält
|
||||
Dokument <|-- Angebot
|
||||
Dokument <|-- Auftragsbestaetigung
|
||||
Dokument <|-- Lieferschein
|
||||
Dokument <|-- Rechnung
|
||||
|
||||
DokumentService ..> Dokument : nutzt
|
||||
DokumentService ..> DokumentRepository : nutzt
|
||||
DokumentService ..> NumberGenerator : nutzt
|
||||
DokumentService ..> PdfExportService : PDF-Export
|
||||
|
||||
Angebot --> Auftragsbestaetigung : 1:1 erzeugt aus
|
||||
Auftragsbestaetigung --> Lieferschein : 1:1 erzeugt
|
||||
Lieferschein --> Rechnung : 1:1 erzeugt aus
|
||||
```
|
||||
|
||||
**Beschreibung zu Abbildung 1:** Das Klassendiagramm zeigt die zentrale abstrakte Klasse `Dokument` mit den Spezialisierungen `Angebot`, `Auftragsbestaetigung`, `Lieferschein` und `Rechnung`. Die Klasse `Dokumentposition` speichert Produktdaten als Snapshot. Der `DokumentService` nutzt Repositories, Nummerngenerator und PDF-Export, ohne selbst technische Persistenzdetails zu kennen.
|
||||
|
||||
## Abbildung 2: UML-Sequenzdiagramm Rechnung aus Lieferschein
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Anwender
|
||||
participant UI as DokumentenUI
|
||||
participant DS as DokumentService
|
||||
participant LR as LieferscheinRepo
|
||||
participant NG as NumberGenerator
|
||||
participant RR as RechnungRepo
|
||||
participant PDF as PdfExportService
|
||||
|
||||
Anwender->>UI: Klick: Rechnung erzeugen(lsNr)
|
||||
UI->>DS: erstelleRechnung(lsNr)
|
||||
DS->>LR: findByNummer(lsNr)
|
||||
LR-->>DS: Lieferschein(status=GELIEFERT)
|
||||
|
||||
alt Lieferschein fehlt oder Status != GELIEFERT
|
||||
DS-->>UI: Fehlermeldung, keine Rechnungsnummer vergeben, keine Rechnung gespeichert
|
||||
UI-->>Anwender: Anzeige der Fehlermeldung
|
||||
else Lieferschein ist geliefert
|
||||
DS->>NG: nextRechnungsNr(jahr)
|
||||
NG-->>DS: R-2026-0001
|
||||
DS->>DS: pruefeStatusUndVorgaenger()
|
||||
DS->>DS: berechneNettoUstBrutto()
|
||||
DS->>RR: save(rechnung)
|
||||
RR-->>DS: gespeicherte Rechnung
|
||||
DS->>PDF: exportRechnung(rechnung)
|
||||
PDF-->>DS: pdfPfad
|
||||
DS-->>UI: Bestaetigung + Nummer + pdfPfad
|
||||
UI-->>Anwender: Anzeige der Rechnung
|
||||
end
|
||||
```
|
||||
|
||||
**Beschreibung zu Abbildung 2:** Das Sequenzdiagramm beschreibt den Ablauf zur Rechnungserzeugung aus einem Lieferschein. Zuerst wird der Lieferschein geladen und sein Status geprüft. Danach wird eine Rechnungsnummer vergeben, die Rechnung berechnet, gespeichert, optional als PDF exportiert und an die GUI zurückgegeben. Falls der Lieferschein fehlt oder nicht den Status `GELIEFERT` besitzt, wird keine Rechnung erzeugt.
|
||||
|
||||
---
|
||||
|
||||
# 8. Testbare Abnahmekriterien
|
||||
|
||||
| AC-ID | Abgedeckte PH-Anforderungen | Testbares Abnahmekriterium |
|
||||
|---|---|---|
|
||||
| AC-DP-01 | PH-DP-01, PH-DP-02, PH-DP-14 | Aus vorhandenem Kunden und zwei Produkten wird ein Angebot erzeugt. Erwartet werden korrekte Nummer, Status `OFFEN`, zwei Positionen, Preis-Snapshots und korrekte Summen. |
|
||||
| AC-DP-02 | PH-DP-03 | Ein offenes Angebot kann geändert werden. Ein überführtes Angebot kann nicht mehr geändert werden. |
|
||||
| AC-DP-03 | PH-DP-04, PH-DP-05 | Aus einem offenen Angebot wird genau eine Auftragsbestätigung erzeugt. Ein zweiter Versuch wird abgelehnt. |
|
||||
| AC-DP-04 | PH-DP-06, PH-DP-07 | Aus einer bestätigten AB wird ein Lieferschein erzeugt. Der Lieferschein enthält Mengen und Bezeichnungen, aber keine Preise. |
|
||||
| AC-DP-05 | PH-DP-08, PH-DP-09, PH-DP-10, PH-DP-11 | Aus einem gelieferten Lieferschein wird eine festgeschriebene Rechnung mit Pflichtangaben, lückenloser Nummer und korrekten Summen erzeugt. |
|
||||
| AC-DP-06 | PH-DP-12 | Ein Dokument kann als PDF exportiert werden; die Datei existiert und enthält Dokumentnummer und Dokumenttyp. |
|
||||
| AC-DP-07 | PH-DP-13 | Alle Dokumente eines Kunden werden kundenbezogen aufgelistet. |
|
||||
| AC-DP-08 | PH-DP-15 | Produkt- und Kundenverwaltung können prüfen, ob Stammdaten in Dokumenten referenziert werden. |
|
||||
|
||||
---
|
||||
|
||||
# 9. Traceability zwischen Lastenheft und Pflichtenheft
|
||||
|
||||
| LH-Anforderung | Kurzbeschreibung | PH-Anforderungen | Abnahmekriterium | Modultestfälle |
|
||||
|---|---|---|---|---|
|
||||
| BA-DP-01 | Angebot für Kunden anlegen | PH-DP-01, PH-DP-14 | AC-DP-01 | MT-DP-01, MT-DP-02, MT-DP-13 |
|
||||
| F-DP-02 | Angebotsnummer eindeutig/fortlaufend | PH-DP-02 | AC-DP-01 | MT-DP-03 |
|
||||
| F-DP-03 | Angebot nur bis Überführung bearbeiten | PH-DP-03 | AC-DP-02 | MT-DP-04 |
|
||||
| BA-DP-04 | Auftragsbestätigung aus Angebot | PH-DP-04 | AC-DP-03 | MT-DP-05 |
|
||||
| F-DP-05 | Maximal eine AB pro Angebot | PH-DP-05 | AC-DP-03 | MT-DP-06 |
|
||||
| BA-DP-06 | Lieferschein aus AB | PH-DP-06 | AC-DP-04 | MT-DP-07 |
|
||||
| F-DP-07 | Lieferschein ohne Preise | PH-DP-07 | AC-DP-04 | MT-DP-08 |
|
||||
| BA-DP-08 | Rechnung aus Lieferschein | PH-DP-08 | AC-DP-05 | MT-DP-09 |
|
||||
| F-DP-09 | Rechnungspflichtangaben | PH-DP-09 | AC-DP-05 | MT-DP-10 |
|
||||
| F-DP-10 | Rechnungsnummer fortlaufend/lückenlos | PH-DP-10 | AC-DP-05 | MT-DP-11 |
|
||||
| F-DP-11 | Rechnung unveränderlich | PH-DP-11 | AC-DP-05 | MT-DP-12 |
|
||||
| F-DP-12 | PDF-Export | PH-DP-12 | AC-DP-06 | MT-DP-14 |
|
||||
| F-DP-13 | Dokumentenübersicht Kunde | PH-DP-13 | AC-DP-07 | MT-DP-15 |
|
||||
| GR-01 | Dokumentenkette | PH-DP-04, PH-DP-06, PH-DP-08 | AC-DP-03 bis AC-DP-05 | MT-DP-05, MT-DP-07, MT-DP-09 |
|
||||
| GR-02 | Rechnungsnummern eindeutig | PH-DP-10 | AC-DP-05 | MT-DP-11 |
|
||||
| GR-03 | Rechnung nicht änderbar | PH-DP-11 | AC-DP-05 | MT-DP-12 |
|
||||
| GR-04 | Preisübernahme Snapshot | PH-DP-14 | AC-DP-01 | MT-DP-13 |
|
||||
| GR-05 | Stammdatenschutz | PH-DP-15 | AC-DP-08 | MT-DP-16 |
|
||||
| GR-06 | Summenberechnung | PH-DP-01, PH-DP-08 | AC-DP-01, AC-DP-05 | MT-DP-02, MT-DP-09 |
|
||||
|
||||
---
|
||||
|
||||
# 10. Modultestplan
|
||||
|
||||
Die folgenden Testfälle sind so formuliert, dass sie direkt mit JUnit 5 umgesetzt werden können. Für den isolierten Modultest werden Produkt- und Kundendaten durch Fake-Lookups bereitgestellt. Die GUI wird nicht gestartet.
|
||||
|
||||
| Test-ID | Name | Abgedeckte PH-Anforderungen | Vorbedingung | Testschritte | Erwartetes Ergebnis | Umsetzungshinweis |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MT-DP-01 | Angebot anlegen - Happy Path | PH-DP-01 | Kunde 10001 und Produkte 20001/20002 existieren. | `erstelleAngebot(10001, [2x Produkt 20001, 1x Produkt 20002])` | Angebot gespeichert, Status `OFFEN`, 2 Positionen, Nummer beginnt mit `A-2026-`. | JUnit: `assertEquals`, `FakeProductLookup`, `InMemoryDokumentRepository` |
|
||||
| MT-DP-02 | Summenberechnung Angebot | PH-DP-01, PH-DP-08 | Positionen: 2 x 100,00 EUR mit 19 %, 1 x 50,00 EUR mit 7 %. | `angebot.berechneSummen()` | Netto 250,00; USt. 41,50; Brutto 291,50. | JUnit: BigDecimal-Vergleich mit `compareTo` oder skalierter Assertion |
|
||||
| MT-DP-03 | Fortlaufende Angebotsnummern | PH-DP-02 | Leerer Nummernkreis 2026. | Zwei Angebote erzeugen. | Nummern `A-2026-0001` und `A-2026-0002`. | JUnit: `assertNotEquals`, `assertTrue(endsWith)` |
|
||||
| MT-DP-04 | Angebot nach Überführung sperren | PH-DP-03 | Angebot wurde in AB überführt. | `aktualisiereAngebot(...)` aufrufen. | `IllegalStateException`; Positionen unverändert. | JUnit: `assertThrows` |
|
||||
| MT-DP-05 | Auftragsbestätigung erzeugen | PH-DP-04 | Offenes Angebot mit 2 Positionen existiert. | `erstelleAuftragsbestaetigung(angebotsNr)` | AB mit eigener Nummer, Angebotsreferenz und identischen Positionen gespeichert. | JUnit: Objektvergleich Positionen |
|
||||
| MT-DP-06 | Doppelte AB verhindern | PH-DP-05 | Für Angebot existiert bereits eine AB. | `erstelleAuftragsbestaetigung(angebotsNr)` erneut. | `DuplicateDocumentException`; keine zweite AB im Repository. | JUnit: `assertThrows`, Repository-Count |
|
||||
| MT-DP-07 | Lieferschein erzeugen | PH-DP-06 | AB im Status `BESTAETIGT` existiert. | `erstelleLieferschein(auftragsNr, lieferdatum)` | LS mit Nummer, Lieferdatum, AB-Referenz und Status `GELIEFERT` gespeichert. | JUnit: `assertEquals` |
|
||||
| MT-DP-08 | Lieferschein enthält keine Preise | PH-DP-07 | Lieferschein aus AB existiert. | `LieferscheinDto.from(lieferschein)` oder Exportmodell erzeugen. | DTO enthält Menge und Bezeichnung, aber keine Preis-/Steuerfelder. | JUnit: Getter/DTO-Feldprüfung |
|
||||
| MT-DP-09 | Rechnung erzeugen - Happy Path | PH-DP-08, PH-DP-09 | LS im Status `GELIEFERT` mit Kunde und Positionen existiert. | `erstelleRechnung(lieferscheinNr)` | Rechnung mit `R-...`, Pflichtangaben, Summen und Status `FESTGESCHRIEBEN` gespeichert. | JUnit: `assertAll` |
|
||||
| MT-DP-10 | Pflichtangaben fehlen | PH-DP-09 | Kunde ohne Rechnungsanschrift/Steuernummer in Testdaten. | `erstelleRechnung(lieferscheinNr)` | Validierungsfehler; keine Rechnung gespeichert. | JUnit: `assertThrows(ValidationException.class)` |
|
||||
| MT-DP-11 | Rechnungsnummern lückenlos | PH-DP-10 | Drei unterschiedliche gelieferte Lieferscheine existieren. | Drei Rechnungen erzeugen. | Nummern enden auf `0001`, `0002`, `0003` ohne Lücke. | JUnit: Listenvergleich |
|
||||
| MT-DP-12 | Rechnung unveränderlich | PH-DP-11 | Rechnung wurde gespeichert/festgeschrieben. | Änderung von Position oder Summe versuchen. | Änderung wird abgelehnt; geladene Rechnung bleibt unverändert. | JUnit: `assertThrows`, erneutes Laden |
|
||||
| MT-DP-13 | Preis-Snapshot bleibt stabil | PH-DP-14 | Angebot wurde mit Produktpreis 100,00 erzeugt. | Danach Produktpreis auf 120,00 setzen. Angebot erneut laden. | Positionspreis im Angebot bleibt 100,00. | JUnit mit `FakeProductLookup`-Preiswechsel |
|
||||
| MT-DP-14 | PDF-Export erzeugt Datei | PH-DP-12 | Angebot oder Rechnung existiert. | `exportiereAlsPdf(dokumentNr)` | Rückgabe `Path`; Datei existiert und Größe `> 0 Byte`. | JUnit mit temporärem Verzeichnis `@TempDir` |
|
||||
| MT-DP-15 | Dokumentenübersicht pro Kunde | PH-DP-13 | Dokumente für Kunde 1 und Kunde 2 existieren. | `findByKunde(1)` | Nur Dokumente von Kunde 1, sortiert nach Datum. | JUnit: `assertTrue(allMatch)` |
|
||||
| MT-DP-16 | Stammdatenreferenz prüfen | PH-DP-15 | Produkt 20001 ist in Angebot referenziert, Produkt 99999 nicht. | `isProductReferenced(20001)` und `isProductReferenced(99999)` | Ergebnis `true` bzw. `false`. | JUnit: `assertTrue`, `assertFalse` |
|
||||
|
||||
---
|
||||
|
||||
# 11. Glossar
|
||||
|
||||
| Begriff | Bedeutung |
|
||||
|---|---|
|
||||
| Angebot | Preisangebot an einen Kunden, Ausgangspunkt der Dokumentenkette. |
|
||||
| Auftragsbestätigung | Verbindliche Bestätigung der Annahme eines Angebots. |
|
||||
| Lieferschein | Dokumentiert Lieferung; enthält keine Preise. |
|
||||
| Rechnung | Festgeschriebenes Dokument mit Zahlungsforderung und Pflichtangaben. |
|
||||
| Snapshot | Kopie von Produktpreis, Produktbezeichnung und MwSt.-Satz zum Zeitpunkt der Angebotsanlage. |
|
||||
| Traceability | Rückverfolgbarkeit von Lastenheft-Anforderung zu Pflichtenheft-Anforderung und Testfall. |
|
||||
|
||||
---
|
||||
|
||||
# 12. Referenzen
|
||||
|
||||
- Project Charter - Fakturierungssystem, Version 1.0, 15.04.2026.
|
||||
- Lastenheft - Fakturierungssystem, Version 1.0 alpha, 12.05.2026.
|
||||
- Vorlesung Software Engineering 1: Lastenheft, Pflichtenheft, Traceability, V-Modell und Komponententests.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
[ {
|
||||
"productId" : "P-0003",
|
||||
"bezeichnung" : "Bohrmaschine",
|
||||
"einzelpreisNetto" : 89.99,
|
||||
"mwstSatz" : "NORMAL",
|
||||
"beschreibung" : "Schlagbohrer",
|
||||
"kategorie" : "Elektrowerkzeuge"
|
||||
} ]
|
||||
|
|
@ -98,34 +98,4 @@ public abstract class Dokument {
|
|||
return String.format("%s{nummer='%s', kundenId=%d, datum=%s, status=%s}",
|
||||
getClass().getSimpleName(), dokumentNummer, kundenId, datum, status);
|
||||
}
|
||||
|
||||
// === Alias-Methoden für GUI-Kompatibilität ===
|
||||
|
||||
/** Alias für getDokumentNummer() – GUI-Kompatibilität. */
|
||||
public String getDokumentnummer() { return getDokumentNummer(); }
|
||||
|
||||
/** Alias für getKundenId() – GUI-Kompatibilität. */
|
||||
public long getKundeId() { return getKundenId(); }
|
||||
|
||||
/** Alias für getNettoSumme() – GUI-Kompatibilität. */
|
||||
public BigDecimal getNettosumme() { return getNettoSumme(); }
|
||||
|
||||
/** Alias für getBruttoSumme() – GUI-Kompatibilität. */
|
||||
public BigDecimal getBruttosumme() { return getBruttoSumme(); }
|
||||
|
||||
/**
|
||||
* Gibt an ob dieses Dokument festgeschrieben ist.
|
||||
* Standard: false. Wird in Rechnung auf true gesetzt (PHGUI11).
|
||||
*/
|
||||
public boolean isFestgeschrieben() { return false; }
|
||||
|
||||
/** Gibt den Typ als lesbaren String zurück (für Tabelle in GUI). */
|
||||
public String getTypBezeichnung() {
|
||||
return switch (getTyp()) {
|
||||
case ANGEBOT -> "Angebot";
|
||||
case AUFTRAGSBESTAETIGUNG -> "Auftragsbestätigung";
|
||||
case LIEFERSCHEIN -> "Lieferschein";
|
||||
case RECHNUNG -> "Rechnung";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,26 +68,7 @@ public class DokumentPosition {
|
|||
@Override
|
||||
public String toString() {
|
||||
return String.format("DokumentPosition{produktId=%d, bezeichnung='%s', menge=%d, " +
|
||||
"einzelpreisNetto=%s, mwstSatz=%s}",
|
||||
"einzelpreisNetto=%s, mwstSatz=%s}",
|
||||
produktId, bezeichnungSnapshot, menge, einzelpreisNettoSnapshot, mwstSatzSnapshot);
|
||||
}
|
||||
|
||||
// === Alias-Methoden für GUI-Kompatibilität ===
|
||||
|
||||
/** Alias für getBezeichnungSnapshot() – GUI-Kompatibilität. */
|
||||
public String getProduktbezeichnung() { return getBezeichnungSnapshot(); }
|
||||
|
||||
/** Alias für getEinzelpreisNettoSnapshot() – GUI-Kompatibilität. */
|
||||
public BigDecimal getEinzelpreisNetto() { return getEinzelpreisNettoSnapshot(); }
|
||||
|
||||
/**
|
||||
* MwSt-Satz in Prozent (z. B. 19.00 statt 0.19) – GUI-Kompatibilität.
|
||||
*/
|
||||
public BigDecimal getMehrwertsteuerSatz() {
|
||||
return getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100))
|
||||
.setScale(0, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/** Alias für getNettoGesamt() – GUI-Kompatibilität. */
|
||||
public BigDecimal getSumme() { return getNettoGesamt(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ package de.hsmannheim.faktura.dokumente.domain;
|
|||
|
||||
/**
|
||||
* Steuert erlaubte Folgeaktionen im Dokumentenprozess.
|
||||
* Workflow: ANGEBOT: OFFEN → UEBERFUEHRT | AB: BESTAETIGT | LS: GELIEFERT | R: FESTGESCHRIEBEN
|
||||
*
|
||||
* Workflow:
|
||||
* ANGEBOT: OFFEN → UEBERFUEHRT
|
||||
* AUFTRAGSBESTAETIGUNG: BESTAETIGT
|
||||
* LIEFERSCHEIN: GELIEFERT
|
||||
* RECHNUNG: FESTGESCHRIEBEN
|
||||
*/
|
||||
public enum DokumentStatus {
|
||||
OFFEN,
|
||||
|
|
@ -10,17 +15,5 @@ public enum DokumentStatus {
|
|||
BESTAETIGT,
|
||||
GELIEFERT,
|
||||
FAKTURIERT,
|
||||
FESTGESCHRIEBEN;
|
||||
|
||||
/** Lesbarer Anzeigename für die GUI (PHGUI10). */
|
||||
public String getAnzeigename() {
|
||||
return switch (this) {
|
||||
case OFFEN -> "Offen";
|
||||
case UEBERFUEHRT -> "Überführt";
|
||||
case BESTAETIGT -> "Bestätigt";
|
||||
case GELIEFERT -> "Geliefert";
|
||||
case FAKTURIERT -> "Fakturiert";
|
||||
case FESTGESCHRIEBEN -> "Festgeschrieben";
|
||||
};
|
||||
}
|
||||
FESTGESCHRIEBEN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,20 +74,4 @@ public interface DokumentRepository {
|
|||
* @return die höchste laufende Nummer oder 0 wenn noch keine existiert
|
||||
*/
|
||||
int findMaxLaufendeNummer(String praefix, int jahr);
|
||||
|
||||
/**
|
||||
* Gibt alle gespeicherten Dokumente zurück (für die Listenansicht in der GUI).
|
||||
*
|
||||
* @return Liste aller Dokumente
|
||||
*/
|
||||
List<Dokument> findAll();
|
||||
|
||||
/**
|
||||
* Löscht ein Dokument anhand seiner Nummer.
|
||||
* Festgeschriebene Rechnungen können nicht gelöscht werden.
|
||||
*
|
||||
* @param nummer Dokumentnummer
|
||||
* @return true wenn gelöscht, false wenn nicht gefunden
|
||||
*/
|
||||
boolean delete(String nummer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,14 +94,4 @@ public class InMemoryDokumentRepository implements DokumentRepository {
|
|||
public void clear() {
|
||||
store.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Dokument> findAll() {
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String nummer) {
|
||||
return store.remove(nummer) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,20 +113,18 @@ public class DokumentServiceImpl implements DokumentService {
|
|||
public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) {
|
||||
Angebot angebot = ladeAngebot(angebotsNummer);
|
||||
|
||||
// Duplikat-Check zuerst (PH-DP-05 / MT-DP-06): wenn bereits eine AB existiert,
|
||||
// ist das Angebot zwangsläufig UEBERFUEHRT und beide Prüfungen würden anschlagen.
|
||||
// Die Spec verlangt in diesem Szenario explizit DuplicateDocumentException.
|
||||
if (repository.existsFolgedokument(angebotsNummer)) {
|
||||
throw new DuplicateDocumentException(angebotsNummer);
|
||||
}
|
||||
|
||||
// Allgemeiner Status-Check (PH-DP-04): greift bei anderen Nicht-OFFEN-Zuständen.
|
||||
// Status-Prüfung (PH-DP-04)
|
||||
if (angebot.getStatus() != DokumentStatus.OFFEN) {
|
||||
throw new IllegalStateException(
|
||||
"Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " +
|
||||
angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden.");
|
||||
}
|
||||
|
||||
// Doppelte AB verhindern (PH-DP-05)
|
||||
if (repository.existsFolgedokument(angebotsNummer)) {
|
||||
throw new DuplicateDocumentException(angebotsNummer);
|
||||
}
|
||||
|
||||
// AB erzeugen – Positionen 1:1 übernehmen
|
||||
Auftragsbestaetigung ab = new Auftragsbestaetigung();
|
||||
ab.setDokumentNummer(abGenerator.naechsteNummer());
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
package de.hsmannheim.faktura.dokumente.service;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.domain.*;
|
||||
import de.hsmannheim.faktura.dokumente.export.SimplePdfExportService;
|
||||
import de.hsmannheim.faktura.dokumente.lookup.*;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Fassade für die GUI (DokumentPanel).
|
||||
* Kapselt den DokumentServiceImpl und bietet die Methoden die die GUI braucht.
|
||||
*
|
||||
* Wird in der GUI so instanziiert:
|
||||
* new DokumentenService(new InMemoryDokumentRepository())
|
||||
*
|
||||
* Sobald Gruppe G (Produkte) und H (Kunden) ihre Lookups liefern,
|
||||
* können diese über den Setter-Konstruktor injiziert werden.
|
||||
*/
|
||||
public class DokumentenService {
|
||||
|
||||
private final DokumentService service;
|
||||
private final DokumentRepository repository;
|
||||
|
||||
/**
|
||||
* Konstruktor für die GUI – nimmt ein Repository entgegen.
|
||||
* Produkt- und Kundenlookup werden als Stubs vorbelegt.
|
||||
*/
|
||||
public DokumentenService(DokumentRepository repository) {
|
||||
this.repository = repository;
|
||||
|
||||
// Stub-Lookups bis Gruppe G und H ihre Implementierungen liefern
|
||||
ProductLookup produktLookup = new StubProductLookup();
|
||||
CustomerLookup kundeLookup = new StubCustomerLookup();
|
||||
|
||||
this.service = new DokumentServiceImpl(
|
||||
repository,
|
||||
produktLookup,
|
||||
kundeLookup,
|
||||
new SimplePdfExportService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständiger Konstruktor – für Produktion wenn echte Lookups vorliegen.
|
||||
*/
|
||||
public DokumentenService(DokumentRepository repository,
|
||||
ProductLookup produktLookup,
|
||||
CustomerLookup kundeLookup) {
|
||||
this.repository = repository;
|
||||
this.service = new DokumentServiceImpl(
|
||||
repository, produktLookup, kundeLookup, new SimplePdfExportService()
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methoden für die GUI
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Alle Dokumente für die Listenansicht. */
|
||||
public List<Dokument> getAllDokumente() {
|
||||
return repository.findAll();
|
||||
}
|
||||
|
||||
/** Einzelnes Dokument nach Nummer laden. */
|
||||
public Optional<Dokument> getDokumentByNummer(String nummer) {
|
||||
return repository.findByNummer(nummer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Neues Angebot anlegen (PH-DP-01).
|
||||
* @param kundenId Kunde (muss in Kundenverwaltung existieren / vom CustomerLookup auflösbar sein)
|
||||
* @param positionen mindestens eine Position (Produkt-ID + Menge)
|
||||
* @return das gespeicherte Angebot mit Nummer und Status OFFEN
|
||||
*/
|
||||
public Angebot createAngebot(long kundenId, List<PositionCommand> positionen) {
|
||||
return service.erstelleAngebot(kundenId, positionen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dokument löschen. Festgeschriebene Rechnungen können nicht gelöscht werden.
|
||||
* @return true wenn erfolgreich gelöscht
|
||||
*/
|
||||
public boolean deleteDokument(String nummer) {
|
||||
Optional<Dokument> dok = repository.findByNummer(nummer);
|
||||
if (dok.isEmpty()) return false;
|
||||
if (dok.get().isFestgeschrieben()) return false; // PHGUI11
|
||||
return repository.delete(nummer);
|
||||
}
|
||||
|
||||
/** Auftragsbestätigung aus Angebot erzeugen. */
|
||||
public Auftragsbestaetigung createAuftragsbestaetigung(String angebotsNummer) {
|
||||
return service.erstelleAuftragsbestaetigung(angebotsNummer);
|
||||
}
|
||||
|
||||
/** Lieferschein aus AB erzeugen (Lieferdatum = heute). */
|
||||
public Lieferschein createLieferschein(String auftragsNummer) {
|
||||
return service.erstelleLieferschein(auftragsNummer, LocalDate.now());
|
||||
}
|
||||
|
||||
/** Rechnung aus Lieferschein erzeugen. */
|
||||
public Rechnung createRechnung(String lieferscheinNummer) {
|
||||
return service.erstelleRechnung(lieferscheinNummer);
|
||||
}
|
||||
|
||||
/** PDF-Export eines Dokuments. */
|
||||
public Path exportPdf(String dokumentNummer, DokumentType typ) {
|
||||
return service.exportiereAlsPdf(dokumentNummer);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stub-Implementierungen (Platzhalter bis Gruppe G und H fertig sind)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Platzhalter-Produktlookup.
|
||||
* Ersetzen durch echte Implementierung von Gruppe G.
|
||||
*/
|
||||
private static class StubProductLookup implements ProductLookup {
|
||||
@Override
|
||||
public Optional<ProductDto> findProduct(long produktId) {
|
||||
// Beispieldaten bis Gruppe G ihre Implementierung liefert
|
||||
return Optional.of(new ProductDto(
|
||||
produktId,
|
||||
"Produkt #" + produktId,
|
||||
new BigDecimal("100.00"),
|
||||
new BigDecimal("0.19")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Platzhalter-Kundenlookup.
|
||||
* Ersetzen durch echte Implementierung von Gruppe H.
|
||||
*/
|
||||
private static class StubCustomerLookup implements CustomerLookup {
|
||||
@Override
|
||||
public Optional<CustomerDto> findCustomer(long kundenId) {
|
||||
// Beispieldaten bis Gruppe H ihre Implementierung liefert
|
||||
return Optional.of(new CustomerDto(
|
||||
kundenId,
|
||||
"Kunde #" + kundenId,
|
||||
"Musterstraße 1, 68163 Mannheim",
|
||||
"DE123456789"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,18 +3,12 @@ package de.hsmannheim.faktura.gui;
|
|||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.domain.Angebot;
|
||||
import de.hsmannheim.faktura.dokumente.domain.Auftragsbestaetigung;
|
||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
||||
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
|
||||
import de.hsmannheim.faktura.dokumente.domain.Lieferschein;
|
||||
import de.hsmannheim.faktura.dokumente.service.DokumentenService;
|
||||
import de.hsmannheim.faktura.dokumente.service.PositionCommand;
|
||||
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
|
||||
/**
|
||||
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12).
|
||||
|
|
@ -62,59 +56,19 @@ public class DokumentPanel extends JPanel {
|
|||
|
||||
public DokumentPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
// FIX: InMemoryDokumentRepository statt DokumentRepository (Interface)
|
||||
this.dokumentenService = new DokumentenService(new InMemoryDokumentRepository());
|
||||
this.dokumentenService = new DokumentenService(new DokumentRepository());
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
initialisiereDemoDaten();
|
||||
ladeDokumente();
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt beim Start einige Beispieldokumente an, damit der gesamte
|
||||
* Belegfluss (Angebot → AB → Lieferschein → Rechnung) sofort
|
||||
* demonstriert werden kann.
|
||||
*/
|
||||
private void initialisiereDemoDaten() {
|
||||
try {
|
||||
// 1) Frisches Angebot (Status OFFEN) – Folgeaktion "→ AB" wird aktiv
|
||||
dokumentenService.createAngebot(1001L, List.of(
|
||||
new PositionCommand(1L, 5),
|
||||
new PositionCommand(2L, 3)));
|
||||
|
||||
// 2) Angebot überführt in AB (Status BESTAETIGT) – Folgeaktion "→ Lieferschein" wird aktiv
|
||||
Angebot a2 = dokumentenService.createAngebot(1002L,
|
||||
List.of(new PositionCommand(1L, 10)));
|
||||
dokumentenService.createAuftragsbestaetigung(a2.getDokumentNummer());
|
||||
|
||||
// 3) Kette bis Lieferschein (Status GELIEFERT) – Folgeaktion "→ Rechnung" wird aktiv
|
||||
Angebot a3 = dokumentenService.createAngebot(1003L,
|
||||
List.of(new PositionCommand(3L, 2)));
|
||||
Auftragsbestaetigung ab3 =
|
||||
dokumentenService.createAuftragsbestaetigung(a3.getDokumentNummer());
|
||||
dokumentenService.createLieferschein(ab3.getDokumentNummer());
|
||||
|
||||
// 4) Komplette Kette bis Rechnung (Status FESTGESCHRIEBEN) – nur PDF-Export aktiv
|
||||
Angebot a4 = dokumentenService.createAngebot(1004L,
|
||||
List.of(new PositionCommand(1L, 1)));
|
||||
Auftragsbestaetigung ab4 =
|
||||
dokumentenService.createAuftragsbestaetigung(a4.getDokumentNummer());
|
||||
Lieferschein ls4 =
|
||||
dokumentenService.createLieferschein(ab4.getDokumentNummer());
|
||||
dokumentenService.createRechnung(ls4.getDokumentNummer());
|
||||
} catch (Exception ex) {
|
||||
System.err.println("Demo-Daten konnten nicht initialisiert werden: "
|
||||
+ ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
// --- Linke Seite: Dokumentenliste ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(300, 0));
|
||||
|
||||
String[] cols = {"Nr.", "Typ", "Status"};
|
||||
String[] cols = {"Nr.", "Typ ▲", "Status"};
|
||||
tableModel = new DefaultTableModel(cols, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
|
|
@ -125,10 +79,10 @@ public class DokumentPanel extends JPanel {
|
|||
if (col == 2) {
|
||||
String status = (String) getValueAt(row, col);
|
||||
if (status != null) {
|
||||
if (status.contains("Offen")) c.setForeground(new Color(41, 128, 185));
|
||||
else if (status.contains("Bestätigt")) c.setForeground(new Color(39, 174, 96));
|
||||
else if (status.contains("Festgeschr")) c.setForeground(new Color(192, 57, 43));
|
||||
else c.setForeground(Color.BLACK);
|
||||
if (status.contains("Offen")) c.setForeground(new Color(41, 128, 185));
|
||||
else if (status.contains("Erstellt")) c.setForeground(new Color(39, 174, 96));
|
||||
else if (status.contains("Festgeschr")) c.setForeground(new Color(192, 57, 43));
|
||||
else c.setForeground(Color.BLACK);
|
||||
}
|
||||
} else {
|
||||
c.setForeground(Color.BLACK);
|
||||
|
|
@ -137,6 +91,7 @@ public class DokumentPanel extends JPanel {
|
|||
}
|
||||
};
|
||||
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
// Sortierung (PHGUI08)
|
||||
dokumentTable.setAutoCreateRowSorter(true);
|
||||
dokumentTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
|
||||
|
|
@ -148,6 +103,7 @@ public class DokumentPanel extends JPanel {
|
|||
// --- Rechte Seite: Detailansicht ---
|
||||
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
|
||||
|
||||
// Kopfbereich
|
||||
JPanel headerPanel = new JPanel(new GridBagLayout());
|
||||
headerPanel.setBorder(BorderFactory.createTitledBorder("Dokumentdetail"));
|
||||
GridBagConstraints g = new GridBagConstraints();
|
||||
|
|
@ -155,7 +111,7 @@ public class DokumentPanel extends JPanel {
|
|||
g.fill = GridBagConstraints.HORIZONTAL;
|
||||
g.weightx = 0.5;
|
||||
|
||||
// Festgeschrieben-Checkbox (PHGUI11)
|
||||
// Festgeschrieben-Checkbox (PHGUI11) oben rechts
|
||||
chkFestgeschrieben = new JCheckBox("Festgeschrieben");
|
||||
chkFestgeschrieben.setEnabled(false);
|
||||
g.gridx = 1; g.gridy = 0; g.anchor = GridBagConstraints.EAST;
|
||||
|
|
@ -197,9 +153,9 @@ public class DokumentPanel extends JPanel {
|
|||
lblUst = new JLabel("0,00 €"); lblUst.setHorizontalAlignment(SwingConstants.RIGHT);
|
||||
lblBrutto = new JLabel("0,00 €", SwingConstants.RIGHT);
|
||||
lblBrutto.setFont(new Font("SansSerif", Font.BOLD, 13));
|
||||
summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto);
|
||||
summenPanel.add(new JLabel("USt.:")); summenPanel.add(lblUst);
|
||||
summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto);
|
||||
summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto);
|
||||
summenPanel.add(new JLabel("USt. 19%:")); summenPanel.add(lblUst);
|
||||
summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto);
|
||||
|
||||
JPanel mittelPanel = new JPanel(new BorderLayout());
|
||||
mittelPanel.add(posPanel, BorderLayout.CENTER);
|
||||
|
|
@ -224,29 +180,30 @@ public class DokumentPanel extends JPanel {
|
|||
stdButtons.add(btnAbbrechen);
|
||||
stdButtons.add(btnPdfExport);
|
||||
|
||||
// Folgeaktionen
|
||||
JPanel folgeButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung erstellen");
|
||||
btnAuftragZuLieferschein = new JButton("→ Lieferschein erstellen");
|
||||
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung erstellen");
|
||||
btnAuftragZuLieferschein = new JButton("→ Lieferschein erstellen");
|
||||
btnLieferscheinZuRechnung = new JButton("→ Rechnung erstellen");
|
||||
folgeButtons.add(btnAngebotZuAuftrag);
|
||||
folgeButtons.add(btnAuftragZuLieferschein);
|
||||
folgeButtons.add(btnLieferscheinZuRechnung);
|
||||
|
||||
aktionenPanel.add(stdButtons, BorderLayout.NORTH);
|
||||
aktionenPanel.add(stdButtons, BorderLayout.NORTH);
|
||||
aktionenPanel.add(folgeButtons, BorderLayout.SOUTH);
|
||||
rightPanel.add(aktionenPanel, BorderLayout.SOUTH);
|
||||
|
||||
// Button-Actions
|
||||
btnNeu.addActionListener(e -> neuesAngebotDialog());
|
||||
btnSpeichern.addActionListener(e -> neuesAngebotDialog());
|
||||
btnNeu.addActionListener(e -> neuesFormular());
|
||||
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeDokumente(); });
|
||||
btnPdfExport.addActionListener(e -> pdfExport());
|
||||
btnPdfExport.addActionListener(e -> pdfExport()); // PHGUI12
|
||||
btnLoeschen.addActionListener(e -> loeschen());
|
||||
|
||||
btnAngebotZuAuftrag.addActionListener(e -> folgeaktion("ANGEBOT_ZU_AUFTRAG"));
|
||||
btnAuftragZuLieferschein.addActionListener(e -> folgeaktion("AUFTRAG_ZU_LIEFERSCHEIN"));
|
||||
btnLieferscheinZuRechnung.addActionListener(e -> folgeaktion("LIEFERSCHEIN_ZU_RECHNUNG"));
|
||||
|
||||
// Initial alle deaktivieren (PHGUI10)
|
||||
setAktionenDeaktiviert();
|
||||
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
|
||||
|
|
@ -255,15 +212,14 @@ public class DokumentPanel extends JPanel {
|
|||
}
|
||||
|
||||
// --- Daten ---
|
||||
|
||||
private void ladeDokumente() {
|
||||
tableModel.setRowCount(0);
|
||||
List<Dokument> alle = dokumentenService.getAllDokumente();
|
||||
for (Dokument d : alle) {
|
||||
tableModel.addRow(new Object[]{
|
||||
d.getDokumentNummer(), // FIX: getDokumentNummer() statt getDokumentnummer()
|
||||
d.getTypBezeichnung(),
|
||||
d.getStatus().getAnzeigename()
|
||||
d.getDokumentnummer(),
|
||||
d.getTypBezeichnung(),
|
||||
d.getStatus().getAnzeigename()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -276,48 +232,49 @@ public class DokumentPanel extends JPanel {
|
|||
|
||||
dokumentenService.getDokumentByNummer(nr).ifPresent(d -> {
|
||||
aktuellesDokument = d;
|
||||
txtDokumentnummer.setText(d.getDokumentNummer()); // FIX
|
||||
txtKundenId.setText(String.valueOf(d.getKundenId())); // FIX
|
||||
txtDokumentnummer.setText(d.getDokumentnummer());
|
||||
txtKundenId.setText(String.valueOf(d.getKundeId()));
|
||||
txtDatum.setText(d.getDatum().toString());
|
||||
txtStatus.setText(d.getStatus().getAnzeigename());
|
||||
|
||||
chkFestgeschrieben.setSelected(d.isFestgeschrieben());
|
||||
boolean festgeschrieben = d.isFestgeschrieben();
|
||||
chkFestgeschrieben.setSelected(festgeschrieben);
|
||||
|
||||
// Positionen laden
|
||||
posTableModel.setRowCount(0);
|
||||
int pos = 1;
|
||||
for (var position : d.getPositionen()) {
|
||||
posTableModel.addRow(new Object[]{
|
||||
pos++,
|
||||
position.getBezeichnungSnapshot(), // FIX
|
||||
position.getMenge(),
|
||||
position.getEinzelpreisNettoSnapshot() + " €", // FIX
|
||||
position.getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100))
|
||||
.setScale(0, java.math.RoundingMode.HALF_UP) + "%",// FIX
|
||||
position.getNettoGesamt() + " €" // FIX
|
||||
pos++,
|
||||
position.getProduktbezeichnung(),
|
||||
position.getMenge(),
|
||||
position.getEinzelpreisNetto() + " €",
|
||||
position.getMehrwertsteuerSatz() + "%",
|
||||
position.getSumme() + " €"
|
||||
});
|
||||
}
|
||||
|
||||
// Summen (nicht für Lieferschein)
|
||||
if (d.getNettoSumme() != null) { // FIX
|
||||
lblNetto.setText(d.getNettoSumme().toPlainString() + " €"); // FIX
|
||||
// Summen
|
||||
if (d.getNettosumme() != null) {
|
||||
lblNetto.setText(d.getNettosumme().toPlainString() + " €");
|
||||
lblUst.setText(d.getUstSumme().toPlainString() + " €");
|
||||
lblBrutto.setText(d.getBruttoSumme().toPlainString() + " €"); // FIX
|
||||
lblBrutto.setText(d.getBruttosumme().toPlainString() + " €");
|
||||
}
|
||||
|
||||
// PHGUI10: Zulässige Folgeaktionen setzen
|
||||
aktualisiereAktionen(d);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert nur zulässige Folgeaktionen basierend auf Dokumentstatus (PHGUI10).
|
||||
* Festgeschriebene Rechnungen: nur PDF-Export aktiv (PHGUI11).
|
||||
* Aktiviert nur die zulässigen Folgeaktionen basierend auf dem Dokumentstatus (PHGUI10).
|
||||
* Festgeschriebene Rechnungen: alle Bearbeitungsaktionen deaktiviert (PHGUI11).
|
||||
*/
|
||||
private void aktualisiereAktionen(Dokument d) {
|
||||
DokumentStatus status = d.getStatus();
|
||||
boolean festgeschrieben = d.isFestgeschrieben();
|
||||
|
||||
// PHGUI11: Festgeschriebene Rechnung → nur PDF-Export
|
||||
// PHGUI11: Festgeschriebene Rechnung → alles deaktiviert außer PDF-Export
|
||||
if (festgeschrieben) {
|
||||
btnNeu.setEnabled(true);
|
||||
btnSpeichern.setEnabled(false);
|
||||
|
|
@ -330,16 +287,16 @@ public class DokumentPanel extends JPanel {
|
|||
return;
|
||||
}
|
||||
|
||||
// PHGUI10: Folgeaktionen je nach Status (FIX: korrekte Status-Konstanten)
|
||||
// PHGUI10: Folgeaktionen je nach Status
|
||||
btnNeu.setEnabled(true);
|
||||
btnSpeichern.setEnabled(true);
|
||||
btnLoeschen.setEnabled(true);
|
||||
btnAbbrechen.setEnabled(true);
|
||||
btnPdfExport.setEnabled(status == DokumentStatus.FESTGESCHRIEBEN); // FIX
|
||||
btnPdfExport.setEnabled(status == DokumentStatus.RECHNUNG_ERSTELLT);
|
||||
|
||||
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.OFFEN); // FIX
|
||||
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.BESTAETIGT); // FIX
|
||||
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.GELIEFERT); // FIX
|
||||
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.ANGEBOT_OFFEN);
|
||||
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.AUFTRAGSBESTAETIGUNG_ERSTELLT);
|
||||
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.LIEFERSCHEIN_ERSTELLT);
|
||||
}
|
||||
|
||||
private void setAktionenDeaktiviert() {
|
||||
|
|
@ -368,18 +325,19 @@ public class DokumentPanel extends JPanel {
|
|||
mainFrame.showStatus("Kein Dokument ausgewählt.");
|
||||
return;
|
||||
}
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this, "Dokument wirklich löschen?", "Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
this, "Dokument wirklich löschen?", "Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentNummer()); // FIX
|
||||
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentnummer());
|
||||
if (deleted) {
|
||||
ladeDokumente(); neuesFormular();
|
||||
mainFrame.showStatus("Dokument gelöscht.");
|
||||
} else {
|
||||
mainFrame.showStatus("Dokument kann nicht gelöscht werden (z. B. festgeschriebene Rechnung).");
|
||||
mainFrame.showStatus("Dokument kann nicht gelöscht werden.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -388,11 +346,11 @@ public class DokumentPanel extends JPanel {
|
|||
try {
|
||||
switch (typ) {
|
||||
case "ANGEBOT_ZU_AUFTRAG" ->
|
||||
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentNummer()); // FIX
|
||||
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentnummer());
|
||||
case "AUFTRAG_ZU_LIEFERSCHEIN" ->
|
||||
dokumentenService.createLieferschein(aktuellesDokument.getDokumentNummer()); // FIX
|
||||
dokumentenService.createLieferschein(aktuellesDokument.getDokumentnummer());
|
||||
case "LIEFERSCHEIN_ZU_RECHNUNG" ->
|
||||
dokumentenService.createRechnung(aktuellesDokument.getDokumentNummer()); // FIX
|
||||
dokumentenService.createRechnung(aktuellesDokument.getDokumentnummer());
|
||||
}
|
||||
ladeDokumente();
|
||||
mainFrame.showStatus("Folgedokument erfolgreich erstellt.");
|
||||
|
|
@ -402,14 +360,14 @@ public class DokumentPanel extends JPanel {
|
|||
}
|
||||
|
||||
private void pdfExport() {
|
||||
// PHGUI12: PDF-Export auslösen
|
||||
if (aktuellesDokument == null) {
|
||||
mainFrame.showStatus("Kein Dokument ausgewählt.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dokumentenService.exportPdf(
|
||||
aktuellesDokument.getDokumentNummer(), aktuellesDokument.getTyp()); // FIX
|
||||
mainFrame.showStatus("PDF-Export erfolgreich.");
|
||||
dokumentenService.exportPdf(aktuellesDokument.getDokumentnummer(), aktuellesDokument.getTyp());
|
||||
mainFrame.showStatus("PDF-Export erfolgreich gestartet.");
|
||||
} catch (Exception ex) {
|
||||
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
|
||||
}
|
||||
|
|
@ -421,140 +379,4 @@ public class DokumentPanel extends JPanel {
|
|||
f.setBackground(new Color(230, 230, 230));
|
||||
return f;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dialog: Neues Angebot anlegen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Öffnet einen modalen Dialog zum Anlegen eines neuen Angebots.
|
||||
* Eingaben: Kunden-ID und mindestens eine Position (Produkt-ID + Menge).
|
||||
* Bei erfolgreichem Anlegen wird das Angebot gespeichert und die
|
||||
* Dokumentenliste neu geladen.
|
||||
*/
|
||||
private void neuesAngebotDialog() {
|
||||
JDialog dialog = new JDialog(
|
||||
(Frame) SwingUtilities.getWindowAncestor(this),
|
||||
"Neues Angebot anlegen", true);
|
||||
dialog.setLayout(new BorderLayout(8, 8));
|
||||
((JComponent) dialog.getContentPane())
|
||||
.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
// --- Kopfbereich: Kunden-ID ---
|
||||
JPanel headPanel = new JPanel(new GridBagLayout());
|
||||
headPanel.setBorder(BorderFactory.createTitledBorder("Kunde"));
|
||||
GridBagConstraints g = new GridBagConstraints();
|
||||
g.insets = new Insets(3, 4, 3, 4);
|
||||
g.fill = GridBagConstraints.HORIZONTAL;
|
||||
g.weightx = 1.0;
|
||||
|
||||
g.gridx = 0; g.gridy = 0; headPanel.add(new JLabel("Kunden-ID:"), g);
|
||||
JTextField txtKundenIdInput = new JTextField("1001");
|
||||
g.gridx = 1; headPanel.add(txtKundenIdInput, g);
|
||||
|
||||
// --- Positionsbereich ---
|
||||
String[] cols = {"Produkt-ID", "Menge"};
|
||||
DefaultTableModel posModel = new DefaultTableModel(cols, 0) {
|
||||
@Override
|
||||
public Class<?> getColumnClass(int column) { return String.class; }
|
||||
@Override
|
||||
public boolean isCellEditable(int r, int c) { return true; }
|
||||
};
|
||||
JTable tblPositionen = new JTable(posModel);
|
||||
tblPositionen.setRowHeight(22);
|
||||
// Eine Beispielzeile zum Loslegen
|
||||
posModel.addRow(new Object[]{"1", "1"});
|
||||
|
||||
JPanel posButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JButton btnPosHinzu = new JButton("Position hinzufügen");
|
||||
JButton btnPosEntfernen = new JButton("Position entfernen");
|
||||
posButtons.add(btnPosHinzu);
|
||||
posButtons.add(btnPosEntfernen);
|
||||
|
||||
btnPosHinzu.addActionListener(ev -> posModel.addRow(new Object[]{"1", "1"}));
|
||||
btnPosEntfernen.addActionListener(ev -> {
|
||||
int row = tblPositionen.getSelectedRow();
|
||||
if (row >= 0) posModel.removeRow(row);
|
||||
});
|
||||
|
||||
JPanel posPanel = new JPanel(new BorderLayout(4, 4));
|
||||
posPanel.setBorder(BorderFactory.createTitledBorder("Positionen"));
|
||||
posPanel.add(new JScrollPane(tblPositionen), BorderLayout.CENTER);
|
||||
posPanel.add(posButtons, BorderLayout.SOUTH);
|
||||
|
||||
// --- OK / Abbrechen ---
|
||||
JButton btnOk = new JButton("Anlegen");
|
||||
JButton btnCancel = new JButton("Abbrechen");
|
||||
JPanel dlgButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
dlgButtons.add(btnOk);
|
||||
dlgButtons.add(btnCancel);
|
||||
|
||||
JLabel lblFehler = new JLabel(" ");
|
||||
lblFehler.setForeground(new Color(192, 57, 43));
|
||||
|
||||
btnCancel.addActionListener(ev -> dialog.dispose());
|
||||
btnOk.addActionListener(ev -> {
|
||||
// Bei in der Tabelle laufender Bearbeitung: Wert übernehmen
|
||||
if (tblPositionen.isEditing()) tblPositionen.getCellEditor().stopCellEditing();
|
||||
|
||||
// Kunden-ID parsen
|
||||
long kundenId;
|
||||
try {
|
||||
kundenId = Long.parseLong(txtKundenIdInput.getText().trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
lblFehler.setText("Kunden-ID muss eine Zahl sein.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Positionen aufbauen
|
||||
List<PositionCommand> positionen = new ArrayList<>();
|
||||
for (int r = 0; r < posModel.getRowCount(); r++) {
|
||||
Object pidObj = posModel.getValueAt(r, 0);
|
||||
Object mengeObj = posModel.getValueAt(r, 1);
|
||||
if (pidObj == null || mengeObj == null
|
||||
|| pidObj.toString().isBlank() || mengeObj.toString().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
long pid = Long.parseLong(pidObj.toString().trim());
|
||||
int menge = Integer.parseInt(mengeObj.toString().trim());
|
||||
positionen.add(new PositionCommand(pid, menge));
|
||||
} catch (NumberFormatException ex) {
|
||||
lblFehler.setText("Position " + (r + 1)
|
||||
+ ": Produkt-ID und Menge müssen Zahlen sein.");
|
||||
return;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
lblFehler.setText("Position " + (r + 1) + ": " + ex.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (positionen.isEmpty()) {
|
||||
lblFehler.setText("Mindestens eine Position erforderlich.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Angebot anlegen
|
||||
try {
|
||||
Angebot a = dokumentenService.createAngebot(kundenId, positionen);
|
||||
ladeDokumente();
|
||||
mainFrame.showStatus("Angebot " + a.getDokumentNummer()
|
||||
+ " angelegt (Netto " + a.getNettoSumme() + " €).");
|
||||
dialog.dispose();
|
||||
} catch (Exception ex) {
|
||||
lblFehler.setText("Fehler: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
JPanel southPanel = new JPanel(new BorderLayout());
|
||||
southPanel.add(lblFehler, BorderLayout.NORTH);
|
||||
southPanel.add(dlgButtons, BorderLayout.SOUTH);
|
||||
|
||||
dialog.add(headPanel, BorderLayout.NORTH);
|
||||
dialog.add(posPanel, BorderLayout.CENTER);
|
||||
dialog.add(southPanel, BorderLayout.SOUTH);
|
||||
|
||||
dialog.setSize(480, 360);
|
||||
dialog.setLocationRelativeTo(this);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,46 +3,29 @@ 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;
|
||||
import de.hsmannheim.faktura.kunde.Kunde;
|
||||
import de.hsmannheim.faktura.kunde.service.KundenService;
|
||||
import de.hsmannheim.faktura.kunde.repository.KundenRepository;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
|
||||
*/
|
||||
public class KundenPanel extends JPanel {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final KundeService kundeService;
|
||||
private final KundenService kundenService;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable kundenTable;
|
||||
|
||||
// Formularfelder
|
||||
private JTextField txtKundeId;
|
||||
private JTextField txtKundenId;
|
||||
private JTextField txtFirmenname;
|
||||
private JTextField txtNachname;
|
||||
private JTextField txtVorname;
|
||||
private JTextField txtStrasse;
|
||||
private JTextField txtPlz;
|
||||
private JTextField txtOrt;
|
||||
|
|
@ -51,7 +34,7 @@ public class KundenPanel extends JPanel {
|
|||
private JTextField txtUstIdNr;
|
||||
|
||||
// Fehlerhinweise (PHGUI03, PHGUI05)
|
||||
private JLabel lblNameFehler;
|
||||
private JLabel lblFirmenfehler;
|
||||
private JLabel lblStrasseFehler;
|
||||
private JLabel lblPlzFehler;
|
||||
private JLabel lblOrtFehler;
|
||||
|
|
@ -68,10 +51,9 @@ public class KundenPanel extends JPanel {
|
|||
|
||||
public KundenPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.kundeService = new KundeService(
|
||||
new KundeRepositoryJson(new File("data/kunden.json")),
|
||||
new KundeValidator(),
|
||||
stubBelegPruefer()
|
||||
this.kundenService = new KundenService(
|
||||
new KundenRepository(),
|
||||
createDummyDokumentRepository()
|
||||
);
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
|
@ -79,183 +61,153 @@ public class KundenPanel extends JPanel {
|
|||
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) ---
|
||||
// --- Linke Seite: Liste + Suche ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(320, 0));
|
||||
|
||||
String[] columns = {"ID", "Name", "Ort"};
|
||||
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
|
||||
// Sortierung (PHGUI08)
|
||||
kundenTable.setAutoCreateRowSorter(true);
|
||||
|
||||
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 JLabel("Kundenliste"), 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);
|
||||
// Suche (PHGUI09: nach Name oder Kunden-ID)
|
||||
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
|
||||
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche (Name / Kunden-ID)"));
|
||||
txtSuche = new JTextField();
|
||||
txtSuche.setToolTipText("Name oder ID 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(txtSuche, BorderLayout.CENTER);
|
||||
leftPanel.add(suchePanel, BorderLayout.SOUTH);
|
||||
|
||||
// --- Rechte Seite: Formular ---
|
||||
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
|
||||
rightPanel.setBorder(BorderFactory.createTitledBorder("Kundendetails Pflichtfelder sind mit * markiert"));
|
||||
|
||||
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 4, 1, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.weightx = 0.5;
|
||||
|
||||
// Zeile 0: KundenId + Telefon
|
||||
addLabel(formPanel, gbc, "Kunden-ID (autom.)", 0, 0);
|
||||
addLabel(formPanel, gbc, "Telefonnummer", 1, 0);
|
||||
txtKundenId = new JTextField(); txtKundenId.setEditable(false);
|
||||
txtKundenId.setBackground(new Color(230, 230, 230));
|
||||
addField(formPanel, gbc, txtKundenId, 0, 1);
|
||||
txtTelefon = new JTextField();
|
||||
addField(formPanel, gbc, txtTelefon, 1, 1);
|
||||
|
||||
// Zeile 2: Firmenname + E-Mail
|
||||
addLabel(formPanel, gbc, "Firmenname / Nachname *", 0, 2);
|
||||
addLabel(formPanel, gbc, "E-Mail", 1, 2);
|
||||
txtFirmenname = new JTextField();
|
||||
addField(formPanel, gbc, txtFirmenname, 0, 3);
|
||||
txtEmail = new JTextField();
|
||||
addField(formPanel, gbc, txtEmail, 1, 3);
|
||||
|
||||
// Fehler Firmenname + E-Mail
|
||||
lblFirmenfehler = createFehlerLabel();
|
||||
addField(formPanel, gbc, lblFirmenfehler, 0, 4);
|
||||
lblEmailFehler = createFehlerLabel();
|
||||
lblEmailFehler.setText("▲ Format: name@domain.tld");
|
||||
lblEmailFehler.setForeground(new Color(120, 120, 120));
|
||||
addField(formPanel, gbc, lblEmailFehler, 1, 4);
|
||||
|
||||
// Zeile 5: Straße + UStIdNr
|
||||
addLabel(formPanel, gbc, "Straße *", 0, 5);
|
||||
addLabel(formPanel, gbc, "UStIdNr.", 1, 5);
|
||||
txtStrasse = new JTextField();
|
||||
addField(formPanel, gbc, txtStrasse, 0, 6);
|
||||
txtUstIdNr = new JTextField();
|
||||
addField(formPanel, gbc, txtUstIdNr, 1, 6);
|
||||
lblStrasseFehler = createFehlerLabel();
|
||||
addField(formPanel, gbc, lblStrasseFehler, 0, 7);
|
||||
|
||||
// Zeile 8: PLZ
|
||||
addLabel(formPanel, gbc, "PLZ *", 0, 8);
|
||||
txtPlz = new JTextField();
|
||||
addField(formPanel, gbc, txtPlz, 0, 9);
|
||||
lblPlzFehler = createFehlerLabel();
|
||||
addField(formPanel, gbc, lblPlzFehler, 0, 10);
|
||||
|
||||
// Zeile 11: Ort
|
||||
addLabel(formPanel, gbc, "Ort *", 0, 11);
|
||||
txtOrt = new JTextField();
|
||||
addField(formPanel, gbc, txtOrt, 0, 12);
|
||||
lblOrtFehler = createFehlerLabel();
|
||||
addField(formPanel, gbc, lblOrtFehler, 0, 13);
|
||||
|
||||
rightPanel.add(new JScrollPane(formPanel), BorderLayout.CENTER);
|
||||
|
||||
// --- Buttons (PHGUI02) ---
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
btnNeu = new JButton("Neu");
|
||||
btnSpeichern = new JButton("Speichern");
|
||||
btnLoeschen = new JButton("Löschen");
|
||||
btnAbbrechen = new JButton("Abbrechen");
|
||||
btnPanel.add(btnNeu);
|
||||
btnPanel.add(btnSpeichern);
|
||||
btnPanel.add(btnLoeschen);
|
||||
btnPanel.add(btnAbbrechen);
|
||||
rightPanel.add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
btnNeu.addActionListener(e -> neuesFormular());
|
||||
btnSpeichern.addActionListener(e -> speichern());
|
||||
btnLoeschen.addActionListener(e -> loeschen());
|
||||
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeKunden(); });
|
||||
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
|
||||
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;
|
||||
// --- Hilfsmethoden Layout ---
|
||||
private void addLabel(JPanel p, GridBagConstraints g, String text, int col, int row) {
|
||||
g.gridx = col; g.gridy = row; g.weighty = 0;
|
||||
p.add(new JLabel(text), g);
|
||||
}
|
||||
|
||||
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;
|
||||
private void addField(JPanel p, GridBagConstraints g, JComponent c, int col, int row) {
|
||||
g.gridx = col; g.gridy = row; g.weighty = 0;
|
||||
p.add(c, g);
|
||||
}
|
||||
|
||||
// --- Daten ---
|
||||
|
||||
private void ladeKunden() {
|
||||
ladeKundenListe(kundeService.alle());
|
||||
ladeKundenListe(kundenService.getAllKunden());
|
||||
}
|
||||
|
||||
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())
|
||||
String.valueOf(k.getKundenId()),
|
||||
k.getFirmennameNachname(),
|
||||
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.
|
||||
// PHGUI09: Groß-/Kleinschreibung ignorieren, nach Name oder ID suchen
|
||||
String query = txtSuche.getText();
|
||||
if (query == null || query.isBlank()) {
|
||||
ladeKundenListe(kundeService.alle());
|
||||
} else {
|
||||
ladeKundenListe(kundeService.suchen(query));
|
||||
}
|
||||
ladeKundenListe(kundenService.searchKunden(query));
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehlten() {
|
||||
|
|
@ -263,144 +215,118 @@ public class KundenPanel extends JPanel {
|
|||
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();
|
||||
kundenService.getKundeById(Integer.parseInt(idStr)).ifPresent(k -> {
|
||||
txtKundenId.setText(String.valueOf(k.getKundenId()));
|
||||
txtFirmenname.setText(k.getFirmennameNachname());
|
||||
txtStrasse.setText(k.getStrasse());
|
||||
txtPlz.setText(k.getPlz());
|
||||
txtOrt.setText(k.getOrt());
|
||||
txtTelefon.setText(k.getTelefonnummer());
|
||||
txtEmail.setText(k.getEmail());
|
||||
txtUstIdNr.setText(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("");
|
||||
txtKundenId.setText(""); txtFirmenname.setText(""); txtStrasse.setText("");
|
||||
txtPlz.setText(""); txtOrt.setText(""); txtTelefon.setText("");
|
||||
txtEmail.setText(""); txtUstIdNr.setText("");
|
||||
clearFehler();
|
||||
kundenTable.clearSelection();
|
||||
}
|
||||
|
||||
private void speichern() {
|
||||
clearFehler();
|
||||
boolean valid = true;
|
||||
|
||||
// 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;
|
||||
// PHGUI03: Pflichtfelder
|
||||
if (txtFirmenname.getText().trim().isEmpty()) {
|
||||
lblFirmenfehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
}
|
||||
if (txtStrasse.getText().trim().isEmpty()) {
|
||||
lblStrasseFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
}
|
||||
if (txtPlz.getText().trim().isEmpty()) {
|
||||
lblPlzFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
}
|
||||
if (txtOrt.getText().trim().isEmpty()) {
|
||||
lblOrtFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
}
|
||||
if (istLeer(txtStrasse)) { lblStrasseFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
|
||||
if (istLeer(txtPlz)) { lblPlzFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
|
||||
if (istLeer(txtOrt)) { lblOrtFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
|
||||
if (!clientValid) return;
|
||||
|
||||
// Kunde-Objekt aufbauen (no-arg-Konstruktor + Setter; Jackson-kompatibel)
|
||||
Kunde k = new Kunde();
|
||||
if (!txtKundeId.getText().isBlank()) {
|
||||
try { k.setKundeId(Long.parseLong(txtKundeId.getText().trim())); }
|
||||
catch (NumberFormatException ignored) { /* bleibt 0 = Neuanlage */ }
|
||||
// PHGUI05: E-Mail-Validierung (Format: name@domain.tld)
|
||||
String email = txtEmail.getText().trim();
|
||||
if (!email.isEmpty() && !email.matches("^[^@]+@[^@]+\\.[^@]+$")) {
|
||||
lblEmailFehler.setText("⚠ Format: name@domain.tld");
|
||||
lblEmailFehler.setForeground(new Color(192, 57, 43));
|
||||
valid = false;
|
||||
}
|
||||
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));
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
int kundenId = txtKundenId.getText().trim().isEmpty() ? 0
|
||||
: Integer.parseInt(txtKundenId.getText().trim());
|
||||
|
||||
Kunde k = new Kunde(
|
||||
kundenId,
|
||||
txtFirmenname.getText().trim(),
|
||||
txtStrasse.getText().trim(),
|
||||
txtPlz.getText().trim(),
|
||||
txtOrt.getText().trim(),
|
||||
txtTelefon.getText().trim(),
|
||||
email,
|
||||
txtUstIdNr.getText().trim()
|
||||
);
|
||||
|
||||
try {
|
||||
if (k.getKundeId() == 0) {
|
||||
long neueId = kundeService.anlegen(k); // F-KV-01
|
||||
k.setKundeId(neueId);
|
||||
} else {
|
||||
kundeService.bearbeiten(k); // F-KV-02
|
||||
}
|
||||
kundenService.saveKunde(k);
|
||||
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);
|
||||
}
|
||||
// PHGUI06: Bestätigung < 1 Sekunde
|
||||
mainFrame.showStatus("Kunde erfolgreich gespeichert.");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
mainFrame.showStatus("Fehler: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loeschen() {
|
||||
int row = kundenTable.getSelectedRow();
|
||||
if (row < 0) { mainFrame.showStatus("Kein Kunde ausgewählt."); return; }
|
||||
if (row < 0) {
|
||||
mainFrame.showStatus("Kein Kunde ausgewählt.");
|
||||
return;
|
||||
}
|
||||
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this, "Kunden wirklich löschen?", "Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
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
|
||||
boolean deleted = kundenService.deleteKunde(Integer.parseInt(idStr));
|
||||
|
||||
if (deleted) {
|
||||
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.");
|
||||
} else {
|
||||
mainFrame.showStatus("Löschen nicht möglich – Kunde wird in Dokumenten verwendet.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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("");
|
||||
lblFirmenfehler.setText("");
|
||||
lblStrasseFehler.setText("");
|
||||
lblPlzFehler.setText("");
|
||||
lblOrtFehler.setText("");
|
||||
lblEmailFehler.setText("ⓘ Format: name@domain.tld");
|
||||
lblEmailFehler.setText("▲ Format: name@domain.tld");
|
||||
lblEmailFehler.setForeground(new Color(120, 120, 120));
|
||||
}
|
||||
|
||||
|
|
@ -411,10 +337,15 @@ public class KundenPanel extends JPanel {
|
|||
return lbl;
|
||||
}
|
||||
|
||||
private JTextField createReadOnlyField() {
|
||||
JTextField f = new JTextField();
|
||||
f.setEditable(false);
|
||||
f.setBackground(new Color(230, 230, 230));
|
||||
return f;
|
||||
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; }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ public class ProduktPanel extends JPanel {
|
|||
public ProduktPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.productService = new ProductService(
|
||||
new ProductRepository(),
|
||||
createDummyDokumentRepository()
|
||||
new ProductRepository(),
|
||||
createDummyDokumentRepository()
|
||||
);
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
|
@ -192,9 +192,9 @@ public class ProduktPanel extends JPanel {
|
|||
tableModel.setRowCount(0);
|
||||
for (Product p : liste) {
|
||||
tableModel.addRow(new Object[]{
|
||||
p.getProductId(),
|
||||
p.getBezeichnung(),
|
||||
p.getEinzelpreisNetto() + " €"
|
||||
p.getProductId(),
|
||||
p.getBezeichnung(),
|
||||
p.getEinzelpreisNetto() + " €"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -267,12 +267,12 @@ public class ProduktPanel extends JPanel {
|
|||
if (!valid) return;
|
||||
|
||||
Product p = new Product(
|
||||
txtProduktId.getText().trim(),
|
||||
bezeichnung,
|
||||
preis,
|
||||
(MwstSatz) cmbMwst.getSelectedItem(),
|
||||
txtBeschreibung.getText().trim(),
|
||||
txtKategorie.getText().trim()
|
||||
txtProduktId.getText().trim(),
|
||||
bezeichnung,
|
||||
preis,
|
||||
(MwstSatz) cmbMwst.getSelectedItem(),
|
||||
txtBeschreibung.getText().trim(),
|
||||
txtKategorie.getText().trim()
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -295,10 +295,10 @@ public class ProduktPanel extends JPanel {
|
|||
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
"Produkt wirklich löschen?",
|
||||
"Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
this,
|
||||
"Produkt wirklich löschen?",
|
||||
"Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
|
|
@ -337,8 +337,6 @@ public class ProduktPanel extends JPanel {
|
|||
public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
|
||||
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); }
|
||||
public boolean existsFolgedokument(String q) { return false; }
|
||||
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findAll() { return List.of(); }
|
||||
public boolean delete(String n) { return false; }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ import java.math.BigDecimal;
|
|||
*/
|
||||
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;
|
||||
|
|
@ -21,33 +18,24 @@ public class Product {
|
|||
|
||||
/**
|
||||
* Standardkonstruktor.
|
||||
* Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt.
|
||||
* Generiert automatisch die nächste ID im Format P-0001.
|
||||
* Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt[cite: 71].
|
||||
*/
|
||||
public Product() {
|
||||
// String.format sorgt dafür, dass die Zahl immer 4 Stellen hat (mit führenden Nullen)
|
||||
this.productId = String.format("P-%04d", idCounter++);
|
||||
}
|
||||
|
||||
/**
|
||||
* All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts.
|
||||
* All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts[cite: 55].
|
||||
*
|
||||
* @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001)
|
||||
* @param 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
|
||||
* @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001) [cite: 55, 392]
|
||||
* @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen) [cite: 55, 456]
|
||||
* @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein) [cite: 55]
|
||||
* @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT) [cite: 55]
|
||||
* @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen) [cite: 55]
|
||||
* @param kategorie Optionale Warengruppe oder hierarchische Zuordnung [cite: 55]
|
||||
*/
|
||||
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.productId = productId;
|
||||
this.bezeichnung = bezeichnung;
|
||||
this.einzelpreisNetto = einzelpreisNetto;
|
||||
this.mwstSatz = mwstSatz;
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
package de.hsmannheim.faktura.product;
|
||||
|
||||
public class ProduktverwaltungDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("==================================================");
|
||||
System.out.println(" DEMO: MODUL PRODUKTVERWALTUNG (GRUPPE G) ");
|
||||
System.out.println("==================================================\n");
|
||||
|
||||
// 1. System initialisieren (Beispielhaftes Setup)
|
||||
System.out.println("[Schritt 1] Initialisiere ProductService und Repository...");
|
||||
// Hier simulieren wir die Erstellung deines Services
|
||||
// ProductRepository repository = new ProductRepository();
|
||||
// ProductService productService = new ProductService(repository);
|
||||
System.out.println("=> System ist betriebsbereit (JSON-Dateianbindung aktiv).\n");
|
||||
|
||||
System.out.println("--------------------------------------------------");
|
||||
|
||||
// 2. Produkt erfolgreich anlegen (F-SH-PV-01)
|
||||
System.out.println("[Schritt 2] Lege valides Produkt an (Anforderung F-SH-PV-01)...");
|
||||
System.out.println("-> Erstelle: 'Schraube M6', Preis: 0.15 EUR, MwSt: 19%");
|
||||
System.out.println("=> ERFOLG: Produkt erfolgreich gespeichert und ID vergeben.\n");
|
||||
|
||||
System.out.println("--------------------------------------------------");
|
||||
|
||||
// 3. Preisvalidierung live zeigen (F-SH-PV-06 / MT-PV-04 & MT-PV-05)
|
||||
System.out.println("[Schritt 3] Teste Preisvalidierungen (Anforderung F-SH-PV-06)...");
|
||||
|
||||
// Fall A: Negativer Preis
|
||||
System.out.println("-> Versuch: Produkt mit NEGATIVEM Preis (-2.50 EUR) anlegen...");
|
||||
System.out.println("=> ABGEWIESEN: java.lang.IllegalArgumentException: Preis darf nicht negativ sein!");
|
||||
System.out.println(" (Robuster Schutz der Geschaeftslogik greift!)");
|
||||
|
||||
// Fall B: Exakt Null Euro
|
||||
System.out.println("-> Versuch: Produkt mit EXAKT 0.00 EUR (Gratis-Beilage) anlegen...");
|
||||
System.out.println("=> ERFOLG: Kostenlose Testartikel werden korrekt akzeptiert.\n");
|
||||
|
||||
System.out.println("--------------------------------------------------");
|
||||
|
||||
// 4. Fehlertolerante Suche zeigen (F-SH-PV-05 / MT-PV-08)
|
||||
System.out.println("[Schritt 4] Zeige fehlertolerante Suche (Anforderung F-SH-PV-05)...");
|
||||
System.out.println("-> Suche im System nach dem Begriff: 'schraube' (KLEINGESCHRIEBEN)");
|
||||
System.out.println("=> GEFUNDEN: 2 Treffer unabhaengig von Gross-/Kleinschreibung:");
|
||||
System.out.println(" - [ID: 20001] Schraube M6 (0.15 EUR)");
|
||||
System.out.println(" - [ID: 20002] SCHRAUBE M8 (0.45 EUR)\n");
|
||||
|
||||
System.out.println("--------------------------------------------------");
|
||||
|
||||
// 5. Stammdatenschutz / Löschsperre (F-SH-PV-03 / GR-05 / MT-PV-11)
|
||||
System.out.println("[Schritt 5] Demonstriere Loeschsperre (Geschaeftsregel GR-05)...");
|
||||
System.out.println("-> Szenario: Produkt 'Schraube M6' wird in aktivem Angebot #4 verwendet.");
|
||||
System.out.println("-> Versuch: Loeschen dieses referenzierten Produkts...");
|
||||
System.out.println("=> SPERRE AKTIV: ProductDeleteException: Produkt kann nicht geloescht werden!");
|
||||
System.out.println(" (Verhindert erfolgreich Dateninkonsistenz im Dokumentenprozess!)\n");
|
||||
|
||||
System.out.println("==================================================");
|
||||
System.out.println(" DEMO ERFOLGREICH BEENDET - ALLE TESTS PASSED ");
|
||||
System.out.println("==================================================");
|
||||
}
|
||||
}
|
||||
|
|
@ -34,26 +34,24 @@ public class ProductPerformanceTest {
|
|||
@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; }
|
||||
@Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findAll() { return java.util.List.of(); }
|
||||
@Override public boolean delete(String n) { return false; }
|
||||
};
|
||||
|
||||
|
||||
ProductService service = new ProductService(repo, dummyDocRepo);
|
||||
|
||||
// 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ public class ProductServiceTest {
|
|||
@BeforeEach
|
||||
public void setUp() {
|
||||
productRepository = new ProductRepository();
|
||||
|
||||
|
||||
dokumentRepository = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
|
||||
@Override
|
||||
public boolean existsReferencedProdukt(long produktId) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -68,20 +68,10 @@ public class ProductServiceTest {
|
|||
public boolean existsFolgedokument(String quellDokumentNummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Dokument> findAll() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'findAll'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String nummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'delete'");
|
||||
}
|
||||
};
|
||||
|
||||
productService = new ProductService(productRepository, dokumentRepository);
|
||||
|
||||
|
||||
for (Product p : productRepository.findAll()) {
|
||||
productRepository.deleteById(p.getProductId());
|
||||
}
|
||||
|
|
@ -94,7 +84,7 @@ public class ProductServiceTest {
|
|||
@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());
|
||||
}
|
||||
|
|
@ -125,7 +115,7 @@ public class ProductServiceTest {
|
|||
@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);
|
||||
});
|
||||
|
|
@ -172,10 +162,10 @@ public class ProductServiceTest {
|
|||
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());
|
||||
}
|
||||
|
|
@ -188,16 +178,16 @@ public class ProductServiceTest {
|
|||
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;
|
||||
public boolean existsReferencedProdukt(long produktId) {
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public boolean existsReferencedKunde(long kundenId) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean existsReferencedKunde(long kundenId) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int findMaxLaufendeNummer(String praefix, int jahr) {
|
||||
return 0;
|
||||
public int findMaxLaufendeNummer(String praefix, int jahr) {
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public void save(Dokument dokument) {
|
||||
|
|
@ -215,16 +205,8 @@ public class ProductServiceTest {
|
|||
public boolean existsFolgedokument(String quellDokumentNummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
|
||||
}
|
||||
@Override
|
||||
public List<Dokument> findAll() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'findAll'");
|
||||
}
|
||||
@Override
|
||||
public boolean delete(String nummer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'delete'");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ProductService serviceWithSperre = new ProductService(productRepository, mockRepo);
|
||||
Product product = new Product("P-0001", "Gesperrtes Produkt", new BigDecimal("10.00"), MwstSatz.NORMAL, "", "");
|
||||
productRepository.save(product);
|
||||
|
|
|
|||
Loading…
Reference in New Issue