$rows, 'total' => $total, 'pages' => (int)ceil($total / $perPage)]; } public static function find(int $id): array|false { return Database::fetchOne( 'SELECT i.*, c.firma, c.nachname, c.vorname, c.anrede, c.kundennummer, c.strasse, c.plz, c.ort, c.land, c.email, c.payment_days FROM invoices i JOIN customers c ON i.customer_id = c.id WHERE i.id = ?', [$id] ); } public static function lines(int $invoiceId): array { return Database::fetchAll( 'SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY id', [$invoiceId] ); } public static function create(array $data): int { $customer = Customer::find((int)$data['customer_id']); $payDays = (int)($data['payment_days'] ?? $customer['payment_days'] ?? 14); $datum = $data['rechnungs_datum'] ?? date('Y-m-d'); $faellig = date('Y-m-d', strtotime($datum . " +{$payDays} days")); Database::execute( 'INSERT INTO invoices (customer_id, betreff, leistungszeitraum, rechnungs_datum, faellig_datum, status, notes) VALUES (?, ?, ?, ?, ?, \'Entwurf\', ?)', [ (int)$data['customer_id'], $data['betreff'], ($data['leistungszeitraum'] ?? '') !== '' ? $data['leistungszeitraum'] : null, $datum, $faellig, $data['notes'] ?? null, ] ); $id = (int)Database::lastInsertId(); // Create lines if (!empty($data['lines'])) { self::saveLines($id, $data['lines']); self::recalcTotals($id); } return $id; } public static function update(int $id, array $data): void { $inv = self::find($id); if ($inv && $inv['status'] !== 'Entwurf') { throw new \RuntimeException('Nur Entwürfe können bearbeitet werden.'); } $customer = Customer::find((int)$data['customer_id']); $payDays = (int)($data['payment_days'] ?? $customer['payment_days'] ?? 14); $datum = $data['rechnungs_datum']; $faellig = date('Y-m-d', strtotime($datum . " +{$payDays} days")); Database::execute( 'UPDATE invoices SET customer_id=?, betreff=?, leistungszeitraum=?, rechnungs_datum=?, faellig_datum=?, notes=?, updated_at=NOW() WHERE id=? AND status=\'Entwurf\'', [ (int)$data['customer_id'], $data['betreff'], ($data['leistungszeitraum'] ?? '') !== '' ? $data['leistungszeitraum'] : null, $datum, $faellig, $data['notes'] ?? null, $id, ] ); // Replace lines Database::execute('DELETE FROM invoice_lines WHERE invoice_id=?', [$id]); if (!empty($data['lines'])) { self::saveLines($id, $data['lines']); } self::recalcTotals($id); } public static function finalize(int $id): string { $inv = self::find($id); if (!$inv) throw new \RuntimeException('Rechnung nicht gefunden.'); if ($inv['status'] !== 'Entwurf') throw new \RuntimeException('Rechnung ist kein Entwurf mehr.'); $nr = self::generateRechnungsNr(); // Set fiscal year $year = (int)date('Y', strtotime($inv['rechnungs_datum'])); $fy = FiscalYear::findOrCreateByYear($year); Database::execute( 'UPDATE invoices SET rechnungs_nr=?, status=\'Finalisiert\', fiscal_year_id=?, updated_at=NOW() WHERE id=?', [$nr, $fy['id'], $id] ); return $nr; } public static function markPaid(int $id): void { Database::execute('UPDATE invoices SET status=\'Bezahlt\', updated_at=NOW() WHERE id=?', [$id]); } public static function cancel(int $id): void { $invoice = self::find($id); if (!$invoice) return; // Stornobuchungen für alle gebuchten Buchungen des Belegs erzeugen if (!empty($invoice['beleg_id'])) { $bookings = Database::fetchAll( 'SELECT id FROM bookings WHERE beleg_id = ? AND is_reversal = 0 AND reversed_by IS NULL', [(int)$invoice['beleg_id']] ); foreach ($bookings as $bk) { Booking::reverse((int)$bk['id'], 'Storno Rechnung ' . ($invoice['rechnungs_nr'] ?? $id)); } } Database::execute('UPDATE invoices SET status=\'Storniert\', updated_at=NOW() WHERE id=?', [$id]); } private static function saveLines(int $invoiceId, array $lines): void { $posCounter = 0; foreach ($lines as $line) { if (empty($line['bezeichnung'])) continue; $type = ($line['type'] ?? 'position') === 'heading' ? 'heading' : 'position'; if ($type === 'heading') { Database::execute( 'INSERT INTO invoice_lines (invoice_id, position, bezeichnung, menge, einheit, einzelpreis, ust_satz, betrag_netto, betrag_ust, betrag_brutto, type) VALUES (?, ?, ?, 0, \'\', 0, 0, 0, 0, 0, ?)', [$invoiceId, 0, $line['bezeichnung'], 'heading'] ); } else { $posCounter++; $menge = round((float)$line['menge'], 3); $preis = round((float)$line['einzelpreis'], 2); $ustSatz = round((float)$line['ust_satz'], 2); $netto = round($menge * $preis, 2); $ust = round($netto * $ustSatz / 100, 2); $brutto = round($netto + $ust, 2); Database::execute( 'INSERT INTO invoice_lines (invoice_id, position, bezeichnung, menge, einheit, einzelpreis, ust_satz, betrag_netto, betrag_ust, betrag_brutto, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ $invoiceId, $posCounter, $line['bezeichnung'], $menge, $line['einheit'] ?? 'Stk.', $preis, $ustSatz, $netto, $ust, $brutto, 'position', ] ); } } } private static function recalcTotals(int $id): void { $result = Database::fetchOne( 'SELECT SUM(betrag_netto) as netto, SUM(betrag_ust) as ust, SUM(betrag_brutto) as brutto FROM invoice_lines WHERE invoice_id = ?', [$id] ); Database::execute( 'UPDATE invoices SET betrag_netto=?, betrag_ust=?, betrag_brutto=?, updated_at=NOW() WHERE id=?', [ round((float)$result['netto'], 2), round((float)$result['ust'], 2), round((float)$result['brutto'], 2), $id, ] ); } public static function delete(int $id): void { $invoice = self::find($id); if (!$invoice) return; $belegId = !empty($invoice['beleg_id']) ? (int)$invoice['beleg_id'] : null; Database::execute('DELETE FROM invoice_lines WHERE invoice_id = ?', [$id]); Database::execute('DELETE FROM invoices WHERE id = ?', [$id]); if ($belegId !== null) { Database::execute('DELETE FROM bookings WHERE beleg_id = ?', [$belegId]); Database::execute('DELETE FROM belege WHERE id = ?', [$belegId]); } } public static function generateRechnungsNr(): string { $settings = Database::fetchOne( 'SELECT invoice_nr_prefix, invoice_nr_next, invoice_nr_year, invoice_nr_month FROM company_settings WHERE id=1' ); $prefix = $settings['invoice_nr_prefix'] ?? 'AR'; $next = (int)($settings['invoice_nr_next'] ?? 1); $savedYear = (int)($settings['invoice_nr_year'] ?? 0); $savedMonth = (int)($settings['invoice_nr_month'] ?? 0); $year = (int)date('Y'); $month = (int)date('n'); if ($savedYear !== $year || $savedMonth !== $month) { $next = 1; } $digits = max(2, strlen((string)$next)); $nr = sprintf('%s%04d%02d-%0' . $digits . 'd', $prefix, $year, $month, $next); Database::execute( 'UPDATE company_settings SET invoice_nr_next=?, invoice_nr_year=?, invoice_nr_month=? WHERE id=1', [$next + 1, $year, $month] ); return $nr; } }