Updated Warehouse
This commit is contained in:
43
Layout/default/WarehouseOrder/PDF_FOOTER.html
Normal file
43
Layout/default/WarehouseOrder/PDF_FOOTER.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Xinon Rechnung</title>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body style="border:0; margin: 0;font-family: sans-serif, Verdana;font-size: 11px;" onload="subst()">
|
||||
|
||||
<script>
|
||||
function subst() {
|
||||
var vars = {};
|
||||
var query_strings_from_url = document.location.search.substring(1).split('&');
|
||||
for (var query_string in query_strings_from_url) {
|
||||
if (query_strings_from_url.hasOwnProperty(query_string)) {
|
||||
var temp_var = query_strings_from_url[query_string].split('=', 2);
|
||||
vars[temp_var[0]] = decodeURI(temp_var[1]);
|
||||
}
|
||||
}
|
||||
var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];
|
||||
for (var css_class in css_selector_classes) {
|
||||
if (css_selector_classes.hasOwnProperty(css_class)) {
|
||||
var element = document.getElementsByClassName(css_selector_classes[css_class]);
|
||||
for (var j = 0; j < element.length; ++j) {
|
||||
element[j].textContent = vars[css_selector_classes[css_class]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="margin-bottom: 16px;height: 1px"></div>
|
||||
<div style="color:grey;text-align: center;margin-bottom: 0">
|
||||
<span>XINON GmbH | Fladnitz 150 | 8322 Studenzen</span><br>
|
||||
<span>Tel.: +43 3115 40800 | E-Mail: office@xinon.at</span><br>
|
||||
<span>UID: ATU68711968 | FN: 416556h | LG: Feldbach</span><br>
|
||||
<span>IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}</span><br>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right">Seite <span class="page"></span> von <span class="topage"></span></div>
|
||||
|
||||
<div style="margin-top: 16px;height: 1px"></div>
|
||||
</body>
|
||||
</html>
|
||||
92
Layout/default/WarehouseOrder/PDF_HEADER.html
Normal file
92
Layout/default/WarehouseOrder/PDF_HEADER.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>XINON Shipping Note Header</title>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif, Verdana;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.customer-details {
|
||||
vertical-align: bottom;
|
||||
font-size: 14px;
|
||||
padding-left: 30pt;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.invoice-details {
|
||||
border: 2px solid #e1e1e1;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.invoice-details td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.invoice-details td:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-top: 24px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
#topSpacer {
|
||||
margin-bottom: 32px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="topSpacer"></div>
|
||||
|
||||
<div style="height: 50px; margin-bottom: 48px">
|
||||
<img alt="Xinon Logo" src="{{ basedir }}/public/assets/images/xinon-full.png" style="text-align:left;height: 85px;">
|
||||
</div>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td class="customer-details" style="float: left">
|
||||
<h3>Lieferant</h3>
|
||||
<div>{{ addressLine_1 }}</div>
|
||||
<div>{{ addressLine_2 }}</div>
|
||||
<div>{{ addressLine_3 }}</div>
|
||||
<div>{{ addressLine_4 }}</div>
|
||||
<div style="margin-bottom: 12pt"></div>
|
||||
<div>{{ externalReference }}</div>
|
||||
</td>
|
||||
<td class="customer-details" align="top">
|
||||
<h3>Rechnungsadresse</h3>
|
||||
<div>{{ billingAddressLine_1 }}</div>
|
||||
<div>{{ billingAddressLine_2 }}</div>
|
||||
<div>{{ billingAddressLine_3 }}</div>
|
||||
<div>{{ billingAddressLine_4 }}</div>
|
||||
<div>{{ billingAddressLine_5 }}</div>
|
||||
<div>{{ billingAddressLine_6 }}</div>
|
||||
</td>
|
||||
<td class="customer-details" style="float: right">
|
||||
<div>{{ shippingAddressLine_1 }}</div>
|
||||
<div>{{ shippingAddressLine_2 }}</div>
|
||||
<div>{{ shippingAddressLine_3 }}</div>
|
||||
<div>{{ shippingAddressLine_4 }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
133
Layout/default/WarehouseOrder/PDF_MAIN.php
Normal file
133
Layout/default/WarehouseOrder/PDF_MAIN.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* @var string $ressourcePathPrefix
|
||||
* @var WarehouseOrderModel $order
|
||||
* @var Array $positions
|
||||
* @var Array $textElements
|
||||
*/
|
||||
$this->setReturnValue(['filename' => $order["id"] . ".pdf"]);
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bestellung</title>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body {
|
||||
margin-top: 0;
|
||||
/*padding-top: 20pt;*/
|
||||
font-family: "Open Sans", sans-serif, Verdana;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.uneven {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
|
||||
table tr td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.additionalRow td:first-child {
|
||||
text-align: left;
|
||||
padding-left: 20pt;
|
||||
}
|
||||
|
||||
th {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
#invoiceTable tr *:nth-child(5),
|
||||
#invoiceTable tr *:nth-child(4),
|
||||
#invoiceTable tr *:nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#invoiceTable tr *:not(:first-child) {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
#invoiceTable tr td {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
tr.position td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr.position td:first-child {
|
||||
vertical-align: middle !important;
|
||||
padding-left: 4pt;
|
||||
}
|
||||
|
||||
#invoiceTable tr td:first-child {
|
||||
max-width: 200pt;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<!--
|
||||
TODO: enable option for showing prices
|
||||
vertauschen
|
||||
Die gelieferte Ware bleibt bis zur vollständigen Bezahlung in unserem Eigentum.
|
||||
-->
|
||||
<h2 style="text-align: center;color: #005384">XINON Lieferantenbestellung vom <?=date("d.m.Y", $order["create"])?></h2>
|
||||
|
||||
<table style="border-collapse: collapse; width: 100%;" id="invoiceTable">
|
||||
<tr style="font-weight: bold; border-bottom: 1px solid black;" class="uneven">
|
||||
<th style="text-align: center;padding-right: 6pt">Position</th>
|
||||
<th style="text-align: center;padding-right: 6pt">Artikel</th>
|
||||
<th style="text-align: center;padding-right: 6pt">Art.-Nr. Lieferant</th>
|
||||
<th style="text-align: right">Menge</th>
|
||||
<th style="text-align: right">Einzelpreis</th>
|
||||
<th style="text-align: right;padding-right: 8pt">Gesamtpreis</th>
|
||||
</tr>
|
||||
<?php $i = 0; foreach($order['positions'] as $p):?>
|
||||
|
||||
<tr class="position <?=($i%2 == 0) ? "even" : "uneven" ?>">
|
||||
<td style="text-align: center;"><?= $i + 1 ?></td>
|
||||
<td style="text-align: left;padding-right: 8pt"><?=$p["articleName"]?></td>
|
||||
<td style="text-align: center;padding-right: 8pt"><?=$p["distributorArticleNumber"]?></td>
|
||||
<td style="text-align: right"><?=$p["amount"]?></td>
|
||||
<td style="text-align: right"><?=number_format($p["buyPrice"], 2, ",", ".")?> €</td>
|
||||
<td style="text-align: right;padding-right: 8pt"><?=number_format($p["amount"] * $p["buyPrice"], 2, ",", ".")?> €</td>
|
||||
</tr>
|
||||
<?php $i++; endforeach;?>
|
||||
<!-- display a grey like header sum with top border to differentiate 2nd last td = Summe , last td is the calculated value both bold-->
|
||||
|
||||
<tr class="uneven">
|
||||
<?php
|
||||
$sum = 0;
|
||||
foreach($order['positions'] as $p){
|
||||
$sum += $p["amount"] * $p["buyPrice"];
|
||||
}
|
||||
?>
|
||||
<td colspan="5" style="text-align: right;border-top: 1px solid black;font-weight: bold
|
||||
;border-bottom: 1px solid black;
|
||||
">Summe</td>
|
||||
<td style="text-align: right;border-top: 1px solid black;font-weight: bold
|
||||
;border-bottom: 1px solid black;
|
||||
"><?=number_format($sum, 2, ",", ".")?> €</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<h3>Anmerkungen</h3>
|
||||
<p>
|
||||
<?=$order["note"]?>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -24,6 +24,8 @@ class WarehouseArticleController extends TTCrud {
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]]
|
||||
];
|
||||
|
||||
protected array $autocompleteColumns = ['articleNumber', 'title', 'description', 'category'];
|
||||
|
||||
protected array $additionalActions = [
|
||||
['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'],
|
||||
['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'],
|
||||
|
||||
9
application/WarehouseOffer/WarehouseOffer.php
Normal file
9
application/WarehouseOffer/WarehouseOffer.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseOffer extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
56
application/WarehouseOffer/WarehouseOfferController.php
Normal file
56
application/WarehouseOffer/WarehouseOfferController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
class WarehouseOfferController extends TTCrud {
|
||||
protected string $headerTitle = 'Angebote';
|
||||
protected bool $createText = false;
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'id', 'text' => 'ID', 'modal' => 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' => 'totalAmount', 'text' => 'Gesamtbetrag', 'required' => true, 'modal' => false],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select']],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']],
|
||||
['key' => 'actions',
|
||||
'text' => 'Aktionen',
|
||||
'required' => false,
|
||||
'modal' => false,
|
||||
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
|
||||
protected array $permissionCheck = ['WarehouseAdmin'];
|
||||
|
||||
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 $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 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);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
||||
}
|
||||
}
|
||||
52
application/WarehouseOffer/WarehouseOfferModel.php
Normal file
52
application/WarehouseOffer/WarehouseOfferModel.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class WarehouseOfferModel
|
||||
*
|
||||
* Represents a warehouse offer with customer details and related metadata.
|
||||
*
|
||||
* @property int $id Unique identifier for the warehouse offer
|
||||
* @property string $offerNumber Unique offer number
|
||||
* @property string $customerNumber Customer number
|
||||
* @property string $customerName Name of the customer
|
||||
* @property string $customerStreet Street address of the customer
|
||||
* @property string $customerCity City of the customer
|
||||
* @property string $customerZip Postal code of the customer
|
||||
* @property string $customerVAT VAT number of the customer
|
||||
* @property int $editor ID of the editor who last modified the offer
|
||||
* @property string $purpose Purpose of the offer
|
||||
* @property string $positions Details about positions in the offer
|
||||
* @property string $alternativePositions Details about alternative 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
|
||||
* @property string $status Current status of the offer
|
||||
* @property float $totalAmount Total amount of the offer
|
||||
* @property int $create Timestamp of the offer creation
|
||||
* @property int $createBy ID of the user who created the offer
|
||||
*/
|
||||
class WarehouseOfferModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $offerNumber;
|
||||
public string $customerNumber;
|
||||
public string $customerName;
|
||||
public string $customerStreet;
|
||||
public string $customerCity;
|
||||
public string $customerZip;
|
||||
public string $customerVAT;
|
||||
public int $editor;
|
||||
public string $purpose;
|
||||
public string $positions;
|
||||
public string $alternativePositions;
|
||||
public float $totalDiscount;
|
||||
public string $paymentTerms;
|
||||
public string $deliveryTerms;
|
||||
public string $closingText;
|
||||
public string $notes;
|
||||
public string $status;
|
||||
public float $totalAmount;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
@@ -1,77 +1,159 @@
|
||||
<?php
|
||||
//TODO: enable switching distributors in the order preview
|
||||
|
||||
class WarehouseOrderController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferantenbestellungen';
|
||||
protected bool $createText = false;
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'id', 'text' => 'ID', 'modal' => false],
|
||||
['key' => 'orderNumber', 'text' => 'Bestellnummer', 'required' => true, 'modal' => false],
|
||||
['key' => 'delAddrCity', 'text' => 'Stadt', 'required' => true, 'modal' => false],
|
||||
['key' => 'delAddrEMail', 'text' => 'E-Mail', 'required' => true, 'modal' => false],
|
||||
['key' => 'delAddrLine', 'text' => 'Adresse', 'required' => true, 'modal' => false],
|
||||
['key' => 'delAddrName', 'text' => 'Name', 'required' => true, 'modal' => false],
|
||||
['key' => 'delAddrPLZ', 'text' => 'PLZ', 'required' => true, 'modal' => false],
|
||||
['key' => 'editor', 'text' => 'Bearbeiter', 'required' => true, 'modal' => false],
|
||||
['key' => 'note', 'text' => 'Notiz', 'required' => true, 'modal' => false],
|
||||
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => false],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']],
|
||||
['key' => 'actions',
|
||||
'text' => 'Aktionen',
|
||||
'required' => false,
|
||||
'modal' => false,
|
||||
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
|
||||
protected array $permissionCheck = ['WarehouseAdmin'];
|
||||
|
||||
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
|
||||
//@formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'id', 'text' => 'ID', 'modal' => false, 'table' => false],
|
||||
['key' => 'orderNumber', 'text' => 'Bestellnummer', 'required' => true, 'modal' => false],
|
||||
['key' => 'distributor', 'text' => 'Lieferant', 'required' => false, 'modal' => false, 'table' => ['filter' => false]],
|
||||
['key' => 'delAddrCity', 'text' => 'Stadt', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'delAddrEMail', 'text' => 'E-Mail', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'delAddrLine', 'text' => 'Adresse', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'delAddrName', 'text' => 'Name', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'delAddrPLZ', 'text' => 'PLZ', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'editor', 'text' => 'Bearbeiter', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
|
||||
['key' => 'note', 'text' => 'Notiz', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'sum', 'text' => 'Summe', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-right']],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => false, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']],
|
||||
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => false, 'table' => false],
|
||||
['key' => 'extReference', 'text' => 'Externe Referenz', 'required' => true, 'modal' => false],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
//@formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.',
|
||||
'update' => 'Bestellung wurde aktualisiert.',
|
||||
'delete' => 'Bestellung wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',];
|
||||
|
||||
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]));
|
||||
|
||||
$statusIndex = array_search('status', array_column($this->columns, 'key'));
|
||||
$this->columns[$statusIndex]['modal']['items'] = [
|
||||
['value' => 'new', 'text' => 'Neu'],
|
||||
['value' => 'accepted', 'text' => 'Akzeptiert'],
|
||||
['value' => 'ordered', 'text' => 'Bestellt'],
|
||||
['value' => 'sent', 'text' => 'Versendet'],
|
||||
['value' => 'partiallyDelivered', 'text' => 'Teilweise geliefert'],
|
||||
['value' => 'fullyDelivered', 'text' => 'Geliefert'],
|
||||
['value' => 'cancelled', 'text' => 'Storniert'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function beforeCreate(): bool {
|
||||
$currentCount = WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]);
|
||||
$this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
|
||||
$this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad(WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]) + 1, 4, '0', STR_PAD_LEFT);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getArticleDistributorDataAction() {
|
||||
$data = [];
|
||||
$article = $this->request->articleId;
|
||||
|
||||
if ($this->request->allDistributor === 'true') {
|
||||
foreach (WarehouseDistributorModel::getAll() as $distributor) {
|
||||
$data[] = [
|
||||
'id' => $distributor->id,
|
||||
'name' => $distributor->name,
|
||||
];
|
||||
}
|
||||
} elseif (!empty($article)) {
|
||||
foreach (WarehouseArticleDistributorModel::getAll(['articleId' => $this->request->articleId]) as $distributor) {
|
||||
$data[] = [
|
||||
'id' => $distributor->distributorId,
|
||||
'name' => WarehouseDistributorModel::get($distributor->distributorId)->name,
|
||||
'purchasePrice' => $distributor->purchasePrice,
|
||||
'externalArticleNumber' => $distributor->externalArticleNumber,
|
||||
];
|
||||
}
|
||||
$articleId = $this->request->articleId;
|
||||
if ($this->request->allDistributor === 'true') self::returnJson(array_map(fn($d) => ['id' => $d->id,
|
||||
'name' => $d->name], WarehouseDistributorModel::getAll()));
|
||||
else if (!empty($articleId)) self::returnJson(array_map(fn($d) => ['id' => $d->distributorId,
|
||||
'name' => WarehouseDistributorModel::get($d->distributorId)->name,
|
||||
'purchasePrice' => $d->purchasePrice,
|
||||
'externalArticleNumber' => $d->externalArticleNumber], WarehouseArticleDistributorModel::getAll(['articleId' => $articleId])));
|
||||
else self::returnJson([]);
|
||||
}
|
||||
|
||||
self::returnJson($data);
|
||||
protected function getByIdParse(array $order): array {
|
||||
$order['positions'] = json_decode($order['positions'], true);
|
||||
|
||||
foreach ($order['positions'] as &$position) {
|
||||
$position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name;
|
||||
$position['articleName'] = WarehouseArticleModel::get($position['article'])->title;
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
return $order;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
||||
protected function createPDFAction() {
|
||||
$order = (array) WarehouseOrderModel::get($this->request->id);
|
||||
$order['positions'] = json_decode($order['positions'], true);
|
||||
// check if all positions have the same distributor
|
||||
$distributorId = $order['positions'][0]['distributorId'];
|
||||
foreach ($order['positions'] as $key => $position) {
|
||||
if ($position['distributorId'] !== $distributorId) {
|
||||
self::returnJson(['error' => 'Die Bestellung enthält Positionen von verschiedenen Lieferanten.']);
|
||||
}
|
||||
|
||||
// we need to get the article name and distributor name for the pdf
|
||||
$position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name;
|
||||
$position['articleName'] = WarehouseArticleModel::get($position['article'])->title;
|
||||
|
||||
$order['positions'][$key] = $position;
|
||||
}
|
||||
|
||||
$pdf_vars = ['order' => $order,
|
||||
'distributor' => WarehouseDistributorModel::get($distributorId),
|
||||
"bank_iban" => TT_INVOICE_BANK_IBAN,
|
||||
"bank_bic" => TT_INVOICE_BANK_BIC,
|
||||
"bank_bank" => TT_INVOICE_BANK_BANK,
|
||||
"bank_owner" => TT_INVOICE_BANK_OWNER];
|
||||
|
||||
|
||||
$countryText = CountryModel::search(['id' => WarehouseDistributorModel::get($distributorId)->countryId])[0]->name;
|
||||
|
||||
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/PDF_HEADER.html");
|
||||
$headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
|
||||
$headerHtml = str_replace("{{ externalReference }}","<strong>Ihre Referenz:</strong> ". $order['extReference'], $headerHtml);
|
||||
|
||||
$headerHtml = str_replace("{{ addressLine_1 }}", WarehouseDistributorModel::get($distributorId)->name, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_2 }}", WarehouseDistributorModel::get($distributorId)->address, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_3 }}", WarehouseDistributorModel::get($distributorId)->plz . " " . WarehouseDistributorModel::get($distributorId)->city, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_4 }}", $countryText, $headerHtml);
|
||||
|
||||
$headerHtml = str_replace("{{ billingAddressLine_1 }}", "Xinon GmbH", $headerHtml);
|
||||
$headerHtml = str_replace("{{ billingAddressLine_2 }}", "Fladnitz im Raabtal 150", $headerHtml);
|
||||
$headerHtml = str_replace("{{ billingAddressLine_3 }}", "8322 Studenzen", $headerHtml);
|
||||
$headerHtml = str_replace("{{ billingAddressLine_4 }}", "Österreich", $headerHtml);
|
||||
$headerHtml = str_replace("{{ billingAddressLine_5 }}", "einkauf@xinon.at", $headerHtml);
|
||||
$headerHtml = str_replace("{{ billingAddressLine_6 }}", "<strong>Referenz: ". $order["orderNumber"] . "</strong>", $headerHtml);
|
||||
|
||||
// if order dellAddrLine is Fladnitz im Raabtal 150 we need to set all template strings to empty
|
||||
|
||||
$chk = $order['delAddrLine'] == "Fladnitz im Raabtal 150";
|
||||
|
||||
$headerHtml = str_replace("{{ shippingAddressLine_1 }}", $chk ? "" : $order['delAddrName'], $headerHtml);
|
||||
$headerHtml = str_replace("{{ shippingAddressLine_2 }}", $chk ? "" : $order['delAddrLine'], $headerHtml);
|
||||
$headerHtml = str_replace("{{ shippingAddressLine_3 }}", $chk ? "" : $order['delAddrPLZ'] . " " . $order['delAddrCity'], $headerHtml);
|
||||
$headerHtml = str_replace("{{ shippingAddressLine_4 }}", $chk ? "" : $order['delAddrEMail'], $headerHtml);
|
||||
|
||||
|
||||
$headerFile = BASEDIR . "/var/temp/order_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
||||
file_put_contents($headerFile, $headerHtml);
|
||||
|
||||
$footerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/PDF_FOOTER.html");
|
||||
$footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
|
||||
$footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
|
||||
$footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
|
||||
$footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
|
||||
|
||||
$footerFile = BASEDIR . "/var/temp/order_footer-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
||||
file_put_contents($footerFile, $footerHtml);
|
||||
|
||||
|
||||
$pdf = new PdfForm("WarehouseOrder/PDF_MAIN", $pdf_vars);
|
||||
$wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
|
||||
$filename = $pdf->render($wkhtmltopdfArgs);
|
||||
|
||||
// return the pdf and die so the client sees the pdf not the filename
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . $filename . '"');
|
||||
readfile($filename);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
|
||||
//TODO: migration for extReference
|
||||
/**
|
||||
* Class WarehouseOrderModel
|
||||
*
|
||||
@@ -7,6 +7,7 @@
|
||||
*
|
||||
* @property int $id Unique identifier for the warehouse order
|
||||
* @property string $orderNumber Unique order number
|
||||
* @property string $extReference External reference number
|
||||
* @property int $distributorId ID of the distributor associated with the order
|
||||
* @property string $delAddrCity City of the delivery address
|
||||
* @property string $delAddrEMail Email associated with the delivery address
|
||||
@@ -22,6 +23,7 @@
|
||||
class WarehouseOrderModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $orderNumber;
|
||||
public string $extReference;
|
||||
public int $distributorId;
|
||||
public string $delAddrCity;
|
||||
public string $delAddrEMail;
|
||||
|
||||
4
application/WarehouseProject/WarehouseProject.php
Normal file
4
application/WarehouseProject/WarehouseProject.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
class WarehouseProject extends mfBaseModel {
|
||||
}
|
||||
36
application/WarehouseProject/WarehouseProjectController.php
Normal file
36
application/WarehouseProject/WarehouseProjectController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
class WarehouseProjectController extends TTCrud {
|
||||
protected string $headerTitle = 'Projekte';
|
||||
protected string $createText = 'Neues Projekt erstellen';
|
||||
|
||||
//@formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true, 'table' => ['class' => 'text-nowrap', 'priority' => 9]],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => ['class' => 'text-nowrap']],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'type' => 'select', 'table' => ['class' => 'text-nowrap', 'filter' => 'select'], 'modal' => ['items' => [], 'type' => 'select']],
|
||||
['key' => 'create', 'text' => 'Erstellt am', 'required' => true, 'table' => ['filter' => 'date', 'class' => 'text-center']],
|
||||
['key' => 'address', 'text' => 'Adresse', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => false], 'modal' => ['apiUrl' => '/Address/api?do=findAddress', 'items' => '/Address/api?do=findAddress', 'type' => 'autocomplete']],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => [ 'type' => 'select', 'items' => [ ['value' => 'erstellt', 'text' => 'Erstellt'], ['value' => 'in_bearbeitung', 'text' => 'In Bearbeitung'], ['value' => 'erledigt', 'text' => 'Erledigt'], ['value' => 'verrechnet', 'text' => 'Verrechnet']]]]
|
||||
];
|
||||
|
||||
|
||||
protected array $additionalActions = [
|
||||
];
|
||||
|
||||
protected array $infoMessages = [
|
||||
'create' => 'Projekt wurde erstellt',
|
||||
'update' => 'Projekt wurde aktualisiert',
|
||||
'delete' => 'Projekt wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',
|
||||
];
|
||||
//@formatter:on
|
||||
|
||||
public function prepareCrudConfig() {
|
||||
$users = array_map(function($user) {
|
||||
return ['value' => $user->id, 'text' => $user->name];
|
||||
}, UserModel::search(['employee' => true]));
|
||||
|
||||
$this->columns[1]['modal']['items'] = $users;
|
||||
}
|
||||
}
|
||||
15
application/WarehouseProject/WarehouseProjectModel.php
Normal file
15
application/WarehouseProject/WarehouseProjectModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
class WarehouseProjectModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public string $description;
|
||||
public string $startDate;
|
||||
public string $endDate;
|
||||
public string $status;
|
||||
public string $priority;
|
||||
|
||||
public int $assignedTo;
|
||||
public int $createBy;
|
||||
public int $create;
|
||||
}
|
||||
21
db/migrations/20250204190000_warehouse_modify_12.php
Normal file
21
db/migrations/20250204190000_warehouse_modify_12.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class WarehouseModify12 extends AbstractMigration {
|
||||
public function up(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
$table = $this->table("WarehouseOrder");
|
||||
$table->addColumn("extReference", "string", ["length" => 255, "null" => true]);
|
||||
$table->save();;
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
$table = $this->table("WarehouseOrder");
|
||||
$table->removeColumn("extReference");
|
||||
$table->save();;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ class Helper {
|
||||
* @param string $columnName The name of the column in the database table.
|
||||
* @return string The SQL condition generated based on the filter value and column name.
|
||||
*/
|
||||
public static function generateFilterCondition($filterValue, string $columnName, bool $exactMatch = false): string {
|
||||
public static function generateFilterCondition($filterValue, $columnName, bool $exactMatch = false): string {
|
||||
$sql = "";
|
||||
|
||||
if (is_array($filterValue)) {
|
||||
@@ -30,6 +30,9 @@ class Helper {
|
||||
} else if (!empty($filterValue)) {
|
||||
if ($exactMatch) {
|
||||
$sql .= " AND `$columnName` = '" . $filterValue . "'";
|
||||
} else if (strpos($columnName, "|") !== false) {
|
||||
foreach (explode(" ", $filterValue) as $item)
|
||||
$sql .= " AND CONCAT(" . join(",", explode("|", $columnName)) . ") LIKE '%" . str_replace("%", "", $item) . "%'";
|
||||
} else if ($filterValue[0] === "%") {
|
||||
$sql .= " AND `$columnName` LIKE '" . $filterValue . "'";
|
||||
} else if ($filterValue[strlen($filterValue) - 1] === "%") {
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
* @property array $additionalJSVariables
|
||||
* @property array $infoMessages
|
||||
* @property bool $onlyView
|
||||
* @property array $defaultOrder
|
||||
* @property array $autocompleteColumns
|
||||
*/
|
||||
class TTCrud extends mfBaseController {
|
||||
public User $user;
|
||||
@@ -251,18 +253,20 @@ class TTCrud extends mfBaseController {
|
||||
}
|
||||
|
||||
protected function autocompleteAction() {
|
||||
$searchedID = $this->request->searchedID;
|
||||
|
||||
$textKey = property_exists($this->model, 'name') ? 'name' : 'title';
|
||||
|
||||
if (strlen($searchedID) > 0) {
|
||||
$filter = ['id' => $searchedID];
|
||||
if (strlen($this->request->searchedID) > 0) {
|
||||
$filter = ['id' => $this->request->searchedID];
|
||||
$data = $this->model::getAll($filter, 10);
|
||||
} else {
|
||||
$filter = [$textKey => $this->request->q . '%'];
|
||||
$data = $this->model::getAll($filter, 10);
|
||||
if (isset($this->autocompleteColumns) && is_array($this->autocompleteColumns)) {
|
||||
$filterKey = join('|', $this->autocompleteColumns);
|
||||
} else {
|
||||
$filterKey = $textKey;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
if (count($data) < 11) {
|
||||
$filter = [$textKey => $this->request->q];
|
||||
$filter = [$filterKey => '%' . $this->request->q . '%'];
|
||||
$lazyData = $this->model::getAll($filter, 10);
|
||||
$data = array_merge($data, $lazyData);
|
||||
$data = array_unique($data, SORT_REGULAR);
|
||||
@@ -270,7 +274,6 @@ class TTCrud extends mfBaseController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self::returnJson(array_map(function ($item) use ($textKey) {
|
||||
return ['value' => $item->id, 'text' => $item->$textKey];
|
||||
}, $data));
|
||||
@@ -285,6 +288,7 @@ class TTCrud extends mfBaseController {
|
||||
}
|
||||
|
||||
$data = (array) $this->model::get($id);
|
||||
if (method_exists($this, 'getByIdParse') && !isset($_GET['disableParse'])) $data = $this->getByIdParse($data);
|
||||
self::returnJson($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,17 +81,13 @@ class TTCrudBaseModel {
|
||||
}
|
||||
|
||||
|
||||
public static function get($id, $die= false): TTCrudBaseModel {
|
||||
public static function get($id): TTCrudBaseModel {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$id = $db->real_escape_string($id);
|
||||
$table = self::getTable();
|
||||
$sql = "SELECT * FROM `$table` WHERE `id` = $id";
|
||||
|
||||
if($die) {
|
||||
die($sql);
|
||||
}
|
||||
|
||||
$result = $db->query($sql);
|
||||
// as TTCRudBaseModel is abstract, we need to get the class name of the child class
|
||||
$class = get_called_class();
|
||||
@@ -109,16 +105,16 @@ class TTCrudBaseModel {
|
||||
return $result->fetch_assoc()['count'];
|
||||
}
|
||||
|
||||
public static function getSQLFilter($filter): string {
|
||||
if (empty($filter)) {
|
||||
return "";
|
||||
}
|
||||
public static function getSQLFilter(array $filter): string {
|
||||
if (empty($filter)) return '';
|
||||
$sql = 'WHERE 1=1';
|
||||
$calledClass = get_called_class();
|
||||
|
||||
$sql = "WHERE 1=1";
|
||||
foreach ($filter as $key => $value) {
|
||||
if (!property_exists(get_called_class(), $key)) {
|
||||
http_response_code(500);
|
||||
throw new Exception("Field $key does not exist in " . get_called_class());
|
||||
foreach (explode('|', $key) as $column) {
|
||||
if (!property_exists($calledClass, $column)) {
|
||||
throw new InvalidArgumentException("Field $column does not exist in $calledClass");
|
||||
}
|
||||
}
|
||||
$sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer");
|
||||
}
|
||||
|
||||
14
public/js/pages/WarehouseOffer/WarehouseOffer.css
Normal file
14
public/js/pages/WarehouseOffer/WarehouseOffer.css
Normal file
@@ -0,0 +1,14 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
178
public/js/pages/WarehouseOffer/WarehouseOffer.js
Normal file
178
public/js/pages/WarehouseOffer/WarehouseOffer.js
Normal file
@@ -0,0 +1,178 @@
|
||||
Vue.component('warehouse-offer-modal', {
|
||||
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-input label="Kundennummer" v-model="offer.customerNumber" sm row/>
|
||||
<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 2fr 1fr 1fr 2fr;">
|
||||
<tt-input label="Name" v-model="offer.customerName" 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="UID" v-model="offer.customerVAT" sm/>
|
||||
</div>
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
<tt-positions-manager ref="positionsManager" v-model="offer.positions" :config="positionsConfig" @updateField-article="fetchArticleData"/>
|
||||
<hr>
|
||||
<h4 class="text-center">Alternative Artikel</h4>
|
||||
<tt-positions-manager ref="alternativePositionsManager" v-model="offer.alternativePositions" :config="alternativePositionsConfig"/>
|
||||
<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-select label="Schlusstext" :options="closingTexts" sm row v-model="offer.closingText"/>
|
||||
<hr>
|
||||
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
|
||||
</div>
|
||||
</tt-modal>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
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'},
|
||||
articleNumber: {type: 'input', label: 'Artikelnummer'},
|
||||
unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'},
|
||||
discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'},
|
||||
},
|
||||
validateForm: (formData) => {
|
||||
const requiredFields = ['article', 'amount', 'unitPrice'];
|
||||
for (const field of requiredFields) {
|
||||
if (!formData[field]) {
|
||||
window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
alternativePositionsConfig: {
|
||||
fields: {
|
||||
article: {type: 'input', label: 'Artikel'},
|
||||
description: {type: 'textarea', label: 'Beschreibung'},
|
||||
},
|
||||
},
|
||||
paymentTerms: [
|
||||
{value: 'net30', text: '30 Tage netto'},
|
||||
{value: 'net60', text: '60 Tage netto'},
|
||||
{value: 'immediate', text: 'Sofort fällig'},
|
||||
],
|
||||
deliveryTerms: [
|
||||
{value: 'ex_works', text: 'Ab Werk'},
|
||||
{value: 'free_delivery', text: 'Frei Haus'},
|
||||
{value: 'fob', text: 'FOB'},
|
||||
],
|
||||
closingTexts: [
|
||||
{value: 'standard', text: 'Standardtext'},
|
||||
{value: 'custom1', text: 'Angepasster Text 1'},
|
||||
{value: 'custom2', text: 'Angepasster Text 2'},
|
||||
],
|
||||
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: 'standard',
|
||||
notes: '',
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (this.id !== 'create') {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/getById`, {params: {id: this.id}});
|
||||
this.offer = response.data;
|
||||
this.offer.positions = JSON.parse(this.offer.positions);
|
||||
this.offer.alternativePositions = JSON.parse(this.offer.alternativePositions);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
||||
|
||||
const url = this.id === 'create'
|
||||
? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/create`
|
||||
: `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/update`;
|
||||
|
||||
const response = await axios.post(url, this.offer);
|
||||
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message ?? 'Angebot erfolgreich gespeichert');
|
||||
this.$emit('close');
|
||||
} else {
|
||||
window.notify('error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
},
|
||||
async fetchArticleData(article) {
|
||||
if (typeof article === 'number') {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, {params: {id: article}});
|
||||
this.$refs.positionsManager.updateField('articleNumber', response.data.articleNumber);
|
||||
this.$refs.positionsManager.updateField('unitPrice',
|
||||
Object.values(JSON.parse(response.data.cheapestSellPrice)).find(price => price.title === 'Verkauf').price);
|
||||
this.$refs.positionsManager.updateField('unit', response.data.unit);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
offerModalId: null,
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -25,3 +25,28 @@
|
||||
grid-gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Expanded Row Styling */
|
||||
.order-summary {
|
||||
padding: 1rem;
|
||||
}
|
||||
.position-item {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.position-header {
|
||||
background-color: #f0f0f0;
|
||||
padding: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.position-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.field-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Vue.component('warehouse-order-modal', {
|
||||
sm
|
||||
row
|
||||
v-model="order.editor"/>
|
||||
<tt-input label="Externe Referenz" v-model="order.extReference" sm row/>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
@@ -80,6 +81,7 @@ Vue.component('warehouse-order-modal', {
|
||||
},
|
||||
},
|
||||
order: {
|
||||
extReference: '',
|
||||
delAddrName: 'XINON GmbH',
|
||||
delAddrLine: 'Fladnitz im Raabtal 150',
|
||||
delAddrPLZ: '8322',
|
||||
@@ -92,11 +94,13 @@ Vue.component('warehouse-order-modal', {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (this.id !== 'create') {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
|
||||
if (this.id === 'create') return;
|
||||
|
||||
console.log(this.id);
|
||||
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById?disableParse`, {params: {id: this.id}});
|
||||
response.data.positions = JSON.parse(response.data.positions);
|
||||
this.order = response.data;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
@@ -112,18 +116,20 @@ Vue.component('warehouse-order-modal', {
|
||||
positions: this.order.positions.filter(position => position.distributorId === distributorId)
|
||||
}
|
||||
);
|
||||
if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt');
|
||||
else window.notify('error',
|
||||
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) window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert');
|
||||
else window.notify('error',
|
||||
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');
|
||||
}
|
||||
this.$emit('close');
|
||||
|
||||
},
|
||||
async fetchDistributors(article) {
|
||||
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
|
||||
@@ -148,22 +154,93 @@ Vue.component('warehouse-order-modal', {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Vue.component('warehouse-order', {
|
||||
Vue.component('warehouse-order-detail', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="orderModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
||||
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
|
||||
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
|
||||
<template v-slot:expandedRow="{ row }"><span>Work in Progress</span></template>
|
||||
</tt-table-crud>
|
||||
<template v-slot:header><h4>Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}</h4></template>
|
||||
|
||||
<template v-if="loading">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<h3>Lieferadresse</h3>
|
||||
<div>{{order.delAddrName}}</div>
|
||||
<div>{{order.delAddrEMail}}</div>
|
||||
<div>{{order.delAddrLine}}</div>
|
||||
<div>{{order.delAddrPLZ}} {{order.delAddrCity}}</div>
|
||||
|
||||
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;margin-top: 24px">
|
||||
<div><strong>Artikel</strong></div>
|
||||
<div><strong>Menge</strong></div>
|
||||
<div><strong>Preis</strong></div>
|
||||
<div><strong>Lieferant</strong></div>
|
||||
<div><strong>Verwendung</strong></div>
|
||||
<div><strong>Summe</strong></div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;" v-for="position in order.positions">
|
||||
<div>{{ position.articleName }}</div>
|
||||
<div>{{ position.amount }}</div>
|
||||
<div>{{ position.buyPrice }}</div>
|
||||
<div>{{ position.distributorName }}</div>
|
||||
<div>{{ position.verwendung }}</div>
|
||||
<div>{{ position.amount * position.buyPrice }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</tt-card>
|
||||
`, data() {
|
||||
`,
|
||||
props: {
|
||||
id: {type: [String, Number], required: true}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
orderModalId: null,
|
||||
|
||||
order: {},
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
})
|
||||
async mounted() {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
|
||||
this.order = response.data;
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('warehouse-order', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="closeOrderModal"/>
|
||||
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
|
||||
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<warehouse-order-detail :id="row['id']"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2)}} €</template>
|
||||
<!-- TODO: think of a way here prob we add it to the database as field-->
|
||||
<template v-slot:distributor="{ row }">{{ row.id % 2 == 0 ? 'Triotronik' : 'Discomp' }}</template>
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
orderModalId: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeOrderModal() {
|
||||
this.orderModalId = null;
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
},
|
||||
calculateSum(positions) {
|
||||
return positions.reduce((sum, position) => sum + position.amount * position.buyPrice, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
14
public/js/pages/WarehouseProject/WarehouseProject.css
Normal file
14
public/js/pages/WarehouseProject/WarehouseProject.css
Normal file
@@ -0,0 +1,14 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
161
public/js/pages/WarehouseProject/WarehouseProject.js
Normal file
161
public/js/pages/WarehouseProject/WarehouseProject.js
Normal file
@@ -0,0 +1,161 @@
|
||||
Vue.component('warehouse-project-modal', {
|
||||
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' ? 'Projekt erstellen' : \`Projekt #\${id} bearbeiten\`"
|
||||
@update:show="$emit('close')">
|
||||
<div style="width: 99%">
|
||||
<h4 class="text-center">Projektübersicht</h4>
|
||||
<tt-input label="Projektnummer" v-model="project.projectNumber" sm row disabled />
|
||||
<tt-textarea label="Um was handelt es sich?" v-model="project.description" sm row/>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Zeitraum</h4>
|
||||
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr;">
|
||||
<tt-date-picker label="Startdatum" v-model="project.startDate" sm/>
|
||||
<tt-date-picker label="Enddatum" v-model="project.endDate" sm/>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Beteiligte Personen</h4>
|
||||
<tt-select label="Personen (XINON MT)"
|
||||
:options="participantsOptions"
|
||||
v-model="project.participants"
|
||||
sm row />
|
||||
<tt-textarea label="Freitext für weitere Personen" v-model="project.additionalParticipants" sm row/>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Projektübersicht</h4>
|
||||
<tt-input label="Gesamtsumme des Projekts (€)" v-model.number="project.totalSum" sm row type="number"/>
|
||||
<tt-positions-manager
|
||||
ref="positionsManager"
|
||||
v-model="project.positions"
|
||||
:config="positionsConfig"
|
||||
@updateField-article="fetchArticleData"
|
||||
/>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Lagerort</h4>
|
||||
<tt-input label="Lagerort für dieses Projekt" v-model="project.storageLocation" sm row/>
|
||||
|
||||
<hr>
|
||||
<tt-textarea label="Notizen" v-model="project.notes" sm row/>
|
||||
</div>
|
||||
</tt-modal>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
participantsOptions: [
|
||||
{ value: 1, text: 'Person A' },
|
||||
{ value: 2, text: 'Person B' },
|
||||
{ value: 3, text: 'Person C' }
|
||||
// Add more participants as needed
|
||||
],
|
||||
positionsConfig: {
|
||||
fields: {
|
||||
article: {
|
||||
type: 'autocomplete',
|
||||
label: 'Artikel',
|
||||
apiUrl: '/WarehouseArticle/autoComplete',
|
||||
customFieldReference: 'WarehouseArticle',
|
||||
},
|
||||
hoursRequired: { type: 'input', label: 'Benötigte Stunden', inputType: 'number' },
|
||||
amountRequired: { type: 'input', label: 'Benötigte Menge', inputType: 'number' },
|
||||
description: { type: 'textarea', label: 'Beschreibung' }
|
||||
},
|
||||
validateForm(formData) {
|
||||
const requiredFields = ['article', 'hoursRequired', 'amountRequired'];
|
||||
for (const field of requiredFields) {
|
||||
if (!formData[field]) {
|
||||
window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
project: {
|
||||
projectNumber: '',
|
||||
description: '',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
participants: [],
|
||||
additionalParticipants: '',
|
||||
totalSum: 0,
|
||||
positions: [],
|
||||
storageLocation: '',
|
||||
notes: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
if (this.id !== 'create') {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getById`, { params: { id: this.id } });
|
||||
this.project = response.data;
|
||||
} else {
|
||||
this.project.projectNumber = await this.generateProjectNumber();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
if (!this.project.description) return window.notify('error', 'Bitte geben Sie eine Beschreibung ein.');
|
||||
|
||||
const url = this.id === 'create'
|
||||
? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/create`
|
||||
: `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/update`;
|
||||
|
||||
const response = await axios.post(url, this.project);
|
||||
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message ?? 'Projekt erfolgreich gespeichert');
|
||||
this.$emit('close');
|
||||
} else {
|
||||
window.notify('error', response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
},
|
||||
async fetchArticleData(article) {
|
||||
if (typeof article === 'number') {
|
||||
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, { params: { id: article } });
|
||||
this.$refs.positionsManager.updateField('description', response.data.description);
|
||||
}
|
||||
},
|
||||
async generateProjectNumber() {
|
||||
const currentCount = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/count`);
|
||||
return `PRJ-${new Date().getFullYear()}-${String(currentCount.data + 1).padStart(4, '0')}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Vue.component('warehouse-project', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-project-modal v-if="projectModalId" :id="projectModalId" @close="projectModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
||||
<button @click="projectModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
|
||||
<tt-table-crud emit-edit @edit="projectModalId = $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>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
projectModalId: null,
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -33,6 +33,13 @@ Vue.component('tt-positions-manager', {
|
||||
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
|
||||
sm
|
||||
/>
|
||||
<tt-textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
:label="field.label"
|
||||
v-model="formData[key]"
|
||||
@input="$emit('updateField-' + key, $event)"
|
||||
sm
|
||||
/>
|
||||
<tt-checkbox
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
:label="field.label"
|
||||
|
||||
@@ -229,6 +229,7 @@ Vue.component('tt-table', {
|
||||
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
|
||||
.format('DD.MM.YYYY HH:mm')) : ''
|
||||
}}</span>
|
||||
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
|
||||
Reference in New Issue
Block a user