diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..60317f4987d885fc380d731c6dd8792325342f44
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,215 @@
+module.exports = {
+  extends: [
+    "eslint:recommended",
+    "plugin:vue/strongly-recommended",
+    "plugin:@intlify/vue-i18n/recommended",
+  ],
+  rules: {
+    "no-unused-vars": "warn",
+    "vue/no-unused-vars": "off",
+    "vue/multi-word-component-names": "off",
+    "@intlify/vue-i18n/key-format-style": [
+      "error",
+      "snake_case",
+      {
+        splitByDots: false,
+      },
+    ],
+    // "@intlify/vue-i18n/no-unused-keys": ["warn", {}],
+    "@intlify/vue-i18n/no-raw-text": [
+      "error",
+      {
+        ignoreNodes: ["v-icon"],
+        ignorePattern: "^[-–—·#:()\\[\\]&\\.\\s]+$",
+      },
+    ],
+    // Fixes for prettier (avoid eslint-config-prettier)
+    // The following rules can be used in some cases. See the README for more
+    // information. (These are marked with `0` instead of `"off"` so that a
+    // script can distinguish them.)
+    curly: 0,
+    "lines-around-comment": 0,
+    "max-len": 0,
+    "no-confusing-arrow": 0,
+    "no-mixed-operators": 0,
+    "no-tabs": 0,
+    "no-unexpected-multiline": 0,
+    quotes: 0,
+    "@typescript-eslint/quotes": 0,
+    "babel/quotes": 0,
+    "vue/html-self-closing": 0,
+    "vue/max-len": 0,
+
+    // The rest are rules that you never need to enable when using Prettier.
+    "array-bracket-newline": "off",
+    "array-bracket-spacing": "off",
+    "array-element-newline": "off",
+    "arrow-parens": "off",
+    "arrow-spacing": "off",
+    "block-spacing": "off",
+    "brace-style": "off",
+    "comma-dangle": "off",
+    "comma-spacing": "off",
+    "comma-style": "off",
+    "computed-property-spacing": "off",
+    "dot-location": "off",
+    "eol-last": "off",
+    "func-call-spacing": "off",
+    "function-call-argument-newline": "off",
+    "function-paren-newline": "off",
+    "generator-star": "off",
+    "generator-star-spacing": "off",
+    "implicit-arrow-linebreak": "off",
+    indent: "off",
+    "jsx-quotes": "off",
+    "key-spacing": "off",
+    "keyword-spacing": "off",
+    "linebreak-style": "off",
+    "multiline-ternary": "off",
+    "newline-per-chained-call": "off",
+    "new-parens": "off",
+    "no-arrow-condition": "off",
+    "no-comma-dangle": "off",
+    "no-extra-parens": "off",
+    "no-extra-semi": "off",
+    "no-floating-decimal": "off",
+    "no-mixed-spaces-and-tabs": "off",
+    "no-multi-spaces": "off",
+    "no-multiple-empty-lines": "off",
+    "no-reserved-keys": "off",
+    "no-space-before-semi": "off",
+    "no-trailing-spaces": "off",
+    "no-whitespace-before-property": "off",
+    "no-wrap-func": "off",
+    "nonblock-statement-body-position": "off",
+    "object-curly-newline": "off",
+    "object-curly-spacing": "off",
+    "object-property-newline": "off",
+    "one-var-declaration-per-line": "off",
+    "operator-linebreak": "off",
+    "padded-blocks": "off",
+    "quote-props": "off",
+    "rest-spread-spacing": "off",
+    semi: "off",
+    "semi-spacing": "off",
+    "semi-style": "off",
+    "space-after-function-name": "off",
+    "space-after-keywords": "off",
+    "space-before-blocks": "off",
+    "space-before-function-paren": "off",
+    "space-before-function-parentheses": "off",
+    "space-before-keywords": "off",
+    "space-in-brackets": "off",
+    "space-in-parens": "off",
+    "space-infix-ops": "off",
+    "space-return-throw-case": "off",
+    "space-unary-ops": "off",
+    "space-unary-word-ops": "off",
+    "switch-colon-spacing": "off",
+    "template-curly-spacing": "off",
+    "template-tag-spacing": "off",
+    "unicode-bom": "off",
+    "wrap-iife": "off",
+    "wrap-regex": "off",
+    "yield-star-spacing": "off",
+    "@babel/object-curly-spacing": "off",
+    "@babel/semi": "off",
+    "@typescript-eslint/brace-style": "off",
+    "@typescript-eslint/comma-dangle": "off",
+    "@typescript-eslint/comma-spacing": "off",
+    "@typescript-eslint/func-call-spacing": "off",
+    "@typescript-eslint/indent": "off",
+    "@typescript-eslint/keyword-spacing": "off",
+    "@typescript-eslint/member-delimiter-style": "off",
+    "@typescript-eslint/no-extra-parens": "off",
+    "@typescript-eslint/no-extra-semi": "off",
+    "@typescript-eslint/object-curly-spacing": "off",
+    "@typescript-eslint/semi": "off",
+    "@typescript-eslint/space-before-blocks": "off",
+    "@typescript-eslint/space-before-function-paren": "off",
+    "@typescript-eslint/space-infix-ops": "off",
+    "@typescript-eslint/type-annotation-spacing": "off",
+    "babel/object-curly-spacing": "off",
+    "babel/semi": "off",
+    "flowtype/boolean-style": "off",
+    "flowtype/delimiter-dangle": "off",
+    "flowtype/generic-spacing": "off",
+    "flowtype/object-type-curly-spacing": "off",
+    "flowtype/object-type-delimiter": "off",
+    "flowtype/quotes": "off",
+    "flowtype/semi": "off",
+    "flowtype/space-after-type-colon": "off",
+    "flowtype/space-before-generic-bracket": "off",
+    "flowtype/space-before-type-colon": "off",
+    "flowtype/union-intersection-spacing": "off",
+    "react/jsx-child-element-spacing": "off",
+    "react/jsx-closing-bracket-location": "off",
+    "react/jsx-closing-tag-location": "off",
+    "react/jsx-curly-newline": "off",
+    "react/jsx-curly-spacing": "off",
+    "react/jsx-equals-spacing": "off",
+    "react/jsx-first-prop-new-line": "off",
+    "react/jsx-indent": "off",
+    "react/jsx-indent-props": "off",
+    "react/jsx-max-props-per-line": "off",
+    "react/jsx-newline": "off",
+    "react/jsx-one-expression-per-line": "off",
+    "react/jsx-props-no-multi-spaces": "off",
+    "react/jsx-tag-spacing": "off",
+    "react/jsx-wrap-multilines": "off",
+    "standard/array-bracket-even-spacing": "off",
+    "standard/computed-property-even-spacing": "off",
+    "standard/object-curly-even-spacing": "off",
+    "unicorn/empty-brace-spaces": "off",
+    "unicorn/no-nested-ternary": "off",
+    "unicorn/number-literal-case": "off",
+    "vue/array-bracket-newline": "off",
+    "vue/array-bracket-spacing": "off",
+    "vue/arrow-spacing": "off",
+    "vue/block-spacing": "off",
+    "vue/block-tag-newline": "off",
+    "vue/brace-style": "off",
+    "vue/comma-dangle": "off",
+    "vue/comma-spacing": "off",
+    "vue/comma-style": "off",
+    "vue/dot-location": "off",
+    "vue/func-call-spacing": "off",
+    "vue/html-closing-bracket-newline": "off",
+    "vue/html-closing-bracket-spacing": "off",
+    "vue/html-end-tags": "off",
+    "vue/html-indent": "off",
+    "vue/html-quotes": "off",
+    "vue/key-spacing": "off",
+    "vue/keyword-spacing": "off",
+    "vue/max-attributes-per-line": "off",
+    "vue/multiline-html-element-content-newline": "off",
+    "vue/multiline-ternary": "off",
+    "vue/mustache-interpolation-spacing": "off",
+    "vue/no-extra-parens": "off",
+    "vue/no-multi-spaces": "off",
+    "vue/no-spaces-around-equal-signs-in-attribute": "off",
+    "vue/object-curly-newline": "off",
+    "vue/object-curly-spacing": "off",
+    "vue/object-property-newline": "off",
+    "vue/operator-linebreak": "off",
+    "vue/quote-props": "off",
+    "vue/script-indent": "off",
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/space-in-parens": "off",
+    "vue/space-infix-ops": "off",
+    "vue/space-unary-ops": "off",
+    "vue/template-curly-spacing": "off",
+  },
+  settings: {
+    "vue-i18n": {
+      localeDir: "./aleksis/core/frontend/messages/*.{json}",
+      messageSyntaxVersion: "^8.0.0",
+    },
+  },
+  env: {
+    es2021: true,
+  },
+  parserOptions: {
+    ecmaVersion: "latest",
+  },
+};
diff --git a/.gitignore b/.gitignore
index 902d52b19777084e9cf9f373502f70b32b4495a6..79b5b76de6f6445254cbf64f7e4fb228ffdf0ab8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,6 +52,11 @@ DEADJOE
 .idea
 .idea/
 
