Fix n.n.: Umbuchung wird auch von der Gegenkontoseite erkannt

Eine Umbuchung gehört ihrem Ursprungskonto; der automatische Abgleich
suchte eine passende Buchung bisher nur über deren konto_id. Kam der
Kontoauszug der Empfängerseite mit ein paar Tagen Verzug herein, fand
er die schon gebuchte Umbuchung nie und legte sie als eigenständige
Buchung an – die Umbuchung stand danach doppelt im Kontoblatt und
verfälschte den Saldo.

Die Suche berücksichtigt jetzt auch die Gegenkonto-Seite einer
Umbuchung, mit umgekehrtem Vorzeichen wie im Kontoblatt auch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-09-27 14:48:08 +02:00
parent c2a6a9b899
commit ecd01cde29
2 changed files with 67 additions and 4 deletions

View file

@ -200,6 +200,12 @@ class Bankumsatz
* Konto, gleicher Betrag, Datum nah beieinander, noch mit keinem anderen
* Umsatz verbunden.
*
* Eine Umbuchung ist dabei auch dann „dasselbe Konto“, wenn sie auf ihrem
* Ursprungskonto steht und dieses Konto nur ihr Gegenkonto ist – sonst fände
* der Kontoauszug der Empfängerseite seine eigene Umbuchung nie, weil die
* Buchung formal einem anderen Konto gehört. Auf der Gegenkonto-Seite zählt
* der Betrag mit umgekehrtem Vorzeichen, wie im Kontoblatt auch.
*
* Die Toleranz in Tagen steht in den Einstellungen (`banking_abgleich_tage`):
* Zwischen dem Tag, an dem man eine Zahlung erfasst, und dem Tag, an dem die
* Bank sie bucht, liegen erfahrungsgemäß ein paar Tage.
@ -214,13 +220,16 @@ class Bankumsatz
return null;
}
$tage = max(0, Einstellung::getInt('banking_abgleich_tage', 5));
$tage = max(0, Einstellung::getInt('banking_abgleich_tage', 5));
$betrag = round((float)$umsatz['betrag'], 2);
$zeile = Database::fetchOne(
'SELECT b.* FROM buchungen b
WHERE b.benutzer_id = ?
AND b.konto_id = ?
AND ABS(b.betrag - ?) < 0.005
AND (
(b.konto_id = ? AND ABS(b.betrag - ?) < 0.005)
OR (b.gegenkonto_id = ? AND b.gesplittet = 0 AND ABS(b.betrag + ?) < 0.005)
)
AND ABS(DATEDIFF(b.datum, ?)) <= ?
AND NOT EXISTS (
SELECT 1 FROM bank_umsaetze u
@ -230,7 +239,9 @@ class Bankumsatz
[
Mandant::id(),
$kontoId,
round((float)$umsatz['betrag'], 2),
$betrag,
$kontoId,
$betrag,
(string)$umsatz['buchungstag'],
$tage,
(int)($umsatz['id'] ?? 0),

View file

@ -47,6 +47,7 @@ class BankingTest extends TestCase
$this->splitsHaltenDasVerhaeltnis();
$this->ungueltigesMusterWirdAbgewiesen();
$this->abgleichFindetVorhandeneBuchung();
$this->umbuchungWirdAuchVonDerGegenkontoseiteErkannt();
$this->uebernehmenErzeugtEineBuchung();
$this->regelmitSplitsErzeugtEineSplitbuchung();
$this->buchungOhneKategorieWirdGefunden();
@ -680,6 +681,57 @@ class BankingTest extends TestCase
);
}
/**
* Eine Umbuchung gehört ihrem Ursprungskonto – hier dem Giro. Der
* Kontoauszug des Gegenkontos (Sparbuch) muss sie trotzdem finden, sonst
* bucht der automatische Abgleich sie dort ein zweites Mal.
*/
private function umbuchungWirdAuchVonDerGegenkontoseiteErkannt(): void
{
Einstellung::set('banking_abgleich_tage', '5', 'banking');
$umbuchungId = Buchung::anlegen([
'konto_id' => $this->giro, 'datum' => '2026-08-20', 'betrag' => -40.00,
'empfaenger' => 'Umbuchung Sparbuch', 'gegenkonto_id' => $this->sparkonto,
]);
// Die Bank bucht das Sparbuch erst einen Tag später – auf der
// Gegenkonto-Seite zählt der Betrag mit umgekehrtem Vorzeichen.
$umsatzId = Bankumsatz::anlegen([
'zugang_id' => $this->zugang,
'konto_id' => $this->sparkonto,
'extern_id' => 'test-gegenkonto-1',
'buchungstag' => '2026-08-21',
'wertstellung' => '2026-08-21',
'betrag' => 40.00,
'empfaenger' => 'Giro',
'buchungsart' => 'GUTSCHRIFT',
]);
$umsatz = Bankumsatz::find((int)$umsatzId);
$treffer = Bankumsatz::passendeBuchung($umsatz);
$this->assertNotEmpty($treffer, 'Die Umbuchung wird auch von der Gegenkontoseite gefunden');
$this->assertEquals($umbuchungId, (int)$treffer['id'], 'Und zwar genau sie');
$vorschlag = Bankumsatz::vorschlag($umsatz);
$this->assertEquals('verbinden', $vorschlag['art'], 'Der Vorschlag lautet: verbinden');
Bankumsatz::verbinden((int)$umsatzId, $umbuchungId);
$this->assertEquals(
1,
(int)Buchung::find($umbuchungId)['abgeglichen'],
'Die Umbuchung gilt jetzt als abgeglichen'
);
// Ein anderer Betrag auf der Gegenkontoseite passt nicht.
$this->assertNull(
Bankumsatz::passendeBuchung([
'konto_id' => $this->sparkonto, 'buchungstag' => '2026-08-21', 'betrag' => 12.34, 'id' => 0,
]),
'Ein anderer Betrag ist keine Umbuchung von drüben'
);
}
private function uebernehmenErzeugtEineBuchung(): void
{
$offen = Bankumsatz::liste(['status' => 'offen']);