Merge pull request 'Fix n.n.: Buchungen im Kontoblatt lassen sich manuell verknüpfen' (#82) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/82
This commit is contained in:
commit
09fd8af15f
7 changed files with 368 additions and 9 deletions
27
Changes.md
27
Changes.md
|
|
@ -1,5 +1,32 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Umbuchung wird auch von der Gegenkontoseite erkannt (27.09.2026)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- `Bankumsatz::passendeBuchung()` berücksichtigt jetzt auch die Gegenkonto-Seite einer
|
||||||
|
Umbuchung, mit umgekehrtem Vorzeichen wie im Kontoblatt auch.
|
||||||
|
- Test in `tests/16_BankingTest.php`
|
||||||
|
(`umbuchungWirdAuchVonDerGegenkontoseiteErkannt`).
|
||||||
|
|
||||||
|
## Buchungen im Kontoblatt verknüpfen (27.09.2026)
|
||||||
|
|
||||||
|
Liegen zwei Buchungen, die eigentlich derselbe Vorgang sind, zu weit auseinander (oder
|
||||||
|
gehörte eine ursprünglich einem anderen Ursprungskonto), findet der automatische
|
||||||
|
Abgleich sie nicht – die Umbuchung und ihre einzeln erfasste Dublette stehen dann
|
||||||
|
dauerhaft nebeneinander im Kontoblatt.
|
||||||
|
|
||||||
|
- Neu: `Buchung::verknuepfen()` führt eine Umbuchung (eigene Seite oder Gegenseite auf
|
||||||
|
diesem Konto) und eine einzeln erfasste Buchung mit passendem Betrag zusammen – die
|
||||||
|
Umbuchung bleibt, die Dublette fällt weg, ihr Bankumsatz hängt sich an die Umbuchung.
|
||||||
|
- Im Kontoblatt lässt sich dafür auch die Gegenseite einer fremden Umbuchung ankreuzen;
|
||||||
|
der Knopf „Verknüpfen“ neben „Auswahl löschen“ nutzt dieselbe Auswahl.
|
||||||
|
- Test in `tests/44_BuchungenVerknuepfenTest.php`.
|
||||||
|
|
||||||
## Buchungsbericht: Splitbuchung nach Konto/Empfänger zusammengefasst (27.09.2026)
|
## Buchungsbericht: Splitbuchung nach Konto/Empfänger zusammengefasst (27.09.2026)
|
||||||
|
|
||||||
Nach Kategorie oder Klasse gruppiert erscheint eine Splitbuchung weiterhin mit jeder
|
Nach Kategorie oder Klasse gruppiert erscheint eine Splitbuchung weiterhin mit jeder
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ $router->get('/konten/{id}/buchung', fn(array $p) => (new BuchungenController()
|
||||||
$router->post('/konten/{id}/buchung', fn(array $p) => (new BuchungenController())->anlegen($p));
|
$router->post('/konten/{id}/buchung', fn(array $p) => (new BuchungenController())->anlegen($p));
|
||||||
$router->post('/konten/{id}/buchungen/loeschen',
|
$router->post('/konten/{id}/buchungen/loeschen',
|
||||||
fn(array $p) => (new BuchungenController())->auswahlLoeschen($p));
|
fn(array $p) => (new BuchungenController())->auswahlLoeschen($p));
|
||||||
|
$router->post('/konten/{id}/buchungen/verknuepfen',
|
||||||
|
fn(array $p) => (new BuchungenController())->verknuepfen($p));
|
||||||
$router->get('/buchungen/{id}', fn(array $p) => (new BuchungenController())->bearbeiten($p));
|
$router->get('/buchungen/{id}', fn(array $p) => (new BuchungenController())->bearbeiten($p));
|
||||||
$router->post('/buchungen/{id}', fn(array $p) => (new BuchungenController())->speichern($p));
|
$router->post('/buchungen/{id}', fn(array $p) => (new BuchungenController())->speichern($p));
|
||||||
$router->post('/buchungen/{id}/loeschen', fn(array $p) => (new BuchungenController())->loeschen($p));
|
$router->post('/buchungen/{id}/loeschen', fn(array $p) => (new BuchungenController())->loeschen($p));
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,10 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formulare mit data-bestaetigen fragen vor dem Absenden nach.
|
// Formulare mit data-bestaetigen fragen vor dem Absenden nach. Trägt der
|
||||||
|
// gedrückte Knopf selbst ein data-bestaetigen, gilt seine Frage – so
|
||||||
|
// können mehrere Knöpfe im selben Formular (verschiedene Ziele über
|
||||||
|
// formaction) je ihre eigene Rückfrage stellen.
|
||||||
// In der Capture-Phase, damit die Rückfrage auch dann greift, wenn ein
|
// In der Capture-Phase, damit die Rückfrage auch dann greift, wenn ein
|
||||||
// anderer Handler das Ereignis unterwegs stoppt.
|
// anderer Handler das Ereignis unterwegs stoppt.
|
||||||
document.addEventListener('submit', function (ereignis) {
|
document.addEventListener('submit', function (ereignis) {
|
||||||
|
|
@ -38,7 +41,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frage = formular.dataset.bestaetigen;
|
const frage = (ereignis.submitter && ereignis.submitter.dataset.bestaetigen)
|
||||||
|
|| formular.dataset.bestaetigen;
|
||||||
if (frage && !window.confirm(frage)) {
|
if (frage && !window.confirm(frage)) {
|
||||||
ereignis.preventDefault();
|
ereignis.preventDefault();
|
||||||
ereignis.stopPropagation();
|
ereignis.stopPropagation();
|
||||||
|
|
|
||||||
|
|
@ -500,6 +500,41 @@ class BuchungenController extends BaseController
|
||||||
: $anzahl . ' Buchungen gelöscht.');
|
: $anzahl . ' Buchungen gelöscht.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt zwei angehakte Buchungen dieses Kontoblatts zusammen: eine schon
|
||||||
|
* fertige Umbuchung und eine einzeln erfasste Buchung, die denselben
|
||||||
|
* Vorgang doppelt zeigen, weil sie zu weit auseinanderlagen, um sich
|
||||||
|
* automatisch zu finden.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $params
|
||||||
|
*/
|
||||||
|
public function verknuepfen(array $params): void
|
||||||
|
{
|
||||||
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
|
$kontoId = (int)$params['id'];
|
||||||
|
if (Konto::find($kontoId) === null) {
|
||||||
|
$this->nichtGefunden('/konten/' . $params['id']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ziel = $this->kontoblattPfad($kontoId);
|
||||||
|
$ids = $this->postIds('buchungen');
|
||||||
|
if (count($ids) !== 2) {
|
||||||
|
View::redirect($ziel, 'Zum Verknüpfen bitte genau zwei Buchungen ankreuzen.', 'warnung');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Buchung::verknuepfen($kontoId, $ids[0], $ids[1]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
View::redirect($ziel, $e->getMessage(), 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
View::redirect($ziel, 'Buchungen verknüpft.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weg zurück ins Kontoblatt – mit demselben Filter und derselben Seite.
|
* Weg zurück ins Kontoblatt – mit demselben Filter und derselben Seite.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,66 @@ class Buchung
|
||||||
Database::execute('DELETE FROM buchungen WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
|
Database::execute('DELETE FROM buchungen WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt zwei Buchungen zusammen, die auf diesem Konto dieselbe Umbuchung
|
||||||
|
* doppelt zeigen: einmal als schon fertige Umbuchung (eigene Seite oder
|
||||||
|
* Gegenseite), einmal als einzeln erfasste Buchung mit demselben Betrag –
|
||||||
|
* etwa weil die Bank sie erst Tage später gebucht hat und der automatische
|
||||||
|
* Abgleich sie deshalb nicht zusammenführte.
|
||||||
|
*
|
||||||
|
* Die Umbuchung bleibt bestehen, die einzelne Buchung fällt weg; ihr
|
||||||
|
* Bankumsatz hängt sich stattdessen an die Umbuchung, wie es ein
|
||||||
|
* automatischer Abgleich auch täte.
|
||||||
|
*/
|
||||||
|
public static function verknuepfen(int $kontoId, int $idA, int $idB): void
|
||||||
|
{
|
||||||
|
$a = self::find($idA);
|
||||||
|
$b = self::find($idB);
|
||||||
|
if ($a === null || $b === null) {
|
||||||
|
throw new \RuntimeException('Eine der beiden Buchungen gibt es nicht.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$istUmbuchungHier = static fn(array $z): bool => $z['gegenkonto_id'] !== null
|
||||||
|
&& (int)$z['gesplittet'] === 0
|
||||||
|
&& ((int)$z['konto_id'] === $kontoId || (int)$z['gegenkonto_id'] === $kontoId);
|
||||||
|
|
||||||
|
$istEinzelnHier = static fn(array $z): bool => $z['gegenkonto_id'] === null
|
||||||
|
&& (int)$z['gesplittet'] === 0
|
||||||
|
&& (int)$z['konto_id'] === $kontoId;
|
||||||
|
|
||||||
|
[$umbuchung, $einzeln] = match (true) {
|
||||||
|
$istUmbuchungHier($a) && $istEinzelnHier($b) => [$a, $b],
|
||||||
|
$istUmbuchungHier($b) && $istEinzelnHier($a) => [$b, $a],
|
||||||
|
default => throw new \RuntimeException(
|
||||||
|
'Verknüpfen geht nur zwischen einer Umbuchung und einer einzeln erfassten '
|
||||||
|
. 'Buchung auf diesem Konto.'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auf der Gegenkontoseite zählt der Betrag mit umgekehrtem Vorzeichen,
|
||||||
|
// wie im Kontoblatt auch – ein grober Kontrollblick, kein Ersatz für die
|
||||||
|
// eigene Prüfung der Auswahl.
|
||||||
|
$vorzeichen = (int)$umbuchung['konto_id'] === $kontoId ? 1 : -1;
|
||||||
|
$effektiv = $vorzeichen * (float)$umbuchung['betrag'];
|
||||||
|
if (abs($effektiv - (float)$einzeln['betrag']) >= self::CENT) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Die Beträge der beiden Buchungen passen nicht zusammen – bitte die Auswahl prüfen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::transaktion(static function () use ($umbuchung, $einzeln): void {
|
||||||
|
Database::execute(
|
||||||
|
'UPDATE bank_umsaetze SET buchung_id = ? WHERE buchung_id = ? AND benutzer_id = ?',
|
||||||
|
[(int)$umbuchung['id'], (int)$einzeln['id'], Mandant::id()]
|
||||||
|
);
|
||||||
|
Database::execute(
|
||||||
|
'UPDATE buchungen SET abgeglichen = 1 WHERE id = ? AND benutzer_id = ?',
|
||||||
|
[(int)$umbuchung['id'], Mandant::id()]
|
||||||
|
);
|
||||||
|
self::loeschen((int)$einzeln['id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Das Kontoblatt: eigene Buchungen und die Gegenseiten fremder Umbuchungen,
|
* Das Kontoblatt: eigene Buchungen und die Gegenseiten fremder Umbuchungen,
|
||||||
* chronologisch mit laufendem Saldo.
|
* chronologisch mit laufendem Saldo.
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ $seitenPfad = static function (int $nr) use ($filter): string {
|
||||||
// Nach dem Löschen soll dieselbe Ansicht wieder erscheinen – Filter und Seite
|
// Nach dem Löschen soll dieselbe Ansicht wieder erscheinen – Filter und Seite
|
||||||
// hängen deshalb am Ziel des Löschformulars. Dasselbe gilt für die
|
// hängen deshalb am Ziel des Löschformulars. Dasselbe gilt für die
|
||||||
// Schnellzuordnung: Ihre Ziele setzt kontoblatt.js aus dieser Abfrage zusammen.
|
// Schnellzuordnung: Ihre Ziele setzt kontoblatt.js aus dieser Abfrage zusammen.
|
||||||
$abfrage = $seitenPfad($seite);
|
$abfrage = $seitenPfad($seite);
|
||||||
$loeschZiel = '/konten/' . (int)$konto['id'] . '/buchungen/loeschen' . $abfrage;
|
$loeschZiel = '/konten/' . (int)$konto['id'] . '/buchungen/loeschen' . $abfrage;
|
||||||
|
$verknuepfenZiel = '/konten/' . (int)$konto['id'] . '/buchungen/verknuepfen' . $abfrage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Zuordnung einer Zeile als Wert der gemeinsamen Auswahlliste – dieselben
|
* Die Zuordnung einer Zeile als Wert der gemeinsamen Auswahlliste – dieselben
|
||||||
|
|
@ -135,7 +136,19 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
data-frage-eine="Die ausgewählte Buchung wirklich löschen?"
|
data-frage-eine="Die ausgewählte Buchung wirklich löschen?"
|
||||||
data-frage-mehrere="{anzahl} ausgewählte Buchungen wirklich löschen?">
|
data-frage-mehrere="{anzahl} ausgewählte Buchungen wirklich löschen?">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken ?? '') ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken ?? '') ?>">
|
||||||
|
<?php /* Der erste Knopf im Formular bleibt „Auswahl löschen“ –
|
||||||
|
kontoblatt.js/app.js greift sich per querySelector den
|
||||||
|
ersten Absende-Knopf, um ihn nach Anzahl der Auswahl zu
|
||||||
|
beschriften und zu sperren. Verknüpfen nutzt dasselbe
|
||||||
|
Formular und Ankreuzfeld, nur mit anderem Ziel: zwei
|
||||||
|
ausgewählte Buchungen, die denselben Vorgang doppelt
|
||||||
|
zeigen (eine Umbuchung und ihre einzeln erfasste
|
||||||
|
Dublette), führt es zusammen. */ ?>
|
||||||
<button type="submit" class="knopf gefahr">Auswahl löschen</button>
|
<button type="submit" class="knopf gefahr">Auswahl löschen</button>
|
||||||
|
<button type="submit" formaction="<?= View::h($verknuepfenZiel) ?>" class="knopf"
|
||||||
|
data-bestaetigen="Beide ausgewählten Buchungen zu einer Umbuchung verknüpfen? Die einzeln erfasste Buchung wird dabei gelöscht.">
|
||||||
|
Verknüpfen
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<span class="fuellung"></span>
|
<span class="fuellung"></span>
|
||||||
|
|
@ -222,11 +235,19 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<?php foreach ($zeilen as $zeile): ?>
|
<?php foreach ($zeilen as $zeile): ?>
|
||||||
<tr class="<?= $zeile['gegensicht'] ? 'gegensicht' : '' ?>">
|
<tr class="<?= $zeile['gegensicht'] ? 'gegensicht' : '' ?>">
|
||||||
<td class="wahlspalte">
|
<td class="wahlspalte">
|
||||||
<?php if (!$zeile['gegensicht']): ?>
|
<?php
|
||||||
<input type="checkbox" name="buchungen[]" value="<?= (int)$zeile['id'] ?>"
|
// Auch die Gegenseite einer fremden Umbuchung lässt sich
|
||||||
form="buchungsauswahl"
|
// ankreuzen – gelöscht wird sie dadurch nicht (das prüft
|
||||||
aria-label="Buchung vom <?= View::h(View::datum($zeile['datum'])) ?> auswählen">
|
// der Controller anhand ihres wirklichen Kontos), aber
|
||||||
<?php endif; ?>
|
// verknüpfen lässt sie sich mit einer einzeln erfassten
|
||||||
|
// Dublette auf diesem Konto.
|
||||||
|
$auswahlLabel = $zeile['gegensicht']
|
||||||
|
? 'Umbuchung vom ' . View::datum($zeile['datum']) . ' auswählen'
|
||||||
|
: 'Buchung vom ' . View::datum($zeile['datum']) . ' auswählen';
|
||||||
|
?>
|
||||||
|
<input type="checkbox" name="buchungen[]" value="<?= (int)$zeile['id'] ?>"
|
||||||
|
form="buchungsauswahl"
|
||||||
|
aria-label="<?= View::h($auswahlLabel) ?>">
|
||||||
</td>
|
</td>
|
||||||
<td class="nowrap"><?= View::datum($zeile['datum']) ?></td>
|
<td class="nowrap"><?= View::datum($zeile['datum']) ?></td>
|
||||||
<td class="nebentext"><?= View::h($zeile['nummer']) ?></td>
|
<td class="nebentext"><?= View::h($zeile['nummer']) ?></td>
|
||||||
|
|
|
||||||
210
tests/44_BuchungenVerknuepfenTest.php
Normal file
210
tests/44_BuchungenVerknuepfenTest.php
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Finanzmanager\Models\Bankumsatz;
|
||||||
|
use Finanzmanager\Models\Bankzugang;
|
||||||
|
use Finanzmanager\Models\Buchung;
|
||||||
|
use Finanzmanager\Models\Konto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manuelles Verknüpfen zweier Buchungen im Kontoblatt: eine Umbuchung und eine
|
||||||
|
* einzeln erfasste Buchung, die denselben Vorgang doppelt zeigen, weil sie zu
|
||||||
|
* weit auseinanderlagen, um sich beim automatischen Abgleich zu finden (siehe
|
||||||
|
* auch Test 16, „Umbuchung wird auch von der Gegenkontoseite erkannt“ – dort
|
||||||
|
* geht es um den automatischen Fall, hier um den, den nur ein Mensch auflösen
|
||||||
|
* kann).
|
||||||
|
*/
|
||||||
|
class BuchungenVerknuepfenTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 44: Buchungen im Kontoblatt verknüpfen';
|
||||||
|
|
||||||
|
private int $paypal = 0;
|
||||||
|
private int $dkb = 0;
|
||||||
|
private int $zugang = 0;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->mandantAnlegen();
|
||||||
|
$this->stammdaten();
|
||||||
|
|
||||||
|
$this->gegenseiteUndDublette();
|
||||||
|
$this->eigeneUmbuchungUndDublette();
|
||||||
|
$this->reihenfolgeDerAuswahlSpieltKeineRolle();
|
||||||
|
$this->zweiEinzelneLassenSichNichtVerknuepfen();
|
||||||
|
$this->zweiUmbuchungenLassenSichNichtVerknuepfen();
|
||||||
|
$this->unpassenderBetragWirdAbgewiesen();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stammdaten(): void
|
||||||
|
{
|
||||||
|
$this->paypal = Konto::anlegen([
|
||||||
|
'name' => 'Paypal', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
$this->dkb = Konto::anlegen([
|
||||||
|
'name' => 'DKB Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
$this->zugang = Bankzugang::anlegen([
|
||||||
|
'name' => 'Testbank', 'bankname' => 'Testbank AG', 'blz' => '12030000',
|
||||||
|
'fints_url' => 'https://banking.example.org/fints', 'benutzerkennung' => 'testkunde',
|
||||||
|
'pin' => 'geheim1234', 'abrufzeitraum' => 'letzter_abruf', 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $daten */
|
||||||
|
private function umsatzAnlegen(array $daten): int
|
||||||
|
{
|
||||||
|
return (int)Bankumsatz::anlegen(array_merge([
|
||||||
|
'zugang_id' => $this->zugang,
|
||||||
|
'extern_id' => 'verknuepfen-' . bin2hex(random_bytes(4)),
|
||||||
|
'wertstellung' => $daten['buchungstag'],
|
||||||
|
'empfaenger' => '',
|
||||||
|
'verwendungszweck' => '',
|
||||||
|
'buchungsart' => 'GUTSCHRIFT',
|
||||||
|
], $daten));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Fall aus der Praxis: Die Umbuchung wurde von Paypal aus gebucht (ihr
|
||||||
|
* Ursprungskonto), im DKB-Kontoblatt steht sie nur als Gegenseite. Die
|
||||||
|
* echte Bankbuchung kommt einen Tag später herein und wird – weil der
|
||||||
|
* automatische Abgleich sie nicht fand – als eigene Buchung übernommen.
|
||||||
|
*/
|
||||||
|
private function gegenseiteUndDublette(): void
|
||||||
|
{
|
||||||
|
$umbuchungId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->paypal, 'datum' => '2026-09-23', 'betrag' => 26.00,
|
||||||
|
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->dkb,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dubletteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-24', 'betrag' => -26.00,
|
||||||
|
'empfaenger' => 'PayPal Europe',
|
||||||
|
]);
|
||||||
|
$umsatzId = $this->umsatzAnlegen([
|
||||||
|
'konto_id' => $this->dkb, 'buchungstag' => '2026-09-24', 'betrag' => -26.00,
|
||||||
|
]);
|
||||||
|
Bankumsatz::verbinden($umsatzId, $dubletteId);
|
||||||
|
|
||||||
|
Buchung::verknuepfen($this->dkb, $umbuchungId, $dubletteId);
|
||||||
|
|
||||||
|
$this->assertNull(Buchung::find($dubletteId), 'Die einzeln erfasste Dublette ist weg');
|
||||||
|
$this->assertNotEmpty(Buchung::find($umbuchungId), 'Die Umbuchung bleibt bestehen');
|
||||||
|
$this->assertEquals(
|
||||||
|
1,
|
||||||
|
(int)Buchung::find($umbuchungId)['abgeglichen'],
|
||||||
|
'Die Umbuchung gilt jetzt als abgeglichen'
|
||||||
|
);
|
||||||
|
|
||||||
|
$umsatz = Bankumsatz::find($umsatzId);
|
||||||
|
$this->assertEquals($umbuchungId, (int)$umsatz['buchung_id'], 'Ihr Bankumsatz zeigt jetzt auf die Umbuchung');
|
||||||
|
$this->assertEquals('uebernommen', $umsatz['status'], 'Und gilt weiter als erledigt');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gespiegelter Fall: Die Umbuchung wurde direkt im DKB-Konto gebucht (ihr
|
||||||
|
* Ursprungskonto liegt hier), die Dublette ebenfalls hier.
|
||||||
|
*/
|
||||||
|
private function eigeneUmbuchungUndDublette(): void
|
||||||
|
{
|
||||||
|
$umbuchungId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-23', 'betrag' => -75.20,
|
||||||
|
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->paypal,
|
||||||
|
]);
|
||||||
|
$dubletteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-26', 'betrag' => -75.20,
|
||||||
|
'empfaenger' => 'Reservix',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Buchung::verknuepfen($this->dkb, $umbuchungId, $dubletteId);
|
||||||
|
|
||||||
|
$this->assertNull(Buchung::find($dubletteId), 'Die Dublette ist weg');
|
||||||
|
$this->assertEquals(
|
||||||
|
$this->paypal,
|
||||||
|
(int)Buchung::find($umbuchungId)['gegenkonto_id'],
|
||||||
|
'Die Umbuchung bleibt, was sie war'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reihenfolgeDerAuswahlSpieltKeineRolle(): void
|
||||||
|
{
|
||||||
|
$umbuchungId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->paypal, 'datum' => '2026-09-23', 'betrag' => 12.00,
|
||||||
|
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->dkb,
|
||||||
|
]);
|
||||||
|
$dubletteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-24', 'betrag' => -12.00,
|
||||||
|
'empfaenger' => 'Dublette',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zuerst die Dublette, dann die Umbuchung ausgewählt.
|
||||||
|
Buchung::verknuepfen($this->dkb, $dubletteId, $umbuchungId);
|
||||||
|
|
||||||
|
$this->assertNull(Buchung::find($dubletteId), 'Auch andersherum verschwindet die Dublette');
|
||||||
|
$this->assertNotEmpty(Buchung::find($umbuchungId), 'Und die Umbuchung bleibt');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function zweiEinzelneLassenSichNichtVerknuepfen(): void
|
||||||
|
{
|
||||||
|
$ersteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-25', 'betrag' => -10.00, 'empfaenger' => 'Erste',
|
||||||
|
]);
|
||||||
|
$zweiteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-25', 'betrag' => -10.00, 'empfaenger' => 'Zweite',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
$this->wirftFehler(fn() => Buchung::verknuepfen($this->dkb, $ersteId, $zweiteId)),
|
||||||
|
'Zwei einzeln erfasste Buchungen lassen sich nicht verknüpfen'
|
||||||
|
);
|
||||||
|
$this->assertNotEmpty(Buchung::find($ersteId), 'Keine der beiden wurde gelöscht');
|
||||||
|
$this->assertNotEmpty(Buchung::find($zweiteId), 'Auch die zweite nicht');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function zweiUmbuchungenLassenSichNichtVerknuepfen(): void
|
||||||
|
{
|
||||||
|
$ersteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->paypal, 'datum' => '2026-09-25', 'betrag' => 5.00,
|
||||||
|
'empfaenger' => 'Umbuchung 1', 'gegenkonto_id' => $this->dkb,
|
||||||
|
]);
|
||||||
|
$zweiteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-25', 'betrag' => -7.00,
|
||||||
|
'empfaenger' => 'Umbuchung 2', 'gegenkonto_id' => $this->paypal,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
$this->wirftFehler(fn() => Buchung::verknuepfen($this->dkb, $ersteId, $zweiteId)),
|
||||||
|
'Zwei Umbuchungen lassen sich nicht verknüpfen'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function unpassenderBetragWirdAbgewiesen(): void
|
||||||
|
{
|
||||||
|
$umbuchungId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->paypal, 'datum' => '2026-09-23', 'betrag' => 26.00,
|
||||||
|
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->dkb,
|
||||||
|
]);
|
||||||
|
$dubletteId = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->dkb, 'datum' => '2026-09-24', 'betrag' => -19.99,
|
||||||
|
'empfaenger' => 'Anderer Betrag',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
$this->wirftFehler(fn() => Buchung::verknuepfen($this->dkb, $umbuchungId, $dubletteId)),
|
||||||
|
'Unterschiedliche Beträge werden abgewiesen'
|
||||||
|
);
|
||||||
|
$this->assertNotEmpty(Buchung::find($dubletteId), 'Die Dublette bleibt unangetastet');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hilfsmittel: Wirft der Aufruf eine Ausnahme? */
|
||||||
|
private function wirftFehler(callable $aufruf): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$aufruf();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue