Fix #7: Lieferantenmanagement mit Dropdown-Auswahl an Belegen hinzufügen

This commit is contained in:
fds2610 2026-03-26 21:06:48 +01:00
parent 7609d1e4e3
commit 29295989aa
13 changed files with 464 additions and 1 deletions

View file

@ -37,6 +37,13 @@ $this->assertGreaterThan($min, $wert, 'Beschreibung');
$this->assertNotEmpty($wert, 'Beschreibung'); $this->assertNotEmpty($wert, 'Beschreibung');
``` ```
## Dokumentation aktuell halten
| Datei | Wann aktualisieren |
|---|---|
| `Changes.md` | Bei jedem Feature, Bugfix oder strukturellen Änderung (nicht bei reinen Test- oder Doku-Commits) |
| `README.md` | Wenn sich der Funktionsumfang, Tech Stack oder die Installationsanleitung ändert |
## Commits ## Commits
``` ```
Fix #NR: Kurze Beschreibung der Änderung Fix #NR: Kurze Beschreibung der Änderung

View file

@ -2,6 +2,24 @@
--- ---
## 2026-03-26 – Lieferantenmanagement (#7)
### Neu
- **Lieferantenverwaltung**: Neue Seite `/suppliers` zum Anlegen, Bearbeiten und Löschen von Lieferanten
- **Lieferant am Beleg**: Im Metadaten-Formular der Belegdetailseite kann ein Lieferant per Dropdown/Freitexteingabe ausgewählt werden; unbekannte Namen werden beim Speichern automatisch angelegt
- **Datenbank**: Tabelle `suppliers` (id, name) und Fremdschlüssel `supplier_id` in `belege`
- **Navigation**: Neuer Menüpunkt "Lieferanten" im Bereich Belege & Buchungen
### Betrifft
- Migration: `007_suppliers.sql`
- Model: `Supplier.php` (neu), `Beleg.php` (JOIN + updateMeta)
- Controller: `SupplierController.php` (neu), `BelegController.php`
- Views: `suppliers/list.php`, `suppliers/form.php` (neu), `belege/detail.php`, `layout/nav.php`
- Routes: `public/index.php`
- Test: `08_LieferantTest.php`
---
## 2026-03-26 – EÜR-Buchungsmodell (Soll/Haben → Einnahme/Ausgabe) ## 2026-03-26 – EÜR-Buchungsmodell (Soll/Haben → Einnahme/Ausgabe)
### Geändert ### Geändert

View file

