1071 lines
50 KiB
JavaScript
1071 lines
50 KiB
JavaScript
Vue.component('change-status-modal', {
|
|
props: {
|
|
orderId: {type: Number, required: true},
|
|
type: {type: String, default: 'accept'}
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
order: null,
|
|
newStatus: 'noChanges',
|
|
note: '',
|
|
file: null,
|
|
uploadedFiles: [],
|
|
deliveryNoteFiles: [],
|
|
sendEmail: false,
|
|
sendEmailViewedPDF: false,
|
|
sendEmailMail: '',
|
|
submitLoading: false,
|
|
deliveredPositions: {}, // To track delivery details for each position
|
|
warehouseLocations: [],
|
|
selectedLocationId: null
|
|
};
|
|
},
|
|
async mounted() {
|
|
const [orderResponse, locationsResponse] = await Promise.all([
|
|
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.orderId}}),
|
|
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getLocations`)
|
|
]);
|
|
|
|
if (orderResponse.data.status === 'cancelled') {
|
|
this.$emit('close');
|
|
window.notify('error', 'Bestellung wurde storniert');
|
|
}
|
|
this.order = orderResponse.data;
|
|
this.warehouseLocations = locationsResponse.data;
|
|
|
|
// Set default location to "K1 Fladnitz 150" if available
|
|
const defaultLocation = this.warehouseLocations.find(loc => loc.text === 'K1 Fladnitz 150');
|
|
this.selectedLocationId = defaultLocation ? defaultLocation.value : (this.warehouseLocations[0]?.value || null);
|
|
|
|
// Initialize deliveredPositions after fetching the order
|
|
if (this.order && this.order.positions) {
|
|
this.order.positions.forEach((pos, index) => {
|
|
this.$set(this.deliveredPositions, index, {
|
|
amount: pos.amount, // Default delivered amount to the ordered amount
|
|
reason: '',
|
|
cancelRest: false,
|
|
articleName: pos.articleName, // Store for easy access in the submit method
|
|
orderedAmount: pos.amount
|
|
});
|
|
});
|
|
}
|
|
},
|
|
computed: {
|
|
movementPreviewCount() {
|
|
if (!this.deliveredPositions) return 0;
|
|
return Object.values(this.deliveredPositions).filter(p => p.amount > 0).length;
|
|
},
|
|
availableStatuses() {
|
|
// This computed property remains unchanged
|
|
switch (this.order.status) {
|
|
case 'new':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'accepted', text: 'Akzeptiert'},
|
|
{value: 'ordered', text: 'Bestellt'},
|
|
{value: 'cancelled', text: 'Storniert'},
|
|
];
|
|
case 'accepted':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'ordered', text: 'Bestellt'},
|
|
{value: 'cancelled', text: 'Storniert'},
|
|
];
|
|
case 'ordered':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'sent', text: 'Versendet'},
|
|
{value: 'fullyDelivered', text: 'Geliefert'},
|
|
{value: 'partiallyDelivered', text: 'Teilweise geliefert'},
|
|
{value: 'cancelled', text: 'Storniert'},
|
|
];
|
|
case 'sent':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'partiallyDelivered', text: 'Teilweise geliefert'},
|
|
{value: 'fullyDelivered', text: 'Geliefert'},
|
|
{value: 'cancelled', text: 'Storniert'},
|
|
];
|
|
case 'partiallyDelivered':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'fullyDelivered', text: 'Geliefert'},
|
|
{value: 'cancelled', text: 'Storniert'},
|
|
];
|
|
case 'fullyDelivered':
|
|
return [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'cancelled', text: 'Storniert'},
|
|
];
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
async handleFileUpload(event, isDeliveryNote = false) {
|
|
const files = event.target.files;
|
|
if (!files.length) return;
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/uploadFile`, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
});
|
|
|
|
if (response.data.success) {
|
|
const fileEntry = {
|
|
id: response.data.fileId,
|
|
name: file.name
|
|
};
|
|
if (isDeliveryNote) {
|
|
this.deliveryNoteFiles.push(fileEntry);
|
|
} else {
|
|
this.uploadedFiles.push(fileEntry);
|
|
}
|
|
window.notify('success', `Datei "${file.name}" erfolgreich hochgeladen`);
|
|
} else {
|
|
window.notify('error', `Datei "${file.name}" Upload fehlgeschlagen: ${response.data.error || 'Unbekannter Fehler'}`);
|
|
}
|
|
} catch (error) {
|
|
window.notify('error', `Fehler beim Hochladen von "${file.name}"`);
|
|
}
|
|
}
|
|
event.target.value = '';
|
|
},
|
|
removeFile(index) {
|
|
this.uploadedFiles.splice(index, 1)
|
|
},
|
|
removeDeliveryNoteFile(index) {
|
|
this.deliveryNoteFiles.splice(index, 1)
|
|
},
|
|
async submit() {
|
|
this.submitLoading = true;
|
|
if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailMail) {
|
|
window.notify('error', 'Bitte geben Sie eine E-Mail-Adresse ein');
|
|
this.submitLoading = false;
|
|
return;
|
|
} else if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailViewedPDF) {
|
|
window.notify('error', 'Bitte öffnen Sie das PDF bevor Sie die E-Mail senden');
|
|
this.submitLoading = false;
|
|
return;
|
|
} else if (this.newStatus === 'accepted' && this.sendEmail && this.sendEmailMail) {
|
|
if (this.order.sendShippingNote > 0) {
|
|
const shippingNoteId = await this.generateShippingNote();
|
|
await this.submitEmail(shippingNoteId);
|
|
} else await this.submitEmail();
|
|
}
|
|
|
|
const fileIds = this.uploadedFiles.map(file => file.id);
|
|
const deliveryNoteFileIds = this.deliveryNoteFiles.map(file => file.id);
|
|
|
|
// Prepare delivery data if the status is related to delivery
|
|
let deliveryData = null;
|
|
if (this.newStatus === 'partiallyDelivered' || this.newStatus === 'fullyDelivered') {
|
|
deliveryData = this.deliveredPositions;
|
|
}
|
|
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createNewLogAction`, {
|
|
orderId: this.order.id,
|
|
status: this.newStatus,
|
|
note: this.note,
|
|
fileIds: JSON.stringify(fileIds),
|
|
deliveryData: deliveryData, // Send the new delivery data to the backend
|
|
locationId: this.selectedLocationId,
|
|
deliveryNoteFileIds: deliveryNoteFileIds
|
|
});
|
|
|
|
if (response.data.success) {
|
|
this.$emit('close');
|
|
window.notify('success', response.data.message ?? 'Status erfolgreich geändert');
|
|
} else {
|
|
window.notify('error',
|
|
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
this.submitLoading = false;
|
|
},
|
|
async generateShippingNote() {
|
|
// This method remains unchanged
|
|
const positions = this.order.positions.map(position => ({
|
|
article: position.article,
|
|
amount: position.amount,
|
|
price: position.buyPrice
|
|
}));
|
|
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/create`,
|
|
{
|
|
"billingAddressId": this.order.sendShippingNote,
|
|
"deliveryAddressName": this.order.delAddrName,
|
|
"deliveryAddressLine": this.order.delAddrLine,
|
|
"deliveryAddressPLZ": this.order.delAddrPLZ,
|
|
"deliveryAddressCity": this.order.delAddrCity,
|
|
"deliveryAddressEMail": this.order.delAddrEMail,
|
|
"textElements": [],
|
|
"hoursEntries": [],
|
|
"positions": positions,
|
|
"note": "Bestellung #" + this.order.orderNumber,
|
|
"status": "new"
|
|
});
|
|
|
|
return response.data.id;
|
|
},
|
|
async submitEmail(shippingNote = null) {
|
|
// This method remains unchanged
|
|
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/sendEmail?id=${this.order.id}&email=${this.sendEmailMail}${shippingNote ? '&shippingNote=' + shippingNote : ''}`);
|
|
|
|
if (response.data.success) {
|
|
window.notify('success', response.data.message);
|
|
} else {
|
|
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<tt-modal :show="true" :delete="false" @submit="submit" @update:show="$emit('close')" title="Status ändern" :save-loading="submitLoading">
|
|
<tt-loader :absolute="false" v-if="!order"/>
|
|
<template v-else>
|
|
<tt-select label="Neuer Status" v-model="newStatus" :options="availableStatuses" sm row/>
|
|
|
|
<div class="form-group" style="margin: 10px 0">
|
|
<label>Dateiupload (Mehrere)</label>
|
|
<input type="file" class="form-control" @change="handleFileUpload" multiple/>
|
|
</div>
|
|
|
|
<div v-if="uploadedFiles.length" class="upload-success-alert">
|
|
<div class="alert-header">
|
|
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
|
<span v-if="uploadedFiles.length === 1">Datei erfolgreich hochgeladen</span>
|
|
<span v-else>Dateien erfolgreich hochgeladen</span>
|
|
</div>
|
|
<ul class="file-list">
|
|
<li v-for="(file, index) in uploadedFiles" :key="file.id" class="file-item">
|
|
<i class="fa fa-file" aria-hidden="true"></i>
|
|
<span class="file-name">{{ file.name }}</span>
|
|
<button type="button" class="remove-btn" @click="removeFile(index)">
|
|
<i class="fa fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<tt-textarea label="Bemerkung" v-model="note" sm/>
|
|
|
|
<div v-if="newStatus === 'partiallyDelivered' || newStatus === 'fullyDelivered'">
|
|
<h4 class="mt-3">Lagerstandort</h4>
|
|
<tt-select label="Lagerstandort für Einbuchung"
|
|
v-model="selectedLocationId"
|
|
:options="warehouseLocations"
|
|
sm row/>
|
|
|
|
<h4 class="mt-3">Positionen Lieferung erfassen</h4>
|
|
<div style="display: grid; grid-template-columns: 2fr 1fr 1fr 2fr 1fr; grid-gap: 10px; font-weight: bold; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #ccc;">
|
|
<div>Artikel</div>
|
|
<div>Bestellt</div>
|
|
<div>Geliefert</div>
|
|
<div>Grund für Abweichung</div>
|
|
<div class="text-center">Rest stornieren</div>
|
|
</div>
|
|
<template v-for="(position, index) in order.positions">
|
|
<div :key="index" style="display: grid; grid-template-columns: 2fr 1fr 1fr 2fr 1fr; grid-gap: 10px; align-items: center; margin-bottom: 15px;">
|
|
<div>{{ position.articleName }}</div>
|
|
<div class="text-center">{{ position.amount }}</div>
|
|
<div>
|
|
<tt-input type="number"
|
|
:max="position.amount"
|
|
min="0"
|
|
v-model.number="deliveredPositions[index].amount"
|
|
sm
|
|
no-form-group
|
|
/>
|
|
</div>
|
|
<div>
|
|
<tt-input type="text"
|
|
v-if="deliveredPositions[index].amount < position.amount"
|
|
v-model="deliveredPositions[index].reason"
|
|
placeholder="z.B. Lieferschaden"
|
|
sm
|
|
no-form-group
|
|
/>
|
|
</div>
|
|
<div class="text-center">
|
|
<input type="checkbox"
|
|
v-if="deliveredPositions[index].amount < position.amount && deliveredPositions[index].amount !== ''"
|
|
v-model="deliveredPositions[index].cancelRest"
|
|
class="form-check-input"
|
|
style="position: relative; margin-left: auto; margin-right: auto;"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="alert alert-info mt-3" v-if="movementPreviewCount > 0">
|
|
<i class="fas fa-info-circle"></i>
|
|
Es werden <strong>{{ movementPreviewCount }}</strong> Lagerbewegung(en) erstellt.
|
|
</div>
|
|
|
|
<h4 class="mt-3">Lieferschein Foto</h4>
|
|
<div class="form-group">
|
|
<label>Lieferschein hochladen</label>
|
|
<input type="file" class="form-control" @change="handleFileUpload($event, true)" multiple accept="image/*,.pdf"/>
|
|
</div>
|
|
<div v-if="deliveryNoteFiles.length" class="upload-success-alert mb-3">
|
|
<div class="alert-header">
|
|
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
|
<span>Lieferschein hochgeladen</span>
|
|
</div>
|
|
<ul class="file-list">
|
|
<li v-for="(file, index) in deliveryNoteFiles" :key="file.id" class="file-item">
|
|
<i class="fa fa-file" aria-hidden="true"></i>
|
|
<span class="file-name">{{ file.name }}</span>
|
|
<button type="button" class="remove-btn" @click="removeDeliveryNoteFile(index)">
|
|
<i class="fa fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="newStatus === 'accepted'">
|
|
<h4>E-Mail verschicken?</h4>
|
|
|
|
<tt-button
|
|
additional-class="btn-outline-primary"
|
|
icon="fa fa-file-pdf text-danger"
|
|
text="PDF anzeigen"
|
|
@click="sendEmailViewedPDF = true;window.open(window.TT_CONFIG.BASE_PATH + '/WarehouseOrder/createPDF?id=' + order.id)"
|
|
></tt-button>
|
|
|
|
<div class="mt-2 d-flex align-items-center">
|
|
<div class="mr-2">E-Mail senden:</div>
|
|
<label class="ios-switch-wrapper" :class="{'disabled': !sendEmailViewedPDF}">
|
|
<input
|
|
type="checkbox"
|
|
v-model="sendEmail"
|
|
:disabled="!sendEmailViewedPDF"
|
|
>
|
|
<span class="ios-switch-slider"></span>
|
|
</label>
|
|
<div v-if="!sendEmailViewedPDF" class="text-muted ml-2 text-danger">
|
|
Bitte erst PDF ansehen
|
|
</div>
|
|
</div>
|
|
<tt-input v-if="sendEmail" label="E-Mail-Adresse" v-model="sendEmailMail" sm row/>
|
|
</div>
|
|
</template>
|
|
</tt-modal>
|
|
`
|
|
});
|
|
|
|
// The other components in WarehouseOrder.js (warehouse-order-modal, tt-file, etc.) remain unchanged.
|
|
// I am including them here to provide the full file content.
|
|
Vue.component('warehouse-order-modal', {
|
|
props: {
|
|
id: {type: [String, Number], required: true},
|
|
mode: {type: String, default: 'sign'}
|
|
},
|
|
template: `
|
|
<tt-modal :show="true"
|
|
@submit="submit"
|
|
@delete="deleteOrder"
|
|
:delete="false"
|
|
:title="id === 'create' ? 'Bestellung erstellen' : \`Bestellung #\${id} bearbeiten\`"
|
|
@update:show="$emit('close')">
|
|
<div style="width: 99%">
|
|
<h4 class="text-center">Bestelldetails</h4>
|
|
<tt-select label="Bearbeiter (XINON)"
|
|
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
|
|
sm
|
|
row
|
|
v-model="order.editor"/>
|
|
<tt-input label="Externe Referenz" v-model="order.extReference" sm row/>
|
|
|
|
<hr>
|
|
<h4 class="text-center">Positionen</h4>
|
|
<tt-positions-manager
|
|
ref="positionsManager"
|
|
v-model="order.positions"
|
|
:config="positionsConfig"
|
|
@updateField-article="fetchDistributors"
|
|
@updateField-article_text="fetchDistributors"
|
|
@updateField-distributorId="fetchDistributorData"
|
|
>
|
|
<template #form-actions-append>
|
|
<tt-button
|
|
v-if="!isNaN(parseInt($refs.positionsManager?.formData?.article))"
|
|
text="Zum Artikel"
|
|
sm
|
|
additional-class="btn-outline-primary"
|
|
@click="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle?showId=' + $refs.positionsManager.formData.article)"/>
|
|
|
|
<tt-button
|
|
v-if="!isNaN(parseInt($refs.positionsManager?.formData?.article)) && !isNaN(parseInt($refs.positionsManager?.formData?.distributorId))"
|
|
text="Preis übern."
|
|
sm
|
|
additional-class="btn-outline-success"
|
|
@click="updateArticlePriceForDistributor($refs.positionsManager.formData.article, $refs.positionsManager.formData.distributorId, $refs.positionsManager.formData.buyPrice)"/>
|
|
</template>
|
|
</tt-positions-manager>
|
|
|
|
<hr>
|
|
<h4 class="text-center">Lieferadresse</h4>
|
|
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 2fr 1fr 1fr 2fr;">
|
|
<tt-input label="Name" v-model="order.delAddrName" sm/>
|
|
<tt-input label="Straße" v-model="order.delAddrLine" sm/>
|
|
<tt-input label="PLZ" v-model="order.delAddrPLZ" sm/>
|
|
<tt-input label="Ort" v-model="order.delAddrCity" sm/>
|
|
<tt-input label="E-Mail" v-model="order.delAddrEMail" sm/>
|
|
</div>
|
|
|
|
<template v-if="id === 'create' && order.delAddrLine !== 'Fladnitz im Raabtal 150'">
|
|
<div class="mt-2 d-flex align-items-center">
|
|
<div class="mr-2">Lieferschein erstellen und anhängen?</div>
|
|
<label class="ios-switch-wrapper">
|
|
<input type="checkbox" v-model="showSendShippingNote">
|
|
<span class="ios-switch-slider"></span>
|
|
</label>
|
|
</div>
|
|
<template v-if="showSendShippingNote">
|
|
<hr>
|
|
<tt-autocomplete v-model="order.sendShippingNote"
|
|
:api-url="window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1'"
|
|
label="Rechnungsadresse"
|
|
sm
|
|
row/>
|
|
</template>
|
|
</template>
|
|
|
|
<hr>
|
|
<tt-textarea label="Interne Notiz" v-model="order.note" sm row/>
|
|
</div>
|
|
</tt-modal>
|
|
`,
|
|
|
|
data() {
|
|
return {
|
|
window: window,
|
|
showSendShippingNote: null,
|
|
lastDistributorFetch: null,
|
|
positionsConfig: {
|
|
customOrdering: 'distributorId',
|
|
fields: {
|
|
article: {
|
|
type: 'input-article',
|
|
label: 'Artikel',
|
|
apiUrl: '/WarehouseArticle/autoComplete',
|
|
customFieldReference: 'WarehouseArticle',
|
|
emitDisplayValue: true,
|
|
},
|
|
distributorId: {type: 'select', label: 'Lieferant', options: [], customFieldReference: 'WarehouseDistributor'},
|
|
distributorArticleNumber: {type: 'input', label: 'Lieferant Art-Nr.'},
|
|
amount: {type: 'input', label: 'Menge', inputType: 'number'},
|
|
buyPrice: {type: 'input', label: 'Einkaufspreis', inputType: 'number'},
|
|
verwendung: {type: 'input', label: 'Verwendung'},
|
|
},
|
|
validateFormOptions: [
|
|
{key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
|
|
{key: 'distributorId', message: 'Bitte füllen Sie den Lieferanten aus'},
|
|
{key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
|
|
{key: 'buyPrice', message: 'Bitte füllen Sie den Einkaufspreis aus'}
|
|
],
|
|
},
|
|
order: {
|
|
extReference: '',
|
|
delAddrName: 'XINON GmbH',
|
|
delAddrLine: 'Fladnitz im Raabtal 150',
|
|
delAddrPLZ: '8322',
|
|
delAddrCity: 'Studenzen',
|
|
delAddrEMail: 'einkauf@xinon.at',
|
|
note: '',
|
|
editor: window.TT_CONFIG['USER_ID'],
|
|
sendShippingNote: null,
|
|
positions: [],
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
if (this.id !== 'create') {
|
|
const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById?disableParse`, {params: {id: this.id}});
|
|
this.order = {...data, positions: JSON.parse(data.positions)};
|
|
return;
|
|
}
|
|
|
|
this.order.editor = parseInt(window.TT_CONFIG['USER_ID']);
|
|
|
|
const orderRequests = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
|
|
if (!orderRequests) return;
|
|
|
|
for (const orderRequest of orderRequests) {
|
|
const positions = JSON.parse(orderRequest.positions);
|
|
const parsedPositions = await Promise.all(positions.map(async p => {
|
|
const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`,
|
|
{params: {articleId: p.articleId}}))
|
|
.data
|
|
.filter(d => !isNaN(d.purchasePrice))
|
|
.sort((a, b) => a.purchasePrice - b.purchasePrice)[0];
|
|
return {
|
|
article: p.articleId,
|
|
amount: p.amount,
|
|
buyPrice: distributor.purchasePrice,
|
|
distributorId: distributor.id,
|
|
distributorArticleNumber: distributor.externalArticleNumber,
|
|
verwendung: `${p.hasOwnProperty('purpose') ? p.purpose : ''} [Bestellwunsch: #${orderRequest.id}]`,
|
|
linkedOrderRequestId: orderRequest.id
|
|
};
|
|
}));
|
|
this.order.positions = [...this.order.positions, ...parsedPositions];
|
|
}
|
|
|
|
localStorage.removeItem('WarehouseOrder_create');
|
|
},
|
|
methods: {
|
|
async submit() {
|
|
if (this.order.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
|
|
|
if (this.id === 'create') {
|
|
const distributorIds = [...new Set(this.order.positions.map(position => position.distributorId))];
|
|
|
|
for (const distributorId of distributorIds) {
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/create`, {
|
|
...this.order,
|
|
distributorId,
|
|
positions: this.order.positions.filter(position => position.distributorId === distributorId)
|
|
}
|
|
);
|
|
if (response.data.success) {
|
|
this.$emit('close');
|
|
window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt');
|
|
} else window.notify('error',
|
|
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
} else {
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/update`, this.order);
|
|
if (response.data.success) {
|
|
this.$emit('close');
|
|
window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert');
|
|
} else window.notify('error',
|
|
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
},
|
|
async deleteOrder() {
|
|
if (!window.confirm('Bestellung wirklich löschen?')) return;
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/delete`, {id: this.id});
|
|
if (response.data.success) {
|
|
this.$emit('close');
|
|
window.notify('success', response.data.message || 'Bestellung erfolgreich gelöscht');
|
|
} else window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
|
},
|
|
async fetchDistributors(article) {
|
|
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
|
|
console.log(article);
|
|
const params = typeof article === 'string' ? {allDistributor: true} : {articleId: article};
|
|
if (JSON.stringify(params) === JSON.stringify(this.lastDistributorFetch)) return;
|
|
|
|
this.lastDistributorFetch = params;
|
|
const response = await axios.get(url, {params});
|
|
this.positionsConfig.fields.distributorId.options = response.data.map(distributor => ({
|
|
value: distributor.id,
|
|
text: distributor.name,
|
|
externalArticleNumber: distributor.externalArticleNumber || null,
|
|
purchasePrice: distributor.purchasePrice || null,
|
|
}));
|
|
},
|
|
async fetchDistributorData(distributorId) {
|
|
if (distributorId && typeof this.$refs.positionsManager.formData.article === 'number') {
|
|
const distributor = this.positionsConfig.fields.distributorId.options.find(distributor => parseInt(distributor.value) ===
|
|
parseInt(distributorId));
|
|
this.$refs.positionsManager.updateField('distributorArticleNumber', distributor.externalArticleNumber);
|
|
this.$refs.positionsManager.updateField('buyPrice', distributor.purchasePrice);
|
|
}
|
|
},
|
|
async updateArticlePriceForDistributor(articleId, distributorId, buyPrice) {
|
|
if (!articleId || !distributorId || !buyPrice) return;
|
|
const res = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticleDistributor/get`, {
|
|
filters: { articleId, distributorId }
|
|
})
|
|
const current = res.data.rows[0];
|
|
if (current && current.purchasePrice === buyPrice) {
|
|
window.notify('info', 'Preis ist bereits aktuell');
|
|
return;
|
|
}
|
|
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticleDistributor/update`, {
|
|
...current,
|
|
purchasePrice: buyPrice,
|
|
});
|
|
|
|
if (response.data.success) {
|
|
window.notify('success', 'Preis erfolgreich aktualisiert');
|
|
} else {
|
|
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
'order.positions': {
|
|
handler(newPositions) {
|
|
if (this.id !== 'create' && new Set(newPositions.map(p => p.distributorId)).size > 1) {
|
|
window.notify('error', 'Eine bestehende Bestellung kann nur Positionen vom gleichen Lieferanten enthalten.');
|
|
this.order.positions = newPositions.filter(p => p.distributorId === this.order.distributorId);
|
|
}
|
|
},
|
|
deep: true
|
|
}
|
|
}
|
|
,
|
|
});
|
|
|
|
Vue.component('tt-file', {
|
|
props: ['id'],
|
|
data: () => ({file: null}),
|
|
async mounted() {
|
|
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/File/getById`, {params: {id: this.id}});
|
|
this.file = response.data;
|
|
},
|
|
template: `
|
|
<div>
|
|
<a :href="'/File/download?id=' + id" target="_blank" v-if="file">{{ file.filename }}</a>
|
|
<template v-else>
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
|
</template>
|
|
</div>
|
|
`
|
|
})
|
|
|
|
|
|
Vue.component('warehouse-order-detail', {
|
|
template: `
|
|
<div class="order-detail-container">
|
|
<template v-if="loading">
|
|
<div class="loading-spinner">
|
|
<div class="spinner-border text-primary" role="status"><span class="sr-only">Laden...</span></div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<!-- HEADER -->
|
|
<div class="order-header">
|
|
<div class="order-info">
|
|
<span class="order-number">#{{ order.orderNumber }}</span>
|
|
<div class="order-meta">
|
|
<span><i class="fas fa-truck"></i> {{ order.distributorName || 'Kein Lieferant' }}</span>
|
|
<span><i class="fas fa-box"></i> {{ order.positions?.length || 0 }} Positionen</span>
|
|
<span><i class="fas fa-euro-sign"></i> {{ formatCurrency(grandTotal) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="order-actions">
|
|
<span :class="['status-badge', 'status-' + order.status]">{{ statusLabel }}</span>
|
|
<button v-if="!showStatusForm && order.status !== 'cancelled' && order.status !== 'fullyDelivered'"
|
|
@click="toggleStatusForm"
|
|
class="btn btn-primary btn-sm">
|
|
<i class="fas fa-edit"></i> Status ändern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- INLINE STATUS FORM -->
|
|
<div v-if="showStatusForm" class="status-form-container">
|
|
<div class="status-form-header">
|
|
<div>
|
|
<label class="form-label"><strong>Neuer Status</strong></label>
|
|
<select v-model="newStatus" class="form-control form-control-sm">
|
|
<option v-for="s in availableStatuses" :value="s.value">{{ s.text }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="isDeliveryStatus">
|
|
<label class="form-label"><strong>Lagerstandort</strong></label>
|
|
<select v-model="selectedLocationId" class="form-control form-control-sm">
|
|
<option v-for="loc in warehouseLocations" :value="loc.value">{{ loc.text }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="isDeliveryStatus">
|
|
<table class="delivery-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Artikel</th>
|
|
<th style="width: 80px;">Bestellt</th>
|
|
<th style="width: 100px;">Geliefert</th>
|
|
<th>Grund für Abweichung</th>
|
|
<th style="width: 80px; text-align: center;">Rest stornieren</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(pos, index) in order.positions" :key="index">
|
|
<td>{{ pos.articleName }}</td>
|
|
<td class="text-center">{{ pos.amount }}</td>
|
|
<td>
|
|
<input type="number" v-model.number="deliveredPositions[index].amount" :max="pos.amount" min="0"/>
|
|
</td>
|
|
<td>
|
|
<input type="text"
|
|
v-if="deliveredPositions[index].amount < pos.amount"
|
|
v-model="deliveredPositions[index].reason"
|
|
placeholder="z.B. Lieferschaden"/>
|
|
</td>
|
|
<td class="text-center">
|
|
<input type="checkbox"
|
|
v-if="deliveredPositions[index].amount < pos.amount"
|
|
v-model="deliveredPositions[index].cancelRest"/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div v-if="movementPreviewCount > 0" class="alert alert-info" style="margin: 12px 0;">
|
|
<i class="fas fa-info-circle"></i>
|
|
Es werden <strong>{{ movementPreviewCount }}</strong> Lagerbewegung(en) erstellt.
|
|
</div>
|
|
</template>
|
|
|
|
<div class="file-upload-row">
|
|
<div class="file-upload-item">
|
|
<label class="form-label"><i class="fas fa-file"></i> Datei anhängen</label>
|
|
<input type="file" class="form-control form-control-sm" @change="handleFileUpload($event, false)" multiple/>
|
|
<div v-if="uploadedFiles.length" class="mt-2">
|
|
<span v-for="(f, i) in uploadedFiles" :key="f.id" class="badge bg-success me-1">
|
|
{{ f.name }} <i class="fas fa-times" style="cursor:pointer" @click="uploadedFiles.splice(i, 1)"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="isDeliveryStatus" class="file-upload-item">
|
|
<label class="form-label"><i class="fas fa-camera"></i> Lieferschein Foto</label>
|
|
<input type="file" class="form-control form-control-sm" @change="handleFileUpload($event, true)" accept="image/*,.pdf"/>
|
|
<div v-if="deliveryNoteFiles.length" class="mt-2">
|
|
<span v-for="(f, i) in deliveryNoteFiles" :key="f.id" class="badge bg-primary me-1">
|
|
{{ f.name }} <i class="fas fa-times" style="cursor:pointer" @click="deliveryNoteFiles.splice(i, 1)"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 12px;">
|
|
<label class="form-label"><i class="fas fa-comment"></i> Bemerkung</label>
|
|
<textarea v-model="note" class="form-control form-control-sm" rows="2" placeholder="Optionale Bemerkung..."></textarea>
|
|
</div>
|
|
|
|
<div class="status-form-actions">
|
|
<button @click="cancelStatusChange" class="btn btn-outline-secondary btn-sm">Abbrechen</button>
|
|
<button @click="submitStatusChange" :disabled="isSubmitting" class="btn btn-success btn-sm">
|
|
<i class="fas fa-check"></i> {{ isSubmitting ? 'Wird gespeichert...' : 'Speichern' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- POSITIONEN -->
|
|
<div class="section-title"><i class="fas fa-list"></i> Positionen</div>
|
|
<div class="positions-container">
|
|
<table class="positions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Artikel</th>
|
|
<th>Menge</th>
|
|
<th>Einzelpreis</th>
|
|
<th>Summe</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="p in order.positions" :key="p.article">
|
|
<td>{{ p.articleName }}</td>
|
|
<td>{{ p.amount }} Stk</td>
|
|
<td>{{ formatCurrency(p.buyPrice) }}</td>
|
|
<td>{{ formatCurrency(p.amount * p.buyPrice) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="total-row">
|
|
<td>Gesamtsumme</td>
|
|
<td></td>
|
|
<td></td>
|
|
<td>{{ formatCurrency(grandTotal) }}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- LAGERBEWEGUNGEN -->
|
|
<template v-if="linkedMovements?.length > 0">
|
|
<div class="section-title"><i class="fas fa-warehouse"></i> Lagerbewegungen</div>
|
|
<table class="movements-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nummer</th>
|
|
<th>Artikel</th>
|
|
<th>Menge</th>
|
|
<th>Lagerort</th>
|
|
<th>Datum</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="m in linkedMovements" :key="m.id">
|
|
<td><a :href="window.TT_CONFIG.BASE_PATH + '/WarehouseMovement?showId=' + m.id" target="_blank">{{ m.movementNumber }}</a></td>
|
|
<td>{{ m.articleName }}</td>
|
|
<td class="movement-qty">+{{ m.quantity }}</td>
|
|
<td>{{ m.locationName }}</td>
|
|
<td>{{ formatDate(m.create) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
|
|
<!-- AKTIVITÄT / TIMELINE -->
|
|
<template v-if="sortedLog?.length > 0">
|
|
<div class="section-title"><i class="fas fa-history"></i> Aktivität</div>
|
|
<div class="timeline-container">
|
|
<div class="timeline">
|
|
<div v-for="(log, index) in sortedLog" :key="log.id || index" class="timeline-item" :class="{ 'is-first': index === 0 }">
|
|
<div class="timeline-marker"></div>
|
|
<div class="timeline-content">
|
|
<div class="timeline-header">
|
|
<span class="timeline-date">{{ formatDate(log.create) }}</span>
|
|
<span class="timeline-author">{{ getUserName(log.createBy) }}</span>
|
|
</div>
|
|
<div class="timeline-body">{{ log.message }}</div>
|
|
<div v-if="log.fileIds && JSON.parse(log.fileIds).length > 0" class="timeline-files">
|
|
<a v-for="fileId in JSON.parse(log.fileIds)" :key="fileId" :href="'/File/download?id=' + fileId" target="_blank">
|
|
<i class="fas fa-paperclip"></i> Anhang
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
`,
|
|
props: ['id'],
|
|
data() {
|
|
return {
|
|
order: {},
|
|
orderLog: null,
|
|
linkedMovements: [],
|
|
loading: true,
|
|
window: window,
|
|
// Status form state
|
|
showStatusForm: false,
|
|
newStatus: 'noChanges',
|
|
selectedLocationId: null,
|
|
warehouseLocations: [],
|
|
deliveredPositions: {},
|
|
uploadedFiles: [],
|
|
deliveryNoteFiles: [],
|
|
note: '',
|
|
isSubmitting: false
|
|
};
|
|
},
|
|
computed: {
|
|
grandTotal() {
|
|
return this.order.positions?.reduce((sum, p) => sum + (p.amount * p.buyPrice), 0) || 0;
|
|
},
|
|
statusLabel() {
|
|
const labels = {
|
|
new: 'Neu',
|
|
accepted: 'Akzeptiert',
|
|
ordered: 'Bestellt',
|
|
sent: 'Versendet',
|
|
partiallyDelivered: 'Teilweise geliefert',
|
|
fullyDelivered: 'Geliefert',
|
|
cancelled: 'Storniert'
|
|
};
|
|
return labels[this.order.status] || this.order.status;
|
|
},
|
|
availableStatuses() {
|
|
const statusMap = {
|
|
new: [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'accepted', text: 'Akzeptiert'},
|
|
{value: 'ordered', text: 'Bestellt'},
|
|
{value: 'cancelled', text: 'Storniert'}
|
|
],
|
|
accepted: [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'ordered', text: 'Bestellt'},
|
|
{value: 'cancelled', text: 'Storniert'}
|
|
],
|
|
ordered: [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'sent', text: 'Versendet'},
|
|
{value: 'fullyDelivered', text: 'Geliefert'},
|
|
{value: 'partiallyDelivered', text: 'Teilweise geliefert'},
|
|
{value: 'cancelled', text: 'Storniert'}
|
|
],
|
|
sent: [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'partiallyDelivered', text: 'Teilweise geliefert'},
|
|
{value: 'fullyDelivered', text: 'Geliefert'},
|
|
{value: 'cancelled', text: 'Storniert'}
|
|
],
|
|
partiallyDelivered: [
|
|
{value: 'noChanges', text: 'Keine Änderungen'},
|
|
{value: 'fullyDelivered', text: 'Geliefert'},
|
|
{value: 'cancelled', text: 'Storniert'}
|
|
]
|
|
};
|
|
return statusMap[this.order.status] || [{value: 'noChanges', text: 'Keine Änderungen'}];
|
|
},
|
|
isDeliveryStatus() {
|
|
return this.newStatus === 'partiallyDelivered' || this.newStatus === 'fullyDelivered';
|
|
},
|
|
movementPreviewCount() {
|
|
if (!this.deliveredPositions) return 0;
|
|
return Object.values(this.deliveredPositions).filter(p => p.amount > 0).length;
|
|
},
|
|
sortedLog() {
|
|
if (!this.orderLog) return [];
|
|
return [...this.orderLog].sort((a, b) => b.create - a.create);
|
|
}
|
|
},
|
|
async mounted() {
|
|
const [orderResponse, logResponse, movementsResponse, locationsResponse] = await Promise.all([
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById`, {params: {id: this.id}}),
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}}),
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLinkedMovements`, {params: {orderId: this.id}}),
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLocations`)
|
|
]);
|
|
this.order = orderResponse.data;
|
|
this.orderLog = logResponse.data;
|
|
this.linkedMovements = movementsResponse.data || [];
|
|
this.warehouseLocations = locationsResponse.data || [];
|
|
|
|
// Set default location
|
|
const defaultLoc = this.warehouseLocations.find(l => l.text === 'K1 Fladnitz 150');
|
|
this.selectedLocationId = defaultLoc ? defaultLoc.value : (this.warehouseLocations[0]?.value || null);
|
|
|
|
this.loading = false;
|
|
},
|
|
methods: {
|
|
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
|
|
formatCurrency(value) {
|
|
return new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value);
|
|
},
|
|
getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text || 'Unbekannt',
|
|
toggleStatusForm() {
|
|
this.showStatusForm = !this.showStatusForm;
|
|
if (this.showStatusForm) {
|
|
this.initDeliveredPositions();
|
|
}
|
|
},
|
|
initDeliveredPositions() {
|
|
this.deliveredPositions = {};
|
|
if (this.order && this.order.positions) {
|
|
this.order.positions.forEach((pos, index) => {
|
|
this.$set(this.deliveredPositions, index, {
|
|
amount: pos.amount,
|
|
reason: '',
|
|
cancelRest: false,
|
|
articleName: pos.articleName,
|
|
orderedAmount: pos.amount
|
|
});
|
|
});
|
|
}
|
|
},
|
|
async handleFileUpload(event, isDeliveryNote) {
|
|
const files = event.target.files;
|
|
if (!files.length) return;
|
|
|
|
for (const file of files) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
try {
|
|
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/uploadFile`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
});
|
|
if (response.data.success) {
|
|
const entry = { id: response.data.fileId, name: file.name };
|
|
if (isDeliveryNote) {
|
|
this.deliveryNoteFiles.push(entry);
|
|
} else {
|
|
this.uploadedFiles.push(entry);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
window.notify('error', 'Fehler beim Hochladen');
|
|
}
|
|
}
|
|
event.target.value = '';
|
|
},
|
|
cancelStatusChange() {
|
|
this.showStatusForm = false;
|
|
this.newStatus = 'noChanges';
|
|
this.note = '';
|
|
this.uploadedFiles = [];
|
|
this.deliveryNoteFiles = [];
|
|
},
|
|
async submitStatusChange() {
|
|
this.isSubmitting = true;
|
|
try {
|
|
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/createNewLogAction`, {
|
|
orderId: this.order.id,
|
|
status: this.newStatus,
|
|
note: this.note,
|
|
fileIds: JSON.stringify(this.uploadedFiles.map(f => f.id)),
|
|
deliveryData: this.isDeliveryStatus ? this.deliveredPositions : null,
|
|
locationId: this.selectedLocationId,
|
|
deliveryNoteFileIds: this.deliveryNoteFiles.map(f => f.id)
|
|
});
|
|
|
|
if (response.data.success) {
|
|
window.notify('success', 'Status erfolgreich geändert');
|
|
this.cancelStatusChange();
|
|
// Reload data
|
|
const [orderRes, logRes, movRes] = await Promise.all([
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById`, {params: {id: this.id}}),
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}}),
|
|
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLinkedMovements`, {params: {orderId: this.id}})
|
|
]);
|
|
this.order = orderRes.data;
|
|
this.orderLog = logRes.data;
|
|
this.linkedMovements = movRes.data || [];
|
|
// Emit event to refresh table
|
|
this.$emit('status-changed');
|
|
} else {
|
|
window.notify('error', response.data.error || 'Fehler beim Speichern');
|
|
}
|
|
} catch (e) {
|
|
window.notify('error', 'Netzwerkfehler');
|
|
}
|
|
this.isSubmitting = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
Vue.component('warehouse-order', {
|
|
template: `
|
|
<tt-card>
|
|
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="closeModal"/>
|
|
<tt-button text="Bestellung erstellen" @click="orderModalId = 'create'" additional-class="btn-primary"/>
|
|
<tt-table-crud emit-edit
|
|
@openpdf="openPDF"
|
|
@edit="orderModalId = $event.id; $refs.table.$refs.table.refreshTable()" ref="table">
|
|
<template v-slot:expandedRow="{ row }">
|
|
<warehouse-order-detail :id="row.id" @status-changed="refreshTable"/>
|
|
</template>
|
|
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2) }} €</template>
|
|
</tt-table-crud>
|
|
</tt-card>
|
|
`,
|
|
data: () => ({
|
|
orderModalId: null
|
|
}),
|
|
mounted() {
|
|
if (JSON.parse(localStorage.getItem('WarehouseOrder_create'))) this.orderModalId = 'create';
|
|
},
|
|
methods: {
|
|
async closeModal() {
|
|
this.orderModalId = null;
|
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
this.$refs.table.$refs.table.refreshTable();
|
|
},
|
|
refreshTable() {
|
|
this.$refs.table.$refs.table.refreshTable();
|
|
},
|
|
calculateSum: positions => positions.reduce((sum, {amount, buyPrice}) => sum + amount * buyPrice, 0),
|
|
openPDF: order => window.open(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createPDF?id=${order.id}`)
|
|
}
|
|
}); |