feat: Kontostand von der Bank – die eigene Rechnung wird geprueft
Certain rechnet den Saldo bisher ausschliesslich selbst aus: Anfangsbestand plus alle Buchungen. Stimmt eine Buchung nicht, faellt das erst auf, wenn man von Hand mit dem Auszug vergleicht. Der von der Bank gemeldete Saldo wird jetzt mitgefuehrt und danebengestellt. Weicht er ab, fehlt oder doppelt eine Buchung – und man sieht es auf der Kontenseite und in den Einstellungen sofort, statt es zu suchen. Banken melden mehrere Saldi nebeneinander; ausgewaehlt wird der gebuchte, denn nur der ist mit Certains Rechnung vergleichbar. Ein verfuegbarer Saldo enthaelt Vorgemerktes oder einen Ueberziehungsrahmen. Die Saldi kommen auch fuer noch nicht zugeordnete Konten: Bei mehreren Revolut-Pockets ist der Betrag oft das Einzige, woran man sie unterscheidet. Ein Fehler bei einem Konto beendet den Durchlauf nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import IconChip from "../components/IconChip";
|
||||
import Money from "../components/Money";
|
||||
import Modal from "../components/Modal";
|
||||
import IconPicker, { ACCOUNT_ICONS } from "../components/IconPicker";
|
||||
import { centsToInput, formatMoney, parseAmountToCents } from "../format";
|
||||
import { centsToInput, formatDate, formatMoney, parseAmountToCents } from "../format";
|
||||
|
||||
const ACCOUNT_TYPES = ["girokonto", "sparkonto", "bargeld", "kreditkarte", "depot", "sonstiges"];
|
||||
const TYPE_ICON = {
|
||||
@@ -248,6 +248,28 @@ export default function Accounts() {
|
||||
{t(`accountTypes.${a.type}`)} · {a.currency}
|
||||
{a.identifiers.length > 0 && ` · ${a.identifiers.length} ${t("accounts.identShort")}`}
|
||||
</div>
|
||||
{/* Bei angebundenen Konten der Saldo, den die Bank fuehrt.
|
||||
Er ersetzt die eigene Rechnung nicht, er prueft sie: Eine
|
||||
Abweichung heisst, dass eine Buchung fehlt oder doppelt
|
||||
ist – sonst faellt das erst beim Vergleich von Hand auf. */}
|
||||
{a.bank_balance_cents != null && (
|
||||
<div className="named-sub">
|
||||
{a.bank_balance_cents === a.balance_cents ? (
|
||||
<span className="saldo-ok">
|
||||
{t("accounts.bankBalanceMatches")}
|
||||
{a.bank_balance_at ? ` · ${formatDate(a.bank_balance_at)}` : ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="saldo-abweichung">
|
||||
{t("accounts.bankBalanceDiffers", {
|
||||
bank: formatMoney(a.bank_balance_cents, a.currency),
|
||||
diff: formatMoney(a.balance_cents - a.bank_balance_cents,
|
||||
a.currency),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Money cents={a.balance_cents} currency={a.currency}
|
||||
baseCents={a.balance_base_cents} baseCurrency={baseCurrency} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useToast } from "../../toast";
|
||||
import { useConfirm } from "../../confirm";
|
||||
import Icon from "../../components/Icon";
|
||||
import Modal from "../../components/Modal";
|
||||
import { formatDate } from "../../format";
|
||||
import { formatDate, formatMoney } from "../../format";
|
||||
|
||||
// Länder, in denen die Banken dieses Nutzers liegen können. Die Schweiz fehlt
|
||||
// bewusst: Sie fällt nicht unter PSD2, dort gibt es keine Schnittstelle.
|
||||
@@ -100,6 +100,19 @@ export default function Bankanbindung() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saldenHolen(v) {
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await api.refreshBankBalances(v.id);
|
||||
toast(t("bank.balancesFetched", { n: r.saldi }));
|
||||
laden();
|
||||
} catch (e) {
|
||||
toast(e.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function abrufen(v) {
|
||||
setBusy(true);
|
||||
try {
|
||||
@@ -231,6 +244,31 @@ export default function Bankanbindung() {
|
||||
: t("bank.noAccountForCurrency", { currency: l.currency })}
|
||||
</p>
|
||||
)}
|
||||
{l.balance_cents != null && (
|
||||
<p className="muted small" style={{ margin: "4px 0 0" }}>
|
||||
{t("bank.bankBalance")}:{" "}
|
||||
<strong>{formatMoney(l.balance_cents, l.currency)}</strong>
|
||||
{l.balance_at ? ` · ${formatDate(l.balance_at)}` : ""}
|
||||
{/* Der eigentliche Nutzen: Certain rechnet den Saldo selbst
|
||||
aus. Weicht er ab, fehlt oder doppelt eine Buchung –
|
||||
das sieht man sonst erst beim Vergleich von Hand. */}
|
||||
{l.account_id && kontoById[l.account_id] && (
|
||||
(() => {
|
||||
const diff = kontoById[l.account_id].balance_cents
|
||||
- l.balance_cents;
|
||||
return diff === 0 ? (
|
||||
<span className="saldo-ok"> · {t("bank.balanceMatches")}</span>
|
||||
) : (
|
||||
<span className="saldo-abweichung">
|
||||
{" "}· {t("bank.balanceDiffers", {
|
||||
diff: formatMoney(diff, l.currency),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{Object.keys(l.bank_felder || {}).length > 0 && (
|
||||
<details className="muted small" style={{ marginTop: 4 }}>
|
||||
<summary>{t("bank.rawFields")}</summary>
|
||||
@@ -254,6 +292,9 @@ export default function Bankanbindung() {
|
||||
<button className="btn sm primary" disabled={busy} onClick={() => abrufen(v)}>
|
||||
{t("bank.sync")}
|
||||
</button>
|
||||
<button className="btn sm" disabled={busy} onClick={() => saldenHolen(v)}>
|
||||
{t("bank.fetchBalances")}
|
||||
</button>
|
||||
<button className="btn sm danger" onClick={() => trennen(v)}>
|
||||
<Icon name="trash" size={15} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user