diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py index 8cc80a04d14c7f020298770d11483580a94291cb..d5d4ab4b39f315c02aa4e68e506cbcb2c3127605 100644 --- a/aleksis/apps/tezor/models/invoice.py +++ b/aleksis/apps/tezor/models/invoice.py @@ -33,6 +33,13 @@ class InvoiceGroup(ExtensibleModel): class Invoice(BasePayment, PureDjangoModel): + VARIANT_DISPLAY = { + "paypal": (_("PayPal"), "logos:paypal"), + "sofort": (_("Klarna / Sofort"), "simple-icons:klarna"), + "pledge": (_("Payment pledge / manual payment"), "mdi:hand-coin"), + "sdd": (_("SEPA Direct Debit"), "mdi:bank-transfer"), + } + group = models.ForeignKey( InvoiceGroup, verbose_name=_("Invoice group"), related_name="invoices", on_delete=models.SET_NULL, null=True ) @@ -43,6 +50,12 @@ class Invoice(BasePayment, PureDjangoModel): for_object_id = models.PositiveIntegerField() for_object = GenericForeignKey("for_content_type", "for_object_id") + def get_variant_name(self): + return self.__class__.VARIANT_DISPLAY[self.variant][0] + + def get_variant_icon(self): + return self.__class__.VARIANT_DISPLAY[self.variant][1] + def get_purchased_items(self): return self.for_object.get_purchased_items() @@ -88,7 +101,7 @@ class Invoice(BasePayment, PureDjangoModel): return TotalsTable(values) def get_success_url(self): - return reverse("invoice_by_pk", kwargs={"pk": self.pk}) + return reverse("invoice_by_token", kwargs={"slug": self.token}) def get_failure_url(self): - return reverse("invoice_by_pk", kwargs={"pk": self.pk}) + return reverse("invoice_by_token", kwargs={"slug": self.token}) diff --git a/aleksis/apps/tezor/predicates.py b/aleksis/apps/tezor/predicates.py index 4980926acb10b6d12db668e2601b0cb0443857df..e9fb75deeb3b67e7063152b13105afa77cd7d81d 100644 --- a/aleksis/apps/tezor/predicates.py +++ b/aleksis/apps/tezor/predicates.py @@ -4,6 +4,8 @@ from rules import predicate from .models.invoice import Invoice +User = get_user_model() + @predicate def is_own_invoice(user: User, obj: Invoice): """Predicate which checks if the invoice is linked to the current user.""" @@ -14,10 +16,10 @@ def has_no_payment_variant(user: User, obj: Invoice): """Predicate which checks that the invoice has no payment variant.""" return not obj.variant -@predicate def is_in_payment_status(status: str): """Predicate which checks whether the invoice is in a specific state.""" + @predicate def _predicate(user: User, obj: Invoice): return obj.status == status diff --git a/aleksis/apps/tezor/rules.py b/aleksis/apps/tezor/rules.py index b82cc5b6d0cbfeb4d105f597231641f9740b6c3f..12a27e3b55a38bbff4d7af450d0b9b757e0a01b6 100644 --- a/aleksis/apps/tezor/rules.py +++ b/aleksis/apps/tezor/rules.py @@ -84,5 +84,8 @@ do_payment_predicate = has_person & (is_in_payment_status(PaymentStatus.WAITING) rules.add_perm("tezor.do_payment", do_payment_predicate) # View invoice -view_invoice_predicate = is_own_invoice | is_site_preference_set("payments", "public_payments") | has_global_perm("tezor.view_invoice") | has_object_perm("tezor.view_invoice") +view_invoice_predicate = has_person & is_own_invoice | is_site_preference_set("payments", "public_payments") | has_global_perm("tezor.view_invoice") | has_object_perm("tezor.view_invoice") rules.add_perm("tezor.view_invoice_rule", view_invoice_predicate) + +print_invoice_predicate = (view_invoice_predicate & display_billing_predicate & display_purchased_items_predicate) +rules.add_perm("tezor.print_invoice_rule", print_invoice_predicate) diff --git a/aleksis/apps/tezor/tables.py b/aleksis/apps/tezor/tables.py index 47660380cc7330a34e46d946c9fce975fe4e1a59..16ed8a0ed491b7eb064c432cfd06950239ba620d 100644 --- a/aleksis/apps/tezor/tables.py +++ b/aleksis/apps/tezor/tables.py @@ -92,14 +92,14 @@ class InvoicesTable(tables.Table): billing_last_name = tables.Column() total = tables.Column() view = tables.LinkColumn( - "invoice_by_pk", - args=[A("id")], + "invoice_by_token", + args=[A("token")], verbose_name=_("View"), text=_("View"), ) print = tables.LinkColumn( "print_invoice", - args=[A("id")], + args=[A("token")], verbose_name=_("Print"), text=_("Print"), ) diff --git a/aleksis/apps/tezor/templates/tezor/invoice/full.html b/aleksis/apps/tezor/templates/tezor/invoice/full.html index d2dd3fe7067fabd0a12166a3bc4f231c1338f66e..7043c5c3bc59224d5d5ec0cdc726cfadced02344 100644 --- a/aleksis/apps/tezor/templates/tezor/invoice/full.html +++ b/aleksis/apps/tezor/templates/tezor/invoice/full.html @@ -1,5 +1,5 @@ {% extends "core/base.html" %} -{% load material_form i18n %} +{% load material_form i18n rules %} {% load render_table from django_tables2 %} @@ -7,10 +7,104 @@ {% block content %} + {% has_perm 'tezor.do_payment' user object as can_do_payment %} + {% has_perm 'tezor.view_invoice_group_rule' user object.group as can_view_invoice_group %} + {% has_perm 'tezor.display_purchased_items_rule' user object as can_view_purchased_items %} + {% has_perm 'tezor.display_billing_rule' user object as can_view_billing_information %} + {% has_perm 'tezor.print_invoice_rule' user object as can_print_invoice %} + <h1>{% trans "Invoice" %} {{ object.number }} — {{ object.created.date }}</h1> - <a class="btn colour-primary waves-effect waves-light" href="{% url 'invoice_group_by_pk' object.group.pk %}">{% trans "Back" %}</a> - {% render_table object.purchased_items_table %} - {% render_table object.totals_table %} + {% if can_view_invoice_group %} + <a class="btn colour-primary waves-effect waves-light" href="{% url 'invoice_group_by_pk' object.group.pk %}">{% trans "Back" %}</a> + {% endif %} + {% if can_print_invoice %} + <a class="btn colour-primary waves-effect waves-light" href="{% url 'print_invoice' object.token %}">{% trans "Print" %}</a> + {% endif %} + + <div class="row"> + {% if can_view_billing_information %} + <div class="col s12 m6"> + <div class="card"> + <div class="card-content"> + <span class="card-title">{% trans "Billing information" %}</span> + <table class="highlight"> + <tr> + <td> + <i class="material-icons small iconify" data-icon="mdi:account-outline"></i> + </td> + <td>{{ object.billing_first_name }} {{object.billing_last_name }}</td> + </tr> + <tr> + <td rowspan="2"> + <i class="material-icons small iconify" data-icon="mdi:map-marker-outline"></i> + </td> + <td>{{ object.billing_address_1 }} {{ object.billing_address_2 }}</td> + </tr> + <tr> + <td>{{ object.billing_postcode }} {{ object.billing_city}}</td> + </tr> + <tr> + <td> + <i class="material-icons small iconify" data-icon="mdi:email-outline"></i> + </td> + <td> + <a href="mailto:{{ object.billing_email }}">{{ object.billing_email }}</a> + </td> + </tr> + </table> + </div> + </div> + </div> + {% endif %} + <div class="col s12 m6"> + <div class="card"> + <div class="card-content"> + <span class="card-title">{% trans "Payment" %}</span> + <table class="highlight"> + <tr> + <td> + <i class="material-icons iconify" data-icon="{{ object.get_variant_icon }}"></i> + </td> + <td> + {{ object.get_variant_name }} + </td> + </tr> + <tr> + <td> + {% if object.status == "waiting" or object.status == "input" %} + <i class="material-icons iconify" data-icon="mdi:cash-lock-open"></i> + {% elif object.status == "rejected" or object.status == "error" %} + <i class="material-icons iconify" data-icon="mdi:cash-remove"></i> + {% elif object.status == "preauth" %} + <i class="material-icons iconfiy" data-icon="mdi:cash-lock"></i> + {% elif object.status == "confirmed" %} + <i class="material-icons iconify" data-icon="mdi:cash-check"></i> + {% elif object.status == "refunded" %} + <i class="material-icons iconify" data-icon="mdi:cash-refund"></i> + {% endif %} + </td> + <td> + {{ object.get_status_display }} + </td> + </tr> + </table> + </div> + {% if object.status == "waiting" or object.status == "rejected" or object.status == "input" and can_do_payment %} + <div class="card-action"> + <a class="btn waves-effect waves-light green" href="{% url 'do_payment' object.token %}"> + <i class="material-icons left iconify" data-icon="mdi:cash-fast"></i> + {% trans "Pay now" %} + </a> + </div> + {% endif %} + </div> + </div> + </div> + + {% if can_view_purchased_items %} + {% render_table object.purchased_items_table %} + {% render_table object.totals_table %} + {% endif %} {% endblock %} diff --git a/aleksis/apps/tezor/urls.py b/aleksis/apps/tezor/urls.py index 54856887d61bf33ec3959d5fc081975a817fa1af..57b92ac76fc5e533d3f2e4ca8dd7a160a9e3a41f 100644 --- a/aleksis/apps/tezor/urls.py +++ b/aleksis/apps/tezor/urls.py @@ -4,8 +4,8 @@ from . import views urlpatterns = [ path("payments/", include("payments.urls")), - path("invoice/<int:pk>/print/", views.GetInvoicePDF.as_view(), name="print_invoice"), - path("invoice/<str:token>/pay", views.do_payment, name="do_payment"), + path("invoice/<str:token>/print/", views.GetInvoicePDF.as_view(), name="print_invoice"), + path("invoice/<str:token>/pay", views.DoPaymentView.as_view(), name="do_payment"), path( "clients/", views.ClientListView.as_view(), @@ -52,8 +52,8 @@ urlpatterns = [ name="delete_invoice_group_by_pk", ), path( - "invoice/<int:pk>/", + "invoice/<str:slug>/", views.InvoiceDetailView.as_view(), - name="invoice_by_pk", + name="invoice_by_token", ), ] diff --git a/aleksis/apps/tezor/views.py b/aleksis/apps/tezor/views.py index 27df0e260bcd2230782829338a781a54b2552044..06a4df8cd04be868ffc0cba7a601bc2c90ef22d3 100644 --- a/aleksis/apps/tezor/views.py +++ b/aleksis/apps/tezor/views.py @@ -21,11 +21,11 @@ from .models.invoice import Invoice, InvoiceGroup class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): - permission_required = "tezor.can_print_invoice" + permission_required = "tezor.print_invoice_rule" def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - invoice = Invoice.objects.get(id=self.kwargs["pk"]) + invoice = Invoice.objects.get(token=self.kwargs["token"]) self.template_name = invoice.group.template_name context["invoice"] = invoice @@ -33,23 +33,29 @@ class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): return context -def do_payment(request, token): - payment = get_object_or_404(get_payment_model(), token=token) +class DoPaymentView(PermissionRequiredMixin, View): - if payment.status not in [PaymentStatus.WAITING, PaymentStatus.INPUT, PaymentStatus.REJECTED]: - return redirect(payment.get_success_url()) + model = Invoice + permission_required = "tezor.do_payment_rule" + template_name = "tezor/invoice/payment.html" + + def dispatch(self, request, token): + self.object = get_object_or_404(self.model, token=token) + + if self.object.status not in [PaymentStatus.WAITING, PaymentStatus.INPUT, PaymentStatus.REJECTED]: + return redirect(self.object.get_success_url()) - try: - form = payment.get_form(data=request.POST or None) - except RedirectNeeded as redirect_to: - return redirect(str(redirect_to)) + try: + form = self.object.get_form(data=request.POST or None) + except RedirectNeeded as redirect_to: + return redirect(str(redirect_to)) - context = { - "form": form, - "payment": payment, - } + context = { + "form": form, + "payment": self.object, + } - return render(request, "tezor/invoice/payment.html", context) + return render(request, self.template_name, context) class ClientListView(PermissionRequiredMixin, SingleTableView): @@ -171,5 +177,6 @@ class InvoiceGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView): class InvoiceDetailView(PermissionRequiredMixin, DetailView): model = Invoice + slug_field = "token" permission_required = "tezor.view_invoice_rule" template_name = "tezor/invoice/full.html"