diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..9ab59b180fb670006ac910a2bc4524a1376dcea0
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+
+# Ignore developer files when exporting
+.gitattributes         export-ignore
+.gitignore             export-ignore
+.gitlab-ci.yml         export-ignore
+.phive                 export-ignore
+captainhook.json       export-ignore
+phpcs.xml.dist         export-ignore
+phpstan-baseline.neon  export-ignore
+phpstan.dist.neon      export-ignore
+phpunit.dist.xml       export-ignore
+tests                  export-ignore
+
+# Explicitly set file type and line endings for PHP files, improves git diff output
+*.php     text eol=lf diff=php
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b9f7c5c4723a7ff60166164bae8afaeec189067a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+
+tools/
+
+phpstan.neon
+
+.phpunit.result.cache
+
+phpunit.xml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b8d20acb6c365ad8036a7b36d47dabd876d08efa..b5a64b401e554341447c74d7cf93a89ac95a3fdb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,17 @@
 include:
-  - project: 'quiqqer/stabilization/semantic-release'
-    file: '/ci-templates/.gitlab-ci.yml'
+  - component: dev.quiqqer.com/quiqqer/stabilization/ci-cd-components/quiqqer-package-bundle/quiqqer-package-bundle@main
+
+# Remove the entire phpunit-php8.1 block, to allow PHPUnit to run on PHP 8.1 in your pipeline
+phpunit-php8.1:
+  rules:
+    - when: never
+
+# Remove the entire phpunit-php8.2 block, to allow PHPUnit to run on PHP 8.2 in your pipeline
+phpunit-php8.2:
+  rules:
+    - when: never
+
+# Remove the entire phpunit-php8.3 block, to allow PHPUnit to run on PHP 8.3 in your pipeline
+phpunit-php8.3:
+  rules:
+    - when: never
\ No newline at end of file
diff --git a/.phive/phars.xml b/.phive/phars.xml
index a1315a09b4adad780a9c5e52f74835c708c5c7d5..5bfa092bfad10dad9d23240281a5a2041acb815b 100644
--- a/.phive/phars.xml
+++ b/.phive/phars.xml
@@ -1,4 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phive xmlns="https://phar.io/phive">
-  <phar name="phpstan" version="^1.10.67" installed="1.10.67" location="./tools/phpstan" copy="false"/>
+  <phar name="phpstan" version="1.11.8" installed="1.11.8" location="./tools/phpstan" copy="false"/>
+  <phar name="phpunit" version="^10.5.20" installed="10.5.20" location="./tools/phpunit" copy="false"/>
+  <phar name="phpcs" version="^3.10.1" installed="3.10.1" location="./tools/phpcs" copy="false"/>
+  <phar name="phpcbf" version="^3.10.1" installed="3.10.1" location="./tools/phpcbf" copy="false"/>
+  <phar name="captainhook" version="^5.23.3" installed="5.23.3" location="./tools/captainhook" copy="false"/>
 </phive>
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..4a69a59b440e5beec561eca1e341509bd5a18688
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,3 @@
+# Contributing
+
+This package follows the [QUIQQER contribution guidelines](https://dev.quiqqer.com/quiqqer/stabilization/documentation/-/wikis/home).
\ No newline at end of file
diff --git a/captainhook.json b/captainhook.json
new file mode 100644
index 0000000000000000000000000000000000000000..3702e1a358868bedd5ff4c7eae40bb1abb589267
--- /dev/null
+++ b/captainhook.json
@@ -0,0 +1,13 @@
+{
+    "pre-commit": {
+        "enabled": true,
+        "actions": [
+            {
+                "action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting"
+            },
+            {
+                "action": "composer test"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 18460b362545e3c1fb9d13fa9d27155c673f8f2d..8165b1d527f3df9dd0d08069c2d2168a9160c19e 100644
--- a/composer.json
+++ b/composer.json
@@ -1,40 +1,82 @@
 {
-  "name": "quiqqer/erp",
-  "type": "quiqqer-application",
-  "description": "The QUIQQER ERP module bundles all QUIQQER modules which you need for a \nERP (Enterprise Resource Planning) system.",
-  "license": [
-    "GPL-3.0+",
-    "PCSG QEL-1.0"
-  ],
-  "authors": [
-    {
-      "name": "Henning Leutz",
-      "email": "leutz@pcsg.de",
-      "homepage": "https://www.pcsg.de",
-      "role": "Developer"
+    "name": "quiqqer/erp",
+    "type": "quiqqer-application",
+    "description": "The QUIQQER ERP module bundles all QUIQQER modules which you need for a \nERP (Enterprise Resource Planning) system.",
+    "license": [
+        "GPL-3.0+",
+        "PCSG QEL-1.0"
+    ],
+    "authors": [
+        {
+            "name": "Henning Leutz",
+            "email": "leutz@pcsg.de",
+            "homepage": "https://www.pcsg.de",
+            "role": "Developer"
+        }
+    ],
+    "support": {
+        "email": "support@pcsg.de",
+        "url": "https://www.pcsg.de"
+    },
+    "require": {
+        "php": "^8",
+        "quiqqer/core": "^2",
+        "quiqqer/utils": "^2",
+        "quiqqer/watcher": "^2",
+        "quiqqer/translator": "^2",
+        "quiqqer/employee": "^2",
+        "quiqqer/customer": "^2",
+        "quiqqer/qui-php": "^2",
+        "quiqqer/countries": "^2",
+        "quiqqer/frontend-users": "^2",
+        "quiqqer/erp-accounting-templates": "^2",
+        "quiqqer/areas": "^2"
+    },
+    "autoload": {
+        "psr-4": {
+            "QUI\\ERP\\": "src/QUI/ERP"
+        }
+    },
+    "scripts": {
+        "test": [
+            "@dev:lint",
+            "@dev:phpunit"
+        ],
+        "dev:phpunit": "./tools/phpunit",
+        "dev:lint": [
+            "@dev:lint:phpstan",
+            "@dev:lint:style"
+        ],
+        "dev:lint:phpstan": "./tools/phpstan",
+        "dev:lint:style": "./tools/phpcs",
+        "dev:lint:style:fix": "./tools/phpcbf",
+        "dev:init": [
+            "@dev:init:check-requirements",
+            "@dev:init:tools",
+            "@dev:init:git-hooks"
+        ],
+        "dev:init:check-requirements": [
+            "which composer > /dev/null || (echo 'Error: composer has to be globally installed'; exit 1)",
+            "which phive > /dev/null || (echo 'Error: PHIVE has to be globally installed'; exit 1)"
+        ],
+        "dev:init:tools": "phive install --temporary",
+        "dev:init:git-hooks": "./tools/captainhook install --only-enabled --force"
+    },
+    "scripts-aliases": {
+        "test": [
+            "dev:test"
+        ]
+    },
+    "scripts-descriptions": {
+        "test": "Runs linting, static analysis, and unit tests.",
+        "dev:phpunit": "Run PHPUnit test suites",
+        "dev:lint": "Run PHPStan and code style check",
+        "dev:lint:phpstan": "Run PHPStan",
+        "dev:lint:style": "Run code style check (PHP_CodeSniffer)",
+        "dev:lint:style:fix": "Try to fix code style errors automatically",
+        "dev:init": "Initialize the developer tooling (tools and git hooks)",
+        "dev:init:check-requirements": "Check if the necessary requirements are met",
+        "dev:init:tools": "Install all developer tools (requires PHIVE)",
+        "dev:init:git-hooks": "Install all git hooks (may require tools to be installed)"
     }
-  ],
-  "support": {
-    "email": "support@pcsg.de",
-    "url": "https://www.pcsg.de"
-  },
-  "require": {
-    "php": "^8",
-    "quiqqer/core": "^2",
-    "quiqqer/utils": "^2",
-    "quiqqer/watcher": "^2",
-    "quiqqer/translator": "^2",
-    "quiqqer/employee": "^2",
-    "quiqqer/customer": "^2",
-    "quiqqer/qui-php": "^2",
-    "quiqqer/countries": "^2",
-    "quiqqer/frontend-users": "^2",
-    "quiqqer/erp-accounting-templates": "^2",
-    "quiqqer/areas": "^2"
-  },
-  "autoload": {
-    "psr-4": {
-      "QUI\\ERP\\": "src/QUI/ERP"
-    }
-  }
-}
+}
\ No newline at end of file
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..03a5e5e0db9da28cd4d1114d41973c77e059d3fb 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -0,0 +1,351 @@
+parameters:
+	ignoreErrors:
+		-
+			message: "#^Else branch is unreachable because previous condition is always true\\.$#"
+			count: 1
+			path: ajax/calcBruttoPrice.php
+
+		-
+			message: "#^Elseif branch is unreachable because previous condition is always true\\.$#"
+			count: 1
+			path: ajax/calcBruttoPrice.php
+
+		-
+			message: "#^Instanceof between \\*NEVER\\* and QUI\\\\ERP\\\\Tax\\\\TaxEntry will always evaluate to false\\.$#"
+			count: 1
+			path: ajax/calcBruttoPrice.php
+
+		-
+			message: "#^Binary operation \"\\*\" between string and \\(float\\|int\\) results in an error\\.$#"
+			count: 1
+			path: ajax/calcNettoPrice.php
+
+		-
+			message: "#^Else branch is unreachable because previous condition is always true\\.$#"
+			count: 1
+			path: ajax/calcNettoPrice.php
+
+		-
+			message: "#^Elseif branch is unreachable because previous condition is always true\\.$#"
+			count: 1
+			path: ajax/calcNettoPrice.php
+
+		-
+			message: "#^Instanceof between \\*NEVER\\* and QUI\\\\ERP\\\\Tax\\\\TaxEntry will always evaluate to false\\.$#"
+			count: 1
+			path: ajax/calcNettoPrice.php
+
+		-
+			message: "#^Parameter \\#1 \\$string of function substr expects string, float given\\.$#"
+			count: 1
+			path: ajax/calcNettoPrice.php
+
+		-
+			message: "#^Parameter \\#2 \\$string of function explode expects string, float given\\.$#"
+			count: 1
+			path: ajax/calcNettoPrice.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\ErpEntityInterface\\:\\:getCustomerFiles\\(\\) invoked with 1 parameter, 0 required\\.$#"
+			count: 1
+			path: ajax/customerFiles/getFiles.php
+
+		-
+			message: "#^Parameter \\#9 \\$attachedMediaFiles of static method QUI\\\\ERP\\\\Output\\\\Output\\:\\:sendPdfViaMail\\(\\) expects array\\<QUI\\\\Projects\\\\Media\\\\File\\|QUI\\\\Projects\\\\Media\\\\Image\\>, array\\<int\\<0, max\\>, QUI\\\\Interfaces\\\\Projects\\\\Media\\\\File\\> given\\.$#"
+			count: 1
+			path: ajax/output/sendMail.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Accounting\\\\ArticleList\\:\\:getOrder\\(\\) has invalid return type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#"
+			count: 2
+			path: src/QUI/ERP/Accounting/ArticleList.php
+
+		-
+			message: "#^Parameter \\$Order of method QUI\\\\ERP\\\\Accounting\\\\ArticleList\\:\\:setOrder\\(\\) has invalid type QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#"
+			count: 2
+			path: src/QUI/ERP/Accounting/ArticleList.php
+
+		-
+			message: "#^Property QUI\\\\ERP\\\\Accounting\\\\ArticleList\\:\\:\\$Order has unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder as its type\\.$#"
+			count: 2
+			path: src/QUI/ERP/Accounting/ArticleList.php
+
+		-
+			message: "#^Call to method getAttribute\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Call to method getDeliveryAddress\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\AbstractOrder\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Class QUI\\\\ERP\\\\Order\\\\AbstractOrder not found\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Offset 0 on array\\<string, mixed\\> in isset\\(\\) does not exist\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Offset 0 on array\\<string, string\\> in isset\\(\\) does not exist\\.$#"
+			count: 1
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Parameter \\$Invoice of method QUI\\\\ERP\\\\Accounting\\\\Calc\\:\\:calculateInvoicePayments\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#"
+			count: 2
+			path: src/QUI/ERP/Accounting/Calc.php
+
+		-
+			message: "#^Parameter \\$Transaction of method QUI\\\\ERP\\\\ErpTransactionsInterface\\:\\:addTransaction\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Transaction\\.$#"
+			count: 1
+			path: src/QUI/ERP/ErpTransactionsInterface.php
+
+		-
+			message: "#^Parameter \\$Transaction of method QUI\\\\ERP\\\\ErpTransactionsInterface\\:\\:linkTransaction\\(\\) has invalid type QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Transaction\\.$#"
+			count: 1
+			path: src/QUI/ERP/ErpTransactionsInterface.php
+
+		-
+			message: "#^Parameter \\#1 \\$code of method QUI\\\\Interfaces\\\\Users\\\\User\\:\\:activate\\(\\) expects string, false given\\.$#"
+			count: 1
+			path: src/QUI/ERP/Manufacturers.php
+
+		-
+			message: "#^Strict comparison using \\=\\=\\= between mixed and '' will always evaluate to false\\.$#"
+			count: 2
+			path: src/QUI/ERP/Manufacturers.php
+
+		-
+			message: "#^Call to method createPDF\\(\\) on an unknown class QUI\\\\HtmlToPdf\\\\Document\\.$#"
+			count: 1
+			path: src/QUI/ERP/Output/Output.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Output\\\\Output\\:\\:getDocumentPdf\\(\\) has invalid return type QUI\\\\HtmlToPdf\\\\Document\\.$#"
+			count: 2
+			path: src/QUI/ERP/Output/Output.php
+
+		-
+			message: "#^Call to method setContentHTML\\(\\) on an unknown class QUI\\\\HtmlToPdf\\\\Document\\.$#"
+			count: 1
+			path: src/QUI/ERP/Output/OutputTemplate.php
+
+		-
+			message: "#^Call to method setFooterHTML\\(\\) on an unknown class QUI\\\\HtmlToPdf\\\\Document\\.$#"
+			count: 1
+			path: src/QUI/ERP/Output/OutputTemplate.php
+
+		-
+			message: "#^Call to method setHeaderHTML\\(\\) on an unknown class QUI\\\\HtmlToPdf\\\\Document\\.$#"
+			count: 1
+			path: src/QUI/ERP/Output/OutputTemplate.php
+
+		-
+			message: "#^Instantiated class QUI\\\\HtmlToPdf\\\\Document not found\\.$#"
+			count: 1
+			path: src/QUI/ERP/Output/OutputTemplate.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Output\\\\OutputTemplate\\:\\:getPDFDocument\\(\\) has invalid return type QUI\\\\HtmlToPdf\\\\Document\\.$#"
+			count: 2
+			path: src/QUI/ERP/Output/OutputTemplate.php
+
+		-
+			message: "#^Call to method getAmountFormatted\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Transaction\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getAttribute\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getAttribute\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getAttribute\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Offers\\\\Offer\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getDate\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Transaction\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getHash\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Offers\\\\Offer\\.$#"
+			count: 3
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getHash\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Transaction\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getHistory\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getHistory\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getHistory\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Offers\\\\Offer\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getPrefixedNumber\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getPrefixedNumber\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to method getUUID\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Offers\\\\Handler\\.$#"
+			count: 3
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getSalesOrder\\(\\) on an unknown class QUI\\\\ERP\\\\SalesOrders\\\\Handler\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getTableSalesOrders\\(\\) on an unknown class QUI\\\\ERP\\\\SalesOrders\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice not found\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary not found\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Process\\:\\:getInvoices\\(\\) has invalid return type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Invoice\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Process\\:\\:getInvoices\\(\\) has invalid return type QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\InvoiceTemporary\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Process\\:\\:getOffers\\(\\) has invalid return type QUI\\\\ERP\\\\Accounting\\\\Offers\\\\Offer\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Process\\:\\:getOrder\\(\\) has invalid return type QUI\\\\ERP\\\\Order\\\\Order\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Process\\:\\:getOrder\\(\\) has invalid return type QUI\\\\ERP\\\\Order\\\\OrderInProcess\\.$#"
+			count: 2
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Method QUI\\\\ERP\\\\Process\\:\\:getTransactions\\(\\) has invalid return type QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Transaction\\.$#"
+			count: 1
+			path: src/QUI/ERP/Process.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Processes.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Offers\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Processes.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Processes.php
+
+		-
+			message: "#^Call to static method getTableSalesOrders\\(\\) on an unknown class QUI\\\\ERP\\\\SalesOrders\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Processes.php
+
+		-
+			message: "#^Call to static method table\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Factory\\.$#"
+			count: 1
+			path: src/QUI/ERP/Processes.php
+
+		-
+			message: "#^Dead catch \\- Exception is never thrown in the try block\\.$#"
+			count: 2
+			path: src/QUI/ERP/Processes.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Payments\\\\Transactions\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Utils/Process.php
+
+		-
+			message: "#^Call to static method getInstance\\(\\) on an unknown class QUI\\\\ERP\\\\Order\\\\Handler\\.$#"
+			count: 1
+			path: src/QUI/ERP/Utils/Process.php
+
+		-
+			message: "#^Call to static method getInvoiceByString\\(\\) on an unknown class QUI\\\\ERP\\\\Accounting\\\\Invoice\\\\Utils\\\\Invoice\\.$#"
+			count: 1
+			path: src/QUI/ERP/Utils/Process.php
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index 255fa86b89c4b3e84073f38e3ef65bb6bb192c3b..b8ce314ac3f62b7515de9d5a486f56f0f80333e3 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -10,6 +10,7 @@ parameters:
         - tests/phpstan-bootstrap.php
     treatPhpDocTypesAsCertain: false
     customRulesetUsed: true
+    reportUnmatchedIgnoredErrors: false
 services:
     -
         class: \PHPStan\Rules\Properties\TypesAssignedToPropertiesRule
diff --git a/phpunit.dist.xml b/phpunit.dist.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f6c7becf0c12757beb871a9333e2d81e02aa7cae
--- /dev/null
+++ b/phpunit.dist.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="tests/phpunit-bootstrap.php">
+    <testsuites>
+        <testsuite name="Tests">
+            <directory>tests/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
diff --git a/src/QUI/ERP/Accounting/ArticleList.php b/src/QUI/ERP/Accounting/ArticleList.php
index 3409713d9404e31eb6a0e6f594689d658aa93048..fe31ee4fc461954242ec7d8c93d377ee64eba6e5 100644
--- a/src/QUI/ERP/Accounting/ArticleList.php
+++ b/src/QUI/ERP/Accounting/ArticleList.php
@@ -552,8 +552,7 @@ public function renderForMail(): string
     public function toHTMLWithCSS(
         bool|string $template = false,
         bool|string $articleTemplate = false
-    ): string
-    {
+    ): string {
         return $this->toUniqueList()->toHTMLWithCSS($template, $articleTemplate);
     }
 
diff --git a/src/QUI/ERP/Accounting/ArticleListUnique.php b/src/QUI/ERP/Accounting/ArticleListUnique.php
index 05254629eb9d6fcc25c7514c72104b45052bb181..64a421da958d26f9e55702a6fa9f35ce0c0a107f 100644
--- a/src/QUI/ERP/Accounting/ArticleListUnique.php
+++ b/src/QUI/ERP/Accounting/ArticleListUnique.php
@@ -560,8 +560,7 @@ public function toMailHTML(): string
     public function toHTMLWithCSS(
         bool|string $template = false,
         bool|string $articleTemplate = false
-    ): string
-    {
+    ): string {
         $style = '<style>';
         $style .= file_get_contents(dirname(__FILE__) . '/ArticleList.css');
         $style .= '</style>';
@@ -581,8 +580,7 @@ public function toHTMLWithCSS(
     public function render(
         bool|string $template = false,
         bool|string $articleTemplate = false
-    ): string
-    {
+    ): string {
         return $this->toHTMLWithCSS($template, $articleTemplate);
     }
 
diff --git a/src/QUI/ERP/Accounting/Calc.php b/src/QUI/ERP/Accounting/Calc.php
index 7d36657777a3777eac00d5952efcb641ea44c6b8..fa79929ed8850e317d3a60454aabbfd4aac98df6 100644
--- a/src/QUI/ERP/Accounting/Calc.php
+++ b/src/QUI/ERP/Accounting/Calc.php
@@ -14,7 +14,6 @@
 use QUI\ERP\Currency\Currency;
 use QUI\ERP\Money\Price;
 use QUI\Interfaces\Users\User as UserInterface;
-
 use QUI\Locale;
 
 use function array_map;
diff --git a/src/QUI/ERP/EventHandler.php b/src/QUI/ERP/EventHandler.php
index 56e021c1f2a75babcde82ea9329429704e4c3c2c..6c70d02267522165ae53d83b74d7b2f2dbb82138 100644
--- a/src/QUI/ERP/EventHandler.php
+++ b/src/QUI/ERP/EventHandler.php
@@ -19,9 +19,11 @@
 use function dirname;
 use function explode;
 use function is_array;
+use function is_object;
 use function is_string;
 use function json_decode;
 use function json_encode;
+use function method_exists;
 
 /**
  * Class EventHandler
@@ -415,17 +417,41 @@ public static function onSmartyInit(Smarty $Smarty): void
     /**
      * erp smarty function {getPrefixedNumber}
      *
-     * @example {erpGetPrefixedNumber assign=prefixedNumber var=$erpUUID}
-     *
      * @param array $params
      * @param $smarty
      * @return string
+     * @example {erpGetPrefixedNumber assign=prefixedNumber var=$erpUUID}
+     *
      */
     public static function getPrefixedNumber(array $params, $smarty): string
     {
         $prefixedNumber = '';
 
-        if (!empty($params['var'])) {
+        if (empty($params['var'])) {
+            return '';
+        }
+
+        $var = $params['var'];
+
+        if (is_object($var)) {
+            if ($var instanceof ErpEntityInterface) {
+                $prefixedNumber = $var->getPrefixedNumber();
+            } elseif (method_exists($var, 'getPrefixedNumber')) {
+                $prefixedNumber = $var->getPrefixedNumber();
+            } elseif (method_exists($var, 'getId')) {
+                $prefixedNumber = $var->getId();
+            }
+        } elseif (is_array($var) && isset($var['prefixedNumber'])) {
+            $prefixedNumber = $var['prefixedNumber'];
+        } elseif (is_array($var) && isset($var['id_str'])) {
+            $prefixedNumber = $var['id_str'];
+        } elseif (is_array($var) && isset($var['hash'])) {
+            try {
+                $Entity = (new Processes())->getEntity($var['hash']);
+                $prefixedNumber = $Entity->getPrefixedNumber();
+            } catch (QUI\Exception) {
+            }
+        } else {
             try {
                 $Entity = (new Processes())->getEntity($params['var']);
                 $prefixedNumber = $Entity->getPrefixedNumber();
diff --git a/src/QUI/ERP/Process.php b/src/QUI/ERP/Process.php
index a1ffb206f9414ef40a6c918c72f906353598494f..8afbb85b72c45ec5271b2bcaa6e1bbe8558cee3a 100644
--- a/src/QUI/ERP/Process.php
+++ b/src/QUI/ERP/Process.php
@@ -141,7 +141,7 @@ class_exists('QUI\ERP\Accounting\Invoice\Invoice')
                     }
                 }
 
-                if (class_exists('QUI\ERP\SalesOrders\SalesOrder')) {
+                if (class_exists('QUI\ERP\SalesOrders\Handler')) {
                     $salesOrder = $Entity->getPaymentData('salesOrder');
 
                     if ($salesOrder) {
@@ -154,6 +154,45 @@ class_exists('QUI\ERP\Accounting\Invoice\Invoice')
             }
         }
 
+        if (empty($groups)) {
+            if (class_exists('QUI\ERP\Order\Order')) {
+                foreach ($entities as $Entity) {
+                    if (!($Entity instanceof QUI\ERP\Order\Order)) {
+                        continue;
+                    }
+
+                    $uuid = $Entity->getUUID();
+
+                    $groups[$uuid][] = $Entity;
+
+                    if (class_exists('QUI\ERP\SalesOrders\Handler')) {
+                        $salesOrder = $Entity->getPaymentData('salesOrder');
+
+                        if ($salesOrder) {
+                            try {
+                                $groups[$uuid][] = QUI\ERP\SalesOrders\Handler::getSalesOrder($salesOrder['hash']);
+                            } catch (QUI\Exception) {
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (empty($groups)) {
+            if (class_exists('QUI\ERP\SalesOrders\SalesOrder')) {
+                foreach ($entities as $Entity) {
+                    if (!($Entity instanceof QUI\ERP\SalesOrders\SalesOrder)) {
+                        continue;
+                    }
+
+                    $uuid = $Entity->getUUID();
+                    $groups[$uuid][] = $Entity;
+                }
+            }
+        }
+
+
         // not group
         $notGroup = [];
         $isInGroups = function (ErpEntityInterface $Entity) use ($groups) {
@@ -852,6 +891,12 @@ public function getSalesOrders(): array
             return [];
         }
 
+        if (!class_exists('QUI\ERP\SalesOrders\Handler')) {
+            return [];
+        }
+
+        $result = [];
+
         try {
             $salesOrders = QUI::getDatabase()->fetch([
                 'select' => 'id,hash,global_process_id,date',
@@ -865,8 +910,6 @@ public function getSalesOrders(): array
             return [];
         }
 
-        $result = [];
-
         foreach ($salesOrders as $salesOrder) {
             try {
                 $result[] = QUI\ERP\SalesOrders\Handler::getSalesOrder($salesOrder['id']);
@@ -874,6 +917,27 @@ public function getSalesOrders(): array
             }
         }
 
+        // drafts
+        try {
+            $salesOrderDrafts = QUI::getDatabase()->fetch([
+                'select' => 'id,hash,global_process_id,date',
+                'from' => QUI\ERP\SalesOrders\Handler::getTableSalesOrderDrafts(),
+                'where_or' => [
+                    'global_process_id' => $this->processId,
+                    'hash' => $this->processId
+                ]
+            ]);
+        } catch (\Exception) {
+            return [];
+        }
+
+        foreach ($salesOrderDrafts as $salesOrder) {
+            try {
+                $result[] = QUI\ERP\SalesOrders\Handler::getSalesOrder($salesOrder['id']);
+            } catch (\Exception) {
+            }
+        }
+
         return $result;
     }
 
diff --git a/src/QUI/ERP/Processes.php b/src/QUI/ERP/Processes.php
index 8d3196dc685bd535a0a8b7ef92068437147e3dc1..ecb9a8666ff47268397e785bf7d9f8922b0111a2 100644
--- a/src/QUI/ERP/Processes.php
+++ b/src/QUI/ERP/Processes.php
@@ -57,6 +57,16 @@ public function getEntity($entityHash, $entityPlugin = false): ErpEntityInterfac
             }
         }
 
+        if (
+            ($entityPlugin === false || $entityPlugin === 'quiqqer/dunning')
+            && class_exists('QUI\ERP\Accounting\Dunning\Handler')
+        ) {
+            try {
+                return QUI\ERP\Accounting\Dunning\Handler::getInstance()->getDunningProcess($entityHash);
+            } catch (\Exception) {
+            }
+        }
+
         if ($entityPlugin === false || $entityPlugin === 'quiqqer/delivery-notes') {
             try {
                 // @todo quiqqer/delivery-notes
diff --git a/tests/phpunit-bootstrap.php b/tests/phpunit-bootstrap.php
new file mode 100644
index 0000000000000000000000000000000000000000..eca92fd67bed8ae4ec424ed82d300119d792f042
--- /dev/null
+++ b/tests/phpunit-bootstrap.php
@@ -0,0 +1,11 @@
+<?php
+
+if (!defined('QUIQQER_SYSTEM')) {
+    define('QUIQQER_SYSTEM', true);
+}
+
+if (!defined('QUIQQER_AJAX')) {
+    define('QUIQQER_AJAX', true);
+}
+
+require_once __DIR__ . '/../../../../bootstrap.php';
diff --git a/tools/phpstan b/tools/phpstan
deleted file mode 120000
index 54ee46842f744a4e7fbd1ebf83ebcb14059f7e15..0000000000000000000000000000000000000000
--- a/tools/phpstan
+++ /dev/null
@@ -1 +0,0 @@
-/home/hen/.phive/phars/phpstan-1.10.67.phar
\ No newline at end of file