+# VSCode
+.vscode/
+.history/
+*.code-workspace
+
 # Database
 db.sqlite3
 
@@ -62,15 +67,28 @@ docs/_build/
 *.aux
 
 # Generated files
-aleksis/node_modules/
-aleksis/static/
+/cache
+/node_modules
+.dev-js/node_modules
+/static/
+/whoosh_index/
+.vite
+.dev-js/.yarn
+.dev-js/.pnp.cjs
+.dev-js/.pnp.loader.mjs
+
+# Lock files
+poetry.lock
+package-lock.json
+yarn.lock
+.dev-js/yarn.lock
 
+# Tests
 .coverage
 .mypy_cache/
 .tox/
 htmlcov/
+
+# Data
 maintenance_mode_state.txt
 media/
-package-lock.json
-
-poetry.lock
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b97f64fa6b8de380209c4d933d18ee374a98e28c..c71fd45f792695baa4f1401c9ba41ce345f6b411 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,15 +1,15 @@
 include:
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/general.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/prepare/lock.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/test/lint.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/test/security.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/build/dist.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/publish/pypi.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/docker/image.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/general.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/prepare/lock.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/test/lint.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/test/security.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/build/dist.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/publish/pypi.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/docker/image.yml
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000000000000000000000000000000000..de783fc60a1ca5f6e25204bb7a51eab815bc77ce
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,94 @@
+# Byte-compiled / optimized / DLL files
+*$py.class
+*.py[cod]
+__pycache__/
+
+# Distribution / packaging
+*.egg
+*.egg-info/
+.Python
+.eggs/
+.installed.cfg
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+
+# Installer logs
+pip-delete-this-directory.txt
+pip-log.txt
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# pyenv
+.python-version
+
+# Environments
+.env
+.venv
+ENV/
+env/
+venv/
+
+# Editors
+*~
+DEADJOE
+\#*#
+
+# IntelliJ
+.idea
+.idea/
+
+# Database
+db.sqlite3
+
+# Sphinx
+docs/_build/
+
+# TeX
+*.aux
+
+# Generated files
+/node_modules/
+/static/
+/whoosh_index/
+poetry.lock
+
+.coverage
+.mypy_cache/
+.tox/
+htmlcov/
+maintenance_mode_state.txt
+media/
+package-lock.json
+yarn.lock
+
+# VSCode
+.vscode/
+.history/
+*.code-workspace
+
+/cache
+
+# Add HTML files to avoid problems with unsupported Django templates
+*.html
+
+# Do not check/reformat generated files
+aleksis/core/util/licenses.json
+.vite/
+
+.pnp.cjs
+.pnp.loader.mjs
diff --git a/aleksis/apps/tezor/forms.py b/aleksis/apps/tezor/forms.py
index e22afc00a68b5146612392d30fb353012f8cace7..e15ad454109751bfaa01aa9c8e73335f10f83ca0 100644
--- a/aleksis/apps/tezor/forms.py
+++ b/aleksis/apps/tezor/forms.py
@@ -74,7 +74,24 @@ class EditClientForm(ExtensibleForm):
 
     class Meta:
         model = Client
