diff --git a/application/Address/AddressModel.php b/application/Address/AddressModel.php
index 4ab52d2e8..0d4f9a10c 100644
--- a/application/Address/AddressModel.php
+++ b/application/Address/AddressModel.php
@@ -322,12 +322,23 @@ class AddressModel {
}
}
- if (array_key_exists("company", $filter)) {
- $company = FronkDB::singleton()->escape($filter["company"]);
- if ($company) {
- $where .= " AND company like '%$company%'";
+ if (array_key_exists("company", $filter)) {
+ $companyInput = trim($filter["company"]);
+ if ($companyInput !== '') {
+ $companyParts = preg_split('/\s+/', $companyInput);
+ $companyConditions = [];
+ foreach ($companyParts as $companyPart) {
+ $escapedCompanyPart = FronkDB::singleton()->escape($companyPart);
+ if ($escapedCompanyPart) {
+ $companyConditions[] = "company LIKE '%{$escapedCompanyPart}%'";
+ }
+ }
+ if (!empty($companyConditions)) {
+ $where .= " AND (" . implode(" AND ", $companyConditions) . ")";
+ }
+ }
}
- }
+
if (array_key_exists("firstname", $filter)) {
$firstname = FronkDB::singleton()->escape($filter["firstname"]);
@@ -343,12 +354,22 @@ class AddressModel {
}
}
- if (array_key_exists("mergedName", $filter)) {
- $name = FronkDB::singleton()->escape($filter["mergedName"]);
- if ($name) {
- $where .= " AND (CONCAT(firstname, ' ', lastname) like '%$name%' OR CONCAT(lastname, ' ', firstname) like '%$name%' )";
+ if (array_key_exists("mergedName", $filter)) {
+ $mergedName = trim($filter["mergedName"]);
+ if ($mergedName !== '') {
+ $names = preg_split('/\s+/', $mergedName);
+ $conditions = [];
+ foreach ($names as $namePart) {
+ $escapedNamePart = FronkDB::singleton()->escape($namePart);
+ if ($escapedNamePart) {
+ $conditions[] = "(firstname LIKE '%{$escapedNamePart}%' OR lastname LIKE '%{$escapedNamePart}%')";
+ }
+ }
+ if (!empty($conditions)) {
+ $where .= " AND (" . implode(" AND ", $conditions) . ")";
+ }
}
- }
+ }
if (array_key_exists("street", $filter)) {
$street = FronkDB::singleton()->escape($filter["street"]);
diff --git a/application/WarehouseArticle/WarehouseArticleController.php b/application/WarehouseArticle/WarehouseArticleController.php
index aef8d13df..0b416255a 100644
--- a/application/WarehouseArticle/WarehouseArticleController.php
+++ b/application/WarehouseArticle/WarehouseArticleController.php
@@ -3,6 +3,7 @@
class WarehouseArticleController extends TTCrud {
protected string $headerTitle = 'Artikel';
protected $createText = 'Artikel erstellen';
+ protected string $singleText = 'Artikel';
// @formatter:off
protected array $columns = [
@@ -16,8 +17,8 @@ class WarehouseArticleController extends TTCrud {
], 'table' => false], // Boolean value
['key' => 'cheapestPurchasePrice', 'text' => 'Einkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
['key' => 'cheapestSellPrice', 'text' => 'Verkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
- ['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
- ['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true,'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
+ ['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false], // Stock/inventory related
+ ['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false], // Stock/inventory related
['key' => 'isSerialDocumentation', 'text' => 'Seriennummern', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isEShop', 'text' => 'Ist E-Shop', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
['key' => 'isEShopHide', 'text' => 'E-Shop Versteckt', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
@@ -26,33 +27,18 @@ class WarehouseArticleController extends TTCrud {
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'],
- ['key' => 'editThresholdEntries','title' => 'Schwellenwerte','class' => 'far fa-fw fa-box-full text-orange'],
- ['key' => 'editPricesEntries','title' => 'Preise','class' => 'fas fa-euro-sign text-green'],
- ['key' => 'addToCart','title' => 'Zur Bestellung hinzufügen','class' => 'fas fa-shopping-cart text-primary'],
- ];
+ protected array $additionalActions = [['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary']];
// @formatter:on
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
- protected array $infoMessages = ['create' => 'Artikel wurde erstellt',
- 'update' => 'Artikel wurde aktualisiert',
- 'delete' => 'Artikel wurde gelöscht',
- 'noChanges' => 'Keine Änderungen',];
-
protected function prepareCrudConfig() {
- if (!$this->user->can('WarehouseAdmin')) {
- // find column with key actions, cheapestPurchasePrice, warningAmount, criticalAmount and set table to false
- foreach ($this->columns as $key => $column) {
- if (in_array($column['key'], ['actions', 'cheapestPurchasePrice', 'warningAmount', 'criticalAmount'])) {
- $this->columns[$key]['table'] = false;
- }
- }
- $this->createText = false;
- $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
- }
+ if ($this->user->can('WarehouseAdmin')) return;
+
+ array_walk($this->columns, fn(&$col) => in_array($col['key'], ['actions', 'cheapestPurchasePrice', 'warningAmount', 'criticalAmount']) && $col['table'] = false);
+
+ $this->createText = false;
+ $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
}
protected function beforeUpdate($postData): bool {
@@ -64,28 +50,11 @@ class WarehouseArticleController extends TTCrud {
self::updateCheapestPurchasePrice($postData['id']);
}
- /**
- * Updates the cheapest purchase price for a given article from WarehouseArticleDistributorModel prices.
- *
- * @param int $articleId The ID of the article to update.
- * @return void
- * @throws Exception If the article is not an instance of WarehouseArticleModel.
- */
- public static function updateCheapestPurchasePrice(int $articleId): void {
- $article = WarehouseArticleModel::get($articleId);
-
- if (!$article instanceof WarehouseArticleModel) {
- throw new Exception("Article is not an instance of WarehouseArticleModel");
- }
-
- $order = ['key' => 'purchasePrice', 'order' => 'ASC'];
- $cheapestDistributorEntry = WarehouseArticleDistributorModel::getAll(['articleId' => $articleId], 1, 0, $order);
-
- if (empty($cheapestDistributorEntry)) return;
-
- $cheapestPurchasePrice = $cheapestDistributorEntry[0]->purchasePrice;
- if ($article->cheapestPurchasePrice == $cheapestPurchasePrice) return;
- WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => $cheapestPurchasePrice]));
+ public static function updateCheapestPurchasePrice(int $id): void {
+ $article = WarehouseArticleModel::get($id);
+ if (!$article instanceof WarehouseArticleModel) throw new Exception("Invalid article type");
+ if (($distributor = WarehouseArticleDistributorModel::getAll(['articleId' => $id], 1, 0, ['key' => 'purchasePrice', 'order' => 'ASC'])) && $article->cheapestPurchasePrice != $distributor[0]->purchasePrice)
+ WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => $distributor[0]->purchasePrice]));
}
protected function afterCreate($postData) {
@@ -93,42 +62,24 @@ class WarehouseArticleController extends TTCrud {
self::updateSellPrices($postData['id']);
}
- /**
- * Updates the sell prices for a given article.
- *
- * @param int $articleId The ID of the article to update.
- * @return void
- * @throws Exception If the article is not an instance of WarehouseArticleModel.
- */
- public static function updateSellPrices(int $articleId) {
- $article = WarehouseArticleModel::get($articleId);
- if (!$article instanceof WarehouseArticleModel) {
- throw new Exception("Article is not an instance of WarehouseArticleModel");
+ public static function updateSellPrices(int $id): void { // Added return type hint
+ $a = WarehouseArticleModel::get($id);
+ if (!$a instanceof WarehouseArticleModel) throw new Exception("Invalid article type");
+
+ $aptsById = array_column(WarehouseArticlePriceModel::getAll(['articleId' => $id]), null, 'articlePriceTypeId');
+ $prices = [];
+ $cpp = $a->cheapestPurchasePrice;
+
+ foreach (WarehouseArticlePriceTypeModel::getAll() as $pt) {
+ $apt = $aptsById[$pt->id] ?? null;
+ $p = $apt ? ($apt->priceOverride ?? $apt->priceMultiplier * $cpp) : ($pt->defaultPriceFactor * $cpp);
+ $prices[] = ['title' => $pt->title, 'price' => round($p, 2)]; // Add title and rounded price
}
- $priceTypes = WarehouseArticlePriceTypeModel::getAll();
- $articlePriceTypes = WarehouseArticlePriceModel::getAll(['articleId' => $articleId]);
+ usort($prices, fn($x,$y)=>(match($x['title']){'Verkauf'=>1,'Partner'=>2,'Energie Steiermark'=>3,default=>4})<=>(match($y['title']){'Verkauf'=>1,'Partner'=>2,'Energie Steiermark'=>3,default=>4}));
- $cheapestSellPrices = [];
- // Calculate sell prices for each price type, use default sell multiplier if no specific price is set
- foreach ($priceTypes as $priceType) {
- $articlePriceType = null;
- foreach ($articlePriceTypes as $apt) {
- if ($apt->articlePriceTypeId == $priceType->id) {
- $articlePriceType = $apt;
- break;
- }
- }
-
- $sellPrice = $priceType->defaultPriceFactor * $article->cheapestPurchasePrice;
- if ($articlePriceType !== null) {
- $sellPrice = $articlePriceType->priceOverride ?: $articlePriceType->priceMultiplier * $article->cheapestPurchasePrice;
- }
- $cheapestSellPrices[$priceType->id] = ['title' => $priceType->title, 'price' => round($sellPrice, 2)];
- }
-
- $article->cheapestSellPrice = json_encode($cheapestSellPrices);
- WarehouseArticleModel::update(get_object_vars($article));
+ $a->cheapestSellPrice = json_encode($prices);
+ WarehouseArticleModel::update(get_object_vars($a));
}
public function updatePricesAction() {
@@ -142,207 +93,4 @@ class WarehouseArticleController extends TTCrud {
protected function getHistoryAction() {
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
}
-
- protected function importAction() {
- error_reporting(E_ALL);
- ini_set('display_errors', 1);
- // read import.json from directory of this file
- $json = fopen(dirname(__FILE__) . '/import.json', 'r') or die('Unable to open file!');
-
- // read file content
- $data = fread($json, filesize(dirname(__FILE__) . '/import.json'));
-
- // decode json
- $data = json_decode($data, true);
-
- // close file
- fclose($json);
-
- // die with data length
- // loop through data
- echo 'Importing ' . count($data) . ' items' . PHP_EOL;
- $count = 0;
- foreach ($data as $item) {
- // echo count + 1
- echo ++$count . PHP_EOL;
- // Check if Distributor exists
- $distributor = WarehouseDistributorModel::getAll(['name' => $item['Lieferant']]);
- if (empty($distributor)) {
- $distributorId = WarehouseDistributorModel::create(['name' => $item['Lieferant'],
- 'address' => 'Missing',
- 'plz' => 'Missing',
- 'city' => 'Missing',
- 'countryId' => 1,
- 'email' => 'Missing',
- 'phone' => 'Missing',
- 'contactPerson' => 'Missing',]);
- } else {
- $distributorId = $distributor[0]->id;
- }
-
- // only continue if PRODUKT 1.ZEILE and PRODUKT 2.ZEILE and ARTIKEL GRUPPE and VK and EK and Lieferant/ Hersteller Artikelnr: are set
- if (!isset($item['PRODUKT 1.ZEILE'])) {
- echo 'Missing data for ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL;
- continue;
- }
-
- if (empty($item['VK'])) {
- $item['VK'] = 0;
- $calcSellPriceMultiplier = 0;
- } else {
- $item['VK'] = floatval(str_replace(',', '', $item['VK']));
- }
-
- if (empty($item['EK'])) {
- $item['EK'] = 0;
- $calcSellPriceMultiplier = 0;
- $purchasePrice = 0;
- } else {
- $item['EK'] = floatval(str_replace(',', '', $item['EK']));
- }
-
- if (!empty($item['VK']) && !empty($item['EK'])) {
- $calcSellPriceMultiplier = $item['VK'] / $item['EK'];
-
- // if calcSellPriceMultiplier has more than 2 decimal places assign $calcSellPriceOverride
- echo strlen(substr(strrchr($calcSellPriceMultiplier, "."), 1)) . PHP_EOL;
- if (strlen(substr(strrchr($calcSellPriceMultiplier, "."), 1)) > 2) {
- $calcSellPriceOverride = $item['VK'];
- }
-
- $purchasePrice = str_replace(',', '', $item['EK']);
- }
-
- if (!isset($calcSellPriceMultiplier) && !isset($calcSellPriceOverride) || !isset($purchasePrice)) {
- echo 'Missing data for ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL;
- continue;
- }
-
-
- // Check if Article exists
- $article = WarehouseArticleModel::getAll(['title' => ['exact' => $item['PRODUKT 1.ZEILE']]]);
- if (empty($article)) {
- $articleCreateData = ['title' => $item['PRODUKT 1.ZEILE'],
- 'description' => $item['PRODUKT 2. ZEILE'],
- 'category' => $item['ARTIKEL GRUPPE'],
- 'cheapestPurchasePrice' => 0,
- 'warningAmount' => 25,
- 'criticalAmount' => 10,
- 'isEShop' => 0,];
-
- // if calcSellPriceOverride is set, add it to the $articleCreateData array
- if (isset($calcSellPriceOverride)) {
- $articleCreateData['sellPriceOverride'] = $calcSellPriceOverride;
- } else if (isset($calcSellPriceMultiplier)) {
- $articleCreateData['sellPriceMultiplier'] = $calcSellPriceMultiplier;
- }
-
- $articleId = WarehouseArticleModel::create($articleCreateData);
- } else {
- echo 'Article already exists with title ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL;
- $articleId = $article[0]->id;
- }
-
- // Check if ArticleDistributor exists
- $articleDistributor = WarehouseArticleDistributorModel::getAll(['articleId' => $articleId,
- 'distributorId' => $distributorId]);
- if (empty($articleDistributor)) {
- WarehouseArticleDistributorModel::create(['articleId' => $articleId,
- 'distributorId' => $distributorId,
- 'purchasePrice' => $purchasePrice,
- 'externalArticleNumber' => $item['Lieferant/ Hersteller Artikelnr:'],]);
- }
-
- }
- }
-
- protected function provArticleNumberImportAction() {
- // if method is post and file is set read the csv and var dump json and die
- // if method is get return basic html with a form to upload a file and a submit button
-
- if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES['file'])) {
- $file = fopen($_FILES['file']['tmp_name'], 'r');
- $data = [];
- // parse csv as object with first row as keys as header and use key => value
- $firstRow = true;
- while (($row = fgetcsv($file)) !== false) {
- if ($firstRow) {
- $header = $row;
- $firstRow = false;
- continue;
- }
- $data[] = array_combine($header, $row);
- }
-
- // loop through all the data and if PRODUKT 1.ZEILE is set and articleNumber is not set push the last 4 numbers of "EAN 13 Code" to articleNumber of the found article
-
- foreach ($data as $item) {
- if (isset($item['PRODUKT 1.ZEILE'])) {
- $articles = WarehouseArticleModel::getAll(['title' => $item['PRODUKT 1.ZEILE']]);
- if (!empty($articles)) {
- $article = (array) WarehouseArticleModel::get($articles[0]->id);
-
- $article['articleNumber'] = substr($item['EAN 13 Code'], -4);
- WarehouseArticleModel::update($article);
- }
- }
- }
-
- fclose($file);
- die(json_encode(['success' => true]));
- }
-
- $html = '
';
- echo $html;
- die();
-
- }
-
- protected function prepareOrderAction() {
- // inside post json it will look like
- // [
- // {
- // "amount": "5",
- // "itemId": 441,
- // "title": "RT-FB-7590AX"
- // },
- // {
- // "amount": "5",
- // "itemId": 421,
- // "title": "RT-FB-7590"
- // }
- //]
- // get the json from the post request
- // then create a array containing each order we need to make, so search through WarehouseArticleDistributorModel to get the distributorId and purchasePrice (use lowest purchasePrice)
- // then get the WarehouseDistributorModel and then create a summary of the orders we need to make for each distributor
-
- $postData = json_decode(file_get_contents('php://input'), true);
- $orders = [];
- foreach ($postData as $order) {
- $articleDistributors = WarehouseArticleDistributorModel::getAll(['articleId' => $order['itemId']]);
- $cheapestArticleDistributor = $articleDistributors[0];
- foreach ($articleDistributors as $articleDistributor) {
- if ($articleDistributor->purchasePrice < $cheapestArticleDistributor->purchasePrice) {
- $cheapestArticleDistributor = $articleDistributor;
- }
- }
- $distributor = WarehouseDistributorModel::get($cheapestArticleDistributor->distributorId);
-
- if (!isset($orders[$distributor->id])) {
- $orders[$distributor->id] = ['distributor' => array($distributor),
- 'orderAmount' => 0,
- 'orders' => []];
- }
-
- $orders[$distributor->id]['orders'][] = ['articleId' => $order['itemId'],
- 'amount' => $order['amount'],
- 'sum' => $order['amount'] * $cheapestArticleDistributor->purchasePrice,
- 'purchasePrice' => $cheapestArticleDistributor->purchasePrice,
- 'externalArticleNumber' => $cheapestArticleDistributor->externalArticleNumber,
- 'title' => $order['title'],];
- $orders[$distributor->id]['orderAmount'] += $order['amount'] * $cheapestArticleDistributor->purchasePrice;
- }
-
- self::returnJson($orders);
- }
}
\ No newline at end of file
diff --git a/application/WarehouseEmailQueue/WarehouseEmailQueue.php b/application/WarehouseEmailQueue/WarehouseEmailQueue.php
new file mode 100644
index 000000000..fb876a06c
--- /dev/null
+++ b/application/WarehouseEmailQueue/WarehouseEmailQueue.php
@@ -0,0 +1,14 @@
+ 'Ungültige Anfrage']);
if (!(WarehouseOrderRequest::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']);
- $currentData = (array) WarehouseOrderRequest::get($id);
+ $currentData = (array)WarehouseOrderRequest::get($id);
WarehouseOrderRequest::update(array_merge($currentData, ['id' => $id, 'cancelled' => $cancel]));
self::returnJson(['success' => true]);
}
@@ -92,66 +92,29 @@ class WarehouseOrderRequestController extends TTCrud {
if (!$id || $done === false) self::returnJson(['error' => 'Ungültige Anfrage']);
if (!(WarehouseOrderRequest::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']);
- $currentData = (array) WarehouseOrderRequest::get($id);
+ $currentData = (array)WarehouseOrderRequest::get($id);
WarehouseOrderRequest::update(array_merge($currentData, ['id' => $id, 'done' => $done]));
self::returnJson(['success' => true]);
}
- private function getPHPMailer() {
- $mail = new PHPMailer\PHPMailer\PHPMailer(true);
- try {
- // Server settings
- $mail->isSMTP();
- $mail->Host = TT_WAREHOUSE_ORDER_SMTP_HOST;
- $mail->SMTPAuth = true;
- $mail->Username = TT_WAREHOUSE_ORDER_SMTP_USER;
- $mail->Password = TT_WAREHOUSE_ORDER_SMTP_PASS;
- $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
- $mail->Port = 587;
-
- return $mail;
- } catch (Exception $e) {
- self::returnJson(['error' => 'Mailer Error: ' . $mail->ErrorInfo]);
- exit;
- }
- }
-
protected function afterCreate($orderRequest) {
- try {
- $mail = $this->getPHPMailer();
-
- $mail->setFrom('einkauf@xinon.at', 'XINON Einkauf');
- $mail->addAddress('einkauf@xinon.at', 'XINON Einkauf');
-
- $mail->isHTML(true);
- $mail->Subject = "Neuer Bestellwunsch #" . $orderRequest['id'] . " von " . $this->user->name . ' eingelangt';
-
-// build html table and fetch articleId if set else use articleId_text if its a text article
- $html = '';
- $html .= '| Artikel | Menge | Zweck |
';
- foreach ($orderRequest['positions'] as $position) {
- $articleId = isset($position['articleId']) ? WarehouseArticleModel::get($position['articleId'])->title : $position['articleId_text'];
- $html .= '';
- $html .= '| ' . htmlspecialchars($articleId) . ' | ';
- $html .= '' . htmlspecialchars($position['amount']) . ' | ';
- $html .= '' . htmlspecialchars($position['purpose']) . ' | ';
- $html .= '
';
- }
- $html .= '
';
-
- // Set the HTML content
- $mail->Body = "Neuer Bestellwunsch #" . $orderRequest['id'] . " von " . $this->user->name . ' eingelangt
' .
- 'Notiz: ' . htmlspecialchars($orderRequest['note']) . '
' . $html;
-
- // Send the email
- if (!$mail->send()) {
- self::returnJson(['error' => 'Message could not be sent. Mailer Error: ' . $mail->ErrorInfo]);
- exit;
- }
- } catch (Exception $e) {
- self::returnJson(['error' => 'Message could not be sent. Mailer Error: ' . $mail->ErrorInfo]);
- exit;
+ $rows = '';
+ foreach ($orderRequest['positions'] as $position) {
+ $rows .= sprintf(
+ '| %s | %s | %s |
',
+ htmlspecialchars(isset($position['articleId']) ? WarehouseArticleModel::get($position['articleId'])->title : $position['articleId_text']),
+ htmlspecialchars($position['amount']),
+ htmlspecialchars($position['purpose'])
+ );
}
+ $html = '| Artikel | Menge | Zweck |
' . $rows . '
';
+
+ WarehouseEmailQueue::create([
+ 'from' => 'einkauf@xinon.at', 'fromName' => 'XINON Einkauf', 'to' => 'einkauf@xinon.at', 'toName' => 'XINON Einkauf',
+ 'subject' => "Neuer Bestellwunsch #{$orderRequest['id']} eingelangt",
+ 'body' => sprintf("Neuer Bestellwunsch #%s von %s eingelangt
Notiz: %s
%s", $orderRequest['id'], $this->user->name, htmlspecialchars($orderRequest['note']), $html),
+ 'create' => time(), 'createBy' => intval($this->user->id),
+ ]);
}
protected function createNewLogAction() {
@@ -162,34 +125,17 @@ class WarehouseOrderRequestController extends TTCrud {
return;
}
- // send email to einkauf@xinon.at
- // Instantiate PHPMailer
- $mail = new PHPMailer\PHPMailer\PHPMailer(true);
-
- try {
- // Server settings
- $mail->isSMTP();
- $mail->Host = TT_WAREHOUSE_ORDER_SMTP_HOST;
- $mail->SMTPAuth = true;
- $mail->Username = TT_WAREHOUSE_ORDER_SMTP_USER;
- $mail->Password = TT_WAREHOUSE_ORDER_SMTP_PASS;
- $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
- $mail->Port = 587;
-
- // Recipients
- $mail->setFrom('einkauf@xinon.at', 'XINON Einkauf');
- $mail->addAddress('einkauf@xinon.at', 'XINON Einkauf');
-
- // Content
- $mail->isHTML(true);
- $mail->Subject = "Neue Nachricht zu Besteallwunsch #" . $postData['orderRequestId'];
- $mail->Body = $postData['note'];
-
- $mail->send();
- } catch (Exception $e) {
- self::returnJson(['error' => 'Message could not be sent. Mailer Error: ' . $mail->ErrorInfo]);
- exit;
- }
+ if (intval($this->user->id !== 1) && intval($this->user->id !== 7))
+ WarehouseEmailQueue::create([
+ 'from' => 'einkauf@xinon.at',
+ 'fromName' => 'XINON Einkauf',
+ 'to' => 'einkauf@xinon.at',
+ 'toName' => 'XINON Einkauf',
+ 'subject' => 'Neue Nachricht zu Besteallwunsch #' . $postData['orderRequestId'],
+ 'body' => $postData['note'],
+ 'create' => date('U'),
+ 'createBy' => intval($this->user->id),
+ ]);
WarehouseLogModel::create([
"table" => "WarehouseOrderRequest",
diff --git a/db/migrations/20250423150000_warehouse_modify_19.php b/db/migrations/20250423150000_warehouse_modify_19.php
new file mode 100644
index 000000000..147f47563
--- /dev/null
+++ b/db/migrations/20250423150000_warehouse_modify_19.php
@@ -0,0 +1,34 @@
+getEnvironment() == "thetool") {
+ $WarehouseEmailQueueTable = $this->table("WarehouseEmailQueue");
+ if (!$WarehouseEmailQueueTable->exists()) {
+ $WarehouseEmailQueueTable
+ ->addColumn("from", "string", ["limit" => 255])
+ ->addColumn("fromName", "string", ["limit" => 255])
+ ->addColumn("to", "string", ["limit" => 255])
+ ->addColumn("toName", "string", ["limit" => 255])
+ ->addColumn("subject", "string", ["limit" => 255])
+ ->addColumn("body", "text")
+ ->addColumn("create", "integer")
+ ->addColumn("createBy", "integer")
+ ->addColumn("sent", "integer", ["default" => 0])
+ ->create();
+ }
+ }
+ }
+
+ public function down(): void {
+ if ($this->getEnvironment() == "thetool") {
+ $WarehouseEmailQueueTable = $this->table("WarehouseEmailQueue");
+ if ($WarehouseEmailQueueTable->exists()) {
+ $WarehouseEmailQueueTable->drop()->save();
+ }
+ }
+ }
+}
diff --git a/public/js/pages/WarehouseArticle/WarehouseArticle.css b/public/js/pages/WarehouseArticle/WarehouseArticle.css
new file mode 100644
index 000000000..4e6b9cf1e
--- /dev/null
+++ b/public/js/pages/WarehouseArticle/WarehouseArticle.css
@@ -0,0 +1,5 @@
+.warehouse-article-prices > div {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(120px, 1fr)) 72px;
+ gap: 10px;
+}
\ No newline at end of file
diff --git a/public/js/pages/WarehouseArticle/WarehouseArticle.js b/public/js/pages/WarehouseArticle/WarehouseArticle.js
index c528b0b49..d281f118b 100644
--- a/public/js/pages/WarehouseArticle/WarehouseArticle.js
+++ b/public/js/pages/WarehouseArticle/WarehouseArticle.js
@@ -1,369 +1,87 @@
-Vue.component('warehouse-distributor-modal', {
- //language=Vue
- template: `
-
-
+Vue.component('warehouse-article-prices', {
+ props: {id: {type: Number, required: true}},
+ template: `
+
-
Lieferant hinzufügen
+
+ Artikelpreise
-
- `, props: {
- show: {type: Boolean, default: false}, id: {type: Number, default: null},
- }, data() {
- return {
- window: window, showModal: false, rows: []
- };
- }, async mounted() {
+
+
+
+
+ {{ index }}
+
+
+
- }, methods: {
- async fetchArticleDistributor() {
- const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/get', {filters: {articleId: this.id}});
+
+
+
+
- this.rows = response.data.rows
- }, addRow() {
- this.rows.push({distributorId: undefined, price: null, externalArticleNumber: null});
- }, async saveRow(index) {
- // post to /WarehouseArticleDistributor/save with rows data and articleId
- const row = this.rows[index];
+
+
+
- const data = {
- articleId: this.id, distributorId: row.distributorId, purchasePrice: row.purchasePrice, externalArticleNumber: row.externalArticleNumber
- }
-
- if (row.id) {
- data.id = row.id;
- }
-
- const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${row.id ? 'update' : 'create'}`, data);
- this.$emit('doUpdate');
- this.window.notify(response.data.success ? 'success' : 'error',
- response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message);
- await this.fetchArticleDistributor();
- }, async deleteRow(index) {
- const row = this.rows[index];
-
- if (!row.id) {
- this.window.notify('error', 'Eintrag hat keine ID');
- }
-
- const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/delete', {id: row.id});
- this.$emit('doUpdate');
- this.window.notify(response.data.success ? 'success' : 'error',
- response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message);
- await this.fetchArticleDistributor();
- }
- }, watch: {
- show(newVal) {
- this.showModal = newVal;
- if (!newVal) {
- this.rows = [];
- return;
- }
- this.rows = [];
- this.fetchArticleDistributor().then();
- }
- }
-});
-
-Vue.component('warehouse-threshold-modal', {
- //language=Vue
- template: `
-
-
-
- Schwellenwert hinzufügen
-
-
- `, props: {
- show: {type: Boolean, default: false}, id: {type: Number, default: null},
- }, data() {
- return {
- window: window, showModal: false, rows: []
- };
- }, async mounted() {
-
- }, methods: {
- async fetchLocationThreshold() {
- const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/get', {filters: {articleId: this.id}});
-
- this.rows = response.data.rows
- }, addRow() {
- this.rows.push({distributor: null, price: null, externalArticleNumber: null});
- }, async saveRow(index) {
- // post to /WarehouseArticleDistributor/save with rows data and articleId
- const row = this.rows[index];
-
- const data = {
- articleId: this.id, locationId: row.locationId, warningAmount: row.warningAmount, criticalAmount: row.criticalAmount
- }
-
- if (row.id) {
- data.id = row.id;
- }
-
- const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseLocationThresholdOverride/${row.id ? 'update' : 'create'}`, data);
- this.$emit('doUpdate');
- this.window.notify(response.data.success ? 'success' : 'error',
- response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message);
- await this.fetchLocationThreshold()
- }, async deleteRow(index) {
- const row = this.rows[index];
-
- if (!row.id) {
- this.window.notify('error', 'Eintrag hat keine ID');
- }
-
- const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/delete', {id: row.id});
- this.$emit('doUpdate');
- this.window.notify(response.data.success ? 'success' : 'error',
- response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message);
- await this.fetchLocationThreshold();
- }
- }, watch: {
- show(newVal) {
- this.showModal = newVal;
- if (!newVal) {
- this.rows = [];
- return;
- }
- this.rows = [];
- this.fetchLocationThreshold().then();
- }
- }
-});
-
-Vue.component('warehouse-article-price-modal', {
- //language=Vue
- template: `
-
-
-
- Preis hinzufügen
-
-
- `, props: {
- show: {type: Boolean, default: false}, id: {type: Number, default: null},
- }, data() {
- return {
- window: window, showModal: false, rows: []
- };
- }, async mounted() {
-
- }, methods: {
- async fetchLocationThreshold() {
- const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}});
-
- this.rows = response.data.rows
- }, addRow() {
- this.rows.push({articlePriceTypeId: null, priceMultiplier: null, priceOverride: null});
- }, async saveRow(index) {
- // post to /WarehouseArticleDistributor/save with rows data and articleId
- const row = this.rows[index];
-
- const data = {
- articleId: this.id, articlePriceTypeId: row.articlePriceTypeId, priceMultiplier: row.priceMultiplier, priceOverride: row.priceOverride
- }
-
- if (row.id) {
- data.id = row.id;
- }
-
- const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${row.id ? 'update' : 'create'}`, data);
- this.$emit('doUpdate');
- this.window.notify(response.data.success ? 'success' : 'error',
- response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message);
- await this.fetchLocationThreshold()
- }, async deleteRow(index) {
- const row = this.rows[index];
-
- if (!row.id) {
- this.window.notify('error', 'Eintrag hat keine ID');
- }
-
- const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/delete', {id: row.id});
- this.$emit('doUpdate');
- this.window.notify(response.data.success ? 'success' : 'error',
- response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message);
- await this.fetchLocationThreshold();
- }
- }, watch: {
- show(newVal) {
- this.showModal = newVal;
- if (!newVal) {
- this.rows = [];
- return;
- }
- this.rows = [];
- this.fetchLocationThreshold().then();
- }
+
`,
+ data: () => ({
+ window,
+ articlePrices: [],
+ }),
+ async mounted() {
+ const [res1, res2] = await Promise.all([
+ axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}}),
+ axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePriceType/get')
+ ]);
+ const prices = {};
+ res2.data.rows.forEach(t => prices[t.title] = {
+ isRobot: true,
+ articlePriceTypeId: t.id,
+ priceMultiplier: t.defaultPriceFactor,
+ priceOverride: null
+ });
+ res1.data.rows.forEach(p => {
+ const t = res2.data.rows.find(t => t.id === p.articlePriceTypeId);
+ if (t) prices[t.title] = {
+ isRobot: false,
+ articlePriceTypeId: p.articlePriceTypeId,
+ priceMultiplier: p.priceMultiplier || t.defaultPriceFactor,
+ priceOverride: p.priceOverride
+ };
+ });
+ this.articlePrices = prices;
}
})
-
-// noinspection EqualityComparisonWithCoercionJS
Vue.component('warehouse-article', {
//language=Vue
- template: `
-
+ template: `
+
-
+
-
-
- {{price.title}}: {{(price.price)}} €
- {{(price.price)}} €
-
-
+
+
+ {{price.title}}: {{(price.price)}} €
+ {{(price.price)}} €
+
+
-
- {{(row.cheapestPurchasePrice * row.sellPriceMultiplier).toFixed(2)}} €
-
+
+
+
-
+
-
-
-
-
-
-
-
- Es werden Bestellungen an folgende Lieferanten gesendet:
-
-
-
{{order.distributor[0].name}} - {{order.orderAmount}} €
-
-
-
- | Artikel |
- Menge |
- Preis |
- Summe |
-
-
- | {{item.title}} |
- {{item.amount}} |
- {{item.purchasePrice}} € |
- {{item.sum.toFixed(2)}} € |
-
-
-
-
-
-
-
-
-
-
- `, data() {
- return {
- window: window,
- historyModal: false,
- historyModalId: null,
- distributorModal: false,
- distributorModalId: null,
- thresholdModal: false,
- thresholdModalId: null,
- priceModal: false,
- priceModalId: null,
- shoppingCart: [],
- addShoppingCartModal: false,
- addShoppingCartModalId: null,
- addShoppingCartModalCount: '',
- confirmOrderModal: false,
- confirmOrderModalData: null,
-
- }
- }, methods: {
- refreshTable() {
- this.$refs.table.$refs.table.refreshTable();
- }, async addToShoppingCart() {
- if (this.addShoppingCartModalCount < 1) { // Check if amount is set
- window.notify('error', 'Bitte geben Sie eine Menge ein.');
- return;
- }
- if (this.shoppingCart.some(item => item.itemId === this.addShoppingCartModalId)) { // Check if same article is already in cart
- window.notify('error', 'Artikel bereits im Warenkorb.');
- return;
- }
-
- const response = await axios.get(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/getById?id=${this.addShoppingCartModalId}`);
- this.shoppingCart.push({amount: parseInt(this.addShoppingCartModalCount), itemId: this.addShoppingCartModalId, title: response.data.title});
- this.addShoppingCartModal = false;
- this.addShoppingCartModalId = null;
- this.addShoppingCartModalCount = '';
- window.notify('success', 'Artikel erfolgreich hinzugefügt.');
- }, async prepareOrder() {
- const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/prepareOrder`, this.shoppingCart);
- this.confirmOrderModal = true;
- this.confirmOrderModalData = response.data;
- },
- async createOrder() {
- const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder/createOrder`, this.confirmOrderModalData);
- this.confirmOrderModal = false;
- this.confirmOrderModalData = null;
- this.shoppingCart = [];
- window.notify(response.data.success ? 'success' : 'error', response.data.message);
- setTimeout(() => {
- window.location.href = `${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder`;
- }, 2000);
- }
- },
+
+
+ `, data: () => ({
+ window,
+ historyModal: false,
+ historyModalId: null,
+ }),
mounted() {
const table = this.$refs.table?.$refs?.table;
if (!table) return;
@@ -372,7 +90,7 @@ Vue.component('warehouse-article', {
const currentFilterId = table.filters?.id;
if ((showId && currentFilterId !== showId) || (!showId && currentFilterId)) {
- table.filters = showId ? { id: showId } : {};
+ table.filters = showId ? {id: showId} : {};
table.refreshTable();
}
}
diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
index f2cb02c3e..6e8dc0fca 100644
--- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
+++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
@@ -297,7 +297,11 @@ Vue.component('warehouse-shipping-note-see-through', {
-
+
+
+
+
+
@@ -368,6 +372,11 @@ Vue.component('warehouse-shipping-note-see-through', {
}
this.loading = false;
+ },
+ async saveAndSetToProgress() {
+ await this.$refs.modal.submit('in_progress');
+ await new Promise(resolve => setTimeout(resolve, 50));
+ await this.fetchData();
}
},
mounted() {
@@ -381,7 +390,7 @@ Vue.component('warehouse-shipping-note', {
template: `
-
+
@@ -138,9 +138,13 @@ Vue.component('warehouse-shipping-note-modal', {
Bitte Lieferadresse eingeben oder nach Kunden suchen
-
+
+
+
+
+
`,
@@ -177,7 +181,7 @@ Vue.component('warehouse-shipping-note-modal', {
}
},
methods: {
- async submit() {
+ async submit(newStatus = null) {
this.loading = true;
if (!this.shippingNote.positions.length && !this.shippingNote.hoursEntries.length) {
this.loading = false;
@@ -188,6 +192,10 @@ Vue.component('warehouse-shipping-note-modal', {
this.shippingNote.type = this.availableTypes.find(t => t.name === this.shippingNote.type).value;
}
+ if (newStatus !== null) {
+ this.shippingNote.status = newStatus;
+ }
+
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/${this.id === 'create' ? 'create' : 'update'}`, this.shippingNote);
if ((this.id !== 'create' && response.data.success === true) || response.data.success === true) this.$emit('close');
diff --git a/public/plugins/vue/tt-components/tt-autocomplete.js b/public/plugins/vue/tt-components/tt-autocomplete.js
index 32dfd9448..408cddd82 100644
--- a/public/plugins/vue/tt-components/tt-autocomplete.js
+++ b/public/plugins/vue/tt-components/tt-autocomplete.js
@@ -80,6 +80,7 @@ Vue.component('tt-autocomplete', {
row: {type: Boolean, default: false},
returnText: {type: Boolean, default: false},
emitDisplayValue: {type: Boolean, default: false},
+ caseInsensitive: {type: Boolean, default: false},
}, data() {
return {
window,
@@ -125,7 +126,9 @@ Vue.component('tt-autocomplete', {
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&searchedID=${this.value}`);
this.displayValue = response.data[0].text;
} else if (this.value) {
- const selectedItem = this.items.find(item => item.value === this.value);
+ const selectedItem = this.caseInsensitive ?
+ this.items.find(item => item.value.toLowerCase() === this.value.toLowerCase()) : this.items.find(item => item.value === this.value)
+ ;
this.displayValue = selectedItem ? selectedItem.text : this.displayValue;
} else {
if (this.returnText === false && !(typeof this.value === 'undefined' || this.value === '')) this.$emit('input', '');
diff --git a/public/plugins/vue/tt-components/tt-table-crud.js b/public/plugins/vue/tt-components/tt-table-crud.js
index 860c07c1a..8fca7e71b 100644
--- a/public/plugins/vue/tt-components/tt-table-crud.js
+++ b/public/plugins/vue/tt-components/tt-table-crud.js
@@ -74,6 +74,8 @@ Vue.component('tt-table-crud', {
+
+
`, props: {
diff --git a/scripts/warehouse-email-queue-process.php b/scripts/warehouse-email-queue-process.php
new file mode 100644
index 000000000..8bd367d71
--- /dev/null
+++ b/scripts/warehouse-email-queue-process.php
@@ -0,0 +1,53 @@
+ 0]) as $email) {
+ $date = date('Y-m-d H:i:s');
+ $mail = new PHPMailer\PHPMailer\PHPMailer(true);
+
+ try {
+ // Server settings
+ $mail->isSMTP();
+ $mail->Host = TT_WAREHOUSE_ORDER_SMTP_HOST;
+ $mail->SMTPAuth = true;
+ $mail->Username = TT_WAREHOUSE_ORDER_SMTP_USER;
+ $mail->Password = TT_WAREHOUSE_ORDER_SMTP_PASS;
+ $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
+ $mail->Port = 587;
+
+ // Recipients
+ $mail->setFrom($email->from, $email->fromName);
+ $mail->addAddress($email->to, $email->toName);
+
+ // Content
+ $mail->isHTML(true);
+ $mail->Subject = $email->subject;
+ $mail->Body = $email->body;
+
+ if ($mail->send()) {
+ $email = (array)$email;
+ $email['sent'] = date('U');
+ WarehouseEmailQueue::update($email);
+ echo "[WarehouseEmailQueue] ($date) Message sent to {$email['to']}
";
+ } else {
+ echo "[WarehouseEmailQueue] ($date) Message could not be sent to {$email['to']}
";
+ echo "[WarehouseEmailQueue] Mailer Error: {$mail->ErrorInfo}
";
+ }
+ } catch (Exception $e) {
+ echo "[WarehouseEmailQueue] ($date) Message could not be sent to {$email['to']}
";
+ echo "[WarehouseEmailQueue] Mailer Error: {$mail->ErrorInfo}
";
+ }
+}
\ No newline at end of file