@ -0,0 +1,20 @@
-- Steuerkram - Lieferantenmanagement
SET NAMES utf8mb4;
-- ============================================================
-- Lieferanten
-- ============================================================
CREATE TABLE IF NOT EXISTS suppliers (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_supplier_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Lieferant-Referenz in Belege
ALTER TABLE belege
ADD COLUMN supplier_id INT UNSIGNED NULL DEFAULT NULL AFTER belegtyp,
ADD CONSTRAINT fk_belege_supplier
FOREIGN KEY (supplier_id) REFERENCES suppliers (id) ON DELETE SET NULL;

View file

@ -14,6 +14,7 @@ use Steuerkram\Controllers\BelegController;
use Steuerkram\Controllers\BookingController; use Steuerkram\Controllers\BookingController;
use Steuerkram\Controllers\CustomerController; use Steuerkram\Controllers\CustomerController;
use Steuerkram\Controllers\InvoiceController; use Steuerkram\Controllers\InvoiceController;
use Steuerkram\Controllers\SupplierController;
use Steuerkram\Controllers\UstvaController; use Steuerkram\Controllers\UstvaController;
use Steuerkram\Controllers\EuerController; use Steuerkram\Controllers\EuerController;
use Steuerkram\Controllers\SettingsController; use Steuerkram\Controllers\SettingsController;
@ -58,6 +59,15 @@ $router->post('/belege/{beleg_id}/bookings', fn($p) => (new BookingController())
$router->post('/bookings/{id}/reverse', fn($p) => (new BookingController())->reverse($p)); $router->post('/bookings/{id}/reverse', fn($p) => (new BookingController())->reverse($p));
$router->post('/bookings/{id}/delete', fn($p) => (new BookingController())->destroy($p)); $router->post('/bookings/{id}/delete', fn($p) => (new BookingController())->destroy($p));
// ── Lieferanten ──────────────────────────────────────────────
$router->get('/suppliers', fn($p) => (new SupplierController())->index($p));
$router->get('/suppliers/new', fn($p) => (new SupplierController())->create($p));
$router->post('/suppliers', fn($p) => (new SupplierController())->store($p));
$router->get('/suppliers/search', fn($p) => (new SupplierController())->search($p));
$router->get('/suppliers/{id}/edit', fn($p) => (new SupplierController())->edit($p));
$router->post('/suppliers/{id}', fn($p) => (new SupplierController())->update($p));
$router->post('/suppliers/{id}/delete', fn($p) => (new SupplierController())->destroy($p));
// ── Kunden ─────────────────────────────────────────────────── // ── Kunden ───────────────────────────────────────────────────
$router->get('/customers', fn($p) => (new CustomerController())->index($p)); $router->get('/customers', fn($p) => (new CustomerController())->index($p));
$router->get('/customers/new', fn($p) => (new CustomerController())->create($p)); $router->get('/customers/new', fn($p) => (new CustomerController())->create($p));

View file

@ -7,6 +7,7 @@ namespace Steuerkram\Controllers;
use Steuerkram\Models\Beleg; use Steuerkram\Models\Beleg;
use Steuerkram\Models\Booking; use Steuerkram\Models\Booking;
use Steuerkram\Models\Account; use Steuerkram\Models\Account;
use Steuerkram\Models\Supplier;
use Steuerkram\Config; use Steuerkram\Config;
use Steuerkram\View; use Steuerkram\View;
@ -140,12 +141,14 @@ class BelegController
$bookings = Booking::forBeleg((int)$params['id']); $bookings = Booking::forBeleg((int)$params['id']);
$integrity = Beleg::verifyIntegrity((int)$params['id']); $integrity = Beleg::verifyIntegrity((int)$params['id']);
$accounts = Account::all(['aktiv' => 1]); $accounts = Account::all(['aktiv' => 1]);
$suppliers = Supplier::all();
$view = new View(); $view = new View();
$view->render('layout/base.php', [ $view->render('layout/base.php', [
'pageTitle' => 'Beleg ' . $beleg['beleg_nr'], 'pageTitle' => 'Beleg ' . $beleg['beleg_nr'],
'content' => $this->renderView('belege/detail', [ 'content' => $this->renderView('belege/detail', [
'beleg' => $beleg, 'beleg' => $beleg,
'suppliers' => $suppliers,
'bookings' => $bookings, 'bookings' => $bookings,
'integrity' => $integrity, 'integrity' => $integrity,
'accounts' => $accounts, 'accounts' => $accounts,
@ -183,11 +186,19 @@ class BelegController
return; return;
} }
// Lieferant: Name → ID (ggf. neu anlegen)
$supplierId = null;
$supplierName = trim($_POST['supplier_name'] ?? '');
if ($supplierName !== '') {
$supplierId = Supplier::findOrCreate($supplierName);
}
Beleg::updateMeta($id, [ Beleg::updateMeta($id, [
'rechnungs_nr' => trim($_POST['rechnungs_nr'] ?? ''), 'rechnungs_nr' => trim($_POST['rechnungs_nr'] ?? ''),
'belegtyp' => $_POST['belegtyp'] ?? $beleg['belegtyp'], 'belegtyp' => $_POST['belegtyp'] ?? $beleg['belegtyp'],
'beleg_datum' => $_POST['beleg_datum'] ?: null, 'beleg_datum' => $_POST['beleg_datum'] ?: null,
'betrag_brutto' => ($_POST['betrag_brutto'] ?? '') !== '' ? $_POST['betrag_brutto'] : null, 'betrag_brutto' => ($_POST['betrag_brutto'] ?? '') !== '' ? $_POST['betrag_brutto'] : null,
'supplier_id' => $supplierId,
]); ]);
if ($isAjax) { if ($isAjax) {

View file

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Steuerkram\Controllers;
use Steuerkram\Models\Supplier;
use Steuerkram\View;
class SupplierController
{
public function index(array $params): void
{
$suppliers = Supplier::all();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Lieferanten',
'content' => $this->renderView('suppliers/list', [
'suppliers' => $suppliers,
]),
'topbarActions' => '<a href="/suppliers/new" class="btn btn-primary">+ Lieferant anlegen</a>',
]);
}
public function create(array $params): void
{
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Lieferant anlegen',
'content' => $this->renderView('suppliers/form', [
'supplier' => [],
'errors' => [],
'action' => '/suppliers',
]),
]);
}
public function store(array $params): void
{
$name = trim($_POST['name'] ?? '');
$errors = [];
if ($name === '') {
$errors['name'] = 'Pflichtfeld';
} elseif (Supplier::findByName($name)) {
$errors['name'] = 'Dieser Lieferantenname existiert bereits.';
}
if ($errors) {
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Lieferant anlegen',
'content' => $this->renderView('suppliers/form', [
'supplier' => ['name' => $name],
'errors' => $errors,
'action' => '/suppliers',
]),
]);
return;
}
Supplier::create($name);
View::redirect('/suppliers', 'Lieferant erfolgreich angelegt.');
}
public function edit(array $params): void
{
$supplier = Supplier::find((int)$params['id']);
if (!$supplier) { http_response_code(404); return; }
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Lieferant bearbeiten',
'content' => $this->renderView('suppliers/form', [
'supplier' => $supplier,
'errors' => [],
'action' => '/suppliers/' . $supplier['id'],
]),
]);
}
public function update(array $params): void
{
$id = (int)$params['id'];
$supplier = Supplier::find($id);
if (!$supplier) { http_response_code(404); return; }
$name = trim($_POST['name'] ?? '');
$errors = [];
if ($name === '') {
$errors['name'] = 'Pflichtfeld';
} else {
$existing = Supplier::findByName($name);
if ($existing && (int)$existing['id'] !== $id) {
$errors['name'] = 'Dieser Lieferantenname existiert bereits.';
}
}
if ($errors) {
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Lieferant bearbeiten',
'content' => $this->renderView('suppliers/form', [
'supplier' => array_merge($supplier, ['name' => $name]),
'errors' => $errors,
'action' => '/suppliers/' . $id,
]),
]);
return;
}
Supplier::update($id, $name);
View::redirect('/suppliers', 'Lieferant aktualisiert.');
}
public function destroy(array $params): void
{
if (!Supplier::delete((int)$params['id'])) {
View::redirect('/suppliers', 'Lieferant kann nicht gelöscht werden (noch in Belegen verwendet).', 'error');
return;
}
View::redirect('/suppliers', 'Lieferant gelöscht.');
}
/**
* AJAX: Autocomplete-Liste aller Lieferanten als JSON.
*/
public function search(array $params): void
{
$suppliers = Supplier::all();
header('Content-Type: application/json');
echo json_encode(array_map(fn($s) => ['id' => $s['id'], 'name' => $s['name']], $suppliers));
}
private function renderView(string $template, array $data): string
{
extract($data, EXTR_SKIP);
ob_start();
include dirname(__DIR__) . '/Views/' . $template . '.php';
return ob_get_clean();
}
}

