added technical data to xinon workorder and workordermph now has a unassign button

This commit is contained in:
Luca Haid
2026-01-18 17:42:11 +00:00
parent a35b865fad
commit 6ab41a9169
13 changed files with 508 additions and 24 deletions
+81 -13
View File
@@ -161,9 +161,18 @@ Vue.component('manual-invoice-modal', {
<tt-autocomplete label="Kunde suchen" :api-url="customerApiUrl" v-model="invoiceData.billingaddress_id" sm row />
</tt-card>
<tt-card><template v-slot:header><h5><i class="fas fa-file-invoice mr-2"></i>Rechnungsdetails</h5></template>
<div class="form-grid">
<tt-input label="Rechnungsdatum" type="date" v-model="invoiceData.invoice_date" sm/>
<tt-select label="Zahlungsart" v-model="invoiceData.billing_type" :options="billingTypeOptions" sm/>
<div class="form-row mb-2">
<div class="col-md-6">
<label class="small text-muted">Zahlungsart</label>
<div class="d-flex align-items-center">
<span :class="['badge', effectiveBillingType === 'sepa' ? 'badge-info' : 'badge-secondary']">
{{ effectiveBillingType === 'sepa' ? 'SEPA' : 'Rechnung' }}
</span>
<small v-if="customerBillingInfo.billing_type === 'sepa' && effectiveBillingType === 'invoice'" class="text-warning ml-2">
<i class="fas fa-exclamation-triangle"></i> Brutto überschreitet SEPA-Limit ({{ formatPrice(customerBillingInfo.manual_invoice_sepa_limit) }})
</small>
</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"/>
@@ -172,9 +181,8 @@ Vue.component('manual-invoice-modal', {
<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" />
</tt-card>
<tt-card><template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Texte & Rabatt</h5></template>
<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-textarea label="Steuerhinweis" v-model="invoiceData.tax_text" rows="2"/>
</tt-card>
</div>
</div>
@@ -197,6 +205,12 @@ Vue.component('manual-invoice-modal', {
pdfLoading: false,
pdfPreviewUrl: '',
previewDebounceTimer: null,
customerBillingInfo: {
billing_type: 'invoice',
manual_invoice_sepa_limit: null,
vatarea: 'domestic',
tax_text: ''
},
invoiceData: {
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,
@@ -205,7 +219,6 @@ Vue.component('manual-invoice-modal', {
leistungszeitraum: '', einleitender_text: '', externe_referenz: '', gesamtrabatt: 0,
positions: [], total: 0, total_gross: 0
},
billingTypeOptions: [{value: 'invoice', text: 'Rechnung'}, {value: 'sepa', text: 'SEPA'}],
positionsConfig: {
fields: {
article_id: {
@@ -270,16 +283,31 @@ Vue.component('manual-invoice-modal', {
});
return { subtotal, net, vat, gross };
},
effectiveBillingType() {
if (this.customerBillingInfo.billing_type !== 'sepa') return 'invoice';
if (this.customerBillingInfo.manual_invoice_sepa_limit === null) return 'sepa';
return this.totals.gross <= this.customerBillingInfo.manual_invoice_sepa_limit ? 'sepa' : 'invoice';
}
},
watch: {
'invoiceData': { handler() { this.debouncedPreviewUpdate(); }, deep: true },
effectiveBillingType: {
handler(newType) {
this.invoiceData.billing_type = newType;
},
immediate: true
},
'invoiceData.billingaddress_id': {
async handler(newId) {
if (!newId) return Object.assign(this.invoiceData, {
company: '', firstname: '', lastname: '', street: '', zip: '', city: '',
country: 'Österreich', uid: '', email: '', customer_number: 0, fibu_account_number: 0, owner_id: 0
});
if (!newId) {
Object.assign(this.invoiceData, {
company: '', firstname: '', lastname: '', street: '', zip: '', city: '',
country: 'Österreich', uid: '', email: '', customer_number: 0, fibu_account_number: 0, owner_id: 0
});
this.customerBillingInfo = { billing_type: 'invoice', manual_invoice_sepa_limit: null, vatarea: 'domestic', tax_text: '' };
return;
}
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Address/api?do=getAddress&id=${newId}`);
if (data.status === 'OK' && data.result.address) {
@@ -291,6 +319,8 @@ Vue.component('manual-invoice-modal', {
fibu_account_number: a.fibu_account_number || 0, owner_id: newId
});
}
await this.fetchCustomerBillingInfo(newId);
}
}
},
@@ -327,10 +357,35 @@ Vue.component('manual-invoice-modal', {
methods: {
close() { this.$emit('close'); },
saveInvoice() {
this.invoiceData.invoice_date = moment().format('YYYY-MM-DD');
this.invoiceData.billing_type = this.effectiveBillingType;
this.invoiceData.tax_text = this.customerBillingInfo.tax_text;
if (!this.invoiceData.billingaddress_id) return window.notify('error', 'Bitte wählen Sie einen Kunden aus.');
if (!this.invoiceData.positions?.length) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
this.$emit('save', this.invoiceData);
},
formatPrice(value) {
if (value === null || value === undefined) return '-';
return new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value);
},
async fetchCustomerBillingInfo(addressId) {
if (!addressId) return;
try {
const vatgroupId = this.invoiceData.vatgroup_id || 2;
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getCustomerBillingInfo?address_id=${addressId}&vatgroup_id=${vatgroupId}`);
if (data.success) {
this.customerBillingInfo = {
billing_type: data.billing_type || 'invoice',
manual_invoice_sepa_limit: data.manual_invoice_sepa_limit,
vatarea: data.vatarea || 'domestic',
tax_text: data.tax_text || ''
};
this.invoiceData.tax_text = data.tax_text || '';
}
} catch (e) {
console.error('Error fetching customer billing info:', e);
}
},
handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
handleGlobalKeydown(e) {
if (e.ctrlKey && e.key === 'q') { e.preventDefault(); this.togglePreviewVisibility(); }
@@ -339,9 +394,9 @@ Vue.component('manual-invoice-modal', {
async onArticleSelected(articleId) {
if (!articleId) return;
try {
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getArticleVatInfo?article_id=${articleId}`);
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) {
// Update the formData in the positions manager
const pm = this.$refs.positionsManager;
if (data.article) {
pm.$set(pm.formData, 'product_name', data.article.title);
@@ -351,13 +406,26 @@ Vue.component('manual-invoice-modal', {
pm.$set(pm.formData, 'fibu_cost_account', data.fibu_cost_account);
pm.$set(pm.formData, 'fibu_cost_account_legacy', data.fibu_cost_account_legacy);
pm.$set(pm.formData, 'fibu_taxcode', data.fibu_taxcode);
// Store vatgroup_id on invoice level if needed
this.invoiceData.vatgroup_id = data.vatgroup_id;
await this.updateTaxText(data.vatgroup_id);
}
} catch (e) {
console.error('Error fetching article VAT info:', e);
}
},
async updateTaxText(vatgroupId) {
if (!vatgroupId) return;
try {
const vatarea = this.customerBillingInfo.vatarea || 'domestic';
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getTaxText?vatgroup_id=${vatgroupId}&vatarea=${vatarea}`);
if (data.success) {
this.customerBillingInfo.tax_text = data.tax_text || '';
this.invoiceData.tax_text = data.tax_text || '';
}
} catch (e) {
console.error('Error fetching tax text:', e);
}
},
debouncedPreviewUpdate() {
clearTimeout(this.previewDebounceTimer);
this.previewDebounceTimer = setTimeout(() => this.updatePdfPreview(), 2000);