-        exclude = []
+        fields = [
+            "name",
+            "email",
+            "pledge_enabled",
+            "sofort_enabled",
+            "sofort_api_id",
+            "sofort_api_key",
+            "sofort_project_id",
+            "paypal_enabled",
+            "paypal_client_id",
+            "paypal_secret",
+            "paypal_capture",
+            "sdd_enabled",
+            "sdd_creditor",
+            "sdd_creditor_identifier",
+            "sdd_iban",
+            "sdd_bic",
+        ]
 
 
 class EditInvoiceGroupForm(ExtensibleForm):
@@ -83,4 +100,4 @@ class EditInvoiceGroupForm(ExtensibleForm):
 
     class Meta:
         model = InvoiceGroup
-        exclude = ["client"]
+        fields = ["name", "template_name"]
diff --git a/aleksis/apps/tezor/frontend/index.js b/aleksis/apps/tezor/frontend/index.js
index 520274cbf77b349020e100a19f5922442c751531..1faad005dfbbe16d7f2190fd2c921721a5e7ed6a 100644
--- a/aleksis/apps/tezor/frontend/index.js
+++ b/aleksis/apps/tezor/frontend/index.js
@@ -1,5 +1,3 @@
-import { hasPersonValidator } from "aleksis.core/routeValidators";
-
 export default {
   meta: {
     inMenu: true,
@@ -131,5 +129,5 @@ export default {
       },
       name: "tezor.sendInvoiceByToken",
     },
-    ],
+  ],
 };
diff --git a/aleksis/apps/tezor/locale/ar/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/ar/LC_MESSAGES/django.po
index b1957a3b565b5394872ec321a986140c4b868b20..056ca67ca1155f5c350485e7d38fc1ed295d4ba5 100644
--- a/aleksis/apps/tezor/locale/ar/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/ar/LC_MESSAGES/django.po
@@ -337,7 +337,7 @@ msgid "Pay now"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/full.html:113
-msgid "Mark as payed"
+msgid "Mark as paid"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/list.html:6
diff --git a/aleksis/apps/tezor/locale/de_DE/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/de_DE/LC_MESSAGES/django.po
index 5c4d8f62cf804bcf0575c10f4b892696c5c7bad8..eba261761da6ad15cf9fce484ad3062301aa953b 100644
--- a/aleksis/apps/tezor/locale/de_DE/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/de_DE/LC_MESSAGES/django.po
@@ -365,7 +365,7 @@ msgid "Pay now"
 msgstr "Jetzt zahlen"
 
 #: aleksis/apps/tezor/templates/tezor/invoice/full.html:113
-msgid "Mark as payed"
+msgid "Mark as paid"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/list.html:6
diff --git a/aleksis/apps/tezor/locale/fr/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/fr/LC_MESSAGES/django.po
index 48ffaaccd554a496b33d5670aa6de26ad167ec0c..1078b7977ad3a2ca10b0a46660e4a15241ee9ef3 100644
--- a/aleksis/apps/tezor/locale/fr/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/fr/LC_MESSAGES/django.po
@@ -337,7 +337,7 @@ msgid "Pay now"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/full.html:113
-msgid "Mark as payed"
+msgid "Mark as paid"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/list.html:6
diff --git a/aleksis/apps/tezor/locale/la/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/la/LC_MESSAGES/django.po
index c5273a35b09c009825f37040d55cbef6fd51e517..889dce8605d2a7c1e9b1e853fa13462e1144c08a 100644
--- a/aleksis/apps/tezor/locale/la/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/la/LC_MESSAGES/django.po
@@ -336,7 +336,7 @@ msgid "Pay now"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/full.html:113
-msgid "Mark as payed"
+msgid "Mark as paid"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/list.html:6
diff --git a/aleksis/apps/tezor/locale/nb_NO/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/nb_NO/LC_MESSAGES/django.po
index c5273a35b09c009825f37040d55cbef6fd51e517..889dce8605d2a7c1e9b1e853fa13462e1144c08a 100644
--- a/aleksis/apps/tezor/locale/nb_NO/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/nb_NO/LC_MESSAGES/django.po
@@ -336,7 +336,7 @@ msgid "Pay now"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/full.html:113
-msgid "Mark as payed"
+msgid "Mark as paid"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/list.html:6
diff --git a/aleksis/apps/tezor/locale/tr_TR/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/tr_TR/LC_MESSAGES/django.po
index c5273a35b09c009825f37040d55cbef6fd51e517..889dce8605d2a7c1e9b1e853fa13462e1144c08a 100644
--- a/aleksis/apps/tezor/locale/tr_TR/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/tr_TR/LC_MESSAGES/django.po
@@ -336,7 +336,7 @@ msgid "Pay now"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/full.html:113
-msgid "Mark as payed"
+msgid "Mark as paid"
 msgstr ""
 
 #: aleksis/apps/tezor/templates/tezor/invoice/list.html:6
diff --git a/aleksis/apps/tezor/migrations/0001_initial.py b/aleksis/apps/tezor/migrations/0001_initial.py
index 98802cabe1a043e935049e80df04c07b1ce497e7..78902e2c22002582ad57b32b646b851d9f328a94 100644
--- a/aleksis/apps/tezor/migrations/0001_initial.py
+++ b/aleksis/apps/tezor/migrations/0001_initial.py
@@ -1,7 +1,7 @@
 # Generated by Django 3.2.12 on 2022-03-06 21:33
 
 import aleksis.core.mixins
-import django.contrib.sites.managers
+import aleksis.core.managers
 from django.db import migrations, models
 import django.db.models.deletion
 
@@ -25,7 +25,7 @@ class Migration(migrations.Migration):
                 ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
             ],
             managers=[
-                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+                ('objects', aleksis.core.managers.AlekSISBaseManager()),
             ],
         ),
         migrations.CreateModel(
@@ -39,7 +39,7 @@ class Migration(migrations.Migration):
                 ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
             ],
             managers=[
-                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+                ('objects', aleksis.core.managers.AlekSISBaseManager()),
             ],
         ),
         migrations.CreateModel(
diff --git a/aleksis/apps/tezor/migrations/0003_manual_invoicing.py b/aleksis/apps/tezor/migrations/0003_manual_invoicing.py
index 90b7100ced2bcb58c6016004b88dcc38d15e5ee9..125bae2603ba33764d4dcb3c245d92dc06a2255e 100644
--- a/aleksis/apps/tezor/migrations/0003_manual_invoicing.py
+++ b/aleksis/apps/tezor/migrations/0003_manual_invoicing.py
@@ -1,6 +1,6 @@
 # Generated by Django 3.2.12 on 2022-03-12 21:41
 
-import django.contrib.sites.managers
+import aleksis.core.managers
 from django.db import migrations, models
 import django.db.models.deletion
 
@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
                 'abstract': False,
             },
             managers=[
-                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+                ('objects', aleksis.core.managers.AlekSISBaseManager()),
             ],
         ),
         migrations.AddField(
diff --git a/aleksis/apps/tezor/migrations/0007_client_payment_variants.py b/aleksis/apps/tezor/migrations/0007_client_payment_variants.py
index 26e258ef53333dc9644b69f289b2d54f6947e9a1..92b9380215f1a1fa285e615ebe39efde0c9a353d 100644
--- a/aleksis/apps/tezor/migrations/0007_client_payment_variants.py
+++ b/aleksis/apps/tezor/migrations/0007_client_payment_variants.py
@@ -39,7 +39,7 @@ def configure_clients(apps, schema_editor):
                     values[f"{variant}_enabled"] = False
                     warnings.warn(f"Payment variant {variant} enabled but {field.name} not configured!")
 
-    Client.objects.update(**values)
+    Client._base_manager.update(**values)
 
 class Migration(migrations.Migration):
 
diff --git a/aleksis/apps/tezor/migrations/0010_alter_client_options_alter_invoice_options_and_more.py b/aleksis/apps/tezor/migrations/0010_alter_client_options_alter_invoice_options_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..45e0a284018bdca6cfcd10fea7e791ba6dc6aa7c
--- /dev/null
+++ b/aleksis/apps/tezor/migrations/0010_alter_client_options_alter_invoice_options_and_more.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.3 on 2023-07-22 15:41
+
+import aleksis.core.managers
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('tezor', '0009_invoice_billing_phone'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='client',
+            name='managed_by_app_label',
+            field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
+        ),
+        migrations.AddField(
+            model_name='invoicegroup',
+            name='managed_by_app_label',
+            field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
+        ),
+        migrations.AddField(
+            model_name='invoiceitem',
+            name='managed_by_app_label',
+            field=models.CharField(blank=True, editable=False, max_length=255, verbose_name='App label of app responsible for managing this instance'),
+        ),
+    ]
diff --git a/aleksis/apps/tezor/models/base.py b/aleksis/apps/tezor/models/base.py
index d44902ea81627a68c07197a309bd8592c6800dc0..a2d27723c65e72b133157d6fd4e735bf825a55ca 100644
--- a/aleksis/apps/tezor/models/base.py
+++ b/aleksis/apps/tezor/models/base.py
@@ -54,6 +54,8 @@ class Client(ExtensibleModel):
     pledge_enabled = models.BooleanField(verbose_name=_("Pledge enabled"), default=False)
 
     class Meta:
