feat: Add complete Verpachtung management system

- Add LandVerpachtung model with Land and Paechter relationships
- Implement full CRUD operations for Verpachtungen
- Add responsive Bootstrap templates with JavaScript calculations
- Integrate document linking functionality similar to other entities
- Add navigation links and URL patterns
- Include CSV import support for Paechter data
- Fix template encoding issues for proper UTF-8 support
- Enhance administration interface with Verpachtung CSV import

This implements the complete Verpachtung management feature requested,
allowing users to manage land lease agreements with proper relationships
to properties (Laenderei) and tenants (Paechter), following the same
patterns as Foerderung/Destinataer relationships.
This commit is contained in:
Stiftung Development
2025-09-15 21:18:01 +02:00
parent c8bef800c8
commit 4be6be203e
14 changed files with 1743 additions and 127 deletions

131
DEVELOPMENT_WORKFLOW.md Normal file
View File

@@ -0,0 +1,131 @@
# 🚀 Development Workflow Guide
## 🔄 Complete Development Process
### Step 1: Make Changes Safely (Local Development)
```bash
# 1. Start development environment (isolated from production)
docker-compose -f compose.dev.yml up -d
# 2. Make your code changes in the /app directory
# - Edit Python files, templates, models, etc.
# - Changes automatically reload in development server
# 3. Test your changes
# - Access: http://localhost:8081 (Django app)
# - Access: http://localhost:8082 (Paperless)
# - Database: localhost:5433 (completely separate from production)
# 4. Run tests if needed
docker-compose -f compose.dev.yml exec web python manage.py test
# 5. When done testing, stop development
docker-compose -f compose.dev.yml down
```
### Step 2: Commit Changes (Git Version Control)
```bash
# 1. Stage your changes
git add .
# 2. Commit with descriptive message
git commit -m "Add new feature: enhanced user management"
# 3. Push to GitHub (but NOT to main branch yet)
git push origin main
```
### Step 3: Deploy to Production (Automated)
When you push to the `main` branch, GitHub Actions automatically:
1. **Builds** the Docker images
2. **Runs tests** (if any fail, deployment stops)
3. **Deploys** to production server
4. **Preserves** production database and secrets
5. **Updates** only the code, not the data
## 🛡️ Environment Separation Details
### Development Environment (`compose.dev.yml`)
- **Database**: `stiftung_dev` (port 5433)
- **Data**: Completely isolated test data
- **Secrets**: Hardcoded safe values
- **Purpose**: Safe testing without any risk
### Production Environment (`compose.yml`)
- **Database**: `stiftung` (internal to server)
- **Data**: Real user data (Destinatäre, Förderungen, etc.)
- **Secrets**: Secure tokens from server's .env file
- **Purpose**: Live system for real users
## 🔒 Safety Guarantees
### What CAN'T Break Production:
✅ Code changes in development
✅ Database changes in development
✅ Testing new features locally
✅ Experimenting with different configurations
### What COULD Affect Production:
⚠️ Pushing broken code to main branch
⚠️ Database migration issues (rare)
⚠️ Configuration changes in compose.yml
## 🧪 Testing New Features
### For Small Changes:
1. Edit code in `/app` directory
2. Test in development (localhost:8081)
3. If good → commit and push
### For Database Changes:
1. Create migration: `docker-compose -f compose.dev.yml exec web python manage.py makemigrations`
2. Test migration: `docker-compose -f compose.dev.yml exec web python manage.py migrate`
3. Verify in development environment
4. If good → commit and push (migration will run automatically in production)
### For Major Features:
1. Create feature branch: `git checkout -b feature/new-feature`
2. Develop and test extensively in development environment
3. Create Pull Request on GitHub
4. Review and merge to main when ready
## 🚀 Deployment Process
```mermaid
graph LR
A[Local Changes] --> B[Development Testing]
B --> C[Git Commit]
C --> D[Push to GitHub]
D --> E[GitHub Actions]
E --> F[Automatic Production Deployment]
F --> G[Live on vhtv-stiftung.de]
```
## 🆘 Emergency Procedures
### If Something Breaks in Production:
1. **Rollback**: `git revert <commit-hash>` and push
2. **Quick Fix**: Make fix, test locally, commit and push
3. **Check Logs**: GitHub Actions shows deployment logs
### If Development Environment Has Issues:
1. **Reset**: `docker-compose -f compose.dev.yml down -v`
2. **Rebuild**: `docker-compose -f compose.dev.yml up -d --build`
3. **Re-migrate**: `docker-compose -f compose.dev.yml exec web python manage.py migrate`
## 📊 Current Status Check
```bash
# Check what's running
docker ps
# Development environment status
docker-compose -f compose.dev.yml ps
# View logs
docker-compose -f compose.dev.yml logs web
```

View File

@@ -1,6 +1,32 @@
# Stiftung Application # Stiftung Application
A comprehensive Django-based foundation management system with modern features and automated deployment. A## 🏃‍♂️ Quick Start
### Development Environment
For local development, use the development Docker Compose configuration:
```bash
# Start development environment (uses hardcoded dev settings)
docker-compose -f compose.dev.yml up -d
# Run migrations
docker-compose -f compose.dev.yml exec web python manage.py migrate
# Create superuser
docker-compose -f compose.dev.yml exec web python manage.py createsuperuser
# Access applications
# - Django: http://localhost:8081
# - Paperless: http://localhost:8082
# - Database: localhost:5433
```
### Production Deployment
Production deployment uses the main `compose.yml` and is handled automatically via GitHub Actions. The production environment reads configuration from the server's `.env` file (not included in the repository for security).
## 🏗️ Architecturengo-based foundation management system with modern features and automated deployment.
## 🌟 Features ## 🌟 Features
@@ -27,21 +53,30 @@ A comprehensive Django-based foundation management system with modern features a
```bash ```bash
# Clone repository # Clone repository
git clone https://github.com/yourusername/stiftung-starter.git git clone https://github.com/remmerinio/stiftung-management-system.git
cd stiftung-starter cd stiftung-management-system
# Start development environment # Start development environment (isolated from production)
docker-compose up -d docker-compose -f compose.dev.yml up -d
# Create superuser # Create superuser
docker-compose exec web python manage.py createsuperuser docker-compose -f compose.dev.yml exec web python manage.py createsuperuser
# Access application # Access application
open http://localhost:8000 open http://localhost:8081 # Django app
open http://localhost:8080 # Paperless-ngx (admin/admin123)
``` ```
### Production Deployment ### Production Deployment
Production uses automated deployment via GitHub Actions. The production environment has its own `.env` file with secure credentials that is not stored in Git.
**For Production:**
- Environment variables are configured on the server
- HTTPS is enabled with Let's Encrypt certificates
- Database users are manually configured for security
- Automated deployments preserve manual configurations
See [deployment documentation](deploy-production/MIGRATION_PLAN.md) for complete setup instructions. See [deployment documentation](deploy-production/MIGRATION_PLAN.md) for complete setup instructions.
## 📁 Project Structure ## 📁 Project Structure

