Files
thetool/application/WarehouseArticle/WarehouseArticleController.php
2025-12-22 09:36:22 +01:00

266 lines
13 KiB
PHP

<?php
class WarehouseArticleController extends TTCrud {
protected string $headerTitle = 'Artikel';
protected $createText = false;
protected string $singleText = 'Artikel';
protected bool $reopenOnCreate = true;
// @formatter:off
protected array $columns = [
['key' => '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' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' => ['type' => 'select', 'items' => [['value' => 0, 'text' => 'Dienstleistungen'], ['value' => 1, '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 = $category->articleNumberPrefix;
$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 = $category->articleNumberPrefix;
$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 50mm --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();
}
}