From 2690451f0b78f0682ae0644c1a1dce46c91f2fc0 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 9 Dec 2025 12:23:53 +0100 Subject: [PATCH] Add shipping note import to manual invoice --- .../WarehouseShippingNoteController.php | 179 ++++++++++++++++++ .../js/pages/ManualInvoice/ManualInvoice.js | 115 ++++++++++- .../WarehouseShippingNote.js | 31 +++ 3 files changed, 322 insertions(+), 3 deletions(-) diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteController.php b/application/WarehouseShippingNote/WarehouseShippingNoteController.php index 0d0d90c89..02b07ee85 100644 --- a/application/WarehouseShippingNote/WarehouseShippingNoteController.php +++ b/application/WarehouseShippingNote/WarehouseShippingNoteController.php @@ -34,6 +34,14 @@ class WarehouseShippingNoteController extends TTCrud { 'delete' => 'Lieferschein wurde gelöscht', 'noChanges' => 'Keine Änderungen vorgenommen']; protected array $permissionCheck = ['WarehouseUser']; + protected array $additionalActions = [ + [ + 'key' => 'createManualInvoice', + 'title' => 'Rechnung erstellen', + 'class' => 'fas fa-file-invoice text-primary', + 'condition' => ['status' => 'accepted'] + ] + ]; //@formatter:on protected function prepareCrudConfig() { @@ -109,6 +117,177 @@ class WarehouseShippingNoteController extends TTCrud { )); } + protected function getShippingNoteForInvoiceAction() { + $id = $this->request->id; + + // Get shipping note + $shippingNote = WarehouseShippingNoteModel::get($id); + if (!$shippingNote) { + self::returnJson(['success' => false, 'message' => 'Lieferschein nicht gefunden']); + return; + } + + // Get billing address info + $billingAddress = null; + if ($shippingNote->billingAddressId) { + $billingAddress = Address::getOne($shippingNote->billingAddressId); + } + + // Determine price type ONCE (not in loop for performance) + $priceType = 'Verkauf'; + if ($shippingNote->billingAddressId) { + $addressPriceType = AddressPriceTypeModel::getFirst(['address_id' => $shippingNote->billingAddressId]); + if ($addressPriceType) { + $warehousePriceType = WarehouseArticlePriceTypeModel::get($addressPriceType->priceType_id); + if ($warehousePriceType) { + $priceType = $warehousePriceType->title; + } + } + } + + // Decode and enrich positions + $positions = json_decode($shippingNote->positions, true); + if (!is_array($positions)) { + $positions = []; + } + + $enrichedPositions = []; + + foreach ($positions as $position) { + if (isset($position['article'])) { + // Fetch article details + $article = WarehouseArticleModel::get($position['article']); + if (!$article) continue; + + // Get price for determined price type + $prices = json_decode($article->cheapestSellPrice, true) ?: []; + $price = 0; + foreach ($prices as $p) { + if ($p['title'] === $priceType) { + $price = $p['price']; + break; + } + } + + $enrichedPositions[] = [ + 'type' => 'article', + 'articleId' => $article->id, + 'product_name' => $article->articleNumber . " | " . $article->title, + 'product_info' => $article->description, + 'amount' => $position['amount'], + 'unit' => $article->unit, + 'price' => $price, + 'discount' => 0, + 'vatrate' => 20 + ]; + + } elseif (isset($position['articlePacket'])) { + // Handle article packets + $packet = WarehouseArticlePacketModel::get($position['articlePacket']); + if (!$packet) continue; + + $enrichedPositions[] = [ + 'type' => 'packet', + 'packetId' => $packet->id, + 'product_name' => $packet->title, + 'product_info' => $packet->description ?? '', + 'amount' => $position['amount'], + 'unit' => 'Pau.', + 'price' => 0, + 'discount' => 0, + 'vatrate' => 20 + ]; + + } elseif (isset($position['articleText'])) { + // Handle custom text entries + $enrichedPositions[] = [ + 'type' => 'text', + 'product_name' => $position['articleText'], + 'product_info' => '', + 'amount' => $position['amount'] ?? 1, + 'unit' => 'Stk.', + 'price' => 0, + 'discount' => 0, + 'vatrate' => 20 + ]; + } + } + + // Add hours entries as positions + $hoursEntries = json_decode($shippingNote->hoursEntries, true); + if (!is_array($hoursEntries)) { + $hoursEntries = []; + } + + foreach ($hoursEntries as $hoursEntry) { + if (empty($hoursEntry['hourCount']) || floatval(str_replace(",", ".", $hoursEntry['hourCount'])) <= 0) { + continue; + } + + $userName = 'Unbekannt'; + if (!empty($hoursEntry['userId']) && is_numeric($hoursEntry['userId'])) { + try { + $user = UserModel::getOne($hoursEntry['userId']); + $userName = $user ? $user->name : 'Unbekannt'; + } catch (Exception $e) { + $userName = 'Unbekannt'; + } + } elseif (!empty($hoursEntry['userId_text'])) { + $userName = $hoursEntry['userId_text']; + } + + $enrichedPositions[] = [ + 'type' => 'hours', + 'product_name' => 'Arbeitsstunden - ' . $userName, + 'product_info' => 'Datum: ' . (isset($hoursEntry['date']) ? date('d.m.Y', strtotime($hoursEntry['date'])) : ''), + 'amount' => str_replace(",", ".", $hoursEntry['hourCount']), + 'unit' => 'h', + 'price' => 60, + 'discount' => 0, + 'vatrate' => 20 + ]; + } + + self::returnJson([ + 'success' => true, + 'data' => [ + 'shippingNoteId' => $shippingNote->id, + 'billingAddress' => $billingAddress ? [ + 'id' => $billingAddress->id, + 'customer_number' => $billingAddress->customer_number, + 'company' => $billingAddress->company, + 'firstname' => $billingAddress->firstname, + 'lastname' => $billingAddress->lastname, + 'street' => $billingAddress->street, + 'zip' => $billingAddress->zip, + 'city' => $billingAddress->city, + 'email' => $billingAddress->email, + 'uid' => $billingAddress->uid, + 'fibu_account_number' => $billingAddress->fibu_account_number, + 'billing_type' => $billingAddress->billing_type, + 'billing_delivery' => $billingAddress->billing_delivery, + 'bank_account_bank' => $billingAddress->bank_account_bank, + 'bank_account_owner' => $billingAddress->bank_account_owner, + 'bank_account_iban' => $billingAddress->bank_account_iban, + 'bank_account_bic' => $billingAddress->bank_account_bic, + 'sepa_date' => $billingAddress->sepa_date, + 'fibu_payment_due' => $billingAddress->fibu_payment_due, + 'fibu_payment_skonto' => $billingAddress->fibu_payment_skonto, + 'fibu_payment_skonto_rate' => $billingAddress->fibu_payment_skonto_rate + ] : null, + 'deliveryAddress' => [ + 'name' => $shippingNote->deliveryAddressName, + 'line' => $shippingNote->deliveryAddressLine, + 'plz' => $shippingNote->deliveryAddressPLZ, + 'city' => $shippingNote->deliveryAddressCity, + 'email' => $shippingNote->deliveryAddressEMail + ], + 'note' => $shippingNote->note, + 'positions' => $enrichedPositions + ] + ]); + } + protected function getArticleAddressPriceAction() { empty($this->request->articleId) && $this->sendError('Keine Artikel ID gefunden'); empty($this->request->addressId) && $this->sendError('Keine Adress ID gefunden'); diff --git a/public/js/pages/ManualInvoice/ManualInvoice.js b/public/js/pages/ManualInvoice/ManualInvoice.js index f81021b9c..4ce7a72cd 100644 --- a/public/js/pages/ManualInvoice/ManualInvoice.js +++ b/public/js/pages/ManualInvoice/ManualInvoice.js @@ -16,12 +16,29 @@ Vue.component('manual-invoice', { - + `, - data: () => ({ isModalOpen: false, editingInvoiceData: null, isGutschriftModalOpen: false, gutschriftInvoiceId: null, isSendModalOpen: false, sendInvoiceId: null }), + data: () => ({ isModalOpen: false, editingInvoiceData: null, shippingNoteImportData: null, isGutschriftModalOpen: false, gutschriftInvoiceId: null, isSendModalOpen: false, sendInvoiceId: null }), + mounted() { + // Check for shipping note import data + const shippingNoteData = localStorage.getItem('ManualInvoice_create'); + if (shippingNoteData) { + try { + // Parse and store the data + this.shippingNoteImportData = JSON.parse(shippingNoteData); + // Delete from localStorage immediately so it doesn't auto-open again on reload + localStorage.removeItem('ManualInvoice_create'); + // Auto-open modal for import + this.openModal(); + } catch (e) { + console.error('Error parsing shipping note data:', e); + localStorage.removeItem('ManualInvoice_create'); + } + } + }, methods: { openModal(invoice = null) { this.editingInvoiceData = invoice ? JSON.parse(JSON.stringify(invoice)) : null; @@ -30,6 +47,7 @@ Vue.component('manual-invoice', { closeModal() { this.isModalOpen = false; this.editingInvoiceData = null; + this.shippingNoteImportData = null; this.$refs.table.$refs.table.refreshTable(); }, async handleSave(invoiceData) { @@ -126,7 +144,7 @@ Vue.component('manual-invoice', { }); Vue.component('manual-invoice-modal', { - props: ['initialData'], + props: ['initialData', 'shippingNoteImport'], template: `
Drücke STRG + Q um die Vorschau umzuschalten.
@@ -278,6 +296,16 @@ Vue.component('manual-invoice-modal', { } if (!Array.isArray(this.invoiceData.positions)) this.invoiceData.positions = []; } + + // Check for shipping note import data from prop + if (this.shippingNoteImport && Array.isArray(this.shippingNoteImport) && this.shippingNoteImport.length > 0) { + try { + this.processShippingNoteImport(this.shippingNoteImport); + } catch (e) { + console.error('Error processing shipping note import:', e); + window.notify('error', 'Fehler beim Importieren des Lieferscheins'); + } + } }, mounted() { window.addEventListener('resize', this.handleResize); @@ -334,6 +362,87 @@ Vue.component('manual-invoice-modal', { } finally { this.pdfLoading = false; } + }, + processShippingNoteImport(shippingNoteDataArray) { + // Temporarily disable the preview update during import to prevent memory leak + clearTimeout(this.previewDebounceTimer); + const originalWatcher = this.$options.watch['invoiceData']; + delete this.$options.watch['invoiceData']; + + try { + for (const shippingNoteData of shippingNoteDataArray) { + // Pre-fill billing address fields + if (shippingNoteData.billingAddress) { + const addr = shippingNoteData.billingAddress; + + Object.assign(this.invoiceData, { + billingaddress_id: addr.id, + customer_number: addr.customer_number || 0, + company: addr.company || '', + firstname: addr.firstname || '', + lastname: addr.lastname || '', + street: addr.street || '', + zip: addr.zip || '', + city: addr.city || '', + email: addr.email || '', + uid: addr.uid || '', + fibu_account_number: addr.fibu_account_number || 0, + fibu_payment_due: addr.fibu_payment_due || 14, + fibu_payment_skonto: addr.fibu_payment_skonto || 0, + fibu_payment_skonto_rate: addr.fibu_payment_skonto_rate || 0, + billing_type: addr.billing_type || 'invoice', + owner_id: addr.id + }); + + // Banking info (if SEPA) + if (addr.billing_type === 'sepa') { + Object.assign(this.invoiceData, { + bank_account_bank: addr.bank_account_bank || '', + bank_account_owner: addr.bank_account_owner || '', + bank_account_iban: addr.bank_account_iban || '', + bank_account_bic: addr.bank_account_bic || '', + sepa_date: addr.sepa_date || '' + }); + } + } + + // Pre-fill external reference with shipping note reference + this.invoiceData.externe_referenz = `Lieferschein #${shippingNoteData.shippingNoteId}`; + + // Add introductory text if shipping note has notes + if (shippingNoteData.note) { + this.invoiceData.einleitender_text = shippingNoteData.note; + } + + // Add all positions (batch operation to avoid triggering watcher for each item) + if (shippingNoteData.positions && Array.isArray(shippingNoteData.positions)) { + const newPositions = shippingNoteData.positions.map(position => ({ + product_name: position.product_name || '', + product_info: position.product_info || '', + amount: parseFloat(position.amount) || 0, + unit: position.unit || 'Stk.', + price: parseFloat(position.price) || 0, + discount: parseFloat(position.discount) || 0, + vatrate: parseFloat(position.vatrate) || 20 + })); + + // Add all positions at once instead of one by one + this.invoiceData.positions.push(...newPositions); + } + } + + // Notify user + const positionCount = shippingNoteDataArray.reduce((sum, sn) => sum + (sn.positions?.length || 0), 0); + window.notify('success', `Lieferschein erfolgreich importiert (${positionCount} Position(en))`); + } finally { + // Re-enable the watcher + this.$options.watch['invoiceData'] = originalWatcher; + + // Trigger one preview update after import is complete + this.$nextTick(() => { + this.debouncedPreviewUpdate(); + }); + } } } }); diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js index 19cede786..9dc3a6b3d 100644 --- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js +++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js @@ -44,6 +44,12 @@ window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [ "key": "print", "title": "Drucken", "class": "fas fa-print text-primary", + }, + { + "key": "createManualInvoice", + "title": "Rechnung erstellen", + "class": "fas fa-file-invoice text-primary", + "condition": (row) => row.status === 'accepted', } ] @@ -547,6 +553,7 @@ Vue.component('warehouse-shipping-note', { @status_to_cancelled="changeStatus($event.id, 'cancelled')" @status_to_new="changeStatus($event.id, 'new')" @add_log="addLogModalId = $event.id" + @createManualInvoice="createManualInvoice($event)" @edit="shippingNoteModalId = $event.id" ref="table"> @@ -678,6 +685,30 @@ Vue.component('warehouse-shipping-note', { this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten'); } }, + async createManualInvoice(row) { + try { + // Fetch shipping note with enriched article data + const res = await axios.get( + `${window.TT_CONFIG.BASE_PATH}/WarehouseShippingNote/getShippingNoteForInvoice`, + { params: { id: row.id } } + ); + + if (!res.data.success) { + window.notify('error', res.data.message || 'Fehler beim Laden der Lieferscheindaten'); + return; + } + + // Store in localStorage as array (to match WarehouseOrder pattern) + localStorage.setItem('ManualInvoice_create', JSON.stringify([res.data.data])); + + // Navigate to ManualInvoice module + window.location.href = `${window.TT_CONFIG.BASE_PATH}/ManualInvoice`; + + } catch (error) { + console.error('Error creating manual invoice:', error); + window.notify('error', 'Fehler beim Erstellen der Rechnung'); + } + }, } })