Compare commits
No commits in common. "2ec8d5c89f107f6ccdb9ae918640531d89a8586d" and "89512b2e8e2543011141426691f97d35e6d23d39" have entirely different histories.
2ec8d5c89f
...
89512b2e8e
File diff suppressed because one or more lines are too long
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.
|
||||
|
|
@ -1,382 +0,0 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
import de.hsmannheim.faktura.dokumente.domain.Dokument;
|
||||
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
|
||||
import de.hsmannheim.faktura.dokumente.service.DokumentenService;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
|
||||
/**
|
||||
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12).
|
||||
* Zeigt Status und zulässige Folgeaktionen an. Deaktiviert unzulässige Aktionen (PHGUI10).
|
||||
* Festgeschriebene Rechnungen sind schreibgeschützt (PHGUI11).
|
||||
*/
|
||||
public class DokumentPanel extends JPanel {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final DokumentenService dokumentenService;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable dokumentTable;
|
||||
|
||||
// Detailfelder (schreibgeschützt)
|
||||
private JTextField txtDokumentnummer;
|
||||
private JTextField txtKundenId;
|
||||
private JTextField txtDatum;
|
||||
private JTextField txtStatus;
|
||||
private JCheckBox chkFestgeschrieben;
|
||||
|
||||
// Positionstabelle
|
||||
private DefaultTableModel posTableModel;
|
||||
private JTable posTable;
|
||||
|
||||
// Summenfelder
|
||||
private JLabel lblNetto;
|
||||
private JLabel lblUst;
|
||||
private JLabel lblBrutto;
|
||||
|
||||
// Aktions-Buttons (PHGUI10)
|
||||
private JButton btnNeu;
|
||||
private JButton btnSpeichern;
|
||||
private JButton btnLoeschen;
|
||||
private JButton btnAbbrechen;
|
||||
private JButton btnPdfExport;
|
||||
|
||||
// Folgeaktions-Buttons
|
||||
private JButton btnAngebotZuAuftrag;
|
||||
private JButton btnAuftragZuLieferschein;
|
||||
private JButton btnLieferscheinZuRechnung;
|
||||
|
||||
private Dokument aktuellesDokument = null;
|
||||
|
||||
public DokumentPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.dokumentenService = new DokumentenService(new DokumentRepository());
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
ladeDokumente();
|
||||
}
|
||||
|
||||
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"};
|
||||
tableModel = new DefaultTableModel(cols, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
dokumentTable = new JTable(tableModel) {
|
||||
// Status-Spalte farbig (PHGUI10)
|
||||
public Component prepareRenderer(TableCellRenderer r, int row, int col) {
|
||||
Component c = super.prepareRenderer(r, row, col);
|
||||
if (col == 2) {
|
||||
String status = (String) getValueAt(row, col);
|
||||
if (status != null) {
|
||||
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);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
};
|
||||
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
// Sortierung (PHGUI08)
|
||||
dokumentTable.setAutoCreateRowSorter(true);
|
||||
dokumentTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
|
||||
});
|
||||
|
||||
leftPanel.add(new JLabel("Dokumente"), BorderLayout.NORTH);
|
||||
leftPanel.add(new JScrollPane(dokumentTable), BorderLayout.CENTER);
|
||||
|
||||
// --- 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();
|
||||
g.insets = new Insets(3, 4, 3, 4);
|
||||
g.fill = GridBagConstraints.HORIZONTAL;
|
||||
g.weightx = 0.5;
|
||||
|
||||
// Festgeschrieben-Checkbox (PHGUI11) oben rechts
|
||||
chkFestgeschrieben = new JCheckBox("Festgeschrieben");
|
||||
chkFestgeschrieben.setEnabled(false);
|
||||
g.gridx = 1; g.gridy = 0; g.anchor = GridBagConstraints.EAST;
|
||||
headerPanel.add(chkFestgeschrieben, g);
|
||||
|
||||
g.anchor = GridBagConstraints.WEST;
|
||||
g.gridx = 0; g.gridy = 1; headerPanel.add(new JLabel("Dokumentnummer"), g);
|
||||
g.gridx = 1; headerPanel.add(new JLabel("Kundennummer"), g);
|
||||
txtDokumentnummer = createReadOnlyField();
|
||||
txtKundenId = createReadOnlyField();
|
||||
g.gridx = 0; g.gridy = 2; headerPanel.add(txtDokumentnummer, g);
|
||||
g.gridx = 1; headerPanel.add(txtKundenId, g);
|
||||
|
||||
g.gridx = 0; g.gridy = 3; headerPanel.add(new JLabel("Datum"), g);
|
||||
g.gridx = 1; headerPanel.add(new JLabel("Status"), g);
|
||||
txtDatum = createReadOnlyField();
|
||||
txtStatus = createReadOnlyField();
|
||||
g.gridx = 0; g.gridy = 4; headerPanel.add(txtDatum, g);
|
||||
g.gridx = 1; headerPanel.add(txtStatus, g);
|
||||
|
||||
rightPanel.add(headerPanel, BorderLayout.NORTH);
|
||||
|
||||
// Positionstabelle
|
||||
String[] posCols = {"Pos", "Bezeichnung", "Menge", "Einzelpreis", "MwSt.", "Summe"};
|
||||
posTableModel = new DefaultTableModel(posCols, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
posTable = new JTable(posTableModel);
|
||||
posTable.setEnabled(false);
|
||||
|
||||
JPanel posPanel = new JPanel(new BorderLayout());
|
||||
posPanel.setBorder(BorderFactory.createTitledBorder("Positionen:"));
|
||||
posPanel.add(new JScrollPane(posTable), BorderLayout.CENTER);
|
||||
|
||||
// Summen
|
||||
JPanel summenPanel = new JPanel(new GridLayout(3, 2, 4, 2));
|
||||
summenPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
|
||||
lblNetto = new JLabel("0,00 €"); lblNetto.setHorizontalAlignment(SwingConstants.RIGHT);
|
||||
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. 19%:")); summenPanel.add(lblUst);
|
||||
summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto);
|
||||
|
||||
JPanel mittelPanel = new JPanel(new BorderLayout());
|
||||
mittelPanel.add(posPanel, BorderLayout.CENTER);
|
||||
mittelPanel.add(summenPanel, BorderLayout.SOUTH);
|
||||
rightPanel.add(mittelPanel, BorderLayout.CENTER);
|
||||
|
||||
// --- Aktionen (PHGUI10: nur zulässige aktiv) ---
|
||||
JPanel aktionenPanel = new JPanel(new BorderLayout());
|
||||
aktionenPanel.setBorder(BorderFactory.createTitledBorder("Aktionen:"));
|
||||
|
||||
JPanel stdButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
btnNeu = new JButton("Neu");
|
||||
btnSpeichern = new JButton("Speichern");
|
||||
btnLoeschen = new JButton("Löschen");
|
||||
btnAbbrechen = new JButton("Abbrechen");
|
||||
btnPdfExport = new JButton("PDF-Export");
|
||||
btnPdfExport.setBackground(new Color(41, 128, 185));
|
||||
btnPdfExport.setForeground(Color.WHITE);
|
||||
stdButtons.add(btnNeu);
|
||||
stdButtons.add(btnSpeichern);
|
||||
stdButtons.add(btnLoeschen);
|
||||
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");
|
||||
btnLieferscheinZuRechnung = new JButton("→ Rechnung erstellen");
|
||||
folgeButtons.add(btnAngebotZuAuftrag);
|
||||
folgeButtons.add(btnAuftragZuLieferschein);
|
||||
folgeButtons.add(btnLieferscheinZuRechnung);
|
||||
|
||||
aktionenPanel.add(stdButtons, BorderLayout.NORTH);
|
||||
aktionenPanel.add(folgeButtons, BorderLayout.SOUTH);
|
||||
rightPanel.add(aktionenPanel, BorderLayout.SOUTH);
|
||||
|
||||
// Button-Actions
|
||||
btnNeu.addActionListener(e -> neuesFormular());
|
||||
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeDokumente(); });
|
||||
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);
|
||||
split.setDividerLocation(300);
|
||||
add(split, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// --- Daten ---
|
||||
private void ladeDokumente() {
|
||||
tableModel.setRowCount(0);
|
||||
List<Dokument> alle = dokumentenService.getAllDokumente();
|
||||
for (Dokument d : alle) {
|
||||
tableModel.addRow(new Object[]{
|
||||
d.getDokumentnummer(),
|
||||
d.getTypBezeichnung(),
|
||||
d.getStatus().getAnzeigename()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehltes() {
|
||||
int row = dokumentTable.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
int modelRow = dokumentTable.convertRowIndexToModel(row);
|
||||
String nr = (String) tableModel.getValueAt(modelRow, 0);
|
||||
|
||||
dokumentenService.getDokumentByNummer(nr).ifPresent(d -> {
|
||||
aktuellesDokument = d;
|
||||
txtDokumentnummer.setText(d.getDokumentnummer());
|
||||
txtKundenId.setText(String.valueOf(d.getKundeId()));
|
||||
txtDatum.setText(d.getDatum().toString());
|
||||
txtStatus.setText(d.getStatus().getAnzeigename());
|
||||
|
||||
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.getProduktbezeichnung(),
|
||||
position.getMenge(),
|
||||
position.getEinzelpreisNetto() + " €",
|
||||
position.getMehrwertsteuerSatz() + "%",
|
||||
position.getSumme() + " €"
|
||||
});
|
||||
}
|
||||
|
||||
// Summen
|
||||
if (d.getNettosumme() != null) {
|
||||
lblNetto.setText(d.getNettosumme().toPlainString() + " €");
|
||||
lblUst.setText(d.getUstSumme().toPlainString() + " €");
|
||||
lblBrutto.setText(d.getBruttosumme().toPlainString() + " €");
|
||||
}
|
||||
|
||||
// PHGUI10: Zulässige Folgeaktionen setzen
|
||||
aktualisiereAktionen(d);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 → alles deaktiviert außer PDF-Export
|
||||
if (festgeschrieben) {
|
||||
btnNeu.setEnabled(true);
|
||||
btnSpeichern.setEnabled(false);
|
||||
btnLoeschen.setEnabled(false);
|
||||
btnAbbrechen.setEnabled(true);
|
||||
btnPdfExport.setEnabled(true); // PHGUI12
|
||||
btnAngebotZuAuftrag.setEnabled(false);
|
||||
btnAuftragZuLieferschein.setEnabled(false);
|
||||
btnLieferscheinZuRechnung.setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// PHGUI10: Folgeaktionen je nach Status
|
||||
btnNeu.setEnabled(true);
|
||||
btnSpeichern.setEnabled(true);
|
||||
btnLoeschen.setEnabled(true);
|
||||
btnAbbrechen.setEnabled(true);
|
||||
btnPdfExport.setEnabled(status == DokumentStatus.RECHNUNG_ERSTELLT);
|
||||
|
||||
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.ANGEBOT_OFFEN);
|
||||
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.AUFTRAGSBESTAETIGUNG_ERSTELLT);
|
||||
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.LIEFERSCHEIN_ERSTELLT);
|
||||
}
|
||||
|
||||
private void setAktionenDeaktiviert() {
|
||||
btnSpeichern.setEnabled(false);
|
||||
btnLoeschen.setEnabled(false);
|
||||
btnPdfExport.setEnabled(false);
|
||||
btnAngebotZuAuftrag.setEnabled(false);
|
||||
btnAuftragZuLieferschein.setEnabled(false);
|
||||
btnLieferscheinZuRechnung.setEnabled(false);
|
||||
}
|
||||
|
||||
private void neuesFormular() {
|
||||
aktuellesDokument = null;
|
||||
txtDokumentnummer.setText(""); txtKundenId.setText("");
|
||||
txtDatum.setText(""); txtStatus.setText("");
|
||||
chkFestgeschrieben.setSelected(false);
|
||||
posTableModel.setRowCount(0);
|
||||
lblNetto.setText("0,00 €"); lblUst.setText("0,00 €"); lblBrutto.setText("0,00 €");
|
||||
setAktionenDeaktiviert();
|
||||
btnNeu.setEnabled(true); btnAbbrechen.setEnabled(true);
|
||||
dokumentTable.clearSelection();
|
||||
}
|
||||
|
||||
private void loeschen() {
|
||||
if (aktuellesDokument == null) {
|
||||
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
|
||||
);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentnummer());
|
||||
if (deleted) {
|
||||
ladeDokumente(); neuesFormular();
|
||||
mainFrame.showStatus("Dokument gelöscht.");
|
||||
} else {
|
||||
mainFrame.showStatus("Dokument kann nicht gelöscht werden.");
|
||||
}
|
||||
}
|
||||
|
||||
private void folgeaktion(String typ) {
|
||||
if (aktuellesDokument == null) return;
|
||||
try {
|
||||
switch (typ) {
|
||||
case "ANGEBOT_ZU_AUFTRAG" ->
|
||||
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentnummer());
|
||||
case "AUFTRAG_ZU_LIEFERSCHEIN" ->
|
||||
dokumentenService.createLieferschein(aktuellesDokument.getDokumentnummer());
|
||||
case "LIEFERSCHEIN_ZU_RECHNUNG" ->
|
||||
dokumentenService.createRechnung(aktuellesDokument.getDokumentnummer());
|
||||
}
|
||||
ladeDokumente();
|
||||
mainFrame.showStatus("Folgedokument erfolgreich erstellt.");
|
||||
} catch (Exception ex) {
|
||||
mainFrame.showStatus("Fehler: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
mainFrame.showStatus("PDF-Export erfolgreich gestartet.");
|
||||
} catch (Exception ex) {
|
||||
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private JTextField createReadOnlyField() {
|
||||
JTextField f = new JTextField();
|
||||
f.setEditable(false);
|
||||
f.setBackground(new Color(230, 230, 230));
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
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).
|
||||
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
|
||||
*/
|
||||
public class KundenPanel extends JPanel {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final KundenService kundenService;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable kundenTable;
|
||||
|
||||
// Formularfelder
|
||||
private JTextField txtKundenId;
|
||||
private JTextField txtFirmenname;
|
||||
private JTextField txtStrasse;
|
||||
private JTextField txtPlz;
|
||||
private JTextField txtOrt;
|
||||
private JTextField txtTelefon;
|
||||
private JTextField txtEmail;
|
||||
private JTextField txtUstIdNr;
|
||||
|
||||
// Fehlerhinweise (PHGUI03, PHGUI05)
|
||||
private JLabel lblFirmenfehler;
|
||||
private JLabel lblStrasseFehler;
|
||||
private JLabel lblPlzFehler;
|
||||
private JLabel lblOrtFehler;
|
||||
private JLabel lblEmailFehler;
|
||||
|
||||
// Suche (PHGUI09)
|
||||
private JTextField txtSuche;
|
||||
|
||||
// Buttons (PHGUI02)
|
||||
private JButton btnNeu;
|
||||
private JButton btnSpeichern;
|
||||
private JButton btnLoeschen;
|
||||
private JButton btnAbbrechen;
|
||||
|
||||
public KundenPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.kundenService = new KundenService(
|
||||
new KundenRepository(),
|
||||
createDummyDokumentRepository()
|
||||
);
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
ladeKunden();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
// --- Linke Seite: Liste + Suche ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(320, 0));
|
||||
|
||||
String[] columns = {"ID", "Name ▲", "Ort"};
|
||||
tableModel = new DefaultTableModel(columns, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
kundenTable = new JTable(tableModel);
|
||||
kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
// Sortierung (PHGUI08)
|
||||
kundenTable.setAutoCreateRowSorter(true);
|
||||
|
||||
kundenTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehlten();
|
||||
});
|
||||
|
||||
leftPanel.add(new JLabel("Kundenliste"), BorderLayout.NORTH);
|
||||
leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER);
|
||||
|
||||
// Suche (PHGUI09: nach Name oder Kunden-ID)
|
||||
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
|
||||
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche (Name / Kunden-ID)"));
|
||||
txtSuche = new JTextField();
|
||||
txtSuche.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);
|
||||
}
|
||||
|
||||
// --- 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 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(kundenService.getAllKunden());
|
||||
}
|
||||
|
||||
private void ladeKundenListe(List<Kunde> liste) {
|
||||
tableModel.setRowCount(0);
|
||||
for (Kunde k : liste) {
|
||||
tableModel.addRow(new Object[]{
|
||||
String.valueOf(k.getKundenId()),
|
||||
k.getFirmennameNachname(),
|
||||
k.getOrt()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sucheAktualisieren() {
|
||||
// PHGUI09: Groß-/Kleinschreibung ignorieren, nach Name oder ID suchen
|
||||
String query = txtSuche.getText();
|
||||
ladeKundenListe(kundenService.searchKunden(query));
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehlten() {
|
||||
int row = kundenTable.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
int modelRow = kundenTable.convertRowIndexToModel(row);
|
||||
String idStr = (String) tableModel.getValueAt(modelRow, 0);
|
||||
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() {
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
kundenService.saveKunde(k);
|
||||
ladeKunden();
|
||||
neuesFormular();
|
||||
// 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;
|
||||
}
|
||||
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this, "Kunden wirklich löschen?", "Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
int modelRow = kundenTable.convertRowIndexToModel(row);
|
||||
String idStr = (String) tableModel.getValueAt(modelRow, 0);
|
||||
boolean deleted = kundenService.deleteKunde(Integer.parseInt(idStr));
|
||||
|
||||
if (deleted) {
|
||||
ladeKunden();
|
||||
neuesFormular();
|
||||
mainFrame.showStatus("Kunde gelöscht.");
|
||||
} else {
|
||||
mainFrame.showStatus("Löschen nicht möglich – Kunde wird in Dokumenten verwendet.");
|
||||
}
|
||||
}
|
||||
|
||||
private void clearFehler() {
|
||||
lblFirmenfehler.setText("");
|
||||
lblStrasseFehler.setText("");
|
||||
lblPlzFehler.setText("");
|
||||
lblOrtFehler.setText("");
|
||||
lblEmailFehler.setText("▲ Format: name@domain.tld");
|
||||
lblEmailFehler.setForeground(new Color(120, 120, 120));
|
||||
}
|
||||
|
||||
private JLabel createFehlerLabel() {
|
||||
JLabel lbl = new JLabel("");
|
||||
lbl.setForeground(new Color(192, 57, 43));
|
||||
lbl.setFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private 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; }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Hauptfenster der Anwendung (PHGUI01, PHGUI02).
|
||||
* Stellt die dauerhaft sichtbare Hauptnavigation bereit und lädt die Modulpanels.
|
||||
*/
|
||||
public class MainFrame extends JFrame {
|
||||
|
||||
private final ProduktPanel produktPanel;
|
||||
private final KundenPanel kundenPanel;
|
||||
private final DokumentPanel dokumentPanel;
|
||||
private final JLabel statusLabel;
|
||||
|
||||
public MainFrame() {
|
||||
setTitle("Fakturierungssystem");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setSize(1100, 700);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
// Hauptlayout
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
// --- Navigation (PHGUI01) ---
|
||||
JPanel navPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
navPanel.setBackground(new Color(52, 73, 94));
|
||||
|
||||
JLabel title = new JLabel(" Fakturierungssystem ");
|
||||
title.setForeground(Color.WHITE);
|
||||
title.setFont(new Font("SansSerif", Font.BOLD, 14));
|
||||
navPanel.add(title);
|
||||
|
||||
JButton btnProdukt = createNavButton("Produktverwaltung");
|
||||
JButton btnKunde = createNavButton("Kundenverwaltung");
|
||||
JButton btnDokument = createNavButton("Dokumentenprozess");
|
||||
navPanel.add(btnProdukt);
|
||||
navPanel.add(btnKunde);
|
||||
navPanel.add(btnDokument);
|
||||
add(navPanel, BorderLayout.NORTH);
|
||||
|
||||
// --- Inhaltsbereich ---
|
||||
JPanel contentArea = new JPanel(new CardLayout());
|
||||
produktPanel = new ProduktPanel(this);
|
||||
kundenPanel = new KundenPanel(this);
|
||||
dokumentPanel = new DokumentPanel(this);
|
||||
|
||||
contentArea.add(produktPanel, "produkt");
|
||||
contentArea.add(kundenPanel, "kunde");
|
||||
contentArea.add(dokumentPanel, "dokument");
|
||||
add(contentArea, BorderLayout.CENTER);
|
||||
|
||||
// --- Statusbereich (PHGUI06) ---
|
||||
statusLabel = new JLabel("Status: Bereit");
|
||||
statusLabel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
|
||||
statusLabel.setFont(new Font("SansSerif", Font.PLAIN, 12));
|
||||
add(statusLabel, BorderLayout.SOUTH);
|
||||
|
||||
// Navigation-Actions (PHGUI01: 1 Klick)
|
||||
CardLayout cl = (CardLayout) contentArea.getLayout();
|
||||
btnProdukt.addActionListener(e -> cl.show(contentArea, "produkt"));
|
||||
btnKunde.addActionListener(e -> cl.show(contentArea, "kunde"));
|
||||
btnDokument.addActionListener(e -> cl.show(contentArea, "dokument"));
|
||||
|
||||
// Standard: Produktverwaltung
|
||||
cl.show(contentArea, "produkt");
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Statusmeldung im Statusbereich an (PHGUI06: innerhalb 1 Sekunde).
|
||||
*/
|
||||
public void showStatus(String message) {
|
||||
statusLabel.setText("Status: " + message);
|
||||
// Reset nach 3 Sekunden
|
||||
Timer timer = new Timer(3000, e -> statusLabel.setText("Status: Bereit"));
|
||||
timer.setRepeats(false);
|
||||
timer.start();
|
||||
}
|
||||
|
||||
private JButton createNavButton(String text) {
|
||||
JButton btn = new JButton(text);
|
||||
btn.setBackground(new Color(52, 73, 94));
|
||||
btn.setForeground(Color.WHITE);
|
||||
btn.setFocusPainted(false);
|
||||
btn.setBorderPainted(false);
|
||||
btn.setFont(new Font("SansSerif", Font.BOLD, 13));
|
||||
btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
btn.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
public void mouseEntered(java.awt.event.MouseEvent e) {
|
||||
btn.setBackground(new Color(41, 128, 185));
|
||||
}
|
||||
public void mouseExited(java.awt.event.MouseEvent e) {
|
||||
btn.setBackground(new Color(52, 73, 94));
|
||||
}
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(MainFrame::new);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
package de.hsmannheim.faktura.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import de.hsmannheim.faktura.product.MwstSatz;
|
||||
import de.hsmannheim.faktura.product.Product;
|
||||
import de.hsmannheim.faktura.product.service.ProductService;
|
||||
import de.hsmannheim.faktura.product.repository.ProductRepository;
|
||||
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
|
||||
|
||||
/**
|
||||
* Panel für die Produktverwaltung (PHGUI01–PHGUI04, PHGUI06–PHGUI09).
|
||||
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
|
||||
*/
|
||||
public class ProduktPanel extends JPanel {
|
||||
|
||||
private final MainFrame mainFrame;
|
||||
private final ProductService productService;
|
||||
|
||||
// Tabelle
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable produktTable;
|
||||
|
||||
// Formularfelder
|
||||
private JTextField txtProduktId;
|
||||
private JTextField txtBezeichnung;
|
||||
private JTextField txtPreis;
|
||||
private JComboBox<MwstSatz> cmbMwst;
|
||||
private JTextArea txtBeschreibung;
|
||||
private JTextField txtKategorie;
|
||||
|
||||
// Fehlerhinweise (PHGUI03)
|
||||
private JLabel lblBezeichnungFehler;
|
||||
private JLabel lblPreisFehler;
|
||||
|
||||
// Suche (PHGUI09)
|
||||
private JTextField txtSuche;
|
||||
|
||||
// Buttons (PHGUI02: einheitlich beschriftet)
|
||||
private JButton btnNeu;
|
||||
private JButton btnSpeichern;
|
||||
private JButton btnLoeschen;
|
||||
private JButton btnAbbrechen;
|
||||
|
||||
public ProduktPanel(MainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
this.productService = new ProductService(
|
||||
new ProductRepository(),
|
||||
createDummyDokumentRepository()
|
||||
);
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
initComponents();
|
||||
ladeProdukte();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
// --- Linke Seite: Liste + Suche ---
|
||||
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
|
||||
leftPanel.setPreferredSize(new Dimension(320, 0));
|
||||
|
||||
String[] columns = {"ID", "Bezeichnung ▲", "Preis"};
|
||||
tableModel = new DefaultTableModel(columns, 0) {
|
||||
public boolean isCellEditable(int r, int c) { return false; }
|
||||
};
|
||||
produktTable = new JTable(tableModel);
|
||||
produktTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
produktTable.getTableHeader().setReorderingAllowed(false);
|
||||
|
||||
// Sortierung (PHGUI08)
|
||||
produktTable.setAutoCreateRowSorter(true);
|
||||
|
||||
produktTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
|
||||
});
|
||||
|
||||
leftPanel.add(new JLabel("Produktliste"), BorderLayout.NORTH);
|
||||
leftPanel.add(new JScrollPane(produktTable), BorderLayout.CENTER);
|
||||
|
||||
// Suche (PHGUI09)
|
||||
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
|
||||
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche"));
|
||||
txtSuche = new JTextField();
|
||||
txtSuche.setToolTipText("Bezeichnung eingeben...");
|
||||
txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
|
||||
public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
|
||||
});
|
||||
suchePanel.add(new JLabel("Bezeichnung:"), BorderLayout.WEST);
|
||||
suchePanel.add(txtSuche, BorderLayout.CENTER);
|
||||
leftPanel.add(suchePanel, BorderLayout.SOUTH);
|
||||
|
||||
// --- Rechte Seite: Formular ---
|
||||
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
|
||||
rightPanel.setBorder(BorderFactory.createTitledBorder("Produktdetails Pflichtfelder sind mit * markiert"));
|
||||
|
||||
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 4, 2, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
// Zeile 0: ProduktId + Einzelpreis
|
||||
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0.5;
|
||||
formPanel.add(new JLabel("Produkt-ID (autom. vergeben)"), gbc);
|
||||
gbc.gridx = 1;
|
||||
formPanel.add(new JLabel("Einzelpreis Netto (€) *"), gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 1;
|
||||
txtProduktId = new JTextField();
|
||||
txtProduktId.setEditable(false);
|
||||
txtProduktId.setBackground(new Color(230, 230, 230));
|
||||
formPanel.add(txtProduktId, gbc);
|
||||
gbc.gridx = 1;
|
||||
txtPreis = new JTextField();
|
||||
formPanel.add(txtPreis, gbc);
|
||||
|
||||
// Fehlerhinweis Preis
|
||||
gbc.gridx = 1; gbc.gridy = 2;
|
||||
lblPreisFehler = createFehlerLabel();
|
||||
formPanel.add(lblPreisFehler, gbc);
|
||||
|
||||
// Zeile 3: Bezeichnung + MwSt
|
||||
gbc.gridx = 0; gbc.gridy = 3;
|
||||
formPanel.add(new JLabel("Bezeichnung *"), gbc);
|
||||
gbc.gridx = 1;
|
||||
formPanel.add(new JLabel("MwSt.-Satz (%) *"), gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 4;
|
||||
txtBezeichnung = new JTextField();
|
||||
formPanel.add(txtBezeichnung, gbc);
|
||||
gbc.gridx = 1;
|
||||
cmbMwst = new JComboBox<>(MwstSatz.values());
|
||||
formPanel.add(cmbMwst, gbc);
|
||||
|
||||
// Fehlerhinweis Bezeichnung
|
||||
gbc.gridx = 0; gbc.gridy = 5;
|
||||
lblBezeichnungFehler = createFehlerLabel();
|
||||
formPanel.add(lblBezeichnungFehler, gbc);
|
||||
|
||||
// Zeile 6: Kategorie
|
||||
gbc.gridx = 0; gbc.gridy = 6; gbc.gridwidth = 2;
|
||||
formPanel.add(new JLabel("Kategorie"), gbc);
|
||||
gbc.gridy = 7;
|
||||
txtKategorie = new JTextField();
|
||||
formPanel.add(txtKategorie, gbc);
|
||||
|
||||
// Zeile 8: Beschreibung
|
||||
gbc.gridy = 8;
|
||||
formPanel.add(new JLabel("Beschreibung"), gbc);
|
||||
gbc.gridy = 9; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH;
|
||||
txtBeschreibung = new JTextArea(4, 20);
|
||||
txtBeschreibung.setLineWrap(true);
|
||||
formPanel.add(new JScrollPane(txtBeschreibung), gbc);
|
||||
|
||||
rightPanel.add(formPanel, BorderLayout.CENTER);
|
||||
|
||||
// --- Buttons (PHGUI02: einheitlich) ---
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
btnNeu = new JButton("Neu");
|
||||
btnSpeichern = new JButton("Speichern");
|
||||
btnLoeschen = new JButton("Löschen");
|
||||
btnAbbrechen = new JButton("Abbrechen");
|
||||
btnPanel.add(btnNeu);
|
||||
btnPanel.add(btnSpeichern);
|
||||
btnPanel.add(btnLoeschen);
|
||||
btnPanel.add(btnAbbrechen);
|
||||
rightPanel.add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
// Button-Actions
|
||||
btnNeu.addActionListener(e -> neuesFormular());
|
||||
btnSpeichern.addActionListener(e -> speichern());
|
||||
btnLoeschen.addActionListener(e -> loeschen());
|
||||
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeProdukte(); });
|
||||
|
||||
// Split
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
|
||||
split.setDividerLocation(320);
|
||||
add(split, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// --- Daten laden ---
|
||||
private void ladeProdukte() {
|
||||
ladeProdukteListe(productService.getAllProducts());
|
||||
}
|
||||
|
||||
private void ladeProdukteListe(List<Product> liste) {
|
||||
tableModel.setRowCount(0);
|
||||
for (Product p : liste) {
|
||||
tableModel.addRow(new Object[]{
|
||||
p.getProductId(),
|
||||
p.getBezeichnung(),
|
||||
p.getEinzelpreisNetto() + " €"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sucheAktualisieren() {
|
||||
String query = txtSuche.getText();
|
||||
// PHGUI09: Groß-/Kleinschreibung ignorieren
|
||||
ladeProdukteListe(productService.searchProducts(query));
|
||||
}
|
||||
|
||||
private void zeigeAusgewaehltes() {
|
||||
int row = produktTable.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
int modelRow = produktTable.convertRowIndexToModel(row);
|
||||
String id = (String) tableModel.getValueAt(modelRow, 0);
|
||||
productService.getProductById(id).ifPresent(p -> {
|
||||
txtProduktId.setText(p.getProductId());
|
||||
txtBezeichnung.setText(p.getBezeichnung());
|
||||
txtPreis.setText(p.getEinzelpreisNetto().toPlainString());
|
||||
cmbMwst.setSelectedItem(p.getMwstSatz());
|
||||
txtBeschreibung.setText(p.getBeschreibung());
|
||||
txtKategorie.setText(p.getKategorie());
|
||||
clearFehler();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Aktionen ---
|
||||
private void neuesFormular() {
|
||||
txtProduktId.setText("");
|
||||
txtBezeichnung.setText("");
|
||||
txtPreis.setText("");
|
||||
txtBeschreibung.setText("");
|
||||
txtKategorie.setText("");
|
||||
cmbMwst.setSelectedIndex(0);
|
||||
clearFehler();
|
||||
produktTable.clearSelection();
|
||||
}
|
||||
|
||||
private void speichern() {
|
||||
clearFehler();
|
||||
boolean valid = true;
|
||||
|
||||
String bezeichnung = txtBezeichnung.getText().trim();
|
||||
String preisText = txtPreis.getText().trim();
|
||||
|
||||
// PHGUI03: Pflichtfeldprüfung
|
||||
if (bezeichnung.isEmpty()) {
|
||||
lblBezeichnungFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
BigDecimal preis = null;
|
||||
if (preisText.isEmpty()) {
|
||||
lblPreisFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
|
||||
valid = false;
|
||||
} else {
|
||||
try {
|
||||
preis = new BigDecimal(preisText.replace(",", "."));
|
||||
// PHGUI04: Negativpreis ablehnen
|
||||
if (preis.compareTo(BigDecimal.ZERO) < 0) {
|
||||
lblPreisFehler.setText("⚠ Preis darf nicht negativ sein");
|
||||
valid = false;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
lblPreisFehler.setText("⚠ Ungültiger Zahlenwert");
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
Product p = new Product(
|
||||
txtProduktId.getText().trim(),
|
||||
bezeichnung,
|
||||
preis,
|
||||
(MwstSatz) cmbMwst.getSelectedItem(),
|
||||
txtBeschreibung.getText().trim(),
|
||||
txtKategorie.getText().trim()
|
||||
);
|
||||
|
||||
try {
|
||||
productService.saveProduct(p);
|
||||
ladeProdukte();
|
||||
neuesFormular();
|
||||
// PHGUI06: Sichtbare Bestätigung < 1 Sekunde
|
||||
mainFrame.showStatus("Produkt erfolgreich gespeichert.");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
mainFrame.showStatus("Fehler: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loeschen() {
|
||||
int row = produktTable.getSelectedRow();
|
||||
if (row < 0) {
|
||||
mainFrame.showStatus("Kein Produkt ausgewählt.");
|
||||
return;
|
||||
}
|
||||
|
||||
// PHGUI07: Bestätigungsdialog
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
"Produkt wirklich löschen?",
|
||||
"Löschen bestätigen",
|
||||
JOptionPane.YES_NO_OPTION
|
||||
);
|
||||
if (confirm != JOptionPane.YES_OPTION) return;
|
||||
|
||||
int modelRow = produktTable.convertRowIndexToModel(row);
|
||||
String id = (String) tableModel.getValueAt(modelRow, 0);
|
||||
boolean deleted = productService.deleteProduct(id);
|
||||
|
||||
if (deleted) {
|
||||
ladeProdukte();
|
||||
neuesFormular();
|
||||
// PHGUI06
|
||||
mainFrame.showStatus("Produkt gelöscht.");
|
||||
} else {
|
||||
mainFrame.showStatus("Löschen nicht möglich – Produkt wird in Dokumenten verwendet.");
|
||||
}
|
||||
}
|
||||
|
||||
private void clearFehler() {
|
||||
lblBezeichnungFehler.setText("");
|
||||
lblPreisFehler.setText("");
|
||||
}
|
||||
|
||||
private JLabel createFehlerLabel() {
|
||||
JLabel lbl = new JLabel("");
|
||||
lbl.setForeground(new Color(192, 57, 43));
|
||||
lbl.setFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private DokumentRepository createDummyDokumentRepository() {
|
||||
return new DokumentRepository() {
|
||||
public boolean existsReferencedProdukt(long id) { return false; }
|
||||
public boolean existsReferencedKunde(long id) { return false; }
|
||||
public int findMaxLaufendeNummer(String p, int j) { return 0; }
|
||||
public void save(de.hsmannheim.faktura.dokumente.domain.Dokument d) {}
|
||||
public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
|
||||
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); }
|
||||
public boolean existsFolgedokument(String q) { return false; }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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("==================================================");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue