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>
250 lines
8.8 KiB
PHP
250 lines
8.8 KiB
PHP
<?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),
|
||
];
|
||
}
|
||
}
|