Steuerkram/src/Controllers/BuchungsvorlageController.php
fds2610-bot 3fe5015027 Feature: Buchungsvorlagen – Bankbuchung verknüpfen, Zahldatum, Standard-Lieferant
- Vorlagenbuchung verknüpft jetzt korrekt die Bankbuchung (bank_tx_id)
- Buchungs-Redirect bleibt nach Vorlagenbuchung auf dem Beleg
- Zahldatum (Wertstellung der Bankbuchung) im Vorlagendialog neben Belegdatum
- USt-Felder im Vorlagendialog als Text (nicht mehr als editierbare Inputs)
- Standard-Lieferant pro Vorlage speicherbar; wird im Buchungsdialog vorbelegt
- Migration 081: spalte standard_lieferant in buchungsvorlagen
2026-05-30 14:22:48 +02:00

254 lines
9.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Steuerkram\Controllers;
use Steuerkram\Models\Account;
use Steuerkram\Models\Beleg;
use Steuerkram\Models\BankTransaction;
use Steuerkram\Models\Buchungsvorlage;
use Steuerkram\Services\VorlageBuchungsService;
use Steuerkram\View;
class BuchungsvorlageController extends BaseController
{
// ── Pflege-UI ──────────────────────────────────────────────────────────
public function index(array $params): void
{
$vorlagen = Buchungsvorlage::findAllIncludingInactive();
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Buchungsvorlagen',
'topbarActions' => '<a href="/einstellungen/buchungsvorlagen/neu" class="btn btn-primary">Neue Vorlage</a>',
'content' => $this->renderView('buchungsvorlagen/index', ['vorlagen' => $vorlagen]),
]);
}
public function create(array $params): void
{
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Neue Buchungsvorlage',
'content' => $this->renderView('buchungsvorlagen/form', [
'vorlage' => null,
'felder' => [],
'regeln' => [],
'errors' => [],
]),
]);
}
public function store(array $params): void
{
$data = $_POST;
$errors = $this->validate($data);
if ($errors) {
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Neue Buchungsvorlage',
'content' => $this->renderView('buchungsvorlagen/form', [
'vorlage' => $data,
'felder' => $this->parseFelderFromPost($data),
'regeln' => $this->parseRegelnFromPost($data),
'errors' => $errors,
]),
]);
return;
}
$id = Buchungsvorlage::create($data);
Buchungsvorlage::saveFelderForVorlage($id, $this->parseFelderFromPost($data));
Buchungsvorlage::saveRegelnForVorlage($id, $this->parseRegelnFromPost($data));
View::redirect('/einstellungen/buchungsvorlagen', 'Vorlage gespeichert.');
}
public function edit(array $params): void
{
$id = (int)$params['id'];
$vorlage = Buchungsvorlage::find($id);
if (!$vorlage) { http_response_code(404); return; }
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Vorlage bearbeiten',
'content' => $this->renderView('buchungsvorlagen/form', [
'vorlage' => $vorlage,
'felder' => Buchungsvorlage::getFelder($id),
'regeln' => Buchungsvorlage::getRegeln($id),
'errors' => [],
]),
]);
}
public function update(array $params): void
{
$id = (int)$params['id'];
$vorlage = Buchungsvorlage::find($id);
if (!$vorlage) { http_response_code(404); return; }
$data = $_POST;
$errors = $this->validate($data);
if ($errors) {
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Vorlage bearbeiten',
'content' => $this->renderView('buchungsvorlagen/form', [
'vorlage' => array_merge($vorlage, $data),
'felder' => $this->parseFelderFromPost($data),
'regeln' => $this->parseRegelnFromPost($data),
'errors' => $errors,
]),
]);
return;
}
Buchungsvorlage::update($id, $data);
Buchungsvorlage::saveFelderForVorlage($id, $this->parseFelderFromPost($data));
Buchungsvorlage::saveRegelnForVorlage($id, $this->parseRegelnFromPost($data));
View::redirect('/einstellungen/buchungsvorlagen', 'Vorlage aktualisiert.');
}
public function destroy(array $params): void
{
$id = (int)$params['id'];
Buchungsvorlage::delete($id);
View::redirect('/einstellungen/buchungsvorlagen', 'Vorlage gelöscht.');
}
// ── AJAX: Modal-HTML ───────────────────────────────────────────────────
public function modal(array $params): void
{
$vorlageId = (int)$params['id'];
$belegId = (int)($_GET['beleg_id'] ?? 0);
$vorlage = Buchungsvorlage::find($vorlageId);
if (!$vorlage || !$vorlage['aktiv']) {
http_response_code(404);
echo '<p>Vorlage nicht gefunden.</p>';
return;
}
$beleg = $belegId > 0 ? Beleg::find($belegId) : null;
$felder = Buchungsvorlage::getFelder($vorlageId);
$zahlungskonten = Account::all(['kontenart' => 'Zahlungskonto', 'aktiv' => 1]);
$zahlungskonten = array_filter($zahlungskonten, fn($a) => (int)$a['kontonummer'] >= 1200 && (int)$a['kontonummer'] <= 1299);
$bankTxId = (int)($_GET['bank_tx'] ?? 0);
$bankTxDatum = '';
$hatBankbuchung = false;
if ($beleg) {
$linked = BankTransaction::findByBelegId((int)$beleg['id']);
$hatBankbuchung = !empty($linked);
if (!$hatBankbuchung && $bankTxId > 0) {
$hatBankbuchung = true;
}
}
if ($bankTxId > 0) {
$bankTx = BankTransaction::find($bankTxId);
$bankTxDatum = $bankTx['wertstellung'] ?? '';
}
echo $this->renderView('buchungsvorlagen/modal', [
'vorlage' => $vorlage,
'felder' => $felder,
'beleg' => $beleg,
'belegId' => $belegId,
'zahlungskonten' => array_values($zahlungskonten),
'hatBankbuchung' => $hatBankbuchung,
'bankTxId' => $bankTxId,
'bankTxDatum' => $bankTxDatum,
]);
}
// ── Buchungen ausführen ────────────────────────────────────────────────
public function buchen(array $params): void
{
$belegId = (int)$params['belegId'];
$vorlageId = (int)$params['vorlageId'];
$beleg = Beleg::find($belegId);
if (!$beleg) { http_response_code(404); return; }
$datum = !empty($_POST['buchungs_datum']) ? $_POST['buchungs_datum'] : date('Y-m-d');
$bankTxId = (int)($_POST['bank_tx_id'] ?? 0);
try {
$svc = new VorlageBuchungsService();
$svc->buchen($belegId, $vorlageId, $_POST, $datum);
if ($bankTxId > 0) {
$totalBrutto = \Steuerkram\Models\Booking::sumBruttoForBeleg($belegId);
BankTransaction::linkBeleg($bankTxId, $belegId, $totalBrutto);
}
} catch (\RuntimeException $e) {
View::redirect("/belege/{$belegId}", $e->getMessage(), 'error');
return;
}
$msg = $bankTxId > 0 ? 'Buchungen aus Vorlage erfasst und Bankbuchung verknüpft.' : 'Buchungen aus Vorlage erfasst.';
View::redirect("/belege/{$belegId}", $msg);
}
// ── Hilfsmethoden ──────────────────────────────────────────────────────
private function validate(array $data): array
{
$errors = [];
if (empty(trim($data['name'] ?? ''))) {
$errors['name'] = 'Name ist ein Pflichtfeld.';
}
return $errors;
}
private function parseFelderFromPost(array $data): array
{
$felder = [];
$raws = $data['felder'] ?? [];
foreach ($raws as $f) {
if (empty(trim($f['feldname'] ?? ''))) continue;
$felder[] = [
'gruppe_nr' => (int)($f['gruppe_nr'] ?? 1),
'sort_order' => (int)($f['sort_order'] ?? 0),
'feldname' => trim($f['feldname']),
'label' => trim($f['label'] ?? ''),
'typ' => $f['typ'] ?? 'text',
'ust_satz' => $f['ust_satz'] ?? '',
'meta_key' => trim($f['meta_key'] ?? ''),
];
}
return $felder;
}
private function parseRegelnFromPost(array $data): array
{
$regeln = [];
$raws = $data['regeln'] ?? [];
foreach ($raws as $r) {
if (empty(trim($r['bedingung_feld'] ?? ''))) continue;
$buchungen = [];
foreach (($r['buchungen'] ?? []) as $b) {
if (empty(trim($b['betrag_ref'] ?? '')) || empty(trim($b['nach_konto'] ?? ''))) continue;
$buchungen[] = [
'betrag_ref' => trim($b['betrag_ref']),
'nach_konto' => trim($b['nach_konto']),
];
}
$regeln[] = [
'sort_order' => (int)($r['sort_order'] ?? 0),
'bedingung_feld' => trim($r['bedingung_feld']),
'bedingung_op' => $r['bedingung_op'] ?? 'neq_zero',
'buchungen' => $buchungen,
];
}
return $regeln;
}
}