Der Plan ging davon aus, Umbuchungen stünden zweimal im Export und müssten zusammengeführt werden. Der Vollexport zeigt das Gegenteil: Eine Umbuchung steht einmal, in dem Konto, in dem sie erfasst wurde; die Gegenseite ist nur durch L[Zielkonto] benannt und muss beim Import erzeugt werden. Von 965 Umbuchungen hat keine ein zweites Vorkommen. Mit erzeugter Gegenseite stimmen acht gegen den FinanzManager geprüfte Kontosalden auf den Cent. Datenmodell: Migration 003 mit buchungen, buchung_splits und buchungsadressen. Eine Umbuchung ist ein Datensatz mit gegenkonto_id, kein gespiegeltes Paar - zwei Zeilen laufen früher oder später auseinander. Das Kontoblatt des Gegenkontos zeigt sie mit umgekehrtem Vorzeichen. Auch einzelne Splitzeilen dürfen ein Gegenkonto tragen. Konten führen zusätzlich in_summe, die Sigma-Spalte der Kontenübersicht, und saldo_online für Phase 5. Oberfläche: Kontoblatt mit laufendem Saldo, Filter auf Zeitraum, Kategorie, Klasse und Abgleich, Volltextsuche und Seitenblättern. Buchungsmaske für Einnahme, Ausgabe und Umbuchung in beide Richtungen, mit Splitzeilen und Restbetragsanzeige. Import: QifBuchungsImport arbeitet mit Stichtag. Alles davor wird je Konto zum Anfangssaldo aufsummiert, Gegenbuchungen eingerechnet; alles danach bis zum Importtag kommt einzeln in die Datenbank. Spätere Buchungen sind Vorausberechnungen des FinanzManagers und bleiben draußen. Wertpapiergeschäfte schlagen vorerst nur mit ihrem Geldfluss auf dem Verrechnungskonto zu Buche. Am Vollexport geprüft: 833 Buchungen ab 01.01.2026, 7946 in die Anfangssalden von 35 Konten verrechnet. Behoben: Die confirm()-Rückfragen vor dem Löschen waren wirkungslos. public/static/app.js brachte den Mechanismus mit, war aber nie im Layout eingebunden; die ersatzweise gesetzten onsubmit-Attribute verstoßen gegen die eigene Content-Security-Policy und wurden vom Browser verworfen - gelöscht wurde ohne Rückfrage. Ein E2E-Test besteht jetzt auf dem Dialog. Tests: 307 PHP-Prüfungen und 21 Playwright-Tests, alle grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
6.6 KiB
Markdown
139 lines
6.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 |
|
||
|
||
## 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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
## 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 €).
|