Merge pull request 'Fix #108: Lieferant in Belegliste inline bearbeiten' (#110) from fds2610-bot/Steuerkram:main into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/110
This commit is contained in:
commit
2de894ccd0
14 changed files with 364 additions and 12 deletions
20
Changes.md
20
Changes.md
|
|
@ -2,6 +2,26 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-06-29 – Feature: Ereignisse-Log für Nextcloud-Import (Issue #109)
|
||||
|
||||
- **`migrations/083_system_events.sql`:** Neue Tabelle `system_events` (id, channel, message, created_at)
|
||||
- **`src/Models/EventLog.php`:** Neues Model mit `add()` und `findByChannel()`
|
||||
- **`src/Services/NextcloudImportService.php`:** Persistiert relevante Ereignisse (Import, Duplikat, Fehler) automatisch; stille Läufe ohne Dateiaktivität erzeugen keine Einträge; Zusammenfassung wird nur bei tatsächlicher Aktivität geloggt
|
||||
- **`src/Controllers/EventController.php`:** Neuer Controller für `/ereignisse` und `/ereignisse/nextcloud-import`
|
||||
- **`src/Views/ereignisse/nextcloud_import.php`:** Log-Ansicht mit Zeitstempel, Kanal und farbiger Meldung
|
||||
- **`src/Views/layout/nav.php`:** Neuer Menüpunkt „Ereignisse" unter Verwaltung mit aufklappbarem Unterpunkt „Nextcloud-Import"
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-29 – Feature: Lieferant in Belegliste inline bearbeiten (Issue #108)
|
||||
|
||||
- **`src/Views/belege/list.php`:** Lieferant-Spalte ist direkt in der Liste klickbar; zeigt ein Inline-Textfeld zur Eingabe, speichert mit Enter, bricht mit Escape ab; andere Spalten öffnen weiterhin die Vorschau
|
||||
- **`src/Models/Beleg.php`:** Neue Methode `updateSupplier()` aktualisiert nur die `supplier_id` ohne andere Metadaten zu verändern
|
||||
- **`src/Controllers/BelegController.php`:** Neue Methode `updateSupplier()` als AJAX-Endpunkt
|
||||
- **`public/index.php`:** Route `POST /belege/{id}/supplier` registriert
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-11 – Fix: Undefined-Array-Key-Warning bei Buchungsvorlagen ohne Standard-Lieferant
|
||||
|
||||
- **`src/Models/Buchungsvorlage.php`:** `create()`/`update()` prüften `$data['standard_lieferant']` direkt ohne Null-Coalescing, was bei fehlendem Schlüssel eine PHP-Warning auslöste
|
||||
|
|
|
|||
7
migrations/083_system_events.sql
Normal file
7
migrations/083_system_events.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS system_events (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
channel VARCHAR(50) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_channel_created (channel, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -28,6 +28,7 @@ use Steuerkram\Controllers\AssetController;
|
|||
use Steuerkram\Controllers\UserAdminController;
|
||||
use Steuerkram\Controllers\BankController;
|
||||
use Steuerkram\Controllers\BuchungsvorlageController;
|
||||
use Steuerkram\Controllers\EventController;
|
||||
|
||||
Config::load();
|
||||
|
||||
|
|
@ -60,8 +61,9 @@ $router->post('/belege/upload', fn($p) => (new BelegController())->upload($p));
|
|||
$router->get('/belege/{id}', fn($p) => (new BelegController())->show($p));
|
||||
$router->get('/belege/{id}/file', fn($p) => (new BelegController())->serveFile($p));
|
||||
$router->post('/belege/{id}/delete', fn($p) => (new BelegController())->destroy($p));
|
||||
$router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($p));
|
||||
$router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImage($p));
|
||||
$router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($p));
|
||||
$router->post('/belege/{id}/supplier', fn($p) => (new BelegController())->updateSupplier($p));
|
||||
$router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImage($p));
|
||||
$router->post('/belege/{id}/vorschlag/generate', fn($p) => (new BelegController())->generateVorschlag($p));
|
||||
$router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController())->resetVorschlag($p));
|
||||
|
||||
|
|
@ -183,6 +185,10 @@ $router->post('/einstellungen/buchungsvorlagen/{id}/loeschen', fn($p) =
|
|||
$router->get('/api/buchungsvorlagen/{id}/modal', fn($p) => (new BuchungsvorlageController())->modal($p));
|
||||
$router->post('/belege/{belegId}/vorlage/{vorlageId}/buchen', fn($p) => (new BuchungsvorlageController())->buchen($p));
|
||||
|
||||
// ── Ereignisse ───────────────────────────────────────────────
|
||||
$router->get('/ereignisse', fn($p) => (new EventController())->index($p));
|
||||
$router->get('/ereignisse/nextcloud-import', fn($p) => (new EventController())->nextcloudImport($p));
|
||||
|
||||
// ── Login / Logout ────────────────────────────────────────────
|
||||
$router->get('/login', fn($p) => (new LoginController())->showLogin($p));
|
||||
$router->post('/login', fn($p) => (new LoginController())->handleLogin($p));
|
||||
|
|
|
|||
|
|
@ -366,6 +366,30 @@ class BelegController extends BaseController
|
|||
View::redirect("/belege/{$id}", 'Metadaten gespeichert.');
|
||||
}
|
||||
|
||||
public function updateSupplier(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
$id = (int)$params['id'];
|
||||
$beleg = Beleg::find($id);
|
||||
|
||||
if (!$beleg) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Nicht gefunden.']);
|
||||
return;
|
||||
}
|
||||
if ($beleg['locked']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'GoBD: Gesperrter Beleg.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$supplierName = trim($_POST['supplier_name'] ?? '');
|
||||
$supplierId = $supplierName !== '' ? Supplier::findOrCreate($supplierName) : null;
|
||||
Beleg::updateSupplier($id, $supplierId);
|
||||
|
||||
echo json_encode(['ok' => true, 'supplier_name' => $supplierName !== '' ? $supplierName : null]);
|
||||
}
|
||||
|
||||
public function generateVorschlag(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
|
|
|||
29
src/Controllers/EventController.php
Normal file
29
src/Controllers/EventController.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Steuerkram\Controllers;
|
||||
|
||||
use Steuerkram\Models\EventLog;
|
||||
use Steuerkram\View;
|
||||
|
||||
class EventController extends BaseController
|
||||
{
|
||||
public function index(array $params): void
|
||||
{
|
||||
View::redirect('/ereignisse/nextcloud-import');
|
||||
}
|
||||
|
||||
public function nextcloudImport(array $params): void
|
||||
{
|
||||
$events = EventLog::findByChannel('nextcloud-import', 500);
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Ereignisse – Nextcloud-Import',
|
||||
'content' => $this->renderView('ereignisse/nextcloud_import', [
|
||||
'events' => $events,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -231,6 +231,14 @@ class Beleg
|
|||
);
|
||||
}
|
||||
|
||||
public static function updateSupplier(int $id, ?int $supplierId): void
|
||||
{
|
||||
Database::execute(
|
||||
'UPDATE belege SET supplier_id=?, updated_at=NOW() WHERE id=? AND locked=0',
|
||||
[$supplierId, $id]
|
||||
);
|
||||
}
|
||||
|
||||
public static function delete(int $id): bool
|
||||
{
|
||||
$beleg = self::find($id);
|
||||
|
|
|
|||
30
src/Models/EventLog.php
Normal file
30
src/Models/EventLog.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Steuerkram\Models;
|
||||
|
||||
use Steuerkram\Database;
|
||||
|
||||
class EventLog
|
||||
{
|
||||
public static function add(string $channel, string $message): void
|
||||
{
|
||||
Database::execute(
|
||||
'INSERT INTO system_events (channel, message) VALUES (?, ?)',
|
||||
[$channel, $message]
|
||||
);
|
||||
}
|
||||
|
||||
public static function findByChannel(string $channel, int $limit = 500): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT id, channel, message, created_at
|
||||
FROM system_events
|
||||
WHERE channel = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ?',
|
||||
[$channel, $limit]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ namespace Steuerkram\Services;
|
|||
|
||||
use Steuerkram\Database;
|
||||
use Steuerkram\Models\Beleg;
|
||||
use Steuerkram\Models\EventLog;
|
||||
use Steuerkram\Models\Setting;
|
||||
|
||||
/**
|
||||
|
|
@ -45,6 +46,7 @@ class NextcloudImportService
|
|||
private string $folder;
|
||||
private string|null $doneFolder;
|
||||
private string $davBase;
|
||||
private array $pendingEvents = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -69,18 +71,23 @@ class NextcloudImportService
|
|||
*/
|
||||
public function import(): array
|
||||
{
|
||||
$start = microtime(true);
|
||||
$result = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'details' => [], 'imported_ids' => []];
|
||||
|
||||
if (!$this->baseUrl || !$this->user || !$this->pass) {
|
||||
$result['details'][] = 'FEHLER: NEXTCLOUD_URL, NEXTCLOUD_USER und NEXTCLOUD_PASS müssen gesetzt sein.';
|
||||
$msg = 'FEHLER: NEXTCLOUD_URL, NEXTCLOUD_USER und NEXTCLOUD_PASS müssen gesetzt sein.';
|
||||
$result['details'][] = $msg;
|
||||
$result['errors']++;
|
||||
$this->persistEvent($msg);
|
||||
return $result;
|
||||
}
|
||||
|
||||
$files = $this->listFiles();
|
||||
if ($files === null) {
|
||||
$result['details'][] = 'FEHLER: PROPFIND fehlgeschlagen. URL/Zugangsdaten prüfen.';
|
||||
$msg = 'FEHLER: PROPFIND fehlgeschlagen. URL/Zugangsdaten prüfen.';
|
||||
$result['details'][] = $msg;
|
||||
$result['errors']++;
|
||||
$this->persistEvent($msg);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
|
@ -90,9 +97,24 @@ class NextcloudImportService
|
|||
$this->processFile($file, $result);
|
||||
}
|
||||
|
||||
if ($result['imported'] + $result['skipped'] + $result['errors'] > 0) {
|
||||
$elapsed = round(microtime(true) - $start, 2);
|
||||
$summary = sprintf(
|
||||
'Fertig in %ss – importiert: %d, übersprungen: %d, Fehler: %d',
|
||||
$elapsed, $result['imported'], $result['skipped'], $result['errors']
|
||||
);
|
||||
$this->persistEvent($summary);
|
||||
$result['details'][] = $summary;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function persistEvent(string $message): void
|
||||
{
|
||||
EventLog::add('nextcloud-import', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listet alle Dateien im konfigurierten Ordner via PROPFIND.
|
||||
* Gibt null zurück bei Verbindungsfehler.
|
||||
|
|
@ -198,7 +220,9 @@ class NextcloudImportService
|
|||
|
||||
// MIME prüfen
|
||||
if (!isset(self::ALLOWED_MIME[$contentType])) {
|
||||
$result['details'][] = "Übersprungen (unbekannter Typ «{$contentType}»): {$filename}";
|
||||
$msg = "Übersprungen (unbekannter Typ «{$contentType}»): {$filename}";
|
||||
$result['details'][] = $msg;
|
||||
$this->persistEvent($msg);
|
||||
$result['skipped']++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -210,7 +234,9 @@ class NextcloudImportService
|
|||
$tmpPath = sys_get_temp_dir() . '/nextcloud_import_' . uniqid() . '.' . $ext;
|
||||
|
||||
if (!$this->downloadFile($fileUrl, $tmpPath)) {
|
||||
$result['details'][] = "FEHLER beim Download: {$filename}";
|
||||
$msg = "FEHLER beim Download: {$filename}";
|
||||
$result['details'][] = $msg;
|
||||
$this->persistEvent($msg);
|
||||
$result['errors']++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -222,7 +248,9 @@ class NextcloudImportService
|
|||
[$rawHash]
|
||||
);
|
||||
if ($existing) {
|
||||
$result['details'][] = "Duplikat (Beleg {$existing['beleg_nr']}): {$filename}";
|
||||
$msg = "Duplikat (Beleg {$existing['beleg_nr']}): {$filename}";
|
||||
$result['details'][] = $msg;
|
||||
$this->persistEvent($msg);
|
||||
$result['skipped']++;
|
||||
unlink($tmpPath);
|
||||
$this->moveFile($file['href'], $filename);
|
||||
|
|
@ -236,7 +264,9 @@ class NextcloudImportService
|
|||
$absDir = dirname(__DIR__, 2) . "/public/{$relDir}";
|
||||
|
||||
if (!is_dir($absDir) && !mkdir($absDir, 0755, true)) {
|
||||
$result['details'][] = "FEHLER: Verzeichnis konnte nicht erstellt werden: {$absDir}";
|
||||
$msg = "FEHLER: Verzeichnis konnte nicht erstellt werden: {$absDir}";
|
||||
$result['details'][] = $msg;
|
||||
$this->persistEvent($msg);
|
||||
$result['errors']++;
|
||||
unlink($tmpPath);
|
||||
return;
|
||||
|
|
@ -246,7 +276,9 @@ class NextcloudImportService
|
|||
$absPath = dirname(__DIR__, 2) . "/public/{$storagePath}";
|
||||
|
||||
if (!rename($tmpPath, $absPath)) {
|
||||
$result['details'][] = "FEHLER: Datei konnte nicht verschoben werden: {$filename}";
|
||||
$msg = "FEHLER: Datei konnte nicht verschoben werden: {$filename}";
|
||||
$result['details'][] = $msg;
|
||||
$this->persistEvent($msg);
|
||||
$result['errors']++;
|
||||
@unlink($tmpPath);
|
||||
return;
|
||||
|
|
@ -278,7 +310,9 @@ class NextcloudImportService
|
|||
// Datei in Nextcloud verschieben/löschen
|
||||
$this->moveFile($file['href'], $filename);
|
||||
|
||||
$result['details'][] = "Importiert: {$filename} → Beleg {$belegNr}";
|
||||
$msg = "Importiert: {$filename} → Beleg {$belegNr}";
|
||||
$result['details'][] = $msg;
|
||||
$this->persistEvent($msg);
|
||||
$result['imported']++;
|
||||
$result['imported_ids'][] = $belegId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,13 @@
|
|||
</div>
|
||||
</td>
|
||||
<td class="td-muted"><?= View::formatDate($b['beleg_datum']) ?></td>
|
||||
<td class="td-muted"><?= $h($b['supplier_name'] ?? '–') ?></td>
|
||||
<td class="td-muted bl-supplier-cell"
|
||||
data-beleg-id="<?= (int)$b['id'] ?>"
|
||||
data-supplier="<?= $h($b['supplier_name'] ?? '') ?>"
|
||||
onclick="event.stopPropagation(); blInlineSupplier(this)"
|
||||
style="cursor:text;min-width:80px"
|
||||
title="Klicken zum Lieferant bearbeiten"
|
||||
><?= $h($b['supplier_name'] ?? '–') ?></td>
|
||||
<td class="td-muted"><?= $h($b['rechnungs_nr'] ?? '–') ?></td>
|
||||
<td class="td-right td-mono"><?= $b['betrag_brutto'] !== null ? View::formatMoney($b['betrag_brutto']) : '–' ?></td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
|
|
@ -245,4 +251,49 @@
|
|||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function blInlineSupplier(td) {
|
||||
if (td.querySelector('input')) return;
|
||||
|
||||
const current = td.dataset.supplier || '';
|
||||
const belegId = td.dataset.belegId;
|
||||
let committed = false;
|
||||
|
||||
td.textContent = '';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = current;
|
||||
input.style.cssText = 'width:100%;border:1px solid var(--color-primary,#3b82f6);border-radius:3px;padding:.1rem .3rem;font-size:inherit;background:#fff;box-sizing:border-box;color:inherit';
|
||||
|
||||
function commit(val) {
|
||||
if (committed) return;
|
||||
committed = true;
|
||||
const fd = new FormData();
|
||||
fd.append('supplier_name', val);
|
||||
fetch('/belege/' + belegId + '/supplier', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: fd,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
td.dataset.supplier = val;
|
||||
td.textContent = val || '–';
|
||||
})
|
||||
.catch(() => { td.textContent = current || '–'; });
|
||||
}
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); commit(input.value.trim()); }
|
||||
if (e.key === 'Escape') { committed = true; td.textContent = current || '–'; }
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function () {
|
||||
commit(input.value.trim());
|
||||
});
|
||||
|
||||
td.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
35
src/Views/ereignisse/nextcloud_import.php
Normal file
35
src/Views/ereignisse/nextcloud_import.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;border-bottom:1px solid var(--color-border)">
|
||||
<strong>Nextcloud-Import</strong>
|
||||
<span class="text-muted text-sm"><?= count($events) ?> Einträge (neueste zuerst)</span>
|
||||
</div>
|
||||
<?php if (empty($events)): ?>
|
||||
<div style="padding:2rem;text-align:center" class="text-muted">Keine Ereignisse vorhanden. Einträge entstehen, sobald der Nextcloud-Import Dateien importiert, überspringt oder auf Fehler stößt.</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:.875rem">
|
||||
<tbody>
|
||||
<?php foreach ($events as $e): ?>
|
||||
<?php
|
||||
$msg = $e['message'];
|
||||
$isError = str_starts_with($msg, 'FEHLER');
|
||||
$isDup = str_starts_with($msg, 'Duplikat');
|
||||
$isOk = str_starts_with($msg, 'Importiert');
|
||||
$color = $isError ? 'var(--color-danger,#dc2626)'
|
||||
: ($isDup ? 'var(--color-warning,#d97706)'
|
||||
: ($isOk ? 'var(--color-success,#16a34a)'
|
||||
: 'inherit'));
|
||||
?>
|
||||
<tr style="border-bottom:1px solid var(--color-border,#e2e8f0)">
|
||||
<td style="padding:.35rem 1rem;white-space:nowrap;color:var(--color-muted);font-family:monospace"><?= $h(date('Y-m-d H:i:s', strtotime($e['created_at']))) ?></td>
|
||||
<td style="padding:.35rem .5rem;white-space:nowrap;color:var(--color-muted);font-family:monospace">[<?= $h($e['channel']) ?>]</td>
|
||||
<td style="padding:.35rem 1rem;color:<?= $color ?>;word-break:break-word"><?= $h($msg) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -84,6 +84,16 @@ function navActive(string $prefix, string $current): string {
|
|||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
|
||||
Benutzer
|
||||
</a>
|
||||
<a href="/ereignisse" class="<?= navActive('/ereignisse', $path) ?>">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22V12M12 12L8 16M12 12l4 4"/><circle cx="12" cy="5" r="3"/><path d="M5.5 8.5A7 7 0 0 0 5 12"/><path d="M18.5 8.5A7 7 0 0 1 19 12"/></svg>
|
||||
Ereignisse
|
||||
</a>
|
||||
<?php if (str_starts_with($path, '/ereignisse')): ?>
|
||||
<a href="/ereignisse/nextcloud-import" class="<?= navActive('/ereignisse/nextcloud-import', $path) ?>" style="padding-left:2.25rem;font-size:.875rem">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92V20a2 2 0 0 1-2.18 2A19.79 19.79 0 0 1 11.39 19a19.5 19.5 0 0 1-4.9-4.9 19.79 19.79 0 0 1-3-9.43A2 2 0 0 1 5.32 3H8.4a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L9.4 10.9a16 16 0 0 0 3.71 3.71l1.26-1.26a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
Nextcloud-Import
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="/settings" class="<?= navActive('/settings', $path) ?>">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M4.93 4.93l1.41 1.41M19.07 19.07l-1.41-1.41M4.93 19.07l1.41-1.41M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>
|
||||
Einstellungen
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1-260611
|
||||
1-260629
|
||||
|
|
|
|||
55
tests/46_BelegLieferantInlineTest.php
Normal file
55
tests/46_BelegLieferantInlineTest.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\Beleg;
|
||||
use Steuerkram\Models\Supplier;
|
||||
|
||||
class BelegLieferantInlineTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 46: Beleg-Lieferant Inline-Update via updateSupplier()';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$belegId = Beleg::create([
|
||||
'beleg_nr' => 'TEST-SUPP-' . uniqid(),
|
||||
'filename' => 'inline-supplier-test.pdf',
|
||||
'storage_path' => '',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_hash' => hash('sha256', 'inline-supplier-' . uniqid()),
|
||||
'beleg_datum' => date('Y-m-d'),
|
||||
'belegtyp' => 'Eingangsrechnung',
|
||||
]);
|
||||
$this->assertGreaterThan(0, $belegId, 'Beleg angelegt');
|
||||
|
||||
// Kein Lieferant initial
|
||||
$beleg = Beleg::find($belegId);
|
||||
$this->assert($beleg['supplier_name'] === null || $beleg['supplier_name'] === '', 'Kein Lieferant initial');
|
||||
|
||||
// Lieferant setzen
|
||||
$supplierId = Supplier::findOrCreate('Testfirma GmbH');
|
||||
$this->assertGreaterThan(0, $supplierId, 'Supplier angelegt oder gefunden');
|
||||
Beleg::updateSupplier($belegId, $supplierId);
|
||||
|
||||
$beleg = Beleg::find($belegId);
|
||||
$this->assertEquals('Testfirma GmbH', $beleg['supplier_name'], 'Lieferant nach updateSupplier gesetzt');
|
||||
|
||||
// Lieferant auf null zurücksetzen (löschen)
|
||||
Beleg::updateSupplier($belegId, null);
|
||||
$beleg = Beleg::find($belegId);
|
||||
$this->assert(
|
||||
$beleg['supplier_name'] === null || $beleg['supplier_name'] === '',
|
||||
'Lieferant nach Nullsetzung leer'
|
||||
);
|
||||
|
||||
// updateSupplier darf bei gesperrtem Beleg nichts ändern
|
||||
\Steuerkram\Database::execute('UPDATE belege SET locked=1 WHERE id=?', [$belegId]);
|
||||
$supplierId2 = Supplier::findOrCreate('Gesperrte Firma');
|
||||
Beleg::updateSupplier($belegId, $supplierId2);
|
||||
$beleg = Beleg::find($belegId);
|
||||
$this->assert(
|
||||
$beleg['supplier_name'] === null || $beleg['supplier_name'] === '',
|
||||
'Lieferant bei gesperrtem Beleg unverändert'
|
||||
);
|
||||
}
|
||||
}
|
||||
43
tests/47_SystemEventsTest.php
Normal file
43
tests/47_SystemEventsTest.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\EventLog;
|
||||
|
||||
class SystemEventsTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 47: EventLog – Einträge schreiben und lesen';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$channel = 'test-' . uniqid();
|
||||
|
||||
// Kein Eintrag initial
|
||||
$before = EventLog::findByChannel($channel);
|
||||
$this->assertEquals(0, count($before), 'Kanal initial leer');
|
||||
|
||||
// Eintrag anlegen
|
||||
EventLog::add($channel, 'Importiert: test.pdf → Beleg TST-0001');
|
||||
EventLog::add($channel, 'Duplikat (Beleg TST-0001): test.pdf');
|
||||
EventLog::add($channel, 'FEHLER beim Download: fehler.pdf');
|
||||
|
||||
$entries = EventLog::findByChannel($channel);
|
||||
$this->assertEquals(3, count($entries), 'Drei Einträge vorhanden');
|
||||
|
||||
// Neueste zuerst (höchste id zuletzt hinzugefügt)
|
||||
$this->assertEquals('FEHLER beim Download: fehler.pdf', $entries[0]['message'], 'Neuester Eintrag zuerst');
|
||||
$this->assertEquals($channel, $entries[0]['channel'], 'Kanal korrekt gesetzt');
|
||||
|
||||
// created_at vorhanden und parseable
|
||||
$ts = strtotime($entries[0]['created_at']);
|
||||
$this->assert($ts > 0, 'created_at ist ein gültiges Datum');
|
||||
|
||||
// Limit wird eingehalten
|
||||
$limited = EventLog::findByChannel($channel, 2);
|
||||
$this->assertEquals(2, count($limited), 'Limit 2 wird eingehalten');
|
||||
|
||||
// Anderer Kanal bleibt unberührt
|
||||
$other = EventLog::findByChannel('other-channel-' . uniqid());
|
||||
$this->assertEquals(0, count($other), 'Anderer Kanal ist leer');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue