diff --git a/migrations/081_buchungsvorlage_standard_lieferant.sql b/migrations/081_buchungsvorlage_standard_lieferant.sql new file mode 100644 index 0000000..298fa8d --- /dev/null +++ b/migrations/081_buchungsvorlage_standard_lieferant.sql @@ -0,0 +1,11 @@ +-- 081_buchungsvorlage_standard_lieferant.sql +-- Standard-Lieferant pro Vorlage; wird im Buchungsdialog vorbelegt. + +ALTER TABLE buchungsvorlagen + ADD COLUMN IF NOT EXISTS standard_lieferant VARCHAR(255) NULL DEFAULT NULL AFTER beschreibung; + +-- Bestehende Vorlage "Essensbeleg" mit Standardlieferant "Lebensmittel" vorbelegen. +UPDATE buchungsvorlagen +SET standard_lieferant = 'Lebensmittel' +WHERE name = 'Essensbeleg n. abzugsfähig (nur USt.)' + AND (standard_lieferant IS NULL OR standard_lieferant = ''); diff --git a/migrations/082_customer_mail_templates.sql b/migrations/082_customer_mail_templates.sql new file mode 100644 index 0000000..d8f0b2a --- /dev/null +++ b/migrations/082_customer_mail_templates.sql @@ -0,0 +1,6 @@ +-- 082_customer_mail_templates.sql +-- E-Mail-Betreff und -Text pro Kunde statt global in company_settings. + +ALTER TABLE customers + ADD COLUMN IF NOT EXISTS mail_subject_template VARCHAR(255) NULL DEFAULT NULL AFTER notes, + ADD COLUMN IF NOT EXISTS mail_body_template TEXT NULL DEFAULT NULL AFTER mail_subject_template; diff --git a/public/index.php b/public/index.php index 4502560..1efd2cb 100644 --- a/public/index.php +++ b/public/index.php @@ -161,7 +161,6 @@ $router->get('/tax/euer/{year}/anlageliste/pdf', fn($p) => (new EuerController() // ── Einstellungen ──────────────────────────────────────────── $router->get('/settings', fn($p) => (new SettingsController())->show($p)); $router->post('/settings', fn($p) => (new SettingsController())->update($p)); -$router->post('/settings/mail-template', fn($p) => (new SettingsController())->mailTemplate($p)); $router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p)); $router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p)); $router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p)); diff --git a/public/static/js/app.js b/public/static/js/app.js index 31277e0..cb668b8 100644 --- a/public/static/js/app.js +++ b/public/static/js/app.js @@ -524,10 +524,11 @@ document.addEventListener('input', function(e) { if (!inp.matches('#vorlage-modal .vorlage-netto, #vorlage-modal .vorlage-brutto')) return; const row = inp.closest('.vorlage-ust-row'); if (!row) return; - const satz = parseFloat(row.dataset.ustSatz) / 100; - const netto = row.querySelector('.vorlage-netto'); - const brutto = row.querySelector('.vorlage-brutto'); - const ust = row.querySelector('.vorlage-ust'); + const satz = parseFloat(row.dataset.ustSatz) / 100; + const netto = row.querySelector('.vorlage-netto'); + const brutto = row.querySelector('.vorlage-brutto'); + const ust = row.querySelector('.vorlage-ust'); + const ustAnzeige = row.querySelector('.vorlage-ust-anzeige'); if (inp.classList.contains('vorlage-netto')) { const n = parseFloat(netto.value) || 0; brutto.value = (n * (1 + satz)).toFixed(2); @@ -538,6 +539,7 @@ document.addEventListener('input', function(e) { netto.value = n.toFixed(2); ust.value = (b - n).toFixed(2); } + if (ustAnzeige) ustAnzeige.textContent = parseFloat(ust.value).toFixed(2).replace('.', ','); vorlageUpdateBruttoGesamt(); }); @@ -548,19 +550,11 @@ function vorlageUpdateBruttoGesamt() { const summe = Array.from(bruttoFelder).reduce((s, el) => s + (parseFloat(el.value) || 0), 0); const anzeige = modal.querySelector('[data-feldname="brutto_gesamt"]'); if (anzeige) anzeige.textContent = summe.toFixed(2).replace('.', ',') + ' €'; - // Auch das buchungs_datum-Hidden synchronisieren mit belegdatum-Feld falls vorhanden - const datumFeld = modal.querySelector('input[name="belegdatum"]'); - const datumHidden = modal.querySelector('input[name="buchungs_datum"]'); - if (datumFeld && datumHidden && datumFeld.value) { - datumHidden.value = datumFeld.value; - } } -// Datum-Feld Änderung → buchungs_datum synchronisieren +// Zahldatum-Feld Änderung → buchungs_datum-Hidden synchronisieren document.addEventListener('change', function(e) { - if (!e.target.matches('#vorlage-modal input[name="belegdatum"]')) return; - const modal = document.getElementById('vorlage-modal'); - if (!modal) return; - const hidden = modal.querySelector('input[name="buchungs_datum"]'); + if (!e.target.matches('#vorlage-modal #vorlage-zahldatum')) return; + const hidden = document.getElementById('vorlage-buchungs-datum'); if (hidden) hidden.value = e.target.value; }); diff --git a/src/Controllers/BookingController.php b/src/Controllers/BookingController.php index e016081..1e23129 100644 --- a/src/Controllers/BookingController.php +++ b/src/Controllers/BookingController.php @@ -209,10 +209,9 @@ class BookingController extends BaseController if ($newAssetId !== null) { View::redirect("/assets/{$newAssetId}/edit", 'Buchung erfasst. Bitte Wirtschaftsgut-Details ergänzen.'); - } elseif ($bankTxId > 0) { - View::redirect('/bank', 'Buchung erfasst und Bankbuchung verknüpft.'); } else { - View::redirect("/belege/{$belegId}", 'Buchung erfasst.'); + $msg = $bankTxId > 0 ? 'Buchung erfasst und Bankbuchung verknüpft.' : 'Buchung erfasst.'; + View::redirect("/belege/{$belegId}", $msg); } } catch (\PDOException $e) { error_log('[Steuerkram] PDO-Fehler in BookingController::store(): ' . $e->getMessage()); diff --git a/src/Controllers/BuchungsvorlageController.php b/src/Controllers/BuchungsvorlageController.php index 00670b5..14fb73d 100644 --- a/src/Controllers/BuchungsvorlageController.php +++ b/src/Controllers/BuchungsvorlageController.php @@ -141,14 +141,20 @@ class BuchungsvorlageController extends BaseController $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 && !empty($_GET['bank_tx'])) { + if (!$hatBankbuchung && $bankTxId > 0) { $hatBankbuchung = true; } } + if ($bankTxId > 0) { + $bankTx = BankTransaction::find($bankTxId); + $bankTxDatum = $bankTx['wertstellung'] ?? ''; + } echo $this->renderView('buchungsvorlagen/modal', [ 'vorlage' => $vorlage, @@ -157,6 +163,8 @@ class BuchungsvorlageController extends BaseController 'belegId' => $belegId, 'zahlungskonten' => array_values($zahlungskonten), 'hatBankbuchung' => $hatBankbuchung, + 'bankTxId' => $bankTxId, + 'bankTxDatum' => $bankTxDatum, ]); } @@ -170,17 +178,24 @@ class BuchungsvorlageController extends BaseController $beleg = Beleg::find($belegId); if (!$beleg) { http_response_code(404); return; } - $datum = !empty($_POST['buchungs_datum']) ? $_POST['buchungs_datum'] : date('Y-m-d'); + $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; } - View::redirect("/belege/{$belegId}", 'Buchungen aus Vorlage erfasst.'); + $msg = $bankTxId > 0 ? 'Buchungen aus Vorlage erfasst und Bankbuchung verknüpft.' : 'Buchungen aus Vorlage erfasst.'; + View::redirect("/belege/{$belegId}", $msg); } // ── Hilfsmethoden ────────────────────────────────────────────────────── diff --git a/src/Controllers/InvoiceController.php b/src/Controllers/InvoiceController.php index 4c72fb7..4060542 100644 --- a/src/Controllers/InvoiceController.php +++ b/src/Controllers/InvoiceController.php @@ -258,7 +258,10 @@ class InvoiceController extends BaseController try { $lines = Invoice::lines($id); $settings = Setting::get(); - (new EmailService())->sendInvoice($invoice, $lines, $settings, $recipients); + $customer = !empty($invoice['customer_id']) + ? \Steuerkram\Models\Customer::find((int)$invoice['customer_id']) + : null; + (new EmailService())->sendInvoice($invoice, $lines, $settings, $recipients, $customer ?: null); if ($invoice['status'] === 'Finalisiert') { \Steuerkram\Database::execute( diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 5e87042..40503aa 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -95,25 +95,6 @@ class SettingsController extends BaseController View::redirect('/settings', 'Logo hochgeladen.'); } - public function mailTemplate(array $params): void - { - header('Content-Type: application/json'); - $subject = trim($_POST['mail_subject_template'] ?? ''); - $body = trim($_POST['mail_body_template'] ?? ''); - - if ($subject === '') { - echo json_encode(['error' => 'Betreff darf nicht leer sein.']); - return; - } - - \Steuerkram\Database::execute( - 'UPDATE company_settings SET mail_subject_template=?, mail_body_template=? WHERE id=1', - [$subject, $body ?: null] - ); - - echo json_encode(['ok' => true]); - } - public function mailTest(array $params): void { header('Content-Type: application/json'); diff --git a/src/Models/Buchungsvorlage.php b/src/Models/Buchungsvorlage.php index 60da401..064be85 100644 --- a/src/Models/Buchungsvorlage.php +++ b/src/Models/Buchungsvorlage.php @@ -54,11 +54,12 @@ class Buchungsvorlage public static function create(array $data): int { Database::execute( - 'INSERT INTO buchungsvorlagen (name, beschreibung, aktiv, sort_order) - VALUES (?, ?, ?, ?)', + 'INSERT INTO buchungsvorlagen (name, beschreibung, standard_lieferant, aktiv, sort_order) + VALUES (?, ?, ?, ?, ?)', [ trim($data['name']), $data['beschreibung'] ?? null, + $data['standard_lieferant'] !== '' ? trim($data['standard_lieferant'] ?? '') : null, isset($data['aktiv']) ? (int)$data['aktiv'] : 1, (int)($data['sort_order'] ?? 0), ] @@ -70,11 +71,12 @@ class Buchungsvorlage { Database::execute( 'UPDATE buchungsvorlagen - SET name = ?, beschreibung = ?, aktiv = ?, sort_order = ? + SET name = ?, beschreibung = ?, standard_lieferant = ?, aktiv = ?, sort_order = ? WHERE id = ?', [ trim($data['name']), $data['beschreibung'] ?? null, + $data['standard_lieferant'] !== '' ? trim($data['standard_lieferant'] ?? '') : null, isset($data['aktiv']) ? (int)$data['aktiv'] : 1, (int)($data['sort_order'] ?? 0), $id, diff --git a/src/Models/Customer.php b/src/Models/Customer.php index 1d6f027..44695c4 100644 --- a/src/Models/Customer.php +++ b/src/Models/Customer.php @@ -39,8 +39,8 @@ class Customer $nr = self::generateKundennummer(); Database::execute( 'INSERT INTO customers (kundennummer, firma, anrede, vorname, nachname, strasse, plz, ort, land, - email, telefon, ust_id, steuernummer, payment_days, notes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + email, telefon, ust_id, steuernummer, payment_days, notes, mail_subject_template, mail_body_template) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ $data['kundennummer'] ?? $nr, $data['firma'] ?? null, @@ -57,6 +57,8 @@ class Customer $data['steuernummer'] ?? null, (int)($data['payment_days'] ?? 14), $data['notes'] ?? null, + ($data['mail_subject_template'] ?? '') !== '' ? trim($data['mail_subject_template']) : null, + ($data['mail_body_template'] ?? '') !== '' ? trim($data['mail_body_template']) : null, ] ); return (int)Database::lastInsertId(); @@ -66,7 +68,8 @@ class Customer { Database::execute( 'UPDATE customers SET firma=?, anrede=?, vorname=?, nachname=?, strasse=?, plz=?, ort=?, land=?, - email=?, telefon=?, ust_id=?, steuernummer=?, payment_days=?, notes=?, updated_at=NOW() + email=?, telefon=?, ust_id=?, steuernummer=?, payment_days=?, notes=?, + mail_subject_template=?, mail_body_template=?, updated_at=NOW() WHERE id=?', [ $data['firma'] ?? null, @@ -83,6 +86,8 @@ class Customer $data['steuernummer'] ?? null, (int)($data['payment_days'] ?? 14), $data['notes'] ?? null, + ($data['mail_subject_template'] ?? '') !== '' ? trim($data['mail_subject_template']) : null, + ($data['mail_body_template'] ?? '') !== '' ? trim($data['mail_body_template']) : null, $id, ] ); diff --git a/src/Services/EmailService.php b/src/Services/EmailService.php index 7941950..a7610fb 100644 --- a/src/Services/EmailService.php +++ b/src/Services/EmailService.php @@ -9,10 +9,11 @@ class EmailService /** * Sendet eine Rechnung als PDF-Anhang an alle Empfänger. * - * @param string[] $recipients E-Mail-Adressen (1–3) + * @param string[] $recipients E-Mail-Adressen (1–3) + * @param array|null $customer Kundendatensatz (für kundenspezifische E-Mail-Vorlage) * @throws \RuntimeException */ - public function sendInvoice(array $invoice, array $lines, array $settings, array $recipients): void + public function sendInvoice(array $invoice, array $lines, array $settings, array $recipients, ?array $customer = null): void { $mailer = $this->buildMailer($settings); @@ -28,8 +29,8 @@ class EmailService . "Mit freundlichen Grüßen\n" . "%INHABER%"; - $subjectTpl = ($settings['mail_subject_template'] ?? '') ?: 'Rechnung %RNR%'; - $bodyTpl = ($settings['mail_body_template'] ?? '') ?: $defaultBody; + $subjectTpl = ($customer['mail_subject_template'] ?? '') ?: 'Rechnung %RNR%'; + $bodyTpl = ($customer['mail_body_template'] ?? '') ?: $defaultBody; $subject = $this->replacePlaceholders($subjectTpl, $invoice, $settings); $body = $this->replacePlaceholders($bodyTpl, $invoice, $settings); diff --git a/src/Views/bank/show.php b/src/Views/bank/show.php index 875ea63..9da9253 100644 --- a/src/Views/bank/show.php +++ b/src/Views/bank/show.php @@ -224,9 +224,7 @@ $restFormatted = number_format($remaining, 2, ',', '.');
| Belegnr. | -Datum | -Typ | +Beleg | Lieferant | Brutto | @@ -239,20 +237,22 @@ $restFormatted = number_format($remaining, 2, ',', '.'); data-beleg-id="= (int)$b['id'] ?>" style="cursor:= $hatBeleg ? 'pointer' : 'default' ?>" = $hatBeleg ? 'title="Klicken zum Vorschau anzeigen"' : '' ?>> - | - - = $h($b['beleg_nr']) ?> - - = $h($b['beleg_nr']) ?> - + |
+
+
+ = $h($b['beleg_nr']) ?>
+
+ = $h($b['beleg_nr']) ?>
+
+
+ = $h(View::formatDate($b['beleg_datum'])) ?>
+ = $h($b['belegtyp']) ?>
|
- = $h(View::formatDate($b['beleg_datum'])) ?> | -= $h($b['belegtyp']) ?> | = $b['supplier_name'] ? $h($b['supplier_name']) : '–' ?> | -- = $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) . ' €' : '–' ?> + | + = $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) : '–' ?> | diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index 2f861e9..d05ea6c 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -484,7 +484,7 @@ $suppliers = $suppliers ?? []; | - + | + 0,00 + | diff --git a/src/Views/customers/form.php b/src/Views/customers/form.php index 634ad6c..b0d664f 100644 --- a/src/Views/customers/form.php +++ b/src/Views/customers/form.php @@ -82,6 +82,32 @@ +
|---|