Stornorechnung: Fix überdeckte Artikelzeile + Regenerate-Command

Der ACHTUNG-Hinweiskasten hat wegen fixer 15mm-Höhe des Titel-Blocks
den Tabellenkopf und die erste Artikelzeile überdeckt (z.B. Artikel
5010400 auf den Stornos zu Bestellung 56965/56966). Titel-Block wächst
jetzt mit dem Inhalt.

Neuer Command sales:regenerate-cancellation-pdf erzeugt die PDFs
bestehender Stornorechnungen neu (gleiche Nummer/Pfade, keine
Punktebuchung, keine Mails) – per Rechnungs-ID oder mit --order per
Bestell-ID.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-14 14:19:28 +00:00
parent 8e09aaefe8
commit 1e7ae92d0c
4 changed files with 271 additions and 2 deletions

View file

@ -0,0 +1,117 @@
<?php
namespace App\Console\Commands;
use App\Models\ShoppingOrder;
use App\Models\UserInvoice;
use App\Repositories\InvoiceRepository;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
class RegenerateCancellationInvoicePdf extends Command
{
/**
* Erzeugt die PDF-Dateien bestehender Stornorechnungen neu (z. B. nach Template-Fixes).
* Nummer, Datum und Dateipfade der Stornorechnung bleiben unverändert – es wird keine
* neue Rechnungsnummer vergeben und es werden keine Punkte gebucht.
*/
protected $signature = 'sales:regenerate-cancellation-pdf
{invoices* : IDs der Stornorechnungen (user_invoices.id)}
{--order : Die übergebenen IDs sind ShoppingOrder-IDs statt Rechnungs-IDs}
{--force : Ohne Rückfrage neu erzeugen}';
protected $description = 'Storno-PDFs (Rechnung + Lieferschein) bestehender Stornorechnungen neu erzeugen';
public function handle(): int
{
$invoices = $this->resolveInvoices();
if ($invoices === null) {
return self::FAILURE;
}
$this->table(
['Storno-Rechnung', 'Storno-Nr.', 'Bestellung', 'Datum', 'Datei'],
$invoices->map(fn (UserInvoice $invoice) => [
$invoice->id,
$invoice->full_number,
$invoice->shopping_order_id,
$invoice->date,
$invoice->dir.$invoice->filename,
])
);
if (! $this->option('force') && ! $this->confirm("Wirklich {$invoices->count()} Storno-PDF(s) neu erzeugen? Die bestehenden Dateien werden überschrieben.")) {
return self::SUCCESS;
}
$errors = 0;
foreach ($invoices as $invoice) {
try {
(new InvoiceRepository($invoice->shopping_order))->regenerateCancellationPDF($invoice);
$this->info("Stornorechnung #{$invoice->id} ({$invoice->full_number}) neu erzeugt: ".Storage::disk('public')->path($invoice->dir.$invoice->filename));
} catch (\Throwable $e) {
$errors++;
$this->error("Stornorechnung #{$invoice->id}: {$e->getMessage()}");
}
}
return $errors > 0 ? self::FAILURE : self::SUCCESS;
}
/**
* Löst die übergebenen IDs zu gültigen Stornorechnungen auf.
*
* @return Collection<int, UserInvoice>|null null bei Validierungsfehlern
*/
private function resolveInvoices(): ?Collection
{
$invoices = collect();
foreach ($this->argument('invoices') as $id) {
if ($this->option('order')) {
$order = ShoppingOrder::find((int) $id);
if (! $order) {
$this->error("ShoppingOrder {$id} nicht gefunden.");
return null;
}
$invoice = $order->user_cancellation_invoice;
if (! $invoice) {
$this->error("Bestellung {$id} hat keine Stornorechnung.");
return null;
}
} else {
$invoice = UserInvoice::find((int) $id);
if (! $invoice) {
$this->error("Stornorechnung {$id} nicht gefunden.");
return null;
}
}
if (! $invoice->cancellation || $invoice->cancellation_id) {
$this->error("Rechnung #{$invoice->id} ({$invoice->full_number}) ist keine Stornorechnung.");
return null;
}
if (! $invoice->shopping_order) {
$this->error("Stornorechnung #{$invoice->id} hat keine zugehörige Bestellung.");
return null;
}
$invoices->push($invoice);
}
return $invoices;
}
}

View file

@ -16,7 +16,7 @@ use Storage;
class InvoiceRepository extends BaseRepository
{
/** @var \App\Models\ShoppingOrder */
/** @var ShoppingOrder */
protected $model;
private $invoice_date;
@ -356,6 +356,32 @@ class InvoiceRepository extends BaseRepository
});
}
/**
* Erzeugt die PDF-Dateien einer bestehenden Stornorechnung neu (z. B. nach Template-Fixes).
*
* Verwendet Nummer, Datum und Dateipfade der bestehenden Stornorechnung und überschreibt
* die vorhandenen Dateien – es wird keine neue Rechnungsnummer vergeben und es werden
* keine Punkte gebucht.
*/
public function regenerateCancellationPDF(UserInvoice $cancellation_invoice): void
{
$original_invoice = UserInvoice::where('cancellation_id', $cancellation_invoice->id)->first();
if (! $original_invoice) {
throw new \Exception('Keine Originalrechnung zur Stornorechnung #'.$cancellation_invoice->id.' gefunden.');
}
$this->makeCancellationPDF(
$cancellation_invoice->date,
$cancellation_invoice->full_number,
$cancellation_invoice->dir,
$cancellation_invoice->filename,
$cancellation_invoice->delivery_dir,
$cancellation_invoice->delivery_filename,
$original_invoice
);
}
/**
* Erstellt die Storno-PDFs (Rechnung und Lieferschein)
*/