+        verbose_name = _("Client")
+        verbose_name_plural = _("Clients")
         constraints = [
             models.UniqueConstraint(fields=["name", "site"], name="uniq_client_per_site"),
             models.CheckConstraint(
diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py
index eda458c5919c003c630fc5a517a3dc85d567b964..408a39512a2053024233bc7cf7b049428d5a573a 100644
--- a/aleksis/apps/tezor/models/invoice.py
+++ b/aleksis/apps/tezor/models/invoice.py
@@ -28,6 +28,13 @@ class InvoiceGroup(ExtensibleModel):
         verbose_name=_("Template to render invoices with as PDF"), blank=True, max_length=255
     )
 
+    class Meta:
+        verbose_name = _("Invoice Group")
+        verbose_name_plural = _("Invoice Groups")
+        constraints = [
+            models.UniqueConstraint(fields=["client", "name"], name="group_uniq_per_client")
+        ]
+
     def __str__(self) -> str:
         return self.name
 
@@ -37,11 +44,6 @@ class InvoiceGroup(ExtensibleModel):
         else:
             return Client.get_variant_choices()
 
-    class Meta:
-        constraints = [
-            models.UniqueConstraint(fields=["client", "name"], name="group_uniq_per_client")
-        ]
-
 
 class Invoice(BasePayment, PureDjangoModel):
     STATUS_ICONS = {
@@ -81,6 +83,17 @@ class Invoice(BasePayment, PureDjangoModel):
     )
     items = models.ManyToManyField("InvoiceItem", verbose_name=_("Invoice items"))
 
+    class Meta:
+        verbose_name = _("Invoice")
+        verbose_name_plural = _("Invoices")
+        constraints = [
+            models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"),
+        ]
+        permissions = (("send_invoice_email", _("Can send invoice by email")),)
+
+    def __str__(self):
+        return self.number
+
     def save(self, *args, **kwargs):
         if self.person:
             person = self.person
@@ -104,6 +117,9 @@ class Invoice(BasePayment, PureDjangoModel):
 
         super().save(*args, **kwargs)
 
+    def get_absolute_url(self):
+        return reverse("invoice_by_token", kwargs={"slug": self.token})
+
     def get_variant_choices(self=None):
         if self and self.group:
             return self.group.get_variant_choices()
@@ -137,12 +153,6 @@ class Invoice(BasePayment, PureDjangoModel):
 
         return None
 
-    class Meta:
-        constraints = [
-            models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"),
-        ]
-        permissions = (("send_invoice_email", _("Can send invoice by email")),)
-
     def get_billing_email_recipients(self):
         if hasattr(self.for_object, "get_billing_email_recipients"):
             return self.for_object.get_billing_email_recipients()
@@ -182,9 +192,6 @@ class Invoice(BasePayment, PureDjangoModel):
 
         return TotalsTable(values)
 
-    def get_absolute_url(self):
-        return reverse("invoice_by_token", kwargs={"slug": self.token})
-
     def get_success_url(self):
         return self.get_absolute_url()
 
@@ -203,6 +210,13 @@ class InvoiceItem(ExtensibleModel):
         verbose_name=_("Tax rate"), max_digits=4, decimal_places=1, default="0.0"
     )
 
