diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 9acc8e70cb8f3c2432a833234a9c0a120ee05585..7e73a33983a777d1fce769c2fad008b26e91d5ab 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -66,6 +66,8 @@
 - Die Evaluationen wurden ausgebaut. Stattdessen sollte man nun die neuen Fragebögen verwenden ([Issue #3787]https://gitlab.studip.de/studip/studip/-/issues/3787)
 - Die Klassen `DbView`, `DbSnapshot` und die zugehörigen Dateien in `lib/dbviews` wurden ausgebaut. ([Issue #4390](https://gitlab.studip.de/studip/studip/-/issues/4390))
   - Als Ersatz dienen Datenbankabfragen mittels der `DBManager`-Klasse oder mittels `SimpleORMap`-Modellen.
+- Es wurden zwei neue CLI-Kommandos hinzugefügt, womit man Klassenrümpfe für SORM-Models und Migrationen erstellen kann. Bei den Migrationen wird die Versionsnummer für die jeweilige `domain` automatisch ermittelt.
+    - `cli/studip make:model` und `cli/studip make:migration`.
 
 ## Security related issues
 
diff --git a/cli/Commands/Make/Migration.php b/cli/Commands/Make/Migration.php
new file mode 100644
index 0000000000000000000000000000000000000000..d9ae1d161728791818f6b168478c590ec785f6de
--- /dev/null
+++ b/cli/Commands/Make/Migration.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Studip\Cli\Commands\Make;
+
+use Nette\PhpGenerator\PhpFile;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+final class Migration extends Command
+{
+    private const DEFAULT_BRANCH = '0';
+
+    protected static $defaultName = 'make:migration';
+
+    protected function configure(): void
+    {
+        $this->setDescription('Create a new migration file');
+        $this->addArgument('name', InputArgument::REQUIRED, 'The name of the migration');
+        $this->addOption(
+            'branch',
+            'b',
+            InputOption::VALUE_OPTIONAL,
+            'The branch of the migration file',
+            self::DEFAULT_BRANCH
+        );
+        $this->addOption('domain', 'd', InputOption::VALUE_OPTIONAL, 'The domain of the migration file', 'studip');
+
+        $defaultPath = $GLOBALS['STUDIP_BASE_PATH'] . '/db/migrations';
+        $this->addOption(
+            'path',
+            'p',
+            InputOption::VALUE_OPTIONAL,
+            'The location where the migration file should be created',
+            $defaultPath
+        );
+        $this->addOption(
+            'description',
+            'D',
+            InputOption::VALUE_OPTIONAL,
+            'The description for the migration',
+            ''
+        );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $branch      = $input->getOption('branch');
+        $domain      = $input->getOption('domain');
+        $name        = $input->getArgument('name');
+        $path        = $input->getOption('path');
+        $description = $input->getOption('description');
+        $verbose     = $input->getOption('verbose');
+
+        $version  = $this->getNextMigrationVersion($branch, $domain, $path, $verbose);
+        $filename = $this->createMigrationFile($path, $version, $name, $description);
+
+        if ($verbose) {
+            $output->writeln('Migration file ' . $filename . ' created.');
+        }
+
+        return Command::SUCCESS;
+    }
+
+    private function getNextMigrationVersion(string $branch, string $domain, string $path, bool $verbose): string
+    {
+        $version     = new \DBSchemaVersion($domain, $branch);
+        $migrator    = new \Migrator($path, $version, $verbose);
+        $topVersions = $migrator->topVersion(true);
+
+        if ($branch === self::DEFAULT_BRANCH) {
+            $branches = array_keys($topVersions);
+            usort($branches, 'version_compare');
+            $branch = array_pop($branches);
+        }
+
+        return sprintf('%s.%s', $branch, isset($topVersions[$branch]) ? $topVersions[$branch] + 1 : 1);
+    }
+
+    private function createMigrationFile(string $path, string $version, string $name, string $description): string
+    {
+        if ($description === '') {
+            $description = '// Add content';
+        } else {
+            $description = "return '$description';";
+        }
+        $file  = new PhpFile();
+        $class = $file->addClass(str_replace(' ', '', ucwords($name)));
+        $class
+            ->setFinal()
+            ->setExtends(\Migration::class)
+            ->addComment("Description of class.\nSecond line\n");
+        $class->addMethod('description')->addBody($description);
+        $class->addMethod('up')->addBody('// Add content');
+        $class->addMethod('down')->addBody('// Add content');
+
+        $printer       = new StudipClassPrinter();
+        $result        = $printer->printFile($file);
+        $migrationName = $version . '_' . str_replace(' ', '_', lcfirst($name));
+        $filename      = $path . '/' . $migrationName . '.php';
+
+        file_put_contents($filename, $result);
+
+        return $filename;
+    }
+}
diff --git a/cli/Commands/Make/Model.php b/cli/Commands/Make/Model.php
new file mode 100644
index 0000000000000000000000000000000000000000..57b15af743215e93f409663f91ca2416ac2251b0
--- /dev/null
+++ b/cli/Commands/Make/Model.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Studip\Cli\Commands\Make;
+
+use Nette\PhpGenerator\PhpFile;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ChoiceQuestion;
+
+
+final class Model extends Command
+{
+    protected static $defaultName = 'make:model';
+
+    protected function configure(): void
+    {
+        $this->setDescription('Create a sorm-model file');
+        $this->addArgument('name', InputArgument::REQUIRED, 'The name of the sorm-model');
+        $this->addArgument('db-table', InputArgument::OPTIONAL, 'The name of the related db-table');
+        $this->addOption('namespace', 's', InputOption::VALUE_OPTIONAL, 'Namespace', '');
+        $defaultPath = $GLOBALS['STUDIP_BASE_PATH'] . '/lib/models';
+        $this->addOption(
+            'path',
+            'p',
+            InputOption::VALUE_OPTIONAL,
+            'The location where the model file should be created',
+            $defaultPath
+        );
+    }
+
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $namespace = $input->getOption('namespace');
+        $name      = $input->getArgument('name');
+        $dbTable   = $input->getArgument('db-table');
+        $path      = $input->getOption('path');
+        $verbose   = $input->getOption('verbose');
+
+        $filename = $this->createModelFile(
+            $path,
+            $name,
+            $input,
+            $output,
+            $dbTable,
+            $namespace
+        );
+
+        if ($verbose) {
+            $output->writeln('Model file ' . $filename . ' created.');
+        }
+
+        return Command::SUCCESS;
+    }
+
+    private function createModelFile(
+        string $path,
+        string $name,
+        InputInterface $input,
+        OutputInterface $output,
+        string $dbTable = null,
+        string $namespace = null,
+    ): string
+    {
+        if (!$dbTable) {
+            $dbTable = strtosnakecase($name);
+        }
+
+        $file      = new PhpFile();
+        $className = str_replace(' ', '', ucwords($name));
+
+        if ($namespace) {
+            $className = ucfirst($namespace) . '\\' . $className;
+        }
+        $class = $file->addClass($className);
+        $class->setExtends(\SimpleORMap::class);
+        $class->addComment(ucfirst($name) . '.php');
+        $class->addComment('model class for table ' . $dbTable);
+        $method = $class->addMethod('configure')
+            ->setStatic()
+            ->setProtected();
+
+        $method->addBody(sprintf('$config[\'db_table\'] = \'%s\';', $dbTable));
+        $method->addBody('parent::configure($config);');
+        $method->addParameter('config', []);
+
+
+        $printer = new StudipClassPrinter();
+        $result  = $printer->printFile($file);
+
+        $modelName = str_replace(' ', '_', ucfirst($name));
+        $filename  = $path . '/' . $modelName . '.php';
+
+        file_put_contents($filename, $result);
+
+        $helper = $this->getHelper('question');
+
+        $tableExists = \DBManager::get()->execute('SHOW TABLES LIKE ?', [$dbTable]);
+
+        $describeModel = false;
+        if ($tableExists) {
+            $question = new ChoiceQuestion(
+                "\nDescribe model:\n",
+                $modelName
+            );
+            $describeModel  = $helper->ask($input, $output, $question);
+        }
+
+        if ($describeModel) {
+            $greetInput = new ArrayInput([
+                'command' => 'sorm:describe',
+                'name'    => 'Fabien',
+                '--yell'  => true,
+            ]);
+
+            $returnCode = $this->getApplication()->doRun($greetInput, $output);
+        }
+
+        return $filename;
+    }
+}
diff --git a/cli/Commands/Make/StudipClassPrinter.php b/cli/Commands/Make/StudipClassPrinter.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e860850ef6bb909099529487725c7a9f4e15287
--- /dev/null
+++ b/cli/Commands/Make/StudipClassPrinter.php
@@ -0,0 +1,18 @@
+<?php
+namespace Studip\Cli\Commands\Make;
+
+use Nette\PhpGenerator\Printer;
+
+final class StudipClassPrinter extends Printer
+{
+    /** length of the line after which the line will break */
+    public int $wrapLength = 120;
+    /** indentation character, can be replaced with a sequence of spaces */
+    public string $indentation = '    ';
+    /** number of blank lines between properties */
+    public int $linesBetweenProperties = 0;
+    /** number of blank lines between methods */
+    public int $linesBetweenMethods = 1;
+
+    public string $returnTypeColon = ': ';
+}
diff --git a/cli/studip b/cli/studip
index 858dc3d2e3fdbd32009dfcda385259b0cd7cd590..4d19e2b1149550418f3b4f315cd542e2c131f5f5 100755
--- a/cli/studip
+++ b/cli/studip
@@ -32,6 +32,8 @@ $commands = [
     Commands\Cronjobs\CronjobWorker::class,
     Commands\DB\Dump::class,
     Commands\DB\MoveMatrikelnummer::class,
+    Commands\Make\Migration::class,
+    Commands\Make\Model::class,
     Commands\DI\Reset::class,
     Commands\Files\Dump::class,
     Commands\Fix\Biest7789::class,
diff --git a/composer.json b/composer.json
index e1288181c1c53a2f060cfd6b40d1f5ff5831307a..efafcdbd86bf5b52b551486c34f22a885c2fa9dd 100644
--- a/composer.json
+++ b/composer.json
@@ -128,7 +128,8 @@
         "symfony/polyfill-php84": "1.30.0",
         "nyholm/psr7": "1.8.1",
         "nyholm/psr7-server": "1.1.0",
-        "league/oauth2-client": "2.7.0"
+        "league/oauth2-client": "2.7.0",
+        "nette/php-generator": "4.1.5"
     },
     "replace": {
         "symfony/polyfill-php73": "*",
diff --git a/composer.lock b/composer.lock
index 0cc31d19a4aea87bb0e1198220a1fe09873d5b65..14101efc5d97e18885a1b348d5c60d9884b8ffbc 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "cad2a823e38968efd43dc8b63fdd8812",
+    "content-hash": "5ec4df54d92509cb7d4ff4f9dc95803b",
     "packages": [
         {
             "name": "algo26-matthias/idna-convert",
@@ -2026,6 +2026,161 @@
             },
             "time": "2022-11-28T03:29:06+00:00"
         },
+        {
+            "name": "nette/php-generator",
+            "version": "v4.1.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nette/php-generator.git",
+                "reference": "690b00d81d42d5633e4457c43ef9754573b6f9d6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nette/php-generator/zipball/690b00d81d42d5633e4457c43ef9754573b6f9d6",
+                "reference": "690b00d81d42d5633e4457c43ef9754573b6f9d6",
+                "shasum": ""
+            },
+            "require": {
+                "nette/utils": "^3.2.9 || ^4.0",
+                "php": "8.0 - 8.3"
+            },
+            "require-dev": {
+                "jetbrains/phpstorm-attributes": "dev-master",
+                "nette/tester": "^2.4",
+                "nikic/php-parser": "^4.18 || ^5.0",
+                "phpstan/phpstan": "^1.0",
+                "tracy/tracy": "^2.8"
+            },
+            "suggest": {
+                "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause",
+                "GPL-2.0-only",
+                "GPL-3.0-only"
+            ],
+            "authors": [
+                {
+                    "name": "David Grudl",
+                    "homepage": "https://davidgrudl.com"
+                },
+                {
+                    "name": "Nette Community",
+                    "homepage": "https://nette.org/contributors"
+                }
+            ],
+            "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.3 features.",
+            "homepage": "https://nette.org",
+            "keywords": [
+                "code",
+                "nette",
+                "php",
+                "scaffolding"
+            ],
+            "support": {
+                "issues": "https://github.com/nette/php-generator/issues",
+                "source": "https://github.com/nette/php-generator/tree/v4.1.5"
+            },
+            "time": "2024-05-12T17:31:02+00:00"
+        },
+        {
+            "name": "nette/utils",
+            "version": "v4.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nette/utils.git",
+                "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
+                "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
+                "shasum": ""
+            },
+            "require": {
+                "php": "8.0 - 8.4"
+            },
+            "conflict": {
+                "nette/finder": "<3",
+                "nette/schema": "<1.2.2"
+            },
+            "require-dev": {
+                "jetbrains/phpstorm-attributes": "dev-master",
+                "nette/tester": "^2.5",
+                "phpstan/phpstan": "^1.0",
+                "tracy/tracy": "^2.9"
+            },
+            "suggest": {
+                "ext-gd": "to use Image",
+                "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
+                "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
+                "ext-json": "to use Nette\\Utils\\Json",
+                "ext-mbstring": "to use Strings::lower() etc...",
+                "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause",
+                "GPL-2.0-only",
+                "GPL-3.0-only"
+            ],
+            "authors": [
+                {
+                    "name": "David Grudl",
+                    "homepage": "https://davidgrudl.com"
+                },
+                {
+                    "name": "Nette Community",
+                    "homepage": "https://nette.org/contributors"
+                }
+            ],
+            "description": "🛠  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
+            "homepage": "https://nette.org",
+            "keywords": [
+                "array",
+                "core",
+                "datetime",
+                "images",
+                "json",
+                "nette",
+                "paginator",
+                "password",
+                "slugify",
+                "string",
+                "unicode",
+                "utf-8",
+                "utility",
+                "validation"
+            ],
+            "support": {
+                "issues": "https://github.com/nette/utils/issues",
+                "source": "https://github.com/nette/utils/tree/v4.0.5"
+            },
+            "time": "2024-08-07T15:39:19+00:00"
+        },
         {
             "name": "nikic/fast-route",
             "version": "v1.3.0",