Finanzmanager/scripts/qif-import.php
fds2610-bot 3e55b3ba73 QIF-Import: nur ausgewählte Konten übernehmen
Der Import kannte bisher nur den Zeitraum als Filter – ein Export ließ sich
nur ganz einlesen. Neu ist die Option "konten": im Dialog eine Ankreuzliste,
die erst nach dem Trockenlauf erscheint (vorher ist die Datei ungelesen), auf
der Kommandozeile --konto=NAME und --konten.

Maßgeblich ist nicht allein der !Account-Abschnitt, sondern jedes Konto, das
der Eintrag berührt: das eigene, die Umbuchungsziele aus L und den Splitzeilen
und beim Wertpapiergeschäft das Verrechnungskonto. Eine Umbuchung steht nur
einmal in der Datei – sonst fehlte dem gewählten Konto die Gegenseite.

Die Anfangssalden werden weiter über die ganze Datei gerechnet, aber nur für
die gewählten Konten geschrieben; ein Teilimport überschreibt sonst fremde
Salden. Die Fingerabdrücke entstehen jetzt vorab in einem eigenen Durchgang:
Der laufende Zähler für gleiche Fingerabdrücke hing sonst an der Auswahl, und
dieselbe Buchung wäre beim zweiten Lauf mit anderer Auswahl doppelt angelegt
worden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:50:43 +02:00

202 lines
6.9 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.

#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* QIF-Import auf der Kommandozeile.
*
* Dieselbe Logik wie im Dialog unter Verwaltung → QIF-Import, aber ohne Upload –
* praktisch für den großen Vollexport und für die Erstbefüllung.
*
* docker compose exec -u www-data php php scripts/qif-import.php data/import/Export.QIF
* docker compose exec -u www-data php php scripts/qif-import.php --uebernehmen data/import/Export.QIF
*
* Ohne --uebernehmen läuft nur der Trockenlauf: es wird nichts gespeichert.
*
* Weitere Optionen:
* --nur-stammdaten Klassen, Kategorien und Konten, keine Buchungen
* --stichtag=JJJJ-MM-TT ab hier werden Buchungen übernommen; alles davor
* fließt in den Anfangssaldo (Vorgabe aus den
* Einstellungen, sonst 2026-01-01)
* --bis=JJJJ-MM-TT Enddatum; spätere Buchungen sind Vorausberechnungen
* des FinanzManagers (Vorgabe: heute)
* --konto=NAME nur dieses Konto übernehmen; mehrfach angebbar. Ohne
* Angabe zählen alle Konten der Datei.
* --konten nennt die Konten der Datei und beendet sich
* --benutzer=NAME Mandant, sonst der erste aktive Benutzer
*/
require dirname(__DIR__) . '/vendor/autoload.php';
use Finanzmanager\Config;
use Finanzmanager\Mandant;
use Finanzmanager\Models\Benutzer;
use Finanzmanager\Models\Einstellung;
use Finanzmanager\Services\QifBuchungsImport;
use Finanzmanager\Services\QifParser;
use Finanzmanager\Services\QifStammdatenImport;
Config::load();
$argumente = array_slice($argv, 1);
$uebernehmen = false;
$nurStammdaten = false;
$nurKonten = false;
$benutzer = null;
$datei = null;
$stichtag = null;
$bis = null;
$konten = [];
foreach ($argumente as $argument) {
if ($argument === '--uebernehmen') {
$uebernehmen = true;
} elseif ($argument === '--nur-stammdaten') {
$nurStammdaten = true;
} elseif ($argument === '--konten') {
$nurKonten = true;
} elseif (str_starts_with($argument, '--konto=')) {
$konten[] = substr($argument, 8);
} elseif (str_starts_with($argument, '--benutzer=')) {
$benutzer = substr($argument, 11);
} elseif (str_starts_with($argument, '--stichtag=')) {
$stichtag = substr($argument, 11);
} elseif (str_starts_with($argument, '--bis=')) {
$bis = substr($argument, 6);
} elseif (str_starts_with($argument, '-')) {
fwrite(STDERR, "Unbekannte Option: {$argument}\n");
exit(1);
} else {
$datei = $argument;
}
}
if ($datei === null) {
fwrite(STDERR, "Aufruf: qif-import.php [--uebernehmen] [--nur-stammdaten] [--konten] "
. "[--konto=NAME] [--stichtag=JJJJ-MM-TT] [--bis=JJJJ-MM-TT] [--benutzer=NAME] <datei.qif>\n");
exit(1);
}
// Relative Angaben beziehen sich auf das Projektverzeichnis.
if (!str_starts_with($datei, '/')) {
$datei = dirname(__DIR__) . '/' . $datei;
}
if (!is_file($datei)) {
fwrite(STDERR, "Datei nicht gefunden: {$datei}\n");
exit(1);
}
if ($benutzer !== null) {
$zeile = Benutzer::findByName($benutzer);
if ($zeile === null) {
fwrite(STDERR, "Benutzer nicht gefunden: {$benutzer}\n");
exit(1);
}
Mandant::setze((int)$zeile['id']);
}
$stichtag ??= (string)Einstellung::get('import_stichtag', '2026-01-01');
$bis ??= date('Y-m-d');
// Die Konten der Datei nennen, ohne etwas anzufassen – so lässt sich
// nachsehen, welche Namen --konto= überhaupt kennt.
if ($nurKonten) {
try {
$parser = QifParser::ausDatei($datei);
} catch (\Throwable $e) {
fwrite(STDERR, 'Datei nicht lesbar: ' . $e->getMessage() . "\n");
exit(1);
}
echo 'Konten in ' . basename($datei) . ":\n";
foreach (QifBuchungsImport::kontenDerDatei($parser) as $name => $anzahl) {
printf(" %-40s %6d Einträge\n", $name, $anzahl);
}
exit(0);
}
try {
$parser = QifParser::ausDatei($datei);
// Ein Tippfehler im Kontonamen bliebe sonst unbemerkt: Der Import liefe
// durch und legte nichts an.
$bekannt = array_keys(QifBuchungsImport::kontenDerDatei($parser));
foreach ($konten as $name) {
if (!in_array($name, $bekannt, true)) {
fwrite(STDERR, "Konto nicht in der Datei: {$name}\n");
fwrite(STDERR, "Vorhanden: " . implode(', ', $bekannt) . "\n");
exit(1);
}
}
$stamm = (new QifStammdatenImport($parser))->ausfuehren(!$uebernehmen);
$buch = null;
if (!$nurStammdaten) {
$buch = (new QifBuchungsImport($parser, [
'stichtag' => $stichtag,
'bis' => $bis,
'konten' => $konten,
]))->ausfuehren(!$uebernehmen);
}
} catch (\Throwable $e) {
fwrite(STDERR, 'Import fehlgeschlagen: ' . $e->getMessage() . "\n");
exit(1);
}
echo ($uebernehmen ? 'Import' : 'Trockenlauf') . ': ' . basename($datei) . "\n";
$abschnitte = [];
foreach ($parser->zusammenfassung() as $typ => $anzahl) {
$abschnitte[] = "{$typ}: {$anzahl}";
}
echo 'Datei enthält ' . implode(' · ', $abschnitte) . "\n\n";
printf("%-12s %8s %12s %8s\n", 'Bereich', $uebernehmen ? 'angelegt' : 'neu', 'vorhanden', 'Fehler');
foreach (['klassen' => 'Klassen', 'kategorien' => 'Kategorien', 'konten' => 'Konten'] as $schluessel => $titel) {
printf(
"%-12s %8d %12d %8d\n",
$titel,
$stamm[$schluessel]['neu'],
$stamm[$schluessel]['vorhanden'],
$stamm[$schluessel]['fehler']
);
}
$meldungen = $stamm['meldungen'];
if ($buch !== null) {
printf("%-12s %8d %12d %8d\n", 'Buchungen',
$buch['buchungen']['neu'], $buch['buchungen']['vorhanden'], $buch['buchungen']['fehler']);
echo "\nZeitraum ab {$stichtag} bis {$bis}\n";
if ($buch['konten'] !== null) {
echo ' nur die Konten: ' . implode(', ', $buch['konten']) . "\n";
printf(" wegen der Kontoauswahl übergangen: %4d Buchungen\n", $buch['buchungen']['nicht_gewaehlt']);
}
printf(" vor dem Stichtag verrechnet: %6d Buchungen auf %d Konten (Summe %s)\n",
$buch['buchungen']['vor_stichtag'],
$buch['anfangssalden']['konten'],
number_format($buch['anfangssalden']['summe'], 2, ',', '.'));
printf(" nach dem Enddatum verworfen: %6d vorausberechnete Buchungen\n", $buch['buchungen']['zukunft']);
printf(" darunter Wertpapiergeschäfte: %5d (nur Geldfluss)\n", $buch['buchungen']['wertpapier']);
$meldungen = array_merge($meldungen, $buch['meldungen']);
}
if ($meldungen !== []) {
echo "\nProtokoll:\n";
foreach ($meldungen as $meldung) {
echo ' - ' . $meldung['text'] . "\n";
}
}
if (!$uebernehmen) {
echo "\nEs wurde nichts gespeichert. Mit --uebernehmen wird der Import durchgeführt.\n";
}
$fehler = $stamm['klassen']['fehler'] + $stamm['kategorien']['fehler'] + $stamm['konten']['fehler']
+ ($buch['buchungen']['fehler'] ?? 0);
exit($fehler > 0 ? 1 : 0);