fxied vat calculation

This commit is contained in:
Luca Haid
2026-01-26 15:44:35 +01:00
parent 6f01440bc9
commit e8002d184f
10 changed files with 224 additions and 124 deletions

View File

@@ -52,27 +52,21 @@ Vue.component('manual-invoice', {
},
async handleSave(invoiceData) {
try {
const positions = invoiceData.positions.map(p => {
const amount = parseFloat(p.amount) || 0;
const price = parseFloat(p.price) || 0;
const discount = parseFloat(p.discount) || 0;
const vatrate = parseFloat(p.vatrate) || 0;
const priceAfterDiscount = amount * price * (1 - discount / 100);
return {
...p, amount, price, discount, vatrate,
unit: p.unit || 'Stk.',
price_total: priceAfterDiscount,
price_gross: priceAfterDiscount * (1 + vatrate / 100),
product_id: p.product_id || 0,
contract_id: p.contract_id || 0,
billing_id: p.billing_id || null,
matchcode: p.matchcode || null,
fibu_cost_account: p.fibu_cost_account || null,
fibu_cost_account_legacy: p.fibu_cost_account_legacy || null,
fibu_taxcode: p.fibu_taxcode || null,
options: p.options || null
};
});
const positions = invoiceData.positions.map(p => ({
...p,
amount: parseFloat(p.amount) || 0,
price: parseFloat(p.price) || 0,
discount: parseFloat(p.discount) || 0,
vatrate: parseFloat(p.vatrate) || 0,
unit: p.unit || 'Stk.',
warehousearticle_id: p.warehousearticle_id || p.product_id || 0,
warehousearticle_name: p.warehousearticle_name || p.product_name || '',
matchcode: p.matchcode || null,
fibu_cost_account: p.fibu_cost_account || null,
fibu_cost_account_legacy: p.fibu_cost_account_legacy || null,
fibu_taxcode: p.fibu_taxcode || null,
options: p.options || null
}));
const payload = {
...invoiceData,
@@ -82,11 +76,13 @@ Vue.component('manual-invoice', {
customer_number: invoiceData.customer_number || 0,
country: invoiceData.country || 'Österreich',
billing_type: invoiceData.billing_type || 'invoice',
billing_delivery: 'email',
fibu_payment_due: 14,
fibu_account_number: invoiceData.fibu_account_number || 0,
vatgroup_id: 1,
gesamtrabatt: parseFloat(invoiceData.gesamtrabatt) || 0
vatgroup_id: invoiceData.vatgroup_id || 1,
performance_period: invoiceData.performance_period || invoiceData.leistungszeitraum || '',
introductory_text: invoiceData.introductory_text || invoiceData.einleitender_text || '',
external_reference: invoiceData.external_reference || invoiceData.externe_referenz || '',
total_discount: parseFloat(invoiceData.total_discount || invoiceData.gesamtrabatt) || 0
};
const url = invoiceData.id ? window.TT_CONFIG.UPDATE_URL : window.TT_CONFIG.CREATE_URL;
@@ -174,15 +170,15 @@ Vue.component('manual-invoice-modal', {
</div>
</div>
</div>
<tt-input label="Leistungszeitraum" v-model="invoiceData.leistungszeitraum" sm row placeholder="z.B. 01.01.2025 - 31.01.2025"/>
<tt-input label="Externe Referenz" v-model="invoiceData.externe_referenz" sm row placeholder="z.B. Auftragsnummer, Bestellnummer"/>
<tt-textarea label="Einleitender Text" v-model="invoiceData.einleitender_text" rows="3" sm row/>
<tt-input label="Leistungszeitraum" v-model="invoiceData.performance_period" sm row placeholder="z.B. 01.01.2025 - 31.01.2025"/>
<tt-input label="Externe Referenz" v-model="invoiceData.external_reference" sm row placeholder="z.B. Auftragsnummer, Bestellnummer"/>
<tt-textarea label="Einleitender Text" v-model="invoiceData.introductory_text" rows="3" sm row/>
</tt-card>
<tt-card><template v-slot:header><h5><i class="fas fa-list-ol mr-2"></i>Positionen</h5></template>
<tt-positions-manager group-mode ref="positionsManager" v-model="invoiceData.positions" :config="positionsConfig" @updateField-article_id="onArticleSelected" @updateField-price_type="onPriceTypeChanged" />
</tt-card>
<tt-card><template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Rabatt</h5></template>
<tt-input label="Gesamtrabatt (%)" v-model.number="invoiceData.gesamtrabatt" sm row type="number" placeholder="0"/>
<tt-input label="Gesamtrabatt (%)" v-model.number="invoiceData.total_discount" sm row type="number" placeholder="0"/>
</tt-card>
</div>
</div>
@@ -215,8 +211,8 @@ Vue.component('manual-invoice-modal', {
id: null, invoice_number: null, invoice_date: moment().format('YYYY-MM-DD'),
billingaddress_id: null, owner_id: null, customer_number: 0, fibu_account_number: 0,
company: '', firstname: '', lastname: '', street: '', zip: '', city: '', country: 'Österreich',
uid: '', email: '', billing_type: 'invoice', tax_text: '',
leistungszeitraum: '', einleitender_text: '', externe_referenz: '', gesamtrabatt: 0,
uid: '', email: '', billing_type: 'invoice', tax_text: '', vatgroup_id: 1,
performance_period: '', introductory_text: '', external_reference: '', total_discount: 0,
positions: [], total: 0, total_gross: 0
},
positionsConfig: {
@@ -228,7 +224,7 @@ Vue.component('manual-invoice-modal', {
customFieldReference: 'WarehouseArticle',
emitDisplayValue: true
},
product_name: { type: 'input', label: 'Bezeichnung' },
warehousearticle_name: { type: 'input', label: 'Bezeichnung' },
product_info: { type: 'input', label: 'Zusatzinfo' },
amount: { type: 'input', label: 'Menge', inputType: 'number' },
price_type: {
@@ -240,7 +236,7 @@ Vue.component('manual-invoice-modal', {
discount: { type: 'input', label: 'Rabatt (%)', inputType: 'number' },
},
validateForm: (d) => {
if (!d.product_name) { window.notify('error', 'Bezeichnung ist erforderlich.'); return false; }
if (!d.warehousearticle_name) { window.notify('error', 'Bezeichnung 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;
@@ -261,18 +257,15 @@ Vue.component('manual-invoice-modal', {
subtotal += lineTotal;
});
// Apply gesamtrabatt
const gesamtrabatt = parseFloat(this.invoiceData.gesamtrabatt) || 0;
const net = subtotal * (1 - gesamtrabatt / 100);
// Calculate VAT
const totalDiscount = parseFloat(this.invoiceData.total_discount) || 0;
const net = subtotal * (1 - totalDiscount / 100);
let vat = {}, gross = 0;
(this.invoiceData.positions || []).forEach(p => {
const amount = parseFloat(p.amount) || 0;
const price = parseFloat(p.price) || 0;
const discount = parseFloat(p.discount) || 0;
const r = parseInt(p.vatrate) || 0;
const lineNet = amount * price * (1 - discount / 100) * (1 - gesamtrabatt / 100);
const lineNet = amount * price * (1 - discount / 100) * (1 - totalDiscount / 100);
const lineVat = lineNet * (r / 100);
vat[r] = (vat[r] || 0) + lineVat;
gross += lineNet + lineVat;
@@ -400,7 +393,7 @@ Vue.component('manual-invoice-modal', {
if (data.success && this.$refs.positionsManager) {
const pm = this.$refs.positionsManager;
if (data.article) {
pm.$set(pm.formData, 'product_name', data.article.articleNumber + ' | ' + data.article.title);
pm.$set(pm.formData, 'warehousearticle_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.');
}
@@ -455,7 +448,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 (allow negative for Gutschrift)
.filter(p => p.warehousearticle_name && (parseFloat(p.amount) || 0) !== 0)
.map(p => {
const amount = parseFloat(p.amount) || 0;
const price = parseFloat(p.price) || 0;
@@ -524,17 +517,17 @@ Vue.component('manual-invoice-modal', {
}
// Pre-fill external reference with shipping note reference
this.invoiceData.externe_referenz = `Lieferschein #${shippingNoteData.shippingNoteId}`;
this.invoiceData.external_reference = `Lieferschein #${shippingNoteData.shippingNoteId}`;
// Add introductory text if shipping note has notes
if (shippingNoteData.note) {
this.invoiceData.einleitender_text = shippingNoteData.note;
this.invoiceData.introductory_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 || '',
warehousearticle_name: position.warehousearticle_name || position.product_name || '',
product_info: position.product_info || '',
amount: parseFloat(position.amount) || 0,
unit: position.unit || 'Stk.',
@@ -583,7 +576,7 @@ Vue.component('gutschrift-modal', {
<tbody>
<tr v-for="(pos, index) in invoice.positions" :key="index">
<td class="text-center"><input type="checkbox" v-model="selectedPositions[index]"></td>
<td><strong>{{ pos.product_name }}</strong><div v-if="pos.product_info" class="text-muted small">{{ pos.product_info }}</div></td>
<td><strong>{{ pos.warehousearticle_name }}</strong><div v-if="pos.product_info" class="text-muted small">{{ pos.product_info }}</div></td>
<td class="text-right">{{ pos.original_amount }}</td><td class="text-right">{{ pos.credited_amount }}</td>
<td class="text-right">{{ pos.available_amount }}</td>
<td><input type="number" class="form-control form-control-sm" v-model.number="creditAmounts[index]" :max="pos.available_amount" :disabled="!selectedPositions[index]" step="0.001" min="0.001"></td>
@@ -624,7 +617,7 @@ Vue.component('gutschrift-modal', {
.map((p, i) => ({ p, i })).filter(({ i }) => this.selectedPositions[i])
.map(({ p, i }) => {
const amt = this.creditAmounts[i];
if (amt > p.available_amount) throw new Error(`Menge zu hoch: ${p.product_name}`);
if (amt > p.available_amount) throw new Error(`Menge zu hoch: ${p.warehousearticle_name}`);
return amt > 0 ? { ...p, amount: amt } : null;
}).filter(Boolean);

View File

@@ -437,8 +437,8 @@ Vue.component('warehouse-article-modal', {
<div class="col-md-2" :class="{ 'wa-field-disabled': !isEditMode && !formData.category_id }">
<tt-select
label="Erlöskonto"
v-model="formData.revenueAccount"
:options="revenueAccountOptions"
v-model="formData.vatgroup_id"
:options="vatgroupOptions"
required
sm/>
</div>
@@ -538,7 +538,7 @@ Vue.component('warehouse-article-modal', {
category_id: null,
articleNumber: '',
unit: 'Stk.',
revenueAccount: 0,
vatgroup_id: 2,
warningAmount: 0,
criticalAmount: 0,
isSerialDocumentation: false,
@@ -566,10 +566,10 @@ Vue.component('warehouse-article-modal', {
{ value: 'km', text: 'km' }
];
},
revenueAccountOptions() {
vatgroupOptions() {
return [
{ value: 0, text: 'Dienstleistungen' },
{ value: 1, text: 'Handelswaren' }
{ value: 2, text: 'Dienstleistungen' },
{ value: 3, text: 'Handelswaren' }
];
},
isValid() {
@@ -607,7 +607,7 @@ Vue.component('warehouse-article-modal', {
category_id: data.category_id,
articleNumber: data.articleNumber || '',
unit: data.unit || 'Stk.',
revenueAccount: data.revenueAccount || 0,
vatgroup_id: data.vatgroup_id || 2,
warningAmount: data.warningAmount || 0,
criticalAmount: data.criticalAmount || 0,
isSerialDocumentation: !!data.isSerialDocumentation,
@@ -637,7 +637,7 @@ Vue.component('warehouse-article-modal', {
category_id: null,
articleNumber: '',
unit: 'Stk.',
revenueAccount: 0,
vatgroup_id: 2,
warningAmount: 0,
criticalAmount: 0,
isSerialDocumentation: false,