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/.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..38d141b743fd55678f50077c0617924475817095
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,91 @@
+# 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/
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 32dbe404414d0a0f687a2c026266bf263e8996a7..054e1dc63d669f99a3c67f4384abf77af7d9068c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,28 @@ All notable changes to this project will be documented in this file.
 The format is based on `Keep a Changelog`_,
 and this project adheres to `Semantic Versioning`_.
 
+Unreleased
+----------
+
+This version requires AlekSIS-Core 3.0. It is incompatible with any previous
+version.
+
+Removed
+~~~~~~~
+
+* Legacy menu integration for AlekSIS-Core pre-3.0
+
+
+Added
+~~~~~
+
+* Support for SPA in AlekSIS-Core 3.0
+
+Fixed
+-----
+
+* Replace usage of deprecated `ugettext_lazy` with `gettext_lazy`.
+
 `1.2`_ - 2022-03-20
 -------------------
 
diff --git a/aleksis/apps/tezor/forms.py b/aleksis/apps/tezor/forms.py
index a28e20466fb6aeb1c469404855a34604d1187bc2..15871e4dec9df19d1fd0a2ee3be5515356bef04d 100644
--- a/aleksis/apps/tezor/forms.py
+++ b/aleksis/apps/tezor/forms.py
@@ -34,7 +34,24 @@ class EditClientForm(ExtensibleForm):
 
     class Meta:
         model = Client
-        fields = ["name", "email", "payment_variants"]
+        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):
@@ -43,4 +60,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
new file mode 100644
index 0000000000000000000000000000000000000000..1faad005dfbbe16d7f2190fd2c921721a5e7ed6a
--- /dev/null
+++ b/aleksis/apps/tezor/frontend/index.js
@@ -0,0 +1,133 @@
+export default {
+  meta: {
+    inMenu: true,
+    titleKey: "tezor.menu_title",
+    icon: "mdi-piggy-bank-outline",
+    permission: "tezor.view_menu_rule",
+  },
+  children: [
+    {
+      path: "invoice/:token/print/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.printInvoice",
+    },
+    {
+      path: "invoice/:token/pay",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.doPayment",
+    },
+    {
+      path: "clients/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.clients",
+      meta: {
+        inMenu: true,
+        titleKey: "tezor.clients.menu_title",
+        icon: "mdi-domain",
+        permission: "tezor.can_view_clients",
+      },
+    },
+    {
+      path: "client/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.createClient",
+    },
+    {
+      path: "client/:pk/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.editClientByPk",
+    },
+    {
+      path: "client/:pk/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.deleteClientByPk",
+    },
+    {
+      path: "client/:pk/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.clientByPk",
+    },
+    {
+      path: "client/:pk/invoice_groups/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.createInvoiceGroup",
+    },
+    {
+      path: "invoice_group/:pk/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.editInvoiceGroupByPk",
+    },
+    {
+      path: "invoice_group/:pk/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.invoiceGroupByPk",
+    },
+    {
+      path: "invoice_group/:pk/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.deleteInvoiceGroupByPk",
+    },
+    {
+      path: "invoices/my/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.personalInvoices",
+      meta: {
+        inMenu: true,
+        titleKey: "tezor.personal_invoices.menu_title",
+        icon: "mdi-receipt-outline",
+      },
+    },
+    {
+      path: "invoice/:slug/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.invoiceByToken",
+    },
+    {
+      path: "invoice/:token/send/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+      name: "tezor.sendInvoiceByToken",
+    },
+  ],
+};
diff --git a/aleksis/apps/tezor/frontend/messages/en.json b/aleksis/apps/tezor/frontend/messages/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..24811b78708ef68a2f6d092cf186b36d0641514a
--- /dev/null
+++ b/aleksis/apps/tezor/frontend/messages/en.json
@@ -0,0 +1,11 @@
+{
+  "tezor": {
+    "menu_title": "Payments and Money",
+    "clients": {
+      "menu_title": "Manage clients"
+    },
+    "personal_invoices": {
+      "menu_title": "My invoices"
+    }
+  }
+}
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 03159e1d58131b47cf73e2789a706e350e3c4321..55d0d147a8b0d6cfd697a011ca7903b09dec3552 100644
--- a/aleksis/apps/tezor/locale/de_DE/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/de_DE/LC_MESSAGES/django.po
@@ -8,10 +8,9 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2022-03-22 21:24+0000\n"
-"PO-Revision-Date: 2022-08-03 16:34+0000\n"
-"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
-"Language-Team: German <https://translate.edugit.org/projects/aleksis/"
-"aleksis-app-tezor/de/>\n"
+"PO-Revision-Date: 2023-01-10 19:53+0000\n"
+"Last-Translator: Robert Seimetz <robert.seimetz@teckids.org>\n"
+"Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis-app-tezor/de/>\n"
 "Language: de_DE\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
