feat: Saldo-Abgleich beim CSV-Import gegen die Kontostand-Spalte
Bisher merkte man einen Importfehler erst am falschen Endsaldo, ohne Hinweis auf die Ursache. Enthält der Auszug eine laufende Kontostand-Spalte, gleicht der Import jetzt jede Zeile dagegen ab: Die Spalte sagt, wie sich der Saldo tatsächlich verändert hat, unsere Erkennung muss dazu passen. - neue Spaltenzuordnung "Kontostand (laufend)"; Presets Raiffeisen/Revolut ergänzt - Prüfansicht zeigt: stimmt überein ODER Differenz + erste abweichende Zeile (Datum, Empfänger, Betrag laut Bank vs. erkannt) - meldet zusätzlich den Anfangssaldo laut Datei – häufigste Fehlerquelle - rechnet mit dem tatsächlich gebuchten Betrag, damit auch nicht gebuchte Zeilen auffallen (z. B. nicht zugeordnete Gebührenspalte); Fremdwährungs-Zeilen gehören zu einem anderen Saldo-Verlauf und bleiben außen vor - erkennt absteigend sortierte Auszüge - Tests: stimmiger Auszug (ok) und fehlende Gebührenspalte (zeigt genau die Zeile). 83 grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@ export default function Import() {
|
||||
const [config, setConfig] = useState(defaultConfig());
|
||||
const [raw, setRaw] = useState(null);
|
||||
const [rows, setRows] = useState(null);
|
||||
const [recon, setRecon] = useState(null);
|
||||
const [docText, setDocText] = useState("");
|
||||
const [docPages, setDocPages] = useState([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -114,7 +115,7 @@ export default function Import() {
|
||||
setBusy(true);
|
||||
try {
|
||||
setRaw(await api.importRaw(baseForm()));
|
||||
setRows(null);
|
||||
setRows(null); setRecon(null);
|
||||
} catch (e) { toast(e.message, "error"); } finally { setBusy(false); }
|
||||
}
|
||||
|
||||
@@ -130,6 +131,7 @@ export default function Import() {
|
||||
fd.append("column_map", JSON.stringify(config.column_map));
|
||||
const res = await api.importPreview(fd);
|
||||
setRows(res.rows.map((r) => ({ ...r })));
|
||||
setRecon(res.reconciliation || null);
|
||||
} catch (e) { toast(e.message, "error"); } finally { setBusy(false); }
|
||||
}
|
||||
|
||||
@@ -142,6 +144,7 @@ export default function Import() {
|
||||
fd.append("account_id", accountId);
|
||||
const res = await api.documentPreview(fd);
|
||||
setRows(res.rows.map((r) => ({ ...r })));
|
||||
setRecon(null);
|
||||
setDocText(res.text || "");
|
||||
setDocPages(res.pages || []);
|
||||
} catch (e) { toast(e.message, "error"); } finally { setBusy(false); }
|
||||
@@ -157,7 +160,7 @@ export default function Import() {
|
||||
rows,
|
||||
});
|
||||
toast(t("import.imported", { n: batch.imported_count }));
|
||||
setRows(null); setRaw(null); setFile(null); setDocText(""); setDocPages([]);
|
||||
setRows(null); setRecon(null); setRaw(null); setFile(null); setDocText(""); setDocPages([]);
|
||||
if (fileRef.current) fileRef.current.value = "";
|
||||
loadStamm();
|
||||
} catch (e) { toast(e.message, "error"); } finally { setBusy(false); }
|
||||
@@ -495,6 +498,7 @@ export default function Import() {
|
||||
["fee", t("import.colFee")],
|
||||
["currency", t("import.colCurrency")],
|
||||
["state", t("import.colState")],
|
||||
["balance", t("import.colBalance")],
|
||||
].map(([field, label]) => (
|
||||
<label key={field}>
|
||||
<span>{label}</span>
|
||||
@@ -537,6 +541,41 @@ export default function Import() {
|
||||
<span>{t("import.noneValid")}</span>
|
||||
</div>
|
||||
)}
|
||||
{recon?.available && (
|
||||
<div className={`alert ${recon.ok ? "ok" : "error"}`} style={{ marginBottom: "var(--sp-3)" }}>
|
||||
<Icon name={recon.ok ? "check" : "alert"} size={16} />
|
||||
<div>
|
||||
{recon.ok ? (
|
||||
<span>{t("import.reconOk", { n: recon.checked })}</span>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{t("import.reconDiff", {
|
||||
file: formatMoney(recon.final_balance_cents, acc?.currency || "EUR"),
|
||||
app: formatMoney(recon.computed_final_cents, acc?.currency || "EUR"),
|
||||
diff: formatMoney(recon.difference_cents, acc?.currency || "EUR"),
|
||||
})}
|
||||
</div>
|
||||
{recon.first_mismatch && (
|
||||
<div className="muted small">
|
||||
{t("import.reconFirst", {
|
||||
date: recon.first_mismatch.date ? formatDate(recon.first_mismatch.date) : "?",
|
||||
payee: recon.first_mismatch.payee || "—",
|
||||
expected: formatMoney(recon.first_mismatch.expected_cents, acc?.currency || "EUR"),
|
||||
actual: formatMoney(recon.first_mismatch.actual_cents, acc?.currency || "EUR"),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="muted small">
|
||||
{t("import.reconOpening", {
|
||||
value: formatMoney(recon.opening_balance_cents, acc?.currency || "EUR"),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DataTable
|
||||
id="import-review"
|
||||
rows={rows}
|
||||
|
||||
Reference in New Issue
Block a user