Finanzmanager/src/Models/Konto.php
fds2610-bot 1e3227ce83 Phase 1: Stammdaten und QIF-Stammdatenimport
Konten, Kontengruppen, Kategorien und Klassen samt Übernahme aus dem
QIF-Export des Lexware FinanzManagers.

Kontenübersicht nach Kontengruppen gebündelt, mit Gruppensummen und
Gesamtsumme; die drei Saldospalten (Online-Saldo, aktueller und erwarteter
Saldo) sind angelegt und werden bis Phase 2 aus dem Anfangssaldo gespeist.

Kategorien sind hierarchisch mit ":" als Trenner. Der vollständige Pfad
steht redundant in der Tabelle, weil jede Anzeige und der gesamte Import
darüber laufen; Umbenennen und Verschieben schreiben den Teilbaum neu. Die
Art (Einnahme/Ausgabe) gilt für den ganzen Ast. Register wie im Original:
Alle, Einnahmen, Ausgaben, Umbuchungskonten.

QifParser liest zustandsbehaftet und sequenziell, wandelt ISO-8859-1 nach
UTF-8 und versteht US-Beträge sowie das Datumsformat aus der Kopfzeile.
QifStammdatenImport ist idempotent und bietet Trockenlauf und Protokoll -
im Dialog unter Verwaltung sowie über scripts/qif-import.php. Am echten
Export geprüft: 17 Klassen, 105 Kategorien, 37 Konten.

Enthält außerdem die Korrektur der englischen Monatsnamen im
Einstellungsdialog: date('F') ist sprachlich nicht umschaltbar, deshalb
liegen die Namen jetzt zentral als View::MONATE vor. Die Änderung ließ
sich nicht als eigener Commit trennen, weil sie dieselben Dateien berührt
wie Phase 1.

Tests: 227 PHP-Prüfungen und 14 Playwright-Tests, alle grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:33:40 +02:00

250 lines
8.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Finanzmanager\Models;
use Finanzmanager\Database;
use Finanzmanager\Mandant;
class Konto
{
/** Kontoarten des FinanzManagers, Schlüssel → Bezeichnung in der Oberfläche. */
public const ARTEN = [
'bank' => 'Bank',
'kreditkarte' => 'Kreditkarte',
'bar' => 'Bar',
'sparkonto' => 'Tagesgeld/Sparkonto',
'vermoegen' => 'Vermögen',
'verbindlichkeit' => 'Verbindlichkeit',
'rente' => 'Rentenversicherung',
'sonstiges' => 'Sonstiges Vermögen',
'depot' => 'Depot',
];
/**
* QIF-Kontotyp → Kontoart. "Oth E" und "Oth F" fasst der FinanzManager beim
* Export zu sonstigem Vermögen zusammen, "Port" und "Invst" beide zum Depot.
*/
public const QIF_ARTEN = [
'Bank' => 'bank',
'CCard' => 'kreditkarte',
'Cash' => 'bar',
'Oth T' => 'sparkonto',
'Oth A' => 'vermoegen',
'Oth L' => 'verbindlichkeit',
'Oth P' => 'rente',
'Oth E' => 'sonstiges',
'Oth F' => 'sonstiges',
'Port' => 'depot',
'Invst' => 'depot',
];
/** Konten, deren Saldo üblicherweise eine Schuld ausweist. */
public const SCHULDARTEN = ['kreditkarte', 'verbindlichkeit'];
public static function artAusQif(string $qifTyp): string
{
$typ = trim($qifTyp);
return self::QIF_ARTEN[$typ] ?? 'sonstiges';
}
public static function bezeichnungDerArt(string $art): string
{
return self::ARTEN[$art] ?? $art;
}
/** @return list<array<string,mixed>> */
public static function alle(bool $nurAktive = false): array
{
$sql = 'SELECT k.*, g.name AS gruppenname
FROM konten k
LEFT JOIN kontengruppen g ON g.id = k.kontengruppe_id
WHERE k.benutzer_id = ?';
if ($nurAktive) {
$sql .= ' AND k.aktiv = 1';
}
return Database::fetchAll(
$sql . ' ORDER BY g.sortierung IS NULL, g.sortierung, g.name, k.sortierung, k.name',
[Mandant::id()]
);
}
public static function find(int $id): ?array
{
$zeile = Database::fetchOne(
'SELECT * FROM konten WHERE id = ? AND benutzer_id = ?',
[$id, Mandant::id()]
);
return $zeile === false ? null : $zeile;
}
public static function findeNachName(string $name): ?array
{
$zeile = Database::fetchOne(
'SELECT * FROM konten WHERE name = ? AND benutzer_id = ?',
[trim($name), Mandant::id()]
);
return $zeile === false ? null : $zeile;
}
/**
* @param array<string,mixed> $daten
*/
public static function anlegen(array $daten): int
{
$name = trim((string)($daten['name'] ?? ''));
if ($name === '') {
throw new \InvalidArgumentException('Das Konto braucht einen Namen.');
}
if (self::findeNachName($name) !== null) {
throw new \RuntimeException('Das Konto „' . $name . '“ gibt es bereits.');
}
$felder = self::normalisiere($daten);
Database::execute(
'INSERT INTO konten
(benutzer_id, kontengruppe_id, name, kontoart, bankname, kontonummer, iban, bic,
beschreibung, anfangssaldo, kreditlimit, waehrung, aktiv, sortierung)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
Mandant::id(), $felder['kontengruppe_id'], $name, $felder['kontoart'],
$felder['bankname'], $felder['kontonummer'], $felder['iban'], $felder['bic'],
$felder['beschreibung'], $felder['anfangssaldo'], $felder['kreditlimit'],
$felder['waehrung'], $felder['aktiv'], $felder['sortierung'],
]
);
return Database::lastInsertId();
}
/**
* @param array<string,mixed> $daten
*/
public static function speichern(int $id, array $daten): void
{
$name = trim((string)($daten['name'] ?? ''));
if ($name === '') {
throw new \InvalidArgumentException('Das Konto braucht einen Namen.');
}
$andere = self::findeNachName($name);
if ($andere !== null && (int)$andere['id'] !== $id) {
throw new \RuntimeException('Das Konto „' . $name . '“ gibt es bereits.');
}
$felder = self::normalisiere($daten);
Database::execute(
'UPDATE konten SET kontengruppe_id = ?, name = ?, kontoart = ?, bankname = ?,
kontonummer = ?, iban = ?, bic = ?, beschreibung = ?, anfangssaldo = ?,
kreditlimit = ?, waehrung = ?, aktiv = ?, sortierung = ?
WHERE id = ? AND benutzer_id = ?',
[
$felder['kontengruppe_id'], $name, $felder['kontoart'], $felder['bankname'],
$felder['kontonummer'], $felder['iban'], $felder['bic'], $felder['beschreibung'],
$felder['anfangssaldo'], $felder['kreditlimit'], $felder['waehrung'],
$felder['aktiv'], $felder['sortierung'], $id, Mandant::id(),
]
);
}
public static function loeschen(int $id): void
{
Database::execute('DELETE FROM konten WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
}
public static function anzahl(): int
{
return (int)Database::fetchValue(
'SELECT COUNT(*) FROM konten WHERE benutzer_id = ?',
[Mandant::id()],
0
);
}
/**
* Kontenübersicht: Konten nach Kontengruppe gebündelt, je Gruppe eine Summe
* und darunter die Gesamtsumme.
*
* Die drei Saldospalten des FinanzManagers stehen schon jetzt in den Daten:
* Online-Saldo (Stand des letzten Bankabrufs, Phase 5), aktueller Saldo
* (gebuchte Umsätze bis heute) und erwarteter Saldo (einschließlich künftig
* datierter Buchungen). Solange es keine Buchungen gibt, entsprechen alle
* drei dem Anfangssaldo.
*
* @return array{gruppen: list<array<string,mixed>>, summe: float}
*/
public static function uebersicht(bool $nurAktive = true): array
{
$gruppen = [];
$summe = 0.0;
foreach (self::alle($nurAktive) as $konto) {
$konto = self::mitSalden($konto);
$name = $konto['gruppenname'] ?? null;
$name = ($name === null || $name === '') ? Kontengruppe::OHNE_GRUPPE : $name;
if (!isset($gruppen[$name])) {
$gruppen[$name] = ['name' => $name, 'konten' => [], 'summe' => 0.0];
}
$gruppen[$name]['konten'][] = $konto;
$gruppen[$name]['summe'] += $konto['saldo_erwartet'];
$summe += $konto['saldo_erwartet'];
}
return ['gruppen' => array_values($gruppen), 'summe' => $summe];
}
/**
* Ergänzt die Saldospalten. Ab Phase 2 kommen hier die Buchungen dazu –
* die Aufrufer bleiben davon unberührt.
*
* @param array<string,mixed> $konto
* @return array<string,mixed>
*/
public static function mitSalden(array $konto): array
{
$anfang = (float)$konto['anfangssaldo'];
$konto['saldo_online'] = null;
$konto['saldo_aktuell'] = $anfang;
$konto['saldo_erwartet'] = $anfang;
return $konto;
}
/**
* @param array<string,mixed> $daten
* @return array<string,mixed>
*/
private static function normalisiere(array $daten): array
{
$art = (string)($daten['kontoart'] ?? 'bank');
if (!isset(self::ARTEN[$art])) {
throw new \InvalidArgumentException('Unbekannte Kontoart: ' . $art);
}
$gruppe = $daten['kontengruppe_id'] ?? null;
$gruppe = ($gruppe === null || $gruppe === '' || (int)$gruppe === 0) ? null : (int)$gruppe;
$limit = $daten['kreditlimit'] ?? null;
$limit = ($limit === null || $limit === '') ? null : (float)$limit;
return [
'kontengruppe_id' => $gruppe,
'kontoart' => $art,
'bankname' => trim((string)($daten['bankname'] ?? '')),
'kontonummer' => trim((string)($daten['kontonummer'] ?? '')),
'iban' => strtoupper(str_replace(' ', '', (string)($daten['iban'] ?? ''))),
'bic' => strtoupper(trim((string)($daten['bic'] ?? ''))),
'beschreibung' => trim((string)($daten['beschreibung'] ?? '')),
'anfangssaldo' => (float)($daten['anfangssaldo'] ?? 0),
'kreditlimit' => $limit,
'waehrung' => strtoupper(trim((string)($daten['waehrung'] ?? 'EUR'))) ?: 'EUR',
'aktiv' => !empty($daten['aktiv']) ? 1 : 0,
'sortierung' => (int)($daten['sortierung'] ?? 0),
];
}
}