Enhanced quarterly confirmation system with approval workflow and export improvements

Features added:
-  Fixed quarterly confirmation approval system with URL pattern
-  Added re-approval and status reset functionality for quarterly confirmations
-  Synchronized quarterly approval status with support payment system
-  Enhanced Destinataer export with missing fields (anrede, titel, mobil)
-  Added quarterly confirmation data and documents to export system
-  Fixed address field display issues in destinataer template
-  Added quarterly statistics dashboard to support payment lists
-  Implemented duplicate support payment prevention and cleanup
-  Added visual indicators for quarterly-linked support payments

Technical improvements:
- Enhanced create_quarterly_support_payment() with duplicate detection
- Added get_related_support_payment() method to VierteljahresNachweis model
- Improved quarterly confirmation workflow with proper status transitions
- Added computed address property to Destinataer model
- Fixed template field mismatches (anrede, titel, mobil vs strasse, plz, ort)
- Enhanced backup system with operation tracking and cancellation

Workflow enhancements:
- Quarterly confirmations now properly sync with support payments
- Single support payment per destinataer per quarter (no duplicates)
- Approval button works for both eingereicht and geprueft status
- Reset functionality allows workflow restart
- Export includes complete quarterly data with uploaded documents
This commit is contained in:
2025-09-28 19:09:08 +02:00
parent b00cf62d87
commit acac8695fd
73 changed files with 283380 additions and 206 deletions

View File

@@ -36,6 +36,9 @@ INSTALLED_APPS = [
"rest_framework", "rest_framework",
"stiftung", "stiftung",
] ]
# Add this to app/core/settings.py
SESSION_COOKIE_NAME = 'stiftung_sessionid' # Different from default 'sessionid'
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
@@ -120,10 +123,6 @@ GRAMPS_STIFTER_IDS = os.environ.get("GRAMPS_STIFTER_IDS", "") # comma-separated
GRAMPS_USERNAME = os.environ.get("GRAMPS_USERNAME", "") GRAMPS_USERNAME = os.environ.get("GRAMPS_USERNAME", "")
GRAMPS_PASSWORD = os.environ.get("GRAMPS_PASSWORD", "") GRAMPS_PASSWORD = os.environ.get("GRAMPS_PASSWORD", "")
# Session Configuration
SESSION_COOKIE_NAME = os.environ.get("SESSION_COOKIE_NAME") or "stiftung_sessionid"
CSRF_COOKIE_NAME = os.environ.get("CSRF_COOKIE_NAME") or "stiftung_csrftoken"
# HTTPS Security Settings (production) # HTTPS Security Settings (production)
if not DEBUG: if not DEBUG:
SECURE_SSL_REDIRECT = True SECURE_SSL_REDIRECT = True

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

View File

@@ -0,0 +1,22 @@
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
1 Vorname Nachname E-Mail Telefon IBAN Straße PLZ Ort Personentyp Geburtsdatum Pachtnummer Pachtbeginn_Erste Pachtende_Letzte Pachtzins_Aktuell Landwirtschaftliche_Ausbildung Berufserfahrung_Jahre Spezialisierung Notizen Aktiv
2 N/A Groiner Milch KG Groiner Allee 18 46459 Rees Gesellschaft WAHR
3 N/A M u D Becker GbR Helweg 76 46348 Raesfeld Gesellschaft WAHR
4 Walter Buchmann Büskes Heide 11 46499 Hamminkeln-Brünen Herrn WAHR
5 Andreas Elbers Grietherbusch Nr. 15 46459 Rees Herrn WAHR
6 N/A Fischer GbR Werricher Strasse 1 46487 Wesel Gesellschaft WAHR
7 Michael Ingenbleek Achterhoeker Schulweg 39 a 47626 Kevelaer Herrn WAHR
8 N/A Bröcheler KG Kervenheimer Str. 30 47626 Kevelaer Gesellschaft WAHR
9 Jens Bodden Moelscher Weg 16 47574 Goch Herrn WAHR
10 Marcel Müller Weysche Strasse 55 47546 Kalkar-Hanselaer Herrn WAHR
11 Ludger Paeßens Bergerfurther Strasse 4 46499 Hamminkeln Herrn WAHR
12 N/A Sander GbR Hardtbergweg 21 46569 Hünxe Gesellschaft WAHR
13 Dirk Schmäh Schlümersweg 7 A 46499 Hamminkeln-Brünen Herrn WAHR
14 Wolfgang Schmitz Höst-Vornicker-Weg 1 47652 Weeze Herrn WAHR
15 Sebastian Scholten Underbergsheide 46 46485 Wesel-Obrighoven Herrn WAHR
16 N/A Ulmenhorst GbR Schlootweg 10 46499 Hamminkeln Gesellschaft WAHR
17 Simone Gerten Obrighovener Str. 107 46485 Wesel Frau WAHR
18 Günther Engelmann Loher Weg 42 46484 Wesel Herrn WAHR
19 N/A Lühlshof KG Auf dem Heidchen 27 46485 Wesel Gesellschaft WAHR
20 Walter Schmidt Ilserheider Str. 4 32469 Petershagen Herrn WAHR
21 N/A Inge und Tom Rose GbR Kohlbreite 22 34414 Warburg-Calenberg Gesellschaft WAHR
22 N/A Stiftung Schwarzensteiner Weg 75 46569 Hünxe WAHR

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -0,0 +1,11 @@
# Static Files Directory
This directory contains static files for the Django application.
## Structure:
- `css/` - Custom CSS files
- `js/` - Custom JavaScript files
- `img/` - Image assets
- `fonts/` - Font files
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.

View File

