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:
Luca Haid
2025-03-31 13:14:15 +00:00
9 changed files with 359 additions and 132 deletions

View File

@@ -169,9 +169,10 @@
<ul class="submenu">
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li class="has-sub-submenu font-weight-bold"><a>XINON</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseArticle")?>"><i class="far fa-fw fa-box text-info"></i> Artikel</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseItem")?>"><i class="far fa-fw fa-boxes text-info"></i> Lagerbestand (WIP)</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrderRecommendation")?>"><i class="far fa-fw fa-box-full text-info"></i> Bestellvorschläge (WIP)</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen (WIP)</a></li><?php endif; ?>
<!-- --><?php //if($me->can("WarehouseAdmin")): ?><!--<li><a href="--><?php //=self::getUrl("WarehouseItem")?><!--"><i class="far fa-fw fa-boxes text-info"></i> Lagerbestand (WIP)</a></li>--><?php //endif; ?>
<!-- --><?php //if($me->can("WarehouseAdmin")): ?><!--<li><a href="--><?php //=self::getUrl("WarehouseOrderRecommendation")?><!--"><i class="far fa-fw fa-box-full text-info"></i> Bestellvorschläge (WIP)</a></li>--><?php //endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOffer")?>"><i class="far fa-fw fa-file-signature text-info"></i> Angebote</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseOrderRequest")?>"><i class="far fa-fw fa-shopping-cart text-info"></i> Bestellwünsche</a></li><?php endif; ?>
<?php if($me->can("WarehouseAdmin") || $me->can("WarehouseUser")): ?><li><a href="<?=self::getUrl("WarehouseShippingNote")?>"><i class="far fa-fw fa-shipping-fast text-info"></i> Lieferscheine</a></li><?php endif; ?>

View File

@@ -2,20 +2,21 @@
class WarehouseOfferController extends TTCrud {
protected string $headerTitle = 'Angebote';
protected string $singleText = 'Angebot';
protected bool $createText = false;
protected array $columns = [
['key' => 'id', 'text' => 'ID', 'modal' => false],
['key' => 'id', 'text' => 'ID', 'modal' => false, 'table' => false],
['key' => 'offerNumber', 'text' => 'Angebotsnummer', 'required' => true, 'modal' => false],
['key' => 'customerNumber', 'text' => 'Kundennummer', 'required' => true, 'modal' => false],
['key' => 'customerName', 'text' => 'Kundenname', 'required' => true, 'modal' => false],
['key' => 'customerCity', 'text' => 'Stadt', 'required' => true, 'modal' => false],
['key' => 'customerVAT', 'text' => 'UID', 'required' => true, 'modal' => false],
['key' => 'editor', 'text' => 'Sachbearbeiter', 'required' => true, 'modal' => false],
['key' => 'editor', 'text' => 'Sachbearbeiter', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
['key' => 'totalAmount', 'text' => 'Gesamtbetrag', 'required' => true, 'modal' => false],
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select']],
['key' => 'status', 'text' => 'Status', 'required' => true],
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
['key' => 'actions',
'text' => 'Aktionen',
'required' => false,
@@ -24,27 +25,19 @@ class WarehouseOfferController extends TTCrud {
];
protected array $permissionCheck = ['WarehouseAdmin'];
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
protected array $additionalActions = [
['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
['key' => 'sendOffer', 'title' => 'Angebot senden', 'class' => 'fas fa-paper-plane text-success']
];
protected array $additionalJS = ['
https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js
https://cdn.jsdelivr.net/npm/vue-draggable-next@2.1.0'];
protected array $infoMessages = [
'create' => 'Angebot wurde erfolgreich erstellt.',
'update' => 'Angebot wurde aktualisiert.',
'delete' => 'Angebot wurde gelöscht',
'noChanges' => 'Keine Änderungen',
'sent' => 'Angebot wurde erfolgreich gesendet',
];
protected function prepareCrudConfig(): void {
$editorColumnIndex = array_search('editor', array_column($this->columns, 'key'));
$this->columns[$editorColumnIndex]['modal']['items'] = array_map(function ($user) {
return ['value' => intval($user->id), 'text' => $user->name];
}, UserModel::search(['employee' => true]));
}
protected function beforeCreate(): bool {
$currentCount = WarehouseOfferModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]);
$this->postData['offerNumber'] = 'AN' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
$this->postData['status'] = 'new';
return true;
}
@@ -57,4 +50,24 @@ class WarehouseOfferController extends TTCrud {
protected function getHistoryAction() {
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
}
protected function createTemplateAction() {
$_POST = json_decode(file_get_contents('php://input'), true);
$templateId = WarehouseOfferTemplateModel::create([
'templateName' => $_POST['name'],
'positions' => $_POST['positions'],
'totalDiscount' => $_POST['totalDiscount'],
'paymentTerms' => $_POST['paymentTerms'],
'deliveryTerms' => $_POST['deliveryTerms'],
'closingText' => $_POST['closingText'],
'notes' => $_POST['notes'],
]);
self::returnJson(['success' => true, 'id' => $templateId]);
}
protected function getTemplatesAction() {
self::returnJson(WarehouseOfferTemplateModel::getAll());
}
}

View File

@@ -7,6 +7,7 @@
*
* @property int $id Unique identifier for the warehouse offer
* @property string $offerNumber Unique offer number
* @property string $reference Reference number for the offer
* @property string $customerNumber Customer number
* @property string $customerName Name of the customer
* @property string $customerStreet Street address of the customer
@@ -30,6 +31,7 @@
class WarehouseOfferModel extends TTCrudBaseModel {
public int $id;
public string $offerNumber;
public string $reference;
public string $customerNumber;
public string $customerName;
public string $customerStreet;
@@ -50,3 +52,31 @@ class WarehouseOfferModel extends TTCrudBaseModel {
public int $create;
public int $createBy;
}
//SQL TO CREATE TABLE
/*
CREATE TABLE `warehouse_offer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`offerNumber` varchar(255) NOT NULL,
`customerNumber` varchar(255) NOT NULL,
`customerName` varchar(255) NOT NULL,
`customerStreet` varchar(255) NOT NULL,
`customerCity` varchar(255) NOT NULL,
`customerZip` varchar(255) NOT NULL,
`customerVAT` varchar(255) NOT NULL,
`editor` int(11) NOT NULL,
`purpose` varchar(255) NOT NULL,
`positions` text NOT NULL,
`alternativePositions` text NOT NULL,
`totalDiscount` float NOT NULL,
`paymentTerms` varchar(255) NOT NULL,
`deliveryTerms` varchar(255) NOT NULL,
`closingText` varchar(255) NOT NULL,
`notes` varchar(255) NOT NULL,
`status` varchar(255) NOT NULL,
`totalAmount` float NOT NULL,
`create` int(11) NOT NULL,
`createBy` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/

View File

@@ -0,0 +1,9 @@
<?php
/**
* @property mixed|null $name
*/
class WarehouseOfferTemplate extends mfBaseModel
{
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Class WarehouseOfferTemplateModel
*
* Represents a warehouse offer template with key details.
*
* @property string $templateName Name of the template
* @property string $positions Details about positions in the offer
* @property float $totalDiscount Total discount applied to the offer
* @property string $paymentTerms Payment terms for the offer
* @property string $deliveryTerms Delivery terms for the offer
* @property string $closingText Closing text for the offer
* @property string $notes Additional notes for the offer
*/
class WarehouseOfferTemplateModel extends TTCrudBaseModel
{
public string $templateName;
public string $positions;
public float $totalDiscount;
public string $paymentTerms;
public string $deliveryTerms;
public string $closingText;
public string $notes;
}

View File

@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify16 extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
$WarehouseArticleTable = $this->table("WarehouseArticle");
$WarehouseArticleTable
->changeColumn("isEShop", "integer", ["default" => 0])
->changeColumn("isEShopHide", "integer", ["default" => 0])
->changeColumn("isSerialDocumentation", "integer", ["default" => 0])
->save();
$WarehouseOfferTable = $this->table("WarehouseOffer", ["id" => false, "primary_key" => "id"]);
$WarehouseOfferTable
->addColumn("id", "integer", ["identity" => true])
->addColumn("offerNumber", "string", ["limit" => 255, "null" => false])
->addColumn("reference", "string", ["limit" => 255, "null" => false])
->addColumn("customerNumber", "string", ["limit" => 255, "null" => false])
->addColumn("customerName", "string", ["limit" => 255, "null" => false])
->addColumn("customerStreet", "string", ["limit" => 255, "null" => false])
->addColumn("customerCity", "string", ["limit" => 255, "null" => false])
->addColumn("customerZip", "string", ["limit" => 255, "null" => false])
->addColumn("customerVAT", "string", ["limit" => 255, "null" => false])
->addColumn("editor", "integer", ["null" => false])
->addColumn("purpose", "string", ["limit" => 255, "null" => false])
->addColumn("positions", "text", ["null" => false])
->addColumn("alternativePositions", "text", ["null" => false])
->addColumn("totalDiscount", "float", ["null" => false])
->addColumn("paymentTerms", "string", ["limit" => 255, "null" => false])
->addColumn("deliveryTerms", "string", ["limit" => 255, "null" => false])
->addColumn("closingText", "text", ["null" => false])
->addColumn("notes", "string", ["limit" => 255, "null" => false])
->addColumn("status", "string", ["limit" => 255, "null" => false])
->addColumn("totalAmount", "float", ["null" => false])
->addColumn("create", "integer", ["null" => false])
->addColumn("createBy", "integer", ["null" => false])
->save();
$WarehouseOfferTemplateTable = $this->table("WarehouseOfferTemplate", ["id" => false, "primary_key" => "id"]);
$WarehouseOfferTemplateTable
->addColumn("id", "integer", ["identity" => true])
->addColumn("templateName", "string", ["limit" => 255, "null" => false])
->addColumn("positions", "text", ["null" => false])
->addColumn("totalDiscount", "float", ["null" => false])
->addColumn("paymentTerms", "string", ["limit" => 255, "null" => false])
->addColumn("deliveryTerms", "string", ["limit" => 255, "null" => false])
->addColumn("closingText", "text", ["null" => false])
->addColumn("notes", "text", ["null" => false])
->save();
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
// change table "WarehouseArticle" and set isEShop, isEShopHide, isSerialDocumentation to no default
$WarehouseArticleTable = $this->table("WarehouseArticle");
$WarehouseArticleTable
->changeColumn("isEShop", "integer", ["default" => null])
->changeColumn("isEShopHide", "integer", ["default" => null])
->changeColumn("isSerialDocumentation", "integer", ["default" => null])
->save();
$this->table("WarehouseOffer")->drop()->save();
$this->table("WarehouseOfferTemplate")->drop()->save();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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');
}
}
});

View File

@@ -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}"