From 36ee8e4f83b88de9716b3f9031ac293700af6522 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Sun, 12 Apr 2026 10:11:36 +0200 Subject: [PATCH 01/10] Fix: Tests an aktuelle DB-Schema angepasst (eingangs_datum entfernt, bewirtungsfelder auf belege, steuercode-Filter) --- tests/01_BelegUploadTest.php | 26 ++++++------ tests/11_KontierungsvorschlagTest.php | 4 +- tests/16_BewirtungssplitTest.php | 60 +++++++++++++-------------- tests/17_AutomatikkontenUstTest.php | 17 ++++---- 4 files changed, 52 insertions(+), 55 deletions(-) diff --git a/tests/01_BelegUploadTest.php b/tests/01_BelegUploadTest.php index 8a88d53..3f5f294 100644 --- a/tests/01_BelegUploadTest.php +++ b/tests/01_BelegUploadTest.php @@ -18,17 +18,16 @@ class BelegUploadTest extends TestCase $datum = date('Y-m-d'); $id = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => $belegNr, - 'filename' => 'test-rechnung.pdf', - 'storage_path' => 'static/uploads/test/test-rechnung.pdf', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', $belegNr), - 'beleg_datum' => $datum, - 'eingangs_datum' => $datum, - 'rechnungs_nr' => 'RE-TEST-001', - 'betrag_brutto' => 119.00, - 'belegtyp' => 'Eingangsrechnung', - 'notes' => 'Automatisch erstellt (Test)', + 'beleg_nr' => $belegNr, + 'filename' => 'test-rechnung.pdf', + 'storage_path' => 'static/uploads/test/test-rechnung.pdf', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', $belegNr), + 'beleg_datum' => $datum, + 'rechnungs_nr' => 'RE-TEST-001', + 'betrag_brutto' => 119.00, + 'belegtyp' => 'Eingangsrechnung', + 'notes' => 'Automatisch erstellt (Test)', ]); $this->assertGreaterThan(0, $id, 'Beleg wurde in der DB angelegt (ID > 0)'); @@ -51,7 +50,8 @@ class BelegUploadTest extends TestCase $this->assert($found, 'Beleg erscheint in Beleg::all() (Belegübersicht)'); $this->assertGreaterThan(0, $result['total'], 'Belegübersicht enthält mindestens 1 Eintrag'); - // Betrag korrekt gespeichert - $this->assertEquals(119.00, (float)($beleg['betrag_brutto'] ?? 0), 'Betrag brutto korrekt gespeichert'); + // Betrag korrekt gespeichert (direkt aus belege, nicht die berechnete Buchungssumme) + $raw = \Steuerkram\Database::fetchOne('SELECT betrag_brutto FROM belege WHERE id = ?', [$id]); + $this->assertEquals(119.00, (float)($raw['betrag_brutto'] ?? 0), 'Betrag brutto korrekt gespeichert'); } } diff --git a/tests/11_KontierungsvorschlagTest.php b/tests/11_KontierungsvorschlagTest.php index 3b0df11..0d44082 100644 --- a/tests/11_KontierungsvorschlagTest.php +++ b/tests/11_KontierungsvorschlagTest.php @@ -50,9 +50,9 @@ class KontierungsvorschlagTest extends TestCase private function createTestBeleg(): int { Database::execute( - "INSERT INTO belege (beleg_nr, filename, storage_path, mime_type, file_hash, eingangs_datum, belegtyp) + "INSERT INTO belege (beleg_nr, filename, storage_path, mime_type, file_hash, belegtyp) VALUES ('TEST-KI-0001', 'test.pdf', 'static/uploads/test/test.pdf', 'application/pdf', - SHA2('test', 256), CURDATE(), 'Eingangsrechnung')" + SHA2('test', 256), 'Eingangsrechnung')" ); return (int)Database::lastInsertId(); } diff --git a/tests/16_BewirtungssplitTest.php b/tests/16_BewirtungssplitTest.php index 6d3bbcd..172aa6f 100644 --- a/tests/16_BewirtungssplitTest.php +++ b/tests/16_BewirtungssplitTest.php @@ -31,15 +31,18 @@ class BewirtungssplitTest extends TestCase // ── Test 1: Split bei Konto 4650 ───────────────────────────── $beleg1 = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-BEW-4650-' . uniqid(), - 'filename' => 'test.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'bew4650' . uniqid()), - 'beleg_datum' => $datum, - 'eingangs_datum' => $datum, - 'belegtyp' => 'Eingangsrechnung', + 'beleg_nr' => 'TEST-BEW-4650-' . uniqid(), + 'filename' => 'test.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'bew4650' . uniqid()), + 'beleg_datum' => $datum, + 'belegtyp' => 'Eingangsrechnung', ]); + \Steuerkram\Database::execute( + 'UPDATE belege SET bewirtungsgrund = ?, teilnehmerliste = ? WHERE id = ?', + ['Kundengespräch', 'Max Mustermann, Erika Mustermann', $beleg1] + ); // Bewirtungssplit direkt über Booking::create + zweite Buchung simulieren // (wie BookingController::bucheBewirtungssplit() es tut) @@ -57,8 +60,6 @@ class BewirtungssplitTest extends TestCase 'betrag_netto' => $netto70, 'ust_satz' => $ustSatz, 'buchungstext' => 'Bewirtung Test 70%', - 'bewirtungsgrund' => 'Kundengespräch', - 'teilnehmerliste' => 'Max Mustermann, Erika Mustermann', ]); $id1b = \Steuerkram\Models\Booking::create([ @@ -70,8 +71,6 @@ class BewirtungssplitTest extends TestCase 'betrag_netto' => $netto30, 'ust_satz' => $ustSatz, 'buchungstext' => 'Bewirtung Test 30%', - 'bewirtungsgrund' => 'Kundengespräch', - 'teilnehmerliste' => 'Max Mustermann, Erika Mustermann', ]); $b1a = \Steuerkram\Models\Booking::find($id1a); @@ -95,20 +94,20 @@ class BewirtungssplitTest extends TestCase 'Gesamt-Vorsteuer korrekt (100% des Nettobetrags)' ); - // Bewirtungsfelder gespeichert - $this->assertEquals('Kundengespräch', $b1a['bewirtungsgrund'], 'Bewirtungsgrund gespeichert'); - $this->assertEquals('Max Mustermann, Erika Mustermann', $b1a['teilnehmerliste'], 'Teilnehmerliste gespeichert'); + // Bewirtungsfelder auf dem Beleg gespeichert (seit Migration 024 in belege, nicht bookings) + $belegDaten = \Steuerkram\Models\Beleg::find($beleg1); + $this->assertEquals('Kundengespräch', $belegDaten['bewirtungsgrund'], 'Bewirtungsgrund gespeichert'); + $this->assertEquals('Max Mustermann, Erika Mustermann', $belegDaten['teilnehmerliste'], 'Teilnehmerliste gespeichert'); // ── Test 2: Split bei Konto 4651 ───────────────────────────── $beleg2 = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-BEW-4651-' . uniqid(), - 'filename' => 'test2.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'bew4651' . uniqid()), - 'beleg_datum' => $datum, - 'eingangs_datum' => $datum, - 'belegtyp' => 'Eingangsrechnung', + 'beleg_nr' => 'TEST-BEW-4651-' . uniqid(), + 'filename' => 'test2.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'bew4651' . uniqid()), + 'beleg_datum' => $datum, + 'belegtyp' => 'Eingangsrechnung', ]); $id2a = \Steuerkram\Models\Booking::create([ @@ -149,14 +148,13 @@ class BewirtungssplitTest extends TestCase $bankCheck = $this->findAccountId('4600'); if ($bankCheck) { $beleg3 = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-BEW-NORMAL-' . uniqid(), - 'filename' => 'test3.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'normal' . uniqid()), - 'beleg_datum' => $datum, - 'eingangs_datum' => $datum, - 'belegtyp' => 'Eingangsrechnung', + 'beleg_nr' => 'TEST-BEW-NORMAL-' . uniqid(), + 'filename' => 'test3.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'normal' . uniqid()), + 'beleg_datum' => $datum, + 'belegtyp' => 'Eingangsrechnung', ]); $idNormal = \Steuerkram\Models\Booking::create([ diff --git a/tests/17_AutomatikkontenUstTest.php b/tests/17_AutomatikkontenUstTest.php index 8a7077b..9cdf35c 100644 --- a/tests/17_AutomatikkontenUstTest.php +++ b/tests/17_AutomatikkontenUstTest.php @@ -37,7 +37,7 @@ class AutomatikkontenUstTest extends TestCase $mapping = ['VSt19' => 19, 'USt19' => 19, 'VSt7' => 7, 'USt7' => 7]; $kontenMitSteuercode = \Steuerkram\Database::fetchAll( - "SELECT kontonummer, steuercode FROM accounts WHERE steuercode IS NOT NULL LIMIT 20" + "SELECT kontonummer, steuercode FROM accounts WHERE steuercode IS NOT NULL AND steuercode != '' LIMIT 20" ); $this->assertNotEmpty($kontenMitSteuercode, 'Konten mit Steuercode vorhanden'); @@ -60,14 +60,13 @@ class AutomatikkontenUstTest extends TestCase } $belegId = \Steuerkram\Models\Beleg::create([ - 'beleg_nr' => 'TEST-AUTOUST-' . uniqid(), - 'filename' => 'test.pdf', - 'storage_path' => '', - 'mime_type' => 'application/pdf', - 'file_hash' => hash('sha256', 'autoust' . uniqid()), - 'beleg_datum' => $datum, - 'eingangs_datum' => $datum, - 'belegtyp' => 'Eingangsrechnung', + 'beleg_nr' => 'TEST-AUTOUST-' . uniqid(), + 'filename' => 'test.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'autoust' . uniqid()), + 'beleg_datum' => $datum, + 'belegtyp' => 'Eingangsrechnung', ]); $bookingId = \Steuerkram\Models\Booking::create([ From 921ba574198b4becc942b421e19c984c1519fa35 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Sun, 12 Apr 2026 10:40:09 +0200 Subject: [PATCH 02/10] =?UTF-8?q?Feature:=20Anlageverm=C3=B6gen=20?= =?UTF-8?q?=E2=80=93=20Navigation,=20Tabelle=20assets,=20CRUD=20und=20Test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changes.md | 13 ++ migrations/028_assets_anlagevermoegen.sql | 20 +++ public/index.php | 9 ++ src/Controllers/AssetController.php | 154 ++++++++++++++++++++++ src/Models/Asset.php | 113 ++++++++++++++++ src/Views/assets/form.php | 81 ++++++++++++ src/Views/assets/list.php | 57 ++++++++ src/Views/layout/nav.php | 10 ++ tests/20_AnlagevermoegensTest.php | 67 ++++++++++ 9 files changed, 524 insertions(+) create mode 100644 migrations/028_assets_anlagevermoegen.sql create mode 100644 src/Controllers/AssetController.php create mode 100644 src/Models/Asset.php create mode 100644 src/Views/assets/form.php create mode 100644 src/Views/assets/list.php create mode 100644 tests/20_AnlagevermoegensTest.php 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 { + + + + + + [ + 'id' => (int)$a['id'], + 'lfd_nr' => (int)$a['lfd_nr'], + 'bezeichnung' => $a['bezeichnung'], + 'anschaffungsdatum' => $a['anschaffungsdatum'], + 'anschaffungskosten' => (float)$a['anschaffungskosten'], + 'account_id' => $a['account_id'] !== null ? (int)$a['account_id'] : null, +], $assets ?? []), JSON_HEX_TAG); +?> + + + - + diff --git a/src/Views/layout/nav.php b/src/Views/layout/nav.php index ba4433a..3003041 100644 --- a/src/Views/layout/nav.php +++ b/src/Views/layout/nav.php @@ -5,7 +5,7 @@ function navActive(string $prefix, string $current): string { } ?>