@@ -189,42 +189,65 @@ def run_restore(restore_job_id, backup_file_path):
restore_job.started_at = timezone.now() restore_job.started_at = timezone.now()
restore_job.save() restore_job.save()
# Verify backup file exists
if not os.path.exists(backup_file_path):
raise Exception(f"Backup file not found: {backup_file_path}")
# Extract backup # Extract backup
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
extract_dir = os.path.join(temp_dir, "restore") extract_dir = os.path.join(temp_dir, "restore")
os.makedirs(extract_dir) os.makedirs(extract_dir)
# Extract tar.gz # Extract tar.gz
with tarfile.open(backup_file_path, "r:gz") as tar: try:
tar.extractall(extract_dir) with tarfile.open(backup_file_path, "r:gz") as tar:
tar.extractall(extract_dir)
except Exception as e:
raise Exception(f"Failed to extract backup file: {e}")
# Validate backup # Validate backup
metadata_file = os.path.join(extract_dir, "backup_metadata.json") metadata_files = [name for name in os.listdir(extract_dir) if name.endswith('backup_metadata.json')]
if not os.path.exists(metadata_file): if not metadata_files:
raise Exception("Invalid backup: missing metadata") raise Exception("Invalid backup: missing metadata file")
# Read metadata # Read metadata
import json import json
with open(metadata_file, "r") as f: try:
metadata = json.load(f) metadata_file = os.path.join(extract_dir, metadata_files[0])
with open(metadata_file, "r") as f:
metadata = json.load(f)
print(f"Restoring backup created at: {metadata.get('created_at', 'unknown')}")
except Exception as e:
print(f"Warning: Could not read backup metadata: {e}")
# Restore database # Restore database
db_backup_file = os.path.join(extract_dir, "database.sql") db_backup_file = os.path.join(extract_dir, "database.sql")
if os.path.exists(db_backup_file): if os.path.exists(db_backup_file):
print("Restoring database...")
restore_database(db_backup_file) restore_database(db_backup_file)
print("Database restore completed")
else:
print("No database backup found in archive")
# Restore files # Restore files
files_dir = os.path.join(extract_dir, "files") files_dir = os.path.join(extract_dir, "files")
if os.path.exists(files_dir): if os.path.exists(files_dir):
print("Restoring files...")
restore_files(files_dir) restore_files(files_dir)
print("Files restore completed")
else:
print("No files backup found in archive")
# Update job status # Update job status
restore_job.status = "completed" restore_job.status = "completed"
restore_job.completed_at = timezone.now() restore_job.completed_at = timezone.now()
restore_job.save() restore_job.save()
print(f"Restore job {restore_job_id} completed successfully")
except Exception as e: except Exception as e:
print(f"Restore job {restore_job_id} failed: {e}")
restore_job = BackupJob.objects.get(id=restore_job_id)
restore_job.status = "failed" restore_job.status = "failed"
restore_job.error_message = str(e) restore_job.error_message = str(e)
restore_job.completed_at = timezone.now() restore_job.completed_at = timezone.now()
@@ -234,49 +257,151 @@ def run_restore(restore_job_id, backup_file_path):
def restore_database(db_backup_file): def restore_database(db_backup_file):
"""Restore database from backup""" """Restore database from backup"""
try: try:
print(f"Starting database restore from: {db_backup_file}")
# Get database settings # Get database settings
db_settings = settings.DATABASES["default"] db_settings = settings.DATABASES["default"]
print(f"Database settings: {db_settings.get('NAME')} at {db_settings.get('HOST')}:{db_settings.get('PORT')}")
# Build pg_restore command # First, try to determine if this is a custom format or SQL format
cmd = [ # by checking if the file starts with binary data (custom format)
"pg_restore", is_custom_format = False
"--host", try:
db_settings.get("HOST", "localhost"), with open(db_backup_file, 'rb') as f:
"--port", header = f.read(8)
str(db_settings.get("PORT", 5432)), # Custom format files start with 'PGDMP' followed by version info
"--username", if header.startswith(b'PGDMP'):
db_settings.get("USER", "postgres"), is_custom_format = True
"--dbname", print(f"Detected custom format backup (header: {header})")
db_settings.get("NAME", "stiftung"), else:
"--clean", # Drop existing objects first print(f"Detected SQL format backup (header: {header})")
"--if-exists", # Don't error if objects don't exist except Exception as e:
"--no-owner", # don't attempt to set original owners print(f"Could not determine backup format, assuming SQL: {e}")
"--role",
db_settings.get("USER", "postgres"), # set target owner if is_custom_format:
"--single-transaction", # restore atomically when possible print("Using pg_restore for custom format")
"--disable-triggers", # avoid FK issues during data load # Use pg_restore for custom format
"--no-password", cmd = [
"--verbose", "pg_restore",
db_backup_file, "--host",
] db_settings.get("HOST", "localhost"),
"--port",
str(db_settings.get("PORT", 5432)),
"--username",
db_settings.get("USER", "postgres"),
"--dbname",
db_settings.get("NAME", "stiftung"),
"--clean", # Drop existing objects first
"--if-exists", # Don't error if objects don't exist
"--no-owner", # don't attempt to set original owners
"--role",
db_settings.get("USER", "postgres"), # set target owner
# Remove --single-transaction to allow partial restore even with configuration errors
"--disable-triggers", # avoid FK issues during data load
"--no-password",
"--verbose",
# Remove --exit-on-error to allow continuation after configuration warnings
db_backup_file,
]
else:
print("Using psql for SQL format")
# Use psql for SQL format
cmd = [
"psql",
"--host",
db_settings.get("HOST", "localhost"),
"--port",
str(db_settings.get("PORT", 5432)),
"--username",
db_settings.get("USER", "postgres"),
"--dbname",
db_settings.get("NAME", "stiftung"),
"--no-password",
"--file",
db_backup_file,
]
print(f"Running command: {' '.join(cmd)}")
# Set environment variables for authentication # Set environment variables for authentication
env = os.environ.copy() env = os.environ.copy()
env["PGPASSWORD"] = db_settings.get("PASSWORD", "") env["PGPASSWORD"] = db_settings.get("PASSWORD", "")
# Run pg_restore # Run the restore command
result = subprocess.run(cmd, env=env, capture_output=True, text=True) result = subprocess.run(cmd, env=env, capture_output=True, text=True)
# Fail if there are real errors print(f"Command exit code: {result.returncode}")
print(f"STDOUT length: {len(result.stdout)} chars")
print(f"STDERR length: {len(result.stderr)} chars")
# Show first 500 chars of output for debugging
if result.stdout:
print(f"STDOUT (first 500 chars): {result.stdout[:500]}...")
if result.stderr:
print(f"STDERR (first 500 chars): {result.stderr[:500]}...")
# Handle different error conditions more gracefully
if result.returncode != 0: if result.returncode != 0:
stderr = result.stderr or "" stderr = result.stderr or ""
# escalate only if we see ERROR stdout = result.stdout or ""
if "ERROR" in stderr.upper():
raise Exception(f"pg_restore failed: {stderr}") # Check for known configuration parameter issues
if "unrecognized configuration parameter" in stderr:
print(f"Warning: Configuration parameter issues detected, but continuing: {stderr[:200]}...")
# For configuration parameter issues, we'll consider this a warning, not a fatal error
# if there are no other serious errors
serious_errors = [line for line in stderr.split('\n')
if 'ERROR' in line and 'unrecognized configuration parameter' not in line]
if serious_errors:
print(f"Serious errors found: {serious_errors}")
raise Exception(f"pg_restore failed with serious errors: {'; '.join(serious_errors)}")
else:
print("Restore completed with configuration warnings (non-fatal)")
elif "ERROR" in stderr.upper():
# Look for specific error patterns we can ignore
ignorable_patterns = [
"already exists",
"does not exist",
"unrecognized configuration parameter"
]
error_lines = [line for line in stderr.split('\n') if 'ERROR' in line]
serious_errors = []
for error_line in error_lines:
is_ignorable = any(pattern in error_line for pattern in ignorable_patterns)
if not is_ignorable:
serious_errors.append(error_line)
if serious_errors:
print(f"Serious errors found: {serious_errors}")
raise Exception(f"Database restore failed with errors: {'; '.join(serious_errors)}")
else:
print(f"Restore completed with ignorable warnings")
else: else:
print(f"pg_restore completed with warnings: {stderr}") print(f"Restore completed with warnings but no errors")
else:
print("Database restore completed successfully with no errors")
# Verify data was actually restored by checking table counts
try:
print("Verifying data was restored...")
from django.db import connection
with connection.cursor() as cursor:
# Check some key tables
test_tables = ['stiftung_person', 'stiftung_land', 'stiftung_destinataer']
for table in test_tables:
try:
cursor.execute(f"SELECT COUNT(*) FROM {table}")
count = cursor.fetchone()[0]
print(f"Table {table}: {count} rows")
except Exception as e:
print(f"Could not check table {table}: {e}")
except Exception as e:
print(f"Could not verify data restoration: {e}")
except Exception as e: except Exception as e:
print(f"Database restore failed with exception: {e}")
raise Exception(f"Database restore failed: {e}") raise Exception(f"Database restore failed: {e}")
@@ -335,3 +460,51 @@ def cleanup_old_backups(keep_count=10):
except Exception as e: except Exception as e:
print(f"Cleanup failed: {e}") print(f"Cleanup failed: {e}")
def validate_backup_file(backup_file_path):
"""Validate that a backup file is valid and can be restored"""
try:
if not os.path.exists(backup_file_path):
return False, "Backup file does not exist"
if not backup_file_path.endswith('.tar.gz'):
return False, "Invalid file format. Only .tar.gz files are supported"
# Try to open and extract metadata
with tempfile.TemporaryDirectory() as temp_dir:
try:
with tarfile.open(backup_file_path, "r:gz") as tar:
# Check if it contains expected files
names = tar.getnames()
# Look for metadata file (could be with or without ./ prefix)
metadata_files = [name for name in names if name.endswith('backup_metadata.json')]
if not metadata_files:
return False, "Invalid backup: missing metadata"
# Extract and validate metadata
metadata_file = metadata_files[0]
tar.extract(metadata_file, temp_dir)
extracted_metadata = os.path.join(temp_dir, metadata_file)
import json
with open(extracted_metadata, "r") as f:
metadata = json.load(f)
# Check metadata structure
if "backup_type" not in metadata:
return False, "Invalid backup metadata"
created_at = metadata.get('created_at', 'unknown date')
backup_type = metadata.get('backup_type', 'unknown type')
return True, f"Valid {backup_type} backup from {created_at}"
except tarfile.TarError as e:
return False, f"Corrupted backup file: {e}"
except json.JSONDecodeError:
return False, "Invalid backup metadata format"
except Exception as e:
return False, f"Validation failed: {e}"

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2025-09-24 20:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0030_vierteljahresnachweis'),
]
operations = [
migrations.AlterField(
model_name='land',
name='ust_satz',
field=models.DecimalField(blank=True, decimal_places=2, default=19.0, max_digits=4, null=True, verbose_name='USt-Satz (%)'),
),
migrations.AlterField(
model_name='land',
name='zahlungsweise',
field=models.CharField(blank=True, choices=[('jaehrlich', 'Jährlich'), ('halbjaehrlich', 'Halbjährlich'), ('vierteljaehrlich', 'Vierteljährlich'), ('monatlich', 'Monatlich')], default='jaehrlich', max_length=20, null=True, verbose_name='Zahlungsweise'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-09-24 21:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0031_alter_land_ust_satz_alter_land_zahlungsweise'),
]
operations = [
migrations.AddField(
model_name='backupjob',
name='operation',
field=models.CharField(choices=[('backup', 'Backup'), ('restore', 'Wiederherstellung')], default='backup', max_length=20, verbose_name='Vorgang'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-09-24 21:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0032_backupjob_operation'),
]
operations = [
migrations.AlterField(
model_name='backupjob',
name='status',
field=models.CharField(choices=[('pending', 'Wartend'), ('running', 'Läuft'), ('completed', 'Abgeschlossen'), ('failed', 'Fehlgeschlagen'), ('cancelled', 'Abgebrochen')], default='pending', max_length=20, verbose_name='Status'),
),
]

View File

@@ -327,6 +327,21 @@ class Destinataer(models.Model):
"""Get the most recent funding grant""" """Get the most recent funding grant"""
return self.foerderung_set.order_by("-jahr", "-betrag").first() return self.foerderung_set.order_by("-jahr", "-betrag").first()
@property
def adresse(self):
"""Construct full address from separate fields"""
parts = []
if self.strasse:
parts.append(self.strasse)
if self.plz or self.ort:
city_part = []
if self.plz:
city_part.append(self.plz)
if self.ort:
city_part.append(self.ort)
parts.append(" ".join(city_part))
return "\n".join(parts) if parts else ""
def erfuellt_voraussetzungen(self): def erfuellt_voraussetzungen(self):
"""Prüft die Unterstützungsvoraussetzungen gemäß Angaben. """Prüft die Unterstützungsvoraussetzungen gemäß Angaben.
- Abkömmling muss True sein - Abkömmling muss True sein
@@ -347,6 +362,20 @@ class Destinataer(models.Model):
vermoegen_ok = (self.vermoegen or Decimal("0")) <= Decimal("15500") vermoegen_ok = (self.vermoegen or Decimal("0")) <= Decimal("15500")
return bool(self.ist_abkoemmling and einkommen_ok and vermoegen_ok) return bool(self.ist_abkoemmling and einkommen_ok and vermoegen_ok)
@property
def adresse(self):
"""Computed address property combining strasse, plz, ort"""
parts = []
if self.strasse:
parts.append(self.strasse)
if self.plz and self.ort:
parts.append(f"{self.plz} {self.ort}")
elif self.plz:
parts.append(self.plz)
elif self.ort:
parts.append(self.ort)
return "\n".join(parts) if parts else None
def naechste_studiennachweis_termine(self): def naechste_studiennachweis_termine(self):
"""Gibt die nächsten beiden Stichtage (15.03, 15.09) zurück.""" """Gibt die nächsten beiden Stichtage (15.03, 15.09) zurück."""
import datetime as _dt import datetime as _dt
@@ -522,6 +551,8 @@ class Land(models.Model):
max_length=20, max_length=20,
choices=ZAHLUNGSWEISE_CHOICES, choices=ZAHLUNGSWEISE_CHOICES,
default="jaehrlich", default="jaehrlich",
null=True,
blank=True,
verbose_name="Zahlungsweise", verbose_name="Zahlungsweise",
) )
pachtzins_pro_ha = models.DecimalField( pachtzins_pro_ha = models.DecimalField(
@@ -544,7 +575,12 @@ class Land(models.Model):
# Umsatzsteuer # Umsatzsteuer
ust_option = models.BooleanField(default=False, verbose_name="USt-Option") ust_option = models.BooleanField(default=False, verbose_name="USt-Option")
ust_satz = models.DecimalField( ust_satz = models.DecimalField(
max_digits=4, decimal_places=2, default=19.00, verbose_name="USt-Satz (%)" max_digits=4,
decimal_places=2,
default=19.00,
null=True,
blank=True,
verbose_name="USt-Satz (%)"
) )
# Umlagen (Durchreichungen) # Umlagen (Durchreichungen)
@@ -2234,6 +2270,7 @@ class BackupJob(models.Model):
("running", "Läuft"), ("running", "Läuft"),
("completed", "Abgeschlossen"), ("completed", "Abgeschlossen"),
("failed", "Fehlgeschlagen"), ("failed", "Fehlgeschlagen"),
("cancelled", "Abgebrochen"),
] ]
TYPE_CHOICES = [ TYPE_CHOICES = [
@@ -2242,9 +2279,17 @@ class BackupJob(models.Model):
("files", "Nur Dateien"), ("files", "Nur Dateien"),
] ]
OPERATION_CHOICES = [
("backup", "Backup"),
("restore", "Wiederherstellung"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Job-Details # Job-Details
operation = models.CharField(
max_length=20, choices=OPERATION_CHOICES, default="backup", verbose_name="Vorgang"
)
backup_type = models.CharField( backup_type = models.CharField(
max_length=20, choices=TYPE_CHOICES, verbose_name="Backup-Typ" max_length=20, choices=TYPE_CHOICES, verbose_name="Backup-Typ"
) )
@@ -2725,6 +2770,20 @@ class VierteljahresNachweis(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_related_support_payment(self):
"""Get the related support payment for this quarterly confirmation"""
from datetime import datetime
quarter_start = datetime(self.jahr, (self.quartal - 1) * 3 + 1, 1).date()
quarter_end = datetime(self.jahr, self.quartal * 3, 1).date()
return DestinataerUnterstuetzung.objects.filter(
destinataer=self.destinataer,
faellig_am__gte=quarter_start,
faellig_am__lt=quarter_end,
beschreibung__contains=f"Q{self.quartal}/{self.jahr}"
).first()
@classmethod @classmethod
def get_or_create_for_period(cls, destinataer, jahr, quartal): def get_or_create_for_period(cls, destinataer, jahr, quartal):
"""Get or create a quarterly confirmation for a specific period""" """Get or create a quarterly confirmation for a specific period"""

View File

@@ -262,6 +262,11 @@ urlpatterns = [
name="backup_download", name="backup_download",
), ),
path("administration/backup/restore/", views.backup_restore, name="backup_restore"), path("administration/backup/restore/", views.backup_restore, name="backup_restore"),
path(
"administration/backup/<uuid:backup_id>/cancel/",
views.backup_cancel,
name="backup_cancel",
),
path( path(
"administration/unterstuetzungen/", "administration/unterstuetzungen/",
views.unterstuetzungen_list, views.unterstuetzungen_list,
@@ -364,4 +369,14 @@ urlpatterns = [
views.quarterly_confirmation_update, views.quarterly_confirmation_update,
name="quarterly_confirmation_update", name="quarterly_confirmation_update",
), ),
path(
"quarterly-confirmations/<uuid:pk>/approve/",
views.quarterly_confirmation_approve,
name="quarterly_confirmation_approve",
),
path(
"quarterly-confirmations/<uuid:pk>/reset/",
views.quarterly_confirmation_reset,
name="quarterly_confirmation_reset",
),
] ]

View File

@@ -1705,16 +1705,23 @@ def land_list(request):
sum_wald_qm=Sum("wald_qm"), sum_wald_qm=Sum("wald_qm"),
sum_sonstiges_qm=Sum("sonstiges_qm"), sum_sonstiges_qm=Sum("sonstiges_qm"),
) )
sum_groesse_qm = float(aggregates.get("sum_groesse_qm") or 0)
sum_gruenland_qm = float(aggregates.get("sum_gruenland_qm") or 0) sum_gruenland_qm = float(aggregates.get("sum_gruenland_qm") or 0)
sum_acker_qm = float(aggregates.get("sum_acker_qm") or 0) sum_acker_qm = float(aggregates.get("sum_acker_qm") or 0)
sum_wald_qm = float(aggregates.get("sum_wald_qm") or 0) sum_wald_qm = float(aggregates.get("sum_wald_qm") or 0)
sum_sonstiges_qm = float(aggregates.get("sum_sonstiges_qm") or 0) sum_sonstiges_qm = float(aggregates.get("sum_sonstiges_qm") or 0)
sum_total_use_qm = sum_gruenland_qm + sum_acker_qm + sum_wald_qm + sum_sonstiges_qm sum_total_use_qm = sum_gruenland_qm + sum_acker_qm + sum_wald_qm + sum_sonstiges_qm
# Calculate verpachtung statistics
total_plots = lands.count()
verpachtete_plots = lands.filter(verp_flaeche_aktuell__gt=0).count()
unveerpachtete_plots = total_plots - verpachtete_plots
def pct(part, total): def pct(part, total):
return round((part / total) * 100, 1) if total and part is not None else 0.0 return round((part / total) * 100, 1) if total and part is not None else 0.0
stats = { stats = {
"sum_groesse_qm": sum_groesse_qm,
"sum_gruenland_qm": sum_gruenland_qm, "sum_gruenland_qm": sum_gruenland_qm,
"sum_acker_qm": sum_acker_qm, "sum_acker_qm": sum_acker_qm,
"sum_wald_qm": sum_wald_qm, "sum_wald_qm": sum_wald_qm,
@@ -1723,6 +1730,11 @@ def land_list(request):
"pct_gruenland": pct(sum_gruenland_qm, sum_total_use_qm), "pct_gruenland": pct(sum_gruenland_qm, sum_total_use_qm),
"pct_acker": pct(sum_acker_qm, sum_total_use_qm), "pct_acker": pct(sum_acker_qm, sum_total_use_qm),
"pct_wald": pct(sum_wald_qm, sum_total_use_qm), "pct_wald": pct(sum_wald_qm, sum_total_use_qm),
"total_plots": total_plots,
"verpachtete_plots": verpachtete_plots,
"unveerpachtete_plots": unveerpachtete_plots,
"pct_verpachtet": pct(verpachtete_plots, total_plots),
"pct_unveerpachtet": pct(unveerpachtete_plots, total_plots),
} }
# Prepare size chart data (top 30 by size) # Prepare size chart data (top 30 by size)
@@ -1783,10 +1795,31 @@ def land_detail(request, pk):
def land_create(request): def land_create(request):
if request.method == "POST": if request.method == "POST":
form = LandForm(request.POST) form = LandForm(request.POST)
# Debug: Print form data
print("=== LAND CREATE DEBUG ===")
print(f"POST data: {dict(request.POST)}")
print(f"Form is valid: {form.is_valid()}")
if not form.is_valid():
print(f"Form errors: {form.errors}")
print(f"Form non-field errors: {form.non_field_errors()}")
# Add error messages for debugging
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f"{field}: {error}")
if form.is_valid(): if form.is_valid():
land = form.save() try:
messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.') land = form.save()
return redirect("stiftung:land_detail", pk=land.pk) messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.')
print(f"Successfully created land: {land}")
return redirect("stiftung:land_detail", pk=land.pk)
except Exception as e:
print(f"Error saving land: {e}")
messages.error(request, f"Fehler beim Speichern: {str(e)}")
else:
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
else: else:
form = LandForm() form = LandForm()
@@ -4456,9 +4489,22 @@ def unterstuetzungen_list(request):
# Enhanced PDF export with corporate identity # Enhanced PDF export with corporate identity
elif export_format == "pdf": elif export_format == "pdf":
return export_unterstuetzungen_pdf(request, qs, selected_ids) return export_unterstuetzungen_pdf(request, qs, selected_ids)
# Get quarterly confirmation statistics
quarterly_stats = {}
total_quarterly = VierteljahresNachweis.objects.count()
for status_code, status_name in VierteljahresNachweis.STATUS_CHOICES:
count = VierteljahresNachweis.objects.filter(status=status_code).count()
quarterly_stats[status_code] = {
'name': status_name,
'count': count
}
context = { context = {
"unterstuetzungen": qs, "unterstuetzungen": qs,
"status_filter": status, "status_filter": status,
"quarterly_stats": quarterly_stats,
"total_quarterly": total_quarterly,
} }
return render(request, "stiftung/unterstuetzungen_list.html", context) return render(request, "stiftung/unterstuetzungen_list.html", context)
@@ -5193,6 +5239,8 @@ def destinataer_export(request, pk):
# 1. Entity data as JSON # 1. Entity data as JSON
entity_data = { entity_data = {
"id": str(destinataer.id), "id": str(destinataer.id),
"anrede": destinataer.get_anrede_display() if hasattr(destinataer, 'anrede') and destinataer.anrede else None,
"titel": destinataer.titel if hasattr(destinataer, 'titel') else None,
"vorname": destinataer.vorname, "vorname": destinataer.vorname,
"nachname": destinataer.nachname, "nachname": destinataer.nachname,
"geburtsdatum": ( "geburtsdatum": (
@@ -5202,6 +5250,7 @@ def destinataer_export(request, pk):
), ),
"email": destinataer.email, "email": destinataer.email,
"telefon": destinataer.telefon, "telefon": destinataer.telefon,
"mobil": destinataer.mobil if hasattr(destinataer, 'mobil') else None,
"iban": destinataer.iban, "iban": destinataer.iban,
"strasse": destinataer.strasse, "strasse": destinataer.strasse,
"plz": destinataer.plz, "plz": destinataer.plz,
@@ -5340,6 +5389,69 @@ def destinataer_export(request, pk):
json.dumps(docs_data, indent=2, ensure_ascii=False), json.dumps(docs_data, indent=2, ensure_ascii=False),
) )
# 4. Quarterly Confirmations with documents
quarterly_confirmations = destinataer.quartalseinreichungen.all().order_by("-jahr", "-quartal")
quarterly_data = []
for confirmation in quarterly_confirmations:
confirmation_data = {
"id": str(confirmation.id),
"jahr": confirmation.jahr,
"quartal": confirmation.quartal,
"quartal_display": confirmation.get_quartal_display(),
"status": confirmation.status,
"status_display": confirmation.get_status_display(),
"studiennachweis_erforderlich": confirmation.studiennachweis_erforderlich,
"studiennachweis_eingereicht": confirmation.studiennachweis_eingereicht,
"studiennachweis_bemerkung": confirmation.studiennachweis_bemerkung,
"einkommenssituation_bestaetigt": confirmation.einkommenssituation_bestaetigt,
"einkommenssituation_text": confirmation.einkommenssituation_text,
"vermogenssituation_bestaetigt": confirmation.vermogenssituation_bestaetigt,
"vermogenssituation_text": confirmation.vermogenssituation_text,
"weitere_dokumente_beschreibung": confirmation.weitere_dokumente_beschreibung,
"interne_notizen": confirmation.interne_notizen,
"erstellt_am": confirmation.erstellt_am.isoformat(),
"aktualisiert_am": confirmation.aktualisiert_am.isoformat(),
"eingereicht_am": confirmation.eingereicht_am.isoformat() if confirmation.eingereicht_am else None,
"geprueft_am": confirmation.geprueft_am.isoformat() if confirmation.geprueft_am else None,
"geprueft_von": confirmation.geprueft_von.username if confirmation.geprueft_von else None,
"faelligkeitsdatum": confirmation.faelligkeitsdatum.isoformat() if confirmation.faelligkeitsdatum else None,
"completion_percentage": confirmation.completion_percentage(),
"uploaded_files": []
}
# Add uploaded files from quarterly confirmation
quarterly_files = [
("studiennachweis", confirmation.studiennachweis_datei),
("einkommenssituation", confirmation.einkommenssituation_datei),
("vermogenssituation", confirmation.vermogenssituation_datei),
("weitere_dokumente", confirmation.weitere_dokumente),
]
for file_type, file_field in quarterly_files:
if file_field and os.path.exists(file_field.path):
file_info = {
"type": file_type,
"name": os.path.basename(file_field.name),
"path": file_field.name
}
confirmation_data["uploaded_files"].append(file_info)
# Add file to ZIP
safe_filename = f"quarterly_{confirmation.jahr}_Q{confirmation.quartal}_{file_type}_{os.path.basename(file_field.name)}"
zipf.write(
file_field.path,
f"vierteljahresnachweis/{safe_filename}"
)
quarterly_data.append(confirmation_data)
if quarterly_data:
zipf.writestr(
"vierteljahresnachweis.json",
json.dumps(quarterly_data, indent=2, ensure_ascii=False),
)
# Prepare response # Prepare response
with open(temp_file.name, "rb") as f: with open(temp_file.name, "rb") as f:
response = HttpResponse(f.read(), content_type="application/zip") response = HttpResponse(f.read(), content_type="application/zip")
@@ -5888,58 +6000,118 @@ def backup_restore(request):
messages.error(request, "Bitte wählen Sie eine Backup-Datei aus.") messages.error(request, "Bitte wählen Sie eine Backup-Datei aus.")
return redirect("stiftung:backup_management") return redirect("stiftung:backup_management")
# Validate file # Validate file format
if not backup_file.name.endswith(".tar.gz"): if not backup_file.name.endswith(".tar.gz"):
messages.error( messages.error(
request, "Ungültiges Backup-Format. Nur .tar.gz Dateien sind erlaubt." request, "Ungültiges Backup-Format. Nur .tar.gz Dateien sind erlaubt."
) )
return redirect("stiftung:backup_management") return redirect("stiftung:backup_management")
# Save uploaded file # Save uploaded file to temporary location
import os import os
import tempfile import tempfile
temp_dir = tempfile.mkdtemp() temp_dir = tempfile.mkdtemp()
backup_path = os.path.join(temp_dir, backup_file.name) backup_path = os.path.join(temp_dir, backup_file.name)
with open(backup_path, "wb+") as destination: try:
for chunk in backup_file.chunks(): with open(backup_path, "wb+") as destination:
destination.write(chunk) for chunk in backup_file.chunks():
destination.write(chunk)
# Create restore job # Validate the backup file
restore_job = BackupJob.objects.create( from stiftung.backup_utils import validate_backup_file
backup_type="full",
created_by=request.user,
backup_filename=backup_file.name,
)
# Log restore initiation is_valid, message = validate_backup_file(backup_path)
if not is_valid:
messages.error(request, f"Ungültiges Backup: {message}")
return redirect("stiftung:backup_management")
# Show validation success
messages.info(request, f"Backup validiert: {message}")
# Create restore job
restore_job = BackupJob.objects.create(
operation="restore",
backup_type="full",
created_by=request.user,
backup_filename=backup_file.name,
)
# Log restore initiation
from stiftung.audit import log_system_action
log_system_action(
request=request,
action="restore",
description=f"Wiederherstellung gestartet von: {backup_file.name}",
details={
"restore_job_id": str(restore_job.id),
"filename": backup_file.name,
},
)
# Start restore process
import threading
from stiftung.backup_utils import run_restore
restore_thread = threading.Thread(
target=run_restore, args=(str(restore_job.id), backup_path)
)
restore_thread.start()
messages.success(
request, f'Wiederherstellung von "{backup_file.name}" wurde gestartet. '
f'Überwachen Sie den Fortschritt in der Backup-Historie.'
)
return redirect("stiftung:backup_management")
except Exception as e:
messages.error(request, f"Fehler beim Verarbeiten der Backup-Datei: {e}")
return redirect("stiftung:backup_management")
return redirect("stiftung:backup_management")
@login_required
def backup_cancel(request, backup_id):
"""Cancel a running backup job"""
try:
backup_job = BackupJob.objects.get(id=backup_id)
# Only allow cancelling running or pending jobs
if backup_job.status not in ['running', 'pending']:
messages.error(request, "Nur laufende oder wartende Backups können abgebrochen werden.")
return redirect("stiftung:backup_management")
# Check if user has permission to cancel (either own job or admin)
if backup_job.created_by != request.user and not request.user.is_staff:
messages.error(request, "Sie können nur Ihre eigenen Backup-Jobs abbrechen.")
return redirect("stiftung:backup_management")
# Mark as cancelled
from django.utils import timezone
backup_job.status = "cancelled"
backup_job.completed_at = timezone.now()
backup_job.error_message = f"Abgebrochen von {request.user.username}"
backup_job.save()
# Log the cancellation
from stiftung.audit import log_system_action from stiftung.audit import log_system_action
log_system_action( log_system_action(
request=request, request=request,
action="restore", action="backup_cancel",
description=f"Wiederherstellung gestartet von: {backup_file.name}", description=f"Backup-Job abgebrochen: {backup_job.get_backup_type_display()}",
details={ details={"backup_job_id": str(backup_job.id)},
"restore_job_id": str(restore_job.id),
"filename": backup_file.name,
},
) )
# Start restore process messages.success(request, f"Backup-Job wurde abgebrochen.")
import threading
from stiftung.backup_utils import run_restore except BackupJob.DoesNotExist:
messages.error(request, "Backup-Job nicht gefunden.")
restore_thread = threading.Thread( except Exception as e:
target=run_restore, args=(str(restore_job.id), backup_path) messages.error(request, f"Fehler beim Abbrechen des Backup-Jobs: {e}")
)
restore_thread.start()
messages.success(
request, f'Wiederherstellung von "{backup_file.name}" wurde gestartet.'
)
return redirect("stiftung:backup_management")
return redirect("stiftung:backup_management") return redirect("stiftung:backup_management")
@@ -6857,7 +7029,16 @@ def unterstuetzungen_all(request):
# Statistics # Statistics
total_betrag = unterstuetzungen.aggregate(total=Sum("betrag"))["total"] or 0 total_betrag = unterstuetzungen.aggregate(total=Sum("betrag"))["total"] or 0
avg_betrag = unterstuetzungen.aggregate(avg=Avg("betrag"))["avg"] or 0
# Get quarterly confirmation statistics
quarterly_stats = {}
total_quarterly = VierteljahresNachweis.objects.count()
for status_code, status_name in VierteljahresNachweis.STATUS_CHOICES:
count = VierteljahresNachweis.objects.filter(status=status_code).count()
quarterly_stats[status_code] = {
'name': status_name,
'count': count
}
# Available destinataer for filter # Available destinataer for filter
destinataer = Destinataer.objects.all().order_by("nachname", "vorname") destinataer = Destinataer.objects.all().order_by("nachname", "vorname")
@@ -6868,7 +7049,8 @@ def unterstuetzungen_all(request):
"title": "Alle Unterstützungen", "title": "Alle Unterstützungen",
"status_filter": status, "status_filter": status,
"total_betrag": total_betrag, "total_betrag": total_betrag,
"avg_betrag": avg_betrag, "quarterly_stats": quarterly_stats,
"total_quarterly": total_quarterly,
"status_choices": DestinataerUnterstuetzung.STATUS_CHOICES, "status_choices": DestinataerUnterstuetzung.STATUS_CHOICES,
"destinataer": destinataer, "destinataer": destinataer,
} }
@@ -7325,7 +7507,8 @@ def quarterly_confirmation_update(request, pk):
def create_quarterly_support_payment(nachweis): def create_quarterly_support_payment(nachweis):
""" """
Create an automatic support payment when all quarterly requirements are met Get or create a single support payment for this quarterly confirmation
Ensures only one payment exists per destinataer per quarter
""" """
destinataer = nachweis.destinataer destinataer = nachweis.destinataer
@@ -7340,18 +7523,31 @@ def create_quarterly_support_payment(nachweis):
if not destinataer.iban: if not destinataer.iban:
return None return None
# Check if a payment for this quarter already exists # Calculate quarter date range for more robust search
quarter_start = datetime(nachweis.jahr, (nachweis.quartal - 1) * 3 + 1, 1).date() quarter_start = datetime(nachweis.jahr, (nachweis.quartal - 1) * 3 + 1, 1).date()
quarter_end = datetime(nachweis.jahr, nachweis.quartal * 3, 1).date() if nachweis.quartal == 4: # Q4 special case
quarter_end = datetime(nachweis.jahr + 1, 1, 1).date()
else:
quarter_end = datetime(nachweis.jahr, nachweis.quartal * 3 + 1, 1).date()
# Search for existing payment - use broader criteria to catch all possibilities
existing_payment = DestinataerUnterstuetzung.objects.filter( existing_payment = DestinataerUnterstuetzung.objects.filter(
destinataer=destinataer, destinataer=destinataer,
faellig_am__gte=quarter_start, faellig_am__gte=quarter_start,
faellig_am__lt=quarter_end, faellig_am__lt=quarter_end
beschreibung__contains=f"Q{nachweis.quartal}/{nachweis.jahr}" ).filter(
Q(beschreibung__contains=f"Q{nachweis.quartal}/{nachweis.jahr}") |
Q(beschreibung__contains=f"Vierteljährliche Unterstützung")
).first() ).first()
if existing_payment: if existing_payment:
# Update existing payment to ensure it matches current requirements
existing_payment.betrag = destinataer.vierteljaehrlicher_betrag
existing_payment.empfaenger_iban = destinataer.iban
existing_payment.empfaenger_name = destinataer.get_full_name()
existing_payment.verwendungszweck = f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} - {destinataer.get_full_name()}"
existing_payment.beschreibung = f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} (automatisch erstellt)"
existing_payment.save()
return existing_payment return existing_payment
# Get default payment account # Get default payment account
@@ -7363,8 +7559,6 @@ def create_quarterly_support_payment(nachweis):
return None return None
# Calculate payment due date (last day of quarter) # Calculate payment due date (last day of quarter)
# Quarter end months and their last days:
# Q1: March (31), Q2: June (30), Q3: September (30), Q4: December (31)
quarter_end_month = nachweis.quartal * 3 quarter_end_month = nachweis.quartal * 3
if nachweis.quartal == 1: # Q1: January-March (ends March 31) if nachweis.quartal == 1: # Q1: January-March (ends March 31)
@@ -7544,21 +7738,103 @@ def quarterly_confirmation_approve(request, pk):
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk) nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
if request.method == "POST": if request.method == "POST":
if nachweis.status == 'eingereicht': if nachweis.status in ['eingereicht', 'geprueft']:
nachweis.status = 'geprueft' # Check if we need to create or update support payment
nachweis.geprueft_am = timezone.now() related_payment = nachweis.get_related_support_payment()
nachweis.geprueft_von = request.user
nachweis.save()
messages.success( if nachweis.status == 'eingereicht' or (nachweis.status == 'geprueft' and not related_payment):
request, # Approve the quarterly confirmation
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} " nachweis.status = 'geprueft'
f"({nachweis.jahr} Q{nachweis.quartal}) wurde freigegeben." nachweis.geprueft_am = timezone.now()
) nachweis.geprueft_von = request.user
nachweis.save()
# Handle support payment - create if missing, update if exists
if not related_payment:
# Create new support payment
related_payment = create_quarterly_support_payment(nachweis)
if related_payment:
related_payment.status = 'in_bearbeitung'
related_payment.aktualisiert_am = timezone.now()
related_payment.save()
messages.success(
request,
f"Vierteljahresnachweis freigegeben und neue Unterstützung über {related_payment.betrag}€ für {nachweis.destinataer.get_full_name()} "
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erstellt."
)
else:
messages.warning(
request,
f"Vierteljahresnachweis freigegeben, aber Unterstützung konnte nicht erstellt werden. "
f"Bitte prüfen Sie die Einstellungen für {nachweis.destinataer.get_full_name()}."
)
elif related_payment.status == 'geplant':
# Update existing payment
related_payment.status = 'in_bearbeitung'
related_payment.aktualisiert_am = timezone.now()
related_payment.save()
messages.success(
request,
f"Vierteljahresnachweis und zugehörige Unterstützung für {nachweis.destinataer.get_full_name()} "
f"({nachweis.jahr} Q{nachweis.quartal}) wurden freigegeben."
)
else:
messages.success(
request,
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
f"({nachweis.jahr} Q{nachweis.quartal}) wurde freigegeben."
)
else: else:
messages.error( messages.error(
request, request,
"Nur eingereichte Nachweise können freigegeben werden." "Nur eingereichte oder bereits genehmigte Nachweise können verarbeitet werden."
)
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
@login_required
def quarterly_confirmation_reset(request, pk):
"""Reset quarterly confirmation status (staff only)"""
if not request.user.is_staff:
messages.error(request, "Sie haben keine Berechtigung für diese Aktion.")
return redirect("stiftung:destinataer_list")
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
if request.method == "POST":
if nachweis.status in ['geprueft', 'eingereicht']:
# Reset the quarterly confirmation status
nachweis.status = 'eingereicht' if nachweis.is_complete() else 'teilweise'
nachweis.geprueft_am = None
nachweis.geprueft_von = None
nachweis.aktualisiert_am = timezone.now()
nachweis.save()
# Reset related support payment status if it exists
related_payment = nachweis.get_related_support_payment()
if related_payment and related_payment.status == 'in_bearbeitung':
related_payment.status = 'geplant'
related_payment.aktualisiert_am = timezone.now()
related_payment.save()
messages.success(
request,
f"Vierteljahresnachweis und zugehörige Unterstützung für {nachweis.destinataer.get_full_name()} "
f"({nachweis.jahr} Q{nachweis.quartal}) wurden zurückgesetzt."
)
else:
messages.success(
request,
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
f"({nachweis.jahr} Q{nachweis.quartal}) wurde zurückgesetzt."
)
else:
messages.error(
request,
"Nur genehmigte oder eingereichte Nachweise können zurückgesetzt werden."
) )
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk) return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)

