Feature: Bank-Zuordnung zeigt Ausgangsrechnungen inkl. Rechnungen ohne Beleg

- Eingabe-Spalte aus Beleg-Liste entfernt (alloc_betrag als Hidden-Field)
- Beleg-Liste nach Datum neu->alt sortiert
- Ausgangsrechnungen ohne Bank-Link in offene Belege aufgenommen
- Rechnungen mit fehlendem Beleg (beleg_id IS NULL) werden angezeigt
- Beim Verknüpfen solcher Rechnungen wird Beleg-PDF automatisch erstellt
This commit is contained in:
fds2610-bot 2026-05-15 19:09:45 +02:00
parent 700dc2952f
commit 4fb0b21a90
5 changed files with 103 additions and 17 deletions

View file

@ -72,6 +72,7 @@ $router->post('/bank/upload/confirm', fn($p) => (new BankController())->up
$router->get('/bank/{id}', fn($p) => (new BankController())->show($p));
$router->post('/bank/{id}/status', fn($p) => (new BankController())->updateStatus($p));
$router->post('/bank/{id}/link', fn($p) => (new BankController())->linkBeleg($p));
$router->post('/bank/{id}/link-invoice', fn($p) => (new BankController())->linkInvoice($p));
$router->post('/bank/{id}/unlink', fn($p) => (new BankController())->unlinkBeleg($p));
$router->post('/bank/{id}/beleg', fn($p) => (new BankController())->uploadBeleg($p));

View file

@ -6,8 +6,10 @@ namespace Steuerkram\Controllers;
use Steuerkram\Models\BankImportFormat;
use Steuerkram\Models\BankTransaction;
use Steuerkram\Controllers\InvoiceController;
use Steuerkram\Database;
use Steuerkram\Models\Beleg;
use Steuerkram\Models\Invoice;
use Steuerkram\Services\BankImportParser;
use Steuerkram\View;
@ -161,7 +163,7 @@ class BankController
$linkedBelege = BankTransaction::getLinkedBelege($id);
$remainingBetrag = BankTransaction::getRemainingBetrag($id);
$offeneBelege = Beleg::all(['booked' => '0'], 1, 200);
$offeneBelege = Beleg::getOffeneFuerBankzuordnung();
$view = new View();
$view->render('layout/base.php', [
@ -170,7 +172,7 @@ class BankController
'transaction' => $transaction,
'linkedBelege' => $linkedBelege,
'remainingBetrag' => $remainingBetrag,
'offeneBelege' => $offeneBelege['rows'],
'offeneBelege' => $offeneBelege,
]),
]);
}
@ -205,6 +207,36 @@ class BankController
View::redirect("/belege/$belegId?bank_tx=$id&alloc_betrag=$betrag");
}
public function linkInvoice(array $params): void
{
$id = (int)($params['id'] ?? 0);
$invoiceId = (int)($_POST['invoice_id'] ?? 0);
$betrag = round((float)str_replace(',', '.', $_POST['alloc_betrag'] ?? ''), 2);
if (!$invoiceId) {
View::redirect("/bank/$id", 'Keine Rechnung ausgewählt.', 'error');
}
$invoice = Invoice::find($invoiceId);
if (!$invoice) {
View::redirect("/bank/$id", 'Rechnung nicht gefunden.', 'error');
}
try {
InvoiceController::createBeleg($invoice);
$invoice = Invoice::find($invoiceId);
} catch (\Exception $e) {
View::redirect("/bank/$id", 'Beleg konnte nicht erstellt werden: ' . $e->getMessage(), 'error');
}
$belegId = (int)($invoice['beleg_id'] ?? 0);
if (!$belegId) {
View::redirect("/bank/$id", 'Beleg-Erstellung fehlgeschlagen.', 'error');
}
View::redirect("/belege/$belegId?bank_tx=$id&alloc_betrag=$betrag");
}
public function unlinkBeleg(array $params): void
{
$id = (int)($params['id'] ?? 0);

View file

@ -212,7 +212,7 @@ class InvoiceController
\Steuerkram\Database::beginTransaction();
$nr = Invoice::finalize($id);
$invoice = Invoice::find($id);
$belegNr = $this->createBeleg($invoice);
$belegNr = self::createBeleg($invoice);
\Steuerkram\Database::commit();
View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert (Beleg {$belegNr}). Bitte Zahlungseingang erfassen.");
} catch (\PDOException $e) {
@ -478,7 +478,7 @@ class InvoiceController
}
}
private function createBeleg(array $invoice): string
public static function createBeleg(array $invoice): string
{
$id = (int)$invoice['id'];
$lines = Invoice::lines($id);
@ -550,7 +550,7 @@ class InvoiceController
$stornoId = Invoice::createCancellation((int)$params['id']);
$storno = Invoice::find($stornoId);
if ($storno) {
$this->createBeleg($storno);
self::createBeleg($storno);
}
View::redirect('/invoices/' . $stornoId, 'Stornorechnung erstellt.');
}

