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"