diff --git a/Layout/default/ManualInvoice/PDF_HEADER.html b/Layout/default/ManualInvoice/PDF_HEADER.html index 075f2c9bc..49e6196c0 100644 --- a/Layout/default/ManualInvoice/PDF_HEADER.html +++ b/Layout/default/ManualInvoice/PDF_HEADER.html @@ -68,9 +68,7 @@ - + {{ qrCodeHtml }}
- QR-Code - diff --git a/application/ManualInvoice/ManualInvoiceController.php b/application/ManualInvoice/ManualInvoiceController.php index 497057215..e55a94a8c 100644 --- a/application/ManualInvoice/ManualInvoiceController.php +++ b/application/ManualInvoice/ManualInvoiceController.php @@ -108,7 +108,9 @@ class ManualInvoiceController extends TTCrud "{{ leistungszeitraumHtml }}" => ($invoice->leistungszeitraum ?? '') ? "" : "", "{{ externeReferenzHtml }}" => ($invoice->externe_referenz ?? '') ? "" : "", "{{ vatHtml }}" => ($invoice->uid ?? '') ? "" : "", - "{{ qrCodeSrc }}" => $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2)) + "{{ qrCodeHtml }}" => ($invoice->total_gross ?? 0) >= 0 + ? '' + : '' ]; $headerHtml = str_replace(array_keys($replacements), array_values($replacements), file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html")); @@ -349,10 +351,17 @@ class ManualInvoiceController extends TTCrud $me = new User(); $me->loadMe(); - // Log download in journal + // Update status to 'gesendet' (same as email) + if ($invoice->status === 'erstellt') { + $invoice->status = 'gesendet'; + $invoice->save(); + } + + // Log download in journal with status change ManualInvoiceJournalModel::create([ 'manualinvoiceId' => $id, 'text' => 'Rechnung heruntergeladen', + 'statusChange' => 'gesendet', 'createBy' => $me->id, 'create' => time() ]); @@ -467,13 +476,20 @@ class ManualInvoiceController extends TTCrud $me = new User(); $me->loadMe(); + // Fields that exist in ManualInvoicepositionModel + $allowedFields = ['billing_id', 'contract_id', 'matchcode', 'product_id', 'product_name', 'product_info', + 'amount', 'unit', 'price', 'discount', 'price_total', 'price_gross', 'vatrate', + 'fibu_cost_account', 'fibu_cost_account_legacy', 'fibu_taxcode', 'options']; + foreach ($this->tempPositions as $position) { // Skip empty positions if (empty($position['product_name']) || ($position['amount'] ?? 0) == 0) continue; // Map _group to position_group $groupName = $position['_group'] ?? null; - unset($position['_group']); + + // Filter to only allowed fields + $filteredPosition = array_intersect_key($position, array_flip($allowedFields)); ManualInvoicepositionModel::create(array_merge([ 'manualinvoice_id' => $invoiceId, @@ -484,7 +500,7 @@ class ManualInvoiceController extends TTCrud 'edit_by' => $me->id, 'create' => time(), 'edit' => time() - ], $position)); + ], $filteredPosition)); } $this->tempPositions = []; } @@ -810,6 +826,15 @@ class ManualInvoiceController extends TTCrud return; } + // Parse prices from cheapestSellPrice JSON + $prices = []; + if (!empty($article->cheapestSellPrice)) { + $pricesData = json_decode($article->cheapestSellPrice, true); + if (is_array($pricesData)) { + $prices = $pricesData; + } + } + self::returnJson([ 'success' => true, 'article' => [ @@ -817,8 +842,10 @@ class ManualInvoiceController extends TTCrud 'title' => $article->title, 'articleNumber' => $article->articleNumber, 'description' => $article->description, - 'revenueAccount' => $article->revenueAccount + 'revenueAccount' => $article->revenueAccount, + 'unit' => $article->unit ], + 'prices' => $prices, 'vatgroup_id' => $vatgroupId, 'fibu_cost_account' => $vatrate->account, 'fibu_cost_account_legacy' => $vatrate->legacy_account, diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteController.php b/application/WarehouseShippingNote/WarehouseShippingNoteController.php index 448e6ab12..fbf175801 100644 --- a/application/WarehouseShippingNote/WarehouseShippingNoteController.php +++ b/application/WarehouseShippingNote/WarehouseShippingNoteController.php @@ -131,7 +131,10 @@ class WarehouseShippingNoteController extends TTCrud { // Get billing address info $billingAddress = null; if ($shippingNote->billingAddressId) { - $billingAddress = Address::getOne($shippingNote->billingAddressId); + $billingAddress = new Address($shippingNote->billingAddressId); + if (!$billingAddress->id) { + $billingAddress = null; + } } // Determine price type ONCE (not in loop for performance) diff --git a/public/js/pages/ManualInvoice/ManualInvoice.js b/public/js/pages/ManualInvoice/ManualInvoice.js index 0348b6e85..ec1b87315 100644 --- a/public/js/pages/ManualInvoice/ManualInvoice.js +++ b/public/js/pages/ManualInvoice/ManualInvoice.js @@ -179,7 +179,7 @@ Vue.component('manual-invoice-modal', { - + @@ -222,35 +222,31 @@ Vue.component('manual-invoice-modal', { positionsConfig: { fields: { article_id: { - type: 'autocomplete', - label: 'Artikel (optional)', + type: 'input-article', + label: 'Artikel', apiUrl: '/WarehouseArticle/autocomplete', - customFieldReference: 'WarehouseArticle' + customFieldReference: 'WarehouseArticle', + emitDisplayValue: true }, product_name: { type: 'input', label: 'Bezeichnung' }, product_info: { type: 'input', label: 'Zusatzinfo' }, amount: { type: 'input', label: 'Menge', inputType: 'number' }, - unit: { + price_type: { type: 'select', - label: 'Einheit', - options: [ - { value: 'Pau.', text: 'Pau.' }, - { value: 'Stk.', text: 'Stk.' }, - { value: 'h', text: 'h' }, - { value: 'm', text: 'm' } - ] + label: 'Preistyp', + options: [] }, price: { type: 'input', label: 'Einzelpreis (€)', inputType: 'number' }, discount: { type: 'input', label: 'Rabatt (%)', inputType: 'number' }, - vatrate: { type: 'input', label: 'USt. (%)', inputType: 'number' }, }, validateForm: (d) => { if (!d.product_name) { window.notify('error', 'Bezeichnung ist erforderlich.'); return false; } - if (!d.amount) { window.notify('error', 'Menge ist erforderlich.'); return false; } + if (d.amount == null || d.amount === '') { window.notify('error', 'Menge ist erforderlich.'); return false; } if (d.price == null) { window.notify('error', 'Preis ist erforderlich.'); return false; } return true; } - } + }, + articlePrices: [] }; }, computed: { @@ -392,15 +388,21 @@ Vue.component('manual-invoice-modal', { }, togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; }, async onArticleSelected(articleId) { - if (!articleId) return; + if (!articleId) { + // Reset price type options when no article selected + this.articlePrices = []; + this.positionsConfig.fields.price_type.options = []; + return; + } try { const vatarea = this.customerBillingInfo.vatarea || 'domestic'; const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getArticleVatInfo?article_id=${articleId}&vatarea=${vatarea}`); if (data.success && this.$refs.positionsManager) { const pm = this.$refs.positionsManager; if (data.article) { - pm.$set(pm.formData, 'product_name', data.article.title); + pm.$set(pm.formData, 'product_name', data.article.articleNumber + ' | ' + data.article.title); pm.$set(pm.formData, 'product_info', data.article.description || ''); + pm.$set(pm.formData, 'unit', data.article.unit || 'Stk.'); } pm.$set(pm.formData, 'vatrate', parseFloat(data.vatrate) || 20); pm.$set(pm.formData, 'fibu_cost_account', data.fibu_cost_account); @@ -408,11 +410,30 @@ Vue.component('manual-invoice-modal', { pm.$set(pm.formData, 'fibu_taxcode', data.fibu_taxcode); this.invoiceData.vatgroup_id = data.vatgroup_id; await this.updateTaxText(data.vatgroup_id); + + // Handle prices and price type selection + this.articlePrices = data.prices || []; + if (this.articlePrices.length > 0) { + const priceOptions = this.articlePrices.map(p => ({ value: p.title, text: `${p.title} (${this.formatPrice(p.price)})` })); + this.positionsConfig.fields.price_type.options = priceOptions; + // Set first price as default + pm.$set(pm.formData, 'price_type', this.articlePrices[0].title); + pm.$set(pm.formData, 'price', this.articlePrices[0].price); + } else { + this.positionsConfig.fields.price_type.options = []; + } } } catch (e) { console.error('Error fetching article VAT info:', e); } }, + onPriceTypeChanged(priceType) { + if (!priceType || !this.articlePrices.length) return; + const selectedPrice = this.articlePrices.find(p => p.title === priceType); + if (selectedPrice && this.$refs.positionsManager) { + this.$refs.positionsManager.$set(this.$refs.positionsManager.formData, 'price', selectedPrice.price); + } + }, async updateTaxText(vatgroupId) { if (!vatgroupId) return; try { @@ -434,7 +455,7 @@ Vue.component('manual-invoice-modal', { this.pdfLoading = true; try { const positions = this.invoiceData.positions - .filter(p => p.product_name && (parseFloat(p.amount) || 0) > 0) // Filter out empty positions + .filter(p => p.product_name && (parseFloat(p.amount) || 0) !== 0) // Filter out empty positions (allow negative for Gutschrift) .map(p => { const amount = parseFloat(p.amount) || 0; const price = parseFloat(p.price) || 0; @@ -546,7 +567,7 @@ Vue.component('manual-invoice-modal', { Vue.component('gutschrift-modal', { props: ['invoiceId'], template: ` - +

Lade Rechnungsdaten...

Originalrechnung: {{ invoice.invoice_number }} - {{ invoice.customer_name }}
@@ -623,7 +644,7 @@ Vue.component('gutschrift-modal', { Vue.component('send-invoice-modal', { props: ['invoiceId'], template: ` - + @@ -639,11 +660,11 @@ Vue.component('send-invoice-modal', {
- +
@@ -653,8 +674,8 @@ Vue.component('send-invoice-modal', {
-
- +
+
@@ -678,7 +699,6 @@ Vue.component('send-invoice-modal', { if (data.success) { this.invoice = data.invoice; this.emailAddress = data.invoice.email || ''; - this.selectedAction = data.invoice.email ? 'email' : 'download'; } else { window.notify('error', data.message || 'Fehler beim Laden der Rechnung'); this.close();
Leistungszeitraum:" . htmlspecialchars($invoice->leistungszeitraum) . "
Externe Referenz:" . htmlspecialchars($invoice->externe_referenz) . "
Ihre UID:" . $invoice->uid . "
QR-Codetotal_gross ?? 0, 2)) . '" style="display: block; height: 100%; max-height: 3.5cm; width: auto;">