Miethai 3: Außenansicht

Obwohl sich die Interaktion mit der Miethai-Anwendung überwiegend in der Administrationsoberfläche abspielt, werden zwei (zugangsbeschränkte) Schnittstellen zur Außenwelt benötigt, eine Webansicht der Nebenkostenabrechnung und ein E-Mail-Versand. Beides ist mit minimalen Aufwand zu realisieren:

# ../landlord/settings.py LOGIN_URL = '/admin/login/' # urls.py urlpatterns = [ path('balance/<balance_id>', views.display_balance, name='display_balance'), path('email/balance/<balance_id>', views.email_balance, name='email_balance'), ] # views.py @login_required def display_balance(request, balance_id): balance = Balance.objects.get(pk=balance_id) balance_entries = BalanceEntry.objects.filter(balance=balance) return render(request, 'index.html', { 'balance': balance, 'balance_entries' : balance_entries }) @login_required def email_balance(request, balance_id): balance = Balance.objects.get(pk=balance_id) balance_entries = BalanceEntry.objects.filter(balance=balance) context = { 'balance': balance, 'balance_entries' : balance_entries } subject = f'Nebenkostenabrechnung {balance.begin.year}' message = loader.get_template('email.txt').render(context, request) html_message = loader.get_template('email.html').render(context, request) from_email = 'wohnung@eden.one' recipient_list = [balance.tenancy_agreement.tenant.email] send_mail(subject, message, from_email, recipient_list, html_message=html_message) return display_balance(request, balance_id)

Das Template index.html enthält eine tabellarische Darstellung der gewählten Abrechnung –

<html lang="de"> <head> <meta charset="utf-8" /> {% block css %}<link rel="stylesheet" href="/static/css/landlord.css" />{% endblock %} </head> <body> {% block email %}{% endblock %} {% block heading %}<h1>Nebenkostenabrechnung {{balance.begin.year}}</h1>{% endblock %} <ul><li><strong>Objekt</strong>: {{balance.tenancy_agreement.unit.building.address}} ({{balance.tenancy_agreement.unit.location}})</li> <li><strong>Mietvertrag</strong>: {{balance.tenancy_agreement.public_id}}</li> <li><strong>Mieter*in</strong>: {{balance.tenancy_agreement.tenant}}</li> <li><strong>Abrechnungsdatum</strong>: {{balance.date}}</li> <li><strong>Abrechnungszeitraum</strong>: {{balance.begin}} – {{balance.end}}</li> </ul> <table><tr><th>Dienst</th><th>Gesamtbetrag</th><th>Anteil (Leistung)</th><th>Anteil (Zeitraum)</th><th>Anteil (Rechnung)</th><th>Betrag</th></tr> {% for entry in balance_entries %} <tr><td>{{entry.service.name}}</td><td>{{entry.invoice_amount_display}}</td><td>{{entry.service_share_display}}</td><td>{% if entry.service.service_type == "CO" %}–{% else %}{{entry.days_share_display}}{% endif %}</td><td>{{entry.invoice_share_display}}</td><td>{{entry.amount_display}}</td></tr> {% endfor %} <tr><td><strong>Gesamtbetrag:</strong></td><td></td><td></td><td></td><td></td><td><strong>{{balance.total_display}}</strong></td></tr> <tr><td>Vorauszahlungen:</td><td></td><td></td><td></td><td></td><td>{{balance.total_payments_display}}</td></tr> <tr><td><strong>{{balance.payment_or_return}}:</strong></td><td></td><td><td></td></td><td></td><td><strong>{{balance.payment_due_display}}</strong></td></tr> </table> <p>{% if balance.payment_due < 0 %}Das <strong>Guthaben von {{balance.payment_due_display}}</strong> wird auf Ihr Konto {{balance.tenancy_agreement.account_iban}} (BIC: {{balance.tenancy_agreement.account_bic}}) überwiesen.{% else %}Bitte überweisen Sie die <strong>Nachzahlung i.H.v. {{balance.payment_due_display}}</strong> auf unser Konto IBAN DE12 3848 3838 2828 3848 (BIC: DEFGMWW).{% endif %}</p> {% block email_link %}{% if balance.tenancy_agreement.tenant.email %}<p><a href="{% url 'email_balance' balance_id=balance.pk %}">Als E-Mail senden</a></p>{% endif %}{% endblock %} </body></html>

– und das Template email.html modifiziert diese Ansicht geringfügig:

{% extends 'index.html' %} {% block css %}<style> [...] </style>{% endblock %} {% block email %}<p>Sehr geehrte*r {{balance.tenancy_agreement.tenant}},<br />anbei erhalten Sie Ihre Nebenkostenabrechnung für das Jahr {{balance.begin.year}}.<br />Freundliche Grüße<br />...</p>{% endblock %} {% block heading %}{% endblock %} {% block email_link %}{% endblock %}

Ein Link zur Webansicht wird durch die Anpassung eines Snippets in der Admin-Ansicht einer Nebenkostenabrechnung eingeblendet:

# templates/admin/services/balance/change_form_object_tools.html {% extends "admin/change_form_object_tools.html" %} {% load i18n admin_urls %} {% block object-tools-items %} <li> <a href="{% url 'display_balance' balance_id=original.pk %}" class="historylink">{% translate "View" %}</a> </li> <li> {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} <a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a> </li> {% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a></li>{% endif %} {% endblock %}

Eine dritte Ansicht dient ausschließlich der Diskretion. Digitale Rechnungen im PDF-Format sind ein optionaler Bestandteil jeder Invoice-Instanz:

class Invoice(models.Model): external_id = models.CharField('Rechnungsnummer', max_length=30) contract = models.ForeignKey(Contract, verbose_name='Vertrag', on_delete=models.CASCADE) date = models.DateField('Rechnungsdatum') begin = models.DateField('Rechnungszeitraum (Beginn)') end = models.DateField('Rechnungszeitraum (Ende)') amount = models.DecimalField('Betrag', max_digits=8, decimal_places=2) consumption = models.DecimalField('Leistung/Verbrauch', max_digits=10, decimal_places=3, blank=True, null=True) counter_consumption = models.DecimalField('Verbrauch laut Zähler', max_digits=10, decimal_places=3, blank=True, null=True) invoice_file = models.FileField('Rechnungskopie', upload_to='invoices', blank=True, null=True)

Die Ablage (und Verlinkung) dieser Dateien wird in settings.py konfiguriert:

MEDIA_ROOT = '/Users/snafu/projects/landlord/media' MEDIA_URL = ''

Damit Nginx diese Dateien nicht ohne Rücksprache mit Django ausliefert, kommt die Direktive internal zum Einsatz:

# nginx.conf location ~ ^/media/ { internal; }

Django seinerseits verarbeitet URLs der Form /invoices/<file_name> und ermöglicht die Anzeige von /media/invoices/<file_name>:

# ../landlord/urls.py from services.views import invoice_file urlpatterns = [ path('services/', include('services.urls')), path('invoices/<file_name>', invoice_file, name="invoice_file"), path('admin/', admin.site.urls), ] # views.py @login_required def invoice_file(request, file_name): response = HttpResponse() response['Content-Type'] = 'application/pdf' response['X-Accel-Redirect'] = '/media/invoices/' + file_name response['Content-Disposition'] = 'inline;filename=' + file_name return response