diff --git a/README.rst b/README.rst
index 980a407509f9611440bf784f8c8a5afb27762781..f5598bb695ae306143d31639f53d742ff18ddd22 100644
--- a/README.rst
+++ b/README.rst
@@ -17,6 +17,7 @@ Licence
 ::
 
   Copyright © 2022 Dominik George <dominik.george@teckids.org>
+  Copyright © 2022 Tom Teichler <tom.teichler@teckids.org>
 
   Licenced under the EUPL, version 1.2 or later
 
diff --git a/aleksis/apps/tezor/apps.py b/aleksis/apps/tezor/apps.py
index 91be7bb899eae1e9d1a56bd40651423b41fe37e8..194ffad797675f5a7d9708c4d1c0df1c72281e7d 100644
--- a/aleksis/apps/tezor/apps.py
+++ b/aleksis/apps/tezor/apps.py
@@ -1,4 +1,7 @@
+from django.apps import apps
+
 from aleksis.core.util.apps import AppConfig
+from aleksis.core.util.core_helpers import get_site_preferences
 
 
 class DefaultConfig(AppConfig):
@@ -10,4 +13,48 @@ class DefaultConfig(AppConfig):
         "Repository": "https://edugit.org/AlekSIS/onboarding//AlekSIS-App-Tezor",
     }
     licence = "EUPL-1.2+"
-    copyright_info = (([2022], "Dominik George", "dominik.george@teckids.org"),)
+    copyright_info = (([2022], "Dominik George", "dominik.george@teckids.org"), ([2022], "Tom Teichler", "tom.teichler@teckids.org"),)
+
+    def ready(self):
+        from django.conf import settings  # noqa
+        settings.PAYMENT_VARIANTS = {}
+
+        for app_config in apps.app_configs.values():
+            if hasattr(app_config, "get_payment_variants"):
+                variants = app_config.get_payment_variants()
+                for name, config in variants.items():
+                    if name not in settings.PAYMENT_VARIANTS:
+                        settings.PAYMENT_VARIANTS[name] = config
+
+    def get_payment_variants(self):
+        prefs = get_site_preferences()
+        variants = {}
+
+        if prefs["payments__sofort_api_id"]:
+            variants["sofort"] = ("payments.sofort.SofortProvider", {
+                "id": prefs["payments__sofort_api_id"],
+                "key": prefs["payments__sofort_api_key"],
+                "project_id": prefs["payments__sofort_project_id"],
+                "endpoint": "https://api.sofort.com/api/xml",
+            })
+
+        if prefs["payments__paypal_client_id"]:
+            variants["paypal"] = ("payments.paypal.PaypalProvider", {
+                "client_id": prefs["payments__paypal_client_id"],
+                "secret": prefs["payments__paypal_secret"],
+                "capture": not prefs["payments__paypal_capture"],
+                "endpoint": "https://api.paypal.com",
+            })
+
+        if prefs["payments__pledge_enabled"]:
+            variants["pledge"] = ("djp_sepa.providers.PaymentPledgeProvider", {
+            })
+
+
+        if prefs["payments__sdd_creditor_identifier"]:
+            variants["sdd"] = ("djp_sepa.providers.DirectDebitProvider", {
+                "creditor": prefs["payments__sdd_creditor"],
+                "creditor_identifier": prefs["payments__sdd_creditor_identifier"],
+            })
+
+        return variants
diff --git a/aleksis/apps/tezor/menus.py b/aleksis/apps/tezor/menus.py
index 49c9644d489921913cd07a84aeaf217008bfa661..1355051be6de23342f18e317f4382aa68a96452e 100644
--- a/aleksis/apps/tezor/menus.py
+++ b/aleksis/apps/tezor/menus.py
@@ -3,19 +3,19 @@ from django.utils.translation import gettext_lazy as _
 MENUS = {
     "NAV_MENU_CORE": [
         {
-            "name": _("Payments"),
+            "name": _("Payments and Money"),
             "url": "#",
             "root": True,
-            "icon": "price_check",
+            "svg_icon": "mdi:piggy_bank",
             "validators": [
                 "menu_generator.validators.is_authenticated",
                 "aleksis.core.util.core_helpers.has_person",
             ],
             "submenu": [
                 {
-                    "name": _("Clients"),
+                    "name": _("Manage clients"),
                     "url": "clients",
-                    "icon": "account_balance",
+                    "svg_icon": "mdi:domain",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
diff --git a/aleksis/apps/tezor/migrations/0001_initial.py b/aleksis/apps/tezor/migrations/0001_initial.py
index 16716174e4829ed019de800d4d83e803f95bee6d..98802cabe1a043e935049e80df04c07b1ce497e7 100644
--- a/aleksis/apps/tezor/migrations/0001_initial.py
+++ b/aleksis/apps/tezor/migrations/0001_initial.py
@@ -85,7 +85,7 @@ class Migration(migrations.Migration):
         ),
         migrations.AddConstraint(
             model_name='invoice',
-            constraint=models.UniqueConstraint(fields=('transaction_id', 'group'), name='number_uniq_per_group'),
+            constraint=models.UniqueConstraint(fields=('number', 'group'), name='number_uniq_per_group'),
         ),
         migrations.AddConstraint(
             model_name='client',
diff --git a/aleksis/apps/tezor/migrations/0002_invoice_due_date.py b/aleksis/apps/tezor/migrations/0002_invoice_due_date.py
new file mode 100644
index 0000000000000000000000000000000000000000..373f9dc61f94d613cce82d789b85d50d2c1154bd
--- /dev/null
+++ b/aleksis/apps/tezor/migrations/0002_invoice_due_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.12 on 2022-03-12 21:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tezor', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='invoice',
+            name='due_date',
+            field=models.DateField(null=True, verbose_name='Payment due date'),
+        ),
+    ]
diff --git a/aleksis/apps/tezor/migrations/0003_manual_invoicing.py b/aleksis/apps/tezor/migrations/0003_manual_invoicing.py
new file mode 100644
index 0000000000000000000000000000000000000000..efff727e2f683d26d8b2872211a14da2924f353a
--- /dev/null
+++ b/aleksis/apps/tezor/migrations/0003_manual_invoicing.py
@@ -0,0 +1,54 @@
+# Generated by Django 3.2.12 on 2022-03-12 21:41
+
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0038_notification_send_at'),
+        ('sites', '0002_alter_domain_unique'),
+        ('tezor', '0002_invoice_due_date'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='InvoiceItem',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('sku', models.CharField(blank=True, max_length=255, verbose_name='Article no.')),
+                ('description', models.CharField(max_length=255, verbose_name='Purchased item')),
+                ('price', models.DecimalField(decimal_places=2, default='0.0', max_digits=9, verbose_name='Item gross price')),
+                ('currency', models.CharField(max_length=10, verbose_name='Currency')),
+                ('tax_rate', models.DecimalField(decimal_places=1, default='0.0', max_digits=4, verbose_name='Tax rate')),
+            ],
+            options={
+                'abstract': False,
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.AddField(
+            model_name='invoice',
+            name='person',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.person', verbose_name='Invoice recipient (person)'),
+        ),
+        migrations.AddConstraint(
+            model_name='invoice',
+            constraint=models.CheckConstraint(check=models.Q(('for_object_id__isnull', True), ('person__isnull', True), _connector='OR'), name='object_or_person'),
+        ),
+        migrations.AddField(
+            model_name='invoiceitem',
+            name='site',
+            field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site'),
+        ),
+        migrations.AddField(
+            model_name='invoice',
+            name='items',
+            field=models.ManyToManyField(to='tezor.InvoiceItem', verbose_name='Invoice items'),
+        ),
+    ]
diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py
index 7dcad881b85266d1eb099b49d5ab7a318b200932..d8ddc3f0d7d270e5519176f8283a2a8e70efa48b 100644
--- a/aleksis/apps/tezor/models/invoice.py
+++ b/aleksis/apps/tezor/models/invoice.py
@@ -1,16 +1,19 @@
+from django.conf import settings
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
+from django.db.models import Q
+from django.shortcuts import reverse
 from django.utils.translation import gettext_lazy as _
 
-from djmoney.models.fields import CurrencyField, MoneyField
-from payments import PurchasedItem
+from payments import PaymentStatus, PurchasedItem
 from payments.models import BasePayment
 
 from aleksis.core.mixins import ExtensibleModel, PureDjangoModel
+from aleksis.core.models import Person
 
-from .base import Client
 from ..tables import PurchasedItemsTable, TotalsTable
+from .base import Client
 
 
 class InvoiceGroup(ExtensibleModel):
@@ -32,22 +35,73 @@ 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"),
+    }
+    STATUS_ICONS = {
+        PaymentStatus.WAITING: "mdi:cash-lock-open",
+        PaymentStatus.INPUT: "mdi:cash-lock-open",
+        PaymentStatus.PREAUTH: "mdi:cash-lock",
+        PaymentStatus.CONFIRMED: "mdi:cash-check",
+        PaymentStatus.REFUNDED: "mdi:cash-refund",
+        PaymentStatus.REJECTED: "mdi:cash-remove",
+        PaymentStatus.ERROR: "mdi:cash-remove",
+    }
+
     group = models.ForeignKey(
         InvoiceGroup, verbose_name=_("Invoice group"), related_name="invoices", on_delete=models.SET_NULL, null=True
     )
 
     number = models.CharField(verbose_name=_("Invoice number"), max_length=255)
+    due_date = models.DateField(verbose_name=_("Payment due date"), null=True)
 
     for_content_type = models.ForeignKey(ContentType, on_delete=models.SET_NULL, null=True)
     for_object_id = models.PositiveIntegerField()
     for_object = GenericForeignKey("for_content_type", "for_object_id")
 
+    # For manual invoicing
+    person = models.ForeignKey(Person, on_delete=models.SET_NULL, verbose_name=_("Invoice recipient (person)"), blank=True, null=True)
+    items = models.ManyToManyField("InvoiceItem", verbose_name=_("Invoice items"))
+
+    @classmethod
+    def get_variant_choices(cls):
+        choices = []
+        for variant in settings.PAYMENT_VARIANTS.keys():
+            choices.append((variant, cls.VARIANT_DISPLAY[variant][0]))
+        return choices
+
+    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_status_icon(self):
+        return self.__class__.STATUS_ICONS[self.status]
+
     def get_purchased_items(self):
-        return self.for_object.get_purchased_items()
+        for item in self.items.all():
+            yield item.as_purchased_item()
+        else:
+            return self.for_object.get_purchased_items()
+
+    def get_person(self):
+        if self.person:
+            return self.person
+        elif hasattr(self.for_object, "person"):
+            return self.for_object.person
+        elif hasattr(self.for_object, "get_person"):
+            return self.for_object.get_person()
+
+        return None
 
     class Meta:
         constraints = [
-            models.UniqueConstraint(fields=["transaction_id", "group"], name="number_uniq_per_group")
+            models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"),
+            models.CheckConstraint(check=(Q(for_object_id__isnull=True) | Q(person__isnull=True)), name="object_or_person"),
         ]
 
     def get_billing_email_recipients(self):
@@ -83,3 +137,20 @@ class Invoice(BasePayment, PureDjangoModel):
         })
 
         return TotalsTable(values)