@@ -219,7 +218,7 @@ msgstr "Art. Nr."
 
 #: aleksis/apps/tezor/tables.py:11
 msgid "Item"
-msgstr "Artikel"
+msgstr "Objekt"
 
 #: aleksis/apps/tezor/tables.py:13
 msgid "Tax Rate"
diff --git a/aleksis/apps/tezor/locale/ru/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/ru/LC_MESSAGES/django.po
index 72578f4ebc9ad160783230d7891396dd906aeabf..507d5d7805c77ed94e21c515323005f12d312855 100644
--- a/aleksis/apps/tezor/locale/ru/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/ru/LC_MESSAGES/django.po
@@ -8,17 +8,14 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2022-04-23 12:54+0000\n"
-"PO-Revision-Date: 2022-06-12 05:32+0000\n"
+"PO-Revision-Date: 2023-05-26 04:39+0000\n"
 "Last-Translator: Serhii Horichenko <m@sgg.im>\n"
-"Language-Team: Russian <https://translate.edugit.org/projects/aleksis/"
-"aleksis-app-tezor/ru/>\n"
+"Language-Team: Russian <https://translate.edugit.org/projects/aleksis/aleksis-app-tezor/ru/>\n"
 "Language: ru\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
-"%100>=11 && n%100<=14)? 2 : 3);\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
 "X-Generator: Weblate 4.12.1\n"
 
 #: aleksis/apps/tezor/forms.py:18
@@ -69,7 +66,7 @@ msgstr "Прямой дебет SEPA"
 
 #: aleksis/apps/tezor/models/base.py:18
 msgid "Name"
-msgstr "Имя"
+msgstr "Полное имя"
 
 #: aleksis/apps/tezor/models/base.py:19
 msgid "Email"
@@ -223,7 +220,7 @@ msgstr "Арт.№"
 
 #: aleksis/apps/tezor/tables.py:11
 msgid "Item"
-msgstr "Артикул"
+msgstr "Объект"
 
 #: aleksis/apps/tezor/tables.py:13
 msgid "Tax Rate"
diff --git a/aleksis/apps/tezor/locale/uk/LC_MESSAGES/django.po b/aleksis/apps/tezor/locale/uk/LC_MESSAGES/django.po
index 209f8eff4a50278cdf2ee396cd39b7eca59022a3..3f14c34808e2d242a10c8c0d8e4a7d54341faae5 100644
--- a/aleksis/apps/tezor/locale/uk/LC_MESSAGES/django.po
+++ b/aleksis/apps/tezor/locale/uk/LC_MESSAGES/django.po
@@ -8,18 +8,14 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2022-04-23 12:54+0000\n"
-"PO-Revision-Date: 2022-07-01 11:55+0000\n"
+"PO-Revision-Date: 2023-01-25 05:58+0000\n"
 "Last-Translator: Serhii Horichenko <m@sgg.im>\n"
-"Language-Team: Ukrainian <https://translate.edugit.org/projects/aleksis/"
-"aleksis-app-tezor/uk/>\n"
+"Language-Team: Ukrainian <https://translate.edugit.org/projects/aleksis/aleksis-app-tezor/uk/>\n"
 "Language: uk\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 "
