Merge pull request 'Fix #155: AfA-Buchungen laufen jetzt über Booking::createAfa()/reverseAfa()' (#178) from fds2610-bot/Steuerkram:main into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/178
This commit is contained in:
commit
e786cd02e1
6 changed files with 344 additions and 78 deletions
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-08-03 – Fix: AfA-Buchungen umgehen das Booking-Modell (#155)
|
||||
|
||||
- `AssetController::bookAfa()` und `reverseAfaBooking()` setzten ihre `INSERT INTO bookings`-Statements bislang direkt ab statt über `Booking::create()`/`reverse()`. Vier Folgen: Die Sperre eines abgeschlossenen Geschäftsjahres griff nicht, ein zweiter, unabhängiger Nummernkreis (`AFA-NNNNN`) lief neben `BU-YYYY-NNNNNN` auf derselben Spalte, die Nummernvergabe stand außerhalb der Transaktion, und das Storno einer AfA-Buchung wurde immer dem *aktuellen* Kalenderjahr statt dem Jahr der Originalbuchung zugeordnet.
|
||||
- **`src/Models/Booking.php`:** Neue Methoden `createAfa()` und `reverseAfa()` bündeln die komplette Schreiblogik für AfA-Buchungen – inklusive Sperrprüfung des Geschäftsjahres, Nummernvergabe (`generateAfaBuchungsNr()`, eigener `AFA-NNNNN`-Kreis, bewusst beibehalten statt vereinheitlicht) innerhalb der Transaktion, und – beim Storno – Übernahme der `fiscal_year_id` der Originalbuchung. Das Buchungsdatum des Storno bleibt das tatsächliche Stornodatum (Ist-Versteuerung).
|
||||
- **`src/Controllers/AssetController.php`:** `bookAfa()` und `reverseAfaBooking()` rufen nur noch die Modellmethoden auf, die bisherigen Vorprüfungen bleiben erhalten. `RuntimeException` aus dem Modell wird als HTTP 409 mit `{"error": "..."}` beantwortet. Kein direkter `Database`-Zugriff auf `bookings` mehr im Controller.
|
||||
- **`migrations/092_afa_storno_fiscal_year_korrektur.sql`** (neu): Korrigiert die `fiscal_year_id` bereits vorhandener AfA-Stornobuchungen rückwirkend auf das Jahr ihrer Originalbuchung.
|
||||
- **`tests/82_AfaBuchungModellTest.php`** (neu): Sperre verhindert AfA-Buchung und -Storno, Buchung im offenen Jahr landet korrekt (Jahr, 31.12.-Datum, `betrag_ust = 0`), Storno übernimmt die `fiscal_year_id` der Originalbuchung, `reversed_by`/`is_reversal` werden gesetzt, Buchungsnummer folgt dem Schema `AFA-NNNNN`.
|
||||
|
||||
## 2026-08-02 – Testsuite als echtes Gate in update.sh (#169)
|
||||
|
||||
- Schritt 9/9 von `scripts/update.sh` gab bei rotem Testergebnis nur eine Warnung aus – Exit-Code 0, kein Abbruch. Ein Update mit fehlgeschlagenen Tests galt damit als erfolgreich, obwohl die neue Version zu diesem Zeitpunkt bereits live war.
|
||||
|
|
|
|||
|
|
@ -100,3 +100,11 @@ Die Tabelle besitzt ein Flag `automatisch_berechnet`:
|
|||
3. Im Dialog: Toggle „Manuell bearbeiten" aktiviert editierbare Zellen.
|
||||
4. **„Neu berechnen"** überschreibt manuelle Änderungen (nach Bestätigung).
|
||||
5. **„Speichern"** sichert die aktuelle Tabelle (inklusive manueller Änderungen) in der Datenbank.
|
||||
|
||||
### AfA-Buchung
|
||||
|
||||
Für eine Zeile der Abschreibungstabelle lässt sich per Klick eine AfA-Buchung anlegen (`Booking::createAfa()`). Sie bucht den Abschreibungsbetrag zum 31.12. des jeweiligen Jahres vom hinterlegten Anlagekonto auf ein gewähltes Ausgabenkonto, ohne Umsatzsteuer.
|
||||
|
||||
- **Sperre des Geschäftsjahres:** Ist das Geschäftsjahr des AfA-Jahres bereits gesperrt, wird die Buchung mit einer Fehlermeldung abgelehnt (HTTP 409) – wie bei jeder anderen Buchung auch.
|
||||
- **Nummernkreis:** AfA-Buchungen erhalten eine eigene, global fortlaufende Nummer im Format `AFA-NNNNN`, getrennt vom regulären Nummernkreis `BU-YYYY-NNNNNN`. Erkennbar sind sie zusätzlich am `buchungstyp = 'AfA'`.
|
||||
- **Storno:** Beim Stornieren (`Booking::reverseAfa()`) erhält die Stornobuchung die `fiscal_year_id` der Originalbuchung, nicht die des aktuellen Kalenderjahres – eine 2026 stornierte AfA-Buchung für 2024 verändert die Jahressumme 2024, nicht 2026. Das Buchungsdatum des Storno bleibt bewusst das tatsächliche Stornodatum (Ist-Versteuerung). Ist das Geschäftsjahr der Originalbuchung gesperrt, wird auch das Storno abgelehnt.
|
||||
|
|
|
|||
11
migrations/092_afa_storno_fiscal_year_korrektur.sql
Normal file
11
migrations/092_afa_storno_fiscal_year_korrektur.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-- Migration 092: Bestandskorrektur – AfA-Stornobuchungen erhalten die
|
||||
-- fiscal_year_id ihrer Originalbuchung statt des Kalenderjahres der Stornierung.
|
||||
-- (Fix #155: reverseAfaBooking() verwendete bislang date('Y') statt afa_jahr/
|
||||
-- fiscal_year_id der stornierten Buchung.)
|
||||
|
||||
UPDATE bookings storno
|
||||
JOIN bookings orig ON orig.reversed_by = storno.id
|
||||
SET storno.fiscal_year_id = orig.fiscal_year_id
|
||||
WHERE storno.buchungstyp = 'AfA'
|
||||
AND storno.is_reversal = 1
|
||||
AND storno.fiscal_year_id <> orig.fiscal_year_id;
|
||||
|
|
@ -9,7 +9,6 @@ use Steuerkram\Models\Account;
|
|||
use Steuerkram\Models\Asset;
|
||||
use Steuerkram\Models\AssetDepreciation;
|
||||
use Steuerkram\Models\Booking;
|
||||
use Steuerkram\Models\FiscalYear;
|
||||
use Steuerkram\Models\Setting;
|
||||
use Steuerkram\View;
|
||||
|
||||
|
|
@ -329,43 +328,26 @@ class AssetController extends BaseController
|
|||
|
||||
$jahr = (int)$scheduleRow['jahr'];
|
||||
$betrag = round((float)$scheduleRow['abschreibungsbetrag'], 2);
|
||||
$datum = $jahr . '-12-31';
|
||||
$text = 'AfA ' . $asset['bezeichnung'] . ' ' . $jahr;
|
||||
|
||||
$fy = FiscalYear::findOrCreateByYear($jahr);
|
||||
try {
|
||||
$bookingId = Booking::createAfa([
|
||||
'asset_id' => $assetId,
|
||||
'afa_jahr' => $jahr,
|
||||
'kategorie_konto_id' => $ausgabekontoId,
|
||||
'zahlungskonto_id' => (int)$asset['account_id'],
|
||||
'betrag' => $betrag,
|
||||
'buchungstext' => $text,
|
||||
'schedule_row_id' => $rowId,
|
||||
]);
|
||||
} catch (\RuntimeException $e) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$buchungsNr = \Steuerkram\Database::fetchOne(
|
||||
"SELECT CONCAT('AFA-', LPAD(COALESCE(MAX(CAST(SUBSTRING(buchungs_nr,5) AS UNSIGNED)),0)+1,5,'0')) AS nr
|
||||
FROM bookings WHERE buchungs_nr LIKE 'AFA-%'"
|
||||
)['nr'] ?? ('AFA-' . str_pad('1', 5, '0', STR_PAD_LEFT));
|
||||
|
||||
\Steuerkram\Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, ?)',
|
||||
[
|
||||
$buchungsNr,
|
||||
$assetId,
|
||||
$jahr,
|
||||
$datum,
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
$ausgabekontoId,
|
||||
(int)$asset['account_id'],
|
||||
$betrag,
|
||||
'normal',
|
||||
$betrag,
|
||||
$text,
|
||||
(int)$fy['id'],
|
||||
]
|
||||
);
|
||||
|
||||
$bookingId = (int)\Steuerkram\Database::lastInsertId();
|
||||
AssetDepreciation::setBookingId($rowId, $bookingId);
|
||||
|
||||
echo json_encode(['ok' => true, 'buchungs_nr' => $buchungsNr]);
|
||||
$buchung = Booking::find($bookingId);
|
||||
echo json_encode(['ok' => true, 'buchungs_nr' => $buchung['buchungs_nr']]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -411,51 +393,16 @@ class AssetController extends BaseController
|
|||
return;
|
||||
}
|
||||
|
||||
$fy = FiscalYear::findOrCreateByYear((int)date('Y'));
|
||||
$nr = \Steuerkram\Database::fetchOne(
|
||||
"SELECT CONCAT('AFA-', LPAD(COALESCE(MAX(CAST(SUBSTRING(buchungs_nr,5) AS UNSIGNED)),0)+1,5,'0')) AS nr
|
||||
FROM bookings WHERE buchungs_nr LIKE 'AFA-%'"
|
||||
)['nr'] ?? 'AFA-00001';
|
||||
|
||||
\Steuerkram\Database::beginTransaction();
|
||||
try {
|
||||
\Steuerkram\Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, is_reversal, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, 1, ?)',
|
||||
[
|
||||
$nr,
|
||||
$orig['asset_id'],
|
||||
$orig['afa_jahr'],
|
||||
date('Y-m-d'),
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
$orig['kategorie_konto_id'],
|
||||
$orig['zahlungskonto_id'],
|
||||
-1 * (float)$orig['betrag_netto'],
|
||||
'normal',
|
||||
-1 * (float)$orig['betrag_brutto'],
|
||||
'STORNO zu ' . $orig['buchungs_nr'],
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
$reversalId = (int)\Steuerkram\Database::lastInsertId();
|
||||
\Steuerkram\Database::execute(
|
||||
'UPDATE bookings SET reversed_by = ? WHERE id = ?',
|
||||
[$reversalId, $bookingId]
|
||||
);
|
||||
AssetDepreciation::setBookingId($rowId, 0);
|
||||
\Steuerkram\Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
\Steuerkram\Database::rollBack();
|
||||
http_response_code(500);
|
||||
$reversalId = Booking::reverseAfa($bookingId, $rowId);
|
||||
} catch (\RuntimeException $e) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'storno_nr' => $nr]);
|
||||
$storno = Booking::find($reversalId);
|
||||
echo json_encode(['ok' => true, 'storno_nr' => $storno['buchungs_nr']]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -335,6 +335,124 @@ class Booking
|
|||
return $reversalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine AfA-Buchung im eigenen AFA-Nummernkreis an. Referenz für Sperrprüfung,
|
||||
* Nummernvergabe innerhalb der Transaktion und Schreibumfang: self::create().
|
||||
*/
|
||||
public static function createAfa(array $data): int
|
||||
{
|
||||
$jahr = (int)$data['afa_jahr'];
|
||||
$fy = FiscalYear::findOrCreateByYear($jahr);
|
||||
|
||||
if ($fy['locked']) {
|
||||
throw new \RuntimeException("Geschäftsjahr {$jahr} ist gesperrt. Neue Buchungen nicht möglich.");
|
||||
}
|
||||
|
||||
$betrag = round((float)$data['betrag'], 2);
|
||||
$datum = $jahr . '-12-31';
|
||||
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
$nr = self::generateAfaBuchungsNr();
|
||||
Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, ?)',
|
||||
[
|
||||
$nr,
|
||||
(int)$data['asset_id'],
|
||||
$jahr,
|
||||
$datum,
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
(int)$data['kategorie_konto_id'],
|
||||
(int)$data['zahlungskonto_id'],
|
||||
$betrag,
|
||||
'normal',
|
||||
$betrag,
|
||||
$data['buchungstext'],
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
|
||||
$id = (int)Database::lastInsertId();
|
||||
|
||||
if (!empty($data['schedule_row_id'])) {
|
||||
AssetDepreciation::setBookingId((int)$data['schedule_row_id'], $id);
|
||||
}
|
||||
|
||||
Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Database::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storniert eine AfA-Buchung im eigenen AFA-Nummernkreis. Das Storno erhält die
|
||||
* fiscal_year_id der Originalbuchung (nicht das laufende Kalenderjahr) – wer 2026
|
||||
* eine AfA-Buchung für 2024 storniert, darf die Jahressumme 2026 nicht verfälschen.
|
||||
* Das Buchungsdatum bleibt bewusst das tatsächliche Stornodatum (Ist-Versteuerung).
|
||||
*/
|
||||
public static function reverseAfa(int $bookingId, ?int $scheduleRowId = null): int
|
||||
{
|
||||
$orig = self::find($bookingId);
|
||||
if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.');
|
||||
if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.');
|
||||
|
||||
$fy = FiscalYear::find((int)$orig['fiscal_year_id']);
|
||||
if (!$fy) throw new \RuntimeException('Geschäftsjahr der Originalbuchung nicht gefunden.');
|
||||
if ($fy['locked']) {
|
||||
throw new \RuntimeException("Geschäftsjahr {$fy['year']} ist gesperrt. Neue Buchungen nicht möglich.");
|
||||
}
|
||||
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
$nr = self::generateAfaBuchungsNr();
|
||||
Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, is_reversal, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, 1, ?)',
|
||||
[
|
||||
$nr,
|
||||
$orig['asset_id'],
|
||||
$orig['afa_jahr'],
|
||||
date('Y-m-d'),
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
$orig['kategorie_konto_id'],
|
||||
$orig['zahlungskonto_id'],
|
||||
-1 * (float)$orig['betrag_netto'],
|
||||
'normal',
|
||||
-1 * (float)$orig['betrag_brutto'],
|
||||
'STORNO zu ' . $orig['buchungs_nr'],
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
|
||||
$reversalId = (int)Database::lastInsertId();
|
||||
|
||||
Database::execute('UPDATE bookings SET reversed_by=? WHERE id=?', [$reversalId, $bookingId]);
|
||||
|
||||
if ($scheduleRowId !== null) {
|
||||
AssetDepreciation::setBookingId($scheduleRowId, 0);
|
||||
}
|
||||
|
||||
Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Database::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $reversalId;
|
||||
}
|
||||
|
||||
public static function upsertEigenanteil30(
|
||||
int $belegId,
|
||||
float $betragNetto,
|
||||
|
|
@ -437,6 +555,17 @@ class Booking
|
|||
return sprintf('BU-%s-%06d', $year, $next);
|
||||
}
|
||||
|
||||
/** Eigener, global fortlaufender Nummernkreis für AfA-Buchungen (AFA-NNNNN). */
|
||||
private static function generateAfaBuchungsNr(): string
|
||||
{
|
||||
$result = Database::fetchOne(
|
||||
"SELECT MAX(CAST(SUBSTRING(buchungs_nr,5) AS UNSIGNED)) as maxnr
|
||||
FROM bookings WHERE buchungs_nr LIKE 'AFA-%'"
|
||||
);
|
||||
$next = (int)($result['maxnr'] ?? 0) + 1;
|
||||
return sprintf('AFA-%05d', $next);
|
||||
}
|
||||
|
||||
/** Summiert betrag_brutto der aktiven Einnahme-Hauptbuchungen einer bereits geladenen Buchungsliste. */
|
||||
public static function sumZahlungen(array $bookings): float
|
||||
{
|
||||
|
|
|
|||
163
tests/82_AfaBuchungModellTest.php
Normal file
163
tests/82_AfaBuchungModellTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\Asset;
|
||||
use Steuerkram\Models\Booking;
|
||||
use Steuerkram\Models\FiscalYear;
|
||||
|
||||
/**
|
||||
* Test 82: AfA-Buchungen laufen über Booking::createAfa()/reverseAfa().
|
||||
*
|
||||
* Fix #155: AssetController setzte INSERT INTO bookings bislang direkt ab und
|
||||
* umging damit die Sperre des Geschäftsjahres, den Transaktionsschutz der
|
||||
* Nummernvergabe und – beim Storno – die richtige Jahreszuordnung.
|
||||
*/
|
||||
class AfaBuchungModellTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 82: AfA-Buchungen über Booking-Modell';
|
||||
|
||||
private int $assetId;
|
||||
private int $kategorieId;
|
||||
private int $zahlungsId;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->kategorieId = (int)$this->findAccountId('4855');
|
||||
$this->zahlungsId = (int)$this->findAccountId('0420');
|
||||
|
||||
$this->assetId = Asset::create([
|
||||
'bezeichnung' => 'Testgerät AfA-Modell',
|
||||
'anschaffungsdatum' => '2024-01-15',
|
||||
'anschaffungskosten' => 1200.00,
|
||||
'zugaenge' => 0,
|
||||
'abgaenge' => 0,
|
||||
'abschreibung_jahr' => 400.00,
|
||||
'nutzungsdauer' => 3,
|
||||
'account_id' => $this->zahlungsId,
|
||||
]);
|
||||
|
||||
$this->gesperrtesJahrLehntAfaBuchungAb();
|
||||
$this->afaBuchungInOffenemJahr();
|
||||
$this->stornoUebernimmtJahrDerOriginalbuchung();
|
||||
$this->gesperrtesJahrLehntStornoAb();
|
||||
}
|
||||
|
||||
private function gesperrtesJahrLehntAfaBuchungAb(): void
|
||||
{
|
||||
$jahr = 2019;
|
||||
$fy = FiscalYear::findOrCreateByYear($jahr);
|
||||
\Steuerkram\Database::execute('UPDATE fiscal_years SET locked = 1 WHERE id = ?', [$fy['id']]);
|
||||
|
||||
$vorher = $this->countBookings();
|
||||
$exception = null;
|
||||
try {
|
||||
Booking::createAfa($this->afaData($jahr));
|
||||
} catch (\RuntimeException $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->assert($exception !== null, 'AfA-Buchung in gesperrtem Geschäftsjahr wirft eine RuntimeException');
|
||||
$this->assertEquals(
|
||||
$vorher,
|
||||
$this->countBookings(),
|
||||
'Bei Sperre wird kein Datensatz in bookings angelegt'
|
||||
);
|
||||
}
|
||||
|
||||
private function afaBuchungInOffenemJahr(): void
|
||||
{
|
||||
$jahr = 2024;
|
||||
$bookingId = Booking::createAfa($this->afaData($jahr));
|
||||
$this->assertGreaterThan(0, $bookingId, 'AfA-Buchung in offenem Jahr wird angelegt');
|
||||
|
||||
$booking = Booking::find($bookingId);
|
||||
$this->assertEquals($jahr, (int)$booking['afa_jahr'], 'afa_jahr ist gesetzt');
|
||||
$this->assertEquals($jahr . '-12-31', $booking['buchungs_datum'], 'buchungs_datum liegt auf dem 31.12. des AfA-Jahres');
|
||||
$this->assertEquals(0.00, (float)$booking['betrag_ust'], 'betrag_ust ist 0');
|
||||
$this->assertEquals(400.00, (float)$booking['betrag_netto'], 'betrag_netto entspricht dem AfA-Betrag');
|
||||
$this->assert(
|
||||
(bool)preg_match('/^AFA-\d{5}$/', $booking['buchungs_nr']),
|
||||
'Buchungsnummer folgt dem Schema AFA-NNNNN (erhalten: ' . $booking['buchungs_nr'] . ')'
|
||||
);
|
||||
}
|
||||
|
||||
private function stornoUebernimmtJahrDerOriginalbuchung(): void
|
||||
{
|
||||
// Vorjahr: 2022 – das laufende Kalenderjahr ist ein anderes.
|
||||
$jahr = 2022;
|
||||
$bookingId = Booking::createAfa($this->afaData($jahr));
|
||||
$original = Booking::find($bookingId);
|
||||
|
||||
$reversalId = Booking::reverseAfa($bookingId);
|
||||
$reversal = Booking::find($reversalId);
|
||||
|
||||
$this->assertEquals(
|
||||
$original['fiscal_year_id'],
|
||||
$reversal['fiscal_year_id'],
|
||||
'Storno erhält die fiscal_year_id der Originalbuchung, nicht die des aktuellen Jahres'
|
||||
);
|
||||
$aktuellesJahr = FiscalYear::findOrCreateByYear((int)date('Y'));
|
||||
$this->assert(
|
||||
(int)$reversal['fiscal_year_id'] !== (int)$aktuellesJahr['id'],
|
||||
'fiscal_year_id des Storno ist nicht die des laufenden Kalenderjahres'
|
||||
);
|
||||
$this->assertEquals(1, (int)$reversal['is_reversal'], 'Stornobuchung ist als is_reversal=1 markiert');
|
||||
|
||||
$updatedOriginal = Booking::find($bookingId);
|
||||
$this->assertEquals(
|
||||
$reversalId,
|
||||
(int)$updatedOriginal['reversed_by'],
|
||||
'reversed_by der Originalbuchung zeigt auf die Stornobuchung'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match('/^AFA-\d{5}$/', $reversal['buchungs_nr']),
|
||||
'Storno-Buchungsnummer folgt ebenfalls dem Schema AFA-NNNNN'
|
||||
);
|
||||
}
|
||||
|
||||
private function gesperrtesJahrLehntStornoAb(): void
|
||||
{
|
||||
$jahr = 2020;
|
||||
$bookingId = Booking::createAfa($this->afaData($jahr));
|
||||
|
||||
$fy = FiscalYear::findByYear($jahr);
|
||||
\Steuerkram\Database::execute('UPDATE fiscal_years SET locked = 1 WHERE id = ?', [$fy['id']]);
|
||||
|
||||
$vorher = $this->countBookings();
|
||||
$exception = null;
|
||||
try {
|
||||
Booking::reverseAfa($bookingId);
|
||||
} catch (\RuntimeException $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->assert($exception !== null, 'Storno einer AfA-Buchung im gesperrten Jahr wirft eine RuntimeException');
|
||||
$this->assertEquals(
|
||||
$vorher,
|
||||
$this->countBookings(),
|
||||
'Bei Sperre wird kein zusätzlicher Storno-Datensatz angelegt'
|
||||
);
|
||||
|
||||
$unverändert = Booking::find($bookingId);
|
||||
$this->assert(empty($unverändert['reversed_by']), 'Originalbuchung bleibt ohne reversed_by, wenn das Storno abgelehnt wird');
|
||||
}
|
||||
|
||||
private function afaData(int $jahr): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $this->assetId,
|
||||
'afa_jahr' => $jahr,
|
||||
'kategorie_konto_id' => $this->kategorieId,
|
||||
'zahlungskonto_id' => $this->zahlungsId,
|
||||
'betrag' => 400.00,
|
||||
'buchungstext' => 'AfA Test ' . $jahr,
|
||||
];
|
||||
}
|
||||
|
||||
private function countBookings(): int
|
||||
{
|
||||
$row = \Steuerkram\Database::fetchOne('SELECT COUNT(*) as cnt FROM bookings');
|
||||
return (int)($row['cnt'] ?? 0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue