gruene-seele/app/Repositories/InvoiceRepository.php
Kevin Adametz 9c362f14d9 Verkauf: Stornorechnung — fehlende createCancellation() im InvoiceRepository ergänzt, eigene Storno-Mail
Der SalesController rief InvoiceRepository::createCancellation() auf, die Methode existierte aber nicht — jeder Storno-Versuch endete im 500er. Die Stornorechnung wird jetzt aus demselben Nummernkreis wie die Rechnung erzeugt (pdf.cancellation_invoice, Rechnungs-Briefpapier), als cancellation_invoice am Auftrag gespeichert und der Zähler erhöht.

Optional geht eine Storno-Mail mit angehängter Stornorechnung (MailCancellation, Texte email.cancellation_title/_copy1line) statt der Rechnungs-Mail an den Kunden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:03:35 +00:00

364 lines
15 KiB
PHP

<?php
namespace App\Repositories;
use App\Libraries\InvoicePDF;
use App\Models\Attribute;
use App\Models\ShoppingOrder;
use App\Services\Invoice;
use App\Services\MyPDFMerger;
use App\Services\UserService;
use PDF;
use Storage;
class InvoiceRepository extends BaseRepository
{
private $invoice_date;
private $invoice_number;
private $filename;
private $dir;
private $delivery_dir;
private $delivery_filename;
private $cancellation_date;
private $cancellation_number;
private $cancellation_filename;
private $cancellation_dir;
public function __construct(ShoppingOrder $model)
{
$this->model = $model;
}
public function create($request = [])
{
// need invoice $data
$number = Invoice::getInvoiceNumber();
$this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : $this->model->created_at->format('d.m.Y');
$invoice_send_mail = isset($request['invoice_send_mail']) ? true : false;
$this->invoice_number = Invoice::createInvoiceNumber($number, $this->invoice_date);
$this->dir = Invoice::getInvoiceStorageDir($this->invoice_date);
$this->filename = Invoice::makeInvoiceFilename($this->invoice_number);
$this->delivery_dir = Invoice::getDeliveryStorageDir($this->invoice_date);
$this->delivery_filename = Invoice::makeDeliveryFilename($this->invoice_number);
$this->makePDF();
$invoice_file = [
'filename' => $this->filename,
'dir' => $this->dir,
'disk' => 'public',
'invoice_number' => $this->invoice_number,
'invoice_date' => $this->invoice_date,
];
$delivery_file = [
'filename' => $this->delivery_filename,
'dir' => $this->delivery_dir,
'disk' => 'public',
'number' => $this->invoice_number,
'date' => $this->invoice_date,
];
$this->model->invoice = $invoice_file;
$this->model->delivery = $delivery_file;
$this->model->invoice_number = $this->invoice_number;
$this->model->save();
Invoice::makeNextInvoiceNumber();
if ($invoice_send_mail) {
Invoice::sendInvoiceMail($this->model);
}
return true;
}
public function update($request = [])
{
if (isset($this->model->invoice['filename'])) {
$this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : Invoice::getDate($this->model);
$invoice_send_mail = isset($request['invoice_send_mail']) ? true : false;
$this->invoice_number = Invoice::getNumber($this->model);
$this->dir = Invoice::getDir($this->model);
$this->filename = Invoice::getFilename($this->model);
$this->delivery_dir = Invoice::getDeliveryDir($this->model);
$this->delivery_filename = Invoice::getDeliveryFilename($this->model);
$this->makePDF();
$invoice_file = [
'filename' => $this->filename,
'dir' => $this->dir,
'disk' => 'public',
'invoice_number' => $this->invoice_number,
'invoice_date' => $this->invoice_date,
];
$delivery_file = [
'filename' => $this->delivery_filename,
'dir' => $this->delivery_dir,
'disk' => 'public',
'number' => $this->invoice_number,
'date' => $this->invoice_date,
];
$this->model->invoice = $invoice_file;
$this->model->delivery = $delivery_file;
$this->model->invoice_number = $this->invoice_number;
$this->model->save();
if ($invoice_send_mail) {
Invoice::sendInvoiceMail($this->model);
}
return true;
}
return null;
}
/**
* Stornorechnung zu einer bereits erstellten Rechnung erzeugen (Fix 08.09.2026:
* die Methode wurde vom SalesController aufgerufen, existierte aber nie —
* jeder Storno-Versuch lief in einen 500er).
*
* Die Stornonummer wird aus demselben Zähler wie die Rechnungsnummer gezogen
* (das Modal zeigt "nächste Rechnungsnummer" an) und der Zähler danach erhöht.
*
* @return bool true = Stornorechnung erstellt
*/
public function createCancellation($request = [])
{
// ohne Ursprungsrechnung gibt es nichts zu stornieren
if (! Invoice::isInvoice($this->model)) {
return false;
}
// Doppel-Storno verhindern (der Controller prüft das ebenfalls)
if (Invoice::isCancellationInvoice($this->model)) {
return false;
}
$number = Invoice::getInvoiceNumber();
$this->cancellation_date = isset($request['cancellation_date']) ? $request['cancellation_date'] : now()->format('d.m.Y');
$cancellation_send_mail = isset($request['cancellation_send_mail']) ? true : false;
$this->cancellation_number = Invoice::createInvoiceNumber($number, $this->cancellation_date);
$this->cancellation_dir = Invoice::getCancellationStorageDir($this->cancellation_date);
$this->cancellation_filename = Invoice::makeCancellationFilename($this->cancellation_number);
$this->makeCancellationPDF();
$this->model->cancellation_invoice = [
'filename' => $this->cancellation_filename,
'dir' => $this->cancellation_dir,
'disk' => 'public',
'cancellation_number' => $this->cancellation_number,
'cancellation_date' => $this->cancellation_date,
'invoice_number' => Invoice::getNumber($this->model),
'invoice_date' => Invoice::getDate($this->model),
];
$this->model->save();
Invoice::makeNextInvoiceNumber();
if ($cancellation_send_mail) {
Invoice::sendCancellationMail($this->model);
}
return true;
}
private function makeCancellationPDF()
{
$data = [
'shopping_order' => $this->model,
'cancellation_date' => $this->cancellation_date,
'cancellation_number' => $this->cancellation_number,
'original_invoice_number' => Invoice::getNumber($this->model),
'original_invoice_date' => Invoice::getDate($this->model),
];
if ($this->model->auth_user_id) {
UserService::checkUserTaxShippingCountry($this->model->auth_user, $this->model->country_id);
$data = array_merge($data, UserService::getYardInfo());
}
if (! Storage::disk('public')->exists($this->cancellation_dir)) {
Storage::disk('public')->makeDirectory($this->cancellation_dir);
}
$path = Storage::disk('public')->path('');
$pdf_file = new InvoicePDF('pdf.cancellation_invoice');
$pdf_file->create($data, $this->cancellation_filename, 'save', $path.$this->cancellation_dir);
$pdfMerger = new MyPDFMerger;
$pdfMerger->addPDF($path.$this->cancellation_dir.$this->cancellation_filename);
// eigenes Storno-Briefpapier gibt es nicht, daher das Rechnungs-Template
$file = $pdfMerger->myMerge('string', $this->cancellation_filename, 'template_invoice_de');
Storage::disk('public')->put($this->cancellation_dir.$this->cancellation_filename, $file);
}
/**
* Altdaten-Reparatur an EINER Stelle (Review 20.07.2026): Rechnung und/oder
* Lieferschein fehlen auf der Platte, obwohl die DB-Metadaten existieren
* (z. B. Storage-Umzug). Regeneriert beide Dateien mit bestehender Nummer.
* Vorher war der Check dreifach kopiert (MailLogistic 2x, batchDocuments) und
* prüfte im Sammel-Druck nur den Lieferschein — eine fehlende Rechnungs-PDF
* ließ den ganzen Batch mit einer Exception platzen.
*
* @return bool true = beide Dateien liegen (ggf. nach Regeneration) auf der Platte
*/
public function ensureFilesOnDisk(): bool
{
if (! Invoice::isInvoice($this->model)) {
return false;
}
$invoicePath = Invoice::getDownloadPath($this->model);
$deliveryPath = Invoice::getDownloadPathDelivery($this->model);
if (! Storage::disk('public')->exists($invoicePath) || ! Storage::disk('public')->exists($deliveryPath)) {
$this->update([]);
}
return Storage::disk('public')->exists($invoicePath)
&& Storage::disk('public')->exists($deliveryPath);
}
private function makePDF()
{
$data = [
'shopping_order' => $this->model,
'invoice_date' => $this->invoice_date,
'invoice_number' => $this->invoice_number,
];
if ($this->model->auth_user_id) {
UserService::checkUserTaxShippingCountry($this->model->auth_user, $this->model->country_id);
$data = array_merge($data, UserService::getYardInfo());
}
$data['delivery_items'] = $this->prepairForDelivery();
if (! Storage::disk('public')->exists($this->dir)) {
Storage::disk('public')->makeDirectory($this->dir); // creates directory
}
if (! Storage::disk('public')->exists($this->delivery_dir)) {
Storage::disk('public')->makeDirectory($this->delivery_dir); // creates directory
}
$path = Storage::disk('public')->path('');
$pdf_file = new InvoicePDF('pdf.invoice');
$pdf_file->create($data, $this->filename, 'save', $path.$this->dir);
$pdfMerger = new MyPDFMerger;
$pdfMerger->addPDF($path.$this->dir.$this->filename);
$file = $pdfMerger->myMerge('string', $this->filename, 'template_invoice_de');
Storage::disk('public')->put($this->dir.$this->filename, $file);
$pdf_file = new InvoicePDF('pdf.delivery');
$pdf_file->create($data, $this->delivery_filename, 'save', $path.$this->delivery_dir);
$pdfMerger = new MyPDFMerger;
$pdfMerger->addPDF($path.$this->delivery_dir.$this->delivery_filename);
$file = $pdfMerger->myMerge('string', $this->delivery_filename, 'template_delivery_de');
Storage::disk('public')->put($this->delivery_dir.$this->delivery_filename, $file);
}
// sort white label products
private function prepairForDelivery()
{
$shopping_order = $this->model;
$wl_items = [];
$items = [];
$counter = 1;
$count = 10000;
foreach ($shopping_order->shopping_order_items as $shopping_order_item) {
$labels = [];
$sorter = 0;
if ($shopping_order_item->product->whitelabel) {
// get white label default etikett (s)
// ist ausschlaggebend für die etikettierung, ob 1 oder mehr etiketten
if ($shopping_order_item->product->whitelabel_images) {
foreach ($shopping_order_item->product->whitelabel_images as $whitelabel_image) {
$labels[] = $whitelabel_image;
}
}
// has white label user etikett
if ($shopping_order->auth_user) {
$whitelabel_product = $shopping_order->auth_user->whitelabel_products()->where('product_id', '=', $shopping_order_item->product->id)->first();
if ($whitelabel_product && $whitelabel_product->whitelabel_images) {
foreach ($whitelabel_product->whitelabel_images as $whitelabel_image) {
if ($whitelabel_image->attributes && is_array($whitelabel_image->attributes)) {
foreach ($labels as $key => $label) {
foreach ($whitelabel_image->attributes as $attribute) {
if (in_array($attribute, $label->attributes)) {
// found and overwrite
if (! $this->model->user_white_label) {
$this->model->user_white_label = true;
$this->model->save();
}
$labels[$key] = $whitelabel_image;
}
}
}
}
}
}
}
foreach ($labels as $key => $label) {
// label hat attribue
$varinats = [];
if ($label->attributes && is_array($label->attributes)) {
foreach ($label->attributes as $attribute) {
if ($attribute = Attribute::find($attribute)) {
if (! isset($varinats[$attribute->value])) {
$temp = explode('#', $attribute->value);
$sorter = $temp[1] > $sorter ? $temp[1] : $sorter;
$label->varinat = [
'key' => $attribute->value,
'id' => $attribute->id,
'vol' => (float) $temp[0],
'size' => (float) $temp[1],
'name' => $label->product_id ? 'Standard' : $attribute->name,
];
}
}
}
} else {
// sonst auf standard zurückgreifen
if ($shopping_order_item->product->attribute_variants) {
foreach ($shopping_order_item->product->attribute_variants as $attribute_variant) {
if (! isset($varinats[$attribute_variant->attribute->value])) {
$temp = explode('#', $attribute_variant->attribute->value);
$sorter = $temp[1] > $sorter ? $temp[1] : $sorter;
$label->varinat = [
'key' => $attribute_variant->attribute->value,
'id' => $attribute_variant->attribute->id,
'vol' => (float) $temp[0],
'size' => (float) $temp[1],
'name' => 'Standard',
];
}
}
}
}
}
// sorten of white label products
$sorter = intval((float) $sorter * 100 + $counter++);
$shopping_order_item->labels = $labels;
$wl_items[$sorter] = $shopping_order_item;
} else {
$items[$count++] = $shopping_order_item;
}
}
// kleine zuerst
ksort($wl_items);
return array_merge($wl_items, $items);
}
}