+
+    def get_success_url(self):
+        return reverse("invoice_by_token", kwargs={"slug": self.token})
+
+    def get_failure_url(self):
+        return reverse("invoice_by_token", kwargs={"slug": self.token})
+
+
+class InvoiceItem(ExtensibleModel):
+    sku = models.CharField(max_length=255, verbose_name=_("Article no."), blank=True)
+    description = models.CharField(max_length=255, verbose_name=_("Purchased item"))
+    price = models.DecimalField(verbose_name=_("Item gross price"), max_digits=9, decimal_places=2, default="0.0")
+    currency = models.CharField(max_length=10, verbose_name=_("Currency"))
+    tax_rate = models.DecimalField(verbose_name=_("Tax rate"), max_digits=4, decimal_places=1, default="0.0")
+
+    def as_purchased_item(self):
+        yield PurchasedItem(name=self.description, quantity=1, price=self.price, currency=self.currency, sku=self.sku, tax_rate=self.tax_rate)
diff --git a/aleksis/apps/tezor/predicates.py b/aleksis/apps/tezor/predicates.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9fb75deeb3b67e7063152b13105afa77cd7d81d
--- /dev/null
+++ b/aleksis/apps/tezor/predicates.py
@@ -0,0 +1,26 @@
+from django.contrib.auth import get_user_model
+
+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."""
+    return obj.get_person() == user.person
+
+@predicate
+def has_no_payment_variant(user: User, obj: Invoice):
+    """Predicate which checks that the invoice has no payment variant."""
+    return not obj.variant
+
+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
+
+    return _predicate
diff --git a/aleksis/apps/tezor/preferences.py b/aleksis/apps/tezor/preferences.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a25f34519a9b34394ec9b126aa6d67651a12049
--- /dev/null
+++ b/aleksis/apps/tezor/preferences.py
@@ -0,0 +1,119 @@
+from django.utils.translation import gettext_lazy as _
+
+from dynamic_preferences.preferences import Section
+from dynamic_preferences.types import BooleanPreference, StringPreference
+
+from aleksis.core.registries import site_preferences_registry
+
+payments = Section("payments", verbose_name=_("Payments"))
+
+
+@site_preferences_registry.register
+class EnablePledge(BooleanPreference):
+    """Allow payments to be made by anyone, not only invoice recipient."""
+
+    section = payments
+    name = "public_payments"
+    verbose_name = _("Public payments")
+    help_text = _("Allow anyone (including guests) to make payments. Basic invoice information will be visible to anyone who knows the invoice token.")
+    default = True
+    required = False
+
+
+@site_preferences_registry.register
+class SofortAPIID(StringPreference):
+    """Sofort payment backend - API ID."""
+
+    section = payments
+    name = "sofort_api_id"
+    verbose_name = _("Sofort / Klarna - API ID")
+    default = ""
+    required = False
+
+
+@site_preferences_registry.register
+class SofortAPIKey(StringPreference):
+    """Sofort payment backend - API key."""
+
+    section = payments
+    name = "sofort_api_key"
+    verbose_name = _("Sofort / Klarna - API Key")
+    default = ""
+    required = False
+
+
+@site_preferences_registry.register
+class SofortProjectID(StringPreference):
+    """Sofort payment backend - project ID."""
+
+    section = payments
+    name = "sofort_project_id"
+    verbose_name = _("Sofort / Klarna - Project ID")
+    default = ""
+    required = False
+
+
+@site_preferences_registry.register
+class PaypalClientID(StringPreference):
+    """PayPal payment backend - client ID."""
+
+    section = payments
+    name = "paypal_client_id"
+    verbose_name = _("PayPal - Client ID")
+    default = ""
+    required = False
+
+
+@site_preferences_registry.register
+class PaypalSecret(StringPreference):
+    """PayPal payment backend - secret."""
+
+    section = payments
+    name = "paypal_secret"
+    verbose_name = _("PayPal - Secret")
+    default = ""
+    required = False
+
+
+@site_preferences_registry.register
+class PaypalCapture(BooleanPreference):
+    """PayPal payment backend - use Authorize & Capture."""
+
+    section = payments
+    name = "paypal_capture"
+    verbose_name = _("PayPal - Use Authorize & Capture")
+    default = False
+    required = False
+
+
+@site_preferences_registry.register
+class EnablePledge(BooleanPreference):
+    """Payment pledge payment backend - enable or not."""
+
+    section = payments
+    name = "pledge_enabled"
+    verbose_name = _("Enabledp ledged payments")
+    default = False
+    required = False
+
+
+@site_preferences_registry.register
+class SDDCreditor(StringPreference):
+    """SEPA direct debit backend - creditor name."""
+
+    section = payments
+    name = "sdd_creditor"
+    verbose_name = _("SEPA Direct Debit - Creditor name")
+    default = ""
+    required = False
+
+
+@site_preferences_registry.register
+class SDDCreditorIdentifier(StringPreference):
+    """SEPA direct debit backend - creditor identifier."""
+
+    section = payments
+    name = "sdd_creditor_identifier"
+    verbose_name = _("SEPA Direct Debit - Creditor identifier")
+    default = ""
+    required = False
diff --git a/aleksis/apps/tezor/rules.py b/aleksis/apps/tezor/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..12a27e3b55a38bbff4d7af450d0b9b757e0a01b6
--- /dev/null
+++ b/aleksis/apps/tezor/rules.py
@@ -0,0 +1,91 @@
+import rules
+from payments import PaymentStatus
+
+from aleksis.core.util.predicates import has_person, has_global_perm, has_any_object, has_object_perm, is_site_preference_set
+
+from .models.base import Client
+from .models.invoice import Invoice, InvoiceGroup
+from .predicates import has_no_payment_variant, is_own_invoice, is_in_payment_status
+
+# View clients
+view_clients_predicate = has_person & (
+    has_global_perm("tezor.view_client") | has_any_object("tezor.view_client", Client)
+)
+rules.add_perm("tezor.view_clients_rule", view_clients_predicate)
+
+# View client
+view_client_predicate = has_person & (
+    has_global_perm("tezor.view_client") | has_object_perm("tezor.view_client")
+)
+rules.add_perm("tezor.view_client_rule", view_client_predicate)
+
+# Edit clients
+edit_client_predicate = has_person & (
+    has_global_perm("tezor.edit_client") | has_object_perm("tezor.edit_client")
+)
+rules.add_perm("tezor.edit_client_rule", edit_client_predicate)
+
+# Create clients
+create_client_predicate = has_person & (
+    has_global_perm("tezor.create_client") | has_any_object("tezor.create_client", Client)
+)
+rules.add_perm("tezor.create_client_rule", create_client_predicate)
+
+# Delete clients
+delete_client_predicate = has_person & (
+    has_global_perm("tezor.delete_client") | has_object_perm("tezor.delete_client")
+)
+rules.add_perm("tezor.delete_client_rule", delete_client_predicate)
+
+# View invoice groups
+view_invoice_groups_predicate = has_person & (
+    has_global_perm("tezor.view_invoice_group") | has_any_object("tezor.view_invoice_group", InvoiceGroup)
+)
+rules.add_perm("tezor.view_invoice_groups_rule", view_invoice_groups_predicate)
+
+# View invoice_group
+view_invoice_group_predicate = has_person & (
+    has_global_perm("tezor.view_invoice_group") | has_object_perm("tezor.view_invoice_group")
+)
+rules.add_perm("tezor.view_invoice_group_rule", view_invoice_group_predicate)
+
+# Edit invoice groups
+edit_invoice_group_predicate = has_person & (
+    has_global_perm("tezor.edit_invoice_group") | has_object_perm("tezor.edit_invoice_group")
+)
+rules.add_perm("tezor.edit_invoice_group_rule", edit_invoice_group_predicate)
+
+# Create invoice groups
+create_invoice_groups_predicate = has_person & (
+    has_global_perm("tezor.create_invoice_group") | has_any_object("tezor.create_invoice_group", InvoiceGroup)
+)
+rules.add_perm("tezor.create_invoice_groups_rule", create_invoice_groups_predicate)
+
+# Delete invoice groups
+delete_invoice_groups_predicate = has_person & (
+    has_global_perm("tezor.delete_invoice_group") | has_any_object("tezor.delete_invoice_group", InvoiceGroup)
+)
+rules.add_perm("tezor.delete_invoice_groups_rule", delete_invoice_groups_predicate)
+
+# Display invoice billing information
+display_billing_predicate = has_person & (is_own_invoice | has_global_perm("tezor.display_billing") | has_object_perm("tezor.display_billing"))
+rules.add_perm("tezor.display_billing_rule", display_billing_predicate)
+
+# Display invoice purchased items
+display_purchased_items_predicate = has_person & (is_own_invoice | has_global_perm("tezor.display_purchased_items") | has_object_perm("tezor.display_purchased_items"))
+rules.add_perm("tezor.display_purchased_items_rule", display_purchased_items_predicate)
+
+# Change payment variant
+change_payment_variant_predicate = has_person & is_in_payment_status(PaymentStatus.WAITING) & ((is_own_invoice & has_no_payment_variant) | has_global_perm("tezor.change_payment_variant") | has_object_perm("tezor.change_payment_variant"))
+rules.add_perm("tezor.change_payment_variant", change_payment_variant_predicate)
+
+# Start payment
+do_payment_predicate = has_person & (is_in_payment_status(PaymentStatus.WAITING) | is_in_payment_status(PaymentStatus.INPUT) | is_in_payment_status(PaymentStatus.ERROR) | is_in_payment_status(PaymentStatus.REJECTED)) & ((is_own_invoice | is_site_preference_set("payments", "public_payments")) | has_global_perm("tezor.do_payment") | has_object_perm("tezor.do_payment"))
+rules.add_perm("tezor.do_payment", do_payment_predicate)
+
+# 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/settings.py b/aleksis/apps/tezor/settings.py
index dabd92baa378f72fb1d665e416b87d09f9d6c18d..e7277712f384d2170c04e46ce10634a01ac71785 100644
--- a/aleksis/apps/tezor/settings.py
+++ b/aleksis/apps/tezor/settings.py
@@ -1 +1,5 @@
-INSTALLED_APPS = ["payments"]
+INSTALLED_APPS = ["payments", "djp_sepa"]
+
+PAYMENT_MODEL = "tezor.Invoice"
+
+overrides = ["PAYMENT_MODEL"]
diff --git a/aleksis/apps/tezor/tables.py b/aleksis/apps/tezor/tables.py
index d9001895cd8d4232f3ff24b25ea847b2747077db..16ed8a0ed491b7eb064c432cfd06950239ba620d 100644
--- a/aleksis/apps/tezor/tables.py
+++ b/aleksis/apps/tezor/tables.py
@@ -85,21 +85,21 @@ class InvoiceGroupsTable(tables.Table):
 
 class InvoicesTable(tables.Table):
 
-    transaction_id = tables.Column()
+    number = tables.Column()
     status = tables.Column()
     created = tables.DateColumn()
     billing_first_name = tables.Column()
     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(
-        "get_invoice_by_pk",
-        args=[A("id")],
+        "print_invoice",
+        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 add38a90fa88f33578f98b3282ae1f315d05a031..345aed939f0ef2ad1e2e8a15b58d18286c0c9eae 100644
--- a/aleksis/apps/tezor/templates/tezor/invoice/full.html
+++ b/aleksis/apps/tezor/templates/tezor/invoice/full.html
@@ -1,16 +1,108 @@
 {% extends "core/base.html" %}
-{% load material_form i18n %}
+{% load material_form i18n rules %}
 
 {% load render_table from django_tables2 %}
 
-{% block browser_title %}{{ object.transaction_id }}{% endblock %}
+{% block browser_title %}{{ object.number }}{% endblock %}
 
 {% block content %}
 
-    <h1>{% trans "Invoice" %} {{ object.transaction_id }} — {{ 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>
+    {% 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 %}
 
-    {% render_table object.purchased_items_table %}
-    {% render_table object.totals_table %}
+    <h1>{% trans "Invoice" %} {{ object.number }} — {{ object.created.date }}</h1>
+
+    {% 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>
+                  <i class="material-icons iconify" data-icon="{{ object.get_status_icon }}"></i>
+                </td>
+                <td>
+                  {{ object.get_status_display }}
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <i class="material-icons iconify" data-icon="mdi:calendar-end"></i>
+                </td>
+                <td>
+                  {{ object.due_date }}
+                </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/templates/tezor/invoice/payment.html b/aleksis/apps/tezor/templates/tezor/invoice/payment.html
new file mode 100644
index 0000000000000000000000000000000000000000..838fa26a54bda50be6865880c7fd21d8c7cabab3
--- /dev/null
+++ b/aleksis/apps/tezor/templates/tezor/invoice/payment.html
@@ -0,0 +1,16 @@
+{% extends 'core/base.html' %}
+
+{% load i18n %}
+{% load material_form %}
+
+{% block page_title %}{% blocktrans %}Make payment for{% endblocktrans %} {{ payment.number }}{% endblock %}
+{% block browser_title %}{% blocktrans %}Make payment for{% endblocktrans %} {{ payment.number }}{% endblock %}
+
+{% block content %}
+<form action="{{ form.action }}" method="{{ form.method }}">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% trans "Confirm payment" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="shopping_cart_checkout" %}
+</form>
+{% endblock %}
diff --git a/aleksis/apps/tezor/urls.py b/aleksis/apps/tezor/urls.py
index baf77d09ab2a7254074da33d2403cf273e776294..237a7f37b34ea87a7fcf000532024e552e8afd51 100644
--- a/aleksis/apps/tezor/urls.py
+++ b/aleksis/apps/tezor/urls.py
@@ -4,56 +4,57 @@ from . import views
 
 urlpatterns = [
     path("payments/", include("payments.urls")),
-    path("invoice/<int:pk>/print", views.GetInvoicePDF.as_view(), name="get_invoice_by_pk"),
+    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/list",
+        "clients/",
         views.ClientListView.as_view(),
         name="clients",
     ),
     path(
-        "clients/create",
+        "client/create/",
         views.ClientCreateView.as_view(),
         name="create_client",
     ),
     path(
-        "clients/<int:pk>/edit",
+        "client/<int:pk>/edit/",
         views.ClientEditView.as_view(),
         name="edit_client_by_pk",
     ),
     path(
-        "clients/<int:pk>/delete",
+        "client/<int:pk>/delete/",
         views.ClientDeleteView.as_view(),
         name="delete_client_by_pk",
     ),
     path(
-        "clients/<int:pk>/",
+        "client/<int:pk>/",
         views.ClientDetailView.as_view(),
         name="client_by_pk",
     ),
     path(
-        "client/<int:pk>/invoice_groups/create",
+        "client/<int:pk>/invoice_groups/create/",
         views.InvoiceGroupCreateView.as_view(),
         name="create_invoice_group",
     ),
     path(
-        "invoice_groups/<int:pk>/edit",
+        "invoice_group/<int:pk>/edit/",
         views.InvoiceGroupEditView.as_view(),
         name="edit_invoice_group_by_pk",
     ),
     path(
-        "invoice_groups/<int:pk>/",
+        "invoice_group/<int:pk>/",
         views.InvoiceGroupDetailView.as_view(),
         name="invoice_group_by_pk",
     ),
     path(
-        "invoice_groups/<int:pk>/delete",
+        "invoice_group/<int:pk>/delete/",
         views.InvoiceGroupDeleteView.as_view(),
         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",
     ),
     path(
         "invoice/<int:pk>/send/",
diff --git a/aleksis/apps/tezor/views.py b/aleksis/apps/tezor/views.py
index f6618033972bf245e12faf564412aabcedf083be..448fe651a811ffe28c193015fd3a4b6b1b353fc1 100644
--- a/aleksis/apps/tezor/views.py
+++ b/aleksis/apps/tezor/views.py
@@ -1,12 +1,12 @@
-from django.views.generic import View
-from django.shortcuts import render
+from django.shortcuts import redirect, render, get_object_or_404
 from django.views.decorators.cache import never_cache
+from django.views.generic import FormView, TemplateView, View
+from django.views.generic.detail import DetailView
+from django.urls import reverse, reverse_lazy
 from django.utils.decorators import method_decorator
-from django.views.generic import FormView, TemplateView
 from django.utils.translation import ugettext as _
-from django.urls import reverse, reverse_lazy
-from django.views.generic.detail import DetailView
 
+from payments import get_payment_model, PaymentStatus, RedirectNeeded
 from rules.contrib.views import PermissionRequiredMixin
 from django_tables2.views import SingleTableView, RequestConfig
 from templated_email import InlineImage, send_templated_mail
@@ -23,23 +23,49 @@ 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
 
         print(invoice.group.__dict__)
         return context
 
+
+class DoPaymentView(PermissionRequiredMixin, View):
+
+    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 = self.object.get_form(data=request.POST or None)
+        except RedirectNeeded as redirect_to:
+            return redirect(str(redirect_to))
+
+        context = {
+            "form": form,
+            "payment": self.object,
+        }
+
+        return render(request, self.template_name, context)
+
+
 class ClientListView(PermissionRequiredMixin, SingleTableView):
     """Table of all clients."""
 
     model = Client
     table_class = ClientsTable
-    permission_required = "tezor.view_clients"
+    permission_required = "tezor.view_clients_rule"
     template_name = "tezor/client/list.html"
 
 
@@ -49,7 +75,7 @@ class ClientCreateView(PermissionRequiredMixin, AdvancedCreateView):
 
     model = Client
     form_class = EditClientForm
-    permission_required = "tezor.add_clients"
+    permission_required = "tezor.create_client_rule"
     template_name = "tezor/client/create.html"
     success_url = reverse_lazy("clients")
     success_message = _("The client has been created.")
@@ -61,7 +87,7 @@ class ClientEditView(PermissionRequiredMixin, AdvancedEditView):
 
     model = Client
     form_class = EditClientForm
-    permission_required = "tezor.edit_clients"
+    permission_required = "tezor.edit_client_rule"
     template_name = "tezor/client/edit.html"
     success_url = reverse_lazy("clients")
     success_message = _("The client has been saved.")
@@ -71,7 +97,7 @@ class ClientDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     """Delete view for client."""
 
     model = Client
-    permission_required = "tezor.delete_client"
+    permission_required = "tezor.delete_client_rule"
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("clients")
     success_message = _("The client has been deleted.")
@@ -80,7 +106,7 @@ class ClientDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
 class ClientDetailView(PermissionRequiredMixin, DetailView):
 
     model = Client
-    permission_required = "tezor.view_client"
+    permission_required = "tezor.view_client_rule"
     template_name = "tezor/client/full.html"
 
     def get_context_data(self, object):
@@ -96,7 +122,7 @@ class ClientDetailView(PermissionRequiredMixin, DetailView):
 class InvoiceGroupDetailView(PermissionRequiredMixin, DetailView):
 
     model = InvoiceGroup
-    permission_required = "tezor.view_invoice_group"
+    permission_required = "tezor.view_invoice_group_rule"
     template_name = "tezor/invoice_group/full.html"
 
     def get_context_data(self, object):
@@ -116,7 +142,7 @@ class InvoiceGroupCreateView(PermissionRequiredMixin, AdvancedCreateView):
 
     model = InvoiceGroup
     form_class = EditInvoiceGroupForm
-    permission_required = "tezor.add_invoice_groups"
+    permission_required = "tezor.create_invoice_groups_rule"
     template_name = "tezor/invoice_group/create.html"
     success_url = reverse_lazy("clients")
     success_message = _("The invoice_group has been created.")
@@ -134,7 +160,7 @@ class InvoiceGroupEditView(PermissionRequiredMixin, AdvancedEditView):
 
     model = InvoiceGroup
     form_class = EditInvoiceGroupForm
-    permission_required = "tezor.edit_invoice_groups"
+    permission_required = "tezor.edit_invoice_group_rule"
     template_name = "tezor/invoice_group/edit.html"
     success_url = reverse_lazy("invoice_groups")
     success_message = _("The invoice_group has been saved.")
@@ -144,7 +170,7 @@ class InvoiceGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     """Delete view for invoice_group."""
 
     model = InvoiceGroup
-    permission_required = "tezor.delete_invoice_group"
+    permission_required = "tezor.delete_invoice_group_rule"
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("invoice_groups")
     success_message = _("The invoice_group has been deleted.")
@@ -153,7 +179,8 @@ class InvoiceGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
 class InvoiceDetailView(PermissionRequiredMixin, DetailView):
 
     model = Invoice
-    permission_required = "tezor.view_invoice"
+    slug_field = "token"
+    permission_required = "tezor.view_invoice_rule"
     template_name = "tezor/invoice/full.html"
 
 
diff --git a/pyproject.toml b/pyproject.toml
index f3f3a24fe11000781e67d9ac34a4398c92c20b9b..cdbd8becfc928118b2f2098f0ff40db2f7516e8f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ include = [
 ]
 
 description = "AlekSIS (School Information System) — App Tezor (account and payment system)"
-authors = ["Dominik George <dominik.george@teckids.org>"]
+authors = ["Dominik George <dominik.george@teckids.org>", "Tom Teichler <tom.teichler@teckids.org>"]
 license = "EUPL-1.2-or-later"
 homepage = "https://aleksis.org"
 repository = "https://edugit.org/AlekSIS/onboarding//AlekSIS-App-Tezor"
@@ -30,8 +30,9 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.9"
-aleksis-core = "^2.7"
-django-payments = "^0.15.0"
+aleksis-core = "^2.8.1.dev0"
+django-payments = "^0.15.1"
+django-payments-sepa = "^1.0.dev0"
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"