'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]], ['key' => 'articleNumber', 'text' => 'Nr.', 'required' => true], ['key' => 'description', 'text' => 'Beschreibung', 'required' => true,'modal' => ['type' => 'textarea'], 'table' => ['sortable' => false]], ['key' => 'category_id', 'text' => 'Kategorie', 'required' => true, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']], ['key' => 'unit', 'text' => 'Einheit', 'required' => true,'modal' => ['type' => 'select', 'items' => [['value' => 'Stk.', 'text' => 'Stk.'], ['value' => 'Pau.', 'text' => 'Pau.'], ['value' => 'm.', 'text' => 'm.'], ['value' => 'Std.', 'text' => 'Std.'], ['value' => 'km', 'text' => 'km']]], 'table' => ['filter' => 'select', 'filterOptions' => [['value' => 'Stk.', 'text' => 'Stk.'], ['value' => 'Pau.', 'text' => 'Pau.'], ['value' => 'm.', 'text' => 'm.'], ['value' => 'Std.', 'text' => 'Std.'], ['value' => 'km', 'text' => 'km']]]], ['key' => 'vatgroup_id', 'text' => 'Erlöskonto', 'required' => true,'modal' => ['type' => 'select', 'items' => [['value' => 2, 'text' => 'Dienstleistungen'], ['value' => 3, 'text' => 'Handelswaren']]], 'table' => false], ['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' => false], ['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true,'modal' => ['type' => 'number'], 'table' => false], ['key' => 'isSerialDocumentation', 'text' => 'Seriennummern', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], ['key' => 'isEndOfLife', 'text' => 'End of Life', 'required' => false,'modal' => ['type' => 'checkbox', 'items' => [ ['value' => 0, 'text' => 'Nicht End of Life', 'icon' => 'fa-regular fa-circle-check text-success'], ['value' => 1, 'text' => 'End of Life', 'icon' => 'fa-regular fa-circle-xmark text-danger']]], 'table' => ['filter' => 'iconSelect']], ['key' => 'isEShop', 'text' => 'Ist E-Shop', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], ['key' => 'isEShopHide', 'text' => 'E-Shop Versteckt', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], ['key' => 'isSbidiShop', 'text' => 'Ist SBIDI-Shop', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], ['key' => 'isSbidiShopHide', 'text' => 'SBIDI-Shop Versteckt', 'required' => false,'modal' => ['type' => 'checkbox'], 'table' => false], ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]] ]; protected array $autocompleteColumns = ['articleNumber', 'title', 'description']; protected array $permissionCheck = ['WarehouseUser']; protected array $additionalActions = [ ['key' => 'printLabel','title' => 'Label drucken','class' => 'fas fa-print text-secondary'], ['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'] ]; // @formatter:on protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true, 'HIDE_PAGE_TITLE' => true]; protected function prepareCrudConfig() { $categories = array_map(fn($category) => ['value' => $category->id, 'text' => $category->name], WarehouseCategory::getAll()); $this->columns[array_search('category_id', array_column($this->columns, 'key'))]['modal']['items'] = $categories; 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 beforeCreate($postData): bool { if (!in_array($this->user->id, [2, 5, 6, 145, 14])) self::sendError("Sie haben keine Berechtigung, Artikel zu erstellen."); $this->validateArticleNumber($postData); return true; } protected function beforeUpdate($postData): bool { if (!in_array($this->user->id, [2, 5, 6, 145, 14])) self::sendError("Sie haben keine Berechtigung, Artikel zu bearbeiten."); $this->validateArticleNumber($postData, $postData['id'] ?? null); (new WarehouseHistoryController)->create($postData, $this->mod); return true; } protected function afterUpdate($postData) { self::updateCheapestPurchasePrice($postData['id']); } public static function updateCheapestPurchasePrice(int $id): void { $article = WarehouseArticleModel::get($id); if (!$article instanceof WarehouseArticleModel) throw new Exception("Invalid article type"); $distributor = WarehouseArticleDistributorModel::getAll(['articleId' => $id], 1, 0, ['key' => 'purchasePrice', 'order' => 'ASC']); if (count($distributor) == 0) WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => null])); else if ($article->cheapestPurchasePrice != $distributor[0]->purchasePrice) WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => $distributor[0]->purchasePrice])); } protected function afterCreate($postData) { self::updateCheapestPurchasePrice($postData['id']); self::updateSellPrices($postData['id']); } /** * Validate article number for duplicates and correct category prefix */ private function validateArticleNumber(array $postData, ?int $excludeId = null): void { $articleNumber = $postData['articleNumber'] ?? ''; $categoryId = $postData['category_id'] ?? null; if (empty($articleNumber)) { self::sendError("Artikelnummer ist erforderlich."); } // Check for duplicate article number $existingArticles = WarehouseArticleModel::getAll(['articleNumber' => $articleNumber]); foreach ($existingArticles as $existing) { if ($excludeId === null || $existing->id != $excludeId) { self::sendError("Artikelnummer '{$articleNumber}' existiert bereits (Artikel ID: {$existing->id})."); } } // Validate category prefix if ($categoryId) { $category = WarehouseCategory::get($categoryId); if ($category && $category->articleNumberPrefix) { $expectedPrefix = str_pad($category->articleNumberPrefix, 4, '0', STR_PAD_LEFT); $articlePrefix = substr($articleNumber, 0, strlen($expectedPrefix)); if ($articlePrefix !== $expectedPrefix) { self::sendError("Artikelnummer muss mit dem Kategorie-Prefix '{$expectedPrefix}' beginnen."); } } } } 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 } usort($prices, function($x, $y) { $priorityX = 4; $titleX = isset($x['title']) ? $x['title'] : null; switch ($titleX) { case 'Verkauf': $priorityX = 1; break; case 'Partner': $priorityX = 2; break; case 'Energie Steiermark': $priorityX = 3; break; } $priorityY = 4; $titleY = isset($y['title']) ? $y['title'] : null; switch ($titleY) { case 'Verkauf': $priorityY = 1; break; case 'Partner': $priorityY = 2; break; case 'Energie Steiermark': $priorityY = 3; break; } return $priorityX <=> $priorityY; }); $a->cheapestSellPrice = json_encode($prices); WarehouseArticleModel::update(get_object_vars($a)); } public function updatePricesAction() { foreach (WarehouseArticleModel::getAll() as $article) { self::updateCheapestPurchasePrice($article->id); self::updateSellPrices($article->id); } self::returnJson(['success' => true, 'message' => 'Preise wurden aktualisiert']); } protected function getHistoryAction() { self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns)); } protected function getNextArticleNumberAction() { $categoryId = intval($this->request->categoryId ?? 0); if (!$categoryId) self::sendError("Kategorie nicht angegeben"); $category = WarehouseCategory::get($categoryId); if (!$category) self::sendError("Kategorie nicht gefunden"); if (!$category->articleNumberPrefix) self::sendError("Kategorie hat keinen Artikelnummer-Prefix"); $prefix = str_pad($category->articleNumberPrefix, 4, '0', STR_PAD_LEFT); $db = FronkDB::singleton(); // Get all existing article numbers with this prefix, sorted $result = $db->query("SELECT CAST(articleNumber AS UNSIGNED) as num FROM WarehouseArticle WHERE articleNumber LIKE '{$prefix}%' ORDER BY num ASC"); $existingNumbers = []; while ($row = $db->fetch_array($result)) { $existingNumbers[] = intval($row['num']); } // Start from prefix * 10000 + 1 (e.g., 1800 -> 18000001) $startNumber = intval($prefix) * 10000 + 1; $nextNumber = $startNumber; // Find first gap foreach ($existingNumbers as $num) { if ($num == $nextNumber) { $nextNumber++; } else if ($num > $nextNumber) { // Found a gap break; } } self::returnJson(['success' => true, 'articleNumber' => str_pad($nextNumber, 8, '0', STR_PAD_LEFT)]); } protected function autocompleteAction() { $textKey = property_exists($this->model, 'name') ? 'name' : 'title'; if (strlen($this->request->searchedID) > 0) { $filter = ['id' => $this->request->searchedID]; $data = $this->model::getAll($filter, 10); } else { if (isset($this->autocompleteColumns) && is_array($this->autocompleteColumns)) { $filterKey = join('|', $this->autocompleteColumns); } else { $filterKey = $textKey; } $data = []; if (count($data) < 11) { if (isset($_GET['hideEndOfLife'])) { $data = $this->model::getAll([$textKey => $this->request->q . '%', 'isEndOfLife' => 0], 10); $lazyData = $this->model::getAll([$filterKey => $this->request->q, 'isEndOfLife' => 0], 10); } else { $data = $this->model::getAll([$textKey => $this->request->q . '%'], 10); $lazyData = $this->model::getAll([$filterKey => $this->request->q], 10); } $data = array_merge($data, $lazyData); $data = array_unique($data, SORT_REGULAR); $data = array_slice($data, 0, 10); } } self::returnJson(array_map(function ($item) use ($textKey) { return ['value' => $item->id, 'text' => $item->$textKey]; }, $data)); } protected function printLabelAction() { $articleId = $this->request->id; $article = WarehouseArticleModel::get($articleId); if (!$article) { self::sendError("Artikel nicht gefunden", 404); } $pdf_vars = [ 'articleId' => $article->id, 'articleNumber' => $article->articleNumber, 'articleTitle' => $article->title ]; $pdf = new PdfForm("WarehouseArticle/LABEL", $pdf_vars); $wkhtmltopdfArgs = "--page-height 25mm --page-width 63mm --margin-top 0 --margin-bottom 0 --margin-left 0 --margin-right 0 --disable-smart-shrinking --encoding utf-8 --dpi 96"; $filename = $pdf->render($wkhtmltopdfArgs); header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="label-' . $article->articleNumber . '.pdf"'); readfile($filename); die(); } protected function printLabelsByCategoryAction() { $categoryId = intval($this->request->categoryId); if (!$categoryId) { self::sendError("Kategorie nicht angegeben", 400); } $articles = WarehouseArticleModel::getAll(['category_id' => $categoryId], 10000, 0, ['key' => 'articleNumber', 'order' => 'ASC']); if (empty($articles)) { self::sendError("Keine Artikel in dieser Kategorie gefunden", 404); } $pdf_vars = ['articles' => $articles]; $pdf = new PdfForm("WarehouseArticle/LABEL_BULK", $pdf_vars); $wkhtmltopdfArgs = "--page-height 25mm --page-width 63mm --margin-top 0 --margin-bottom 0 --margin-left 0 --margin-right 0 --disable-smart-shrinking --encoding utf-8 --dpi 96"; $filename = $pdf->render($wkhtmltopdfArgs); $category = WarehouseCategory::get($categoryId); $categoryName = $category ? $category->name : 'category-' . $categoryId; header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="labels-' . str_replace(' ', '_', $categoryName) . '.pdf"'); readfile($filename); die(); } }