From c50e41e57b9f0e5bdbf677a87c59d7c405f76ac1 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Tue, 28 Apr 2026 15:07:47 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20E-Mail-Versand=20Rechnungen=20?= =?UTF-8?q?=E2=80=93=20Texte,=20Status=20Finalisiert,=20SMTP-Einstellungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/040_invoice_status_finalisiert.sql | 3 + migrations/041_mail_templates.sql | 3 + public/index.php | 1 + src/Controllers/SettingsController.php | 19 ++ src/Models/Invoice.php | 2 +- src/Models/Setting.php | 3 + src/Services/EmailService.php | 23 +- src/Views/invoices/show.php | 11 +- src/Views/settings/form.php | 238 ++++++++++++------ tests/04_InvoiceBuchungTest.php | 2 +- tests/09_InvoiceToReportsTest.php | 2 +- tests/10_IstVersteuerungTest.php | 2 +- 12 files changed, 212 insertions(+), 97 deletions(-) create mode 100644 migrations/040_invoice_status_finalisiert.sql create mode 100644 migrations/041_mail_templates.sql diff --git a/migrations/040_invoice_status_finalisiert.sql b/migrations/040_invoice_status_finalisiert.sql new file mode 100644 index 0000000..67693f8 --- /dev/null +++ b/migrations/040_invoice_status_finalisiert.sql @@ -0,0 +1,3 @@ +ALTER TABLE invoices + MODIFY COLUMN status ENUM('Entwurf','Finalisiert','Gesendet','Bezahlt','Storniert') + NOT NULL DEFAULT 'Entwurf'; diff --git a/migrations/041_mail_templates.sql b/migrations/041_mail_templates.sql new file mode 100644 index 0000000..addafa2 --- /dev/null +++ b/migrations/041_mail_templates.sql @@ -0,0 +1,3 @@ +ALTER TABLE company_settings + ADD COLUMN IF NOT EXISTS mail_subject_template VARCHAR(500) NOT NULL DEFAULT 'Rechnung %RNR%' AFTER mail_from_email, + ADD COLUMN IF NOT EXISTS mail_body_template TEXT AFTER mail_subject_template; diff --git a/public/index.php b/public/index.php index a8558ae..dc3b910 100644 --- a/public/index.php +++ b/public/index.php @@ -131,6 +131,7 @@ $router->get('/tax/euer/{year}/konto/{konto_id}', 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/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 2ad8598..56aaaf2 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -90,6 +90,25 @@ class SettingsController 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/Invoice.php b/src/Models/Invoice.php index 8fbbc20..821fc6a 100644 --- a/src/Models/Invoice.php +++ b/src/Models/Invoice.php @@ -150,7 +150,7 @@ class Invoice $fy = FiscalYear::findOrCreateByYear($year); Database::execute( - 'UPDATE invoices SET rechnungs_nr=?, status=\'Gesendet\', fiscal_year_id=?, updated_at=NOW() WHERE id=?', + 'UPDATE invoices SET rechnungs_nr=?, status=\'Finalisiert\', fiscal_year_id=?, updated_at=NOW() WHERE id=?', [$nr, $fy['id'], $id] ); diff --git a/src/Models/Setting.php b/src/Models/Setting.php index 3c3a964..289b763 100644 --- a/src/Models/Setting.php +++ b/src/Models/Setting.php @@ -39,6 +39,7 @@ class Setting mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?, mail_smtp_user=?, mail_smtp_pass=?, mail_from_name=?, mail_from_email=?, + mail_subject_template=?, mail_body_template=?, logo_max_width=?, logo_max_height=?, logo_position=?, logo_margin_side=?, logo_margin_top=? WHERE id=1', @@ -78,6 +79,8 @@ class Setting $data['mail_smtp_pass'] ?: '', $data['mail_from_name'] ?: '', $data['mail_from_email'] ?: '', + $data['mail_subject_template'] ?: 'Rechnung %RNR%', + $data['mail_body_template'] ?: null, max(50, (int)($data['logo_max_width'] ?? 600)), max(20, (int)($data['logo_max_height'] ?? 100)), $logoPosition, diff --git a/src/Services/EmailService.php b/src/Services/EmailService.php index c7ae4fb..7941950 100644 --- a/src/Services/EmailService.php +++ b/src/Services/EmailService.php @@ -20,15 +20,19 @@ class EmailService $pdfContent = $pdfService->renderToString($invoice, $lines, $settings); $nr = $invoice['rechnungs_nr'] ?? 'Entwurf'; - $firma = $settings['firma'] ?? ''; - $subject = 'Rechnung ' . $nr; $filename = 'Rechnung-' . $nr . '.pdf'; - $body = "Guten Tag,\n\n" - . "anbei erhalten Sie die Rechnung {$nr} von {$firma}.\n\n" + $defaultBody = "Guten Tag,\n\n" + . "anbei erhalten Sie die Rechnung %RNR% von %FNAME%.\n\n" . "Bei Fragen stehen wir Ihnen gerne zur Verfügung.\n\n" . "Mit freundlichen Grüßen\n" - . $firma; + . "%INHABER%"; + + $subjectTpl = ($settings['mail_subject_template'] ?? '') ?: 'Rechnung %RNR%'; + $bodyTpl = ($settings['mail_body_template'] ?? '') ?: $defaultBody; + + $subject = $this->replacePlaceholders($subjectTpl, $invoice, $settings); + $body = $this->replacePlaceholders($bodyTpl, $invoice, $settings); $mailer->send($recipients, $subject, $body, $pdfContent, $filename); } @@ -48,6 +52,15 @@ class EmailService ); } + private function replacePlaceholders(string $text, array $invoice, array $settings): string + { + return str_replace( + ['%RNR%', '%FNAME%', '%INHABER%'], + [$invoice['rechnungs_nr'] ?? 'Entwurf', $settings['firma'] ?? '', $settings['inhaber'] ?? ''], + $text + ); + } + private function buildMailer(array $settings): SmtpMailer { $host = $settings['mail_smtp_host'] ?? ''; diff --git a/src/Views/invoices/show.php b/src/Views/invoices/show.php index 002fabb..f064f0c 100644 --- a/src/Views/invoices/show.php +++ b/src/Views/invoices/show.php @@ -10,10 +10,11 @@ $customerName = !empty($invoice['firma']) $lzHtml = $h($invoice['leistungszeitraum'] ?? ''); $badgeClass = match($invoice['status']) { - 'Bezahlt' => 'badge-green', - 'Storniert' => 'badge-red', - 'Gesendet' => 'badge-yellow', - default => 'badge-gray', + 'Bezahlt' => 'badge-green', + 'Storniert' => 'badge-red', + 'Gesendet' => 'badge-yellow', + 'Finalisiert' => 'badge-blue', + default => 'badge-gray', }; ?> @@ -40,7 +41,7 @@ $badgeClass = match($invoice['status']) { Zahlungseingang erfassen - +
diff --git a/src/Views/settings/form.php b/src/Views/settings/form.php index a02119f..8bec451 100644 --- a/src/Views/settings/form.php +++ b/src/Views/settings/form.php @@ -1,6 +1,10 @@ View::h($v); ?> + +
@@ -81,9 +85,55 @@
+ +
+
Bankdaten
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
Nummernkreise
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
- +
@@ -210,52 +260,6 @@
-
-
Bankdaten
-
-
- - -
-
- - -
-
- - -
-
-
- -
-
Nummernkreise
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
E-Mail-Versand (SMTP)
@@ -298,45 +302,15 @@
- - +
+ + + +
- - - -
- - - -
-
-
+
Logo
@@ -407,9 +381,61 @@ document.getElementById('btn-mail-test')?.addEventListener('click', function ()
+ +
+ + + + + + + + diff --git a/tests/04_InvoiceBuchungTest.php b/tests/04_InvoiceBuchungTest.php index bad1962..8761282 100644 --- a/tests/04_InvoiceBuchungTest.php +++ b/tests/04_InvoiceBuchungTest.php @@ -63,7 +63,7 @@ class InvoiceBuchungTest extends TestCase $this->assertNotEmpty($nr, 'Rechnung wurde finalisiert (Rechnungsnummer erhalten)'); $invoice = \Steuerkram\Models\Invoice::find($invoiceId); - $this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Rechnungsstatus ist Gesendet'); + $this->assertEquals('Finalisiert', $invoice['status'] ?? null, 'Rechnungsstatus ist Finalisiert'); // Buchungen erzeugen (EÜR: Einnahme) $belegNr = \Steuerkram\Models\Beleg::generateBelegNr(); diff --git a/tests/09_InvoiceToReportsTest.php b/tests/09_InvoiceToReportsTest.php index 72a344f..71a578d 100644 --- a/tests/09_InvoiceToReportsTest.php +++ b/tests/09_InvoiceToReportsTest.php @@ -62,7 +62,7 @@ class InvoiceToReportsTest extends TestCase $this->assertNotEmpty($rechnungsNr, 'Rechnungsnummer nach Finalisierung vorhanden'); $invoice = \Steuerkram\Models\Invoice::find($invoiceId); - $this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Status nach Finalisierung: Gesendet'); + $this->assertEquals('Finalisiert', $invoice['status'] ?? null, 'Status nach Finalisierung: Finalisiert'); // ── Beleg und Buchungen anlegen (wie InvoiceController::createBelegAndBookings) ── $belegNr = \Steuerkram\Models\Beleg::generateBelegNr(); diff --git a/tests/10_IstVersteuerungTest.php b/tests/10_IstVersteuerungTest.php index 83895bb..9f15580 100644 --- a/tests/10_IstVersteuerungTest.php +++ b/tests/10_IstVersteuerungTest.php @@ -65,7 +65,7 @@ class IstVersteuerungTest extends TestCase $this->assertNotEmpty($nr, 'Rechnungsnummer vergeben'); $invoice = \Steuerkram\Models\Invoice::find($invoiceId); - $this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Status nach Finalisierung: Gesendet'); + $this->assertEquals('Finalisiert', $invoice['status'] ?? null, 'Status nach Finalisierung: Finalisiert'); // Beleg anlegen (wie in InvoiceController::createBeleg) $belegNr = \Steuerkram\Models\Beleg::generateBelegNr();