View file

@ -72,6 +72,50 @@ class Beleg
return ['rows' => $rows, 'total' => $total, 'pages' => (int)ceil($total / $perPage)];
}
public static function getOffeneFuerBankzuordnung(): array
{
$belege = Database::fetchAll(
'SELECT b.*,
COALESCE(
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL),
b.betrag_brutto
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name
FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE (
(b.belegtyp = \'Eingangsrechnung\'
AND NOT EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id))
OR
(b.belegtyp = \'Ausgangsrechnung\'
AND NOT EXISTS (SELECT 1 FROM bank_transaction_belege btb WHERE btb.beleg_id = b.id))
)',
[]
);
$rechnungenOhneBeleg = Database::fetchAll(
'SELECT NULL AS id,
i.id AS invoice_id,
i.rechnungs_nr AS beleg_nr,
i.rechnungs_datum AS beleg_datum,
\'Ausgangsrechnung\' AS belegtyp,
i.betrag_brutto,
COALESCE(NULLIF(TRIM(c.firma), \'\'), CONCAT(TRIM(c.vorname), \' \', TRIM(c.nachname))) AS supplier_name
FROM invoices i
JOIN customers c ON c.id = i.customer_id
WHERE i.status IN (\'Finalisiert\', \'Gesendet\')
AND i.beleg_id IS NULL',
[]
);
$alle = array_merge($belege, $rechnungenOhneBeleg);
usort($alle, fn($a, $b) => strcmp($b['beleg_datum'] ?? '', $a['beleg_datum'] ?? ''));
return $alle;
}
public static function find(int $id): array|false
{
return Database::fetchOne(

View file

@ -222,18 +222,22 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<th>Typ</th>
<th>Lieferant</th>
<th class="td-right">Brutto</th>
<th class="td-right" style="width:130px">Zuzuordnen (€)</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($offeneBelege as $b): ?>
<?php $hatBeleg = $b['id'] !== null; ?>
<tr class="beleg-row"
data-beleg-id="<?= (int)$b['id'] ?>"
style="cursor:pointer"
title="Klicken zum Vorschau anzeigen">
style="cursor:<?= $hatBeleg ? 'pointer' : 'default' ?>"
<?= $hatBeleg ? 'title="Klicken zum Vorschau anzeigen"' : '' ?>>
<td class="td-mono" onclick="event.stopPropagation()">
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
<?php if ($hatBeleg): ?>
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
<?php else: ?>
<a href="/invoices/<?= (int)$b['invoice_id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
<?php endif; ?>
</td>
<td class="td-mono"><?= $h(View::formatDate($b['beleg_datum'])) ?></td>
<td><span class="badge badge-gray"><?= $h($b['belegtyp']) ?></span></td>
@ -243,17 +247,22 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<td class="td-right td-mono">
<?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) . ' €' : '<span class="text-muted">–</span>' ?>
</td>
<td class="td-right" onclick="event.stopPropagation()">
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link"
style="display:flex;gap:.4rem;align-items:center;justify-content:flex-end">
<td onclick="event.stopPropagation()">
<?php if ($hatBeleg): ?>
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link">
<input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>">
<input type="number" name="alloc_betrag"
value="<?= $h(number_format($remaining, 2, '.', '')) ?>"
min="0.01" step="0.01"
style="width:90px;text-align:right"
required>
<input type="hidden" name="alloc_betrag"
value="<?= $h(number_format($remaining, 2, '.', '')) ?>">
<button type="submit" class="btn btn-primary btn-sm">Verknüpfen</button>
</form>
<?php else: ?>
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link-invoice">
<input type="hidden" name="invoice_id" value="<?= (int)$b['invoice_id'] ?>">
<input type="hidden" name="alloc_betrag"
value="<?= $h(number_format($remaining, 2, '.', '')) ?>">
<button type="submit" class="btn btn-primary btn-sm">Verknüpfen</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>