View File

@@ -6,7 +6,7 @@ from django.utils import timezone
from .models import (BankTransaction, Destinataer, DestinataerNotiz, from .models import (BankTransaction, Destinataer, DestinataerNotiz,
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land, DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
LandAbrechnung, Paechter, Person, Rentmeister, LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister,
StiftungsKonto, UnterstuetzungWiederkehrend, StiftungsKonto, UnterstuetzungWiederkehrend,
Verwaltungskosten) Verwaltungskosten)
@@ -534,6 +534,53 @@ class LandForm(forms.ModelForm):
} }
class LandVerpachtungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Verpachtungen"""
class Meta:
model = LandVerpachtung
fields = [
'land',
'paechter',
'vertragsnummer',
'pachtbeginn',
'pachtende',
'verlaengerung_klausel',
'verpachtete_flaeche',
'pachtzins_pauschal',
'pachtzins_pro_ha',
'zahlungsweise',
'ust_option',
'ust_satz',
'grundsteuer_umlage',
'versicherungen_umlage',
'verbandsbeitraege_umlage',
'jagdpacht_anteil_umlage',
'status',
'bemerkungen'
]
widgets = {
'land': forms.Select(attrs={'class': 'form-select'}),
'paechter': forms.Select(attrs={'class': 'form-select'}),
'vertragsnummer': forms.TextInput(attrs={'class': 'form-control'}),
'pachtbeginn': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'pachtende': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'verlaengerung_klausel': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'verpachtete_flaeche': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'pachtzins_pauschal': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'pachtzins_pro_ha': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'zahlungsweise': forms.Select(attrs={'class': 'form-select'}),
'ust_option': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'ust_satz': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'grundsteuer_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'versicherungen_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'verbandsbeitraege_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'jagdpacht_anteil_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'bemerkungen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class LandAbrechnungForm(forms.ModelForm): class LandAbrechnungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Landabrechnungen""" """Form für das Erstellen und Bearbeiten von Landabrechnungen"""

View File

@@ -109,6 +109,20 @@ urlpatterns = [
views.land_verpachtung_end_direct, views.land_verpachtung_end_direct,
name="land_verpachtung_end_direct", name="land_verpachtung_end_direct",
), ),
# Verpachtung URLs (Management Overview)
path("verpachtungen/", views.verpachtung_list, name="verpachtung_list"),
path("verpachtungen/<uuid:pk>/", views.verpachtung_detail, name="verpachtung_detail"),
path("verpachtungen/neu/", views.verpachtung_create, name="verpachtung_create"),
path(
"verpachtungen/<uuid:pk>/bearbeiten/",
views.verpachtung_update,
name="verpachtung_update",
),
path(
"verpachtungen/<uuid:pk>/loeschen/",
views.verpachtung_delete,
name="verpachtung_delete",
),
# Förderung URLs # Förderung URLs
path("foerderungen/", views.foerderung_list, name="foerderung_list"), path("foerderungen/", views.foerderung_list, name="foerderung_list"),
path("foerderungen/<uuid:pk>/", views.foerderung_detail, name="foerderung_detail"), path("foerderungen/<uuid:pk>/", views.foerderung_detail, name="foerderung_detail"),

View File