View file

@ -69,9 +69,11 @@ class Beleg
(SELECT SUM(bk.betrag_brutto) FROM bookings bk (SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0) WHERE bk.beleg_id = b.id AND bk.is_reversal = 0)
) AS betrag_brutto, ) AS betrag_brutto,
fy.year as fiscal_year_nr fy.year as fiscal_year_nr,
s.name as supplier_name
FROM belege b FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.id = ?', WHERE b.id = ?',
[$id] [$id]
); );
@ -156,6 +158,7 @@ class Beleg
belegtyp = ?, belegtyp = ?,
beleg_datum = ?, beleg_datum = ?,
betrag_brutto = ?, betrag_brutto = ?,
supplier_id = ?,
fiscal_year_id = COALESCE(?, fiscal_year_id), fiscal_year_id = COALESCE(?, fiscal_year_id),
updated_at = NOW() updated_at = NOW()
WHERE id = ? AND locked = 0', WHERE id = ? AND locked = 0',
@ -164,6 +167,7 @@ class Beleg
$data['belegtyp'], $data['belegtyp'],
$data['beleg_datum'] ?: null, $data['beleg_datum'] ?: null,
$data['betrag_brutto'] !== null && $data['betrag_brutto'] !== '' ? (float)$data['betrag_brutto'] : null, $data['betrag_brutto'] !== null && $data['betrag_brutto'] !== '' ? (float)$data['betrag_brutto'] : null,
isset($data['supplier_id']) && $data['supplier_id'] !== '' ? (int)$data['supplier_id'] : null,
$fyId, $fyId,
$id, $id,
] ]