+    class Meta:
+        verbose_name = _("Invoice Item")
+        verbose_name_plural = _("Invoice Items")
+
+    def __str__(self):
+        return f"{self.sku}: {self.description}"
+
     def as_purchased_item(self):
         return PurchasedItem(
             name=self.description,
diff --git a/aleksis/apps/tezor/rules.py b/aleksis/apps/tezor/rules.py
index d595e53b56073a0096341d1d98551a387dbb2f7f..53ab4a7d3d37cca3dcdbc5f017337abbe1f7975c 100644
--- a/aleksis/apps/tezor/rules.py
+++ b/aleksis/apps/tezor/rules.py
@@ -154,5 +154,7 @@ rules.add_perm("tezor.send_invoice_email_rule", send_invoice_email_predicate)
 view_own_invoices_predicate = has_person
 rules.add_perm("tezor.view_own_invoices_list_rule", view_own_invoices_predicate)
 
-view_menu_predicate = view_own_invoices_predicate | view_clients_predicate | view_invoice_groups_predicate
+view_menu_predicate = (
+    view_own_invoices_predicate | view_clients_predicate | view_invoice_groups_predicate
+)
 rules.add_perm("tezor.view_menu_rule", view_menu_predicate)
diff --git a/aleksis/apps/tezor/tables.py b/aleksis/apps/tezor/tables.py
index 187a5a9fd4a0e73c6a99e823d9e08fbdd40432f9..10227b1c90e997cbcd376cfcadb45ba025f3935c 100644
--- a/aleksis/apps/tezor/tables.py
+++ b/aleksis/apps/tezor/tables.py
@@ -102,7 +102,7 @@ class InvoicesTable(tables.Table):
         verbose_name=_("View"),
         text=_("View"),
     )
