$rows, 'total' => $total, 'pages' => (int)ceil($total / $perPage)]; } public static function find(int $id): array|false { return Database::fetchOne('SELECT * FROM bookings WHERE id = ?', [$id]); } public static function create(array $data): int { // Determine fiscal year $year = (int)date('Y', strtotime($data['buchungs_datum'])); $fy = FiscalYear::findOrCreateByYear($year); if ($fy['locked']) { throw new \RuntimeException("Geschäftsjahr {$year} ist gesperrt. Neue Buchungen nicht möglich."); } // Calculate amounts $netto = round((float)$data['betrag_netto'], 2); $ust = round((float)($data['ust_satz'] ?? 0) / 100 * $netto, 2); $brutto = round($netto + $ust, 2); $nr = self::generateBuchungsNr(); Database::execute( 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, soll_konto_id, haben_konto_id, betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ $nr, (int)$data['beleg_id'], $data['buchungs_datum'], (int)$data['soll_konto_id'], (int)$data['haben_konto_id'], $netto, (float)($data['ust_satz'] ?? 0), $ust, $brutto, $data['buchungstext'], $fy['id'], ] ); $id = (int)Database::lastInsertId(); // Mark Beleg as booked Database::execute('UPDATE belege SET booked=1, updated_at=NOW() WHERE id=?', [(int)$data['beleg_id']]); return $id; } public static function reverse(int $id, string $reason): int { $orig = self::find($id); if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.'); if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.'); // Create reversal: swap Soll/Haben, same amounts $reversalData = [ 'beleg_id' => $orig['beleg_id'], 'buchungs_datum' => date('Y-m-d'), 'soll_konto_id' => $orig['haben_konto_id'], 'haben_konto_id' => $orig['soll_konto_id'], 'betrag_netto' => $orig['betrag_netto'], 'ust_satz' => $orig['ust_satz'], 'buchungstext' => 'STORNO zu ' . $orig['buchungs_nr'] . ': ' . $reason, ]; $year = (int)date('Y', strtotime($reversalData['buchungs_datum'])); $fy = FiscalYear::findOrCreateByYear($year); $nr = self::generateBuchungsNr(); Database::execute( 'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, soll_konto_id, haben_konto_id, betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, is_reversal, fiscal_year_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)', [ $nr, $reversalData['beleg_id'], $reversalData['buchungs_datum'], $reversalData['soll_konto_id'], $reversalData['haben_konto_id'], $orig['betrag_netto'], $orig['ust_satz'], $orig['betrag_ust'], $orig['betrag_brutto'], $reversalData['buchungstext'], $fy['id'], ] ); $reversalId = (int)Database::lastInsertId(); // Link original to reversal Database::execute('UPDATE bookings SET reversed_by=? WHERE id=?', [$reversalId, $id]); return $reversalId; } public static function generateBuchungsNr(): string { $year = date('Y'); $result = Database::fetchOne( 'SELECT COUNT(*) as cnt FROM bookings WHERE YEAR(buchungs_datum) = ?', [$year] ); $next = (int)($result['cnt'] ?? 0) + 1; return sprintf('BU-%s-%06d', $year, $next); } public static function sumByYear(int $year): array { return Database::fetchAll( 'SELECT bk.soll_konto_id, bk.haben_konto_id, SUM(bk.betrag_netto) as sum_netto, SUM(bk.betrag_ust) as sum_ust, SUM(bk.betrag_brutto) as sum_brutto FROM bookings bk JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id WHERE fy.year = ? GROUP BY bk.soll_konto_id, bk.haben_konto_id', [$year] ); } }