-"? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > "
-"14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % "
-"100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
 "X-Generator: Weblate 4.12.1\n"
 
 #: aleksis/apps/tezor/forms.py:18
@@ -224,7 +220,7 @@ msgstr "Арт.№"
 
 #: aleksis/apps/tezor/tables.py:11
 msgid "Item"
-msgstr "Артикул"
+msgstr "Об'єкт"
 
 #: aleksis/apps/tezor/tables.py:13
 msgid "Tax Rate"
diff --git a/aleksis/apps/tezor/menus.py b/aleksis/apps/tezor/menus.py
deleted file mode 100644
index ed66d6af6513265871fcc9de8054e6d147d2d690..0000000000000000000000000000000000000000
--- a/aleksis/apps/tezor/menus.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from django.utils.translation import gettext_lazy as _
-
-MENUS = {
-    "NAV_MENU_CORE": [
-        {
-            "name": _("Payments and Money"),
-            "url": "#",
-            "root": True,
-            "svg_icon": "mdi:piggy-bank",
-            "validators": [
-                "menu_generator.validators.is_authenticated",
-                "aleksis.core.util.core_helpers.has_person",
-            ],
-            "submenu": [
-                {
-                    "name": _("Manage clients"),
-                    "url": "clients",
-                    "svg_icon": "mdi:domain",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "tezor.view_clients_rule",
-                        )
-                    ],
-                },
-                {
-                    "name": _("Payment variants"),
-                    "url": "payment_variants",
-                    "svg_icon": "mdi:account-credit-card-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "tezor.view_paymentvariants_rule",
-                        )
-                    ],
-                },
-                {
-                    "name": _("My invoices"),
-                    "url": "personal_invoices",
-                    "svg_icon": "fa6-solid:file-invoice-dollar",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "aleksis.core.util.core_helpers.has_person",
-                    ],
-                },
-            ],
-        }
-    ]
-}
diff --git a/aleksis/apps/tezor/migrations/0001_initial.py b/aleksis/apps/tezor/migrations/0001_initial.py
index cbf05ad8ae5f1d30f15e09b5865f0484d5c9ab7d..a3de6485422db8c19122e4def2e93f803b737b34 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 f7a052ac8ea3fe7b1c1bc41d62b8b722355f4c10..9adda38b122458151649d996cb979b2eba5e1eef 100644
--- a/aleksis/apps/tezor/models/base.py
+++ b/aleksis/apps/tezor/models/base.py
@@ -147,3 +147,6 @@ class Client(ExtensibleModel):
     class Meta:
         verbose_name = _("Client")
         verbose_name_plural = _("Clients")
+        constraints = [
+            models.UniqueConstraint(fields=["name", "site"], name="uniq_client_per_site"),
+        ]
diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py
index 5ea79e900e9c21879b2305d309e702e87e2a28e8..8e5c9d0ed0aada84fa1ed03a0c75e70ef19886c8 100644
--- a/aleksis/apps/tezor/models/invoice.py
+++ b/aleksis/apps/tezor/models/invoice.py
@@ -27,14 +27,16 @@ class InvoiceGroup(ExtensibleModel):
         verbose_name=_("Template to render invoices with as PDF"), blank=True, max_length=255
     )
 
-    def __str__(self) -> str:
-        return self.name
-
     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
