feat: Regeln nachtraeglich auf Buchungen ohne Kategorie anwenden
Regeln greifen beim Import. Alles, was vorher hereinkam – oder hereinkam, bevor es die Regel gab –, bleibt liegen. Bei einer Bankanbindung passiert genau das dauernd: Die Umsaetze kommen von selbst, die Regel entsteht spaeter. Die Revolut-Aufrundungen aufs Sparen-Pocket sind der Fall, an dem es aufgefallen ist. Der Knopf steht bei der Transfer-Erkennung und zeigt erst, was passieren wuerde. Uebertrags-Regeln laufen durch denselben Dienst wie jeder andere Uebertrag – er lehnt ab, wo die Gegenseite schon gebucht ist, und solche Buchungen bleiben unangetastet und werden mit Grund gemeldet. Ohne festes Zielkonto wird es aus dem Buchungstext erraten, aber nur bei GENAU einem Treffer und nur an Wortgrenzen: "Euro" darf nicht "PayPal Europe" treffen. Zwei Moeglichkeiten heisst raten, und dann passiert nichts. Angefasst werden nur Buchungen OHNE Kategorie – sonst ueberschriebe der Knopf stillschweigend, was von Hand zugeordnet wurde. Ein Uebertrag ist kein Kandidat mehr, ein zweiter Klick verschiebt das Geld also nicht erneut. Zwei Dinge fielen beim Testen auf und sind mit behoben: - Ein fehlgeschlagener Uebertrag verwarf per rollback() auch alles, was im selben Durchlauf schon gelungen war. Jetzt haelt ein Rettungspunkt je Uebertrag den Schaden bei dem einen. - test_neutral_category nahm VORHANDENE Kategorien und stellte eine davon auf "nicht einrechnen". Benutzten andere Buchungen sie, fielen die mit heraus und die Rechnung ging um deren Summe daneben – ohne Bezug zum Gepruefften. Der Test legt sich seine Kategorien jetzt selbst an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,8 +44,42 @@ function TransferKeywords({ accounts }) {
|
||||
const [amount, setAmount] = useState("");
|
||||
const [target, setTarget] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
// Was beim nachträglichen Anwenden übersprungen wurde, samt Grund.
|
||||
const [uebersprungen, setUebersprungen] = useState([]);
|
||||
const confirm = useConfirm();
|
||||
const accName = (id) => accounts.find((a) => a.id === id)?.name;
|
||||
|
||||
// Regeln greifen beim Import. Alles, was vorher hereinkam – oder bevor es die
|
||||
// Regel gab –, liegt noch ohne Kategorie da; bei einer Bankanbindung dauernd.
|
||||
// Erst zeigen, dann tun: Ein Übertrag verschiebt Geld.
|
||||
async function nachtraeglichAnwenden() {
|
||||
setBusy(true);
|
||||
try {
|
||||
const vor = await api.applyRules(true);
|
||||
if (!vor.kategorien && !vor.uebertraege) {
|
||||
toast(t("categories.applyNothing"));
|
||||
setUebersprungen(vor.uebersprungen || []);
|
||||
return;
|
||||
}
|
||||
if (!(await confirm({
|
||||
title: t("categories.applyRules"),
|
||||
message: t("categories.applyPreview", {
|
||||
kategorien: vor.kategorien, uebertraege: vor.uebertraege,
|
||||
}),
|
||||
confirmLabel: t("categories.applyRules"),
|
||||
}))) return;
|
||||
const r = await api.applyRules(false);
|
||||
toast(t("categories.applyDone", {
|
||||
kategorien: r.kategorien, uebertraege: r.uebertraege,
|
||||
}));
|
||||
setUebersprungen(r.uebersprungen || []);
|
||||
} catch (err) {
|
||||
toast(err.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function laden() {
|
||||
try { setWords(await api.listTransferKeywords()); }
|
||||
@@ -99,6 +133,22 @@ function TransferKeywords({ accounts }) {
|
||||
<Icon name="plus" size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mail-actions" style={{ marginTop: "var(--sp-2)" }}>
|
||||
<button className="btn sm" disabled={busy} onClick={nachtraeglichAnwenden}>
|
||||
{t("categories.applyRules")}
|
||||
</button>
|
||||
<span className="muted small">{t("categories.applyRulesHint")}</span>
|
||||
</div>
|
||||
{uebersprungen.length > 0 && (
|
||||
<div className="alert warn" style={{ display: "block", marginTop: "var(--sp-2)" }}>
|
||||
<strong className="small">{t("categories.applySkipped")}</strong>
|
||||
<ul className="muted small" style={{ paddingLeft: "1.2em", margin: 0 }}>
|
||||
{uebersprungen.map((m, i) => (
|
||||
<li key={i} className="selectable">{m.datum} · {m.text} — {m.grund}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{words.length === 0 ? (
|
||||
<div className="muted small">{t("transferRules.empty")}</div>
|
||||
) : (
|
||||
@@ -265,7 +315,10 @@ export default function Accounts() {
|
||||
<div><strong>{a.name}</strong></div>
|
||||
<div className="named-sub">
|
||||
{t(`accountTypes.${a.type}`)} · {a.currency}
|
||||
{a.identifiers.length > 0 && ` · ${a.identifiers.length} ${t("accounts.identShort")}`}
|
||||
{/* „1 Nr." sagte niemandem, was gemeint ist: die Kennungen, an denen
|
||||
Certain eine Auszugszeile diesem Konto zuordnet. */}
|
||||
{a.identifiers.length > 0
|
||||
&& ` · ${t("accounts.identShort", { count: a.identifiers.length })}`}
|
||||
</div>
|
||||
{/* Bei angebundenen Konten der Saldo, den die Bank fuehrt.
|
||||
Er ersetzt die eigene Rechnung nicht, er prueft sie: Eine
|
||||
|
||||
Reference in New Issue
Block a user