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 .= ''; - foreach ($orderRequest['positions'] as $position) { - $articleId = isset($position['articleId']) ? WarehouseArticleModel::get($position['articleId'])->title : $position['articleId_text']; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - } - $html .= '
ArtikelMengeZweck
' . htmlspecialchars($articleId) . '' . htmlspecialchars($position['amount']) . '' . htmlspecialchars($position['purpose']) . '
'; - - // 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 = '' . $rows . '
ArtikelMengeZweck
'; + + 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: ` + - + - + - + - + - - - - - - - - Es werden Bestellungen an folgende Lieferanten gesendet: - -
-

{{order.distributor[0].name}} - {{order.orderAmount}} € -

- - - - - - - - - - - - - -
ArtikelMengePreisSumme
{{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
- + +
`, 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