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])) { [$vol, $size] = self::parseVariantValue($attribute->value); $sorter = max($sorter, $size); $label->varinat = [ 'key' => $attribute->value, 'id' => $attribute->id, 'vol' => $vol, 'size' => $size, '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])) { [$vol, $size] = self::parseVariantValue($attribute_variant->attribute->value); $sorter = max($sorter, $size); $label->varinat = [ 'key' => $attribute_variant->attribute->value, 'id' => $attribute_variant->attribute->id, 'vol' => $vol, 'size' => $size, '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); } /** * @return array{0: float, 1: float} */ public static function parseVariantValue(?string $value): array { return Attribute::parseVariantValue($value); } }