diff --git a/Layout/default/WarehouseOrder/PDF_FOOTER.html b/Layout/default/WarehouseOrder/PDF_FOOTER.html
new file mode 100644
index 000000000..0a984b7d6
--- /dev/null
+++ b/Layout/default/WarehouseOrder/PDF_FOOTER.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
XINON Lieferantenbestellung vom =date("d.m.Y", $order["create"])?>
+
+
+
+ | Position |
+ Artikel |
+ Art.-Nr. Lieferant |
+ Menge |
+ Einzelpreis |
+ Gesamtpreis |
+
+
+
+ ">
+ | = $i + 1 ?> |
+ =$p["articleName"]?> |
+ =$p["distributorArticleNumber"]?> |
+ =$p["amount"]?> |
+ =number_format($p["buyPrice"], 2, ",", ".")?> € |
+ =number_format($p["amount"] * $p["buyPrice"], 2, ",", ".")?> € |
+
+
+
+
+
+
+ | Summe |
+ =number_format($sum, 2, ",", ".")?> € |
+
+
+
+
+
+
+
Anmerkungen
+
+ =$order["note"]?>
+
+
+
+
\ No newline at end of file
diff --git a/application/WarehouseArticle/WarehouseArticleController.php b/application/WarehouseArticle/WarehouseArticleController.php
index 77c80a59e..93eae95cc 100644
--- a/application/WarehouseArticle/WarehouseArticleController.php
+++ b/application/WarehouseArticle/WarehouseArticleController.php
@@ -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'],
diff --git a/application/WarehouseOffer/WarehouseOffer.php b/application/WarehouseOffer/WarehouseOffer.php
new file mode 100644
index 000000000..7aff40082
--- /dev/null
+++ b/application/WarehouseOffer/WarehouseOffer.php
@@ -0,0 +1,9 @@
+ '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));
+ }
+}
diff --git a/application/WarehouseOffer/WarehouseOfferModel.php b/application/WarehouseOffer/WarehouseOfferModel.php
new file mode 100644
index 000000000..c49405483
--- /dev/null
+++ b/application/WarehouseOffer/WarehouseOfferModel.php
@@ -0,0 +1,52 @@
+ '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;
+ $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([]);
+ }
- 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,
- ];
- }
+ 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;
}
- self::returnJson($data);
+ return $order;
}
- protected function beforeUpdate($postData): bool {
- (new WarehouseHistoryController)->create($postData, $this->mod);
- return true;
+ 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 }}","
Ihre Referenz: ". $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 }}", "
Referenz: ". $order["orderNumber"] . "", $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);
+
}
- protected function getHistoryAction() {
- self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
- }
+
}
\ No newline at end of file
diff --git a/application/WarehouseOrder/WarehouseOrderModel.php b/application/WarehouseOrder/WarehouseOrderModel.php
index 84070625f..ac4677800 100644
--- a/application/WarehouseOrder/WarehouseOrderModel.php
+++ b/application/WarehouseOrder/WarehouseOrderModel.php
@@ -1,5 +1,5 @@
'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;
+ }
+}
\ No newline at end of file
diff --git a/application/WarehouseProject/WarehouseProjectModel.php b/application/WarehouseProject/WarehouseProjectModel.php
new file mode 100644
index 000000000..a1aa8f680
--- /dev/null
+++ b/application/WarehouseProject/WarehouseProjectModel.php
@@ -0,0 +1,15 @@
+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();;
+ }
+ }
+}
diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php
index 3668e1b8d..aa31b8c6e 100644
--- a/lib/Helper/Helper.php
+++ b/lib/Helper/Helper.php
@@ -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] === "%") {
diff --git a/lib/TTCrud/TTCrud.php b/lib/TTCrud/TTCrud.php
index 8288b6123..a79537acb 100644
--- a/lib/TTCrud/TTCrud.php
+++ b/lib/TTCrud/TTCrud.php
@@ -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);
}
}
diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php
index 3184ffa10..4815dde9f 100644
--- a/lib/TTCrudBaseModel/TTCrudBaseModel.php
+++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php
@@ -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");
}
diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.css b/public/js/pages/WarehouseOffer/WarehouseOffer.css
new file mode 100644
index 000000000..7951745f0
--- /dev/null
+++ b/public/js/pages/WarehouseOffer/WarehouseOffer.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.js b/public/js/pages/WarehouseOffer/WarehouseOffer.js
new file mode 100644
index 000000000..fe1365051
--- /dev/null
+++ b/public/js/pages/WarehouseOffer/WarehouseOffer.js
@@ -0,0 +1,178 @@
+Vue.component('warehouse-offer-modal', {
+ props: {
+ id: {type: [String, Number], required: true},
+ mode: {type: String, default: 'edit'}
+ },
+ template: `
+
+ Angebotdetails
+
+
+
+
+
+
Kundenadresse
+
+
+
+
+
+
+
+
+
Positionen
+
+
+
Alternative Artikel
+
+
+
+
+
+
+
+
+
+
+ `,
+ 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('
') : 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: `
+
+
+
+
+
+
+
Notizen
+
{{ row.notes }}
+
Verlauf
+
+ - {{ entry.date }} - {{ entry.description }}
+
+
+
+
+
+ `,
+ data() {
+ return {
+ window: window,
+ offerModalId: null,
+ }
+ },
+});
diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.css b/public/js/pages/WarehouseOrder/WarehouseOrder.css
index 878a34a77..9b3746d82 100644
--- a/public/js/pages/WarehouseOrder/WarehouseOrder.css
+++ b/public/js/pages/WarehouseOrder/WarehouseOrder.css
@@ -24,4 +24,29 @@
grid-template-columns: 1fr 1fr !important;
grid-gap: 10px;
}
-}
\ No newline at end of file
+}
+
+
+/* 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;
+}
+
diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js
index fcd97373a..c9b937d86 100644
--- a/public/js/pages/WarehouseOrder/WarehouseOrder.js
+++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js
@@ -16,6 +16,7 @@ Vue.component('warehouse-order-modal', {
sm
row
v-model="order.editor"/>
+
Positionen
@@ -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}});
- response.data.positions = JSON.parse(response.data.positions);
- this.order = response.data;
- }
+ 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('
') : 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('
') : 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: `
-
-
-
- Work in Progress
-
-
- `, data() {
- return {
- window: window,
- orderModalId: null,
+
Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}
+
+
+
+
+
+ Lieferadresse
+ {{order.delAddrName}}
+ {{order.delAddrEMail}}
+ {{order.delAddrLine}}
+ {{order.delAddrPLZ}} {{order.delAddrCity}}
+
+
+
Artikel
+
Menge
+
Preis
+
Lieferant
+
Verwendung
+
Summe
+
+
+
+
{{ position.articleName }}
+
{{ position.amount }}
+
{{ position.buyPrice }}
+
{{ position.distributorName }}
+
{{ position.verwendung }}
+
{{ position.amount * position.buyPrice }}
+
+
+
+
+ `,
+ props: {
+ id: {type: [String, Number], required: true}
+ },
+ data() {
+ return {
+ window: window,
+ order: {},
+ loading: true
}
},
-})
\ No newline at end of file
+ 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: `
+
+
+
+
+
+
+
+
+ {{ calculateSum(JSON.parse(row["positions"])).toFixed(2)}} €
+
+ {{ row.id % 2 == 0 ? 'Triotronik' : 'Discomp' }}
+
+
+ `,
+ 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);
+ }
+ }
+});
diff --git a/public/js/pages/WarehouseProject/WarehouseProject.css b/public/js/pages/WarehouseProject/WarehouseProject.css
new file mode 100644
index 000000000..7951745f0
--- /dev/null
+++ b/public/js/pages/WarehouseProject/WarehouseProject.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/public/js/pages/WarehouseProject/WarehouseProject.js b/public/js/pages/WarehouseProject/WarehouseProject.js
new file mode 100644
index 000000000..3bfeb5578
--- /dev/null
+++ b/public/js/pages/WarehouseProject/WarehouseProject.js
@@ -0,0 +1,161 @@
+Vue.component('warehouse-project-modal', {
+ props: {
+ id: { type: [String, Number], required: true },
+ mode: { type: String, default: 'edit' }
+ },
+ template: `
+
+
+
Projektübersicht
+
+
+
+
+
Zeitraum
+
+
+
+
+
+
+
Beteiligte Personen
+
+
+
+
+
Projektübersicht
+
+
+
+
+
Lagerort
+
+
+
+
+
+
+ `,
+ 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('
') : 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: `
+
+
+
+
+
+
+
Notizen
+
{{ row.notes }}
+
Verlauf
+
+ - {{ entry.date }} - {{ entry.description }}
+
+
+
+
+
+ `,
+ data() {
+ return {
+ window: window,
+ projectModalId: null,
+ }
+ },
+});
diff --git a/public/plugins/vue/tt-components/tt-position-manager.js b/public/plugins/vue/tt-components/tt-position-manager.js
index 182f5b04e..7d8996bbd 100644
--- a/public/plugins/vue/tt-components/tt-position-manager.js
+++ b/public/plugins/vue/tt-components/tt-position-manager.js
@@ -33,6 +33,13 @@ Vue.component('tt-positions-manager', {
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
sm
/>
+
+ {{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}