@@ -1689,7 +1689,7 @@ def verpachtung_list(request):
"beginn": ["pachtbeginn"], "beginn": ["pachtbeginn"],
"ende": ["pachtende"], "ende": ["pachtende"],
"flaeche": ["verpachtete_flaeche"], "flaeche": ["verpachtete_flaeche"],
"pachtzins": ["pachtzins_jaehrlich"], "pachtzins": ["pachtzins_pauschal"],
"status": ["status"], "status": ["status"],
} }
if sort in sort_map: if sort in sort_map:
@@ -1723,7 +1723,7 @@ def verpachtung_list(request):
# Total annual rent (only active verpachtungen) # Total annual rent (only active verpachtungen)
jaehrlicher_pachtzins_result = all_verpachtungen.filter(status="aktiv").aggregate( jaehrlicher_pachtzins_result = all_verpachtungen.filter(status="aktiv").aggregate(
total=Sum("pachtzins_jaehrlich") total=Sum("pachtzins_pauschal")
) )
jaehrlicher_pachtzins = ( jaehrlicher_pachtzins = (
jaehrlicher_pachtzins_result["total"] jaehrlicher_pachtzins_result["total"]
@@ -1787,7 +1787,7 @@ def land_verpachtung_update(request, pk):
vertragsnummer = request.POST.get("vertragsnummer") vertragsnummer = request.POST.get("vertragsnummer")
pachtbeginn = request.POST.get("pachtbeginn") pachtbeginn = request.POST.get("pachtbeginn")
pachtende = request.POST.get("pachtende") pachtende = request.POST.get("pachtende")
pachtzins_jaehrlich = request.POST.get("pachtzins_jaehrlich") pachtzins_pauschal = request.POST.get("pachtzins_pauschal")
if vertragsnummer: if vertragsnummer:
verpachtung.vertragsnummer = vertragsnummer verpachtung.vertragsnummer = vertragsnummer
@@ -1795,8 +1795,8 @@ def land_verpachtung_update(request, pk):
verpachtung.pachtbeginn = pachtbeginn verpachtung.pachtbeginn = pachtbeginn
if pachtende: if pachtende:
verpachtung.pachtende = pachtende verpachtung.pachtende = pachtende
if pachtzins_jaehrlich: if pachtzins_pauschal:
verpachtung.pachtzins_jaehrlich = pachtzins_jaehrlich verpachtung.pachtzins_pauschal = pachtzins_pauschal
verpachtung.save() verpachtung.save()
messages.success(request, "Verpachtung wurde erfolgreich aktualisiert.") messages.success(request, "Verpachtung wurde erfolgreich aktualisiert.")
@@ -2130,7 +2130,11 @@ def dokument_create(request):
) )
# Zurück zur verknüpften Entität leiten # Zurück zur verknüpften Entität leiten
if dokument.verpachtung_id: if dokument.land_verpachtung_id:
return redirect(
"stiftung:verpachtung_detail", pk=dokument.land_verpachtung_id
)
elif dokument.verpachtung_id:
return redirect( return redirect(
"stiftung:verpachtung_detail", pk=dokument.verpachtung_id "stiftung:verpachtung_detail", pk=dokument.verpachtung_id
) )
@@ -2149,6 +2153,8 @@ def dokument_create(request):
else: else:
# Initial-Werte aus GET-Parametern setzen # Initial-Werte aus GET-Parametern setzen
initial_data = {} initial_data = {}
if request.GET.get("land_verpachtung_id"):
initial_data["land_verpachtung_id"] = request.GET.get("land_verpachtung_id")
if request.GET.get("verpachtung"): if request.GET.get("verpachtung"):
initial_data["verpachtung_id"] = request.GET.get("verpachtung") initial_data["verpachtung_id"] = request.GET.get("verpachtung")
if request.GET.get("land"): if request.GET.get("land"):
@@ -5435,7 +5441,7 @@ def verpachtung_export(request, pk):
else None else None
), ),
"pachtzins_pro_qm": str(verpachtung.pachtzins_pro_qm), "pachtzins_pro_qm": str(verpachtung.pachtzins_pro_qm),
"pachtzins_jaehrlich": str(verpachtung.pachtzins_jaehrlich), "pachtzins_jaehrlich": str(verpachtung.pachtzins_pauschal),
"verpachtete_flaeche": str(verpachtung.verpachtete_flaeche), "verpachtete_flaeche": str(verpachtung.verpachtete_flaeche),
"status": verpachtung.get_status_display(), "status": verpachtung.get_status_display(),
"verwendungsnachweis": ( "verwendungsnachweis": (
@@ -6914,3 +6920,126 @@ def edit_help_box(request):
"title": "Hilfs-Infoboxen verwalten", "title": "Hilfs-Infoboxen verwalten",
} }
return render(request, "stiftung/help_boxes_admin.html", context) return render(request, "stiftung/help_boxes_admin.html", context)
# =============================================================================
# Verpachtung Management Views (Standalone CRUD)
# =============================================================================
@login_required
def verpachtung_detail(request, pk):
"""Standalone detail view for verpachtung"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
# Alle mit dieser Verpachtung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
land_verpachtung_id=verpachtung.pk
).order_by("kontext", "titel")
context = {
"verpachtung": verpachtung,
"landverpachtung": verpachtung, # Template compatibility
"verknuepfte_dokumente": verknuepfte_dokumente,
"title": f"Verpachtung {verpachtung.vertragsnummer}",
}
return render(request, "stiftung/verpachtung_detail.html", context)
@login_required
def verpachtung_create(request):
"""Standalone create view for verpachtung"""
from .forms import LandVerpachtungForm
from datetime import datetime as dt
if request.method == 'POST':
form = LandVerpachtungForm(request.POST, request.FILES)
if form.is_valid():
verpachtung = form.save()
# Update the Land model to reflect this verpachtung
land = verpachtung.land
land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name()
land.paechter_anschrift = f"{verpachtung.paechter.strasse or ''}\n{verpachtung.paechter.plz or ''} {verpachtung.paechter.ort or ''}".strip()
land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende
land.pachtzins_pauschal = verpachtung.pachtzins_pauschal
land.zahlungsweise = verpachtung.zahlungsweise
land.ust_option = verpachtung.ust_option
land.verpachtete_gesamtflaeche = verpachtung.verpachtete_flaeche
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
land.save()
# Create automatic abrechnung
current_year = dt.now().year
expected_annual_rent = verpachtung.pachtzins_pauschal if verpachtung.pachtzins_pauschal else 0
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=land,
abrechnungsjahr=current_year,
defaults={
"pacht_vereinnahmt": expected_annual_rent,
"umlagen_vereinnahmt": 0,
"grundsteuer_betrag": 0,
"versicherungen_betrag": 0,
},
)
if not created and expected_annual_rent > abrechnung.pacht_vereinnahmt:
abrechnung.pacht_vereinnahmt = expected_annual_rent
abrechnung.save()
success_msg = f'Verpachtung "{verpachtung.vertragsnummer}" wurde erfolgreich erstellt.'
if created:
success_msg += f" Abrechnung für {current_year} wurde automatisch angelegt"
if expected_annual_rent > 0:
success_msg += f" (Erwartete Jahrespacht: {expected_annual_rent}€)"
success_msg += "."
elif expected_annual_rent > 0:
success_msg += f" Erwartete Jahrespacht in Abrechnung {current_year} wurde aktualisiert ({expected_annual_rent}€)."
messages.success(request, success_msg)
return redirect('stiftung:verpachtung_detail', pk=verpachtung.pk)
else:
form = LandVerpachtungForm()
# Get available Länder and Pächter for the template
laender_list = Land.objects.all().order_by('lfd_nr')
paechter_list = Paechter.objects.filter(aktiv=True).order_by('nachname', 'vorname')
context = {
'form': form,
'title': 'Neue Verpachtung erstellen',
'laender_list': laender_list,
'paechter_list': paechter_list,
'current_year': dt.now().year,
'is_edit': False,
}
return render(request, 'stiftung/verpachtung_form.html', context)
@login_required
def verpachtung_update(request, pk):
"""Standalone update view for verpachtung"""
return land_verpachtung_update(request, pk)
@login_required
def verpachtung_delete(request, pk):
"""Standalone delete view for verpachtung"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
if request.method == 'POST':
vertragsnummer = verpachtung.vertragsnummer
verpachtung.delete()
messages.success(
request,
f'Verpachtung "{vertragsnummer}" wurde erfolgreich gelöscht.'
)
return redirect('stiftung:verpachtung_list')
context = {
'verpachtung': verpachtung,
'title': f'Verpachtung {verpachtung.vertragsnummer} löschen',
}
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)

View File

@@ -331,6 +331,13 @@
<i class="fas fa-plus me-2"></i>Neue Länderei <i class="fas fa-plus me-2"></i>Neue Länderei
</a></li> </a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'stiftung:verpachtung_list' %}">
<i class="fas fa-handshake me-2"></i>Alle Verpachtungen
</a></li>
<li><a class="dropdown-item" href="{% url 'stiftung:verpachtung_create' %}">
<i class="fas fa-plus me-2"></i>Neue Verpachtung
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'stiftung:land_abrechnung_list' %}"> <li><a class="dropdown-item" href="{% url 'stiftung:land_abrechnung_list' %}">
<i class="fas fa-calculator me-2"></i>Abrechnungen <i class="fas fa-calculator me-2"></i>Abrechnungen
</a></li> </a></li>

View File

@@ -229,6 +229,12 @@
<span>Backup & Restore</span> <span>Backup & Restore</span>
</a> </a>
</div> </div>
<div class="col-md-3 mb-3">
<a href="{% url 'stiftung:csv_import_list' %}" class="btn btn-outline-info w-100">
<i class="fas fa-upload d-block mb-2 fa-2x"></i>
<span>CSV Import</span>
</a>
</div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<a href="{% url 'stiftung:unterstuetzungen_list' %}" class="btn btn-outline-success w-100"> <a href="{% url 'stiftung:unterstuetzungen_list' %}" class="btn btn-outline-success w-100">
<i class="fas fa-hand-holding-usd d-block mb-2 fa-2x"></i> <i class="fas fa-hand-holding-usd d-block mb-2 fa-2x"></i>

View File

@@ -1,11 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}{{ title }} - {{ block.super }}{% endblock %} {% block title %}{{ title }} - Stiftung Management{% endblock %}
{% block content %} {% block content %}
<div class="row">
<div class="row">
<div class="col-lg-8 mx-auto"> <div class="col-lg-8 mx-auto">
<div class="card shadow"> <div class="card shadow">
<div class="card-header"> <div class="card-header">
@@ -17,6 +16,39 @@
<form method="post"> <form method="post">
{% csrf_token %} {% 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="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="{{ form.paperless_document_id.id_for_label }}" class="form-label"> <label for="{{ form.paperless_document_id.id_for_label }}" class="form-label">
@@ -71,40 +103,13 @@
</div> </div>
<!-- Versteckte Verknüpfungsfelder --> <!-- Versteckte Verknüpfungsfelder -->
{{ form.land_verpachtung_id }}
{{ form.verpachtung_id }} {{ form.verpachtung_id }}
{{ form.land_id }} {{ form.land_id }}
{{ form.paechter_id }} {{ form.paechter_id }}
{{ form.destinataer_id }} {{ form.destinataer_id }}
{{ form.foerderung_id }} {{ form.foerderung_id }}
<!-- Verknüpfungsinfo anzeigen -->
{% if 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 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="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{% url 'stiftung:dokument_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'stiftung:dokument_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Zurück zur Liste <i class="fas fa-arrow-left me-1"></i>Zurück zur Liste
@@ -118,8 +123,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}

View File

@@ -0,0 +1,81 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ title }} - 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>Verpachtung 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 die Verpachtung
<strong>{{ verpachtung.vertragsnummer }}</strong>
löschen möchten?
</p>
</div>
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title text-primary">
<i class="fas fa-info-circle me-2"></i>Verpachtungsdetails
</h6>
<div class="row">
<div class="col-md-6">
<p><strong>Vertragsnummer:</strong><br>{{ verpachtung.vertragsnummer }}</p>
<p><strong>Land:</strong><br>{{ verpachtung.land.bezeichnung }}</p>
<p><strong>Pächter:</strong><br>{{ verpachtung.paechter.get_full_name }}</p>
</div>
<div class="col-md-6">
<p><strong>Pachtzeit:</strong><br>
{{ verpachtung.pachtbeginn|date:"d.m.Y" }}
{% if verpachtung.pachtende %} - {{ verpachtung.pachtende|date:"d.m.Y" }}{% else %} (unbefristet){% endif %}
</p>
<p><strong>Pachtzins:</strong><br>€{{ verpachtung.pachtzins_pauschal|floatformat:2 }} / Jahr</p>
<p><strong>Status:</strong><br>{{ verpachtung.get_status_display }}</p>
</div>
</div>
</div>
</div>
<div class="alert alert-info">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Wichtige Hinweise:</h6>
<ul class="list-unstyled mb-0 text-start">
<li><i class="fas fa-arrow-right me-2"></i>Alle verknüpften Dokumente bleiben erhalten</li>
<li><i class="fas fa-arrow-right me-2"></i>Abrechnungsdaten werden entsprechend aktualisiert</li>
<li><i class="fas fa-arrow-right me-2"></i>Diese Aktion kann nicht rückgängig gemacht werden</li>
<li><i class="fas fa-arrow-right me-2"></i>Bitte stellen Sie sicher, dass alle Daten gesichert sind</li>
</ul>
</div>
<p class="text-muted">
Diese Aktion kann nicht rückgängig gemacht werden. Alle zugehörigen Daten werden permanent gelöscht.
</p>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.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 %}

View File

@@ -0,0 +1,378 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-10 mx-auto">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="fas fa-handshake text-primary me-2"></i>
Verpachtung: {{ verpachtung.vertragsnummer }}
</h1>
<div class="btn-group" role="group">
<a href="{% url 'stiftung:verpachtung_update' verpachtung.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-1"></i>Bearbeiten
</a>
<a href="{% url 'stiftung:verpachtung_delete' verpachtung.pk %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i>Löschen
</a>
</div>
</div>
<!-- Status Badge -->
<div class="row mb-4">
<div class="col-12">
{% if verpachtung.status == 'aktiv' %}
{% if verpachtung.is_aktiv %}
<span class="badge bg-success fs-6 me-2">
<i class="fas fa-check me-1"></i>Aktiv
</span>
{% else %}
<span class="badge bg-warning fs-6 me-2">
<i class="fas fa-clock me-1"></i>Inaktiv
</span>
{% endif %}
{% elif verpachtung.status == 'beendet' %}
<span class="badge bg-secondary fs-6 me-2">
<i class="fas fa-stop me-1"></i>Beendet
</span>
{% elif verpachtung.status == 'gekuendigt' %}
<span class="badge bg-danger fs-6 me-2">
<i class="fas fa-times me-1"></i>Gekündigt
</span>
{% elif verpachtung.status == 'verlaengert' %}
<span class="badge bg-info fs-6 me-2">
<i class="fas fa-refresh me-1"></i>Verlängert
</span>
{% endif %}
{% if verpachtung.verlaengerung_klausel %}
<span class="badge bg-info fs-6 me-2">
<i class="fas fa-sync me-1"></i>Auto-Verlängerung
</span>
{% endif %}
{% if verpachtung.ust_option %}
<span class="badge bg-warning fs-6">
<i class="fas fa-percent me-1"></i>USt-Option
</span>
{% endif %}
</div>
</div>
<div class="row">
<!-- Grunddaten -->
<div class="col-lg-6">
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>Grunddaten
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Vertragsnummer:</strong><br>{{ verpachtung.vertragsnummer }}</p>
<p><strong>Status:</strong><br>{{ verpachtung.get_status_display }}</p>
</div>
<div class="col-md-6">
<p><strong>Erstellt:</strong><br>{{ verpachtung.erstellt_am|date:"d.m.Y H:i" }}</p>
<p><strong>Aktualisiert:</strong><br>{{ verpachtung.aktualisiert_am|date:"d.m.Y H:i" }}</p>
</div>
</div>
</div>
</div>
<!-- Land -->
<div class="card shadow mb-4">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">
<i class="fas fa-map-marked-alt me-2"></i>Länderei
</h5>
</div>
<div class="card-body">
<h6 class="text-primary">
<a href="{% url 'stiftung:land_detail' verpachtung.land.pk %}"
class="text-decoration-none">
{{ verpachtung.land.bezeichnung }}
</a>
</h6>
<div class="row">
<div class="col-md-6">
{% if verpachtung.land.flur %}
<p><strong>Flur:</strong> {{ verpachtung.land.flur }}</p>
{% endif %}
{% if verpachtung.land.flurstueck %}
<p><strong>Flurstück:</strong> {{ verpachtung.land.flurstueck }}</p>
{% endif %}
</div>
<div class="col-md-6">
<p><strong>Gesamtfläche:</strong><br>{{ verpachtung.land.groesse_qm|floatformat:0 }} qm ({{ verpachtung.land.groesse_hektar|floatformat:2 }} ha)</p>
</div>
</div>
{% if verpachtung.land.lage %}
<p><strong>Lage:</strong><br>{{ verpachtung.land.lage }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Pächter -->
<div class="col-lg-6">
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-user me-2"></i>Pächter
</h5>
</div>
<div class="card-body">
<h6 class="text-primary">
<a href="{% url 'stiftung:paechter_detail' verpachtung.paechter.pk %}"
class="text-decoration-none">
{{ verpachtung.paechter.get_full_name }}
</a>
</h6>
<div class="row">
<div class="col-md-6">
{% if verpachtung.paechter.email %}
<p><strong>E-Mail:</strong><br>
<a href="mailto:{{ verpachtung.paechter.email }}">{{ verpachtung.paechter.email }}</a>
</p>
{% endif %}
{% if verpachtung.paechter.telefon %}
<p><strong>Telefon:</strong><br>{{ verpachtung.paechter.telefon }}</p>
{% endif %}
</div>
<div class="col-md-6">
{% if verpachtung.paechter.strasse or verpachtung.paechter.plz or verpachtung.paechter.ort %}
<p><strong>Adresse:</strong><br>
{% if verpachtung.paechter.strasse %}{{ verpachtung.paechter.strasse }}<br>{% endif %}
{% if verpachtung.paechter.plz %}{{ verpachtung.paechter.plz }} {% endif %}{{ verpachtung.paechter.ort }}
</p>
{% endif %}
</div>
</div>
<p><strong>Typ:</strong> {{ verpachtung.paechter.get_personentyp_display }}</p>
</div>
</div>
<!-- Pachtzeit -->
<div class="card shadow mb-4">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">
<i class="fas fa-calendar me-2"></i>Pachtzeit
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Pachtbeginn:</strong><br>{{ verpachtung.pachtbeginn|date:"d.m.Y" }}</p>
{% if verpachtung.pachtende %}
<p><strong>Pachtende:</strong><br>{{ verpachtung.pachtende|date:"d.m.Y" }}</p>
{% else %}
<p><strong>Pachtende:</strong><br><span class="text-success">Unbefristet</span></p>
{% endif %}
</div>
<div class="col-md-6">
<p><strong>Auto-Verlängerung:</strong><br>
{% if verpachtung.verlaengerung_klausel %}
<span class="badge bg-success">Ja</span>
{% else %}
<span class="badge bg-secondary">Nein</span>
{% endif %}
</p>
{% if verpachtung.get_restlaufzeit_tage %}
<p><strong>Restlaufzeit:</strong><br>{{ verpachtung.get_restlaufzeit_tage }} Tage</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Finanzielle Details -->
<div class="row">
<div class="col-lg-6">
<div class="card shadow mb-4">
<div class="card-header bg-secondary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-euro-sign me-2"></i>Pachtzins
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Pachtzins pauschal:</strong><br>
<span class="text-success fw-bold fs-5">€{{ verpachtung.pachtzins_pauschal|floatformat:2 }}</span> / Jahr
</p>
{% if verpachtung.pachtzins_pro_ha %}
<p><strong>Pachtzins pro ha:</strong><br>€{{ verpachtung.pachtzins_pro_ha|floatformat:2 }} / ha</p>
{% endif %}
</div>
<div class="col-md-6">
<p><strong>Zahlungsweise:</strong><br>{{ verpachtung.get_zahlungsweise_display }}</p>
<p><strong>Verpachtete Fläche:</strong><br>
{{ verpachtung.verpachtete_flaeche|floatformat:0 }} qm<br>
<small class="text-muted">({{ verpachtung.verpachtete_flaeche_hektar|floatformat:2 }} ha)</small>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow mb-4">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">
<i class="fas fa-percent me-2"></i>Steuern & Umlagen
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>USt-Option:</strong><br>
{% if verpachtung.ust_option %}
<span class="badge bg-warning">Ja ({{ verpachtung.ust_satz }}%)</span>
{% if verpachtung.ust_pacht_betrag %}
<br><small class="text-muted">USt-Betrag: €{{ verpachtung.ust_pacht_betrag|floatformat:2 }}</small>
{% endif %}
{% else %}
<span class="badge bg-secondary">Nein</span>
{% endif %}
</p>
</div>
<div class="col-md-6">
<p><strong>Umlagefähig:</strong></p>
<ul class="list-unstyled">
<li>
<i class="fas fa-{% if verpachtung.grundsteuer_umlage %}check text-success{% else %}times text-danger{% endif %} me-1"></i>
Grundsteuer
</li>
<li>
<i class="fas fa-{% if verpachtung.versicherungen_umlage %}check text-success{% else %}times text-danger{% endif %} me-1"></i>
Versicherungen
</li>
<li>
<i class="fas fa-{% if verpachtung.verbandsbeitraege_umlage %}check text-success{% else %}times text-danger{% endif %} me-1"></i>
Verbandsbeiträge
</li>
<li>
<i class="fas fa-{% if verpachtung.jagdpacht_anteil_umlage %}check text-success{% else %}times text-danger{% endif %} me-1"></i>
Jagdpachtanteile
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bemerkungen -->
{% if verpachtung.bemerkungen %}
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-sticky-note me-2"></i>Bemerkungen
</h5>
</div>
<div class="card-body">
<p>{{ verpachtung.bemerkungen|linebreaks }}</p>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Verknüpfte Dokumente -->
{% 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="col-12">
<div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-paperclip me-2"></i>Dokumente verwalten
</h6>
<a href="{% url 'stiftung:dokument_create' %}?land_verpachtung_id={{ verpachtung.pk }}"
class="btn btn-sm btn-success">
<i class="fas fa-plus me-1"></i>Dokument verknüpfen
</a>
</div>
<div class="card-body">
{% if verknuepfte_dokumente %}
<p class="text-muted mb-3">{{ verknuepfte_dokumente.count }} Dokument{{ verknuepfte_dokumente.count|pluralize:"e" }} verknüpft</p>
{% else %}
<p class="text-muted mb-0">
<i class="fas fa-info-circle me-2"></i>
Noch keine Dokumente mit dieser Verpachtung verknüpft.
Klicken Sie auf "Dokument verknüpfen", um Dokumente aus dem Paperless-System zu verknüpfen.
</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Navigation -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Zurück zur Liste
</a>
<a href="{% url 'stiftung:verpachtung_update' verpachtung.pk %}" class="btn btn-primary">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,421 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
{% block content %}
<!-- Header -->
<div class="row mb-4">
<div class="col-md-8">
<h1 class="h3">
<i class="fas fa-handshake text-success me-2"></i>
{{ title }}
</h1>
<p class="text-muted">
{% if form.instance.land %}Länderei: {{ form.instance.land }}{% endif %}
{% if form.instance.paechter %} | Pächter: {{ form.instance.paechter.get_full_name }}{% endif %}
</p>
</div>
<div class="col-md-4 text-end">
<a href="{% if form.instance.pk %}{% url 'stiftung:verpachtung_detail' form.instance.pk %}{% else %}{% url 'stiftung:verpachtung_list' %}{% endif %}"
class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Abbrechen
</a>
</div>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="row">
<!-- Main Form -->
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-user-tie me-2"></i>Verpachtungsdetails
</h6>
</div>
<div class="card-body">
<!-- Pächter und Grunddaten -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.land.id_for_label }}" class="form-label">
<i class="fas fa-map-marked-alt me-1"></i>Länderei *
</label>
{{ form.land }}
{% if form.land.errors %}
<div class="invalid-feedback d-block">
{{ form.land.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.paechter.id_for_label }}" class="form-label">
<i class="fas fa-user me-1"></i>Pächter *
</label>
{{ form.paechter }}
{% if form.paechter.errors %}
<div class="invalid-feedback d-block">
{{ form.paechter.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Vertragsnummer und Status -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.vertragsnummer.id_for_label }}" class="form-label">
<i class="fas fa-file-contract me-1"></i>Vertragsnummer *
</label>
{{ form.vertragsnummer }}
{% if form.vertragsnummer.errors %}
<div class="invalid-feedback d-block">
{{ form.vertragsnummer.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.status.id_for_label }}" class="form-label">
<i class="fas fa-flag me-1"></i>Status
</label>
{{ form.status }}
{% if form.status.errors %}
<div class="invalid-feedback d-block">
{{ form.status.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Pachtzeiten -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.pachtbeginn.id_for_label }}" class="form-label">
<i class="fas fa-calendar me-1"></i>Pachtbeginn *
</label>
{{ form.pachtbeginn }}
{% if form.pachtbeginn.errors %}
<div class="invalid-feedback d-block">
{{ form.pachtbeginn.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.pachtende.id_for_label }}" class="form-label">
<i class="fas fa-calendar-times me-1"></i>Pachtende
</label>
{{ form.pachtende }}
{% if form.pachtende.errors %}
<div class="invalid-feedback d-block">
{{ form.pachtende.errors.0 }}
</div>
{% endif %}
<div class="form-text">Leer lassen für unbefristete Verpachtung</div>
</div>
</div>
</div>
<!-- Automatische Verlängerung -->
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<div class="form-check">
{{ form.verlaengerung_klausel }}
<label class="form-check-label" for="{{ form.verlaengerung_klausel.id_for_label }}">
<i class="fas fa-refresh me-1"></i>Automatische Verlängerung
</label>
</div>
{% if form.verlaengerung_klausel.errors %}
<div class="invalid-feedback d-block">
{{ form.verlaengerung_klausel.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Flächeninformationen -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.verpachtete_flaeche.id_for_label }}" class="form-label">
<i class="fas fa-expand-arrows-alt me-1"></i>Verpachtete Fläche (qm) *
</label>
{{ form.verpachtete_flaeche }}
{% if form.verpachtete_flaeche.errors %}
<div class="invalid-feedback d-block">
{{ form.verpachtete_flaeche.errors.0 }}
</div>
{% endif %}
<div class="form-text">
<span id="verpachtete_flaeche_ha">0,00 ha</span>
</div>
</div>
</div>
</div>
<!-- Pachtzins -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.pachtzins_pauschal.id_for_label }}" class="form-label">
<i class="fas fa-euro-sign me-1"></i>Pachtzins pauschal/Jahr (€) *
</label>
{{ form.pachtzins_pauschal }}
{% if form.pachtzins_pauschal.errors %}
<div class="invalid-feedback d-block">
{{ form.pachtzins_pauschal.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.pachtzins_pro_ha.id_for_label }}" class="form-label">
<i class="fas fa-euro-sign me-1"></i>Pachtzins pro ha (€)
</label>
{{ form.pachtzins_pro_ha }}
{% if form.pachtzins_pro_ha.errors %}
<div class="invalid-feedback d-block">
{{ form.pachtzins_pro_ha.errors.0 }}
</div>
{% endif %}
<div class="form-text">Wird automatisch berechnet</div>
</div>
</div>
</div>
<!-- Zahlungsweise -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.zahlungsweise.id_for_label }}" class="form-label">
<i class="fas fa-calendar-check me-1"></i>Zahlungsweise
</label>
{{ form.zahlungsweise }}
{% if form.zahlungsweise.errors %}
<div class="invalid-feedback d-block">
{{ form.zahlungsweise.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Umsatzsteuer -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
{{ form.ust_option }}
<label class="form-check-label" for="{{ form.ust_option.id_for_label }}">
<i class="fas fa-percent me-1"></i>USt-Option
</label>
</div>
{% if form.ust_option.errors %}
<div class="invalid-feedback d-block">
{{ form.ust_option.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ust_satz.id_for_label }}" class="form-label">
<i class="fas fa-percent me-1"></i>USt-Satz (%)
</label>
{{ form.ust_satz }}
{% if form.ust_satz.errors %}
<div class="invalid-feedback d-block">
{{ form.ust_satz.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Umlagen -->
<h6 class="mt-4 mb-3">
<i class="fas fa-share-alt me-2"></i>Umlagefähige Kosten
</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
{{ form.grundsteuer_umlage }}
<label class="form-check-label" for="{{ form.grundsteuer_umlage.id_for_label }}">
<i class="fas fa-home me-1"></i>Grundsteuer umlagefähig
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
{{ form.versicherungen_umlage }}
<label class="form-check-label" for="{{ form.versicherungen_umlage.id_for_label }}">
<i class="fas fa-shield-alt me-1"></i>Versicherungen umlagefähig
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
{{ form.verbandsbeitraege_umlage }}
<label class="form-check-label" for="{{ form.verbandsbeitraege_umlage.id_for_label }}">
<i class="fas fa-users me-1"></i>Verbandsbeiträge umlagefähig
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
{{ form.jagdpacht_anteil_umlage }}
<label class="form-check-label" for="{{ form.jagdpacht_anteil_umlage.id_for_label }}">
<i class="fas fa-tree me-1"></i>Jagdpachtanteile umlagefähig
</label>
</div>
</div>
</div>
</div>
<!-- Bemerkungen -->
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label for="{{ form.bemerkungen.id_for_label }}" class="form-label">
<i class="fas fa-sticky-note me-1"></i>Bemerkungen
</label>
{{ form.bemerkungen }}
{% if form.bemerkungen.errors %}
<div class="invalid-feedback d-block">
{{ form.bemerkungen.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Aktionen -->
<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-cogs me-2"></i>Aktionen
</h6>
</div>
<div class="card-body">
<button type="submit" class="btn btn-primary btn-block mb-2">
<i class="fas fa-save me-2"></i>Verpachtung speichern
</button>
<a href="{% if form.instance.pk %}{% url 'stiftung:verpachtung_detail' form.instance.pk %}{% else %}{% url 'stiftung:verpachtung_list' %}{% endif %}"
class="btn btn-outline-secondary btn-block">
<i class="fas fa-times me-2"></i>Abbrechen
</a>
</div>
</div>
{% if form.instance.pk %}
<!-- Dokumente -->
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-paperclip me-2"></i>Dokumente verknüpfen
</h6>
</div>
<div class="card-body">
<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 }}"
class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>Neues Dokument
</a>
</div>
</div>
{% else %}
<!-- Info Card -->
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-info">
<i class="fas fa-info-circle me-2"></i>Hinweis
</h6>
</div>
<div class="card-body">
<p class="text-muted small">
Nach dem Erstellen der Verpachtung können Dokumente verknüpft und weitere Details bearbeitet werden.
</p>
</div>
</div>
{% endif %}
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-calculate Pachtzins pro Hektar and Fläche in Hektar
const pachtzinsPauschal = document.getElementById('{{ form.pachtzins_pauschal.id_for_label }}');
const pachtzinsProHa = document.getElementById('{{ form.pachtzins_pro_ha.id_for_label }}');
const verpachteteFlaeche = document.getElementById('{{ form.verpachtete_flaeche.id_for_label }}');
const verpachteteFlaeheHa = document.getElementById('verpachtete_flaeche_ha');
function updateCalculations() {
const flaeche = parseFloat(verpachteteFlaeche.value) || 0;
const pachtzins = parseFloat(pachtzinsPauschal.value) || 0;
// Update Hektar display
const hektar = flaeche / 10000;
verpachteteFlaeheHa.textContent = hektar.toFixed(2) + ' ha';
// Calculate Pachtzins pro Hektar
if (hektar > 0) {
const pachtzinsPerHa = pachtzins / hektar;
pachtzinsProHa.value = pachtzinsPerHa.toFixed(2);
} else {
pachtzinsProHa.value = '';
}
}
// Attach event listeners
if (verpachteteFlaeche) verpachteteFlaeche.addEventListener('input', updateCalculations);
if (pachtzinsPauschal) pachtzinsPauschal.addEventListener('input', updateCalculations);
// Initial calculation
updateCalculations();
// USt-Satz handling
const ustOption = document.getElementById('{{ form.ust_option.id_for_label }}');
const ustSatz = document.getElementById('{{ form.ust_satz.id_for_label }}');
function toggleUstSatz() {
if (ustSatz) {
ustSatz.disabled = !ustOption.checked;
if (!ustOption.checked) {
ustSatz.value = '19.00';
}
}
}
if (ustOption) {
ustOption.addEventListener('change', toggleUstSatz);
toggleUstSatz(); // Initial state
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,244 @@
{% 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-handshake text-primary me-2"></i>
{{ title }}
</h1>
<a href="{% url 'stiftung:verpachtung_create' %}" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Neue Verpachtung
</a>
</div>
<!-- Filters -->
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-filter me-2"></i>Filter und Suche
</h6>
</div>
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-4">
<label for="search" class="form-label">Suche</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search_query }}"
placeholder="Vertragsnummer, Land, Pächter...">
</div>
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">Alle Status</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if status_filter == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="year" class="form-label">Jahr</label>
<input type="number" class="form-control" id="year" name="year"
value="{{ year_filter }}"
placeholder="2024" min="1900" max="2100">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary me-2">
<i class="fas fa-search me-2"></i>Filtern
</button>
<a href="{% url 'stiftung:verpachtung_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Zurücksetzen
</a>
</div>
</form>
</div>
</div>
<!-- Verpachtungen Table -->
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-list me-2"></i>Verpachtungen ({{ page_obj.paginator.count }})
</h6>
</div>
<div class="card-body">
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Vertragsnummer</th>
<th>Land</th>
<th>Pächter</th>
<th>Pachtzeit</th>
<th>Fläche</th>
<th>Pachtzins</th>
<th>Status</th>
<th width="120">Aktionen</th>
</tr>
</thead>
<tbody>
{% for verpachtung in page_obj %}
<tr>
<td>
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.pk %}"
class="text-decoration-none fw-bold">
{{ verpachtung.vertragsnummer }}
</a>
</td>
<td>
<a href="{% url 'stiftung:land_detail' verpachtung.land.pk %}"
class="text-decoration-none">
{{ verpachtung.land.bezeichnung }}
</a>
{% if verpachtung.land.flur %}
<br><small class="text-muted">Flur {{ verpachtung.land.flur }}</small>
{% endif %}
</td>
<td>
<a href="{% url 'stiftung:paechter_detail' verpachtung.paechter.pk %}"
class="text-decoration-none">
{{ verpachtung.paechter.get_full_name }}
</a>
{% if verpachtung.paechter.ort %}
<br><small class="text-muted">{{ verpachtung.paechter.ort }}</small>
{% endif %}
</td>
<td>
{{ verpachtung.pachtbeginn|date:"d.m.Y" }}
{% if verpachtung.pachtende %}
<br><small class="text-muted">bis {{ verpachtung.pachtende|date:"d.m.Y" }}</small>
{% else %}
<br><small class="text-success">Unbefristet</small>
{% endif %}
{% if verpachtung.verlaengerung_klausel %}
<br><small class="badge bg-info">Auto-Verlängerung</small>
{% endif %}
</td>
<td>
{{ verpachtung.verpachtete_flaeche|floatformat:0 }} qm
<br><small class="text-muted">({{ verpachtung.verpachtete_flaeche_hektar|floatformat:2 }} ha)</small>
</td>
<td>
<strong class="text-success">€{{ verpachtung.pachtzins_pauschal|floatformat:2 }}</strong>
{% if verpachtung.pachtzins_pro_ha %}
<br><small class="text-muted">€{{ verpachtung.pachtzins_pro_ha|floatformat:2 }}/ha</small>
{% endif %}
{% if verpachtung.ust_option %}
<br><small class="badge bg-warning">+ {{ verpachtung.ust_satz }}% USt</small>
{% endif %}
</td>
<td>
{% if verpachtung.status == 'aktiv' %}
{% if verpachtung.is_aktiv %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-warning">Inaktiv</span>
{% endif %}
{% elif verpachtung.status == 'beendet' %}
<span class="badge bg-secondary">Beendet</span>
{% elif verpachtung.status == 'gekuendigt' %}
<span class="badge bg-danger">Gekündigt</span>
{% elif verpachtung.status == 'verlaengert' %}
<span class="badge bg-info">Verlängert</span>
{% else %}
<span class="badge bg-secondary">{{ verpachtung.get_status_display }}</span>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'stiftung:verpachtung_detail' verpachtung.pk %}"
class="btn btn-sm btn-outline-primary" title="Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'stiftung:verpachtung_update' verpachtung.pk %}"
class="btn btn-sm btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'stiftung:verpachtung_delete' verpachtung.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>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Verpachtungen Pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if year_filter %}&year={{ year_filter }}{% endif %}">
<i class="fas fa-angle-double-left"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if year_filter %}&year={{ year_filter }}{% endif %}">
<i class="fas fa-angle-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if year_filter %}&year={{ year_filter }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if year_filter %}&year={{ year_filter }}{% endif %}">
<i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if year_filter %}&year={{ year_filter }}{% endif %}">
<i class="fas fa-angle-double-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-4">
<i class="fas fa-handshake fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Keine Verpachtungen gefunden</h5>
<p class="text-muted">
{% if search_query or status_filter or year_filter %}
Keine Verpachtungen entsprechen den angegebenen Filterkriterien.
{% else %}
Es sind noch keine Verpachtungen erfasst.
{% endif %}
</p>
<a href="{% url 'stiftung:verpachtung_create' %}" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Erste Verpachtung erstellen
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

82
compose.dev.yml Normal file
View File

@@ -0,0 +1,82 @@
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: stiftung_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres_dev
volumes:
- dbdata_dev:/var/lib/postgresql/data
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d stiftung_dev"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6380:6379"
web:
build: ./app
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
- POSTGRES_DB=stiftung_dev
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres_dev
- DB_HOST=db
- DB_PORT=5432
- DJANGO_SECRET_KEY=dev-secret-key-not-for-production
- DJANGO_DEBUG=1
- DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
- LANGUAGE_CODE=de
- TIME_ZONE=Europe/Berlin
- REDIS_URL=redis://redis:6379/0
- PAPERLESS_API_URL=http://paperless:8000
- PAPERLESS_API_TOKEN=dev-token-will-be-set-after-paperless-setup
ports:
- "8081:8000"
volumes:
- ./app:/app
command: ["python", "manage.py", "runserver", "0.0.0.0:8000"]
paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
ports:
- "8082:8000"
environment:
- PAPERLESS_REDIS=redis://redis:6379
- PAPERLESS_DBHOST=db
- PAPERLESS_DBPORT=5432
- PAPERLESS_DBNAME=stiftung_dev
- PAPERLESS_DBUSER=postgres
- PAPERLESS_DBPASS=postgres_dev
- PAPERLESS_SECRET_KEY=dev-paperless-secret-key
- PAPERLESS_URL=http://localhost:8082
- PAPERLESS_ALLOWED_HOSTS=localhost,127.0.0.1
- PAPERLESS_CORS_ALLOWED_HOSTS=http://localhost:8082,http://localhost:8081
- PAPERLESS_ADMIN_USER=admin
- PAPERLESS_ADMIN_PASSWORD=admin123
- PAPERLESS_ADMIN_MAIL=admin@localhost
volumes:
- paperless_data_dev:/usr/src/paperless/data
- paperless_media_dev:/usr/src/paperless/media
- paperless_export_dev:/usr/src/paperless/export
- paperless_consume_dev:/usr/src/paperless/consume
depends_on:
- db
- redis
volumes:
dbdata_dev:
paperless_data_dev:
paperless_media_dev:
paperless_export_dev:
paperless_consume_dev:

View File

@@ -1,3 +1,11 @@
# Production Docker Compose Configuration
# This file is used for production deployment via GitHub Actions
# For local development, use: docker-compose -f compose.dev.yml up
#
# IMPORTANT: This configuration requires ALL environment variables to be
# provided via the production server's .env file. No fallback values are
# included for security reasons.
services: services:
db: db:
image: postgres:16-alpine image: postgres:16-alpine
@@ -8,7 +16,7 @@ services:
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
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
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -23,7 +31,20 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_started condition: service_started
env_file: ./app/.env environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- DJANGO_ALLOWED_HOSTS=${DJANGO_ALLOWED_HOSTS}
- LANGUAGE_CODE=${LANGUAGE_CODE}
- TIME_ZONE=${TIME_ZONE}
- REDIS_URL=${REDIS_URL}
- PAPERLESS_API_URL=${PAPERLESS_API_URL}
- PAPERLESS_API_TOKEN=${PAPERLESS_API_TOKEN}
ports: ports:
- "8081:8000" - "8081:8000"
volumes: volumes:
@@ -32,7 +53,15 @@ services:
worker: worker:
build: ./app build: ./app
env_file: ./app/.env environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- REDIS_URL=${REDIS_URL}
depends_on: depends_on:
- redis - redis
- db - db
@@ -40,7 +69,15 @@ services:
beat: beat:
build: ./app build: ./app
env_file: ./app/.env environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- REDIS_URL=${REDIS_URL}
depends_on: depends_on:
- redis - redis
- db - db
@@ -76,9 +113,9 @@ services:
- PAPERLESS_STATIC_URL=/paperless/static/ - PAPERLESS_STATIC_URL=/paperless/static/
- PAPERLESS_LOGIN_REDIRECT_URL=/paperless/ - PAPERLESS_LOGIN_REDIRECT_URL=/paperless/
- PAPERLESS_LOGOUT_REDIRECT_URL=/paperless/ - PAPERLESS_LOGOUT_REDIRECT_URL=/paperless/
- PAPERLESS_ADMIN_USER=${PAPERLESS_ADMIN_USER:-admin} - PAPERLESS_ADMIN_USER=${PAPERLESS_ADMIN_USER}
- PAPERLESS_ADMIN_PASSWORD=${PAPERLESS_ADMIN_PASSWORD:-admin123} - PAPERLESS_ADMIN_PASSWORD=${PAPERLESS_ADMIN_PASSWORD}
- PAPERLESS_ADMIN_MAIL=${PAPERLESS_ADMIN_MAIL:-admin@vhtv-stiftung.de} - PAPERLESS_ADMIN_MAIL=${PAPERLESS_ADMIN_MAIL}
volumes: volumes:
- paperless_data:/usr/src/paperless/data - paperless_data:/usr/src/paperless/data
- paperless_media:/usr/src/paperless/media - paperless_media:/usr/src/paperless/media