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>
187 lines
9.6 KiB
Markdown
187 lines
9.6 KiB
Markdown
# QIF-Import
|
||
|
||
Der Lexware FinanzManager exportiert erweitertes Quicken-QIF. Die `.QDF`-Datei ist
|
||
verschlüsselt und damit keine Migrationsquelle – der QIF-Export ist der einzige Weg.
|
||
|
||
Stand: Klassen, Kategorien, Konten (Phase 1) sowie Buchungen samt Splitzeilen und
|
||
Umbuchungen (Phase 2) werden übernommen. Wertpapierbestände folgen in Phase 6,
|
||
Buchungsvorlagen in Phase 7.
|
||
|
||
## Aufbau der Datei
|
||
|
||
```
|
||
!Option:MDY Datumsformat für die gesamte Datei
|
||
!Type:Class Klassen
|
||
!Type:Cat Kategorien
|
||
!Option:AutoSwitch Beginn der Kontenliste
|
||
!Account ein Konto je Eintrag
|
||
!Clear:AutoSwitch Ende der Kontenliste
|
||
!Account / !Type:… ab hier abwechselnd Konto und dessen Buchungen
|
||
```
|
||
|
||
Ein Eintrag endet mit `^`. Jede Zeile beginnt mit einem Feldbuchstaben, der Rest ist der
|
||
Wert. Felder dürfen mehrfach auftreten (Splitzeilen, Adresszeilen), deshalb hält
|
||
`QifParser` jeden Eintrag als Liste von Paaren und bietet `feld()`, `felder()` und
|
||
`hatFeld()`.
|
||
|
||
**Die Datei ist zustandsbehaftet.** Der Kontobezug einer Buchung steckt allein darin,
|
||
welcher `!Account`-Abschnitt zuletzt kam – nach Sektionstypen gruppiert gelesen wäre er
|
||
verloren. Der Parser liefert daher eine flache Liste in Dateireihenfolge, jeder Eintrag mit
|
||
Sektionstyp und aktuellem Konto.
|
||
|
||
Jeder Kontoeintrag setzt zugleich das aktuelle Konto. In der Kontenliste folgen mehrere
|
||
aufeinander, ohne dass Buchungen dazwischenstehen; das schadet nicht und deckt zugleich die
|
||
zweite Schreibweise ab, bei der ein einzelner `!Account` den Kontowechsel bedeutet.
|
||
|
||
## Felder der Stammdaten
|
||
|
||
| Sektion | Feld | Bedeutung |
|
||
|---|---|---|
|
||
| `!Type:Class` | `N` / `D` | Name, Beschreibung |
|
||
| `!Type:Cat` | `N` | vollständiger Pfad mit `:` |
|
||
| | `D` | Beschreibung |
|
||
| | `I` / `E` | Einnahme bzw. Ausgabe (Marker ohne Wert) |
|
||
| | `T` | steuerrelevant (Marker ohne Wert) |
|
||
| | `R` | Kennziffer des Steuerformulars |
|
||
| `!Account` | `N` / `T` | Name, Kontotyp (siehe `docs/stammdaten.md`) |
|
||
| | `D` | Beschreibung; `Kontonr. 12345` wird als Kontonummer übernommen |
|
||
| | `L` | Kreditlimit im US-Format |
|
||
|
||
**Kontengruppen fehlen im Export.** Ein `!Account`-Eintrag führt nur die vier Felder oben –
|
||
kein Feld nennt die Gruppe, und auch die `QSD`-Datei enthält weder Gruppennamen noch
|
||
Zuordnung. Der Import kann sie deshalb nicht anlegen: nach dem Einlesen stehen alle Konten
|
||
unter „Nicht gruppiert“. Die Gruppen werden einmalig unter *Verwaltung → Kontengruppen*
|
||
angelegt; dort lassen sich die Konten einer Gruppe in einem Zug ankreuzen.
|
||
|
||
## Fallstricke
|
||
|
||
1. **Zeichensatz ISO-8859-1, Zeilenenden CRLF.** `QifParser::nachUtf8()` prüft erst, ob
|
||
schon UTF-8 vorliegt – eine zweite Konvertierung zerstörte die Umlaute.
|
||
2. **Beträge im US-Format**: `-1,241.04`. Das Komma ist Tausendertrenner.
|
||
3. **Datum laut Kopfzeile**, meist `MDY` mit zweistelligem Jahr: `D3.18.26` ist der
|
||
18.03.2026. Jahreszahlen ab 70 gelten als 19xx.
|
||
4. **Kategorie und Klasse in einer Zeile**: `L400 - Leben:B-Essen alleine/Privat`.
|
||
5. **Unterkategorien ohne genannte Elternkategorie.** Der Import legt fehlende
|
||
Zwischenstufen selbst an (`Kategorie::findenOderAnlegenNachPfad()`).
|
||
6. **Konten stehen zweimal in der Datei** – einmal in der Kontenliste, einmal als Kopf
|
||
ihrer Buchungen. Der zweite Treffer ist keine Dublette.
|
||
7. **Umbuchungen stehen nur einmal in der Datei** – in dem Konto, in dem sie erfasst
|
||
wurden. Die Gegenseite ist allein durch `L[Zielkonto]` benannt und wird beim Import
|
||
erzeugt; ohne sie fehlte dem Gegenkonto der Betrag. Auch Splitzeilen können ein
|
||
Umbuchungsziel tragen (`S[Konto]`). Eine Buchung, die auf ihr eigenes Konto verweist,
|
||
ist die Eröffnungsbuchung und bekommt keine Gegenseite.
|
||
8. **`C` ist der Abgleichstatus** (`X` = abgeglichen), kein Kennzeichen für
|
||
vorausberechnete Buchungen – Wertpapierbuchungen führen es nie. Geplante Buchungen
|
||
sortiert man über das Datum aus.
|
||
9. **Anfangssalden stehen nicht als eigenes Feld im Export.** Sie ergeben sich aus den
|
||
Buchungen vor dem Stichtag, Gegenbuchungen eingerechnet.
|
||
|
||
## Arbeitsweise der Importeure
|
||
|
||
Beide sind **idempotent**: Was es schon gibt, wird gezählt und übersprungen, nie doppelt
|
||
angelegt. Ein zweiter Lauf derselben Datei ändert nichts. Der echte Lauf liegt jeweils in
|
||
einer Transaktion – ein Abbruch mitten in der Kategorieliste hinterließe sonst einen halben
|
||
Baum.
|
||
|
||
`QifStammdatenImport` (Klassen, Kategorien, Konten): Der **Trockenlauf** schreibt gar
|
||
nicht, sondern zählt mit einer Menge bereits bekannter Pfade mit; auch die Zwischenstufen,
|
||
die der Import selbst anlegen würde, erscheinen in der Vorschau.
|
||
|
||
`QifBuchungsImport` arbeitet mit einem **Stichtag**:
|
||
|
||
* Buchungen **vor** dem Stichtag werden nicht gespeichert, sondern je Konto zum
|
||
Anfangssaldo aufsummiert – einschließlich der Gegenbuchungen aus Umbuchungen. Die Salden
|
||
stimmen dadurch exakt, ohne die ganze Historie mitzuschleppen.
|
||
* Buchungen **im Zeitraum** kommen einzeln in die Datenbank, mit Splitzeilen, Klasse,
|
||
Abgleichstatus und Adresszeilen.
|
||
* Buchungen **nach dem Enddatum** (Vorgabe: heute) sind Vorausberechnungen des
|
||
FinanzManagers und bleiben draußen.
|
||
* **Wertpapiergeschäfte** schlagen vorerst nur mit ihrem Geldfluss auf dem
|
||
Verrechnungskonto zu Buche (`L|[Konto]` und `$`). Ein Betrag auf dem Depot ohne
|
||
Gegenwert würde die Summe der Kontostände verfälschen; Bestände folgen in Phase 6.
|
||
|
||
## Nur einzelne Konten übernehmen
|
||
|
||
Der Import lässt sich auf einzelne Konten begrenzen (Option `konten`, im Dialog eine
|
||
Ankreuzliste, auf der Kommandozeile `--konto=NAME`). Drei Dinge sind dabei entschieden:
|
||
|
||
1. **Maßgeblich ist jedes berührte Konto, nicht nur der `!Account`-Abschnitt.** Eine
|
||
Umbuchung steht nur einmal in der Datei, beim erfassenden Konto. Zählte allein dieser
|
||
Abschnitt, fehlte dem gewählten Konto die Gegenseite. Deshalb kommt ein Eintrag mit,
|
||
sobald sein Konto, ein Umbuchungsziel aus `L` oder `S` oder – beim Wertpapiergeschäft –
|
||
das Verrechnungskonto in der Auswahl steht. Umgekehrt heißt das: Wer nur das Zielkonto
|
||
wählt, bekommt die Buchung auch auf dem erfassenden Konto zu sehen; sie gehört dort hin
|
||
und lässt sich nicht halbieren.
|
||
2. **Anfangssalden werden weiterhin über die ganze Datei gerechnet, aber nur für die
|
||
gewählten Konten geschrieben.** Der Saldo eines Kontos steckt auch in Buchungen, die
|
||
anderswo erfasst wurden – ohne den vollen Durchlauf wäre er falsch. Und ein Teilimport
|
||
darf die Salden der übrigen Konten nicht überschreiben.
|
||
3. **Die Fingerabdrücke entstehen vorab in einem eigenen Durchgang.** Der laufende Zähler
|
||
für gleiche Fingerabdrücke (siehe unten) hinge sonst an der Auswahl: Dieselbe Buchung
|
||
bekäme im Teilimport einen anderen Hash als im Vollimport und käme ein zweites Mal in
|
||
die Datenbank. So lassen sich die Konten nacheinander einlesen, ohne dass die
|
||
Umbuchungen zwischen ihnen doppelt entstehen.
|
||
|
||
Die auswählbaren Konten liefert `QifBuchungsImport::kontenDerDatei()` – erst nach dem
|
||
Lesen steht fest, was die Datei führt. Enthalten sind auch Konten, die selbst keine
|
||
Buchung tragen und nur Umbuchungsziel sind; sonst ließe sich genau deren Gegenseite nicht
|
||
übernehmen.
|
||
|
||
Die Dublettenerkennung nutzt einen Fingerabdruck aus Konto, Datum, Betrag, Nummer und
|
||
Text. Weil es echte Doppelbuchungen am selben Tag gibt (zweimal dasselbe im selben Laden),
|
||
zählt ein laufender Index gleiche Fingerabdrücke durch – stabil über Läufe hinweg, weil
|
||
die Datei immer dieselbe Reihenfolge hat.
|
||
|
||
## Bedienung
|
||
|
||
Im Dialog unter *Verwaltung → QIF-Import*: Datei hochladen, Trockenlauf, dann übernehmen.
|
||
Hochgeladene Dateien bleiben im Importverzeichnis (`data/import`), damit derselbe Export
|
||
erst geprüft und dann übernommen werden kann.
|
||
|
||
Nach dem Trockenlauf steht unter *Konten* die Ankreuzliste der Konten, die in der Datei
|
||
vorkommen – vorher gibt es nichts zu wählen, weil die Datei noch nicht gelesen wurde. Von
|
||
dort führen zwei Wege weiter: ein erneuter Trockenlauf mit der Auswahl oder das Übernehmen.
|
||
Ohne Häkchen zählt die ganze Datei.
|
||
|
||
Auf der Kommandozeile:
|
||
|
||
```bash
|
||
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. `--benutzer=NAME` wählt den Mandanten,
|
||
sonst gilt der erste aktive Benutzer.
|
||
|
||
`--stichtag=` und `--bis=` überschreiben die Vorgaben; ohne Angabe gilt der Stichtag aus
|
||
den Einstellungen (Reiter *Import/Export*) und als Enddatum der heutige Tag.
|
||
`--nur-stammdaten` lässt die Buchungen aus.
|
||
|
||
```bash
|
||
docker compose exec -u www-data php php scripts/qif-import.php --konten data/import/Export.QIF
|
||
docker compose exec -u www-data php php scripts/qif-import.php \
|
||
--konto="DKB Girokonto" --konto="Tagesgeld" --uebernehmen data/import/Export.QIF
|
||
```
|
||
|
||
`--konten` nennt die Konten der Datei und beendet sich, ohne etwas anzufassen.
|
||
`--konto=` grenzt den Import ein und ist mehrfach angebbar; ein Name, den die Datei nicht
|
||
führt, bricht ab – sonst liefe der Import durch und legte nichts an.
|
||
|
||
## Prüfung gegen den echten Export
|
||
|
||
Der Vollexport (8780 Buchungen, 2017–2026) ergibt mit Stichtag 01.01.2026:
|
||
|
||
```
|
||
Klassen 17 angelegt
|
||
Kategorien 105 angelegt
|
||
Konten 37 angelegt, 36 vorhanden (die Zweitnennung je Konto)
|
||
Buchungen 833 angelegt
|
||
|
||
vor dem Stichtag verrechnet: 7946 Buchungen auf 35 Konten
|
||
nach dem Enddatum verworfen: 1 vorausberechnete Buchung
|
||
darunter Wertpapiergeschäfte: 16 (nur Geldfluss)
|
||
```
|
||
|
||
Der Härtetest sind die Salden: Acht Konten, deren Werte aus der Kontenübersicht des
|
||
FinanzManagers vorliegen, stimmen danach **auf den Cent** – von „Whg Besigheim“
|
||
(210.601,99 €) bis „Auslagen Eltern Wohnungskauf“ (−22.410,94 €).
|