-    print = tables.LinkColumn(
+    print_action = tables.LinkColumn(
         "print_invoice",
         args=[A("token")],
         verbose_name=_("Print"),
diff --git a/aleksis/apps/tezor/templates/tezor/invoice/full.html b/aleksis/apps/tezor/templates/tezor/invoice/full.html
index 2d20d990cc57f56b3062db772b68766f64dfb71b..16cc9ad74654cf2174e5002bb6f32c4939f1b11b 100644
--- a/aleksis/apps/tezor/templates/tezor/invoice/full.html
+++ b/aleksis/apps/tezor/templates/tezor/invoice/full.html
@@ -108,9 +108,9 @@
             {% endif %}
             {% if object.status == "preauth" %}
             <div class="card-action">
-              <a class="btn waves-effect waves-light green" href="{% url 'mark_invoice_payed_by_token' object.token %}">
+              <a class="btn waves-effect waves-light green" href="{% url 'mark_invoice_paid_by_token' object.token %}">
                 <i class="material-icons left iconify" data-icon="mdi:check-all"></i>
-                {% trans "Mark as payed" %}
+                {% trans "Mark as paid" %}
               </a>
             </div>
             {% endif %}
diff --git a/aleksis/apps/tezor/urls.py b/aleksis/apps/tezor/urls.py
index cb883f1e2b09b3ff6a4d65185ea741d5ec346ec9..6b327c8a407805cd21f5fac5eb391fb1423f2861 100644
--- a/aleksis/apps/tezor/urls.py
+++ b/aleksis/apps/tezor/urls.py
@@ -64,8 +64,8 @@ urlpatterns = [
         name="send_invoice_by_token",
     ),
     path(
-        "invoice/<str:token>/mark_payed/",
+        "invoice/<str:token>/mark_paid/",
         views.MarkPayedView.as_view(),
-        name="mark_invoice_payed_by_token",
+        name="mark_invoice_paid_by_token",
     ),
 ]
diff --git a/aleksis/apps/tezor/views.py b/aleksis/apps/tezor/views.py
index fc21bea109390410916934ead624940076f4983b..b494fca37b314b0f4f7affa7dbe2f22b1001a170 100644
--- a/aleksis/apps/tezor/views.py
+++ b/aleksis/apps/tezor/views.py
@@ -282,7 +282,7 @@ class MyInvoicesListView(PermissionRequiredMixin, SingleTableView):
 class MarkPayedView(PermissionRequiredMixin, View):
 
     model = Invoice
-    permission_required = "tezor.mark_payed_rule"
+    permission_required = "tezor.mark_paid_rule"
     template_name = "tezor/invoice/full.html"
 
 
diff --git a/pyproject.toml b/pyproject.toml
index bc6fb80efb2d0aeb43883ee274c5e6dd2bb4b0e0..c3c19671c6c43a12fc61d987162859cfb13dcfa7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,24 +22,67 @@ classifiers = [
     "Intended Audience :: Education",
     "Topic :: Education"
 ]
+maintainers = ["Jonathan Weth <dev@jonathanweth.de>", "Dominik George <dominik.george@teckids.org>"]
+
+[[tool.poetry.source]]
+name = "PyPI"
+priority = "primary"
 
 [[tool.poetry.source]]
 name = "gitlab"
 url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
-secondary = true
-
+priority = "supplemental"
 [tool.poetry.dependencies]
 python = "^3.9"
 django-payments-sepa = { version = "^1.2.dev0", allow-prereleases = true, extras = ["fints"] }
-aleksis-core = "^3.0"
+aleksis-core = "^4.0.0.dev0"
 django-payments = { version = "^1.0.0", extras = ["sofort"] }
 
-[tool.poetry.dev-dependencies]
-aleksis-builddeps = "*"
-
 [tool.poetry.plugins."aleksis.app"]
 tezor = "aleksis.apps.tezor.apps:DefaultConfig"
 
+
+[tool.poetry.group.dev.dependencies]
+django-stubs = "^4.2"
+
+safety = "^2.3.5"
+
+flake8 = "^6.0.0"
+flake8-django = "^1.0.0"
+flake8-fixme = "^1.1.1"
+flake8-mypy = "^17.8.0"
+flake8-bandit = "^4.1.1"
+flake8-builtins = "^2.0.0"
+flake8-docstrings = "^1.5.0"
+flake8-rst-docstrings = "^0.3.0"
+
+black = ">=21.0"
+flake8-black = "^0.3.0"
+
+isort = "^5.0.0"
+flake8-isort = "^6.0.0"
+
+curlylint = "^0.13.0"
+
+[tool.poetry.group.test.dependencies]
+pytest = "^7.2"
+pytest-django = "^4.1"
+pytest-django-testing-postgresql = "^0.2"
+pytest-cov = "^4.0.0"
+pytest-sugar = "^0.9.2"
+selenium = "<4.10.0"
+freezegun = "^1.1.0"
+
+[tool.poetry.group.docs]
+optional = true
+
+[tool.poetry.group.docs.dependencies]
+sphinx = "^7.0"
+sphinxcontrib-django = "^2.3.0"
+sphinxcontrib-svg2pdfconverter = "^1.1.1"
+sphinx-autodoc-typehints = "^1.7"
+sphinx_material = "^0.0.35"
+
 [tool.black]
 line-length = 100
 exclude = "/migrations/"
@@ -57,5 +100,5 @@ no_autofocus = true
 tabindex_no_positive = true
 
 [build-system]
-requires = ["poetry>=1.0"]
-build-backend = "poetry.masonry.api"
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tox.ini b/tox.ini
index 6e4b77ab1ded935257117696975c2150772cd85c..85c2494a5a2f5480bb05d48781edfaa74803eeab 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@ skip_missing_interpreters = true
 envlist = py39,py310,py311
 
 [testenv]
-whitelist_externals = poetry
+allowlist_externals = poetry
 skip_install = true
 envdir = {toxworkdir}/globalenv
 commands_pre =
@@ -68,18 +68,6 @@ known_django = django
 skip = migrations
 sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
 
-[mypy]
-plugins = mypy_django_plugin.main
-python_version = 3.8
-platform = linux
-show_column_numbers = True
-follow_imports = skip
-ignore_missing_imports = True
-cache_dir = /dev/null
-
-[mypy.plugins.django-stubs]
-django_settings_module = aleksis.core.settings
-
 [pytest]
 DJANGO_SETTINGS_MODULE = aleksis.core.settings
 junit_family = legacy