63
src/Models/Supplier.php Normal file
View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Steuerkram\Models;
use Steuerkram\Database;
class Supplier
{
public static function all(): array
{
return Database::fetchAll('SELECT * FROM suppliers ORDER BY name');
}
public static function find(int $id): array|false
{
return Database::fetchOne('SELECT * FROM suppliers WHERE id = ?', [$id]);
}
public static function findByName(string $name): array|false
{
return Database::fetchOne('SELECT * FROM suppliers WHERE name = ?', [trim($name)]);
}
public static function create(string $name): int
{
Database::execute('INSERT INTO suppliers (name) VALUES (?)', [trim($name)]);
return (int)Database::lastInsertId();
}
public static function update(int $id, string $name): void
{
Database::execute('UPDATE suppliers SET name = ?, updated_at = NOW() WHERE id = ?', [trim($name), $id]);
}
public static function delete(int $id): bool
{
// Prüfen ob Lieferant noch in Belegen verwendet wird
$cnt = Database::fetchOne('SELECT COUNT(*) as cnt FROM belege WHERE supplier_id = ?', [$id]);
if ($cnt && $cnt['cnt'] > 0) {
return false;
}
Database::execute('DELETE FROM suppliers WHERE id = ?', [$id]);
return true;
}
/**
* Gibt ID eines vorhandenen Lieferanten zurück oder legt ihn neu an.
*/
public static function findOrCreate(string $name): int
{
$name = trim($name);
if ($name === '') {
throw new \InvalidArgumentException('Lieferantenname darf nicht leer sein.');
}
$existing = self::findByName($name);
if ($existing) {
return (int)$existing['id'];
}
return self::create($name);
}
}

View file

@ -4,6 +4,7 @@ use Steuerkram\Models\Setting;
$h = fn($v) => View::h($v); $h = fn($v) => View::h($v);
$fmt = fn($v) => View::formatMoney($v); $fmt = fn($v) => View::formatMoney($v);
$testMode = Setting::isTestMode(); $testMode = Setting::isTestMode();
$suppliers = $suppliers ?? [];
?> ?>
<!-- Beleg-Info-Bar --> <!-- Beleg-Info-Bar -->
@ -23,6 +24,10 @@ $testMode = Setting::isTestMode();
<span class="text-muted text-sm">Typ</span><br> <span class="text-muted text-sm">Typ</span><br>
<span class="badge badge-gray"><?= $h($beleg['belegtyp']) ?></span> <span class="badge badge-gray"><?= $h($beleg['belegtyp']) ?></span>
</div> </div>
<div>
<span class="text-muted text-sm">Lieferant</span><br>
<?= $beleg['supplier_name'] ? $h($beleg['supplier_name']) : '<span class="text-muted">–</span>' ?>
</div>
<div> <div>
<span class="text-muted text-sm">Belegdatum<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br> <span class="text-muted text-sm">Belegdatum<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
<?php if (!$beleg['locked']): ?> <?php if (!$beleg['locked']): ?>
@ -108,6 +113,19 @@ $testMode = Setting::isTestMode();
<input type="number" name="betrag_brutto" step="0.01" min="0" <input type="number" name="betrag_brutto" step="0.01" min="0"
value="<?= $h($beleg['betrag_brutto'] ?? '') ?>" placeholder="0.00"> value="<?= $h($beleg['betrag_brutto'] ?? '') ?>" placeholder="0.00">
</div> </div>
<div class="form-group" style="margin:0;min-width:180px">
<label style="font-size:.8rem">Lieferant</label>
<input type="text" name="supplier_name" id="supplier_name_input"
value="<?= $h($beleg['supplier_name'] ?? '') ?>"
placeholder="Name eingeben oder wählen…"
list="supplier-datalist"
autocomplete="off">
<datalist id="supplier-datalist">
<?php foreach ($suppliers as $sup): ?>
<option value="<?= $h($sup['name']) ?>">
<?php endforeach; ?>
</datalist>
</div>
<button type="submit" class="btn btn-primary btn-sm">Speichern</button> <button type="submit" class="btn btn-primary btn-sm">Speichern</button>
<button type="button" class="btn btn-secondary btn-sm" <button type="button" class="btn btn-secondary btn-sm"
onclick="document.getElementById('beleg-meta-form').style.display='none'">Abbrechen</button> onclick="document.getElementById('beleg-meta-form').style.display='none'">Abbrechen</button>

View file

