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>
117 lines
3.8 KiB
PHP
117 lines
3.8 KiB
PHP
<?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;
|
||
}
|
||
}
|