+
 
 class Invoice(BasePayment, PureDjangoModel):
     STATUS_ICONS = {
@@ -73,6 +75,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
@@ -100,6 +113,9 @@ class Invoice(BasePayment, PureDjangoModel):
         variants = [v for v in self.group.client.payment_variants.all() if v.label == self.variant]
         return variants[0] if variants else None
 
+    def get_absolute_url(self):
+        return reverse("invoice_by_token", kwargs={"slug": self.token})
+
     def get_variant_name(self):
         return PaymentVariant.get_payment_variants_as_dict()[self.variant].name
 
@@ -127,12 +143,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()
@@ -171,9 +181,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()
 
@@ -192,6 +199,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 eebd93ae55f20cff8fade37929b2849007d5e950..c96f6de07293312f178b3922fb0f8525d154e8ad 100644
--- a/aleksis/apps/tezor/rules.py
+++ b/aleksis/apps/tezor/rules.py
@@ -182,3 +182,8 @@ 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
+)
+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 f4091e710ca9ddb6767515f8b103ab8949ac0c41..97490264c69c0508d6bfe862b8b3691b390c81ce 100644
--- a/aleksis/apps/tezor/tables.py
+++ b/aleksis/apps/tezor/tables.py
@@ -119,7 +119,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/views.py b/aleksis/apps/tezor/views.py
index a5724182e1ca7e5002c12c53dc5d2dad80829b3e..76fb0b9673314f84dbc3a3f1079bfae75e0f1365 100644
--- a/aleksis/apps/tezor/views.py
+++ b/aleksis/apps/tezor/views.py
@@ -194,11 +194,9 @@ class InvoiceGroupCreateView(PermissionRequiredMixin, AdvancedCreateView):
 
     def form_valid(self, form):
         client = Client.objects.get(id=self.kwargs["pk"])
-        InvoiceGroup.objects.create(
-            client=client,
-            name=form.cleaned_data["name"],
-            template_name=form.cleaned_data["template_name"],
-        )
+        self.object = form.save()
+        self.object.client = client
+        self.object.save()
 
         return redirect(self.get_success_url())
 
diff --git a/pyproject.toml b/pyproject.toml
index e888291ee7889b3902944b3e8dd57b9cbba38633..dfd2d24e55b11cca61dbc7ca891df91f902d4fed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Tezor"
-version = "1.2.1.dev0"
+version = "2.0.dev0"
 packages = [
     { include = "aleksis" }
 ]
@@ -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"
-aleksis-core = "^2.8.1.dev0"
+aleksis-core = "^4.0.0.dev0"
 django-payments = { version = "^1.0.0", extras = ["sofort"] }
 django-payments-sepa = "^1.0.1"
 
-[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 4a7c30f2c068c0bf871527ec6e248af8591e7c25..6e4b77ab1ded935257117696975c2150772cd85c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,16 +1,15 @@
 [tox]
 skipsdist = True
 skip_missing_interpreters = true
-envlist = py37,py38,py39
+envlist = py39,py310,py311
 
 [testenv]
 whitelist_externals = poetry
-		      sudo
 skip_install = true
 envdir = {toxworkdir}/globalenv
 commands_pre =
      poetry install
-     poetry run aleksis-admin yarn install
+     poetry run aleksis-admin vite build
      poetry run aleksis-admin collectstatic --no-input
 commands =
     poetry run pytest --cov=. {posargs} aleksis/
@@ -27,6 +26,8 @@ commands =
     poetry run black --check --diff aleksis/
     poetry run isort -c --diff --stdout aleksis/
     poetry run flake8 {posargs} aleksis/
+    poetry run sh -c "aleksis-admin yarn run prettier --check --ignore-path={toxinidir}/.prettierignore {toxinidir}"
+    poetry run sh -c "aleksis-admin yarn run eslint {toxinidir}/aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=."
 
 [testenv:security]
 commands =
@@ -46,6 +47,7 @@ commands = poetry run make -C docs/ html {posargs}
 commands =
     poetry run isort aleksis/
     poetry run black aleksis/
+    poetry run sh -c "aleksis-admin yarn run prettier --write --ignore-path={toxinidir}/.prettierignore {toxinidir}"
 
 [testenv:makemessages]
 commands =
@@ -55,7 +57,7 @@ commands =
 [flake8]
 max_line_length = 100
 exclude = migrations,tests
-ignore = A002,A003,BLK100,E203,E231,W503,D100,D101,D102,D103,D104,D105,D106,D107,RST215,RST214,F821,F841,S106,T100,T101,DJ05
+ignore = BLK100,E203,E231,W503,D100,D101,D102,D103,D104,D105,D106,D107,RST215,RST214,F821,F841,S106,T100,T101,DJ05
 
 [isort]
 profile = black