Skip to content
Snippets Groups Projects
Commit e9498e70 authored by Tom Teichler's avatar Tom Teichler :beers:
Browse files

Merge branch '3-add-payment-processing-ui-2' into 'payment-backends'

Resolve "Add payment processing UI"

See merge request !9
parents 74877fb4 388a9119
No related branches found
No related tags found
2 merge requests!9Resolve "Add payment processing UI",!3Implement payment backends and interaction
Pipeline #59266 failed
...@@ -33,6 +33,13 @@ class InvoiceGroup(ExtensibleModel): ...@@ -33,6 +33,13 @@ class InvoiceGroup(ExtensibleModel):
class Invoice(BasePayment, PureDjangoModel): 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( group = models.ForeignKey(
InvoiceGroup, verbose_name=_("Invoice group"), related_name="invoices", on_delete=models.SET_NULL, null=True InvoiceGroup, verbose_name=_("Invoice group"), related_name="invoices", on_delete=models.SET_NULL, null=True
) )
...@@ -43,6 +50,12 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -43,6 +50,12 @@ class Invoice(BasePayment, PureDjangoModel):
for_object_id = models.PositiveIntegerField() for_object_id = models.PositiveIntegerField()
for_object = GenericForeignKey("for_content_type", "for_object_id") 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): def get_purchased_items(self):
return self.for_object.get_purchased_items() return self.for_object.get_purchased_items()
...@@ -88,7 +101,7 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -88,7 +101,7 @@ class Invoice(BasePayment, PureDjangoModel):
return TotalsTable(values) return TotalsTable(values)
def get_success_url(self): 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): def get_failure_url(self):
return reverse("invoice_by_pk", kwargs={"pk": self.pk}) return reverse("invoice_by_token", kwargs={"slug": self.token})
...@@ -4,6 +4,8 @@ from rules import predicate ...@@ -4,6 +4,8 @@ from rules import predicate
from .models.invoice import Invoice from .models.invoice import Invoice
User = get_user_model()
@predicate @predicate
def is_own_invoice(user: User, obj: Invoice): def is_own_invoice(user: User, obj: Invoice):
"""Predicate which checks if the invoice is linked to the current user.""" """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): ...@@ -14,10 +16,10 @@ def has_no_payment_variant(user: User, obj: Invoice):
"""Predicate which checks that the invoice has no payment variant.""" """Predicate which checks that the invoice has no payment variant."""
return not obj.variant return not obj.variant
@predicate
def is_in_payment_status(status: str): def is_in_payment_status(status: str):
"""Predicate which checks whether the invoice is in a specific state.""" """Predicate which checks whether the invoice is in a specific state."""
@predicate
def _predicate(user: User, obj: Invoice): def _predicate(user: User, obj: Invoice):
return obj.status == status return obj.status == status
......
...@@ -84,5 +84,8 @@ do_payment_predicate = has_person & (is_in_payment_status(PaymentStatus.WAITING) ...@@ -84,5 +84,8 @@ do_payment_predicate = has_person & (is_in_payment_status(PaymentStatus.WAITING)
rules.add_perm("tezor.do_payment", do_payment_predicate) rules.add_perm("tezor.do_payment", do_payment_predicate)
# View invoice # 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) 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)
...@@ -92,14 +92,14 @@ class InvoicesTable(tables.Table): ...@@ -92,14 +92,14 @@ class InvoicesTable(tables.Table):
billing_last_name = tables.Column() billing_last_name = tables.Column()
total = tables.Column() total = tables.Column()
view = tables.LinkColumn( view = tables.LinkColumn(
"invoice_by_pk", "invoice_by_token",
args=[A("id")], args=[A("token")],
verbose_name=_("View"), verbose_name=_("View"),
text=_("View"), text=_("View"),
) )
print = tables.LinkColumn( print = tables.LinkColumn(
"print_invoice", "print_invoice",
args=[A("id")], args=[A("token")],
verbose_name=_("Print"), verbose_name=_("Print"),
text=_("Print"), text=_("Print"),
) )
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load material_form i18n %} {% load material_form i18n rules %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
...@@ -7,10 +7,104 @@ ...@@ -7,10 +7,104 @@
{% block content %} {% 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> <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 %} {% if can_view_invoice_group %}
{% render_table object.totals_table %} <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 %} {% endblock %}
...@@ -4,8 +4,8 @@ from . import views ...@@ -4,8 +4,8 @@ from . import views
urlpatterns = [ urlpatterns = [
path("payments/", include("payments.urls")), path("payments/", include("payments.urls")),
path("invoice/<int:pk>/print/", views.GetInvoicePDF.as_view(), name="print_invoice"), path("invoice/<str:token>/print/", views.GetInvoicePDF.as_view(), name="print_invoice"),
path("invoice/<str:token>/pay", views.do_payment, name="do_payment"), path("invoice/<str:token>/pay", views.DoPaymentView.as_view(), name="do_payment"),
path( path(
"clients/", "clients/",
views.ClientListView.as_view(), views.ClientListView.as_view(),
...@@ -52,8 +52,8 @@ urlpatterns = [ ...@@ -52,8 +52,8 @@ urlpatterns = [
name="delete_invoice_group_by_pk", name="delete_invoice_group_by_pk",
), ),
path( path(
"invoice/<int:pk>/", "invoice/<str:slug>/",
views.InvoiceDetailView.as_view(), views.InvoiceDetailView.as_view(),
name="invoice_by_pk", name="invoice_by_token",
), ),
] ]
...@@ -21,11 +21,11 @@ from .models.invoice import Invoice, InvoiceGroup ...@@ -21,11 +21,11 @@ from .models.invoice import Invoice, InvoiceGroup
class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView):
permission_required = "tezor.can_print_invoice" permission_required = "tezor.print_invoice_rule"
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*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 self.template_name = invoice.group.template_name
context["invoice"] = invoice context["invoice"] = invoice
...@@ -33,23 +33,29 @@ class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): ...@@ -33,23 +33,29 @@ class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView):
return context return context
def do_payment(request, token): class DoPaymentView(PermissionRequiredMixin, View):
payment = get_object_or_404(get_payment_model(), token=token)
if payment.status not in [PaymentStatus.WAITING, PaymentStatus.INPUT, PaymentStatus.REJECTED]: model = Invoice
return redirect(payment.get_success_url()) 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: try:
form = payment.get_form(data=request.POST or None) form = self.object.get_form(data=request.POST or None)
except RedirectNeeded as redirect_to: except RedirectNeeded as redirect_to:
return redirect(str(redirect_to)) return redirect(str(redirect_to))
context = { context = {
"form": form, "form": form,
"payment": payment, "payment": self.object,
} }
return render(request, "tezor/invoice/payment.html", context) return render(request, self.template_name, context)
class ClientListView(PermissionRequiredMixin, SingleTableView): class ClientListView(PermissionRequiredMixin, SingleTableView):
...@@ -171,5 +177,6 @@ class InvoiceGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView): ...@@ -171,5 +177,6 @@ class InvoiceGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
class InvoiceDetailView(PermissionRequiredMixin, DetailView): class InvoiceDetailView(PermissionRequiredMixin, DetailView):
model = Invoice model = Invoice
slug_field = "token"
permission_required = "tezor.view_invoice_rule" permission_required = "tezor.view_invoice_rule"
template_name = "tezor/invoice/full.html" template_name = "tezor/invoice/full.html"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment