Merge branch 'Warehouse/fix-and-improve' into 'master'
Added WarehouseOffer and WarehouseOfferTemplate, also fixed menu for Lager Point See merge request fronk/thetool!1156
This commit is contained in:
@@ -1,14 +1,5 @@
|
||||
@media (min-width: 992px) {
|
||||
.modal-lg, .modal-xl {
|
||||
/*max width either 90% or 1120px*/
|
||||
max-width: min(90vw, 1120px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.warehouse-order-modal-positions-entry-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
grid-gap: 10px;
|
||||
max-width: min(90vw) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,73 @@
|
||||
Vue.component('warehouse-offer-modal', {
|
||||
props: {
|
||||
id: {type: [String, Number], required: true},
|
||||
props: {
|
||||
id: {type: [String, Number], required: true},
|
||||
mode: {type: String, default: 'edit'}
|
||||
},
|
||||
template: `
|
||||
<tt-modal :show="true"
|
||||
@submit="submit"
|
||||
:delete="id !== 'create'"
|
||||
:title="id === 'create' ? 'Angebot erstellen' : \`Angebot #\${id} bearbeiten\`"
|
||||
@update:show="$emit('close')">
|
||||
<div style="width: 99%"><h4 class="text-center">Angebotdetails</h4>
|
||||
<tt-select label="Sachbearbeiter"
|
||||
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
|
||||
sm
|
||||
row
|
||||
v-model="offer.editor"/>
|
||||
|
||||
<tt-autocomplete label="Kunde" v-model="offer.customerNumber" sm row :api-url="billAddrAutoCompleteUrl"/>
|
||||
<tt-input label="Kundenreferenz" v-model="offer.reference" sm row/>
|
||||
<tt-textarea label="Angebotszweck" v-model="offer.purpose" sm row/>
|
||||
<hr>
|
||||
<h4 class="text-center">Kundenadresse</h4>
|
||||
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 1fr 2fr 1fr 1fr;">
|
||||
<tt-input label="Name" v-model="offer.customerName" sm/>
|
||||
<tt-input label="Kontakt" v-model="offer.contactPerson" sm/>
|
||||
<tt-input label="Straße" v-model="offer.customerStreet" sm/>
|
||||
<tt-input label="PLZ" v-model="offer.customerZip" sm/>
|
||||
<tt-input label="Ort" v-model="offer.customerCity" sm/>
|
||||
</div>
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
<tt-positions-manager group-mode ref="positionsManager" v-model="offer.positions" :config="positionsConfig" @updateField-article="fetchArticleData"/>
|
||||
<hr>
|
||||
<tt-input label="Gesamtrabatt (%)" v-model="offer.totalDiscount" sm row type="number"/>
|
||||
<tt-select label="Zahlungskonditionen" :options="paymentTerms" sm row v-model="offer.paymentTerms"/>
|
||||
<tt-select label="Lieferkonditionen" :options="deliveryTerms" sm row v-model="offer.deliveryTerms"/>
|
||||
<tt-textarea label="Schlusstext" sm rows="11" row v-model="offer.closingText"/>
|
||||
<hr>
|
||||
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
|
||||
</div>
|
||||
</tt-modal>
|
||||
`,
|
||||
<tt-modal :show="true"
|
||||
@submit="submit"
|
||||
:delete="id !== 'create'"
|
||||
:title="id === 'create' ? 'Angebot erstellen' : \`Angebot #\${id} bearbeiten\`"
|
||||
@update:show="$emit('close')">
|
||||
<div style="width: 99%"><h4 class="text-center">Angebotdetails</h4>
|
||||
<tt-select label="Sachbearbeiter"
|
||||
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
|
||||
sm
|
||||
row
|
||||
v-model="offer.editor"/>
|
||||
|
||||
<tt-autocomplete label="Kunde" v-model="offer.customerNumber" sm row :api-url="billAddrAutoCompleteUrl"/>
|
||||
<tt-input label="Kundenreferenz" v-model="offer.reference" sm row/>
|
||||
<tt-textarea label="Angebotszweck" v-model="offer.purpose" sm row/>
|
||||
<hr>
|
||||
<h4 class="text-center">Kundenadresse</h4>
|
||||
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 1fr 2fr 1fr 1fr 1fr;">
|
||||
<tt-input label="Name" v-model="offer.customerName" sm/>
|
||||
<tt-input label="Kontakt" v-model="offer.contactPerson" sm/>
|
||||
<tt-input label="Straße" v-model="offer.customerStreet" sm/>
|
||||
<tt-input label="PLZ" v-model="offer.customerZip" sm/>
|
||||
<tt-input label="Ort" v-model="offer.customerCity" sm/>
|
||||
<tt-input label="USt-IdNr." v-model="offer.customerVAT" sm/>
|
||||
</div>
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
<tt-positions-manager group-mode ref="positionsManager" v-model="offer.positions" :config="positionsConfig"
|
||||
@updateField-article="fetchArticleData"/>
|
||||
<hr>
|
||||
<tt-input label="Gesamtrabatt (%)" v-model="offer.totalDiscount" sm row type="number"/>
|
||||
<tt-input label="Gesamtsumme" v-model="offerTotalPrice" sm row type="number" disabled/>
|
||||
<tt-select label="Zahlungskonditionen" :options="paymentTerms" sm row v-model="offer.paymentTerms"/>
|
||||
<tt-select label="Lieferkonditionen" :options="deliveryTerms" sm row v-model="offer.deliveryTerms"/>
|
||||
<tt-textarea label="Schlusstext" sm rows="11" row v-model="offer.closingText"/>
|
||||
<hr>
|
||||
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
|
||||
</div>
|
||||
|
||||
|
||||
<template v-slot:footer-prepend>
|
||||
<tt-input placeholder="Vorlagenname" no-form-group v-model="templateName"/>
|
||||
<tt-button text="Als Vorlage speichern" @click="saveTemplate" icon="fas fa-save" additional-class="btn-success"/>
|
||||
</template>
|
||||
</tt-modal>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
|
||||
window: window,
|
||||
positionsConfig: {
|
||||
fields: {
|
||||
article: {
|
||||
type: 'autocomplete',
|
||||
label: 'Artikel',
|
||||
apiUrl: '/WarehouseArticle/autoComplete',
|
||||
window: window,
|
||||
positionsConfig: {
|
||||
fields: {
|
||||
article: {
|
||||
type: 'autocomplete',
|
||||
label: 'Artikel',
|
||||
apiUrl: '/WarehouseArticle/autoComplete',
|
||||
customFieldReference: 'WarehouseArticle',
|
||||
},
|
||||
amount: {type: 'input', label: 'Menge', inputType: 'number'},
|
||||
unit: {type: 'input', label: 'Einheit'},
|
||||
amount: {type: 'input', label: 'Menge', inputType: 'number'},
|
||||
unit: {type: 'input', label: 'Einheit'},
|
||||
articleNumber: {type: 'input', label: 'Artikelnummer'},
|
||||
isAlternative: {type: 'checkbox', label: 'Alternativposition'},
|
||||
unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'},
|
||||
discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'},
|
||||
unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'},
|
||||
discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'},
|
||||
},
|
||||
validateForm: (formData) => {
|
||||
const requiredFields = ['article', 'amount', 'unitPrice'];
|
||||
@@ -71,45 +80,46 @@ Vue.component('warehouse-offer-modal', {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
paymentTerms: [
|
||||
paymentTerms: [
|
||||
{value: 'net30', text: '30 Tage netto'},
|
||||
{value: 'net60', text: '60 Tage netto'},
|
||||
{value: 'immediate', text: 'Sofort fällig'},
|
||||
],
|
||||
deliveryTerms: [
|
||||
deliveryTerms: [
|
||||
{value: 'ex_works', text: 'Ab Werk'},
|
||||
{value: 'free_delivery', text: 'Frei Haus'},
|
||||
{value: 'fob', text: 'FOB'},
|
||||
],
|
||||
offer: {
|
||||
editor: window.TT_CONFIG['USER_ID'],
|
||||
customerNumber: '',
|
||||
reference: '',
|
||||
purpose: '',
|
||||
customerName: '',
|
||||
customerStreet: '',
|
||||
customerZip: '',
|
||||
customerCity: '',
|
||||
customerVAT: '',
|
||||
positions: [],
|
||||
offer: {
|
||||
editor: window.TT_CONFIG['USER_ID'],
|
||||
customerNumber: '',
|
||||
reference: '',
|
||||
purpose: '',
|
||||
customerName: '',
|
||||
customerStreet: '',
|
||||
customerZip: '',
|
||||
customerCity: '',
|
||||
customerVAT: '',
|
||||
positions: [],
|
||||
alternativePositions: [],
|
||||
totalDiscount: 0,
|
||||
paymentTerms: 'net30',
|
||||
deliveryTerms: 'ex_works',
|
||||
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n' +
|
||||
'\n' +
|
||||
'Auftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n' +
|
||||
'\n' +
|
||||
'Diese Angebot hat eine Gültigkeit von 4 Wochen.\n' +
|
||||
'\n' +
|
||||
'Verrechnung erfolgt nach tatsächlichem Aufwand.\n' +
|
||||
'\n' +
|
||||
'Wir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n' +
|
||||
'\n' +
|
||||
'Sollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.\n' +
|
||||
' ',
|
||||
notes: '',
|
||||
}
|
||||
totalDiscount: 0,
|
||||
paymentTerms: 'net30',
|
||||
deliveryTerms: 'ex_works',
|
||||
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n' +
|
||||
'\n' +
|
||||
'Auftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n' +
|
||||
'\n' +
|
||||
'Diese Angebot hat eine Gültigkeit von 4 Wochen.\n' +
|
||||
'\n' +
|
||||
'Verrechnung erfolgt nach tatsächlichem Aufwand.\n' +
|
||||
'\n' +
|
||||
'Wir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n' +
|
||||
'\n' +
|
||||
'Sollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.\n' +
|
||||
' ',
|
||||
notes: '',
|
||||
},
|
||||
templateName: '',
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -122,6 +132,7 @@ Vue.component('warehouse-offer-modal', {
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.offer.totalAmount = this.offerTotalPrice;
|
||||
if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
||||
|
||||
const url = this.id === 'create'
|
||||
@@ -147,32 +158,107 @@ Vue.component('warehouse-offer-modal', {
|
||||
this.$refs.positionsManager.updateField('unit', response.data.unit);
|
||||
}
|
||||
},
|
||||
async saveTemplate() {
|
||||
if (!this.templateName) return window.notify('error', 'Bitte geben Sie einen Namen für die Vorlage ein.');
|
||||
if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
||||
|
||||
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/createTemplate`, {
|
||||
name: this.templateName,
|
||||
positions: this.offer.positions,
|
||||
totalDiscount: this.offer.totalDiscount,
|
||||
paymentTerms: this.offer.paymentTerms,
|
||||
deliveryTerms: this.offer.deliveryTerms,
|
||||
closingText: this.offer.closingText,
|
||||
notes: this.offer.notes
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message ?? 'Vorlage erfolgreich gespeichert');
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'offer.customerNumber': async function () {
|
||||
if (!this.offer.customerNumber) return;
|
||||
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/Address/api?do=getAddress&id=${this.offer.customerNumber}`);
|
||||
if (response.data.status !== 'OK' || !response.data.result.address) {
|
||||
this.window.notify('error', 'Kundenadresse konnte nicht gefunden werden');
|
||||
return;
|
||||
}
|
||||
|
||||
const address = response.data.result.address;
|
||||
this.offer.customerName = address.company || `${address.firstname} ${address.lastname}`;
|
||||
this.offer.customerStreet = address.street;
|
||||
this.offer.customerZip = address.zip;
|
||||
this.offer.customerCity = address.city;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
offerTotalPrice() {
|
||||
const totalPrice = this.offer.positions.reduce((total, position) => {
|
||||
if (!position.amount) return total;
|
||||
const discount = position.discount ? (position.unitPrice * position.amount) * position.discount / 100 : 0;
|
||||
return total + (position.unitPrice * position.amount) - discount;
|
||||
}, 0);
|
||||
|
||||
return totalPrice - (totalPrice * this.offer.totalDiscount / 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('warehouse-offer', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" @close="offerModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
||||
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
|
||||
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<div>
|
||||
<h5>Notizen</h5>
|
||||
<p>{{ row.notes }}</p>
|
||||
<h5>Verlauf</h5>
|
||||
<ul>
|
||||
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`,
|
||||
<tt-card>
|
||||
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" ref="modal"
|
||||
@close="offerModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" @click="offerTemplatesDropdown = !offerTemplatesDropdown">
|
||||
Angebot aus Vorlage erstellen <i class="fas fa-caret-down"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" :class="{'show': offerTemplatesDropdown}">
|
||||
<li v-for="template in offerTemplates" @click="createOfferFromTemplate(template)">
|
||||
<a class="dropdown-item">{{ template.templateName }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
window: window,
|
||||
offerModalId: null,
|
||||
offerTemplates: [],
|
||||
offerTemplatesDropdown: false,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/getTemplates`);
|
||||
this.offerTemplates = response.data;
|
||||
},
|
||||
methods: {
|
||||
async createOfferFromTemplate(template) {
|
||||
this.offerModalId = 'create';
|
||||
await this.$nextTick();
|
||||
|
||||
this.$refs.modal.offer.positions = JSON.parse(template.positions);
|
||||
this.$refs.modal.offer.totalDiscount = template.totalDiscount;
|
||||
this.$refs.modal.offer.paymentTerms = template.paymentTerms;
|
||||
this.$refs.modal.offer.deliveryTerms = template.deliveryTerms;
|
||||
this.$refs.modal.offer.closingText = template.closingText;
|
||||
this.$refs.modal.offer.notes = template.notes;
|
||||
|
||||
this.window.notify('success', 'Angebot aus Vorlage erstellt');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ Vue.component('tt-input', {
|
||||
hint: String,
|
||||
additionalProps: Object,
|
||||
sm: {type: Boolean, default: false},
|
||||
noFormGroup: {type: Boolean, default: false},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -22,7 +23,7 @@ Vue.component('tt-input', {
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<div :class="{'row': row, 'form-group' : !noFormGroup}">
|
||||
<slot name="prepend"></slot>
|
||||
<label
|
||||
:class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
|
||||
Reference in New Issue
Block a user