Feature: E-Mail-Versand Rechnungen – Texte, Status Finalisiert, SMTP-Einstellungen
This commit is contained in:
parent
4485acbe1f
commit
c50e41e57b
12 changed files with 212 additions and 97 deletions
3
migrations/040_invoice_status_finalisiert.sql
Normal file
3
migrations/040_invoice_status_finalisiert.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE invoices
|
||||
MODIFY COLUMN status ENUM('Entwurf','Finalisiert','Gesendet','Bezahlt','Storniert')
|
||||
NOT NULL DEFAULT 'Entwurf';
|
||||
3
migrations/041_mail_templates.sql
Normal file
3
migrations/041_mail_templates.sql
Normal file
|
|
@ -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;
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'] ?? '';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ $badgeClass = match($invoice['status']) {
|
|||
'Bezahlt' => 'badge-green',
|
||||
'Storniert' => 'badge-red',
|
||||
'Gesendet' => 'badge-yellow',
|
||||
'Finalisiert' => 'badge-blue',
|
||||
default => 'badge-gray',
|
||||
};
|
||||
?>
|
||||
|
|
@ -40,7 +41,7 @@ $badgeClass = match($invoice['status']) {
|
|||
<?php if ($invoice['status'] === 'Gesendet'): ?>
|
||||
<a href="/invoices/<?= $h($invoice['id']) ?>/book" class="btn btn-success">Zahlungseingang erfassen</a>
|
||||
<?php endif; ?>
|
||||
<?php if (in_array($invoice['status'], ['Entwurf','Gesendet'])): ?>
|
||||
<?php if (in_array($invoice['status'], ['Entwurf','Finalisiert','Gesendet'])): ?>
|
||||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel"
|
||||
data-confirm="Rechnung stornieren?">
|
||||
<button type="submit" class="btn btn-danger">Stornieren</button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||
|
||||
<form id="settings-form" method="post" action="/settings">
|
||||
<input type="hidden" id="hf-mail-subject" name="mail_subject_template"
|
||||
value="<?= $h($settings['mail_subject_template'] ?? 'Rechnung %RNR%') ?>">
|
||||
<input type="hidden" id="hf-mail-body" name="mail_body_template"
|
||||
value="<?= $h($settings['mail_body_template'] ?? '') ?>">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
|
||||
|
||||
<!-- Firmendaten -->
|
||||
|
|
@ -81,9 +85,55 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">Bankdaten</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Bankname</label>
|
||||
<input type="text" name="bank_name" value="<?= $h($settings['bank_name'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>IBAN</label>
|
||||
<input type="text" name="iban" value="<?= $h($settings['iban'] ?? '') ?>" placeholder="DE89 3704 0044 0532 0130 00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>BIC</label>
|
||||
<input type="text" name="bic" value="<?= $h($settings['bic'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bankdaten + Logo + Nummernkreise -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">Nummernkreise</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid form-grid-2">
|
||||
<div class="form-group">
|
||||
<label>Rechnungs-Präfix</label>
|
||||
<input type="text" name="invoice_nr_prefix" value="<?= $h($settings['invoice_nr_prefix'] ?? 'AR') ?>" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Beleg-Präfix</label>
|
||||
<input type="text" name="beleg_nr_prefix" value="<?= $h($settings['beleg_nr_prefix'] ?? 'BLG') ?>" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Anlagen-Präfix</label>
|
||||
<input type="text" name="anlage_nr_prefix" value="<?= $h($settings['anlage_nr_prefix'] ?? 'ANL') ?>" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kunden-Präfix</label>
|
||||
<input type="text" name="customer_nr_prefix" value="<?= $h($settings['customer_nr_prefix'] ?? 'K-') ?>" maxlength="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Standard-Zahlungsziel (Tage)</label>
|
||||
<input type="number" name="payment_days_default" value="<?= $h($settings['payment_days_default'] ?? 14) ?>" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- rechte Spalte -->
|
||||
<div>
|
||||
<!-- Betriebsmodus -->
|
||||
<div class="card mb-2" style="border:2px solid <?= ($settings['test_mode'] ?? 1) ? 'var(--color-warning,#f59e0b)' : 'var(--color-success,#22c55e)' ?>">
|
||||
|
|
@ -210,52 +260,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">Bankdaten</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>Bankname</label>
|
||||
<input type="text" name="bank_name" value="<?= $h($settings['bank_name'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>IBAN</label>
|
||||
<input type="text" name="iban" value="<?= $h($settings['iban'] ?? '') ?>" placeholder="DE89 3704 0044 0532 0130 00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>BIC</label>
|
||||
<input type="text" name="bic" value="<?= $h($settings['bic'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">Nummernkreise</div></div>
|
||||
<div class="card-body">
|
||||
<div class="form-grid form-grid-2">
|
||||
<div class="form-group">
|
||||
<label>Rechnungs-Präfix</label>
|
||||
<input type="text" name="invoice_nr_prefix" value="<?= $h($settings['invoice_nr_prefix'] ?? 'AR') ?>" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Beleg-Präfix</label>
|
||||
<input type="text" name="beleg_nr_prefix" value="<?= $h($settings['beleg_nr_prefix'] ?? 'BLG') ?>" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Anlagen-Präfix</label>
|
||||
<input type="text" name="anlage_nr_prefix" value="<?= $h($settings['anlage_nr_prefix'] ?? 'ANL') ?>" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kunden-Präfix</label>
|
||||
<input type="text" name="customer_nr_prefix" value="<?= $h($settings['customer_nr_prefix'] ?? 'K-') ?>" maxlength="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Standard-Zahlungsziel (Tage)</label>
|
||||
<input type="number" name="payment_days_default" value="<?= $h($settings['payment_days_default'] ?? 14) ?>" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail-Versand -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">E-Mail-Versand (SMTP)</div></div>
|
||||
|
|
@ -298,45 +302,15 @@
|
|||
<input type="email" name="mail_from_email" value="<?= $h($settings['mail_from_email'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||||
<button type="button" class="btn btn-secondary" id="btn-mail-text">Text bearbeiten</button>
|
||||
<button type="button" class="btn btn-secondary" id="btn-mail-test">Test-Mail senden</button>
|
||||
<span id="mail-test-result" style="margin-left:.75rem;font-size:.8rem"></span>
|
||||
<span id="mail-test-result" style="font-size:.8rem"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
var result = document.getElementById('mail-test-result');
|
||||
btn.disabled = true;
|
||||
result.textContent = 'Sende…';
|
||||
result.style.color = '';
|
||||
fetch('/settings/mail-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.ok) {
|
||||
result.textContent = '✓ Test-Mail erfolgreich gesendet.';
|
||||
result.style.color = 'green';
|
||||
} else {
|
||||
result.textContent = '✗ ' + (data.error || 'Unbekannter Fehler');
|
||||
result.style.color = 'red';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
result.textContent = '✗ Netzwerkfehler';
|
||||
result.style.color = 'red';
|
||||
})
|
||||
.finally(function () { btn.disabled = false; });
|
||||
});
|
||||
</script>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-top:0">
|
||||
<div></div>
|
||||
<div>
|
||||
<!-- Logo -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">Logo</div></div>
|
||||
<div class="card-body">
|
||||
|
|
@ -407,8 +381,60 @@ document.getElementById('btn-mail-test')?.addEventListener('click', function ()
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Modal: E-Mail-Text bearbeiten -->
|
||||
<div id="mail-text-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;align-items:center;justify-content:center">
|
||||
<div style="background:#fff;border-radius:8px;padding:1.5rem;width:520px;max-width:95vw;box-shadow:0 8px 32px rgba(0,0,0,.2)">
|
||||
<h3 style="margin:0 0 1rem">E-Mail-Text bearbeiten</h3>
|
||||
<p style="font-size:.78rem;color:#666;margin:0 0 1rem">
|
||||
Platzhalter: <code>%RNR%</code> = Rechnungsnummer · <code>%FNAME%</code> = Firmenname · <code>%INHABER%</code> = Inhaber
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label>Betreff</label>
|
||||
<input type="text" id="mail-text-subject" style="width:100%"
|
||||
value="<?= $h($settings['mail_subject_template'] ?? 'Rechnung %RNR%') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Text</label>
|
||||
<textarea id="mail-text-body" rows="8" style="width:100%;resize:vertical;font-family:inherit;font-size:.85rem"><?= $h($settings['mail_body_template'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;justify-content:flex-end;margin-top:1.25rem">
|
||||
<button type="button" class="btn btn-secondary" id="btn-mail-text-cancel">Abbrechen</button>
|
||||
<button type="button" class="btn btn-primary" id="btn-mail-text-save">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
var result = document.getElementById('mail-test-result');
|
||||
btn.disabled = true;
|
||||
result.textContent = 'Sende…';
|
||||
result.style.color = '';
|
||||
fetch('/settings/mail-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.ok) {
|
||||
result.textContent = '✓ Test-Mail erfolgreich gesendet.';
|
||||
result.style.color = 'green';
|
||||
} else {
|
||||
result.textContent = '✗ ' + (data.error || 'Unbekannter Fehler');
|
||||
result.style.color = 'red';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
result.textContent = '✗ Netzwerkfehler';
|
||||
result.style.color = 'red';
|
||||
})
|
||||
.finally(function () { btn.disabled = false; });
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function updateModeLabel(cb) {
|
||||
|
|
@ -442,4 +468,50 @@ function logoPositionChanged() {
|
|||
document.getElementById('logo-margin-fields').style.display =
|
||||
(pos === 'left' || pos === 'right') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// E-Mail-Text-Modal
|
||||
document.getElementById('btn-mail-text')?.addEventListener('click', function () {
|
||||
document.getElementById('mail-text-modal').style.display = 'flex';
|
||||
});
|
||||
document.getElementById('btn-mail-text-cancel')?.addEventListener('click', function () {
|
||||
document.getElementById('mail-text-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('btn-mail-text-save')?.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
var subject = document.getElementById('mail-text-subject').value.trim();
|
||||
var body = document.getElementById('mail-text-body').value;
|
||||
var result = document.getElementById('mail-test-result');
|
||||
|
||||
if (!subject) {
|
||||
document.getElementById('mail-text-subject').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('mail_subject_template', subject);
|
||||
fd.append('mail_body_template', body);
|
||||
|
||||
fetch('/settings/mail-template', { method: 'POST', body: fd })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.ok) {
|
||||
// Versteckte Felder im Hauptformular synchron halten
|
||||
document.getElementById('hf-mail-subject').value = subject;
|
||||
document.getElementById('hf-mail-body').value = body;
|
||||
document.getElementById('mail-text-modal').style.display = 'none';
|
||||
result.textContent = '✓ E-Mail-Text gespeichert.';
|
||||
result.style.color = 'green';
|
||||
setTimeout(function () { result.textContent = ''; }, 3000);
|
||||
} else {
|
||||
result.textContent = '✗ ' + (data.error || 'Fehler beim Speichern.');
|
||||
result.style.color = 'red';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
result.textContent = '✗ Netzwerkfehler.';
|
||||
result.style.color = 'red';
|
||||
})
|
||||
.finally(function () { btn.disabled = false; });
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue