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 }}
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 ?? '') ? " | Leistungszeitraum: | " . htmlspecialchars($invoice->leistungszeitraum) . " | " : "",
"{{ externeReferenzHtml }}" => ($invoice->externe_referenz ?? '') ? "| Externe Referenz: | " . htmlspecialchars($invoice->externe_referenz) . " | " : "",
"{{ vatHtml }}" => ($invoice->uid ?? '') ? "| Ihre UID: | " . $invoice->uid . " | " : "",
- "{{ qrCodeSrc }}" => $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2))
+ "{{ qrCodeHtml }}" => ($invoice->total_gross ?? 0) >= 0
+ ? ' total_gross ?? 0, 2)) . '" style="display: block; height: 100%; max-height: 3.5cm; width: auto;"> | '
+ : ''
];
$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', {
Positionen
-
+
Rabatt
@@ -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: `
-
+
Originalrechnung: {{ invoice.invoice_number }} - {{ invoice.customer_name }}
@@ -623,7 +644,7 @@ Vue.component('gutschrift-modal', {
Vue.component('send-invoice-modal', {
props: ['invoiceId'],
template: `
-
+
Rechnung aussenden
@@ -639,11 +660,11 @@ 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();
| |