@ -28,6 +28,10 @@ function navActive(string $prefix, string $current): string {
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>
Buchungsjournal Buchungsjournal
</a> </a>
<a href="/suppliers" class="<?= navActive('/suppliers', $path) ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
Lieferanten
</a>
</nav> </nav>
</div> </div>

View file

@ -0,0 +1,24 @@
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
<div class="card" style="max-width:480px">
<div class="card-body">
<form method="post" action="<?= $h($action) ?>">
<div class="form-group">
<label for="name">Lieferantenname *</label>
<input type="text" id="name" name="name"
value="<?= $h($supplier['name'] ?? '') ?>"
class="<?= isset($errors['name']) ? 'input-error' : '' ?>"
required autofocus>
<?php if (isset($errors['name'])): ?>
<div class="error-text"><?= $h($errors['name']) ?></div>
<?php endif; ?>
</div>
<div style="display:flex;gap:.75rem;margin-top:1rem">
<button type="submit" class="btn btn-primary">Speichern</button>
<a href="/suppliers" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,45 @@
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
<div class="toolbar">
<div class="toolbar-left">
</div>
<div class="toolbar-right">
<span class="text-muted text-sm"><?= count($suppliers) ?> Lieferanten</span>
</div>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Angelegt am</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($suppliers as $s): ?>
<tr>
<td class="td-mono text-sm"><?= $h($s['id']) ?></td>
<td><?= $h($s['name']) ?></td>
<td class="td-muted text-sm"><?= View::formatDate($s['created_at']) ?></td>
<td>
<div style="display:flex;gap:.5rem">
<a href="/suppliers/<?= $h($s['id']) ?>/edit" class="btn btn-secondary btn-sm">Bearbeiten</a>
<form method="post" action="/suppliers/<?= $h($s['id']) ?>/delete" style="margin:0"
data-confirm="Lieferant &quot;<?= $h($s['name']) ?>&quot; wirklich löschen?">
<button type="submit" class="btn btn-danger btn-sm">Löschen</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($suppliers)): ?>
<tr><td colspan="4" class="text-center text-muted" style="padding:2rem">Noch keine Lieferanten angelegt.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* Test 8: Lieferantenmanagement
*
* Prüft, ob Lieferanten angelegt, gefunden, aktualisiert und
* einem Beleg zugewiesen werden können.
*/
class LieferantTest extends TestCase
{
protected string $testName = 'Test 8: Lieferantenmanagement';
public function run(): void
{
// 1. Lieferant anlegen
$name = 'Testlieferant GmbH ' . uniqid();
$id = \Steuerkram\Models\Supplier::create($name);
$this->assertGreaterThan(0, $id, 'Lieferant wurde angelegt (ID > 0)');
// 2. find() liefert den angelegten Lieferanten
$supplier = \Steuerkram\Models\Supplier::find($id);
$this->assertNotEmpty($supplier, 'Supplier::find() liefert Datensatz');
$this->assertEquals($name, $supplier['name'], 'Lieferantenname stimmt überein');
// 3. findByName() findet den Lieferanten
$found = \Steuerkram\Models\Supplier::findByName($name);
$this->assertNotEmpty($found, 'Supplier::findByName() findet den Lieferanten');
$this->assertEquals($id, (int)$found['id'], 'ID stimmt überein');
// 4. all() enthält den Lieferanten
$all = \Steuerkram\Models\Supplier::all();
$inList = false;
foreach ($all as $s) {
if ((int)$s['id'] === $id) { $inList = true; break; }
}
$this->assert($inList, 'Lieferant erscheint in Supplier::all()');
// 5. findOrCreate() gibt vorhandenen zurück
$id2 = \Steuerkram\Models\Supplier::findOrCreate($name);
$this->assertEquals($id, $id2, 'findOrCreate() gibt vorhandene ID zurück');
// 6. findOrCreate() legt neuen an
$newName = 'Neuer Lieferant ' . uniqid();
$id3 = \Steuerkram\Models\Supplier::findOrCreate($newName);
$this->assertGreaterThan(0, $id3, 'findOrCreate() legt neuen Lieferanten an');
$this->assert($id3 !== $id, 'Neue ID ist verschieden von der ersten');
// 7. update()
$updatedName = 'Umbenannt ' . uniqid();
\Steuerkram\Models\Supplier::update($id, $updatedName);
$updated = \Steuerkram\Models\Supplier::find($id);
$this->assertEquals($updatedName, $updated['name'], 'Name nach update() korrekt geändert');
// 8. Lieferant einem Beleg zuweisen via Beleg::create() + updateMeta()
$belegNr = 'TEST-SUP-' . uniqid();
$datum = date('Y-m-d');
$belegId = \Steuerkram\Models\Beleg::create([
'beleg_nr' => $belegNr,
'filename' => 'test-lieferant.pdf',
'storage_path' => 'static/uploads/test/test-lieferant.pdf',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $belegNr),
'beleg_datum' => $datum,
'eingangs_datum' => $datum,
'rechnungs_nr' => 'RE-SUP-001',
'betrag_brutto' => 59.50,
'belegtyp' => 'Eingangsrechnung',
]);
\Steuerkram\Models\Beleg::updateMeta($belegId, [
'rechnungs_nr' => 'RE-SUP-001',
'belegtyp' => 'Eingangsrechnung',
'beleg_datum' => $datum,
'betrag_brutto' => 59.50,
'supplier_id' => $id3,
]);
$beleg = \Steuerkram\Models\Beleg::find($belegId);
$this->assertEquals($id3, (int)$beleg['supplier_id'], 'supplier_id korrekt am Beleg gespeichert');
$this->assertNotEmpty($beleg['supplier_name'], 'supplier_name via JOIN abrufbar');
// 9. Löschen: Lieferant der noch verwendet wird kann nicht gelöscht werden
$deleted = \Steuerkram\Models\Supplier::delete($id3);
$this->assert(!$deleted, 'Lieferant in Verwendung kann nicht gelöscht werden');
// 10. Nicht verwendeten Lieferanten löschen
$unusedName = 'Ungenutzt ' . uniqid();
$unusedId = \Steuerkram\Models\Supplier::create($unusedName);
$deletedOk = \Steuerkram\Models\Supplier::delete($unusedId);
$this->assert($deletedOk, 'Nicht verwendeter Lieferant kann gelöscht werden');
$this->assert(!\Steuerkram\Models\Supplier::find($unusedId), 'Gelöschter Lieferant nicht mehr auffindbar');
}
}