Generalize email system with invoice workflow and Stiftungsgeschichte category
- Rename DestinataerEmailEingang → EmailEingang with category support (destinataer, rechnung, land_pacht, stiftungsgeschichte, allgemein) - Add invoice capture workflow: create Verwaltungskosten from email, link DMS documents as invoice attachments, track payment status - Add Stiftungsgeschichte email category with auto-detection patterns (Ahnenforschung, Genealogie, Chronik, etc.) and DMS integration - Update poll_emails task with category detection and DMS context mapping - Show available history documents in Geschichte editor sidebar - Consolidate DMS views, remove legacy dokument templates - Update all detail/form templates for DMS document linking - Add deploy.sh script and streamline compose.yml Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -124,9 +124,9 @@ CELERY_RESULT_BACKEND = os.getenv("REDIS_URL", "redis://redis:6379/0")
|
|||||||
from celery.schedules import crontab # noqa: E402
|
from celery.schedules import crontab # noqa: E402
|
||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
# E-Mail-Postfach alle 15 Minuten auf neue Destinatär-Nachrichten prüfen
|
# E-Mail-Postfach alle 15 Minuten auf neue Nachrichten pruefen
|
||||||
"poll-destinataer-emails": {
|
"poll-emails": {
|
||||||
"task": "stiftung.tasks.poll_destinataer_emails",
|
"task": "stiftung.tasks.poll_emails",
|
||||||
"schedule": crontab(minute="*/15"),
|
"schedule": crontab(minute="*/15"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -140,15 +140,6 @@ IMAP_PASSWORD = os.getenv("IMAP_PASSWORD", "")
|
|||||||
IMAP_FOLDER = os.getenv("IMAP_FOLDER", "INBOX")
|
IMAP_FOLDER = os.getenv("IMAP_FOLDER", "INBOX")
|
||||||
IMAP_USE_SSL = os.getenv("IMAP_USE_SSL", "true").lower() == "true"
|
IMAP_USE_SSL = os.getenv("IMAP_USE_SSL", "true").lower() == "true"
|
||||||
|
|
||||||
# Paperless
|
|
||||||
PAPERLESS_API_URL = os.getenv("PAPERLESS_API_URL", "https://vhtv-stiftung.de/paperless")
|
|
||||||
PAPERLESS_API_TOKEN = os.getenv("PAPERLESS_API_TOKEN")
|
|
||||||
PAPERLESS_REQUIRED_TAG = os.getenv("PAPERLESS_REQUIRED_TAG", "Stiftung_Destinatäre")
|
|
||||||
PAPERLESS_LAND_TAG = os.getenv("PAPERLESS_LAND_TAG", "Stiftung_Land_und_Pächter")
|
|
||||||
PAPERLESS_ADMIN_TAG = os.getenv("PAPERLESS_ADMIN_TAG", "Stiftung_Administration")
|
|
||||||
PAPERLESS_DESTINATAERE_TAG_ID = os.getenv("PAPERLESS_DESTINATAERE_TAG_ID")
|
|
||||||
PAPERLESS_LAND_TAG_ID = os.getenv("PAPERLESS_LAND_TAG_ID")
|
|
||||||
PAPERLESS_ADMIN_TAG_ID = os.getenv("PAPERLESS_ADMIN_TAG_ID")
|
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
LOGIN_URL = "/login/"
|
LOGIN_URL = "/login/"
|
||||||
|
|||||||
@@ -7,90 +7,6 @@ class Command(BaseCommand):
|
|||||||
help = "Initialize default app configuration settings"
|
help = "Initialize default app configuration settings"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
# Paperless Integration Settings
|
|
||||||
paperless_settings = [
|
|
||||||
{
|
|
||||||
"key": "paperless_api_url",
|
|
||||||
"display_name": "Paperless API URL",
|
|
||||||
"description": "The base URL for your Paperless-NGX API (e.g., http://paperless.example.com:8000)",
|
|
||||||
"value": "http://192.168.178.167:30070",
|
|
||||||
"default_value": "http://192.168.178.167:30070",
|
|
||||||
"setting_type": "url",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_api_token",
|
|
||||||
"display_name": "Paperless API Token",
|
|
||||||
"description": "The authentication token for Paperless API access",
|
|
||||||
"value": "",
|
|
||||||
"default_value": "",
|
|
||||||
"setting_type": "text",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_destinataere_tag",
|
|
||||||
"display_name": "Destinatäre Tag Name",
|
|
||||||
"description": "The tag name used to identify Destinatäre documents in Paperless",
|
|
||||||
"value": "Stiftung_Destinatäre",
|
|
||||||
"default_value": "Stiftung_Destinatäre",
|
|
||||||
"setting_type": "tag",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_destinataere_tag_id",
|
|
||||||
"display_name": "Destinatäre Tag ID",
|
|
||||||
"description": "The numeric ID of the Destinatäre tag in Paperless",
|
|
||||||
"value": "210",
|
|
||||||
"default_value": "210",
|
|
||||||
"setting_type": "tag_id",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_land_tag",
|
|
||||||
"display_name": "Land & Pächter Tag Name",
|
|
||||||
"description": "The tag name used to identify Land and Pächter documents in Paperless",
|
|
||||||
"value": "Stiftung_Land_und_Pächter",
|
|
||||||
"default_value": "Stiftung_Land_und_Pächter",
|
|
||||||
"setting_type": "tag",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_land_tag_id",
|
|
||||||
"display_name": "Land & Pächter Tag ID",
|
|
||||||
"description": "The numeric ID of the Land & Pächter tag in Paperless",
|
|
||||||
"value": "204",
|
|
||||||
"default_value": "204",
|
|
||||||
"setting_type": "tag_id",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_admin_tag",
|
|
||||||
"display_name": "Administration Tag Name",
|
|
||||||
"description": "The tag name used to identify Administration documents in Paperless",
|
|
||||||
"value": "Stiftung_Administration",
|
|
||||||
"default_value": "Stiftung_Administration",
|
|
||||||
"setting_type": "tag",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paperless_admin_tag_id",
|
|
||||||
"display_name": "Administration Tag ID",
|
|
||||||
"description": "The numeric ID of the Administration tag in Paperless",
|
|
||||||
"value": "216",
|
|
||||||
"default_value": "216",
|
|
||||||
"setting_type": "tag_id",
|
|
||||||
"category": "paperless",
|
|
||||||
"order": 8,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# E-Mail / IMAP Settings
|
# E-Mail / IMAP Settings
|
||||||
email_settings = [
|
email_settings = [
|
||||||
{
|
{
|
||||||
@@ -155,7 +71,7 @@ class Command(BaseCommand):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
all_settings = paperless_settings + email_settings
|
all_settings = email_settings
|
||||||
|
|
||||||
created_count = 0
|
created_count = 0
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
|
|||||||
33
app/stiftung/migrations/0049_phase3_email_dms_m2m.py
Normal file
33
app/stiftung/migrations/0049_phase3_email_dms_m2m.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-03-12 08:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stiftung', '0048_phase3_dms_dokument_datei'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='destinataeremaileingang',
|
||||||
|
name='dokument_dateien',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Automatisch befüllte Anhänge als Django-DMS-Dateien.', related_name='email_eingaenge', to='stiftung.dokumentdatei', verbose_name='DMS-Dokumente (Anhänge)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='appconfiguration',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(choices=[('paperless', 'Paperless Integration'), ('email', 'E-Mail / IMAP'), ('general', 'General Settings'), ('corporate', 'Corporate Identity'), ('notifications', 'Notifications'), ('system', 'System Settings')], default='general', max_length=50, verbose_name='Category'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='appconfiguration',
|
||||||
|
name='setting_type',
|
||||||
|
field=models.CharField(choices=[('text', 'Text'), ('password', 'Password'), ('number', 'Number'), ('boolean', 'Boolean'), ('url', 'URL'), ('tag', 'Tag Name'), ('tag_id', 'Tag ID')], default='text', max_length=20, verbose_name='Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='destinataeremaileingang',
|
||||||
|
name='paperless_dokument_ids',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text='Veraltet – wird nach vollständiger Migration entfernt. Neue Anhänge in dokument_dateien.', verbose_name='Paperless Dokument-IDs (Anhänge, veraltet)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Phase 4: Generalize EmailEingang + Rechnungsworkflow
|
||||||
|
# - Rename DestinataerEmailEingang → EmailEingang
|
||||||
|
# - Add kategorie, verwaltungskosten FK, land FK, verpachtung FK
|
||||||
|
# - Expand status choices (rechnung_erfasst, zahlung_gebucht)
|
||||||
|
# - Add verwaltungskosten FK to DokumentDatei
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stiftung', '0049_phase3_email_dms_m2m'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# 1. Rename model (preserves DB table, updates Django state)
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='DestinataerEmailEingang',
|
||||||
|
new_name='EmailEingang',
|
||||||
|
),
|
||||||
|
|
||||||
|
# 2. Add kategorie field to EmailEingang
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='kategorie',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('destinataer', 'Destinataer'),
|
||||||
|
('rechnung', 'Rechnung'),
|
||||||
|
('land_pacht', 'Grundstueck / Pacht'),
|
||||||
|
('allgemein', 'Allgemein'),
|
||||||
|
],
|
||||||
|
default='allgemein',
|
||||||
|
max_length=20,
|
||||||
|
verbose_name='Kategorie',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# 3. Add verwaltungskosten FK to EmailEingang
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='verwaltungskosten',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='email_eingaenge',
|
||||||
|
to='stiftung.verwaltungskosten',
|
||||||
|
verbose_name='Verwaltungskosten / Rechnung',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# 4. Add land FK to EmailEingang
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='land',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='email_eingaenge',
|
||||||
|
to='stiftung.land',
|
||||||
|
verbose_name='Laenderei',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# 5. Add verpachtung FK to EmailEingang
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='verpachtung',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='email_eingaenge',
|
||||||
|
to='stiftung.landverpachtung',
|
||||||
|
verbose_name='Verpachtung',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# 6. Update status choices on EmailEingang
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('neu', 'Neu / Unbearbeitet'),
|
||||||
|
('zugewiesen', 'Destinataer zugewiesen'),
|
||||||
|
('verarbeitet', 'Verarbeitet'),
|
||||||
|
('rechnung_erfasst', 'Rechnung erfasst'),
|
||||||
|
('zahlung_gebucht', 'Zahlung gebucht'),
|
||||||
|
('unbekannt', 'Unbekannter Absender'),
|
||||||
|
('fehler', 'Fehler bei Verarbeitung'),
|
||||||
|
],
|
||||||
|
default='neu',
|
||||||
|
max_length=20,
|
||||||
|
verbose_name='Status',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# 7. Update Meta on EmailEingang
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='emaileingang',
|
||||||
|
options={
|
||||||
|
'ordering': ['-eingangsdatum'],
|
||||||
|
'verbose_name': 'E-Mail-Eingang',
|
||||||
|
'verbose_name_plural': 'E-Mail-Eingaenge',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# 8. Set kategorie='destinataer' for existing emails that have a destinataer FK
|
||||||
|
migrations.RunSQL(
|
||||||
|
sql="UPDATE stiftung_emaileingang SET kategorie = 'destinataer' WHERE destinataer_id IS NOT NULL;",
|
||||||
|
reverse_sql=migrations.RunSQL.noop,
|
||||||
|
),
|
||||||
|
|
||||||
|
# 9. Add verwaltungskosten FK to DokumentDatei
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='dokumentdatei',
|
||||||
|
name='verwaltungskosten',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='dms_dokumente',
|
||||||
|
to='stiftung.verwaltungskosten',
|
||||||
|
verbose_name='Verwaltungskosten / Rechnung',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-03-12 09:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stiftung', '0050_generalize_email_rechnungsworkflow'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='destinataer',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='email_eingaenge', to='stiftung.destinataer', verbose_name='Destinataer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='dokument_dateien',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Automatisch befuellte Anhaenge als Django-DMS-Dateien.', related_name='email_eingaenge', to='stiftung.dokumentdatei', verbose_name='DMS-Dokumente (Anhaenge)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='paperless_dokument_ids',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text='Veraltet – wird nach vollstaendiger Migration entfernt. Neue Anhaenge in dokument_dateien.', verbose_name='Paperless Dokument-IDs (Anhaenge, veraltet)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-03-12 10:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stiftung', '0051_alter_emaileingang_destinataer_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dokumentdatei',
|
||||||
|
name='kontext',
|
||||||
|
field=models.CharField(choices=[('pachtvertrag', 'Pachtvertrag'), ('antrag', 'Antrag / Förderantrag'), ('verwendungsnachweis', 'Verwendungsnachweis'), ('studiennachweis', 'Studiennachweis'), ('rechnung', 'Rechnung'), ('vertrag', 'Vertrag'), ('bericht', 'Bericht'), ('landkarte', 'Landkarte / Kataster'), ('korrespondenz', 'Korrespondenz / Brief'), ('bescheid', 'Bescheid / Behörde'), ('stiftungsgeschichte', 'Stiftungsgeschichte / Archiv'), ('anderes', 'Sonstiges')], default='anderes', max_length=30, verbose_name='Dokumententyp'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='emaileingang',
|
||||||
|
name='kategorie',
|
||||||
|
field=models.CharField(choices=[('destinataer', 'Destinataer'), ('rechnung', 'Rechnung'), ('land_pacht', 'Grundstueck / Pacht'), ('stiftungsgeschichte', 'Stiftungsgeschichte'), ('allgemein', 'Allgemein')], default='allgemein', max_length=20, verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -32,6 +32,7 @@ from .finanzen import ( # noqa: F401
|
|||||||
from .destinataere import ( # noqa: F401
|
from .destinataere import ( # noqa: F401
|
||||||
Destinataer,
|
Destinataer,
|
||||||
DestinataerEmailEingang,
|
DestinataerEmailEingang,
|
||||||
|
EmailEingang,
|
||||||
DestinataerNotiz,
|
DestinataerNotiz,
|
||||||
DestinataerUnterstuetzung,
|
DestinataerUnterstuetzung,
|
||||||
Foerderung,
|
Foerderung,
|
||||||
|
|||||||
@@ -1104,34 +1104,79 @@ class VierteljahresNachweis(models.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class DestinataerEmailEingang(models.Model):
|
class EmailEingang(models.Model):
|
||||||
"""
|
"""
|
||||||
Erfasst eingehende E-Mails von Destinatären.
|
Erfasst eingehende E-Mails (Destinataere, Rechnungen, Grundstuecke, Allgemein).
|
||||||
|
|
||||||
Wird automatisch durch den Celery-Task `poll_destinataer_emails` befüllt,
|
Wird automatisch durch den Celery-Task `poll_emails` befuellt,
|
||||||
der das IMAP-Postfach der Stiftung (paperless@vhtv-stiftung.de) überwacht.
|
der das IMAP-Postfach der Stiftung (paperless@vhtv-stiftung.de) ueberwacht.
|
||||||
Anhänge werden automatisch in Paperless-NGX hochgeladen und als DokumentLink
|
Anhaenge werden direkt als DokumentDatei im Django-DMS gespeichert.
|
||||||
mit dem jeweiligen Destinatär verknüpft.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
KATEGORIE_CHOICES = [
|
||||||
|
("destinataer", "Destinataer"),
|
||||||
|
("rechnung", "Rechnung"),
|
||||||
|
("land_pacht", "Grundstueck / Pacht"),
|
||||||
|
("stiftungsgeschichte", "Stiftungsgeschichte"),
|
||||||
|
("allgemein", "Allgemein"),
|
||||||
|
]
|
||||||
|
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("neu", "Neu / Unbearbeitet"),
|
("neu", "Neu / Unbearbeitet"),
|
||||||
("zugewiesen", "Destinatär zugewiesen"),
|
("zugewiesen", "Destinataer zugewiesen"),
|
||||||
("verarbeitet", "Verarbeitet"),
|
("verarbeitet", "Verarbeitet"),
|
||||||
|
("rechnung_erfasst", "Rechnung erfasst"),
|
||||||
|
("zahlung_gebucht", "Zahlung gebucht"),
|
||||||
("unbekannt", "Unbekannter Absender"),
|
("unbekannt", "Unbekannter Absender"),
|
||||||
("fehler", "Fehler bei Verarbeitung"),
|
("fehler", "Fehler bei Verarbeitung"),
|
||||||
]
|
]
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
# Verknüpfung zum Destinatär (None = unbekannter Absender)
|
# Klassifizierung
|
||||||
|
kategorie = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=KATEGORIE_CHOICES,
|
||||||
|
default="allgemein",
|
||||||
|
verbose_name="Kategorie",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verknuepfung zum Destinataer (None = kein Destinataer-Bezug)
|
||||||
destinataer = models.ForeignKey(
|
destinataer = models.ForeignKey(
|
||||||
Destinataer,
|
Destinataer,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="email_eingaenge",
|
related_name="email_eingaenge",
|
||||||
verbose_name="Destinatär",
|
verbose_name="Destinataer",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verknuepfung zu Verwaltungskosten (Rechnungsworkflow)
|
||||||
|
verwaltungskosten = models.ForeignKey(
|
||||||
|
"Verwaltungskosten",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="email_eingaenge",
|
||||||
|
verbose_name="Verwaltungskosten / Rechnung",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verknuepfung zu Land / Verpachtung
|
||||||
|
land = models.ForeignKey(
|
||||||
|
"Land",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="email_eingaenge",
|
||||||
|
verbose_name="Laenderei",
|
||||||
|
)
|
||||||
|
verpachtung = models.ForeignKey(
|
||||||
|
"LandVerpachtung",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="email_eingaenge",
|
||||||
|
verbose_name="Verpachtung",
|
||||||
)
|
)
|
||||||
|
|
||||||
# E-Mail-Metadaten
|
# E-Mail-Metadaten
|
||||||
@@ -1143,12 +1188,21 @@ class DestinataerEmailEingang(models.Model):
|
|||||||
eingangsdatum = models.DateTimeField(verbose_name="Eingangsdatum")
|
eingangsdatum = models.DateTimeField(verbose_name="Eingangsdatum")
|
||||||
email_text = models.TextField(blank=True, verbose_name="E-Mail-Text")
|
email_text = models.TextField(blank=True, verbose_name="E-Mail-Text")
|
||||||
|
|
||||||
# Anhänge: Liste der Paperless-Dokument-IDs (JSON-Format)
|
# Anhaenge: DMS-Dokumente (Phase 3 – DokumentDatei)
|
||||||
|
dokument_dateien = models.ManyToManyField(
|
||||||
|
"DokumentDatei",
|
||||||
|
blank=True,
|
||||||
|
related_name="email_eingaenge",
|
||||||
|
verbose_name="DMS-Dokumente (Anhaenge)",
|
||||||
|
help_text="Automatisch befuellte Anhaenge als Django-DMS-Dateien.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Anhaenge: Liste der Paperless-Dokument-IDs (JSON-Format, deprecated)
|
||||||
paperless_dokument_ids = models.JSONField(
|
paperless_dokument_ids = models.JSONField(
|
||||||
default=list,
|
default=list,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Paperless Dokument-IDs (Anhänge)",
|
verbose_name="Paperless Dokument-IDs (Anhaenge, veraltet)",
|
||||||
help_text="Automatisch befüllte Liste der hochgeladenen Anhänge in Paperless-NGX",
|
help_text="Veraltet – wird nach vollstaendiger Migration entfernt. Neue Anhaenge in dokument_dateien.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verarbeitungsstatus
|
# Verarbeitungsstatus
|
||||||
@@ -1182,8 +1236,8 @@ class DestinataerEmailEingang(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erfasst am")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erfasst am")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "E-Mail-Eingang (Destinatär)"
|
verbose_name = "E-Mail-Eingang"
|
||||||
verbose_name_plural = "E-Mail-Eingänge (Destinatäre)"
|
verbose_name_plural = "E-Mail-Eingaenge"
|
||||||
ordering = ["-eingangsdatum"]
|
ordering = ["-eingangsdatum"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -1191,10 +1245,18 @@ class DestinataerEmailEingang(models.Model):
|
|||||||
return f"[{self.eingangsdatum.strftime('%d.%m.%Y')}] {dest}: {self.betreff[:60]}"
|
return f"[{self.eingangsdatum.strftime('%d.%m.%Y')}] {dest}: {self.betreff[:60]}"
|
||||||
|
|
||||||
def get_paperless_links(self):
|
def get_paperless_links(self):
|
||||||
"""Gibt Liste der Paperless-Dokument-URLs zurück."""
|
"""Gibt Liste der Paperless-Dokument-URLs zurueck (deprecated)."""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
base = settings.PAPERLESS_API_URL or ""
|
base = settings.PAPERLESS_API_URL or ""
|
||||||
return [
|
return [
|
||||||
f"{base}/documents/{doc_id}/"
|
f"{base}/documents/{doc_id}/"
|
||||||
for doc_id in (self.paperless_dokument_ids or [])
|
for doc_id in (self.paperless_dokument_ids or [])
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_dms_dokumente(self):
|
||||||
|
"""Gibt alle verknuepften DokumentDatei-Objekte zurueck."""
|
||||||
|
return self.dokument_dateien.all()
|
||||||
|
|
||||||
|
|
||||||
|
# Backward-compatible alias
|
||||||
|
DestinataerEmailEingang = EmailEingang
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class DokumentDatei(models.Model):
|
|||||||
("landkarte", "Landkarte / Kataster"),
|
("landkarte", "Landkarte / Kataster"),
|
||||||
("korrespondenz", "Korrespondenz / Brief"),
|
("korrespondenz", "Korrespondenz / Brief"),
|
||||||
("bescheid", "Bescheid / Behörde"),
|
("bescheid", "Bescheid / Behörde"),
|
||||||
|
("stiftungsgeschichte", "Stiftungsgeschichte / Archiv"),
|
||||||
("anderes", "Sonstiges"),
|
("anderes", "Sonstiges"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -109,6 +110,13 @@ class DokumentDatei(models.Model):
|
|||||||
related_name="dms_dokumente",
|
related_name="dms_dokumente",
|
||||||
verbose_name="Rentmeister",
|
verbose_name="Rentmeister",
|
||||||
)
|
)
|
||||||
|
verwaltungskosten = models.ForeignKey(
|
||||||
|
"Verwaltungskosten",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True, blank=True,
|
||||||
|
related_name="dms_dokumente",
|
||||||
|
verbose_name="Verwaltungskosten / Rechnung",
|
||||||
|
)
|
||||||
|
|
||||||
# Herkunft (optional: Verweis auf altes Paperless-Dokument zur Rückverfolgung)
|
# Herkunft (optional: Verweis auf altes Paperless-Dokument zur Rückverfolgung)
|
||||||
paperless_dokument_id = models.IntegerField(
|
paperless_dokument_id = models.IntegerField(
|
||||||
|
|||||||
@@ -972,6 +972,11 @@ class LandAbrechnung(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class DokumentLink(models.Model):
|
class DokumentLink(models.Model):
|
||||||
|
"""
|
||||||
|
DEPRECATED: Paperless-basierte Dokumentverknüpfung.
|
||||||
|
Wird durch DokumentDatei (Django-natives DMS) ersetzt.
|
||||||
|
Dieses Modell bleibt für historische Daten erhalten, wird aber nicht mehr aktiv genutzt.
|
||||||
|
"""
|
||||||
KONTEXT_CHOICES = [
|
KONTEXT_CHOICES = [
|
||||||
("pachtvertrag", "Pachtvertrag"),
|
("pachtvertrag", "Pachtvertrag"),
|
||||||
("antrag", "Antrag"),
|
("antrag", "Antrag"),
|
||||||
@@ -1020,18 +1025,6 @@ class DokumentLink(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.titel} ({self.get_kontext_display()})"
|
return f"{self.titel} ({self.get_kontext_display()})"
|
||||||
|
|
||||||
def get_paperless_url(self):
|
|
||||||
"""Gibt die URL zum Dokument in Paperless zurück (über Django Redirect)"""
|
|
||||||
return f"/api/paperless/documents/{self.paperless_document_id}/"
|
|
||||||
|
|
||||||
def get_paperless_thumbnail_url(self):
|
|
||||||
"""Gibt die URL zum Thumbnail in Paperless zurück"""
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
if settings.PAPERLESS_API_URL:
|
|
||||||
return f"{settings.PAPERLESS_API_URL}/api/documents/{self.paperless_document_id}/thumb/"
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_verpachtung(self):
|
def get_verpachtung(self):
|
||||||
"""Gibt die verknüpfte Verpachtung zurück"""
|
"""Gibt die verknüpfte Verpachtung zurück"""
|
||||||
if self.verpachtung_id:
|
if self.verpachtung_id:
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
Celery-Tasks für die automatische Verarbeitung von Destinatär-E-Mails.
|
Celery-Tasks fuer die automatische Verarbeitung eingehender E-Mails.
|
||||||
|
|
||||||
Workflow:
|
Workflow:
|
||||||
1. `poll_destinataer_emails` läuft alle 15 Minuten (Celery Beat)
|
1. `poll_emails` laeuft alle 15 Minuten (Celery Beat)
|
||||||
2. Er liest ungelesene E-Mails aus dem IMAP-Postfach (paperless@vhtv-stiftung.de)
|
2. Er liest ungelesene E-Mails aus dem IMAP-Postfach
|
||||||
3. Für jede E-Mail:
|
3. Fuer jede E-Mail:
|
||||||
a) Absender wird mit Destinatär-Datenbank abgeglichen (E-Mail-Feld)
|
a) Absender wird mit Destinataer-Datenbank abgeglichen (E-Mail-Feld)
|
||||||
b) Ein DestinataerEmailEingang-Datensatz wird angelegt
|
b) Betreff/Body wird auf Rechnungs-Keywords geprueft
|
||||||
c) Alle Anhänge werden per Paperless-API hochgeladen
|
c) Ein EmailEingang-Datensatz wird angelegt (mit Kategorie)
|
||||||
d) Für jeden Anhang wird ein DokumentLink erstellt
|
d) Alle Anhaenge werden als DokumentDatei im Django-DMS gespeichert
|
||||||
4. Nicht erkannte Absender werden als "unbekannt" markiert (manuelle Nachbearbeitung)
|
4. Nicht erkannte Absender werden als "unbekannt" markiert (manuelle Nachbearbeitung)
|
||||||
|
|
||||||
Konfiguration (Umgebungsvariablen in .env / compose.yml):
|
Konfiguration (Umgebungsvariablen in .env / compose.yml):
|
||||||
IMAP_HOST — IMAP-Server-Adresse (z. B. mail.vhtv-stiftung.de)
|
IMAP_HOST — IMAP-Server-Adresse (z. B. mail.vhtv-stiftung.de)
|
||||||
IMAP_PORT — Port (Standard: 993 für SSL)
|
IMAP_PORT — Port (Standard: 993 fuer SSL)
|
||||||
IMAP_USER — Benutzername (z. B. paperless@vhtv-stiftung.de)
|
IMAP_USER — Benutzername
|
||||||
IMAP_PASSWORD — Passwort
|
IMAP_PASSWORD — Passwort
|
||||||
IMAP_FOLDER — Ordner (Standard: INBOX)
|
IMAP_FOLDER — Ordner (Standard: INBOX)
|
||||||
"""
|
"""
|
||||||
@@ -22,22 +22,39 @@ Konfiguration (Umgebungsvariablen in .env / compose.yml):
|
|||||||
import email
|
import email
|
||||||
import email.utils
|
import email.utils
|
||||||
import imaplib
|
import imaplib
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import re
|
||||||
from datetime import datetime, timezone as dt_timezone
|
from datetime import datetime, timezone as dt_timezone
|
||||||
from email.header import decode_header, make_header
|
from email.header import decode_header, make_header
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Patterns fuer Rechnungserkennung im Betreff/Body
|
||||||
|
RECHNUNG_PATTERNS = [
|
||||||
|
re.compile(r"\brechnung\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\binvoice\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\brechnungs[-\s]?nr\.?\s*[:\s]?\s*\d+", re.IGNORECASE),
|
||||||
|
re.compile(r"\bRE[-/]\d{4,}", re.IGNORECASE), # RE-2024001, RE/20240315
|
||||||
|
]
|
||||||
|
|
||||||
|
GESCHICHTE_PATTERNS = [
|
||||||
|
re.compile(r"\bstiftungsgeschichte\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bahnenforschung\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bgenealogie\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bstammbaum\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bhistorisch", re.IGNORECASE),
|
||||||
|
re.compile(r"\bchronik\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\barchiv\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bfamiliengeschichte\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\burkunde\b", re.IGNORECASE),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Hilfsfunktionen
|
# Hilfsfunktionen
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -54,7 +71,7 @@ def _decode_header_value(raw_value: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _parse_email_date(date_str: str) -> datetime:
|
def _parse_email_date(date_str: str) -> datetime:
|
||||||
"""Parst das E-Mail-Datum und gibt ein timezone-aware datetime zurück."""
|
"""Parst das E-Mail-Datum und gibt ein timezone-aware datetime zurueck."""
|
||||||
try:
|
try:
|
||||||
parsed = email.utils.parsedate_to_datetime(date_str)
|
parsed = email.utils.parsedate_to_datetime(date_str)
|
||||||
if parsed.tzinfo is None:
|
if parsed.tzinfo is None:
|
||||||
@@ -86,148 +103,88 @@ def _get_email_body(msg) -> str:
|
|||||||
return "\n".join(body_parts).strip()
|
return "\n".join(body_parts).strip()
|
||||||
|
|
||||||
|
|
||||||
def _poll_paperless_task(api_url: str, headers: dict, task_id: str, filename: str, max_wait: int = 120) -> int | None:
|
def _detect_kategorie(betreff: str, email_text: str, has_destinataer: bool) -> str:
|
||||||
"""
|
"""
|
||||||
Pollt den Paperless-ngx Task-Status bis das Dokument verarbeitet wurde.
|
Erkennt die Kategorie einer Email anhand von Betreff und Body.
|
||||||
Gibt die Dokument-ID zurück, oder None bei Fehler/Timeout.
|
Gibt 'destinataer', 'rechnung', 'stiftungsgeschichte', oder 'allgemein' zurueck.
|
||||||
"""
|
"""
|
||||||
task_url = f"{api_url.rstrip('/')}/api/tasks/?task_id={task_id}"
|
if has_destinataer:
|
||||||
waited = 0
|
return "destinataer"
|
||||||
interval = 2
|
|
||||||
while waited < max_wait:
|
text_to_check = f"{betreff}\n{email_text[:2000]}"
|
||||||
try:
|
|
||||||
resp = requests.get(task_url, headers=headers, timeout=30)
|
# Rechnungserkennung via Patterns
|
||||||
resp.raise_for_status()
|
for pattern in RECHNUNG_PATTERNS:
|
||||||
tasks = resp.json()
|
if pattern.search(text_to_check):
|
||||||
if tasks:
|
return "rechnung"
|
||||||
task = tasks[0] if isinstance(tasks, list) else tasks
|
|
||||||
status = task.get("status", "")
|
# Stiftungsgeschichte-Erkennung
|
||||||
if status == "SUCCESS":
|
for pattern in GESCHICHTE_PATTERNS:
|
||||||
related = task.get("related_document")
|
if pattern.search(text_to_check):
|
||||||
if related:
|
return "stiftungsgeschichte"
|
||||||
# related_document can be a URL like "/api/documents/42/"
|
|
||||||
# or just an ID
|
return "allgemein"
|
||||||
if isinstance(related, str):
|
|
||||||
parts = related.rstrip("/").split("/")
|
|
||||||
try:
|
|
||||||
return int(parts[-1])
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
logger.warning("Konnte Dokument-ID nicht aus '%s' extrahieren.", related)
|
|
||||||
return None
|
|
||||||
return int(related)
|
|
||||||
# Some versions use result_id or document_id
|
|
||||||
for key in ("result_id", "document_id", "id"):
|
|
||||||
val = task.get(key)
|
|
||||||
if val is not None:
|
|
||||||
try:
|
|
||||||
return int(val)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
pass
|
|
||||||
logger.warning("Task %s erfolgreich aber keine Dokument-ID gefunden: %s", task_id, task)
|
|
||||||
return None
|
|
||||||
elif status == "FAILURE":
|
|
||||||
logger.error("Paperless-Task %s fehlgeschlagen für '%s': %s", task_id, filename, task.get("result"))
|
|
||||||
return None
|
|
||||||
except requests.RequestException as exc:
|
|
||||||
logger.warning("Fehler beim Abfragen von Paperless-Task %s: %s", task_id, exc)
|
|
||||||
time.sleep(interval)
|
|
||||||
waited += interval
|
|
||||||
logger.error("Timeout beim Warten auf Paperless-Task %s für '%s' (%ds).", task_id, filename, max_wait)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _upload_to_paperless(content: bytes, filename: str, destinataer=None, betreff: str = "") -> int | None:
|
def _save_to_dms(content: bytes, filename: str, destinataer=None, betreff: str = "", kontext: str = "korrespondenz"):
|
||||||
"""
|
"""
|
||||||
Lädt einen Anhang in Paperless-NGX hoch.
|
Speichert einen E-Mail-Anhang direkt als DokumentDatei im Django-DMS.
|
||||||
|
|
||||||
Gibt die neue Paperless-Dokument-ID zurück, oder None bei Fehler.
|
Gibt das DokumentDatei-Objekt zurueck, oder None bei Fehler.
|
||||||
"""
|
"""
|
||||||
api_url = getattr(settings, "PAPERLESS_API_URL", None)
|
from stiftung.models import DokumentDatei
|
||||||
api_token = getattr(settings, "PAPERLESS_API_TOKEN", None)
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
if not api_url or not api_token:
|
safe_filename = filename or "anhang.bin"
|
||||||
logger.warning("Paperless nicht konfiguriert – Anhang '%s' wird nicht hochgeladen.", filename)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Tag-ID für Destinatäre ermitteln
|
|
||||||
tag_ids = []
|
|
||||||
dest_tag_id = getattr(settings, "PAPERLESS_DESTINATAERE_TAG_ID", None)
|
|
||||||
if dest_tag_id:
|
|
||||||
try:
|
|
||||||
tag_ids.append(int(dest_tag_id))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Correspondent: Name des Destinatärs (optional, Paperless sucht/erstellt ihn)
|
|
||||||
correspondent_name = None
|
|
||||||
if destinataer:
|
|
||||||
correspondent_name = f"{destinataer.vorname} {destinataer.nachname}".strip()
|
|
||||||
|
|
||||||
# Dateiname bereinigen
|
|
||||||
safe_filename = filename or "anhang.pdf"
|
|
||||||
|
|
||||||
# Mime-Type bestimmen
|
|
||||||
mime_type, _ = mimetypes.guess_type(safe_filename)
|
mime_type, _ = mimetypes.guess_type(safe_filename)
|
||||||
mime_type = mime_type or "application/octet-stream"
|
mime_type = mime_type or "application/octet-stream"
|
||||||
|
|
||||||
upload_url = f"{api_url.rstrip('/')}/api/documents/post_document/"
|
titel = f"{betreff[:100]} – {safe_filename}" if betreff else safe_filename
|
||||||
headers = {"Authorization": f"Token {api_token}"}
|
beschreibung = ""
|
||||||
|
if destinataer:
|
||||||
form_data = {}
|
beschreibung = (
|
||||||
if tag_ids:
|
f"Automatisch importiert aus E-Mail-Eingang.\n"
|
||||||
form_data["tags"] = tag_ids
|
f"Absender: {destinataer.vorname} {destinataer.nachname} <{destinataer.email}>"
|
||||||
if correspondent_name:
|
)
|
||||||
form_data["correspondent_name"] = correspondent_name
|
|
||||||
if betreff:
|
|
||||||
form_data["title"] = betreff[:128]
|
|
||||||
|
|
||||||
files = {"document": (safe_filename, io.BytesIO(content), mime_type)}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
doc = DokumentDatei(
|
||||||
upload_url,
|
titel=titel[:255],
|
||||||
headers=headers,
|
beschreibung=beschreibung,
|
||||||
data=form_data,
|
kontext=kontext,
|
||||||
files=files,
|
dateiname_original=safe_filename,
|
||||||
timeout=300, # 5 Minuten für große Anhänge
|
dateityp=mime_type,
|
||||||
|
dateigroesse=len(content),
|
||||||
|
destinataer=destinataer,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
doc.datei.save(safe_filename, ContentFile(content), save=False)
|
||||||
result = response.json()
|
doc.save()
|
||||||
|
logger.info("Anhang '%s' als DokumentDatei gespeichert (ID: %s).", safe_filename, doc.pk)
|
||||||
# Paperless-ngx post_document returns a task UUID string.
|
return doc
|
||||||
# We need to poll the task status to get the actual document ID.
|
except Exception as exc:
|
||||||
if isinstance(result, str):
|
logger.error("Fehler beim Speichern von '%s' im DMS: %s", safe_filename, exc)
|
||||||
task_id = result
|
|
||||||
doc_id = _poll_paperless_task(api_url, headers, task_id, safe_filename)
|
|
||||||
elif isinstance(result, int):
|
|
||||||
doc_id = result
|
|
||||||
else:
|
|
||||||
doc_id = result.get("id")
|
|
||||||
|
|
||||||
logger.info("Anhang '%s' erfolgreich in Paperless hochgeladen (ID: %s).", safe_filename, doc_id)
|
|
||||||
return doc_id
|
|
||||||
except requests.RequestException as exc:
|
|
||||||
logger.error("Fehler beim Hochladen von '%s' in Paperless: %s", safe_filename, exc)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Haupttask
|
# Haupttask
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, max_retries=3, default_retry_delay=300, name="stiftung.tasks.poll_destinataer_emails")
|
@shared_task(bind=True, max_retries=3, default_retry_delay=300, name="stiftung.tasks.poll_emails")
|
||||||
def poll_destinataer_emails(self, search_all_recent_days=0):
|
def poll_emails(self, search_all_recent_days=0):
|
||||||
"""
|
"""
|
||||||
Liest E-Mails aus dem IMAP-Postfach und verarbeitet sie.
|
Liest E-Mails aus dem IMAP-Postfach und verarbeitet sie.
|
||||||
|
|
||||||
Wird durch Celery Beat alle 15 Minuten ausgeführt.
|
Wird durch Celery Beat alle 15 Minuten ausgefuehrt.
|
||||||
|
Erkennt automatisch Destinataer-Emails, Rechnungen und allgemeine Post.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
search_all_recent_days: Wenn > 0, werden alle E-Mails der letzten N Tage
|
search_all_recent_days: Wenn > 0, werden alle E-Mails der letzten N Tage
|
||||||
durchsucht (nicht nur ungelesene). Nützlich für manuellen Abruf.
|
durchsucht (nicht nur ungelesene). Nuetzlich fuer manuellen Abruf.
|
||||||
"""
|
"""
|
||||||
from stiftung.models import Destinataer, DestinataerEmailEingang, DokumentLink
|
from stiftung.models import Destinataer, EmailEingang
|
||||||
|
|
||||||
# IMAP-Konfiguration: DB (AppConfiguration) mit Fallback auf env/settings
|
# IMAP-Konfiguration: DB (AppConfiguration) mit Fallback auf env/settings
|
||||||
from stiftung.utils.config import get_config
|
from stiftung.utils.config import get_config
|
||||||
@@ -242,12 +199,12 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
if not all([imap_host, imap_user, imap_password]):
|
if not all([imap_host, imap_user, imap_password]):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"IMAP nicht konfiguriert (IMAP_HOST, IMAP_USER, IMAP_PASSWORD fehlen). "
|
"IMAP nicht konfiguriert (IMAP_HOST, IMAP_USER, IMAP_PASSWORD fehlen). "
|
||||||
"Task wird übersprungen."
|
"Task wird uebersprungen."
|
||||||
)
|
)
|
||||||
return {"status": "skipped", "reason": "IMAP not configured"}
|
return {"status": "skipped", "reason": "IMAP not configured"}
|
||||||
|
|
||||||
# Vorab: Destinatär-E-Mail-Index für schnelle Zuordnung
|
# Vorab: Destinataer-E-Mail-Index fuer schnelle Zuordnung
|
||||||
# Nur aktive Destinatäre mit gesetzter E-Mail-Adresse
|
# Nur aktive Destinataere mit gesetzter E-Mail-Adresse
|
||||||
destinataer_by_email = {
|
destinataer_by_email = {
|
||||||
d.email.lower(): d
|
d.email.lower(): d
|
||||||
for d in Destinataer.objects.filter(aktiv=True, email__isnull=False).exclude(email="")
|
for d in Destinataer.objects.filter(aktiv=True, email__isnull=False).exclude(email="")
|
||||||
@@ -257,8 +214,8 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
errors = 0
|
errors = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# IMAP-Verbindung aufbauen (mit Socket-Timeout für große E-Mails)
|
# IMAP-Verbindung aufbauen (mit Socket-Timeout fuer grosse E-Mails)
|
||||||
imap_timeout = 120 # Sekunden – genug für große Anhänge
|
imap_timeout = 120 # Sekunden – genug fuer grosse Anhaenge
|
||||||
if imap_use_ssl:
|
if imap_use_ssl:
|
||||||
mail = imaplib.IMAP4_SSL(imap_host, imap_port, timeout=imap_timeout)
|
mail = imaplib.IMAP4_SSL(imap_host, imap_port, timeout=imap_timeout)
|
||||||
else:
|
else:
|
||||||
@@ -289,7 +246,7 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
# Absender ermitteln
|
# Absender ermitteln
|
||||||
from_raw = msg.get("From", "")
|
from_raw = msg.get("From", "")
|
||||||
absender_name_raw, absender_email_raw = email.utils.parseaddr(from_raw)
|
absender_name_raw, absender_email_raw = email.utils.parseaddr(from_raw)
|
||||||
absender_email = absender_email_raw.lower().strip()
|
absender_email_addr = absender_email_raw.lower().strip()
|
||||||
absender_name = _decode_header_value(absender_name_raw)
|
absender_name = _decode_header_value(absender_name_raw)
|
||||||
|
|
||||||
# Betreff
|
# Betreff
|
||||||
@@ -301,30 +258,48 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
# E-Mail-Text
|
# E-Mail-Text
|
||||||
email_text = _get_email_body(msg)
|
email_text = _get_email_body(msg)
|
||||||
|
|
||||||
# Destinatär zuordnen
|
# Destinataer zuordnen
|
||||||
destinataer = destinataer_by_email.get(absender_email)
|
destinataer = destinataer_by_email.get(absender_email_addr)
|
||||||
status = "zugewiesen" if destinataer else "unbekannt"
|
|
||||||
|
|
||||||
# Prüfen ob diese E-Mail bereits verarbeitet wurde (Duplikat-Check via
|
# Kategorie erkennen
|
||||||
|
kategorie = _detect_kategorie(betreff, email_text, has_destinataer=bool(destinataer))
|
||||||
|
|
||||||
|
# Status basierend auf Kategorie
|
||||||
|
if destinataer:
|
||||||
|
status = "zugewiesen"
|
||||||
|
elif kategorie == "rechnung":
|
||||||
|
status = "neu" # Muss manuell als Rechnung erfasst werden
|
||||||
|
else:
|
||||||
|
status = "unbekannt"
|
||||||
|
|
||||||
|
# DMS-Kontext fuer Anhaenge basierend auf Kategorie
|
||||||
|
dms_kontext_map = {
|
||||||
|
"rechnung": "rechnung",
|
||||||
|
"stiftungsgeschichte": "stiftungsgeschichte",
|
||||||
|
}
|
||||||
|
dms_kontext = dms_kontext_map.get(kategorie, "korrespondenz")
|
||||||
|
|
||||||
|
# Pruefen ob diese E-Mail bereits verarbeitet wurde (Duplikat-Check via
|
||||||
# Datum + Absender + Betreff)
|
# Datum + Absender + Betreff)
|
||||||
already_exists = DestinataerEmailEingang.objects.filter(
|
already_exists = EmailEingang.objects.filter(
|
||||||
absender_email=absender_email,
|
absender_email=absender_email_addr,
|
||||||
eingangsdatum=eingangsdatum,
|
eingangsdatum=eingangsdatum,
|
||||||
betreff=betreff[:500],
|
betreff=betreff[:500],
|
||||||
).exists()
|
).exists()
|
||||||
if already_exists:
|
if already_exists:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"E-Mail von %s am %s bereits vorhanden – wird übersprungen.",
|
"E-Mail von %s am %s bereits vorhanden – wird uebersprungen.",
|
||||||
absender_email, eingangsdatum,
|
absender_email_addr, eingangsdatum,
|
||||||
)
|
)
|
||||||
# Als gelesen markieren
|
# Als gelesen markieren
|
||||||
mail.store(msg_id, "+FLAGS", "\\Seen")
|
mail.store(msg_id, "+FLAGS", "\\Seen")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Datensatz anlegen
|
# Datensatz anlegen
|
||||||
eingang = DestinataerEmailEingang(
|
eingang = EmailEingang(
|
||||||
|
kategorie=kategorie,
|
||||||
destinataer=destinataer,
|
destinataer=destinataer,
|
||||||
absender_email=absender_email,
|
absender_email=absender_email_addr,
|
||||||
absender_name=absender_name,
|
absender_name=absender_name,
|
||||||
betreff=betreff[:500],
|
betreff=betreff[:500],
|
||||||
eingangsdatum=eingangsdatum,
|
eingangsdatum=eingangsdatum,
|
||||||
@@ -332,8 +307,8 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Anhänge verarbeiten
|
# Anhaenge verarbeiten und als DokumentDatei im DMS speichern
|
||||||
paperless_ids = []
|
dms_dokumente = []
|
||||||
if msg.is_multipart():
|
if msg.is_multipart():
|
||||||
for part in msg.walk():
|
for part in msg.walk():
|
||||||
disposition = str(part.get_content_disposition() or "")
|
disposition = str(part.get_content_disposition() or "")
|
||||||
@@ -342,51 +317,42 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
content = part.get_payload(decode=True)
|
content = part.get_payload(decode=True)
|
||||||
if not content:
|
if not content:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Anhang '%s' hat keinen Inhalt (möglicherweise zu groß oder beschädigt) – wird übersprungen.",
|
"Anhang '%s' hat keinen Inhalt – wird uebersprungen.",
|
||||||
filename,
|
filename,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
doc_id = _upload_to_paperless(
|
doc = _save_to_dms(
|
||||||
content=content,
|
content=content,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
destinataer=destinataer,
|
destinataer=destinataer,
|
||||||
betreff=betreff,
|
betreff=betreff,
|
||||||
|
kontext=dms_kontext,
|
||||||
)
|
)
|
||||||
if doc_id:
|
if doc:
|
||||||
paperless_ids.append(doc_id)
|
dms_dokumente.append(doc)
|
||||||
# DokumentLink anlegen
|
|
||||||
DokumentLink.objects.create(
|
|
||||||
paperless_document_id=doc_id,
|
|
||||||
kontext="verwendungsnachweis",
|
|
||||||
titel=f"{betreff[:100]} – {filename}" if filename else betreff[:200],
|
|
||||||
beschreibung=(
|
|
||||||
f"Automatisch importiert aus E-Mail-Eingang.\n"
|
|
||||||
f"Absender: {absender_name} <{absender_email}>\n"
|
|
||||||
f"Datum: {eingangsdatum.strftime('%d.%m.%Y %H:%M')}"
|
|
||||||
),
|
|
||||||
destinataer_id=destinataer.pk if destinataer else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
eingang.paperless_dokument_ids = paperless_ids
|
if dms_dokumente:
|
||||||
if paperless_ids:
|
eingang.status = "verarbeitet" if destinataer else status
|
||||||
eingang.status = "verarbeitet" if destinataer else "unbekannt"
|
|
||||||
eingang.save()
|
eingang.save()
|
||||||
|
if dms_dokumente:
|
||||||
|
eingang.dokument_dateien.set(dms_dokumente)
|
||||||
|
|
||||||
# Als gelesen markieren
|
# Als gelesen markieren
|
||||||
mail.store(msg_id, "+FLAGS", "\\Seen")
|
mail.store(msg_id, "+FLAGS", "\\Seen")
|
||||||
processed += 1
|
processed += 1
|
||||||
logger.info(
|
logger.info(
|
||||||
"E-Mail verarbeitet: von=%s, Destinatär=%s, Anhänge=%d",
|
"E-Mail verarbeitet: von=%s, Kategorie=%s, Destinataer=%s, Anhaenge=%d",
|
||||||
absender_email,
|
absender_email_addr,
|
||||||
str(destinataer) if destinataer else "unbekannt",
|
kategorie,
|
||||||
len(paperless_ids),
|
str(destinataer) if destinataer else "–",
|
||||||
|
len(dms_dokumente),
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors += 1
|
errors += 1
|
||||||
logger.exception("Fehler bei Verarbeitung von Nachricht %s: %s", msg_id, exc)
|
logger.exception("Fehler bei Verarbeitung von Nachricht %s: %s", msg_id, exc)
|
||||||
# Nicht als gelesen markieren – wird beim nächsten Lauf erneut versucht
|
# Nicht als gelesen markieren – wird beim naechsten Lauf erneut versucht
|
||||||
|
|
||||||
mail.close()
|
mail.close()
|
||||||
mail.logout()
|
mail.logout()
|
||||||
@@ -395,9 +361,13 @@ def poll_destinataer_emails(self, search_all_recent_days=0):
|
|||||||
logger.error("IMAP-Fehler: %s", exc)
|
logger.error("IMAP-Fehler: %s", exc)
|
||||||
raise self.retry(exc=exc)
|
raise self.retry(exc=exc)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Unerwarteter Fehler im poll_destinataer_emails-Task: %s", exc)
|
logger.exception("Unerwarteter Fehler im poll_emails-Task: %s", exc)
|
||||||
raise self.retry(exc=exc)
|
raise self.retry(exc=exc)
|
||||||
|
|
||||||
result = {"status": "done", "processed": processed, "errors": errors}
|
result = {"status": "done", "processed": processed, "errors": errors}
|
||||||
logger.info("poll_destinataer_emails abgeschlossen: %s", result)
|
logger.info("poll_emails abgeschlossen: %s", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Backward-compatible alias for existing Celery Beat schedules
|
||||||
|
poll_destinataer_emails = poll_emails
|
||||||
|
|||||||
@@ -140,46 +140,7 @@ urlpatterns = [
|
|||||||
views.foerderung_delete,
|
views.foerderung_delete,
|
||||||
name="foerderung_delete",
|
name="foerderung_delete",
|
||||||
),
|
),
|
||||||
# Dokumente URLs
|
# Dokumente-URLs (DMS) – Legacy-Paperless-URLs entfernt (Phase 3)
|
||||||
path("dokumente/", views.dokument_list, name="dokument_list"),
|
|
||||||
path("dokumente/<uuid:pk>/", views.dokument_detail, name="dokument_detail"),
|
|
||||||
path("dokumente/neu/", views.dokument_create, name="dokument_create"),
|
|
||||||
path(
|
|
||||||
"dokumente/<uuid:pk>/bearbeiten/", views.dokument_update, name="dokument_update"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"dokumente/<uuid:pk>/loeschen/", views.dokument_delete, name="dokument_delete"
|
|
||||||
),
|
|
||||||
# Dokumentenverwaltung (Paperless-Integration, Verwaltung & Verknüpfung)
|
|
||||||
path(
|
|
||||||
"dokumente/verwaltung/", views.dokument_management, name="dokument_management"
|
|
||||||
),
|
|
||||||
# Legacy document URLs removed - use dokument_management instead
|
|
||||||
# Dokument-Verknüpfung
|
|
||||||
path(
|
|
||||||
"api/link-document/search/",
|
|
||||||
views.link_document_search,
|
|
||||||
name="link_document_search",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/link-document/create/",
|
|
||||||
views.link_document_create,
|
|
||||||
name="link_document_create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/link-document/list/", views.link_document_list, name="link_document_list"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/link-document/update/",
|
|
||||||
views.link_document_update,
|
|
||||||
name="link_document_update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"api/link-document/delete/<uuid:link_id>/",
|
|
||||||
views.link_document_delete,
|
|
||||||
name="link_document_delete",
|
|
||||||
),
|
|
||||||
# Legacy dokument_verknuepfung URL removed - use dokument_management instead
|
|
||||||
# Jahresbericht URLs
|
# Jahresbericht URLs
|
||||||
path("berichte/", views.bericht_list, name="bericht_list"),
|
path("berichte/", views.bericht_list, name="bericht_list"),
|
||||||
path(
|
path(
|
||||||
@@ -355,19 +316,6 @@ urlpatterns = [
|
|||||||
# API URLs
|
# API URLs
|
||||||
path("api/land-stats/", views.land_stats_api, name="land_stats_api"),
|
path("api/land-stats/", views.land_stats_api, name="land_stats_api"),
|
||||||
path("api/health/", views.health_check, name="health_check"),
|
path("api/health/", views.health_check, name="health_check"),
|
||||||
path("api/paperless/ping/", views.paperless_ping, name="paperless_ping"),
|
|
||||||
path(
|
|
||||||
"api/paperless/documents/",
|
|
||||||
views.paperless_documents,
|
|
||||||
name="paperless_documents",
|
|
||||||
),
|
|
||||||
path("api/paperless/tags/", views.paperless_tags_only, name="paperless_tags_only"),
|
|
||||||
path("api/paperless/debug/", views.paperless_debug, name="paperless_debug"),
|
|
||||||
path(
|
|
||||||
"api/paperless/documents/<int:doc_id>/",
|
|
||||||
views.paperless_document_redirect,
|
|
||||||
name="paperless_document_redirect",
|
|
||||||
),
|
|
||||||
# Veranstaltungsmodul
|
# Veranstaltungsmodul
|
||||||
path("veranstaltungen/", views.veranstaltung_list, name="veranstaltung_list"),
|
path("veranstaltungen/", views.veranstaltung_list, name="veranstaltung_list"),
|
||||||
path("veranstaltungen/neu/", views.veranstaltung_create, name="veranstaltung_create"),
|
path("veranstaltungen/neu/", views.veranstaltung_create, name="veranstaltung_create"),
|
||||||
|
|||||||
@@ -30,25 +30,6 @@ def get_config(key, default=None, fallback_to_settings=True):
|
|||||||
return value if value is not None else default
|
return value if value is not None else default
|
||||||
|
|
||||||
|
|
||||||
def get_paperless_config():
|
|
||||||
"""
|
|
||||||
Get all Paperless-related configuration values
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Dictionary containing all Paperless configuration
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"api_url": get_config("paperless_api_url"),
|
|
||||||
"api_token": get_config("paperless_api_token"),
|
|
||||||
"destinataere_tag": get_config("paperless_destinataere_tag"),
|
|
||||||
"destinataere_tag_id": get_config("paperless_destinataere_tag_id"),
|
|
||||||
"land_tag": get_config("paperless_land_tag"),
|
|
||||||
"land_tag_id": get_config("paperless_land_tag_id"),
|
|
||||||
"admin_tag": get_config("paperless_admin_tag"),
|
|
||||||
"admin_tag_id": get_config("paperless_admin_tag_id"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def set_config(key, value, **kwargs):
|
def set_config(key, value, **kwargs):
|
||||||
"""
|
"""
|
||||||
Set a configuration value
|
Set a configuration value
|
||||||
@@ -63,13 +44,3 @@ def set_config(key, value, **kwargs):
|
|||||||
"""
|
"""
|
||||||
return AppConfiguration.set_setting(key, value, **kwargs)
|
return AppConfiguration.set_setting(key, value, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def is_paperless_configured():
|
|
||||||
"""
|
|
||||||
Check if Paperless is properly configured
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if API URL and token are configured
|
|
||||||
"""
|
|
||||||
config = get_paperless_config()
|
|
||||||
return bool(config["api_url"] and config["api_token"])
|
|
||||||
|
|||||||
@@ -23,25 +23,6 @@ from .destinataere import ( # noqa: F401
|
|||||||
destinataer_export,
|
destinataer_export,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .dokumente import ( # noqa: F401
|
|
||||||
dokument_management,
|
|
||||||
paperless_document_redirect,
|
|
||||||
dokument_list,
|
|
||||||
dokument_detail,
|
|
||||||
dokument_create,
|
|
||||||
dokument_update,
|
|
||||||
dokument_delete,
|
|
||||||
paperless_ping,
|
|
||||||
paperless_documents,
|
|
||||||
paperless_debug,
|
|
||||||
paperless_tags_only,
|
|
||||||
link_document_search,
|
|
||||||
create_paechter_link_for_verpachtung,
|
|
||||||
link_document_create,
|
|
||||||
link_document_list,
|
|
||||||
link_document_update,
|
|
||||||
link_document_delete,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .finanzen import ( # noqa: F401
|
from .finanzen import ( # noqa: F401
|
||||||
bericht_list,
|
bericht_list,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransact
|
|||||||
BriefVorlage, CSVImport, Destinataer,
|
BriefVorlage, CSVImport, Destinataer,
|
||||||
DestinataerEmailEingang, DestinataerNotiz,
|
DestinataerEmailEingang, DestinataerNotiz,
|
||||||
DestinataerUnterstuetzung,
|
DestinataerUnterstuetzung,
|
||||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
DokumentDatei, DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||||
@@ -268,8 +268,8 @@ def destinataer_detail(request, pk):
|
|||||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||||
|
|
||||||
# Alle mit diesem Destinatär verknüpften Dokumente laden
|
# Alle mit diesem Destinatär verknüpften Dokumente laden
|
||||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
verknuepfte_dokumente = DokumentDatei.objects.filter(
|
||||||
destinataer_id=destinataer.pk
|
destinataer=destinataer
|
||||||
).order_by("kontext", "titel")
|
).order_by("kontext", "titel")
|
||||||
|
|
||||||
# Förderungen für diesen Destinatär laden
|
# Förderungen für diesen Destinatär laden
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ def dms_upload(request):
|
|||||||
"land_id": request.GET.get("land", ""),
|
"land_id": request.GET.get("land", ""),
|
||||||
"paechter_id": request.GET.get("paechter", ""),
|
"paechter_id": request.GET.get("paechter", ""),
|
||||||
"verpachtung_id": request.GET.get("verpachtung", ""),
|
"verpachtung_id": request.GET.get("verpachtung", ""),
|
||||||
|
"foerderung_id": request.GET.get("foerderung", ""),
|
||||||
"kontext": request.GET.get("kontext", "anderes"),
|
"kontext": request.GET.get("kontext", "anderes"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +145,7 @@ def dms_upload(request):
|
|||||||
land_id = request.POST.get("land_id", "").strip()
|
land_id = request.POST.get("land_id", "").strip()
|
||||||
paechter_id = request.POST.get("paechter_id", "").strip()
|
paechter_id = request.POST.get("paechter_id", "").strip()
|
||||||
verp_id = request.POST.get("verpachtung_id", "").strip()
|
verp_id = request.POST.get("verpachtung_id", "").strip()
|
||||||
|
foerd_id = request.POST.get("foerderung_id", "").strip()
|
||||||
|
|
||||||
if dest_id:
|
if dest_id:
|
||||||
try:
|
try:
|
||||||
@@ -165,6 +167,11 @@ def dms_upload(request):
|
|||||||
dok.verpachtung_id = verp_id
|
dok.verpachtung_id = verp_id
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if foerd_id:
|
||||||
|
try:
|
||||||
|
dok.foerderung_id = foerd_id
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
_save_upload(request, dok)
|
_save_upload(request, dok)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransact
|
|||||||
BriefVorlage, CSVImport, Destinataer,
|
BriefVorlage, CSVImport, Destinataer,
|
||||||
DestinataerEmailEingang, DestinataerNotiz,
|
DestinataerEmailEingang, DestinataerNotiz,
|
||||||
DestinataerUnterstuetzung,
|
DestinataerUnterstuetzung,
|
||||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
DokumentDatei, DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||||
@@ -138,8 +138,8 @@ def foerderung_detail(request, pk):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Alle mit dieser Förderung verknüpften Dokumente laden
|
# Alle mit dieser Förderung verknüpften Dokumente laden
|
||||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
verknuepfte_dokumente = DokumentDatei.objects.filter(
|
||||||
foerderung_id=foerderung.pk
|
foerderung=foerderung
|
||||||
).order_by("kontext", "titel")
|
).order_by("kontext", "titel")
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||||
BriefVorlage, CSVImport, Destinataer,
|
BriefVorlage, CSVImport, Destinataer,
|
||||||
DestinataerEmailEingang, DestinataerNotiz,
|
DestinataerEmailEingang, EmailEingang, DestinataerNotiz,
|
||||||
DestinataerUnterstuetzung,
|
DestinataerUnterstuetzung,
|
||||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||||
@@ -105,9 +105,16 @@ def geschichte_create(request):
|
|||||||
else:
|
else:
|
||||||
form = GeschichteSeiteForm()
|
form = GeschichteSeiteForm()
|
||||||
|
|
||||||
|
# Verfuegbare Stiftungsgeschichte-Dokumente aus DMS
|
||||||
|
from stiftung.models import DokumentDatei
|
||||||
|
geschichte_dokumente = DokumentDatei.objects.filter(
|
||||||
|
kontext="stiftungsgeschichte"
|
||||||
|
).order_by("-erstellt_am")[:20]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'form': form,
|
'form': form,
|
||||||
'title': 'Neue Geschichtsseite'
|
'title': 'Neue Geschichtsseite',
|
||||||
|
'geschichte_dokumente': geschichte_dokumente,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'stiftung/geschichte/form.html', context)
|
return render(request, 'stiftung/geschichte/form.html', context)
|
||||||
@@ -134,10 +141,17 @@ def geschichte_edit(request, slug):
|
|||||||
else:
|
else:
|
||||||
form = GeschichteSeiteForm(instance=seite)
|
form = GeschichteSeiteForm(instance=seite)
|
||||||
|
|
||||||
|
# Verfuegbare Stiftungsgeschichte-Dokumente aus DMS
|
||||||
|
from stiftung.models import DokumentDatei
|
||||||
|
geschichte_dokumente = DokumentDatei.objects.filter(
|
||||||
|
kontext="stiftungsgeschichte"
|
||||||
|
).order_by("-erstellt_am")[:20]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'form': form,
|
'form': form,
|
||||||
'seite': seite,
|
'seite': seite,
|
||||||
'title': f'Bearbeiten: {seite.titel}'
|
'title': f'Bearbeiten: {seite.titel}',
|
||||||
|
'geschichte_dokumente': geschichte_dokumente,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'stiftung/geschichte/form.html', context)
|
return render(request, 'stiftung/geschichte/form.html', context)
|
||||||
@@ -583,16 +597,19 @@ def kalender_view(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def email_eingang_list(request):
|
def email_eingang_list(request):
|
||||||
"""
|
"""
|
||||||
Übersicht aller eingegangenen E-Mails von Destinatären.
|
Uebersicht aller eingegangenen E-Mails.
|
||||||
Zeigt ungeklärte Absender zuerst, dann chronologisch absteigend.
|
Filtert nach Status und Kategorie, zeigt ungeklaerte Absender zuerst.
|
||||||
"""
|
"""
|
||||||
status_filter = request.GET.get("status", "")
|
status_filter = request.GET.get("status", "")
|
||||||
|
kategorie_filter = request.GET.get("kategorie", "")
|
||||||
search = request.GET.get("q", "").strip()
|
search = request.GET.get("q", "").strip()
|
||||||
|
|
||||||
qs = DestinataerEmailEingang.objects.select_related("destinataer", "quartalsnachweis")
|
qs = EmailEingang.objects.select_related("destinataer", "quartalsnachweis", "verwaltungskosten")
|
||||||
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
qs = qs.filter(status=status_filter)
|
qs = qs.filter(status=status_filter)
|
||||||
|
if kategorie_filter:
|
||||||
|
qs = qs.filter(kategorie=kategorie_filter)
|
||||||
if search:
|
if search:
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(absender_email__icontains=search)
|
Q(absender_email__icontains=search)
|
||||||
@@ -604,7 +621,7 @@ def email_eingang_list(request):
|
|||||||
|
|
||||||
# Unbekannte Absender zuerst, dann nach Datum absteigend
|
# Unbekannte Absender zuerst, dann nach Datum absteigend
|
||||||
qs = qs.order_by(
|
qs = qs.order_by(
|
||||||
"status", # "unbekannt" kommt alphabetisch vor "verarbeitet" / "zugewiesen"
|
"status",
|
||||||
"-eingangsdatum",
|
"-eingangsdatum",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -612,16 +629,19 @@ def email_eingang_list(request):
|
|||||||
page_obj = paginator.get_page(request.GET.get("page"))
|
page_obj = paginator.get_page(request.GET.get("page"))
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"title": "E-Mail-Eingang (Destinatäre)",
|
"title": "E-Mail-Eingang",
|
||||||
"page_obj": page_obj,
|
"page_obj": page_obj,
|
||||||
"status_filter": status_filter,
|
"status_filter": status_filter,
|
||||||
|
"kategorie_filter": kategorie_filter,
|
||||||
"search": search,
|
"search": search,
|
||||||
"status_choices": DestinataerEmailEingang.STATUS_CHOICES,
|
"status_choices": EmailEingang.STATUS_CHOICES,
|
||||||
|
"kategorie_choices": EmailEingang.KATEGORIE_CHOICES,
|
||||||
"counts": {
|
"counts": {
|
||||||
"gesamt": DestinataerEmailEingang.objects.count(),
|
"gesamt": EmailEingang.objects.count(),
|
||||||
"neu": DestinataerEmailEingang.objects.filter(status="neu").count(),
|
"neu": EmailEingang.objects.filter(status="neu").count(),
|
||||||
"unbekannt": DestinataerEmailEingang.objects.filter(status="unbekannt").count(),
|
"unbekannt": EmailEingang.objects.filter(status="unbekannt").count(),
|
||||||
"fehler": DestinataerEmailEingang.objects.filter(status="fehler").count(),
|
"rechnung": EmailEingang.objects.filter(kategorie="rechnung").count(),
|
||||||
|
"fehler": EmailEingang.objects.filter(status="fehler").count(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return render(request, "stiftung/email_eingang/list.html", context)
|
return render(request, "stiftung/email_eingang/list.html", context)
|
||||||
@@ -629,8 +649,8 @@ def email_eingang_list(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def email_eingang_detail(request, pk):
|
def email_eingang_detail(request, pk):
|
||||||
"""Detailansicht einer eingegangenen E-Mail mit Möglichkeit zur manuellen Zuordnung."""
|
"""Detailansicht einer eingegangenen E-Mail mit Zuordnung und Rechnungserfassung."""
|
||||||
eingang = get_object_or_404(DestinataerEmailEingang, pk=pk)
|
eingang = get_object_or_404(EmailEingang, pk=pk)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
action = request.POST.get("action")
|
action = request.POST.get("action")
|
||||||
@@ -641,19 +661,62 @@ def email_eingang_detail(request, pk):
|
|||||||
try:
|
try:
|
||||||
destinataer = Destinataer.objects.get(pk=dest_id)
|
destinataer = Destinataer.objects.get(pk=dest_id)
|
||||||
eingang.destinataer = destinataer
|
eingang.destinataer = destinataer
|
||||||
|
eingang.kategorie = "destinataer"
|
||||||
eingang.status = "zugewiesen"
|
eingang.status = "zugewiesen"
|
||||||
eingang.save()
|
eingang.save()
|
||||||
# Verknüpfte DokumentLinks ebenfalls dem Destinatär zuordnen
|
eingang.dokument_dateien.filter(destinataer__isnull=True).update(
|
||||||
if eingang.paperless_dokument_ids:
|
destinataer=destinataer
|
||||||
DokumentLink.objects.filter(
|
|
||||||
paperless_document_id__in=eingang.paperless_dokument_ids
|
|
||||||
).update(destinataer_id=destinataer.pk)
|
|
||||||
messages.success(
|
|
||||||
request,
|
|
||||||
f"E-Mail wurde {destinataer} zugeordnet.",
|
|
||||||
)
|
)
|
||||||
|
messages.success(request, f"E-Mail wurde {destinataer} zugeordnet.")
|
||||||
except Destinataer.DoesNotExist:
|
except Destinataer.DoesNotExist:
|
||||||
messages.error(request, "Destinatär nicht gefunden.")
|
messages.error(request, "Destinataer nicht gefunden.")
|
||||||
|
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||||||
|
|
||||||
|
elif action == "erfasse_rechnung":
|
||||||
|
# Erstelle Verwaltungskosten-Eintrag aus Email
|
||||||
|
bezeichnung = request.POST.get("bezeichnung", eingang.betreff[:200]).strip()
|
||||||
|
betrag = request.POST.get("betrag", "0").strip().replace(",", ".")
|
||||||
|
kategorie = request.POST.get("vk_kategorie", "rechnung_intern")
|
||||||
|
lieferant = request.POST.get("lieferant", eingang.absender_name or eingang.absender_email).strip()
|
||||||
|
rechnungsnummer = request.POST.get("rechnungsnummer", "").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from decimal import Decimal
|
||||||
|
vk = Verwaltungskosten(
|
||||||
|
bezeichnung=bezeichnung[:200],
|
||||||
|
kategorie=kategorie,
|
||||||
|
betrag=Decimal(betrag) if betrag else Decimal("0"),
|
||||||
|
datum=eingang.eingangsdatum.date(),
|
||||||
|
lieferant_firma=lieferant[:200],
|
||||||
|
rechnungsnummer=rechnungsnummer[:100],
|
||||||
|
status="erhalten",
|
||||||
|
beschreibung=f"Automatisch erfasst aus E-Mail-Eingang.\nBetreff: {eingang.betreff}\nAbsender: {eingang.absender_email}",
|
||||||
|
)
|
||||||
|
vk.save()
|
||||||
|
|
||||||
|
# Verknuepfe Email mit Verwaltungskosten
|
||||||
|
eingang.verwaltungskosten = vk
|
||||||
|
eingang.kategorie = "rechnung"
|
||||||
|
eingang.status = "rechnung_erfasst"
|
||||||
|
eingang.save()
|
||||||
|
|
||||||
|
# Verknuepfe angehaengte Dokumente mit Verwaltungskosten
|
||||||
|
for dok in eingang.dokument_dateien.all():
|
||||||
|
dok.verwaltungskosten = vk
|
||||||
|
dok.kontext = "rechnung"
|
||||||
|
dok.save()
|
||||||
|
|
||||||
|
messages.success(request, f'Rechnung "{bezeichnung}" erfasst (€{betrag}).')
|
||||||
|
except Exception as exc:
|
||||||
|
messages.error(request, f"Fehler beim Erfassen der Rechnung: {exc}")
|
||||||
|
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||||||
|
|
||||||
|
elif action == "set_kategorie":
|
||||||
|
new_kategorie = request.POST.get("kategorie", "")
|
||||||
|
if new_kategorie in dict(EmailEingang.KATEGORIE_CHOICES):
|
||||||
|
eingang.kategorie = new_kategorie
|
||||||
|
eingang.save()
|
||||||
|
messages.success(request, f"Kategorie auf '{dict(EmailEingang.KATEGORIE_CHOICES)[new_kategorie]}' gesetzt.")
|
||||||
return redirect("stiftung:email_eingang_detail", pk=pk)
|
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||||||
|
|
||||||
elif action == "mark_verarbeitet":
|
elif action == "mark_verarbeitet":
|
||||||
@@ -669,38 +732,29 @@ def email_eingang_detail(request, pk):
|
|||||||
messages.success(request, "Notizen gespeichert.")
|
messages.success(request, "Notizen gespeichert.")
|
||||||
return redirect("stiftung:email_eingang_detail", pk=pk)
|
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||||||
|
|
||||||
# Paperless-Links zusammenstellen
|
# DMS-Dokumente
|
||||||
paperless_links = eingang.get_paperless_links()
|
dms_dokumente = eingang.dokument_dateien.all().order_by("erstellt_am")
|
||||||
|
|
||||||
# DokumentLinks für diese E-Mail (über paperless_dokument_ids)
|
# Alle aktiven Destinataere fuer manuelle Zuordnung
|
||||||
dokument_links = []
|
|
||||||
if eingang.paperless_dokument_ids:
|
|
||||||
dokument_links = DokumentLink.objects.filter(
|
|
||||||
paperless_document_id__in=eingang.paperless_dokument_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
# Alle aktiven Destinatäre für manuelle Zuordnung
|
|
||||||
alle_destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
alle_destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"title": f"E-Mail-Eingang: {eingang}",
|
"title": f"E-Mail-Eingang: {eingang}",
|
||||||
"eingang": eingang,
|
"eingang": eingang,
|
||||||
"paperless_links": paperless_links,
|
"dms_dokumente": dms_dokumente,
|
||||||
"dokument_links": dokument_links,
|
|
||||||
"alle_destinataere": alle_destinataere,
|
"alle_destinataere": alle_destinataere,
|
||||||
|
"vk_kategorie_choices": Verwaltungskosten.KATEGORIE_CHOICES,
|
||||||
}
|
}
|
||||||
return render(request, "stiftung/email_eingang/detail.html", context)
|
return render(request, "stiftung/email_eingang/detail.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def email_eingang_poll_trigger(request):
|
def email_eingang_poll_trigger(request):
|
||||||
"""Löst den IMAP-Poll manuell aus – sucht alle E-Mails der letzten 30 Tage."""
|
"""Loest den IMAP-Poll manuell aus – sucht alle E-Mails der letzten 30 Tage."""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
from stiftung.tasks import poll_destinataer_emails
|
from stiftung.tasks import poll_emails
|
||||||
try:
|
try:
|
||||||
# Synchron ausführen für sofortiges Feedback; sucht auch bereits
|
result = poll_emails.apply(kwargs={"search_all_recent_days": 30}).get(timeout=300)
|
||||||
# gelesene E-Mails der letzten 30 Tage (Duplikate werden übersprungen).
|
|
||||||
result = poll_destinataer_emails.apply(kwargs={"search_all_recent_days": 30}).get(timeout=300)
|
|
||||||
processed = result.get("processed", 0) if isinstance(result, dict) else 0
|
processed = result.get("processed", 0) if isinstance(result, dict) else 0
|
||||||
if result and result.get("status") == "skipped":
|
if result and result.get("status") == "skipped":
|
||||||
messages.warning(request, "IMAP ist nicht konfiguriert. Bitte Einstellungen unter Administration → E-Mail / IMAP prüfen.")
|
messages.warning(request, "IMAP ist nicht konfiguriert. Bitte Einstellungen unter Administration → E-Mail / IMAP prüfen.")
|
||||||
@@ -723,8 +777,8 @@ def email_eingang_poll_trigger(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def email_eingang_delete(request, pk):
|
def email_eingang_delete(request, pk):
|
||||||
"""Löscht eine eingegangene E-Mail."""
|
"""Loescht eine eingegangene E-Mail."""
|
||||||
eingang = get_object_or_404(DestinataerEmailEingang, pk=pk)
|
eingang = get_object_or_404(EmailEingang, pk=pk)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
betreff = eingang.betreff or "(kein Betreff)"
|
betreff = eingang.betreff or "(kein Betreff)"
|
||||||
eingang.delete()
|
eingang.delete()
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
import qrcode.image.svg
|
import qrcode.image.svg
|
||||||
import requests
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
@@ -35,7 +33,7 @@ from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransact
|
|||||||
BriefVorlage, CSVImport, Destinataer,
|
BriefVorlage, CSVImport, Destinataer,
|
||||||
DestinataerEmailEingang, DestinataerNotiz,
|
DestinataerEmailEingang, DestinataerNotiz,
|
||||||
DestinataerUnterstuetzung,
|
DestinataerUnterstuetzung,
|
||||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
DokumentDatei, DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||||
@@ -143,8 +141,8 @@ def paechter_detail(request, pk):
|
|||||||
paechter = get_object_or_404(Paechter, pk=pk)
|
paechter = get_object_or_404(Paechter, pk=pk)
|
||||||
|
|
||||||
# Alle mit diesem Pächter verknüpften Dokumente laden
|
# Alle mit diesem Pächter verknüpften Dokumente laden
|
||||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
verknuepfte_dokumente = DokumentDatei.objects.filter(
|
||||||
paechter_id=paechter.pk
|
paechter=paechter
|
||||||
).order_by("kontext", "titel")
|
).order_by("kontext", "titel")
|
||||||
|
|
||||||
# Neue LandVerpachtungen für diesen Pächter laden
|
# Neue LandVerpachtungen für diesen Pächter laden
|
||||||
@@ -392,7 +390,7 @@ def land_detail(request, pk):
|
|||||||
land = get_object_or_404(Land, pk=pk)
|
land = get_object_or_404(Land, pk=pk)
|
||||||
|
|
||||||
# Alle mit dieser Länderei verknüpften Dokumente laden
|
# Alle mit dieser Länderei verknüpften Dokumente laden
|
||||||
verknuepfte_dokumente = DokumentLink.objects.filter(land_id=land.pk).order_by(
|
verknuepfte_dokumente = DokumentDatei.objects.filter(land=land).order_by(
|
||||||
"kontext", "titel"
|
"kontext", "titel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -584,8 +582,8 @@ def land_verpachtung_detail(request, pk):
|
|||||||
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
||||||
|
|
||||||
# Alle mit dieser Verpachtung verknüpften Dokumente laden
|
# Alle mit dieser Verpachtung verknüpften Dokumente laden
|
||||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
verknuepfte_dokumente = DokumentDatei.objects.filter(
|
||||||
land_verpachtung_id=verpachtung.pk
|
verpachtung=verpachtung
|
||||||
).order_by("kontext", "titel")
|
).order_by("kontext", "titel")
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
@@ -747,55 +745,26 @@ def paechter_export(request, pk):
|
|||||||
json.dumps(entity_data, indent=2, ensure_ascii=False),
|
json.dumps(entity_data, indent=2, ensure_ascii=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Linked documents from Paperless
|
# 2. DMS-Dokumente (Django-natives DMS)
|
||||||
dokumente = DokumentLink.objects.filter(paechter_id=paechter.pk)
|
dokumente = DokumentDatei.objects.filter(paechter=paechter)
|
||||||
docs_data = []
|
docs_data = []
|
||||||
for doc in dokumente:
|
for doc in dokumente:
|
||||||
doc_data = {
|
doc_data = {
|
||||||
"paperless_id": doc.paperless_document_id,
|
"dms_id": str(doc.id),
|
||||||
"titel": doc.titel,
|
"titel": doc.titel,
|
||||||
"kontext": doc.get_kontext_display(),
|
"kontext": doc.get_kontext_display(),
|
||||||
"beschreibung": doc.beschreibung,
|
"beschreibung": doc.beschreibung,
|
||||||
|
"dateiname": doc.dateiname_original,
|
||||||
}
|
}
|
||||||
docs_data.append(doc_data)
|
docs_data.append(doc_data)
|
||||||
|
|
||||||
# Try to download document from Paperless
|
|
||||||
try:
|
try:
|
||||||
if (
|
if doc.datei:
|
||||||
hasattr(settings, "PAPERLESS_API_URL")
|
safe_filename = doc.dateiname_original or str(doc.id)
|
||||||
and settings.PAPERLESS_API_URL
|
zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read())
|
||||||
):
|
doc_data["included"] = True
|
||||||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
|
||||||
headers = {}
|
|
||||||
if (
|
|
||||||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
|
||||||
and settings.PAPERLESS_API_TOKEN
|
|
||||||
):
|
|
||||||
headers["Authorization"] = (
|
|
||||||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.get(doc_url, headers=headers, timeout=30)
|
|
||||||
if response.status_code == 200:
|
|
||||||
content_type = response.headers.get("content-type", "")
|
|
||||||
if "pdf" in content_type:
|
|
||||||
ext = ".pdf"
|
|
||||||
elif "jpeg" in content_type or "jpg" in content_type:
|
|
||||||
ext = ".jpg"
|
|
||||||
elif "png" in content_type:
|
|
||||||
ext = ".png"
|
|
||||||
else:
|
|
||||||
ext = ".pdf"
|
|
||||||
|
|
||||||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
|
||||||
zipf.writestr(
|
|
||||||
f"dokumente/{safe_filename}", response.content
|
|
||||||
)
|
|
||||||
doc_data["downloaded"] = True
|
|
||||||
else:
|
|
||||||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc_data["download_error"] = str(e)
|
doc_data["error"] = str(e)
|
||||||
|
|
||||||
if docs_data:
|
if docs_data:
|
||||||
zipf.writestr(
|
zipf.writestr(
|
||||||
@@ -871,55 +840,26 @@ def land_export(request, pk):
|
|||||||
"land_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False)
|
"land_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Linked documents from Paperless
|
# 2. DMS-Dokumente (Django-natives DMS)
|
||||||
dokumente = DokumentLink.objects.filter(land_id=land.pk)
|
dokumente = DokumentDatei.objects.filter(land=land)
|
||||||
docs_data = []
|
docs_data = []
|
||||||
for doc in dokumente:
|
for doc in dokumente:
|
||||||
doc_data = {
|
doc_data = {
|
||||||
"paperless_id": doc.paperless_document_id,
|
"dms_id": str(doc.id),
|
||||||
"titel": doc.titel,
|
"titel": doc.titel,
|
||||||
"kontext": doc.get_kontext_display(),
|
"kontext": doc.get_kontext_display(),
|
||||||
"beschreibung": doc.beschreibung,
|
"beschreibung": doc.beschreibung,
|
||||||
|
"dateiname": doc.dateiname_original,
|
||||||
}
|
}
|
||||||
docs_data.append(doc_data)
|
docs_data.append(doc_data)
|
||||||
|
|
||||||
# Try to download document from Paperless
|
|
||||||
try:
|
try:
|
||||||
if (
|
if doc.datei:
|
||||||
hasattr(settings, "PAPERLESS_API_URL")
|
safe_filename = doc.dateiname_original or str(doc.id)
|
||||||
and settings.PAPERLESS_API_URL
|
zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read())
|
||||||
):
|
doc_data["included"] = True
|
||||||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
|
||||||
headers = {}
|
|
||||||
if (
|
|
||||||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
|
||||||
and settings.PAPERLESS_API_TOKEN
|
|
||||||
):
|
|
||||||
headers["Authorization"] = (
|
|
||||||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.get(doc_url, headers=headers, timeout=30)
|
|
||||||
if response.status_code == 200:
|
|
||||||
content_type = response.headers.get("content-type", "")
|
|
||||||
if "pdf" in content_type:
|
|
||||||
ext = ".pdf"
|
|
||||||
elif "jpeg" in content_type or "jpg" in content_type:
|
|
||||||
ext = ".jpg"
|
|
||||||
elif "png" in content_type:
|
|
||||||
ext = ".png"
|
|
||||||
else:
|
|
||||||
ext = ".pdf"
|
|
||||||
|
|
||||||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
|
||||||
zipf.writestr(
|
|
||||||
f"dokumente/{safe_filename}", response.content
|
|
||||||
)
|
|
||||||
doc_data["downloaded"] = True
|
|
||||||
else:
|
|
||||||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc_data["download_error"] = str(e)
|
doc_data["error"] = str(e)
|
||||||
|
|
||||||
if docs_data:
|
if docs_data:
|
||||||
zipf.writestr(
|
zipf.writestr(
|
||||||
@@ -996,55 +936,26 @@ def verpachtung_export(request, pk):
|
|||||||
json.dumps(entity_data, indent=2, ensure_ascii=False),
|
json.dumps(entity_data, indent=2, ensure_ascii=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Linked documents from Paperless
|
# 2. DMS-Dokumente (Django-natives DMS)
|
||||||
dokumente = DokumentLink.objects.filter(verpachtung_id=verpachtung.pk)
|
dokumente = DokumentDatei.objects.filter(verpachtung=verpachtung)
|
||||||
docs_data = []
|
docs_data = []
|
||||||
for doc in dokumente:
|
for doc in dokumente:
|
||||||
doc_data = {
|
doc_data = {
|
||||||
"paperless_id": doc.paperless_document_id,
|
"dms_id": str(doc.id),
|
||||||
"titel": doc.titel,
|
"titel": doc.titel,
|
||||||
"kontext": doc.get_kontext_display(),
|
"kontext": doc.get_kontext_display(),
|
||||||
"beschreibung": doc.beschreibung,
|
"beschreibung": doc.beschreibung,
|
||||||
|
"dateiname": doc.dateiname_original,
|
||||||
}
|
}
|
||||||
docs_data.append(doc_data)
|
docs_data.append(doc_data)
|
||||||
|
|
||||||
# Try to download document from Paperless
|
|
||||||
try:
|
try:
|
||||||
if (
|
if doc.datei:
|
||||||
hasattr(settings, "PAPERLESS_API_URL")
|
safe_filename = doc.dateiname_original or str(doc.id)
|
||||||
and settings.PAPERLESS_API_URL
|
zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read())
|
||||||
):
|
doc_data["included"] = True
|
||||||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
|
||||||
headers = {}
|
|
||||||
if (
|
|
||||||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
|
||||||
and settings.PAPERLESS_API_TOKEN
|
|
||||||
):
|
|
||||||
headers["Authorization"] = (
|
|
||||||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.get(doc_url, headers=headers, timeout=30)
|
|
||||||
if response.status_code == 200:
|
|
||||||
content_type = response.headers.get("content-type", "")
|
|
||||||
if "pdf" in content_type:
|
|
||||||
ext = ".pdf"
|
|
||||||
elif "jpeg" in content_type or "jpg" in content_type:
|
|
||||||
ext = ".jpg"
|
|
||||||
elif "png" in content_type:
|
|
||||||
ext = ".png"
|
|
||||||
else:
|
|
||||||
ext = ".pdf"
|
|
||||||
|
|
||||||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
|
||||||
zipf.writestr(
|
|
||||||
f"dokumente/{safe_filename}", response.content
|
|
||||||
)
|
|
||||||
doc_data["downloaded"] = True
|
|
||||||
else:
|
|
||||||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc_data["download_error"] = str(e)
|
doc_data["error"] = str(e)
|
||||||
|
|
||||||
if docs_data:
|
if docs_data:
|
||||||
zipf.writestr(
|
zipf.writestr(
|
||||||
@@ -1438,8 +1349,8 @@ def verpachtung_detail(request, pk):
|
|||||||
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
||||||
|
|
||||||
# Alle mit dieser Verpachtung verknüpften Dokumente laden
|
# Alle mit dieser Verpachtung verknüpften Dokumente laden
|
||||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
verknuepfte_dokumente = DokumentDatei.objects.filter(
|
||||||
land_verpachtung_id=verpachtung.pk
|
verpachtung=verpachtung
|
||||||
).order_by("kontext", "titel")
|
).order_by("kontext", "titel")
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
|||||||
@@ -657,11 +657,7 @@
|
|||||||
<div class="sidebar-heading">Dokumente</div>
|
<div class="sidebar-heading">Dokumente</div>
|
||||||
<a class="sidebar-link" href="{% url 'stiftung:dms_list' %}">
|
<a class="sidebar-link" href="{% url 'stiftung:dms_list' %}">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<span>DMS</span>
|
<span>Dokumente</span>
|
||||||
</a>
|
|
||||||
<a class="sidebar-link" href="{% url 'stiftung:dokument_management' %}">
|
|
||||||
<i class="fas fa-archive"></i>
|
|
||||||
<span>Paperless (Legacy)</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-outline-secondary w-100">
|
<a href="{% url 'stiftung:dms_list' %}" class="btn btn-outline-secondary w-100">
|
||||||
<i class="fas fa-folder-open d-block mb-2 fa-2x"></i>
|
<i class="fas fa-folder-open d-block mb-2 fa-2x"></i>
|
||||||
<span>Dokumentenverwaltung</span>
|
<span>Dokumentenverwaltung</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -622,8 +622,8 @@
|
|||||||
{# ════════ TAB: Dokumente ════════ #}
|
{# ════════ TAB: Dokumente ════════ #}
|
||||||
<div class="tab-pane fade" id="pane-dokumente" role="tabpanel">
|
<div class="tab-pane fade" id="pane-dokumente" role="tabpanel">
|
||||||
<div class="d-flex justify-content-end mb-3">
|
<div class="d-flex justify-content-end mb-3">
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-success btn-sm">
|
<a href="{% url 'stiftung:dms_upload' %}?destinataer={{ destinataer.pk }}" class="btn btn-success btn-sm">
|
||||||
<i class="fas fa-plus me-1"></i>Dokument verknuepfen
|
<i class="fas fa-upload me-1"></i>Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% if verknuepfte_dokumente %}
|
{% if verknuepfte_dokumente %}
|
||||||
@@ -635,14 +635,17 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for d in verknuepfte_dokumente %}
|
{% for d in verknuepfte_dokumente %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{{ d.titel }}</strong><br><small class="text-muted">ID: {{ d.paperless_document_id }}</small></td>
|
<td>
|
||||||
|
<strong>{{ d.titel }}</strong>
|
||||||
|
{% if d.dateiname_original %}<br><small class="text-muted">{{ d.dateiname_original }} ({{ d.get_human_size }})</small>{% endif %}
|
||||||
|
</td>
|
||||||
<td><span class="badge bg-secondary">{{ d.get_kontext_display }}</span></td>
|
<td><span class="badge bg-secondary">{{ d.get_kontext_display }}</span></td>
|
||||||
<td>{{ d.beschreibung|default:"-"|truncatewords:10 }}</td>
|
<td>{{ d.beschreibung|default:"-"|truncatewords:10 }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<a href="{{ d.get_paperless_url }}" target="_blank" class="btn btn-outline-primary" title="In Paperless oeffnen"><i class="fas fa-external-link-alt"></i></a>
|
<a href="{% url 'stiftung:dms_download' d.pk %}" class="btn btn-outline-primary" title="Herunterladen"><i class="fas fa-download"></i></a>
|
||||||
<a href="{% url 'stiftung:dokument_update' d.pk %}" class="btn btn-outline-warning" title="Bearbeiten"><i class="fas fa-edit"></i></a>
|
<a href="{% url 'stiftung:dms_edit' d.pk %}" class="btn btn-outline-warning" title="Bearbeiten"><i class="fas fa-edit"></i></a>
|
||||||
<a href="{% url 'stiftung:dokument_delete' d.pk %}" class="btn btn-outline-danger" title="Loeschen"><i class="fas fa-unlink"></i></a>
|
<a href="{% url 'stiftung:dms_delete' d.pk %}" class="btn btn-outline-danger" title="Loeschen"><i class="fas fa-trash"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -653,7 +656,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4 text-muted">
|
<div class="text-center py-4 text-muted">
|
||||||
<i class="fas fa-file-alt fa-2x mb-2"></i>
|
<i class="fas fa-file-alt fa-2x mb-2"></i>
|
||||||
<p>Keine Dokumente verknuepft.</p>
|
<p>Keine Dokumente vorhanden.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" id="upload-form">
|
<form method="post" enctype="multipart/form-data" id="upload-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% if initial.foerderung_id %}<input type="hidden" name="foerderung_id" value="{{ initial.foerderung_id }}">{% endif %}
|
||||||
|
{% if initial.verpachtung_id %}<input type="hidden" name="verpachtung_id" value="{{ initial.verpachtung_id }}">{% endif %}
|
||||||
|
|
||||||
<!-- Drag & Drop Zone -->
|
<!-- Drag & Drop Zone -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}Dokument löschen - Stiftungsverwaltung{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6 mx-auto">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header bg-danger text-white">
|
|
||||||
<h4 class="mb-0">
|
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>Dokument löschen
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h5 class="alert-heading">
|
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>Warnung!
|
|
||||||
</h5>
|
|
||||||
<p class="mb-0">
|
|
||||||
Sind Sie sicher, dass Sie das Dokument <strong>{{ dokument.titel }}</strong> löschen möchten?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title">Dokumentdetails:</h6>
|
|
||||||
<p class="card-text">
|
|
||||||
<strong>Titel:</strong> {{ dokument.titel }}<br>
|
|
||||||
<strong>Kontext:</strong> {{ dokument.get_kontext_display }}<br>
|
|
||||||
<strong>Paperless ID:</strong> {{ dokument.paperless_document_id }}<br>
|
|
||||||
{% if dokument.beschreibung %}
|
|
||||||
<strong>Beschreibung:</strong> {{ dokument.beschreibung }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<h6 class="alert-heading">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>Wichtiger Hinweis
|
|
||||||
</h6>
|
|
||||||
<p class="mb-0">
|
|
||||||
Diese Aktion kann nicht rückgängig gemacht werden. Alle zugehörigen Verknüpfungen werden ebenfalls gelöscht.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<a href="{% url 'stiftung:dokument_detail' dokument.pk %}" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Abbrechen
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-danger">
|
|
||||||
<i class="fas fa-trash me-2"></i>Endgültig löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1 class="h3">
|
|
||||||
<i class="fas fa-file-alt text-primary me-2"></i>{{ title }}
|
|
||||||
</h1>
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-warning">
|
|
||||||
<i class="fas fa-edit me-2"></i>Bearbeiten
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_delete' dokument.pk %}" class="btn btn-danger">
|
|
||||||
<i class="fas fa-trash me-2"></i>Löschen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Main Information -->
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<!-- Dokument Details -->
|
|
||||||
<div class="card shadow mb-4">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>Dokumentdetails
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="text-primary">Titel</h6>
|
|
||||||
<p class="mb-3">{{ dokument.titel }}</p>
|
|
||||||
|
|
||||||
<h6 class="text-primary">Kontext</h6>
|
|
||||||
<p class="mb-3">
|
|
||||||
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="text-primary">Paperless ID</h6>
|
|
||||||
<p class="mb-3">
|
|
||||||
<code>{{ dokument.paperless_document_id }}</code>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h6 class="text-primary">Erstellt</h6>
|
|
||||||
<p class="mb-3">{{ dokument.id }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if dokument.beschreibung %}
|
|
||||||
<hr class="my-3">
|
|
||||||
<h6 class="text-primary">Beschreibung</h6>
|
|
||||||
<p class="mb-0">{{ dokument.beschreibung }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Verknüpfungen -->
|
|
||||||
<div class="card shadow mb-4">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-link me-2"></i>Verknüpfungen
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if dokument.foerderung_set.exists or dokument.verpachtung_set.exists %}
|
|
||||||
{% if dokument.foerderung_set.exists %}
|
|
||||||
<h6 class="text-primary">Förderungen</h6>
|
|
||||||
<div class="list-group list-group-flush mb-3">
|
|
||||||
{% for foerderung in dokument.foerderung_set.all %}
|
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<strong>{{ foerderung.person.get_full_name }}</strong> - {{ foerderung.jahr }}
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">€{{ foerderung.betrag|floatformat:2 }}</small>
|
|
||||||
</div>
|
|
||||||
<a href="{% url 'stiftung:foerderung_detail' foerderung.pk %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if dokument.verpachtung_set.exists %}
|
|
||||||
<h6 class="text-primary">Verpachtungen</h6>
|
|
||||||
<div class="list-group list-group-flush">
|
|
||||||
{% for verpachtung in dokument.verpachtung_set.all %}
|
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<strong>{{ verpachtung.vertragsnummer }}</strong> - {{ verpachtung.land.gemeinde }}
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">{{ verpachtung.paechter.get_full_name }}</small>
|
|
||||||
</div>
|
|
||||||
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.pk %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-4">
|
|
||||||
<i class="fas fa-link fa-3x text-muted mb-3"></i>
|
|
||||||
<h5 class="text-muted">Keine Verknüpfungen</h5>
|
|
||||||
<p class="text-muted">Dieses Dokument ist noch nicht mit Förderungen oder Verpachtungen verknüpft.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<!-- Quick Stats -->
|
|
||||||
<div class="card shadow mb-4">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-chart-pie me-2"></i>Übersicht
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row text-center">
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="border-end">
|
|
||||||
<h4 class="text-primary">{{ dokument.foerderung_set.count }}</h4>
|
|
||||||
<small class="text-muted">Förderungen</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<h4 class="text-success">{{ dokument.verpachtung_set.count }}</h4>
|
|
||||||
<small class="text-muted">Verpachtungen</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-bolt me-2"></i>Schnellzugriff
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-grid gap-2">
|
|
||||||
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-warning">
|
|
||||||
<i class="fas fa-edit me-2"></i>Dokument bearbeiten
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_list' %}" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Zurück zur Liste
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ title }} - Stiftung Management{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8 mx-auto">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="mb-0">
|
|
||||||
<i class="fas fa-file-alt text-primary me-2"></i>{{ title }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<!-- Verknüpfungsanzeigen -->
|
|
||||||
{% if form.land_verpachtung_id.value %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
<strong>Verknüpfung:</strong> Dieses Dokument wird mit einer Verpachtung verknüpft.
|
|
||||||
</div>
|
|
||||||
{% elif form.verpachtung_id.value %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
<strong>Verknüpfung:</strong> Dieses Dokument wird mit einer Verpachtung (Legacy) verknüpft.
|
|
||||||
</div>
|
|
||||||
{% elif form.land_id.value %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
<strong>Verknüpfung:</strong> Dieses Dokument wird mit einer Länderei verknüpft.
|
|
||||||
</div>
|
|
||||||
{% elif form.paechter_id.value %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
<strong>Verknüpfung:</strong> Dieses Dokument wird mit einem Pächter verknüpft.
|
|
||||||
</div>
|
|
||||||
{% elif form.destinataer_id.value %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
<strong>Verknüpfung:</strong> Dieses Dokument wird mit einem Destinatär verknüpft.
|
|
||||||
</div>
|
|
||||||
{% elif form.foerderung_id.value %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
<strong>Verknüpfung:</strong> Dieses Dokument wird mit einer Förderung verknüpft.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="{{ form.paperless_document_id.id_for_label }}" class="form-label">
|
|
||||||
{{ form.paperless_document_id.label }} *
|
|
||||||
</label>
|
|
||||||
{{ form.paperless_document_id }}
|
|
||||||
{% if form.paperless_document_id.errors %}
|
|
||||||
<div class="invalid-feedback d-block">
|
|
||||||
{{ form.paperless_document_id.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
Die Dokument-ID aus Paperless (z.B. aus der URL: /documents/12345/)
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="{{ form.kontext.id_for_label }}" class="form-label">
|
|
||||||
{{ form.kontext.label }} *
|
|
||||||
</label>
|
|
||||||
{{ form.kontext }}
|
|
||||||
{% if form.kontext.errors %}
|
|
||||||
<div class="invalid-feedback d-block">
|
|
||||||
{{ form.kontext.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.titel.id_for_label }}" class="form-label">
|
|
||||||
{{ form.titel.label }} *
|
|
||||||
</label>
|
|
||||||
{{ form.titel }}
|
|
||||||
{% if form.titel.errors %}
|
|
||||||
<div class="invalid-feedback d-block">
|
|
||||||
{{ form.titel.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.beschreibung.id_for_label }}" class="form-label">
|
|
||||||
{{ form.beschreibung.label }}
|
|
||||||
</label>
|
|
||||||
{{ form.beschreibung }}
|
|
||||||
{% if form.beschreibung.errors %}
|
|
||||||
<div class="invalid-feedback d-block">
|
|
||||||
{{ form.beschreibung.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Versteckte Verknüpfungsfelder -->
|
|
||||||
{{ form.land_verpachtung_id }}
|
|
||||||
{{ form.verpachtung_id }}
|
|
||||||
{{ form.land_id }}
|
|
||||||
{{ form.paechter_id }}
|
|
||||||
{{ form.destinataer_id }}
|
|
||||||
{{ form.foerderung_id }}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<a href="{% url 'stiftung:dokument_list' %}" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-arrow-left me-1"></i>Zurück zur Liste
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="fas fa-save me-1"></i>
|
|
||||||
{% if dokument %}Aktualisieren{% else %}Verknüpfen{% endif %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.form-control, .form-select {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
.form-control:focus, .form-select:focus {
|
|
||||||
border-color: #86b7fe;
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}Alle Dokumente - Stiftungsverwaltung{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1 class="h3 mb-0">
|
|
||||||
<i class="fas fa-file-alt text-primary me-2"></i>
|
|
||||||
Alle Dokumente
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-info me-2">
|
|
||||||
<i class="fas fa-external-link-alt me-1"></i>Dokumentenverwaltung
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Verknüpfte Dokumente -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-success">
|
|
||||||
<i class="fas fa-link me-2"></i>Verknüpfte Dokumente ({{ dokumente|length }})
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if dokumente %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>Dokument</th>
|
|
||||||
<th>Kontext</th>
|
|
||||||
<th>Verknüpft mit</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for dokument in dokumente %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong>{{ dokument.titel }}</strong>
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">ID: {{ dokument.paperless_document_id }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if dokument.verpachtung_id %}
|
|
||||||
<span class="badge bg-info">Verpachtung</span>
|
|
||||||
{% elif dokument.land_id %}
|
|
||||||
<span class="badge bg-success">Länderei</span>
|
|
||||||
{% elif dokument.paechter_id %}
|
|
||||||
<span class="badge bg-primary">Pächter</span>
|
|
||||||
{% elif dokument.destinataer_id %}
|
|
||||||
<span class="badge bg-warning">Destinatär</span>
|
|
||||||
{% elif dokument.foerderung_id %}
|
|
||||||
<span class="badge bg-secondary">Förderung</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Keine Verknüpfung</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<a href="{{ dokument.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_detail' dokument.pk %}" class="btn btn-sm btn-outline-info" title="Details">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-4">
|
|
||||||
<i class="fas fa-link fa-3x text-muted mb-3"></i>
|
|
||||||
<h5 class="text-muted">Keine Dokumente verknüpft</h5>
|
|
||||||
<p class="text-muted">Verknüpfen Sie Dokumente aus Paperless mit Ihren Entitäten.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Verfügbare Paperless-Dokumente -->
|
|
||||||
{% if available_dokumente %}
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-info">
|
|
||||||
<i class="fas fa-plus-circle me-2"></i>Verfügbare Paperless-Dokumente ({{ available_dokumente|length }})
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
{% for doc in available_dokumente %}
|
|
||||||
<div class="col-md-6 col-lg-4 mb-3">
|
|
||||||
<div class="card h-100 border-info">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title">{{ doc.title }}</h6>
|
|
||||||
<div class="mb-2">
|
|
||||||
{% for tag in doc.tags %}
|
|
||||||
{% if tag == 'Stiftung_Destinatäre' or tag == 'Stiftung_Land_und_Pächter' or tag == 'Stiftung_Administration' %}
|
|
||||||
<span class="badge bg-primary me-1">{{ tag }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-light text-dark me-1">{{ tag }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<a href="{{ doc.document_url }}" target="_blank" class="btn btn-sm btn-outline-info">
|
|
||||||
<i class="fas fa-external-link-alt me-1"></i>In Paperless öffnen
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-sm btn-success">
|
|
||||||
<i class="fas fa-link me-1"></i>Verknüpfen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}Dokumentenverwaltung - Stiftung{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1><i class="fas fa-folder-open me-2"></i>Dokumentenverwaltung</h1>
|
|
||||||
<div>
|
|
||||||
<a href="{% url 'stiftung:dokument_list' %}" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-list me-1"></i>Alle Dokumente anzeigen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="statusMessages"></div>
|
|
||||||
|
|
||||||
<!-- Filter -->
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-filter me-2"></i>Filter
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Kategorie</label>
|
|
||||||
<select id="filterCategory" class="form-select">
|
|
||||||
<option value="all">Alle</option>
|
|
||||||
<option value="destinaere">Destinatäre</option>
|
|
||||||
<option value="land">Ländereien</option>
|
|
||||||
<option value="admin">Administration</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Suche im Titel</label>
|
|
||||||
<input id="filterQuery" class="form-control" placeholder="Titel enthält..." />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 d-flex align-items-end">
|
|
||||||
<button id="refreshDocuments" class="btn btn-primary w-100">
|
|
||||||
<i class="fas fa-sync me-1"></i>Aktualisieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dokumente-Liste -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<div><i class="fas fa-file-alt me-2"></i>Dokumente</div>
|
|
||||||
<small class="text-muted" id="counts"></small>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="documentsContainer">
|
|
||||||
<div class="text-center py-5 text-muted" id="loadingState">
|
|
||||||
<div class="spinner-border" role="status"></div>
|
|
||||||
<p class="mt-2">Lade Dokumente...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Re-Link Modal -->
|
|
||||||
<div class="modal fade" id="relinkModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Dokument neu verknüpfen</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3"><strong id="relinkDocTitle"></strong></div>
|
|
||||||
<div id="currentLinks" class="mb-3"></div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Kategorie</label>
|
|
||||||
<select id="relinkCategory" class="form-select">
|
|
||||||
<option value="destinataer">Destinatäre</option>
|
|
||||||
<option value="land">Ländereien</option>
|
|
||||||
<option value="paechter">Pächter</option>
|
|
||||||
<option value="verpachtung">Verpachtungen</option>
|
|
||||||
<option value="foerderung">Förderungen</option>
|
|
||||||
<option value="abrechnung">Abrechnungen</option>
|
|
||||||
<option value="rentmeister">Rentmeister</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label class="form-label">Suche</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="relinkQuery" class="form-control" placeholder="Name, Ort, E-Mail, Telefon, Adresse..." />
|
|
||||||
<button id="relinkSearch" class="btn btn-outline-secondary"><i class="fas fa-search"></i></button>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Durchsucht Name, Adresse, E-Mail, Telefon und weitere Felder</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3" id="relinkResults" style="max-height: 400px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 0.375rem; padding: 0.5rem;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.search-result-item:hover {
|
|
||||||
background-color: #f8f9fa !important;
|
|
||||||
border-color: #0d6efd !important;
|
|
||||||
}
|
|
||||||
#relinkResults::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
#relinkResults::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
#relinkResults::-webkit-scrollbar-thumb {
|
|
||||||
background: #c1c1c1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
#relinkResults::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a8a8a8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script>
|
|
||||||
let allDocuments = [];
|
|
||||||
let linksByPaperlessId = new Map();
|
|
||||||
let currentRelink = { linkId: null, paperlessId: null };
|
|
||||||
|
|
||||||
function showMessage(message, type) {
|
|
||||||
const statusDiv = document.getElementById('statusMessages');
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
|
||||||
alert.innerHTML = `${message}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
|
|
||||||
statusDiv.appendChild(alert);
|
|
||||||
setTimeout(() => alert.remove(), 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchData() {
|
|
||||||
console.log('Fetching updated document data...'); // Debug log
|
|
||||||
const loadingState = document.getElementById('loadingState');
|
|
||||||
if (loadingState) {
|
|
||||||
loadingState.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Making API calls to:', [
|
|
||||||
'/api/paperless/documents/?poll=1',
|
|
||||||
'/api/link-document/list/'
|
|
||||||
]);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
fetch('/api/paperless/documents/?poll=1').then(r => {
|
|
||||||
console.log('Paperless API response status:', r.status, r.ok);
|
|
||||||
if (!r.ok) {
|
|
||||||
throw new Error(`Paperless API failed: ${r.status} ${r.statusText}`);
|
|
||||||
}
|
|
||||||
return r.json();
|
|
||||||
}),
|
|
||||||
fetch('/api/link-document/list/').then(r => {
|
|
||||||
console.log('Link API response status:', r.status, r.ok);
|
|
||||||
if (!r.ok) {
|
|
||||||
throw new Error(`Link API failed: ${r.status} ${r.statusText}`);
|
|
||||||
}
|
|
||||||
return r.json();
|
|
||||||
})
|
|
||||||
]).then(([docs, linksResp]) => {
|
|
||||||
console.log('Data fetched successfully:', { docs: docs.documents?.length, links: linksResp.links?.length }); // Debug log
|
|
||||||
console.log('Full docs response:', docs); // Debug the full response
|
|
||||||
console.log('Full links response:', linksResp); // Debug the full response
|
|
||||||
allDocuments = docs.documents || [];
|
|
||||||
linksByPaperlessId = new Map();
|
|
||||||
// Handle new grouped links format
|
|
||||||
(linksResp.links || []).forEach(docLinks => {
|
|
||||||
console.log(`Setting linksByPaperlessId for document ${docLinks.paperless_id}:`, docLinks);
|
|
||||||
linksByPaperlessId.set(docLinks.paperless_id, docLinks);
|
|
||||||
});
|
|
||||||
console.log('Final linksByPaperlessId Map:', linksByPaperlessId);
|
|
||||||
renderDocuments();
|
|
||||||
const countsElement = document.getElementById('counts');
|
|
||||||
if (countsElement) {
|
|
||||||
countsElement.textContent = `Gesamt: ${docs.total_all} | Destinatäre: ${docs.total_destinaere} | Land: ${docs.total_land} | Admin: ${docs.total_admin}`;
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Error fetching data:', err);
|
|
||||||
console.error('Error details:', {
|
|
||||||
message: err.message,
|
|
||||||
stack: err.stack,
|
|
||||||
name: err.name
|
|
||||||
});
|
|
||||||
showMessage('Fehler beim Laden der Daten: ' + err.message, 'danger');
|
|
||||||
|
|
||||||
// Show error in the container
|
|
||||||
const container = document.getElementById('documentsContainer');
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<h6><i class="fas fa-exclamation-triangle me-2"></i>Fehler beim Laden der Dokumente</h6>
|
|
||||||
<p class="mb-0">${err.message}</p>
|
|
||||||
<button class="btn btn-primary mt-2" onclick="fetchData()">
|
|
||||||
<i class="fas fa-sync me-1"></i>Erneut versuchen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
const loadingState = document.getElementById('loadingState');
|
|
||||||
if (loadingState) {
|
|
||||||
loadingState.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDocuments() {
|
|
||||||
const container = document.getElementById('documentsContainer');
|
|
||||||
const category = document.getElementById('filterCategory').value;
|
|
||||||
const query = document.getElementById('filterQuery').value.toLowerCase();
|
|
||||||
let filtered = allDocuments.slice();
|
|
||||||
if (category !== 'all') {
|
|
||||||
filtered = filtered.filter(d => d.tag_category === category);
|
|
||||||
}
|
|
||||||
if (query) {
|
|
||||||
filtered = filtered.filter(d => (d.title || '').toLowerCase().includes(query));
|
|
||||||
}
|
|
||||||
if (!filtered.length) {
|
|
||||||
container.innerHTML = '<p class="text-muted">Keine Dokumente gefunden.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<div class="table-responsive"><table class="table table-striped align-middle"><thead><tr><th>Titel</th><th>Kategorie</th><th>Verknüpft mit</th><th>Aktionen</th></tr></thead><tbody>';
|
|
||||||
filtered.forEach(doc => {
|
|
||||||
const linkData = linksByPaperlessId.get(doc.id);
|
|
||||||
let linkedTo = '<span class="text-muted">nicht verknüpft</span>';
|
|
||||||
let hasLinks = false;
|
|
||||||
|
|
||||||
if (linkData && linkData.links && linkData.links.length > 0) {
|
|
||||||
console.log(`Document ${doc.id} (${doc.title}) has ${linkData.links.length} links:`, linkData.links);
|
|
||||||
hasLinks = true;
|
|
||||||
const linkItems = linkData.links.map(link => {
|
|
||||||
const obj = link.linked_object;
|
|
||||||
if (!obj) {
|
|
||||||
console.warn('Link missing linked_object:', link);
|
|
||||||
return `<div class="mb-1"><span class="badge bg-warning me-1">Fehler</span> Fehlerhafter Link</div>`;
|
|
||||||
}
|
|
||||||
// Generate the appropriate detail URL based on link type
|
|
||||||
let detailUrl = '#';
|
|
||||||
if (link.link_type === 'destinataer') {
|
|
||||||
detailUrl = `/destinataere/${obj.id}/`;
|
|
||||||
} else if (link.link_type === 'land') {
|
|
||||||
detailUrl = `/laendereien/${obj.id}/`;
|
|
||||||
} else if (link.link_type === 'paechter') {
|
|
||||||
detailUrl = `/paechter/${obj.id}/`;
|
|
||||||
} else if (link.link_type === 'verpachtung') {
|
|
||||||
detailUrl = `/laendereien/verpachtungen/${obj.id}/`;
|
|
||||||
} else if (link.link_type === 'foerderung') {
|
|
||||||
detailUrl = `/foerderungen/${obj.id}/`;
|
|
||||||
} else if (link.link_type === 'abrechnung') {
|
|
||||||
detailUrl = `/laendereien/abrechnungen/${obj.id}/`;
|
|
||||||
} else if (link.link_type === 'rentmeister') {
|
|
||||||
detailUrl = `/geschaeftsfuehrung/rentmeister/${obj.id}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<div class="mb-1 d-flex align-items-center justify-content-between">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<span class="badge bg-info me-1">${obj?.type || 'Unbekannt'}</span>
|
|
||||||
<a href="${detailUrl}" class="text-decoration-none small text-primary" title="Zu ${obj?.type || 'Entität'} navigieren">
|
|
||||||
${obj?.name || 'Unbekannt'}
|
|
||||||
<i class="fas fa-external-link-alt ms-1" style="font-size: 0.7em;"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-outline-danger ms-2" onclick="onDeleteLink('${link.id}')" title="Diese Verknüpfung löschen">
|
|
||||||
<i class="fas fa-times" style="font-size: 0.7em;"></i>
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
linkedTo = `<div class="small">${linkItems}</div>`;
|
|
||||||
console.log(`Final linkedTo HTML for doc ${doc.id}:`, linkedTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
const openUrl = `/api/paperless/documents/${doc.id}/`;
|
|
||||||
html += `
|
|
||||||
<tr>
|
|
||||||
<td><strong>${doc.title || 'Ohne Titel'}</strong><br><small class="text-muted">Paperless-ID: ${doc.id}</small></td>
|
|
||||||
<td><span class="badge bg-secondary">${doc.tag_category}</span></td>
|
|
||||||
<td>${linkedTo}</td>
|
|
||||||
<td>
|
|
||||||
<a class="btn btn-sm btn-outline-primary" href="${openUrl}" target="_blank" title="In Paperless öffnen"><i class="fas fa-external-link-alt"></i></a>
|
|
||||||
${hasLinks ? `<button class="btn btn-sm btn-outline-danger" onclick="onDeleteAllLinks(${doc.id})" title="Alle Verknüpfungen löschen"><i class="fas fa-trash"></i></button>` : ''}
|
|
||||||
<button class="btn btn-sm btn-outline-success" onclick="onRelink('', ${doc.id}, '${(doc.title||'').replace(/'/g, "'")}')" title="Neu verknüpfen"><i class="fas fa-link"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
|
||||||
});
|
|
||||||
html += '</tbody></table></div>';
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRelink(linkId, paperlessId, title) {
|
|
||||||
currentRelink = { linkId, paperlessId };
|
|
||||||
document.getElementById('relinkDocTitle').textContent = title;
|
|
||||||
|
|
||||||
// Show current links in modal
|
|
||||||
const linkData = linksByPaperlessId.get(paperlessId);
|
|
||||||
const currentLinksDiv = document.getElementById('currentLinks');
|
|
||||||
if (linkData && linkData.links && linkData.links.length > 0) {
|
|
||||||
let currentLinksHtml = '<div class="alert alert-info"><strong>Aktuell verknüpft mit:</strong><ul class="mb-0 mt-2">';
|
|
||||||
linkData.links.forEach(link => {
|
|
||||||
const obj = link.linked_object;
|
|
||||||
currentLinksHtml += `<li><span class="badge bg-secondary me-1">${obj?.type || 'Unbekannt'}</span> ${obj?.name || 'Unbekannt'}</li>`;
|
|
||||||
});
|
|
||||||
currentLinksHtml += '</ul></div>';
|
|
||||||
currentLinksDiv.innerHTML = currentLinksHtml;
|
|
||||||
} else {
|
|
||||||
currentLinksDiv.innerHTML = '<div class="alert alert-warning">Dieses Dokument ist noch nicht verknüpft.</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('relinkResults').innerHTML = '';
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('relinkModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('relinkSearch').addEventListener('click', function() {
|
|
||||||
const category = document.getElementById('relinkCategory').value;
|
|
||||||
const q = document.getElementById('relinkQuery').value || 'all';
|
|
||||||
const target = document.getElementById('relinkResults');
|
|
||||||
target.innerHTML = '<div class="d-flex align-items-center"><div class="spinner-border spinner-border-sm me-2" role="status"></div>Suche...</div>';
|
|
||||||
fetch(`/api/link-document/search/?q=${encodeURIComponent(q)}&category=${category}`)
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
let items = data[category] || [];
|
|
||||||
if (!items.length) {
|
|
||||||
target.innerHTML = '<div class="text-center py-3 text-muted"><i class="fas fa-search me-2"></i>Keine Treffer gefunden.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let html = `<div class="mb-2"><small class="text-muted">${items.length} Treffer gefunden</small></div>`;
|
|
||||||
|
|
||||||
// Get current links for this document to mark already linked entities
|
|
||||||
const linkData = linksByPaperlessId.get(currentRelink.paperlessId);
|
|
||||||
const currentlyLinkedIds = new Set();
|
|
||||||
if (linkData && linkData.links) {
|
|
||||||
linkData.links.forEach(link => {
|
|
||||||
if (link.linked_object && link.linked_object.id) {
|
|
||||||
currentlyLinkedIds.add(link.linked_object.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach(it => {
|
|
||||||
const isLinked = currentlyLinkedIds.has(it.id);
|
|
||||||
const linkClass = isLinked ? 'border-success bg-light' : 'border';
|
|
||||||
const buttonClass = isLinked ? 'btn-success' : 'btn-outline-primary';
|
|
||||||
const buttonIcon = isLinked ? 'fas fa-check-circle' : 'fas fa-plus';
|
|
||||||
const buttonText = isLinked ? 'Bereits verknüpft' : 'Verknüpfen';
|
|
||||||
|
|
||||||
html += `<div class="d-flex justify-content-between align-items-start ${linkClass} rounded p-3 mb-2 search-result-item" style="transition: all 0.2s;">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="fw-bold mb-1">${it.name}</div>
|
|
||||||
<div class="text-muted small mb-1">${it.details || ''}</div>
|
|
||||||
${isLinked ? '<span class="badge bg-success mt-1"><i class="fas fa-check-circle me-1"></i>Aktuell verknüpft</span>' : ''}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm ${buttonClass} ms-3" onclick="confirmRelink('${it.id}', '${category}')" title="${buttonText}" ${isLinked ? 'disabled' : ''}>
|
|
||||||
<i class="${buttonIcon} me-1"></i>${isLinked ? 'Verknüpft' : 'Auswählen'}
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
target.innerHTML = html;
|
|
||||||
})
|
|
||||||
.catch(() => { target.innerHTML = '<div class="text-center py-3 text-danger"><i class="fas fa-exclamation-triangle me-2"></i>Fehler bei der Suche</div>'; });
|
|
||||||
});
|
|
||||||
|
|
||||||
function onDeleteAllLinks(paperlessId) {
|
|
||||||
const linkData = linksByPaperlessId.get(paperlessId);
|
|
||||||
if (!linkData || !linkData.links || linkData.links.length === 0) {
|
|
||||||
showMessage('Keine Verknüpfungen zum Löschen gefunden', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm(`Möchten Sie wirklich alle ${linkData.links.length} Verknüpfung(en) für dieses Dokument löschen?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Deleting all ${linkData.links.length} links for document ${paperlessId}:`, linkData.links);
|
|
||||||
|
|
||||||
// Delete all links for this document with proper CSRF token
|
|
||||||
const deletePromises = linkData.links.map(link => {
|
|
||||||
console.log(`Deleting link ${link.id}`);
|
|
||||||
return fetch(`/api/link-document/delete/${link.id}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(deletePromises).then(responses => {
|
|
||||||
console.log('All delete responses:', responses.map(r => ({ status: r.status, ok: r.ok })));
|
|
||||||
const allSuccessful = responses.every(r => r.ok);
|
|
||||||
const failedCount = responses.filter(r => !r.ok).length;
|
|
||||||
|
|
||||||
if (allSuccessful) {
|
|
||||||
showMessage('Alle Verknüpfungen erfolgreich gelöscht', 'success');
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchData();
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
console.error(`${failedCount} of ${responses.length} delete requests failed`);
|
|
||||||
showMessage(`Fehler beim Löschen von ${failedCount} Verknüpfung(en)`, 'danger');
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Error during bulk delete:', err);
|
|
||||||
showMessage('Fehler beim Löschen der Verknüpfungen', 'danger');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Enter key support for search
|
|
||||||
document.getElementById('relinkQuery').addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
document.getElementById('relinkSearch').click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function confirmRelink(targetId, category) {
|
|
||||||
console.log('confirmRelink called:', { targetId, category, currentRelink });
|
|
||||||
|
|
||||||
const isUpdate = !!currentRelink.linkId;
|
|
||||||
const url = isUpdate ? '/api/link-document/update/' : '/api/link-document/create/';
|
|
||||||
const payload = isUpdate ? {
|
|
||||||
link_id: currentRelink.linkId,
|
|
||||||
link_type: category,
|
|
||||||
link_id_target: targetId
|
|
||||||
} : {
|
|
||||||
paperless_id: currentRelink.paperlessId,
|
|
||||||
paperless_title: document.getElementById('relinkDocTitle').textContent,
|
|
||||||
paperless_url: `/api/paperless/documents/${currentRelink.paperlessId}/`,
|
|
||||||
link_type: category,
|
|
||||||
link_id: targetId
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Sending request:', { url, payload });
|
|
||||||
|
|
||||||
fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
}).then(async r => {
|
|
||||||
console.log('Response status:', r.status, r.ok);
|
|
||||||
|
|
||||||
let resp = {};
|
|
||||||
try {
|
|
||||||
resp = await r.json();
|
|
||||||
console.log('Response data:', resp);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('No JSON response, treating as success if status OK');
|
|
||||||
if (r.ok) {
|
|
||||||
resp = { success: true };
|
|
||||||
} else {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r.ok && (resp.success || resp.message)) {
|
|
||||||
console.log('Success! Showing message and refreshing...');
|
|
||||||
showMessage(resp.message || 'Verknüpfung gespeichert', 'success');
|
|
||||||
|
|
||||||
// Close modal first, then refresh data
|
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('relinkModal'));
|
|
||||||
if (modal) {
|
|
||||||
console.log('Closing modal...');
|
|
||||||
modal.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear search results to prevent confusion
|
|
||||||
document.getElementById('relinkResults').innerHTML = '';
|
|
||||||
document.getElementById('relinkQuery').value = '';
|
|
||||||
|
|
||||||
// Try immediate refresh first
|
|
||||||
console.log('Calling fetchData() immediately...');
|
|
||||||
fetchData();
|
|
||||||
|
|
||||||
// Also schedule a backup refresh to be sure
|
|
||||||
console.log('Scheduling backup refresh in 500ms...');
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Backup fetchData() call...');
|
|
||||||
fetchData();
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
console.log('Error response:', resp);
|
|
||||||
showMessage(resp.error || 'Fehler beim Speichern', 'danger');
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Relink error:', err);
|
|
||||||
showMessage('Fehler beim Speichern', 'danger');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteLink(linkId) {
|
|
||||||
if (!confirm('Diese Verknüpfung wirklich löschen?')) return;
|
|
||||||
|
|
||||||
console.log('Deleting individual link:', linkId);
|
|
||||||
|
|
||||||
fetch(`/api/link-document/delete/${linkId}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
|
||||||
})
|
|
||||||
.then(async r => {
|
|
||||||
console.log('Delete response status:', r.status, r.ok);
|
|
||||||
let data = {};
|
|
||||||
try {
|
|
||||||
data = await r.json();
|
|
||||||
console.log('Delete response data:', data);
|
|
||||||
} catch (_) {
|
|
||||||
console.log('No JSON response, treating as success if status OK');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r.ok && (data.success === undefined || data.success === true)) {
|
|
||||||
showMessage('Verknüpfung gelöscht', 'success');
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchData();
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
showMessage((data && data.error) || 'Fehler beim Löschen', 'danger');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Delete error:', err);
|
|
||||||
showMessage('Fehler beim Löschen', 'danger');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
let cookieValue = null;
|
|
||||||
if (document.cookie && document.cookie !== '') {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i].trim();
|
|
||||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('refreshDocuments').addEventListener('click', fetchData);
|
|
||||||
document.getElementById('filterCategory').addEventListener('change', renderDocuments);
|
|
||||||
document.getElementById('filterQuery').addEventListener('input', () => { renderDocuments(); });
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
console.log('Dokumentenverwaltung page loaded, calling fetchData()...');
|
|
||||||
fetchData();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block title %}E-Mail-Eingang Detail - van Hees-Theyssen-Vogel'sche Stiftung{% endblock %}
|
{% block title %}E-Mail-Eingang Detail - Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -11,22 +11,31 @@
|
|||||||
<i class="fas fa-envelope me-2"></i>E-Mail-Eingang
|
<i class="fas fa-envelope me-2"></i>E-Mail-Eingang
|
||||||
</h1>
|
</h1>
|
||||||
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary btn-sm">
|
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="fas fa-arrow-left me-1"></i>Zurück zur Übersicht
|
<i class="fas fa-arrow-left me-1"></i>Zurueck zur Uebersicht
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Linke Spalte: E-Mail-Details -->
|
{# Linke Spalte: E-Mail-Details #}
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="fas fa-envelope-open me-2"></i>E-Mail-Details</span>
|
<span><i class="fas fa-envelope-open me-2"></i>E-Mail-Details</span>
|
||||||
<span>
|
<span>
|
||||||
|
{# Kategorie-Badge #}
|
||||||
|
{% if eingang.kategorie == "rechnung" %}<span class="badge bg-warning text-dark me-1"><i class="fas fa-file-invoice me-1"></i>Rechnung</span>
|
||||||
|
{% elif eingang.kategorie == "destinataer" %}<span class="badge bg-info me-1"><i class="fas fa-user me-1"></i>Destinataer</span>
|
||||||
|
{% elif eingang.kategorie == "land_pacht" %}<span class="badge bg-success me-1"><i class="fas fa-map me-1"></i>Land/Pacht</span>
|
||||||
|
{% elif eingang.kategorie == "stiftungsgeschichte" %}<span class="badge bg-dark me-1"><i class="fas fa-landmark me-1"></i>Geschichte</span>
|
||||||
|
{% endif %}
|
||||||
|
{# Status-Badge #}
|
||||||
{% if eingang.status == "neu" %}<span class="badge bg-warning text-dark">Neu</span>
|
{% if eingang.status == "neu" %}<span class="badge bg-warning text-dark">Neu</span>
|
||||||
{% elif eingang.status == "zugewiesen" %}<span class="badge bg-primary">Zugewiesen</span>
|
{% elif eingang.status == "zugewiesen" %}<span class="badge bg-primary">Zugewiesen</span>
|
||||||
{% elif eingang.status == "verarbeitet" %}<span class="badge bg-success">Verarbeitet</span>
|
{% elif eingang.status == "verarbeitet" %}<span class="badge bg-success">Verarbeitet</span>
|
||||||
|
{% elif eingang.status == "rechnung_erfasst" %}<span class="badge bg-info">Rechnung erfasst</span>
|
||||||
|
{% elif eingang.status == "zahlung_gebucht" %}<span class="badge bg-success">Zahlung gebucht</span>
|
||||||
{% elif eingang.status == "unbekannt" %}<span class="badge bg-danger">Unbekannter Absender</span>
|
{% elif eingang.status == "unbekannt" %}<span class="badge bg-danger">Unbekannter Absender</span>
|
||||||
{% elif eingang.status == "fehler" %}<span class="badge bg-secondary">Fehler</span>
|
{% elif eingang.status == "fehler" %}<span class="badge bg-secondary">Fehler</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -47,17 +56,27 @@
|
|||||||
<dt class="col-sm-3">Betreff</dt>
|
<dt class="col-sm-3">Betreff</dt>
|
||||||
<dd class="col-sm-9">{{ eingang.betreff|default:"(kein Betreff)" }}</dd>
|
<dd class="col-sm-9">{{ eingang.betreff|default:"(kein Betreff)" }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Destinatär</dt>
|
<dt class="col-sm-3">Destinataer</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
{% if eingang.destinataer %}
|
{% if eingang.destinataer %}
|
||||||
<a href="{% url 'stiftung:destinataer_detail' eingang.destinataer.pk %}">
|
<a href="{% url 'stiftung:destinataer_detail' eingang.destinataer.pk %}">
|
||||||
{{ eingang.destinataer }}
|
{{ eingang.destinataer }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>Nicht zugeordnet</span>
|
<span class="text-muted">Nicht zugeordnet</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
{% if eingang.verwaltungskosten %}
|
||||||
|
<dt class="col-sm-3">Rechnung</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<a href="{% url 'stiftung:verwaltungskosten_edit' eingang.verwaltungskosten.pk %}">
|
||||||
|
{{ eingang.verwaltungskosten.bezeichnung }} ({{ eingang.verwaltungskosten.betrag }} EUR)
|
||||||
|
</a>
|
||||||
|
<span class="badge bg-{{ eingang.verwaltungskosten.get_status_color }}">{{ eingang.verwaltungskosten.get_status_display }}</span>
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if eingang.quartalsnachweis %}
|
{% if eingang.quartalsnachweis %}
|
||||||
<dt class="col-sm-3">Quartalsnachweis</dt>
|
<dt class="col-sm-3">Quartalsnachweis</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
@@ -82,32 +101,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Anhänge / Paperless-Dokumente -->
|
{# Anhaenge / DMS-Dokumente #}
|
||||||
{% if dokument_links %}
|
{% if dms_dokumente %}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="fas fa-paperclip me-2"></i>Anhänge in Paperless-NGX
|
<i class="fas fa-paperclip me-2"></i>Anhaenge ({{ dms_dokumente|length }})
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table mb-0">
|
<table class="table mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Titel</th>
|
<th>Dateiname</th>
|
||||||
<th>Kontext</th>
|
<th>Typ</th>
|
||||||
<th>Paperless-ID</th>
|
<th>Groesse</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for link in dokument_links %}
|
{% for dok in dms_dokumente %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ link.titel }}</td>
|
<td>{{ dok.dateiname_original|default:dok.titel }}</td>
|
||||||
<td>{{ link.get_kontext_display }}</td>
|
<td><span class="text-muted small">{{ dok.dateityp|default:"–" }}</span></td>
|
||||||
<td><code>{{ link.paperless_document_id }}</code></td>
|
<td><span class="text-muted small">{{ dok.get_human_size }}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ link.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-info">
|
{% if dok.datei %}
|
||||||
<i class="fas fa-external-link-alt me-1"></i>Öffnen
|
<a href="{% url 'stiftung:dms_download' dok.pk %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-download me-1"></i>Herunterladen
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -115,43 +136,109 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif eingang.paperless_dokument_ids %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle me-1"></i>
|
|
||||||
{{ eingang.paperless_dokument_ids|length }} Anhang/-hänge in Paperless hochgeladen
|
|
||||||
(IDs: {{ eingang.paperless_dokument_ids|join:", " }}), aber noch kein DokumentLink erstellt.
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body text-muted text-center py-3">
|
<div class="card-body text-muted text-center py-3">
|
||||||
<i class="fas fa-paperclip me-1"></i>Keine Anhänge in dieser E-Mail.
|
<i class="fas fa-paperclip me-1"></i>Keine Anhaenge in dieser E-Mail.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rechte Spalte: Aktionen -->
|
{# Rechte Spalte: Aktionen #}
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
|
|
||||||
<!-- Manuelle Destinatär-Zuordnung -->
|
{# Kategorie aendern #}
|
||||||
{% if not eingang.destinataer or eingang.status == "unbekannt" %}
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="fas fa-tag me-2"></i>Kategorie
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="set_kategorie">
|
||||||
|
<div class="mb-2">
|
||||||
|
<select class="form-select form-select-sm" name="kategorie">
|
||||||
|
<option value="allgemein" {% if eingang.kategorie == "allgemein" %}selected{% endif %}>Allgemein</option>
|
||||||
|
<option value="destinataer" {% if eingang.kategorie == "destinataer" %}selected{% endif %}>Destinataer</option>
|
||||||
|
<option value="rechnung" {% if eingang.kategorie == "rechnung" %}selected{% endif %}>Rechnung</option>
|
||||||
|
<option value="land_pacht" {% if eingang.kategorie == "land_pacht" %}selected{% endif %}>Grundstueck / Pacht</option>
|
||||||
|
<option value="stiftungsgeschichte" {% if eingang.kategorie == "stiftungsgeschichte" %}selected{% endif %}>Stiftungsgeschichte</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-outline-primary btn-sm w-100">
|
||||||
|
<i class="fas fa-save me-1"></i>Kategorie setzen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Rechnung erfassen (nur wenn noch keine zugeordnet) #}
|
||||||
|
{% if not eingang.verwaltungskosten and eingang.status != "zahlung_gebucht" %}
|
||||||
<div class="card mb-4 border-warning">
|
<div class="card mb-4 border-warning">
|
||||||
<div class="card-header bg-warning text-dark">
|
<div class="card-header bg-warning text-dark">
|
||||||
<i class="fas fa-user-plus me-2"></i>Destinatär manuell zuordnen
|
<i class="fas fa-file-invoice-dollar me-2"></i>Als Rechnung erfassen
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="small text-muted">
|
<p class="small text-muted">
|
||||||
Die E-Mail-Adresse <strong>{{ eingang.absender_email }}</strong>
|
Erstellt einen Verwaltungskosten-Eintrag und verknuepft die Anhaenge als Rechnungsdokumente.
|
||||||
konnte keinem Destinatär automatisch zugeordnet werden.
|
</p>
|
||||||
Bitte wählen Sie den passenden Destinatär aus.
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="erfasse_rechnung">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Bezeichnung</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="bezeichnung"
|
||||||
|
value="{{ eingang.betreff }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Betrag (EUR)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control form-control-sm" name="betrag"
|
||||||
|
placeholder="0.00" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Lieferant / Firma</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="lieferant"
|
||||||
|
value="{{ eingang.absender_name|default:eingang.absender_email }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Rechnungsnummer</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="rechnungsnummer"
|
||||||
|
placeholder="z.B. RE-2026001">
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label small">Kategorie</label>
|
||||||
|
<select class="form-select form-select-sm" name="vk_kategorie">
|
||||||
|
{% for key, label in vk_kategorie_choices %}
|
||||||
|
<option value="{{ key }}" {% if key == "rechnung_intern" %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-warning w-100">
|
||||||
|
<i class="fas fa-file-invoice me-1"></i>Rechnung erfassen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Manuelle Destinataer-Zuordnung #}
|
||||||
|
{% if not eingang.destinataer or eingang.status == "unbekannt" %}
|
||||||
|
<div class="card mb-4 border-info">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Destinataer zuordnen
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small text-muted">
|
||||||
|
Absender <strong>{{ eingang.absender_email }}</strong>
|
||||||
|
konnte nicht automatisch zugeordnet werden.
|
||||||
</p>
|
</p>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="assign_destinataer">
|
<input type="hidden" name="action" value="assign_destinataer">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Destinatär</label>
|
<select class="form-select form-select-sm" name="destinataer_id" required>
|
||||||
<select class="form-select" name="destinataer_id" required>
|
<option value="">– Bitte waehlen –</option>
|
||||||
<option value="">– Bitte wählen –</option>
|
|
||||||
{% for d in alle_destinataere %}
|
{% for d in alle_destinataere %}
|
||||||
<option value="{{ d.pk }}">{{ d.nachname }}, {{ d.vorname }}
|
<option value="{{ d.pk }}">{{ d.nachname }}, {{ d.vorname }}
|
||||||
{% if d.email %} ({{ d.email }}){% endif %}
|
{% if d.email %} ({{ d.email }}){% endif %}
|
||||||
@@ -159,16 +246,16 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-warning w-100">
|
<button type="submit" class="btn btn-info w-100">
|
||||||
<i class="fas fa-link me-1"></i>Zuordnen & Speichern
|
<i class="fas fa-link me-1"></i>Zuordnen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Als verarbeitet markieren -->
|
{# Als verarbeitet markieren #}
|
||||||
{% if eingang.status != "verarbeitet" %}
|
{% if eingang.status != "verarbeitet" and eingang.status != "zahlung_gebucht" %}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="fas fa-check-circle me-2"></i>Als verarbeitet markieren
|
<i class="fas fa-check-circle me-2"></i>Als verarbeitet markieren
|
||||||
@@ -178,9 +265,8 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="mark_verarbeitet">
|
<input type="hidden" name="action" value="mark_verarbeitet">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Interne Notiz (optional)</label>
|
<textarea class="form-control form-control-sm" name="notizen" rows="3"
|
||||||
<textarea class="form-control" name="notizen" rows="3"
|
placeholder="Optionale Notiz...">{{ eingang.notizen }}</textarea>
|
||||||
placeholder="Z. B. 'Studiennachweis für WS 2025/26 eingegangen und geprüft.'">{{ eingang.notizen }}</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success w-100">
|
<button type="submit" class="btn btn-success w-100">
|
||||||
<i class="fas fa-check me-1"></i>Verarbeitet
|
<i class="fas fa-check me-1"></i>Verarbeitet
|
||||||
@@ -190,7 +276,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Notizen bearbeiten -->
|
{# Notizen #}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="fas fa-sticky-note me-2"></i>Interne Notizen
|
<i class="fas fa-sticky-note me-2"></i>Interne Notizen
|
||||||
@@ -200,41 +286,42 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="save_notizen">
|
<input type="hidden" name="action" value="save_notizen">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<textarea class="form-control" name="notizen" rows="5"
|
<textarea class="form-control form-control-sm" name="notizen" rows="4"
|
||||||
placeholder="Interne Notizen zur E-Mail...">{{ eingang.notizen }}</textarea>
|
placeholder="Interne Notizen...">{{ eingang.notizen }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-outline-secondary w-100">
|
<button type="submit" class="btn btn-outline-secondary btn-sm w-100">
|
||||||
<i class="fas fa-save me-1"></i>Notizen speichern
|
<i class="fas fa-save me-1"></i>Notizen speichern
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metadaten -->
|
{# Metadaten #}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header"><i class="fas fa-info-circle me-2"></i>Metadaten</div>
|
<div class="card-header"><i class="fas fa-info-circle me-2"></i>Metadaten</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row mb-0 small">
|
<dl class="row mb-0 small">
|
||||||
<dt class="col-6">Erfasst am</dt>
|
<dt class="col-6">Erfasst am</dt>
|
||||||
<dd class="col-6">{{ eingang.created_at|date:"d.m.Y H:i" }}</dd>
|
<dd class="col-6">{{ eingang.created_at|date:"d.m.Y H:i" }}</dd>
|
||||||
|
<dt class="col-6">Kategorie</dt>
|
||||||
|
<dd class="col-6">{{ eingang.get_kategorie_display }}</dd>
|
||||||
<dt class="col-6">Datensatz-ID</dt>
|
<dt class="col-6">Datensatz-ID</dt>
|
||||||
<dd class="col-6 text-muted"><code>{{ eingang.pk|stringformat:"s"|slice:":8" }}…</code></dd>
|
<dd class="col-6 text-muted"><code>{{ eingang.pk|stringformat:"s"|slice:":8" }}...</code></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Löschen -->
|
{# Loeschen #}
|
||||||
<div class="card border-danger">
|
<div class="card border-danger">
|
||||||
<div class="card-header text-danger">
|
<div class="card-header text-danger">
|
||||||
<i class="fas fa-trash-alt me-2"></i>E-Mail löschen
|
<i class="fas fa-trash-alt me-2"></i>E-Mail loeschen
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="small text-muted mb-2">Diese E-Mail unwiderruflich aus dem System entfernen.</p>
|
|
||||||
<form method="post" action="{% url 'stiftung:email_eingang_delete' eingang.pk %}"
|
<form method="post" action="{% url 'stiftung:email_eingang_delete' eingang.pk %}"
|
||||||
onsubmit="return confirm('E-Mail wirklich löschen?');">
|
onsubmit="return confirm('E-Mail wirklich loeschen?');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-outline-danger w-100">
|
<button type="submit" class="btn btn-outline-danger btn-sm w-100">
|
||||||
<i class="fas fa-trash-alt me-1"></i>Löschen
|
<i class="fas fa-trash-alt me-1"></i>Loeschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block title %}E-Mail-Eingang (Destinatäre) - van Hees-Theyssen-Vogel'sche Stiftung{% endblock %}
|
{% block title %}E-Mail-Eingang - Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
<h1 class="h3 mb-0 text-gray-800">
|
<h1 class="h3 mb-0 text-gray-800">
|
||||||
<i class="fas fa-envelope-open-text me-2"></i>E-Mail-Eingang (Destinatäre)
|
<i class="fas fa-envelope-open-text me-2"></i>E-Mail-Eingang
|
||||||
</h1>
|
</h1>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<form method="post" action="{% url 'stiftung:email_eingang_poll_trigger' %}" class="d-inline">
|
<form method="post" action="{% url 'stiftung:email_eingang_poll_trigger' %}" class="d-inline">
|
||||||
@@ -17,79 +17,65 @@
|
|||||||
<i class="fas fa-sync-alt me-1"></i>Jetzt abrufen
|
<i class="fas fa-sync-alt me-1"></i>Jetzt abrufen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="{% url 'stiftung:destinataer_list' %}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="fas fa-arrow-left me-1"></i>Destinatäre
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statuskarten -->
|
{# Statuskarten #}
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3 col-6 mb-2">
|
||||||
<div class="card border-left-primary h-100 py-2">
|
<div class="card border-left-primary h-100 py-2">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row no-gutters align-items-center">
|
|
||||||
<div class="col mr-2">
|
|
||||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Gesamt</div>
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Gesamt</div>
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.gesamt }}</div>
|
<div class="h5 mb-0 font-weight-bold">{{ counts.gesamt }}</div>
|
||||||
</div>
|
|
||||||
<div class="col-auto"><i class="fas fa-envelope fa-2x text-gray-300"></i></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-3 col-6 mb-2">
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card border-left-warning h-100 py-2">
|
<div class="card border-left-warning h-100 py-2">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row no-gutters align-items-center">
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Neu</div>
|
||||||
<div class="col mr-2">
|
<div class="h5 mb-0 font-weight-bold">{{ counts.neu }}</div>
|
||||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Neu / Unbearbeitet</div>
|
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.neu }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto"><i class="fas fa-exclamation-circle fa-2x text-gray-300"></i></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3 col-6 mb-2">
|
||||||
|
<div class="card border-left-info h-100 py-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Rechnungen</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold">{{ counts.rechnung }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-6 mb-2">
|
||||||
<div class="card border-left-danger h-100 py-2">
|
<div class="card border-left-danger h-100 py-2">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row no-gutters align-items-center">
|
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Unbekannt</div>
|
||||||
<div class="col mr-2">
|
<div class="h5 mb-0 font-weight-bold">{{ counts.unbekannt }}</div>
|
||||||
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Unbekannter Absender</div>
|
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.unbekannt }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto"><i class="fas fa-user-times fa-2x text-gray-300"></i></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card border-left-secondary h-100 py-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row no-gutters align-items-center">
|
|
||||||
<div class="col mr-2">
|
|
||||||
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">Fehler</div>
|
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.fehler }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto"><i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter -->
|
{# Filter #}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header"><i class="fas fa-filter me-2"></i>Filter</div>
|
<div class="card-header"><i class="fas fa-filter me-2"></i>Filter</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Suche</label>
|
<label class="form-label">Suche</label>
|
||||||
<input type="text" class="form-control" name="q" value="{{ search }}"
|
<input type="text" class="form-control" name="q" value="{{ search }}"
|
||||||
placeholder="Absender, Betreff, Destinatär...">
|
placeholder="Absender, Betreff...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Kategorie</label>
|
||||||
|
<select class="form-select" name="kategorie">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
{% for value, label in kategorie_choices %}
|
||||||
|
<option value="{{ value }}" {% if kategorie_filter == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Status</label>
|
<label class="form-label">Status</label>
|
||||||
@@ -100,15 +86,15 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 d-flex align-items-end">
|
<div class="col-md-1 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<i class="fas fa-search me-1"></i>Filtern
|
<i class="fas fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% if search or status_filter %}
|
{% if search or status_filter or kategorie_filter %}
|
||||||
<div class="col-md-2 d-flex align-items-end">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary w-100">
|
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary w-100">
|
||||||
<i class="fas fa-times me-1"></i>Zurücksetzen
|
<i class="fas fa-times me-1"></i>Reset
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -116,11 +102,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabelle -->
|
{# Tabelle #}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="fas fa-inbox me-2"></i>Eingegangene E-Mails</span>
|
<span><i class="fas fa-inbox me-2"></i>Eingegangene E-Mails</span>
|
||||||
<span class="text-muted small">{{ page_obj.paginator.count }} Einträge</span>
|
<span class="text-muted small">{{ page_obj.paginator.count }} Eintraege</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
@@ -130,9 +116,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Absender</th>
|
<th>Absender</th>
|
||||||
<th>Destinatär</th>
|
|
||||||
<th>Betreff</th>
|
<th>Betreff</th>
|
||||||
<th>Anhänge</th>
|
<th>Kategorie</th>
|
||||||
|
<th>Zuordnung</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -149,19 +135,27 @@
|
|||||||
<small class="text-muted">{{ e.absender_email }}</small>
|
<small class="text-muted">{{ e.absender_email }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ e.betreff|truncatechars:50 }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if e.destinataer %}
|
{% if e.kategorie == "rechnung" %}
|
||||||
<a href="{% url 'stiftung:destinataer_detail' e.destinataer.pk %}">
|
<span class="badge bg-warning text-dark"><i class="fas fa-file-invoice me-1"></i>Rechnung</span>
|
||||||
{{ e.destinataer }}
|
{% elif e.kategorie == "destinataer" %}
|
||||||
</a>
|
<span class="badge bg-info"><i class="fas fa-user me-1"></i>Destinataer</span>
|
||||||
|
{% elif e.kategorie == "land_pacht" %}
|
||||||
|
<span class="badge bg-success"><i class="fas fa-map me-1"></i>Land/Pacht</span>
|
||||||
|
{% elif e.kategorie == "stiftungsgeschichte" %}
|
||||||
|
<span class="badge bg-dark"><i class="fas fa-landmark me-1"></i>Geschichte</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-danger"><i class="fas fa-question-circle me-1"></i>Unbekannt</span>
|
<span class="badge bg-secondary">Allgemein</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ e.betreff|truncatechars:60 }}</td>
|
<td>
|
||||||
<td class="text-center">
|
{% if e.destinataer %}
|
||||||
{% if e.paperless_dokument_ids %}
|
<a href="{% url 'stiftung:destinataer_detail' e.destinataer.pk %}" class="text-decoration-none">
|
||||||
<span class="badge bg-info">{{ e.paperless_dokument_ids|length }}</span>
|
{{ e.destinataer }}
|
||||||
|
</a>
|
||||||
|
{% elif e.verwaltungskosten %}
|
||||||
|
<span class="text-info"><i class="fas fa-file-invoice-dollar me-1"></i>{{ e.verwaltungskosten.bezeichnung|truncatechars:30 }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">–</span>
|
<span class="text-muted">–</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -173,6 +167,10 @@
|
|||||||
<span class="badge bg-primary">Zugewiesen</span>
|
<span class="badge bg-primary">Zugewiesen</span>
|
||||||
{% elif e.status == "verarbeitet" %}
|
{% elif e.status == "verarbeitet" %}
|
||||||
<span class="badge bg-success">Verarbeitet</span>
|
<span class="badge bg-success">Verarbeitet</span>
|
||||||
|
{% elif e.status == "rechnung_erfasst" %}
|
||||||
|
<span class="badge bg-info">Rechnung erfasst</span>
|
||||||
|
{% elif e.status == "zahlung_gebucht" %}
|
||||||
|
<span class="badge bg-success">Bezahlt</span>
|
||||||
{% elif e.status == "unbekannt" %}
|
{% elif e.status == "unbekannt" %}
|
||||||
<span class="badge bg-danger">Unbekannt</span>
|
<span class="badge bg-danger">Unbekannt</span>
|
||||||
{% elif e.status == "fehler" %}
|
{% elif e.status == "fehler" %}
|
||||||
@@ -180,18 +178,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<a href="{% url 'stiftung:email_eingang_detail' e.pk %}" class="btn btn-sm btn-outline-primary" title="Details">
|
||||||
<a href="{% url 'stiftung:email_eingang_detail' e.pk %}" class="btn btn-outline-primary" title="Details">
|
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<form method="post" action="{% url 'stiftung:email_eingang_delete' e.pk %}" class="d-inline"
|
|
||||||
onsubmit="return confirm('E-Mail wirklich löschen?');">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" class="btn btn-outline-danger" title="Löschen">
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -199,14 +188,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
{# Pagination #}
|
||||||
{% if page_obj.has_other_pages %}
|
{% if page_obj.has_other_pages %}
|
||||||
<div class="d-flex justify-content-center py-3">
|
<div class="d-flex justify-content-center py-3">
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination mb-0">
|
<ul class="pagination mb-0">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&q={{ search }}&status={{ status_filter }}">
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&q={{ search }}&status={{ status_filter }}&kategorie={{ kategorie_filter }}">
|
||||||
«
|
«
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -216,7 +205,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}&q={{ search }}&status={{ status_filter }}">
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}&q={{ search }}&status={{ status_filter }}&kategorie={{ kategorie_filter }}">
|
||||||
»
|
»
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -230,7 +219,7 @@
|
|||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||||
<p>Keine E-Mails gefunden.</p>
|
<p>Keine E-Mails gefunden.</p>
|
||||||
<small>Der automatische Abruf erfolgt alle 15 Minuten. Über den Button "Jetzt abrufen" kann der Vorgang manuell ausgelöst werden.</small>
|
<small>Der automatische Abruf erfolgt alle 15 Minuten.</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,18 +72,21 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h6 class="text-primary">Verwendungsnachweis</h6>
|
<h6 class="text-primary">Verwendungsnachweis</h6>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
<a href="{% url 'stiftung:dokument_detail' foerderung.verwendungsnachweis.pk %}">
|
|
||||||
{{ foerderung.verwendungsnachweis.titel }}
|
{{ foerderung.verwendungsnachweis.titel }}
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Verknüpfte Dokumente -->
|
<!-- Dokumente (DMS) -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h6 class="text-primary">Verknüpfte Dokumente</h6>
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h6 class="text-primary mb-0">Dokumente</h6>
|
||||||
|
<a href="{% url 'stiftung:dms_upload' %}?foerderung={{ foerderung.pk }}" class="btn btn-sm btn-success">
|
||||||
|
<i class="fas fa-upload me-1"></i>Dokument hochladen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% if verknuepfte_dokumente %}
|
{% if verknuepfte_dokumente %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover">
|
<table class="table table-sm table-hover">
|
||||||
@@ -100,8 +103,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ dokument.titel }}</strong>
|
<strong>{{ dokument.titel }}</strong>
|
||||||
<br>
|
{% if dokument.dateiname_original %}<br><small class="text-muted">{{ dokument.dateiname_original }} ({{ dokument.get_human_size }})</small>{% endif %}
|
||||||
<small class="text-muted">ID: {{ dokument.paperless_document_id }}</small>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
||||||
@@ -115,12 +117,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{{ dokument.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
|
<a href="{% url 'stiftung:dms_download' dokument.pk %}" class="btn btn-sm btn-outline-primary" title="Herunterladen">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
<a href="{% url 'stiftung:dms_edit' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'stiftung:dms_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Löschen">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -128,17 +133,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-sm btn-success">
|
|
||||||
<i class="fas fa-plus me-1"></i>Weiteres Dokument verknüpfen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-3">
|
<div class="text-center py-3">
|
||||||
<i class="fas fa-file-alt fa-2x text-muted mb-2"></i>
|
<i class="fas fa-file-alt fa-2x text-muted mb-2"></i>
|
||||||
<p class="text-muted mb-2">Keine Dokumente verknüpft</p>
|
<p class="text-muted mb-2">Keine Dokumente vorhanden</p>
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-success btn-sm">
|
<a href="{% url 'stiftung:dms_upload' %}?foerderung={{ foerderung.pk }}" class="btn btn-success btn-sm">
|
||||||
<i class="fas fa-plus me-1"></i>Erstes Dokument verknüpfen
|
<i class="fas fa-upload me-1"></i>Erstes Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Optionale Verknüpfung zu einem Dokument aus dem Paperless-System
|
Optionale Verknüpfung zu einem Verwendungsnachweis (Legacy)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -177,6 +177,35 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if geschichte_dokumente %}
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header bg-dark text-white">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-landmark me-2"></i>Verfuegbare Geschichtsdokumente</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for dok in geschichte_dokumente %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<div class="small fw-bold">{{ dok.titel|truncatechars:40 }}</div>
|
||||||
|
<small class="text-muted">{{ dok.dateiname_original }} ({{ dok.get_human_size }})</small>
|
||||||
|
<br><small class="text-muted">{{ dok.erstellt_am|date:"d.m.Y" }}</small>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'stiftung:dms_download' dok.pk %}" class="btn btn-sm btn-outline-primary" title="Herunterladen">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer small text-muted">
|
||||||
|
Dokumente aus dem DMS mit Kontext "Stiftungsgeschichte". Eingegangen per E-Mail oder manuell hochgeladen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -303,11 +303,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-outline-primary btn-sm">
|
<a href="{% url 'stiftung:dms_list' %}" class="btn btn-outline-primary btn-sm">
|
||||||
<i class="fas fa-link me-2"></i>Dokumente verknüpfen
|
<i class="fas fa-folder-open me-2"></i>Zum DMS
|
||||||
</a>
|
|
||||||
<a href="mailto:paperless@vhtv-stiftung.de?subject=Dokumente für {{ land }}" class="btn btn-outline-info btn-sm">
|
|
||||||
<i class="fas fa-envelope me-2"></i>E-Mail an Paperless
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -234,14 +234,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted mb-3">
|
<p class="text-muted mb-3">
|
||||||
Dokumente werden über Paperless verwaltet und verknüpft.
|
Dokumente werden im DMS verwaltet.
|
||||||
</p>
|
</p>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-outline-primary btn-sm">
|
<a href="{% url 'stiftung:dms_list' %}" class="btn btn-outline-primary btn-sm">
|
||||||
<i class="fas fa-link me-2"></i>Dokumente verknüpfen
|
<i class="fas fa-folder-open me-2"></i>Zum DMS
|
||||||
</a>
|
|
||||||
<a href="mailto:paperless@vhtv-stiftung.de" class="btn btn-outline-info btn-sm">
|
|
||||||
<i class="fas fa-envelope me-2"></i>E-Mail an Paperless
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -479,14 +479,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Verknüpfte Dokumente -->
|
<!-- Dokumente (DMS) -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||||
<h6 class="m-0 font-weight-bold text-success">
|
<h6 class="m-0 font-weight-bold text-success">
|
||||||
<i class="fas fa-file-alt me-2"></i>Verknüpfte Dokumente
|
<i class="fas fa-file-alt me-2"></i>Dokumente
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-sm btn-success">
|
<a href="{% url 'stiftung:dms_upload' %}?land={{ land.pk }}" class="btn btn-sm btn-success">
|
||||||
<i class="fas fa-plus me-2"></i>Dokument verknüpfen
|
<i class="fas fa-upload me-2"></i>Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -506,8 +506,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ dokument.titel }}</strong>
|
<strong>{{ dokument.titel }}</strong>
|
||||||
<br>
|
{% if dokument.dateiname_original %}<br><small class="text-muted">{{ dokument.dateiname_original }} ({{ dokument.get_human_size }})</small>{% endif %}
|
||||||
<small class="text-muted">ID: {{ dokument.paperless_document_id }}</small>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
||||||
@@ -521,17 +520,14 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{{ dokument.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
|
<a href="{% url 'stiftung:dms_download' dokument.pk %}" class="btn btn-sm btn-outline-primary" title="Herunterladen">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ dokument.get_paperless_thumbnail_url }}" target="_blank" class="btn btn-sm btn-outline-info" title="Thumbnail anzeigen">
|
<a href="{% url 'stiftung:dms_edit' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
||||||
<i class="fas fa-image"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'stiftung:dokument_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Verknüpfung löschen">
|
<a href="{% url 'stiftung:dms_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Löschen">
|
||||||
<i class="fas fa-unlink"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -543,10 +539,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
|
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
|
||||||
<h5 class="text-muted">Keine Dokumente verknüpft</h5>
|
<h5 class="text-muted">Keine Dokumente vorhanden</h5>
|
||||||
<p class="text-muted">Verknüpfen Sie Dokumente aus Paperless mit dieser Länderei.</p>
|
<p class="text-muted">Laden Sie Dokumente direkt hoch und verknüpfen Sie sie mit dieser Länderei.</p>
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-success">
|
<a href="{% url 'stiftung:dms_upload' %}?land={{ land.pk }}" class="btn btn-success">
|
||||||
<i class="fas fa-plus me-2"></i>Erstes Dokument verknüpfen
|
<i class="fas fa-upload me-2"></i>Erstes Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -188,12 +188,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Verknüpfte Dokumente -->
|
<!-- Dokumente (DMS) -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0"><i class="fas fa-folder-open me-2"></i>Verknüpfte Dokumente</h5>
|
<h5 class="mb-0"><i class="fas fa-folder-open me-2"></i>Dokumente</h5>
|
||||||
<a href="/dokumente/verwaltung/" class="btn btn-sm btn-outline-primary">
|
<a href="{% url 'stiftung:dms_upload' %}?verpachtung={{ landverpachtung.pk }}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="fas fa-link me-1"></i>Dokument verknüpfen
|
<i class="fas fa-upload me-1"></i>Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -212,14 +212,21 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ doc.titel|default:"Ohne Titel" }}</strong>
|
<strong>{{ doc.titel|default:"Ohne Titel" }}</strong>
|
||||||
<br>
|
{% if doc.dateiname_original %}<br><small class="text-muted">{{ doc.dateiname_original }} ({{ doc.get_human_size }})</small>{% endif %}
|
||||||
<small class="text-muted">Paperless-ID: {{ doc.paperless_document_id }}</small>
|
|
||||||
</td>
|
</td>
|
||||||
<td>{{ doc.get_kontext_display }}</td>
|
<td>{{ doc.get_kontext_display }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ doc.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
|
<div class="btn-group" role="group">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<a href="{% url 'stiftung:dms_download' doc.pk %}" class="btn btn-sm btn-outline-primary" title="Herunterladen">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'stiftung:dms_edit' doc.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:dms_delete' doc.pk %}" class="btn btn-sm btn-outline-danger" title="Löschen">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -227,7 +234,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">Keine Dokumente verknüpft.</p>
|
<p class="text-muted">Keine Dokumente vorhanden.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -279,14 +279,14 @@
|
|||||||
|
|
||||||
<!-- Legacy Verpachtungen entfernt für saubere UI -->
|
<!-- Legacy Verpachtungen entfernt für saubere UI -->
|
||||||
|
|
||||||
<!-- Verknüpfte Dokumente -->
|
<!-- Dokumente (DMS) -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
<i class="fas fa-file-alt me-2"></i>Verknüpfte Dokumente
|
<i class="fas fa-file-alt me-2"></i>Dokumente
|
||||||
</h5>
|
</h5>
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-light btn-sm">
|
<a href="{% url 'stiftung:dms_upload' %}?paechter={{ paechter.pk }}" class="btn btn-light btn-sm">
|
||||||
<i class="fas fa-plus me-1"></i>Dokument verknüpfen
|
<i class="fas fa-upload me-1"></i>Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -306,8 +306,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ dokument.titel }}</strong>
|
<strong>{{ dokument.titel }}</strong>
|
||||||
<br>
|
{% if dokument.dateiname_original %}<br><small class="text-muted">{{ dokument.dateiname_original }} ({{ dokument.get_human_size }})</small>{% endif %}
|
||||||
<small class="text-muted">ID: {{ dokument.paperless_document_id }}</small>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
<span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span>
|
||||||
@@ -321,17 +320,14 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{{ dokument.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="In Paperless öffnen">
|
<a href="{% url 'stiftung:dms_download' dokument.pk %}" class="btn btn-sm btn-outline-primary" title="Herunterladen">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ dokument.get_paperless_thumbnail_url }}" target="_blank" class="btn btn-sm btn-outline-info" title="Thumbnail anzeigen">
|
<a href="{% url 'stiftung:dms_edit' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
||||||
<i class="fas fa-image"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'stiftung:dokument_update' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'stiftung:dokument_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Verknüpfung löschen">
|
<a href="{% url 'stiftung:dms_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Löschen">
|
||||||
<i class="fas fa-unlink"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -343,10 +339,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
|
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
|
||||||
<h5 class="text-muted">Keine Dokumente verknüpft</h5>
|
<h5 class="text-muted">Keine Dokumente vorhanden</h5>
|
||||||
<p class="text-muted">Verknüpfen Sie Dokumente aus Paperless mit diesem Pächter.</p>
|
<p class="text-muted">Laden Sie Dokumente direkt hoch und verknüpfen Sie sie mit diesem Pächter.</p>
|
||||||
<a href="{% url 'stiftung:dokument_management' %}" class="btn btn-success">
|
<a href="{% url 'stiftung:dms_upload' %}?paechter={{ paechter.pk }}" class="btn btn-success">
|
||||||
<i class="fas fa-plus me-2"></i>Erstes Dokument verknüpfen
|
<i class="fas fa-upload me-2"></i>Erstes Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -296,8 +296,8 @@
|
|||||||
<span class="badge bg-primary ms-2">{{ verknuepfte_dokumente.count }}</span>
|
<span class="badge bg-primary ms-2">{{ verknuepfte_dokumente.count }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h6>
|
</h6>
|
||||||
<a href="/dokumente/verwaltung/" class="btn btn-outline-primary btn-sm">
|
<a href="{% url 'stiftung:dms_list' %}" class="btn btn-outline-primary btn-sm">
|
||||||
<i class="fas fa-link me-1"></i>Dokumentenverwaltung
|
<i class="fas fa-folder-open me-1"></i>Dokumentenverwaltung
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -318,10 +318,7 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="/api/paperless/documents/{{ dokument.paperless_document_id }}/"
|
<span class="badge bg-secondary">Legacy</span>
|
||||||
class="btn btn-outline-primary btn-sm" target="_blank" title="In Paperless öffnen">
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -332,7 +329,7 @@
|
|||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="fas fa-info-circle me-1"></i>
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
Es werden nur die neuesten 10 Dokumente angezeigt.
|
Es werden nur die neuesten 10 Dokumente angezeigt.
|
||||||
<a href="/dokumente/verwaltung/" class="text-decoration-none">Alle Dokumente anzeigen</a>
|
<a href="{% url 'stiftung:dms_list' %}" class="text-decoration-none">Alle Dokumente anzeigen</a>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -342,7 +339,7 @@
|
|||||||
<h5 class="text-muted">Keine Dokumente verknüpft</h5>
|
<h5 class="text-muted">Keine Dokumente verknüpft</h5>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Verknüpfen Sie Dokumente über die
|
Verknüpfen Sie Dokumente über die
|
||||||
<a href="/dokumente/verwaltung/" class="text-decoration-none">Dokumentenverwaltung</a>.
|
<a href="{% url 'stiftung:dms_list' %}" class="text-decoration-none">Dokumentenverwaltung</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -292,66 +292,60 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Verknüpfte Dokumente -->
|
<!-- Dokumente (DMS) -->
|
||||||
{% if verknuepfte_dokumente %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow mb-4">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
<i class="fas fa-file-alt me-2"></i>Verknüpfte Dokumente ({{ verknuepfte_dokumente.count }})
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
{% for dokument in verknuepfte_dokumente %}
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<div class="card border-left-success">
|
|
||||||
<div class="card-body py-2">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-1">{{ dokument.titel }}</h6>
|
|
||||||
<small class="text-muted">{{ dokument.kontext }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="{{ dokument.paperless_url }}" target="_blank"
|
|
||||||
class="btn btn-sm btn-outline-success">
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Dokument Management Section -->
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<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 d-flex justify-content-between align-items-center">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
<i class="fas fa-paperclip me-2"></i>Dokumente verwalten
|
<i class="fas fa-file-alt me-2"></i>Dokumente{% if verknuepfte_dokumente %} ({{ verknuepfte_dokumente.count }}){% endif %}
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{% url 'stiftung:dokument_create' %}?land_verpachtung_id={{ verpachtung.pk }}"
|
<a href="{% url 'stiftung:dms_upload' %}?verpachtung={{ verpachtung.pk }}"
|
||||||
class="btn btn-sm btn-success">
|
class="btn btn-sm btn-success">
|
||||||
<i class="fas fa-plus me-1"></i>Dokument verknüpfen
|
<i class="fas fa-upload me-1"></i>Dokument hochladen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if verknuepfte_dokumente %}
|
{% if verknuepfte_dokumente %}
|
||||||
<p class="text-muted mb-3">{{ verknuepfte_dokumente.count }} Dokument{{ verknuepfte_dokumente.count|pluralize:"e" }} verknüpft</p>
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Kontext</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for dokument in verknuepfte_dokumente %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ dokument.titel }}</strong>
|
||||||
|
{% if dokument.dateiname_original %}<br><small class="text-muted">{{ dokument.dateiname_original }} ({{ dokument.get_human_size }})</small>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><span class="badge bg-secondary">{{ dokument.get_kontext_display }}</span></td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'stiftung:dms_download' dokument.pk %}" class="btn btn-sm btn-outline-primary" title="Herunterladen">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:dms_edit' dokument.pk %}" class="btn btn-sm btn-outline-warning" title="Bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:dms_delete' dokument.pk %}" class="btn btn-sm btn-outline-danger" title="Löschen">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
Noch keine Dokumente mit dieser Verpachtung verknüpft.
|
Noch keine Dokumente vorhanden.
|
||||||
Klicken Sie auf "Dokument verknüpfen", um Dokumente aus dem Paperless-System zu verknüpfen.
|
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -341,7 +341,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted small">Dokumente können nach dem Speichern der Verpachtung verknüpft werden.</p>
|
<p class="text-muted small">Dokumente können nach dem Speichern der Verpachtung verknüpft werden.</p>
|
||||||
<a href="{% url 'stiftung:dokument_create' %}?land_verpachtung_id={{ form.instance.pk }}"
|
<a href="{% url 'stiftung:dms_upload' %}?land_verpachtung_id={{ form.instance.pk }}"
|
||||||
class="btn btn-outline-primary btn-sm">
|
class="btn btn-outline-primary btn-sm">
|
||||||
<i class="fas fa-plus me-1"></i>Neues Dokument
|
<i class="fas fa-plus me-1"></i>Neues Dokument
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
52
compose.yml
52
compose.yml
@@ -15,7 +15,6 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- dbdata:/var/lib/postgresql/data
|
- dbdata:/var/lib/postgresql/data
|
||||||
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -46,14 +45,6 @@ services:
|
|||||||
- REDIS_URL=${REDIS_URL}
|
- REDIS_URL=${REDIS_URL}
|
||||||
- SESSION_COOKIE_NAME=${SESSION_COOKIE_NAME}
|
- SESSION_COOKIE_NAME=${SESSION_COOKIE_NAME}
|
||||||
- CSRF_COOKIE_NAME=${CSRF_COOKIE_NAME}
|
- CSRF_COOKIE_NAME=${CSRF_COOKIE_NAME}
|
||||||
- PAPERLESS_API_URL=${PAPERLESS_API_URL}
|
|
||||||
- PAPERLESS_API_TOKEN=${PAPERLESS_API_TOKEN}
|
|
||||||
- PAPERLESS_REQUIRED_TAG=${PAPERLESS_REQUIRED_TAG}
|
|
||||||
- PAPERLESS_LAND_TAG=${PAPERLESS_LAND_TAG}
|
|
||||||
- PAPERLESS_ADMIN_TAG=${PAPERLESS_ADMIN_TAG}
|
|
||||||
- PAPERLESS_DESTINATAERE_TAG_ID=${PAPERLESS_DESTINATAERE_TAG_ID}
|
|
||||||
- PAPERLESS_LAND_TAG_ID=${PAPERLESS_LAND_TAG_ID}
|
|
||||||
- PAPERLESS_ADMIN_TAG_ID=${PAPERLESS_ADMIN_TAG_ID}
|
|
||||||
- GRAMPS_URL=${GRAMPS_URL}
|
- GRAMPS_URL=${GRAMPS_URL}
|
||||||
- GRAMPS_USERNAME=${GRAMPS_USERNAME}
|
- GRAMPS_USERNAME=${GRAMPS_USERNAME}
|
||||||
- GRAMPS_PASSWORD=${GRAMPS_PASSWORD}
|
- GRAMPS_PASSWORD=${GRAMPS_PASSWORD}
|
||||||
@@ -91,9 +82,6 @@ services:
|
|||||||
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
||||||
- IMAP_FOLDER=${IMAP_FOLDER}
|
- IMAP_FOLDER=${IMAP_FOLDER}
|
||||||
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
||||||
- PAPERLESS_API_URL=${PAPERLESS_API_URL}
|
|
||||||
- PAPERLESS_API_TOKEN=${PAPERLESS_API_TOKEN}
|
|
||||||
- PAPERLESS_DESTINATAERE_TAG_ID=${PAPERLESS_DESTINATAERE_TAG_ID}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- db
|
- db
|
||||||
@@ -120,9 +108,6 @@ services:
|
|||||||
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
||||||
- IMAP_FOLDER=${IMAP_FOLDER}
|
- IMAP_FOLDER=${IMAP_FOLDER}
|
||||||
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
||||||
- PAPERLESS_API_URL=${PAPERLESS_API_URL}
|
|
||||||
- PAPERLESS_API_TOKEN=${PAPERLESS_API_TOKEN}
|
|
||||||
- PAPERLESS_DESTINATAERE_TAG_ID=${PAPERLESS_DESTINATAERE_TAG_ID}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- db
|
- db
|
||||||
@@ -150,43 +135,6 @@ services:
|
|||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
# Phase 3 (Vision 2026): Paperless-NGX durch Django-natives DMS ersetzt.
|
|
||||||
# Dienst deaktiviert. Bestehende Dokumente via: python manage.py migrate_paperless_dokumente
|
|
||||||
# paperless:
|
|
||||||
# image: ghcr.io/remmerinio/stiftung-management-system-paperless:latest
|
|
||||||
# ports:
|
|
||||||
# - "8080:8000"
|
|
||||||
# environment:
|
|
||||||
# - PAPERLESS_REDIS=redis://redis:6379
|
|
||||||
# - PAPERLESS_DBHOST=db
|
|
||||||
# - PAPERLESS_DBPORT=5432
|
|
||||||
# - PAPERLESS_DBNAME=${PAPERLESS_DBNAME:-paperless}
|
|
||||||
# - PAPERLESS_DBUSER=${POSTGRES_USER}
|
|
||||||
# - PAPERLESS_DBPASS=${POSTGRES_PASSWORD}
|
|
||||||
# - PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
|
|
||||||
# - PAPERLESS_URL=https://vhtv-stiftung.de
|
|
||||||
# - PAPERLESS_ALLOWED_HOSTS=vhtv-stiftung.de,localhost,paperless
|
|
||||||
# - PAPERLESS_CORS_ALLOWED_HOSTS=https://vhtv-stiftung.de
|
|
||||||
# - PAPERLESS_FORCE_SCRIPT_NAME=/paperless
|
|
||||||
# - PAPERLESS_STATIC_URL=/paperless/static/
|
|
||||||
# - PAPERLESS_LOGIN_REDIRECT_URL=/paperless/
|
|
||||||
# - PAPERLESS_LOGOUT_REDIRECT_URL=/paperless/
|
|
||||||
# - PAPERLESS_ADMIN_USER=${PAPERLESS_ADMIN_USER}
|
|
||||||
# - PAPERLESS_ADMIN_PASSWORD=${PAPERLESS_ADMIN_PASSWORD}
|
|
||||||
# - PAPERLESS_ADMIN_MAIL=${PAPERLESS_ADMIN_MAIL}
|
|
||||||
# volumes:
|
|
||||||
# - paperless_data:/usr/src/paperless/data
|
|
||||||
# - paperless_media:/usr/src/paperless/media
|
|
||||||
# - paperless_export:/usr/src/paperless/export
|
|
||||||
# - paperless_consume:/usr/src/paperless/consume
|
|
||||||
# depends_on:
|
|
||||||
# - db
|
|
||||||
# - redis
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dbdata:
|
dbdata:
|
||||||
gramps_data:
|
gramps_data:
|
||||||
paperless_data:
|
|
||||||
paperless_media:
|
|
||||||
paperless_export:
|
|
||||||
paperless_consume:
|
|
||||||
|
|||||||
103
deploy.sh
Executable file
103
deploy.sh
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# deploy.sh — Deploy main branch to production (vhtv-stiftung.de)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./deploy.sh # Deploy current main to production
|
||||||
|
# ./deploy.sh --dry-run # Show what would happen without deploying
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - SSH access to the production server (key-based auth)
|
||||||
|
# - Production .env file at /opt/stiftung/.env on the server
|
||||||
|
# - Git remote 'origin' configured on the server pointing to Gitea
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVER="${DEPLOY_SERVER:-remmer@vhtv-stiftung.de}"
|
||||||
|
PROD_DIR="${DEPLOY_DIR:-/opt/stiftung}"
|
||||||
|
COMPOSE_FILE="compose.yml"
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||||
|
DRY_RUN=true
|
||||||
|
echo "=== DRY RUN — no changes will be made ==="
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Stiftung Production Deployment ==="
|
||||||
|
echo "Server: $SERVER"
|
||||||
|
echo "Path: $PROD_DIR"
|
||||||
|
echo "Compose: $COMPOSE_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify local main is up to date with remote
|
||||||
|
LOCAL_MAIN=$(git rev-parse main 2>/dev/null || echo "unknown")
|
||||||
|
echo "Local main: $LOCAL_MAIN"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Would SSH to $SERVER and:"
|
||||||
|
echo " 1. git fetch origin main && git reset --hard origin/main"
|
||||||
|
echo " 2. docker compose down"
|
||||||
|
echo " 3. docker compose up -d --build"
|
||||||
|
echo " 4. Run migrations and collectstatic"
|
||||||
|
echo ""
|
||||||
|
echo "=== Dry run complete ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
read -p "Deploy main ($LOCAL_MAIN) to production? [y/N] " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Deploying to $SERVER:$PROD_DIR ==="
|
||||||
|
|
||||||
|
ssh "$SERVER" bash -s "$PROD_DIR" "$COMPOSE_FILE" << 'DEPLOY_SCRIPT'
|
||||||
|
set -euo pipefail
|
||||||
|
PROD_DIR="$1"
|
||||||
|
COMPOSE_FILE="$2"
|
||||||
|
cd "$PROD_DIR"
|
||||||
|
|
||||||
|
echo "--- Fetching latest main ---"
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Pulling standard images ---"
|
||||||
|
docker compose -f "$COMPOSE_FILE" pull db redis grampsweb || echo "Some pulls failed, using cached"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Stopping containers ---"
|
||||||
|
docker compose -f "$COMPOSE_FILE" down
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Building and starting ---"
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Waiting for services to start ---"
|
||||||
|
sleep 20
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Running migrations ---"
|
||||||
|
docker compose -f "$COMPOSE_FILE" exec -T web python manage.py migrate
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Collecting static files ---"
|
||||||
|
docker compose -f "$COMPOSE_FILE" exec -T web python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Service status ---"
|
||||||
|
docker compose -f "$COMPOSE_FILE" ps
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Deployment complete ==="
|
||||||
|
DEPLOY_SCRIPT
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Done! Production updated to main ($LOCAL_MAIN) ==="
|
||||||
|
echo "Site: https://vhtv-stiftung.de"
|
||||||
Reference in New Issue
Block a user