diff --git a/Changes.md b/Changes.md index d3aba7c..c42af07 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,19 @@ --- +## 2026-04-12 – Anlagevermögen (Wirtschaftsgüter) + +### Neu +- Navigation: neuer Abschnitt „Betriebsvermögen" mit Menüpunkt „Anlagevermögen" +- `migrations/028_assets_anlagevermoegen.sql`: neue Tabelle `assets` für Wirtschaftsgüter +- `src/Models/Asset.php`: CRUD-Model mit automatischer laufender Nummer und Geschäftsjahrzuordnung +- `src/Controllers/AssetController.php`: Liste, Anlegen, Bearbeiten, Löschen +- `src/Views/assets/list.php`: Anlagenspiegel-Tabelle mit Bleistift- und Papierkorb-Aktionen +- `src/Views/assets/form.php`: Formular für Wirtschaftsgut (Bezeichnung, Anschaffungsdatum, Kosten, Zugänge, Abgänge, AfA, Nutzungsdauer, Notizen) +- `tests/20_AnlagevermoegensTest.php`: 14 Tests für CRUD-Operationen + +--- + ## 2026-04-12 – Backup: smarte Rotation und Lock-Schutz ### Geändert diff --git a/migrations/028_assets_anlagevermoegen.sql b/migrations/028_assets_anlagevermoegen.sql new file mode 100644 index 0000000..84e2f88 --- /dev/null +++ b/migrations/028_assets_anlagevermoegen.sql @@ -0,0 +1,20 @@ +-- Migration 028: Tabelle für Anlagevermögen (Wirtschaftsgüter) +CREATE TABLE IF NOT EXISTS assets ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + lfd_nr INT UNSIGNED NOT NULL COMMENT 'Laufende Nummer im Anlagenspiegel', + bezeichnung VARCHAR(255) NOT NULL COMMENT 'Bezeichnung des Wirtschaftsguts', + anschaffungsdatum DATE NOT NULL, + anschaffungskosten DECIMAL(12,2) NOT NULL DEFAULT 0.00, + zugaenge DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 'Zugänge im laufenden Wirtschaftsjahr', + abgaenge DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 'Abgänge im laufenden Wirtschaftsjahr', + abschreibung_jahr DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 'Abschreibung des laufenden Wirtschaftsjahres', + nutzungsdauer SMALLINT UNSIGNED NULL COMMENT 'Nutzungsdauer in Jahren (für AfA-Berechnung)', + notizen TEXT NULL, + fiscal_year_id INT UNSIGNED NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_assets_fiscal_year (fiscal_year_id), + KEY idx_assets_lfd_nr (lfd_nr) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Anlagevermögen / Wirtschaftsgüter (Anlagenspiegel)'; diff --git a/public/index.php b/public/index.php index 15d739f..a97660f 100644 --- a/public/index.php +++ b/public/index.php @@ -19,6 +19,7 @@ use Steuerkram\Controllers\UstvaController; use Steuerkram\Controllers\EuerController; use Steuerkram\Controllers\SettingsController; use Steuerkram\Controllers\LoginController; +use Steuerkram\Controllers\AssetController; use Steuerkram\Controllers\UserAdminController; Config::load(); @@ -97,6 +98,14 @@ $router->post('/invoices/{id}/cancel', fn($p) => (new InvoiceController())->canc $router->post('/invoices/{id}/lines', fn($p) => (new InvoiceController())->addLine($p)); $router->post('/invoices/{id}/lines/{line_id}/delete', fn($p) => (new InvoiceController())->deleteLine($p)); +// ── Anlagevermögen ─────────────────────────────────────────── +$router->get('/assets', fn($p) => (new AssetController())->index($p)); +$router->get('/assets/new', fn($p) => (new AssetController())->create($p)); +$router->post('/assets', fn($p) => (new AssetController())->store($p)); +$router->get('/assets/{id}/edit', fn($p) => (new AssetController())->edit($p)); +$router->post('/assets/{id}', fn($p) => (new AssetController())->update($p)); +$router->post('/assets/{id}/delete', fn($p) => (new AssetController())->destroy($p)); + // ── UStVA ──────────────────────────────────────────────────── $router->get('/tax/ustva', fn($p) => (new UstvaController())->index($p)); $router->get('/tax/ustva/{year}/{month}', fn($p) => (new UstvaController())->show($p)); diff --git a/src/Controllers/AssetController.php b/src/Controllers/AssetController.php new file mode 100644 index 0000000..6f435ac --- /dev/null +++ b/src/Controllers/AssetController.php @@ -0,0 +1,154 @@ +render('layout/base.php', [ + 'pageTitle' => 'Anlagevermögen', + 'content' => $this->renderView('assets/list', [ + 'assets' => $assets, + ]), + 'topbarActions' => '+ Wirtschaftsgut anlegen', + ]); + } + + public function create(array $params): void + { + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Wirtschaftsgut anlegen', + 'content' => $this->renderView('assets/form', [ + 'asset' => [], + 'errors' => [], + 'action' => '/assets', + ]), + ]); + } + + public function store(array $params): void + { + $data = $this->extractPost(); + $errors = $this->validate($data); + + if ($errors) { + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Wirtschaftsgut anlegen', + 'content' => $this->renderView('assets/form', [ + 'asset' => $data, + 'errors' => $errors, + 'action' => '/assets', + ]), + ]); + return; + } + + Asset::create($data); + View::redirect('/assets', 'Wirtschaftsgut erfolgreich angelegt.'); + } + + public function edit(array $params): void + { + $asset = Asset::find((int)$params['id']); + if (!$asset) { http_response_code(404); return; } + + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Wirtschaftsgut bearbeiten', + 'content' => $this->renderView('assets/form', [ + 'asset' => $asset, + 'errors' => [], + 'action' => '/assets/' . $asset['id'], + ]), + ]); + } + + public function update(array $params): void + { + $id = (int)$params['id']; + $asset = Asset::find($id); + if (!$asset) { http_response_code(404); return; } + + $data = $this->extractPost(); + $errors = $this->validate($data); + + if ($errors) { + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Wirtschaftsgut bearbeiten', + 'content' => $this->renderView('assets/form', [ + 'asset' => array_merge($asset, $data), + 'errors' => $errors, + 'action' => '/assets/' . $id, + ]), + ]); + return; + } + + Asset::update($id, $data); + View::redirect('/assets', 'Wirtschaftsgut aktualisiert.'); + } + + public function destroy(array $params): void + { + if (!Asset::delete((int)$params['id'])) { + View::redirect('/assets', 'Wirtschaftsgut konnte nicht gelöscht werden.', 'error'); + return; + } + View::redirect('/assets', 'Wirtschaftsgut gelöscht.'); + } + + // ── Private helpers ────────────────────────────────────────── + + private function extractPost(): array + { + return [ + 'bezeichnung' => trim($_POST['bezeichnung'] ?? ''), + 'anschaffungsdatum' => trim($_POST['anschaffungsdatum'] ?? ''), + 'anschaffungskosten' => $_POST['anschaffungskosten'] ?? '0', + 'zugaenge' => $_POST['zugaenge'] ?? '0', + 'abgaenge' => $_POST['abgaenge'] ?? '0', + 'abschreibung_jahr' => $_POST['abschreibung_jahr'] ?? '0', + 'nutzungsdauer' => trim($_POST['nutzungsdauer'] ?? ''), + 'notizen' => trim($_POST['notizen'] ?? ''), + ]; + } + + private function validate(array $data): array + { + $errors = []; + + if ($data['bezeichnung'] === '') { + $errors['bezeichnung'] = 'Pflichtfeld'; + } + if ($data['anschaffungsdatum'] === '') { + $errors['anschaffungsdatum'] = 'Pflichtfeld'; + } elseif (!strtotime($data['anschaffungsdatum'])) { + $errors['anschaffungsdatum'] = 'Ungültiges Datum'; + } + if (!is_numeric($data['anschaffungskosten']) || (float)$data['anschaffungskosten'] < 0) { + $errors['anschaffungskosten'] = 'Muss eine positive Zahl sein'; + } + + return $errors; + } + + private function renderView(string $template, array $data): string + { + extract($data, EXTR_SKIP); + ob_start(); + include dirname(__DIR__) . '/Views/' . $template . '.php'; + return ob_get_clean(); + } +} diff --git a/src/Models/Asset.php b/src/Models/Asset.php new file mode 100644 index 0000000..9b7f5e5 --- /dev/null +++ b/src/Models/Asset.php @@ -0,0 +1,113 @@ + View::h($v); ?> + +
+
+
+ +
+ + + +
+ +
+ +
+ + + +
+ +
+ +
+ + + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + Abbrechen +
+
+
+
diff --git a/src/Views/assets/list.php b/src/Views/assets/list.php new file mode 100644 index 0000000..bb5c113 --- /dev/null +++ b/src/Views/assets/list.php @@ -0,0 +1,57 @@ + View::h($v); ?> + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nr.AnschaffungsdatumWirtschaftsgutAnschaffungskostenZugängeAbgängeAbschreibungen
€ € € € +
+ + + + +
+ +
+
+
+ Noch keine Wirtschaftsgüter angelegt. +
+
+
diff --git a/src/Views/layout/nav.php b/src/Views/layout/nav.php index ba4433a..2831e11 100644 --- a/src/Views/layout/nav.php +++ b/src/Views/layout/nav.php @@ -49,6 +49,16 @@ function navActive(string $prefix, string $current): string { + +