View File

@@ -101,6 +101,7 @@
<thead> <thead>
<tr> <tr>
<th>Erstellt</th> <th>Erstellt</th>
<th>Vorgang</th>
<th>Typ</th> <th>Typ</th>
<th>Status</th> <th>Status</th>
<th>Größe</th> <th>Größe</th>
@@ -117,10 +118,19 @@
<small class="text-muted">{{ backup.created_at|date:"H:i:s" }}</small> <small class="text-muted">{{ backup.created_at|date:"H:i:s" }}</small>
</td> </td>
<td> <td>
<span class="badge bg-info">{{ backup.get_backup_type_display }}</span> <span class="badge bg-{% if backup.operation == 'backup' %}info{% else %}warning{% endif %}">
{% if backup.operation == 'backup' %}
<i class="fas fa-save me-1"></i>Backup
{% else %}
<i class="fas fa-undo me-1"></i>Restore
{% endif %}
</span>
</td> </td>
<td> <td>
<span class="badge bg-{% if backup.status == 'completed' %}success{% elif backup.status == 'failed' %}danger{% elif backup.status == 'running' %}primary{% else %}secondary{% endif %}"> <span class="badge bg-secondary">{{ backup.get_backup_type_display }}</span>
</td>
<td>
<span class="badge bg-{% if backup.status == 'completed' %}success{% elif backup.status == 'failed' %}danger{% elif backup.status == 'running' %}primary{% elif backup.status == 'cancelled' %}warning{% else %}secondary{% endif %}">
{{ backup.get_status_display }} {{ backup.get_status_display }}
</span> </span>
{% if backup.status == 'running' %} {% if backup.status == 'running' %}
@@ -153,7 +163,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if backup.status == 'completed' %} {% if backup.status == 'completed' and backup.operation == 'backup' %}
<a href="{% url 'stiftung:backup_download' backup.id %}" class="btn btn-outline-primary btn-sm" title="Herunterladen"> <a href="{% url 'stiftung:backup_download' backup.id %}" class="btn btn-outline-primary btn-sm" title="Herunterladen">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</a> </a>
@@ -162,9 +172,27 @@
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
</button> </button>
{% elif backup.status == 'running' %} {% elif backup.status == 'running' %}
<button class="btn btn-outline-primary btn-sm" disabled title="Läuft..."> <div class="btn-group" role="group">
<i class="fas fa-spinner fa-spin"></i> <button class="btn btn-outline-primary btn-sm" disabled title="Läuft...">
</button> <i class="fas fa-spinner fa-spin"></i>
</button>
{% if backup.created_by == request.user or request.user.is_staff %}
<a href="{% url 'stiftung:backup_cancel' backup.id %}"
class="btn btn-outline-danger btn-sm"
onclick="return confirm('Sind Sie sicher, dass Sie diesen Backup-Job abbrechen möchten?')"
title="Abbrechen">
<i class="fas fa-times"></i>
</a>
{% endif %}
</div>
{% elif backup.status == 'cancelled' %}
<span class="badge bg-warning" title="Abgebrochen">
<i class="fas fa-ban"></i> Abgebrochen
</span>
{% elif backup.status == 'completed' and backup.operation == 'restore' %}
<span class="badge bg-success" title="Wiederherstellung abgeschlossen">
<i class="fas fa-check"></i> Fertig
</span>
{% else %} {% else %}
<span class="text-muted">-</span> <span class="text-muted">-</span>
{% endif %} {% endif %}

View File

@@ -198,7 +198,17 @@
<em class="text-muted">Nicht angegeben</em> <em class="text-muted">Nicht angegeben</em>
{% endif %} {% endif %}
</span> </span>
<textarea name="adresse" class="form-control edit-mode" style="display: none;" rows="3" placeholder="Straße Nr.&#10;PLZ Ort">{{ destinataer.adresse }}</textarea> <div class="edit-mode" style="display: none;">
<input name="strasse" class="form-control mb-2" placeholder="Straße" value="{{ destinataer.strasse|default:'' }}">
<div class="row">
<div class="col-4">
<input name="plz" class="form-control" placeholder="PLZ" value="{{ destinataer.plz|default:'' }}">
</div>
<div class="col-8">
<input name="ort" class="form-control" placeholder="Ort" value="{{ destinataer.ort|default:'' }}">
</div>
</div>
</div>
</p> </p>
</div> </div>
</div> </div>
@@ -592,13 +602,28 @@
title="Bearbeiten (Vollbild)"> title="Bearbeiten (Vollbild)">
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
</a> </a>
{% if nachweis.status == 'eingereicht' and user.is_staff %} {% if user.is_staff %}
<button type="button" {% if nachweis.status == 'eingereicht' %}
class="btn btn-sm btn-outline-success" <button type="button"
onclick="approveQuarterly('{{ nachweis.id }}')" class="btn btn-sm btn-outline-success"
title="Freigeben"> onclick="approveQuarterly('{{ nachweis.id }}')"
<i class="fas fa-check"></i> title="Freigeben">
</button> <i class="fas fa-check"></i>
</button>
{% elif nachweis.status == 'geprueft' %}
<button type="button"
class="btn btn-sm btn-outline-success"
onclick="approveQuarterly('{{ nachweis.id }}')"
title="Erneut freigeben / Unterstützung synchronisieren">
<i class="fas fa-sync"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-warning"
onclick="resetQuarterly('{{ nachweis.id }}')"
title="Status zurücksetzen">
<i class="fas fa-undo"></i>
</button>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</td> </td>
@@ -1353,5 +1378,28 @@ function approveQuarterly(nachweisId) {
}); });
} }
} }
function resetQuarterly(nachweisId) {
if (confirm('Möchten Sie den Status dieses Vierteljahresnachweis wirklich zurücksetzen?')) {
fetch(`/quarterly-confirmations/${nachweisId}/reset/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json',
},
})
.then(response => {
if (response.ok) {
location.reload(); // Reload to show updated status
} else {
alert('Fehler beim Zurücksetzen des Nachweises.');
}
})
.catch(error => {
console.error('Error:', error);
alert('Fehler beim Zurücksetzen des Nachweises.');
});
}
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -65,51 +65,79 @@
</div> </div>
</div> </div>
<!-- Stats and Chart --> <!-- Charts Row -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-lg-6 mb-3"> <!-- Usage Chart -->
<div class="card shadow h-100"> <div class="col-lg-4 mb-3">
<div class="card shadow">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"> <h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-percentage me-2"></i>Flächennutzung (aktuelle Auswahl) <i class="fas fa-percentage me-2"></i>Flächennutzung
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row text-center"> <div class="row text-center mb-3">
<div class="col-4"> <div class="col-4">
<div class="h4 mb-0">{{ stats.pct_wald|default:0 }}%</div> <div class="h6 mb-0 text-success">{{ stats.pct_wald|default:0 }}%</div>
<div class="text-muted">Wald</div> <div class="small text-muted">Wald</div>
</div> </div>
<div class="col-4"> <div class="col-4">
<div class="h4 mb-0">{{ stats.pct_acker|default:0 }}%</div> <div class="h6 mb-0 text-info">{{ stats.pct_acker|default:0 }}%</div>
<div class="text-muted">Acker</div> <div class="small text-muted">Acker</div>
</div> </div>
<div class="col-4"> <div class="col-4">
<div class="h4 mb-0">{{ stats.pct_gruenland|default:0 }}%</div> <div class="h6 mb-0 text-warning">{{ stats.pct_gruenland|default:0 }}%</div>
<div class="text-muted">Grünland</div> <div class="small text-muted">Grünland</div>
</div> </div>
</div> </div>
<div class="mt-3 small text-muted text-center"> <div style="height: 200px;">
Gesamt (Nutzungsarten): {{ stats.sum_total_use_qm|floatformat:0 }} qm
</div>
<div class="mt-3" style="height:200px">
<canvas id="usageChart"></canvas> <canvas id="usageChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-6 mb-3">
<!-- Sizes Chart -->
<div class="col-lg-4 mb-3">
<div class="card shadow"> <div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"> <h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-bar me-2"></i>Größen der Grundstücke (Top 30) <i class="fas fa-chart-bar me-2"></i>Größen (Top 30)
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div style="height: 140px; overflow: hidden;"> <div class="text-center mb-3">
<div id="sizesChart" style="display: flex; align-items: end; height: 100%; gap: 2px; padding: 5px;"> <div class="h6 mb-0">{{ stats.total_plots }}</div>
<!-- Simple CSS bar chart will be populated by JavaScript --> <div class="small text-muted">Grundstücke gesamt</div>
</div>
<div style="height: 200px;">
<canvas id="sizesChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Verpachtung Chart -->
<div class="col-lg-4 mb-3">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-handshake me-2"></i>Verpachtungsstatus
</h6>
</div>
<div class="card-body">
<div class="row text-center mb-3">
<div class="col-6">
<div class="h6 mb-0 text-success">{{ stats.pct_verpachtet|default:0 }}%</div>
<div class="small text-muted">Verpachtet</div>
</div> </div>
<div class="col-6">
<div class="h6 mb-0 text-secondary">{{ stats.pct_unveerpachtet|default:0 }}%</div>
<div class="small text-muted">Verfügbar</div>
</div>
</div>
<div style="height: 200px;">
<canvas id="verpachtungChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
@@ -379,8 +407,13 @@
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
legend: { position: 'bottom' }, legend: {
display: true,
position: 'bottom',
labels: { padding: 10, fontSize: 12 }
},
tooltip: { tooltip: {
callbacks: { callbacks: {
label: function(context) { label: function(context) {
@@ -399,50 +432,107 @@
console.log('usageChart canvas not found'); console.log('usageChart canvas not found');
} }
// Simple CSS bar chart for sizes (no Chart.js) // Bar chart for sizes
const chartContainer = document.getElementById('sizesChart'); const ctx = document.getElementById('sizesChart');
if (chartContainer) { if (ctx) {
console.log('Found sizesChart container'); console.log('Found sizesChart canvas');
const labels = JSON.parse('{{ size_chart_labels_json|escapejs }}'); const labels = JSON.parse('{{ size_chart_labels_json|escapejs }}');
const dataVals = JSON.parse('{{ size_chart_values_json|escapejs }}'); const dataVals = JSON.parse('{{ size_chart_values_json|escapejs }}');
console.log('Bar chart data:', {labels: labels.length, values: dataVals.length}); console.log('Bar chart data:', {labels: labels.length, values: dataVals.length});
if (dataVals.length > 0) { new Chart(ctx, {
const maxValue = Math.max(...dataVals); type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Größe (qm)',
data: dataVals,
backgroundColor: 'rgba(0, 104, 55, 0.6)',
borderColor: '#006837',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { ticks: { autoSkip: true, maxTicksLimit: 8 } },
y: { beginAtZero: true }
},
plugins: {
legend: { display: false },
tooltip: { callbacks: { label: function(ctx) { return ctx.parsed.y.toLocaleString() + ' qm'; } } }
}
}
});
console.log('Bar chart created successfully');
} else {
console.log('sizesChart canvas not found');
}
chartContainer.innerHTML = ''; // Clear container // Doughnut chart for verpachtung
const vctx = document.getElementById('verpachtungChart');
if (vctx) {
console.log('Found verpachtungChart canvas');
// Create bars const verpachtet = {{ stats.verpachtete_plots|default:0 }};
dataVals.forEach((value, index) => { const verfuegbar = {{ stats.unveerpachtete_plots|default:0 }};
const barHeight = (value / maxValue) * 120; // Max height 120px
const bar = document.createElement('div');
bar.style.cssText = `
background-color: rgba(0, 104, 55, 0.8);
border: 1px solid #006837;
width: ${Math.max(100 / dataVals.length - 1, 8)}%;
height: ${barHeight}px;
min-height: 2px;
display: flex;
align-items: end;
justify-content: center;
font-size: 10px;
color: white;
text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
cursor: pointer;
`;
// Add tooltip on hover console.log('Verpachtung data:', {verpachtet: verpachtet, verfuegbar: verfuegbar});
bar.title = `${labels[index] || 'N/A'}: ${value.toLocaleString()} qm`;
chartContainer.appendChild(bar); if (verpachtet > 0 || verfuegbar > 0) {
const verpachtungData = [];
const verpachtungLabels = [];
const verpachtungColors = [];
if (verpachtet > 0) {
verpachtungData.push(verpachtet);
verpachtungLabels.push('Verpachtet');
verpachtungColors.push('rgba(40, 167, 69, 0.8)');
}
if (verfuegbar > 0) {
verpachtungData.push(verfuegbar);
verpachtungLabels.push('Verfügbar');
verpachtungColors.push('rgba(108, 117, 125, 0.8)');
}
new Chart(vctx, {
type: 'doughnut',
data: {
labels: verpachtungLabels,
datasets: [{
data: verpachtungData,
backgroundColor: verpachtungColors,
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'bottom',
labels: { padding: 10, fontSize: 12 }
},
tooltip: {
callbacks: {
label: function(ctx) {
const percentage = ((ctx.parsed / (verpachtet + verfuegbar)) * 100).toFixed(1);
return ctx.label + ': ' + ctx.parsed + ' (' + percentage + '%)';
}
}
}
}
}
}); });
console.log('Verpachtung chart created successfully');
console.log('CSS bar chart created successfully');
} else { } else {
chartContainer.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">Keine Daten verfügbar</div>'; console.log('No data for verpachtung chart');
} }
} else { } else {
console.log('sizesChart container not found'); console.log('verpachtungChart canvas not found');
} }
} catch (e) { } catch (e) {
console.error('Chart initialization error:', e); console.error('Chart initialization error:', e);

View File

@@ -189,7 +189,7 @@
<!-- Statistics Cards --> <!-- Statistics Cards -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-3"> <div class="col-md-4">
<div class="card bg-primary text-white"> <div class="card bg-primary text-white">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Gesamtbetrag</h5> <h5 class="card-title">Gesamtbetrag</h5>
@@ -197,15 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-4">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">Durchschnitt</h5>
<h3 class="card-text">€{{ avg_betrag|floatformat:2 }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white"> <div class="card bg-info text-white">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Anzahl</h5> <h5 class="card-title">Anzahl</h5>
@@ -213,6 +205,38 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body text-center">
<h6 class="card-title text-muted mb-2">
<i class="fas fa-chart-pie me-2"></i>Vierteljahresnachweis Status
</h6>
<div class="row g-1">
<div class="col-6">
<div class="text-primary fw-bold">{{ total_quarterly }}</div>
<small class="text-muted">Gesamt</small>
</div>
{% for status_code, status_data in quarterly_stats.items %}
{% if status_data.count > 0 %}
<div class="col-6">
<div class="fw-bold
{% if status_code == 'offen' %}text-secondary
{% elif status_code == 'teilweise' %}text-warning
{% elif status_code == 'eingereicht' %}text-info
{% elif status_code == 'geprueft' %}text-success
{% elif status_code == 'nachbesserung' %}text-danger
{% elif status_code == 'abgelehnt' %}text-dark
{% endif %}">
{{ status_data.count }}
</div>
<small class="text-muted">{{ status_data.name }}</small>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
</div> </div>
<!-- Filters --> <!-- Filters -->

View File

@@ -212,6 +212,47 @@
</div> </div>
</div> </div>
<!-- Quarterly Confirmation Statistics -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-light">
<h6 class="card-title mb-0">
<i class="fas fa-chart-pie me-2"></i>
Vierteljahresnachweis Status Übersicht
</h6>
</div>
<div class="card-body py-2">
<div class="row text-center g-0">
<div class="col-lg-2 col-md-3 col-4 mb-2">
<div class="px-2">
<div class="h5 fw-bold text-primary mb-0">{{ total_quarterly }}</div>
<small class="text-muted">Gesamt</small>
</div>
</div>
{% for status_code, status_data in quarterly_stats.items %}
<div class="col-lg-2 col-md-3 col-4 mb-2">
<div class="px-2">
<div class="h6 fw-bold mb-0
{% if status_code == 'offen' %}text-secondary
{% elif status_code == 'teilweise' %}text-warning
{% elif status_code == 'eingereicht' %}text-info
{% elif status_code == 'geprueft' %}text-success
{% elif status_code == 'nachbesserung' %}text-danger
{% elif status_code == 'abgelehnt' %}text-dark
{% endif %}">
{{ status_data.count }}
</div>
<small class="text-muted">{{ status_data.name }}</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="card shadow"> <div class="card shadow">
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
@@ -258,7 +299,12 @@
{{ u.get_status_display }} {{ u.get_status_display }}
</span> </span>
</td> </td>
<td>{{ u.beschreibung }}</td> <td>
{{ u.beschreibung }}
{% if "Q" in u.beschreibung and "/" in u.beschreibung %}
<i class="fas fa-file-contract text-primary ms-2" title="Verknüpft mit Vierteljahresnachweis" data-bs-toggle="tooltip"></i>
{% endif %}
</td>
<td> <td>
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<a href="{% url 'stiftung:unterstuetzung_edit' pk=u.pk %}" class="btn btn-outline-warning"><i class="fas fa-edit"></i></a> <a href="{% url 'stiftung:unterstuetzung_edit' pk=u.pk %}" class="btn btn-outline-warning"><i class="fas fa-edit"></i></a>
@@ -424,6 +470,12 @@ document.getElementById('exportForm').addEventListener('submit', function(e) {
// Initialize counts on page load // Initialize counts on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
updateSelectedCount(); updateSelectedCount();
// Initialize tooltips for quarterly confirmation indicators
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}); });
</script> </script>

View File

@@ -39,8 +39,6 @@ services:
- LANGUAGE_CODE=de - LANGUAGE_CODE=de
- TIME_ZONE=Europe/Berlin - TIME_ZONE=Europe/Berlin
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- SESSION_COOKIE_NAME=stiftung_sessionid
- CSRF_COOKIE_NAME=stiftung_csrftoken
- PAPERLESS_API_URL=http://paperless:8000 - PAPERLESS_API_URL=http://paperless:8000
- PAPERLESS_API_TOKEN=d477152aca264ea00620910ac09a06f0a4faaecc - PAPERLESS_API_TOKEN=d477152aca264ea00620910ac09a06f0a4faaecc
- PAPERLESS_REQUIRED_TAG=Stiftung_Destinatäre - PAPERLESS_REQUIRED_TAG=Stiftung_Destinatäre
@@ -56,9 +54,7 @@ services:
command: ["python", "manage.py", "runserver", "0.0.0.0:8000"] command: ["python", "manage.py", "runserver", "0.0.0.0:8000"]
paperless: paperless:
build: image: ghcr.io/paperless-ngx/paperless-ngx:latest
context: ./paperless
dockerfile: Dockerfile
ports: ports:
- "8082:8000" - "8082:8000"
environment: environment:

0
debug-502-detailed.sh Normal file
View File

0
debug-502.sh Normal file
View File

0
emergency-fix.sh Normal file
View File

View File

@@ -1,22 +0,0 @@
# Custom Paperless-ngx with German OCR support
FROM ghcr.io/paperless-ngx/paperless-ngx:latest
# Switch to root to install packages
USER root
# Update package list and install German OCR language data
# The correct package name is tesseract-ocr-deu (not tesseract-data-deu)
RUN apt-get update && \
apt-get install -y \
tesseract-ocr-deu \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Verify German language pack is installed
RUN tesseract --list-langs | grep deu || echo "German language pack not found"
# Switch back to paperless user
USER paperless
# Set the OCR language to include German and English
ENV PAPERLESS_OCR_LANGUAGE=deu+eng

View File

0
paperless/README.md Normal file
View File