273 lines
9.4 KiB
PHP
273 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Steuerkram\Models;
|
|
|
|
use Steuerkram\Database;
|
|
|
|
class Invoice
|
|
{
|
|
public static function all(array $filter = [], int $page = 1, int $perPage = 30): array
|
|
{
|
|
$where = ['1=1'];
|
|
$params = [];
|
|
|
|
if (!empty($filter['status'])) {
|
|
$where[] = 'i.status = ?';
|
|
$params[] = $filter['status'];
|
|
}
|
|
if (!empty($filter['year'])) {
|
|
$where[] = 'YEAR(i.rechnungs_datum) = ?';
|
|
$params[] = (int)$filter['year'];
|
|
}
|
|
if (!empty($filter['customer_id'])) {
|
|
$where[] = 'i.customer_id = ?';
|
|
$params[] = (int)$filter['customer_id'];
|
|
}
|
|
if (!empty($filter['search'])) {
|
|
$where[] = '(i.rechnungs_nr LIKE ? OR i.betreff LIKE ? OR c.nachname LIKE ? OR c.firma LIKE ?)';
|
|
$s = '%' . $filter['search'] . '%';
|
|
$params = array_merge($params, [$s, $s, $s, $s]);
|
|
}
|
|
|
|
$offset = ($page - 1) * $perPage;
|
|
$sql = 'SELECT i.*, c.firma, c.nachname, c.vorname, c.anrede, c.kundennummer
|
|
FROM invoices i
|
|
JOIN customers c ON i.customer_id = c.id
|
|
WHERE ' . implode(' AND ', $where) . '
|
|
ORDER BY i.rechnungs_datum DESC, i.id DESC
|
|
LIMIT ? OFFSET ?';
|
|
|
|
$params[] = $perPage;
|
|
$params[] = $offset;
|
|
$rows = Database::fetchAll($sql, $params);
|
|
|
|
$countSql = 'SELECT COUNT(*) as cnt FROM invoices i JOIN customers c ON i.customer_id = c.id WHERE ' . implode(' AND ', $where);
|
|
$countParams = array_slice($params, 0, -2);
|
|
$total = (int)(Database::fetchOne($countSql, $countParams)['cnt'] ?? 0);
|
|
|
|
return ['rows' => $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 position',
|
|
[$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=\'Gesendet\', 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
|
|
{
|
|
foreach ($lines as $i => $line) {
|
|
if (empty($line['bezeichnung'])) continue;
|
|
$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)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
$invoiceId,
|
|
$i + 1,
|
|
$line['bezeichnung'],
|
|
$menge,
|
|
$line['einheit'] ?? 'Stk.',
|
|
$preis,
|
|
$ustSatz,
|
|
$netto,
|
|
$ust,
|
|
$brutto,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
$year = (int)date('Y');
|
|
|
|
// Reset counter on new year
|
|
if ($savedYear !== $year) {
|
|
$next = 1;
|
|
}
|
|
|
|
$nr = sprintf('%s-%s-%04d', $prefix, $year, $next);
|
|
|
|
Database::execute(
|
|
'UPDATE company_settings SET invoice_nr_next=?, invoice_nr_year=? WHERE id=1',
|
|
[$next + 1, $year]
|
|
);
|
|
|
|
return $nr;
|
|
}
|
|
}
|