GoBD-konforme Rechnungsverwaltung hinzugefügt
@@ -0,0 +1,701 @@
|
|||||||
|
Für eine **GoBD-konforme Rechnungsverwaltung** in deinem ERP/CRM-System müssen bestimmte **Funktionen und Prozesse** implementiert sein, um den Anforderungen der **Grundsätze zur ordnungsmäßigen Führung und Aufbewahrung von Büchern, Aufzeichnungen und Unterlagen in elektronischer Form (GoBD)** zu entsprechen. Hier sind die **konkreten Funktionen**, die du implementieren solltest, inklusive technischer Umsetzungsvorschläge für Laravel/Vue.js:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. GoBD-Kernanforderungen und deren Umsetzung**
|
||||||
|
Die GoBD (ab 2020 gültig) stellt folgende **zentrale Anforderungen** an digitale Rechnungssysteme:
|
||||||
|
|
||||||
|
| **GoBD-Anforderung** | **Konkrete Umsetzung in deinem ERP/CRM** | **Technische Lösung (Laravel/Vue.js)** |
|
||||||
|
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
|
||||||
|
| **1. Nachvollziehbarkeit** | Jede Rechnung muss **lückenlos nachvollziehbar** sein (wer, wann, was geändert hat). | **Laravel Auditing** für Änderungen + **Revision History** in der Datenbank. |
|
||||||
|
| **2. Unveränderbarkeit** | Gestellte Rechnungen dürfen **nicht mehr geändert** werden (nur Stornierung oder Gutschrift möglich). | **Immutable Records**: Rechnungen nach dem "Stellen" als `readonly` markieren. |
|
||||||
|
| **3. Vollständigkeit** | **Keine Lücken** in der Rechnungsnummernfolge. | **Automatische Nummerngenerierung** mit Transaktionen (siehe Code-Beispiele unten). |
|
||||||
|
| **4. Aufbewahrungspflicht** | Rechnungen müssen **10 Jahre** unverändert archiviert werden. | **Automatische Backups** (z. B. mit `spatie/laravel-backup`) + **Speicherung als PDF/XML**. |
|
||||||
|
| **5. Zeitgerechte Buchung** | Rechnungen müssen **zeitnah** (innerhalb weniger Tage) erfasst werden. | **Automatische Erinnerungen** für offene Entwürfe (z. B. mit Laravel Scheduling). |
|
||||||
|
| **6. Ordnungsgemäße Speicherung** | Rechnungen müssen in **maschinenlesbarer Form** (z. B. ZUGFeRD/XML) gespeichert werden. | **PDF + XML-Export** (z. B. mit `zugferd/php`). |
|
||||||
|
| **7. Zugriffsschutz** | Nur **autorisierte Personen** dürfen Rechnungen ändern/löschen. | **Laravel Policies** für Berechtigungen. |
|
||||||
|
| **8. Prüfbarkeit** | Rechnungen müssen **jederzeit prüfbar** sein (z. B. durch Finanzamt). | **Prüfpade (Audit Logs)** + **Dokumentation von Änderungen**. |
|
||||||
|
| **9. Belegabbildung** | Rechnungen müssen **1:1 dem Original entsprechen** (keine Datenverluste bei Export/Import). | **ZUGFeRD/XRechnung** für elektronische Rechnungen. |
|
||||||
|
| **10. Verfahrensdokumentation** | Es muss eine **Dokumentation der Prozesse** existieren (z. B. wie Rechnungen erstellt/archiviert werden). | **Automatisch generierte Dokumentation** (z. B. mit Laravel Nova oder Markdown-Export). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Konkrete Funktionen für deine Rechnungsverwaltung**
|
||||||
|
### **A. Rechnungserstellung (GoBD-konform)**
|
||||||
|
| **Funktion** | **Beschreibung** | **Technische Umsetzung** |
|
||||||
|
|----------------------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Entwürfe vs. gestellte Rechnungen** | Rechnungen werden **erst als Entwurf** gespeichert und erhalten **erst bei "Stellen" eine Nummer**. | **Status-Feld** (`draft`, `issued`, `sent`, `paid`, `cancelled`) + `number`-Feld (nullable). |
|
||||||
|
| **Automatische Nummerngenerierung** | Rechnungsnummern werden **fortlaufend und ohne Lücken** vergeben (z. B. `RE-2023-001`). | **Datenbank-Transaktionen** + `generateNumber()`-Methode (siehe Code unten). |
|
||||||
|
| **Unveränderlichkeit nach Stellung** | Gestellte Rechnungen können **nicht mehr bearbeitet**, sondern nur **storniert** werden. | **`readonly`-Felder im Frontend** + **Datenbank-Constraints**. |
|
||||||
|
| **ZUGFeRD/XRechnung-Export** | Rechnungen werden im **GoBD-konformen Format** (PDF + XML) exportiert. | **`zugferd/php`-Bibliothek** (siehe Beispiel unten). |
|
||||||
|
| **Mehrwertsteuer-Validierung** | Prüft **gültige USt-IDs** (z. B. über die [VIES-API](http://ec.europa.eu/taxation_customs/vies/)). | **HTTP-Request an VIES-API** (siehe Code unten). |
|
||||||
|
|
||||||
|
---
|
||||||
|
### **B. Archivierung und Aufbewahrung**
|
||||||
|
| **Funktion** | **Beschreibung** | **Technische Umsetzung** |
|
||||||
|
|----------------------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Automatische PDF/XML-Speicherung** | Jede gestellte Rechnung wird als **PDF + XML** in einem **unveränderlichen Speicher** abgelegt. | **Laravel Storage** (z. B. S3) + **Backups mit `spatie/laravel-backup`**. |
|
||||||
|
| **10-Jahres-Aufbewahrung** | Rechnungen werden **automatisch archiviert** und sind **10 Jahre lang verfügbar**. | **Laravel Retention Policies** (z. B. mit `aws/s3-lifecycle`) oder **Datenbank-Archivtabellen**. |
|
||||||
|
| **Unveränderliche Backups** | Backups werden **verschlüsselt und signiert** abgelegt, um Manipulationen zu verhindern. | **`spatie/laravel-backup` mit Verschlüsselung**. |
|
||||||
|
| **Prüfpfade (Audit Logs)** | Alle Änderungen an Rechnungen werden **protokolliert** (wer, wann, was). | **`laravel-auditing`** (siehe Beispiel unten). |
|
||||||
|
|
||||||
|
---
|
||||||
|
### **C. Sicherheit und Zugriffskontrolle**
|
||||||
|
| **Funktion** | **Beschreibung** | **Technische Umsetzung** |
|
||||||
|
|----------------------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Rollenbasierte Berechtigungen** | Nur **Buchhalter/Admins** dürfen Rechnungen stellen/stornieren. | **Laravel Policies** (z. B. `InvoicePolicy`). |
|
||||||
|
| **Zwei-Faktor-Authentifizierung** | Zusätzliche Sicherheit für Rechnungsfreigaben. | **`laravel-fortify`** oder **`laravel-2fa`**. |
|
||||||
|
| **IP- und Zeitstempel-Logging** | Protokolliert **wer, wann und von wo** Rechnungen bearbeitet wurden. | **Middleware für Request-Logging**. |
|
||||||
|
| **Datenverschlüsselung** | Sensible Rechnungsdaten (z. B. Kundendaten) werden **verschlüsselt** gespeichert. | **Laravel Encryption** (`Illuminate\Support\Facades\Crypt`) oder **`spatie/laravel-encryption`**. |
|
||||||
|
|
||||||
|
---
|
||||||
|
### **D. Prüfung und Dokumentation**
|
||||||
|
| **Funktion** | **Beschreibung** | **Technische Umsetzung** |
|
||||||
|
|----------------------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Automatische Prüfprotokolle** | Generiert **Prüfberichte** für das Finanzamt (z. B. monatliche Zusammenfassungen). | **Laravel Reports** (z. B. mit `laravel-excel`). |
|
||||||
|
| **Verfahrensdokumentation** | Dokumentiert **wie Rechnungen erstellt/archiviert** werden (Pflicht nach GoBD). | **Markdown-Dokumentation** im Projekt + **automatische API-Docs** (z. B. mit Swagger). |
|
||||||
|
| **Revisionssichere Protokolle** | Alle Änderungen werden **unveränderlich** protokolliert. | **Datenbank-Trigger** oder **Laravel Events**. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Code-Beispiele für GoBD-konforme Funktionen**
|
||||||
|
### **A. Rechnungsmodell mit GoBD-Logik**
|
||||||
|
```php
|
||||||
|
// app/Models/Invoice.php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use OwenIt\Auditing\Auditable;
|
||||||
|
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class Invoice extends Model implements AuditableContract
|
||||||
|
{
|
||||||
|
use Auditable; // Für Audit Logs
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'date' => 'date',
|
||||||
|
'billing_address' => 'array',
|
||||||
|
'shipping_address' => 'array',
|
||||||
|
'metadata' => 'array', // Zusätzliche Metadaten (z. B. für GoBD)
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $auditInclude = [
|
||||||
|
'number',
|
||||||
|
'date',
|
||||||
|
'status',
|
||||||
|
'total',
|
||||||
|
'customer_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert eine GoBD-konforme Rechnungsnummer.
|
||||||
|
*/
|
||||||
|
public static function generateNumber(): string
|
||||||
|
{
|
||||||
|
$year = now()->format('Y');
|
||||||
|
$prefix = 'RE'; // Präfix für Rechnungen
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($year, $prefix) {
|
||||||
|
$lastInvoice = self::where('number', 'like', "{$prefix}-{$year}-%")
|
||||||
|
->lockForUpdate() // Sperrt die Zeilen für andere Transaktionen
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$nextNumber = $lastInvoice ? (int) substr($lastInvoice->number, -3) + 1 : 1;
|
||||||
|
return sprintf('%s-%s-%03d', $prefix, $year, $nextNumber);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt die Rechnung (vergibt Nummer und macht sie unveränderlich).
|
||||||
|
*/
|
||||||
|
public function issue(): void
|
||||||
|
{
|
||||||
|
if ($this->status !== 'draft') {
|
||||||
|
throw new \Exception('Nur Entwürfe können gestellt werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () {
|
||||||
|
$this->update([
|
||||||
|
'number' => self::generateNumber(),
|
||||||
|
'date' => now(),
|
||||||
|
'status' => 'issued',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// PDF/XML generieren und speichern (GoBD-Pflicht!)
|
||||||
|
$this->generateAndStorePdf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert ein ZUGFeRD-konformes PDF/XML und speichert es.
|
||||||
|
*/
|
||||||
|
protected function generateAndStorePdf(): void
|
||||||
|
{
|
||||||
|
$pdfService = new \App\Services\ZugferdPdfService();
|
||||||
|
$pdfPath = $pdfService->generate($this);
|
||||||
|
|
||||||
|
// Speichere PDF in unveränderlichem Storage (z. B. S3 mit Versionierung)
|
||||||
|
Storage::disk('invoices')->put(
|
||||||
|
"{$this->number}.pdf",
|
||||||
|
file_get_contents($pdfPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optional: XML separat speichern (für maschinelle Verarbeitung)
|
||||||
|
$xml = $pdfService->getXml($this);
|
||||||
|
Storage::disk('invoices')->put(
|
||||||
|
"{$this->number}.xml",
|
||||||
|
$xml
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pfad in der Datenbank speichern (für spätere Referenz)
|
||||||
|
$this->update([
|
||||||
|
'pdf_path' => "invoices/{$this->number}.pdf",
|
||||||
|
'xml_path' => "invoices/{$this->number}.xml",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storniert die Rechnung (erstellt eine Gutschrift).
|
||||||
|
*/
|
||||||
|
public function cancel(string $reason): void
|
||||||
|
{
|
||||||
|
if ($this->status === 'cancelled') {
|
||||||
|
throw new \Exception('Rechnung ist bereits storniert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($reason) {
|
||||||
|
$this->update([
|
||||||
|
'status' => 'cancelled',
|
||||||
|
'cancelled_at' => now(),
|
||||||
|
'cancel_reason' => $reason,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Erstelle eine Gutschrift (Credit Note)
|
||||||
|
$creditNote = $this->replicate();
|
||||||
|
$creditNote->number = 'CN-' . substr($this->number, 3); // CN-2023-001
|
||||||
|
$creditNote->status = 'issued';
|
||||||
|
$creditNote->type = 'credit_note';
|
||||||
|
$creditNote->total = -$this->total; // Negativer Betrag
|
||||||
|
$creditNote->save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **B. Audit-Logging mit `laravel-auditing`**
|
||||||
|
1. **Paket installieren**:
|
||||||
|
```bash
|
||||||
|
composer require owen-it/laravel-auditing
|
||||||
|
php artisan vendor:publish --provider="OwenIt\Auditing\AuditingServiceProvider"
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Model anpassen** (bereits im Beispiel oben enthalten).
|
||||||
|
|
||||||
|
3. **Audit-Logs abfragen**:
|
||||||
|
```php
|
||||||
|
// Alle Änderungen einer Rechnung abrufen
|
||||||
|
$audits = $invoice->audits()
|
||||||
|
->with('user') // Wer hat geändert?
|
||||||
|
->latest()
|
||||||
|
->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **C. ZUGFeRD-PDF-Generierung**
|
||||||
|
```php
|
||||||
|
// app/Services/ZugferdPdfService.php
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use ZUGFeRD\ZUGFeRDDocument;
|
||||||
|
|
||||||
|
class ZugferdPdfService
|
||||||
|
{
|
||||||
|
public function generate(Invoice $invoice): string
|
||||||
|
{
|
||||||
|
$document = new ZUGFeRDDocument();
|
||||||
|
$document->setNumber($invoice->number);
|
||||||
|
$document->setDate($invoice->date);
|
||||||
|
$document->setSeller([
|
||||||
|
'name' => config('app.name'),
|
||||||
|
'street' => config('app.address.street'),
|
||||||
|
'zip' => config('app.address.zip'),
|
||||||
|
'city' => config('app.address.city'),
|
||||||
|
'country' => config('app.address.country'),
|
||||||
|
'vat_id' => config('app.vat_id'),
|
||||||
|
]);
|
||||||
|
$document->setBuyer([
|
||||||
|
'name' => $invoice->customer->full_name,
|
||||||
|
'street' => $invoice->billing_address['street'],
|
||||||
|
'zip' => $invoice->billing_address['postal_code'],
|
||||||
|
'city' => $invoice->billing_address['city'],
|
||||||
|
'country' => $invoice->billing_address['country'],
|
||||||
|
'vat_id' => $invoice->customer->vat_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($invoice->items as $item) {
|
||||||
|
$document->addItem([
|
||||||
|
'name' => $item->description,
|
||||||
|
'quantity' => $item->quantity,
|
||||||
|
'price' => $item->unit_price,
|
||||||
|
'vat_rate' => $item->vat_rate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdfPath = tempnam(sys_get_temp_dir(), 'zugferd_') . '.pdf';
|
||||||
|
$document->render($pdfPath);
|
||||||
|
|
||||||
|
return $pdfPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getXml(Invoice $invoice): string
|
||||||
|
{
|
||||||
|
// Hier würde die XML-Generierung erfolgen (vereinfacht)
|
||||||
|
return '<invoice>' . $invoice->toXml() . '</invoice>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **D. VIES-API für USt-ID-Prüfung**
|
||||||
|
```php
|
||||||
|
// app/Services/ViesService.php
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class ViesService
|
||||||
|
{
|
||||||
|
public function validateVatId(string $countryCode, string $vatId): bool
|
||||||
|
{
|
||||||
|
$response = Http::get("http://ec.europa.eu/taxation_customs/vies/viesquer.do", [
|
||||||
|
'countryCode' => $countryCode,
|
||||||
|
'vatNumber' => $vatId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = $response->body();
|
||||||
|
return str_contains($body, 'VAT number is valid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung im Model/Controller**:
|
||||||
|
```php
|
||||||
|
// Im Customer-Model oder Invoice-Controller
|
||||||
|
$isValid = (new ViesService())->validateVatId(
|
||||||
|
substr($invoice->customer->vat_id, 0, 2), // Ländercode (z. B. "DE")
|
||||||
|
substr($invoice->customer->vat_id, 2) // USt-ID ohne Ländercode
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **E. Automatische Backups mit `spatie/laravel-backup`**
|
||||||
|
1. **Paket installieren**:
|
||||||
|
```bash
|
||||||
|
composer require spatie/laravel-backup
|
||||||
|
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
|
||||||
|
php artisan backup:install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Konfiguration** (`config/backup.php`):
|
||||||
|
```php
|
||||||
|
'disks' => [
|
||||||
|
'invoices', // Speichert Backups der Rechnungen
|
||||||
|
],
|
||||||
|
'monitor_backups' => [
|
||||||
|
[
|
||||||
|
'name' => 'invoices',
|
||||||
|
'disks' => ['invoices'],
|
||||||
|
'health_checks' => [
|
||||||
|
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaxAgeInDays::class => 30,
|
||||||
|
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaxStorageInMb::class => 5000,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Backup-Job einrichten** (`app/Console/Kernel.php`):
|
||||||
|
```php
|
||||||
|
protected function schedule(Schedule $schedule)
|
||||||
|
{
|
||||||
|
$schedule->command('backup:run --only-db --disable-notifications')->dailyAt('3:00');
|
||||||
|
$schedule->command('backup:clean')->dailyAt('4:00');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **F. GoBD-konforme Speicherung (Storage-Konfiguration)**
|
||||||
|
```php
|
||||||
|
// config/filesystems.php
|
||||||
|
'disks' => [
|
||||||
|
'invoices' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_INVOICES_BUCKET'),
|
||||||
|
'visibility' => 'private',
|
||||||
|
'options' => [
|
||||||
|
'Versioning' => 'Enabled', // Wichtig für GoBD!
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **G. Policies für Berechtigungen**
|
||||||
|
```php
|
||||||
|
// app/Policies/InvoicePolicy.php
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class InvoicePolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
public function create(User $user)
|
||||||
|
{
|
||||||
|
return $user->can('create invoices');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function issue(User $user, Invoice $invoice)
|
||||||
|
{
|
||||||
|
return $user->can('issue invoices') && $invoice->status === 'draft';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(User $user, Invoice $invoice)
|
||||||
|
{
|
||||||
|
return $user->can('cancel invoices') && $invoice->status !== 'cancelled';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, Invoice $invoice)
|
||||||
|
{
|
||||||
|
return false; // Rechnungen dürfen nie gelöscht werden (GoBD)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Registrierung im `AuthServiceProvider`**:
|
||||||
|
```php
|
||||||
|
// app/Providers/AuthServiceProvider.php
|
||||||
|
protected $policies = [
|
||||||
|
Invoice::class => InvoicePolicy::class,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## **4. Frontend-Integration (Vue.js + Inertia)**
|
||||||
|
### **A. Rechnungsformular mit GoBD-Hinweisen**
|
||||||
|
```vue
|
||||||
|
<!-- resources/js/Pages/Invoices/Create.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>{{ invoice.id ? 'Rechnung bearbeiten' : 'Neue Rechnung erstellen' }}</h1>
|
||||||
|
<form @submit.prevent="save">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kunde</label>
|
||||||
|
<select v-model="form.customer_id" required>
|
||||||
|
<option v-for="customer in customers" :value="customer.id" :key="customer.id">
|
||||||
|
{{ customer.full_name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Rechnungsdatum</label>
|
||||||
|
<input type="date" v-model="form.date" :readonly="invoice.status !== 'draft'" />
|
||||||
|
<small v-if="invoice.status !== 'draft'">
|
||||||
|
Das Datum kann nach dem Stellen der Rechnung nicht mehr geändert werden (GoBD).
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Rechnungsnummer</label>
|
||||||
|
<input type="text" v-model="form.number" readonly />
|
||||||
|
<small>
|
||||||
|
Die Rechnungsnummer wird automatisch beim Stellen der Rechnung vergeben (GoBD-konform).
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<!-- Weitere Felder für Positionen, Steuern etc. -->
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" v-if="invoice.status === 'draft'">Als Entwurf speichern</button>
|
||||||
|
<button type="button" @click="issue" v-if="invoice.status === 'draft'">
|
||||||
|
Rechnung stellen (GoBD)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useForm } from '@inertiajs/vue3';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
invoice: Object,
|
||||||
|
customers: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
customer_id: props.invoice?.customer_id || '',
|
||||||
|
date: props.invoice?.date || new Date().toISOString().split('T')[0],
|
||||||
|
number: props.invoice?.number || 'Entwurf (noch keine Nummer)',
|
||||||
|
// ... weitere Felder
|
||||||
|
});
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (props.invoice?.id) {
|
||||||
|
form.put(`/api/invoices/${props.invoice.id}`);
|
||||||
|
} else {
|
||||||
|
form.post('/api/invoices');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const issue = () => {
|
||||||
|
if (confirm('Soll die Rechnung gestellt werden? Danach kann sie nicht mehr bearbeitet werden (GoBD).')) {
|
||||||
|
form.post(`/api/invoices/${props.invoice.id}/issue`, {}, {
|
||||||
|
onSuccess: () => alert('Rechnung wurde gestellt!'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### **B. Rechnungsliste mit GoBD-Filtern**
|
||||||
|
```vue
|
||||||
|
<!-- resources/js/Pages/Invoices/Index.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Rechnungen</h1>
|
||||||
|
<div class="filters">
|
||||||
|
<select v-model="statusFilter">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<option value="draft">Entwürfe</option>
|
||||||
|
<option value="issued">Gestellt</option>
|
||||||
|
<option value="sent">Versendet</option>
|
||||||
|
<option value="paid">Bezahlt</option>
|
||||||
|
<option value="cancelled">Storniert</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" v-model="searchQuery" placeholder="Suche..." />
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nummer</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Kunde</th>
|
||||||
|
<th>Betrag</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="invoice in filteredInvoices" :key="invoice.id">
|
||||||
|
<td>{{ invoice.number || 'Entwurf' }}</td>
|
||||||
|
<td>{{ invoice.date || '—' }}</td>
|
||||||
|
<td>{{ invoice.customer.full_name }}</td>
|
||||||
|
<td>{{ invoice.total }} €</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge" :class="statusBadgeClass(invoice.status)">
|
||||||
|
{{ invoice.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button v-if="invoice.status === 'draft'" @click="edit(invoice.id)">Bearbeiten</button>
|
||||||
|
<button v-if="invoice.status === 'draft'" @click="issue(invoice.id)">
|
||||||
|
Rechnung stellen
|
||||||
|
</button>
|
||||||
|
<a v-if="invoice.status !== 'draft'" :href="`/invoices/${invoice.id}/pdf`" target="_blank">
|
||||||
|
PDF
|
||||||
|
</a>
|
||||||
|
<button v-if="invoice.status !== 'draft' && invoice.status !== 'cancelled'"
|
||||||
|
@click="cancel(invoice.id)">
|
||||||
|
Stornieren
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const invoices = ref([]);
|
||||||
|
const statusFilter = ref('');
|
||||||
|
const searchQuery = ref('');
|
||||||
|
|
||||||
|
const fetchInvoices = async () => {
|
||||||
|
const response = await axios.get('/api/invoices');
|
||||||
|
invoices.value = response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredInvoices = computed(() => {
|
||||||
|
return invoices.value
|
||||||
|
.filter(invoice => !statusFilter.value || invoice.status === statusFilter.value)
|
||||||
|
.filter(invoice =>
|
||||||
|
invoice.number?.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||||
|
invoice.customer.full_name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusBadgeClass = (status) => ({
|
||||||
|
'badge-draft': status === 'draft',
|
||||||
|
'badge-issued': status === 'issued',
|
||||||
|
'badge-sent': status === 'sent',
|
||||||
|
'badge-paid': status === 'paid',
|
||||||
|
'badge-cancelled': status === 'cancelled',
|
||||||
|
});
|
||||||
|
|
||||||
|
const issue = async (id) => {
|
||||||
|
await axios.post(`/api/invoices/${id}/issue`);
|
||||||
|
await fetchInvoices();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = async (id) => {
|
||||||
|
const reason = prompt('Grund für die Stornierung (GoBD-Pflicht):');
|
||||||
|
if (reason) {
|
||||||
|
await axios.post(`/api/invoices/${id}/cancel`, { reason });
|
||||||
|
await fetchInvoices();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchInvoices();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.badge {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.badge-draft { background: #6c757d; color: white; }
|
||||||
|
.badge-issued { background: #0d6efd; color: white; }
|
||||||
|
.badge-sent { background: #198754; color: white; }
|
||||||
|
.badge-paid { background: #198754; color: white; }
|
||||||
|
.badge-cancelled { background: #dc3545; color: white; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## **5. GoBD-konforme Dokumentation**
|
||||||
|
Erstelle eine **Verfahrensdokumentation** für dein Rechnungssystem (Pflicht nach GoBD!). Beispiel:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Verfahrensdokumentation: Rechnungsverwaltung (GoBD-konform)
|
||||||
|
|
||||||
|
## 1. Systembeschreibung
|
||||||
|
- **Zweck**: Erstellung, Verwaltung und Archivierung von Rechnungen gemäß §14 UStG und GoBD.
|
||||||
|
- **Technologie**: Laravel (Backend), Vue.js (Frontend), SQLite/MySQL (Datenbank), S3 (Speicher).
|
||||||
|
|
||||||
|
## 2. Prozesse
|
||||||
|
### 2.1 Rechnungserstellung
|
||||||
|
1. Nutzer erstellt **Entwurf** (Status: `draft`).
|
||||||
|
2. System speichert Entwurf **ohne Rechnungsnummer**.
|
||||||
|
3. Bei "Rechnung stellen":
|
||||||
|
- System vergibt **fortlaufende Nummer** (z. B. `RE-2023-001`).
|
||||||
|
- Status wird auf `issued` gesetzt.
|
||||||
|
- **PDF/XML** wird generiert und **unveränderlich** gespeichert (S3 mit Versionierung).
|
||||||
|
- **Audit-Log** wird erstellt.
|
||||||
|
|
||||||
|
### 2.2 Rechnungsstornierung
|
||||||
|
1. Nutzer wählt "Stornieren" und gibt **Grund** an (GoBD-Pflicht!).
|
||||||
|
2. System:
|
||||||
|
- Setzt Status auf `cancelled`.
|
||||||
|
- Erstellt **Gutschrift** (Credit Note) mit negativem Betrag.
|
||||||
|
- Speichert Stornierungsgrund im **Audit-Log**.
|
||||||
|
|
||||||
|
### 2.3 Archivierung
|
||||||
|
- **Automatische Backups** täglich um 3:00 Uhr (`spatie/laravel-backup`).
|
||||||
|
- **Aufbewahrungsfrist**: 10 Jahre (S3 Lifecycle Policy).
|
||||||
|
- **Speicherformat**: PDF + XML (ZUGFeRD-konform).
|
||||||
|
|
||||||
|
## 3. Technische Maßnahmen
|
||||||
|
| Maßnahme | Umsetzung |
|
||||||
|
|------------------------------|---------------------------------------------------------------------------------------------|
|
||||||
|
| Unveränderbarkeit | Rechnungen nach Stellung als `readonly` markiert. Datenbank-Trigger verhindern Änderungen. |
|
||||||
|
| Nachvollziehbarkeit | `laravel-auditing` protokolliert alle Änderungen (wer, wann, was). |
|
||||||
|
| Vollständigkeit | Fortlaufende Nummernvergabe ohne Lücken (Datenbank-Transaktionen). |
|
||||||
|
| Aufbewahrung | Tägliche Backups auf S3 mit Versionierung (10 Jahre Aufbewahrung). |
|
||||||
|
| Zugriffsschutz | Laravel Policies schränken Berechtigungen ein (z. B. nur Buchhalter dürfen stornieren). |
|
||||||
|
| Prüfbarkeit | Audit-Logs und Revisionsprotokolle sind über Admin-Oberfläche einsehbar. |
|
||||||
|
|
||||||
|
## 4. Verantwortlichkeiten
|
||||||
|
| Rolle | Verantwortung |
|
||||||
|
|---------------------|-------------------------------------------------------------------------------------------------|
|
||||||
|
| Buchhaltung | Erstellung, Stellung und Stornierung von Rechnungen. |
|
||||||
|
| IT-Administration | Wartung der Systeme, Backups, Zugriffsrechte. |
|
||||||
|
| Geschäftsführung | Freigabe von Prozessen, regelmäßige Prüfung der GoBD-Konformität. |
|
||||||
|
|
||||||
|
## 5. Prüfprotokolle
|
||||||
|
- **Monatlich**: Automatischer Report aller gestellten/stornierten Rechnungen (via `laravel-excel`).
|
||||||
|
- **Jährlich**: Manuelle Prüfung der Archivierung durch Steuerberater.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## **6. Checkliste für GoBD-Konformität**
|
||||||
|
| **Anforderung** | **Umsetzung in deinem System** | **Erledigt?** |
|
||||||
|
|--------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------|
|
||||||
|
| **Nachvollziehbarkeit** | Audit-Logs mit `laravel-auditing` + Revision History. | ☐ |
|
||||||
|
| **Unveränderlichkeit** | Rechnungen nach Stellung als `readonly` + Datenbank-Constraints. | ☐ |
|
||||||
|
| **Vollständigkeit** | Fortlaufende Nummernvergabe ohne Lücken. | ☐ |
|
||||||
|
| **Aufbewahrung (10 Jahre)** | Automatische Backups auf S3 mit Versionierung. | ☐ |
|
||||||
|
| **Maschinenlesbare Formate** | ZUGFeRD/XML-Export für jede Rechnung. | ☐ |
|
||||||
|
| **Zugriffsschutz** | Laravel Policies für Berechtigungen. | ☐ |
|
||||||
|
| **Prüfbarkeit** | Audit-Logs und Revisionsprotokolle. | ☐ |
|
||||||
|
| **Verfahrensdokumentation** | Markdown-Dokumentation im Projekt (siehe oben). | ☐ |
|
||||||
|
| **VIES-Prüfung** | Automatische Validierung von USt-IDs über die VIES-API. | ☐ |
|
||||||
|
| **Echtzeit-Protokollierung** | Laravel Echo für Benachrichtigungen bei Änderungen. | ☐ |
|
||||||
|
|
||||||
|
---
|
||||||
|
## **7. Tools für die GoBD-Umsetzung**
|
||||||
|
| **Tool** | **Zweck** | **Link** |
|
||||||
|
|------------------------------|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
|
||||||
|
| **laravel-auditing** | Protokolliert alle Änderungen an Modellen (GoBD-Pflicht!). | [https://github.com/owen-it/laravel-auditing](https://github.com/owen-it/laravel-auditing) |
|
||||||
|
| **spatie/laravel-backup** | Automatische Backups für 10 Jahre Aufbewahrung. | [https://spatie.be/docs/laravel-backup](https://spatie.be/docs/laravel-backup) |
|
||||||
|
| **zugferd/php** | Generiert GoBD-konforme ZUGFeRD-Rechnungen (PDF + XML). | [https://github.com/zugferd/php](https://github.com/zugferd/php) |
|
||||||
|
| **laravel-excel** | Exportiert Rechnungslisten für Prüfprotokolle. | [https://laravel-excel.com/](https://laravel-excel.com/) |
|
||||||
|
| **laravel-nova** | Admin-Oberfläche für Prüfung/Audit-Logs. | [https://nova.laravel.com/](https://nova.laravel.com/) |
|
||||||
|
| **AWS S3 + Versioning** | Unveränderliche Speicherung von Rechnungs-PDFs. | [https://aws.amazon.com/s3/](https://aws.amazon.com/s3/) |
|
||||||
|
| **Laravel Echo + Pusher** | Echtzeit-Benachrichtigungen bei Rechnungsänderungen. | [https://laravel.com/docs/broadcasting](https://laravel.com/docs/broadcasting) |
|
||||||
|
|
||||||
|
---
|
||||||
|
## **8. Zusammenfassung: Was du jetzt tun solltest**
|
||||||
|
1. **Datenbank-Struktur anpassen**:
|
||||||
|
- `number` als `string` (nicht `integer`!) speichern.
|
||||||
|
- Status-Feld (`draft`, `issued`, etc.) und Audit-Logs hinzufügen.
|
||||||
|
|
||||||
|
2. **Rechnungsmodell erweitern**:
|
||||||
|
- `generateNumber()` mit Transaktionen.
|
||||||
|
- `issue()`-Methode für das "Stellen" von Rechnungen.
|
||||||
|
- ZUGFeRD-PDF/XML-Generierung.
|
||||||
|
|
||||||
|
3. **GoBD-konforme Speicherung einrichten**:
|
||||||
|
- S3 mit Versionierung für Rechnungs-PDFs.
|
||||||
|
- Automatische Backups mit `spatie/laravel-backup`.
|
||||||
|
|
||||||
|
4. **Frontend anpassen**:
|
||||||
|
- Rechnungsformular mit GoBD-Hinweisen.
|
||||||
|
- Status-Filter für die Liste.
|
||||||
|
|
||||||
|
5. **Dokumentation erstellen**:
|
||||||
|
- Verfahrensdokumentation (Markdown) im Projekt ablegen.
|
||||||
|
|
||||||
|
6. **Tests durchführen**:
|
||||||
|
- Prüfe, ob Rechnungen **nicht bearbeitbar** sind nach dem Stellen.
|
||||||
|
- Teste **Nummernvergabe ohne Lücken**.
|
||||||
|
- Überprüfe **Backups und Wiederherstellung**.
|
||||||
|
|
||||||
|
---
|
||||||
|
## **Frage an dich**
|
||||||
|
Möchtest du:
|
||||||
|
1. Ein **komplettes Beispiel für die S3-Konfiguration** (mit Versionierung)?
|
||||||
|
2. Hilfe bei der **Einrichtung von Laravel Auditing** für detaillierte Protokolle?
|
||||||
|
3. Ein **Beispiel für die monatlichen Prüfberichte** (mit Laravel Excel)?
|
||||||
Reference in New Issue
Block a user