'id', 'text' => 'ID', 'modal' => false, 'table' => false], ['key' => 'orderNumber', 'text' => 'Bestellnummer', 'required' => true, 'modal' => false], ['key' => 'distributorId', 'text' => 'Lieferant', 'required' => false, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']], ['key' => 'delAddrCity', 'text' => 'Stadt', 'required' => true, 'modal' => false, 'table' => false], ['key' => 'delAddrEMail', 'text' => 'E-Mail', 'required' => true, 'modal' => false, 'table' => false], ['key' => 'delAddrLine', 'text' => 'Adresse', 'required' => true, 'modal' => false, 'table' => false], ['key' => 'delAddrName', 'text' => 'Name', 'required' => true, 'modal' => false, 'table' => false], ['key' => 'delAddrPLZ', 'text' => 'PLZ', 'required' => true, 'modal' => false, 'table' => false], ['key' => 'editor', 'text' => 'Bearbeiter', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']], ['key' => 'note', 'text' => 'Interne Notiz', 'required' => false, 'modal' => false, 'table' => false], ['key' => 'sum', 'text' => 'Summe', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-right']], ['key' => 'status', 'text' => 'Status', 'required' => false, 'modal' => ['type' => 'select', 'items' => [ ['value' => 'new', 'text' => 'Neu'], ['value' => 'accepted', 'text' => 'Akzeptiert'], ['value' => 'ordered', 'text' => 'Bestellt'], ['value' => 'sent', 'text' => 'Versendet'], ['value' => 'partiallyDelivered', 'text' => 'Teilweise geliefert'], ['value' => 'fullyDelivered', 'text' => 'Geliefert'], ['value' => 'cancelled', 'text' => 'Storniert'], ]], 'table' => ['filter' => 'select']], ['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => false, 'table' => false], ['key' => 'extReference', 'text' => 'Externe Referenz', 'required' => false, 'modal' => false], ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']], ['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false], ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], ]; //@formatter:on protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.', 'update' => 'Bestellung wurde aktualisiert.', 'delete' => 'Bestellung wurde gelöscht', 'noChanges' => 'Keine Änderungen',]; protected array $additionalActions = [['key' => 'openpdf', 'title' => 'PDF öffnen', 'class' => 'fas fa-file-pdf', 'color' => 'primary']]; protected function prepareCrudConfig(): void { $editorColumnIndex = array_search('editor', array_column($this->columns, 'key')); $this->columns[$editorColumnIndex]['modal']['items'] = array_map(function ($user) { return ['value' => intval($user->id), 'text' => $user->name]; }, UserModel::search(['employee' => true])); $distributorIndex = array_search('distributorId', array_column($this->columns, 'key')); $this->columns[$distributorIndex]['modal']['items'] = array_map(function ($distributor) { return ['value' => intval($distributor->id), 'text' => $distributor->name]; }, WarehouseDistributorModel::getAll()); $this->additionalActions[] = [ 'key' => 'changeStatus', 'title' => 'Status ändern', 'class' => 'fas fa-exchange-alt', 'color' => 'warning', ]; } protected function beforeCreate(): bool { $this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad(WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]) + 1, 4, '0', STR_PAD_LEFT); return true; } protected function getArticleDistributorDataAction() { $articleId = $this->request->articleId; if ($this->request->allDistributor === 'true') self::returnJson(array_map(fn($d) => ['id' => $d->id, 'name' => $d->name], WarehouseDistributorModel::getAll())); else if (!empty($articleId)) self::returnJson(array_map(fn($d) => ['id' => $d->distributorId, 'name' => WarehouseDistributorModel::get($d->distributorId)->name, 'purchasePrice' => $d->purchasePrice, 'externalArticleNumber' => $d->externalArticleNumber], WarehouseArticleDistributorModel::getAll(['articleId' => $articleId]))); else self::returnJson([]); } protected function getByIdParse(array $order): array { $order['positions'] = json_decode($order['positions'], true); foreach ($order['positions'] as &$position) { $position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name; $position['articleName'] = $position['article_text'] ?? WarehouseArticleModel::get($position['article'])->title; } return $order; } protected function createPDFAction($returnFilename = false) { $order = (array) WarehouseOrderModel::get($this->request->id); $order['positions'] = json_decode($order['positions'], true); // Validate distributor consistency $distributorIds = array_column($order['positions'], 'distributorId'); if (count(array_unique($distributorIds)) > 1) { self::returnJson(['error' => 'Die Bestellung enthält Positionen von verschiedenen Lieferanten.']); } // Prepare data $distributor = WarehouseDistributorModel::get($distributorIds[0]); $countryText = (new Country($distributor->countryId))->name; $isGermanSpeaking = in_array($countryText, ['Österreich', 'Deutschland', 'Schweiz']); $isDefaultAddress = $order['delAddrLine'] === "Fladnitz im Raabtal 150"; // Enhance positions foreach ($order['positions'] as &$position) { $article = WarehouseArticleModel::get($position['article']); $position += [ 'distributorName' => $distributor->name, 'articleName' => $article->title, 'articleDescription' => $article->description ]; } // Prepare PDF variables $pdfVars = [ 'order' => $order, 'distributor' => $distributor, 'distributorCountryText' => $countryText, 'bank' => [ 'iban' => TT_INVOICE_BANK_IBAN, 'bic' => TT_INVOICE_BANK_BIC, 'bank' => TT_INVOICE_BANK_BANK, 'owner' => TT_INVOICE_BANK_OWNER ] ]; // Generate HTML templates $replacements = [ 'basedir' => BASEDIR, 'externalReference' => !empty($order['extReference']) ? "Ext. Ref.: ".$order['extReference'] : "", 'isGermanSpeaking' => $isGermanSpeaking, // Required for conditional logic 'isDefaultAddress' => $isDefaultAddress, // Needed for address display rules 'distributor' => $distributor, 'countryText' => $countryText, 'order' => $order, // Full order data access 'orderNumber' => $order['orderNumber'], 'bank_iban' => TT_INVOICE_BANK_IBAN_FORMATTED, // Direct footer access 'bank_bic' => TT_INVOICE_BANK_BIC, 'bank_bank' => TT_INVOICE_BANK_BANK, 'bank_owner' => TT_INVOICE_BANK_OWNER ]; // put header and footer into temp files $headerFile = BASEDIR . "/var/temp/order_header-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($headerFile, $this->generateTemplate('PDF_HEADER', $replacements)); $footerFile = BASEDIR . "/var/temp/order_footer-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($footerFile, $this->generateTemplate('PDF_FOOTER', $replacements)); // Generate PDF $pdf = new PdfForm("WarehouseOrder/PDF_MAIN", $pdfVars); $filename = $pdf->render("--header-html $headerFile --footer-html $footerFile"); if ($returnFilename === true) return $filename; // Output PDF header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="' . basename($filename) . '"'); readfile($filename); exit; } protected function sendEmailAction() { $pdfFilename = $this->createPDFAction(true); // Get order details for email content $order = (array) WarehouseOrderModel::get($this->request->id); $orderNumber = $order['orderNumber']; $order['positions'] = json_decode($order['positions'], true); $distributorName = WarehouseDistributorModel::get($order['distributorId'])->name; $texts = [ 'EN' => [ 'taxFree' => 'Please provide tax-free delivery according to § 6a UStG (Austrian Sales tax law).
Our VAT ID number: ATU68711968. Delivery to Austria.', 'orderConfirmation' => 'Please send the order confirmation to einkauf@xinon.at', 'sendShippingNote' => 'Please send the delivery note with our shipping note and neutral packaging.', ], 'DE' => [ 'taxFree' => 'Bitte um steuerfreie Lieferung gemäß § 6a UStG.
Unsere UID-Nr.: ATU68711968. Lieferung nach Österreich.', 'orderConfirmation' => 'Wir bitten um Zusendung der Auftragsbestätigung für die Bestellung an einkauf@xinon.at.', 'sendShippingNote' => 'Bitte senden Sie den Lieferschein mit unserem Versandschein und neutraler Verpackung.', ]]; $countryText = (new Country(WarehouseDistributorModel::get($order['distributorId'])->countryId))->name; $isGermanSpeaking = in_array($countryText, ['Österreich', 'Deutschland', 'Schweiz']); $appendToBody = ''; if (!$isGermanSpeaking) { $appendToBody .= $texts['EN']['taxFree'] . '

'; } $appendToBody .= $texts[$isGermanSpeaking ? 'DE' : 'EN']['orderConfirmation']; if ($order['sendShippingNote'] > 0) { $appendToBody .= '

' . $texts[$isGermanSpeaking ? 'DE' : 'EN']['sendShippingNote']; } $appendToBody .= $isGermanSpeaking ? '

Die Bestellung liegt als PDF-Anhang bei.' : '

The order is attached as a PDF file.'; // 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; $mail->CharSet = "UTF-8"; // Recipients $mail->setFrom('einkauf@xinon.at', 'XINON Einkauf'); $mail->addAddress($this->request->email, $this->request->email); // $mail->addCC('cc@example.com'); // Uncomment for CC // Content $mail->isHTML(true); $mail->Subject = "Neue Bestellung #$orderNumber"; $mail->Body = " XINON E-Mail Template

Neue Bestellung

Bestellnummer: $orderNumber
Datum: " . date('d.m.Y H:i', $order['create']) . "
Lieferant: $distributorName

$appendToBody "; // Attach PDF $mail->addAttachment($pdfFilename, "Bestellung_$orderNumber.pdf"); if ($order['sendShippingNote'] > 0) { $shippingNoteController = new WarehouseShippingNoteController(); $shippingNoteFilename = $shippingNoteController->createPDFAction(true, $this->request->shippingNote); $mail->addAttachment($shippingNoteFilename, "Lieferschein_$orderNumber.pdf"); } $mail->send(); $log = [ "table" => "WarehouseOrder", "rowId" => $this->request->id, "type" => "noChanges", "message" => "Bestellung per Mail versendet", "createBy" => intval($this->user->id), "create" => time() ]; WarehouseLogModel::create($log); // Cleanup temporary files if (file_exists($pdfFilename)) { unlink($pdfFilename); } self::returnJson(['success' => true, 'message' => 'E-Mail erfolgreich versendet']); exit; } catch (Exception $e) { http_response_code(500); self::returnJson([ 'success' => false, 'message' => "E-Mail konnte nicht versendet werden: {$mail->ErrorInfo}", ]); exit; } } private function generateTemplate(string $type, array $data): string { $template = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/{$type}.html"); $replacements = [ '{{ basedir }}' => BASEDIR, '{{ externalReference }}' => !empty($data['order']['extReference']) ? "Ext. Ref.: ".$data['order']['extReference'] : "", '{{ addressLine_header }}' => $data['isGermanSpeaking'] ? 'Lieferant' : 'Supplier', '{{ addressLine_1 }}' => $data['distributor']->name, '{{ addressLine_2 }}' => $data['distributor']->address, '{{ addressLine_3 }}' => $data['distributor']->plz . ' ' . $data['distributor']->city, '{{ addressLine_4 }}' => $data['countryText'], '{{ billingAddressLine_header }}' => $this->getBillingHeader($data), '{{ billingAddressLine_1 }}' => 'Xinon GmbH', '{{ billingAddressLine_2 }}' => 'Fladnitz im Raabtal 150', '{{ billingAddressLine_3 }}' => 'A-8322 Studenzen', '{{ billingAddressLine_4 }}' => 'UID: ATU68711968', '{{ billingAddressLine_5 }}' => 'EORI-Nr.: ATEOS1000085074', '{{ billingAddressLine_6 }}' => "Referenz: ".$data['order']['orderNumber']."", '{{ shippingAddressLine_header }}' => $data['isDefaultAddress'] ? '' : ($data['isGermanSpeaking'] ? 'Lieferadresse' : 'Shipping Address'), '{{ shippingAddressLine_1 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrName'], '{{ shippingAddressLine_2 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrLine'], '{{ shippingAddressLine_3 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrPLZ'] . ' ' . $data['order']['delAddrCity'], '{{ shippingAddressLine_4 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrEMail'], '{{ bank_iban }}' => TT_INVOICE_BANK_IBAN_FORMATTED, '{{ bank_bic }}' => TT_INVOICE_BANK_BIC, '{{ bank_bank }}' => TT_INVOICE_BANK_BANK, '{{ bank_owner }}' => TT_INVOICE_BANK_OWNER ]; return strtr($template, $replacements); } private function getBillingHeader(array $data): string { $base = $data['isGermanSpeaking'] ? 'Rechnungsadresse' : 'Billing Address'; return $data['isDefaultAddress'] ? $base . ' / ' . ($data['isGermanSpeaking'] ? 'Lieferadresse' : 'Shipping Address'):$base; } protected function getLogAction() { $orderId = $this->request->orderId; if (empty($orderId)) { self::returnJson(['error' => 'Order ID is required']); return; } $logs = WarehouseLogModel::getAll(['table' => 'WarehouseOrder','rowId' => $orderId], ['timestamp' => 'DESC']); self::returnJson($logs); } protected function createNewLogAction() { $postData = json_decode(file_get_contents('php://input'), true); if (empty($postData['orderId']) || empty($postData['status'])) { self::returnJson(['error' => 'Order ID and Status are required']); return; } $order = WarehouseOrderModel::get($postData['orderId']); $orderAsArray = (array) $order; $fullLogMessage = ''; // 1. Status change message if ($postData['status'] !== 'noChanges') { $statusColumn = array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items']; $oldStatusKey = array_search($order->status, array_column($statusColumn, 'value')); $newStatusKey = array_search($postData['status'], array_column($statusColumn, 'value')); $oldStatusText = ($oldStatusKey !== false) ? $statusColumn[$oldStatusKey]['text'] : $order->status; $newStatusText = ($newStatusKey !== false) ? $statusColumn[$newStatusKey]['text'] : $postData['status']; $fullLogMessage .= 'Status wurde geändert von ' . $oldStatusText . ' auf ' . $newStatusText . '.'; } // 2. Main note from user if (!empty($postData['note'])) { $fullLogMessage .= ($fullLogMessage ? "\n\n" : "") . $postData['note']; } // 3. Handle delivery data if present if (isset($postData['deliveryData']) && is_array($postData['deliveryData'])) { $deliveryDetails = []; foreach ($postData['deliveryData'] as $delivery) { $orderedAmount = floatval($delivery['orderedAmount']); $deliveredAmount = floatval($delivery['amount']); $articleName = $delivery['articleName']; // Only log if there's a discrepancy if ($deliveredAmount < $orderedAmount) { $reasonText = !empty($delivery['reason']) ? " Grund: " . $delivery['reason'] . "." : ""; $discrepancyMessage = "Artikel '$articleName': {$deliveredAmount} von {$orderedAmount} Stk. geliefert.{$reasonText}"; if (isset($delivery['cancelRest']) && $delivery['cancelRest']) { $remaining = $orderedAmount - $deliveredAmount; $discrepancyMessage .= " Restliche {$remaining} Stk. storniert."; } $deliveryDetails[] = $discrepancyMessage; } } if (!empty($deliveryDetails)) { $fullLogMessage .= ($fullLogMessage ? "\n\n" : "") . "Lieferdetails:\n- " . implode("\n- ", $deliveryDetails); } } $log = [ "table" => "WarehouseOrder", "rowId" => intval($postData['orderId']), "type" => $postData['status'] === 'noChanges' ? 'noChanges' : 'statusChange', "fileIds" => $postData['fileIds'] ?? null, "message" => trim($fullLogMessage), "createBy" => intval($this->user->id), "create" => time() ]; try { $createdMovementIds = []; // Create warehouse movements for delivery statuses if (in_array($postData['status'], ['partiallyDelivered', 'fullyDelivered']) && isset($postData['deliveryData']) && is_array($postData['deliveryData'])) { // Get location ID from request or use default (K1 Fladnitz 150) $locationId = intval($postData['locationId'] ?? 0); if ($locationId <= 0) { // Default to K1 Fladnitz 150 $allLocations = WarehouseLocationModel::getAll(); $defaultLocation = null; foreach ($allLocations as $loc) { if ($loc->title === 'K1 Fladnitz 150') { $defaultLocation = $loc; break; } } $locationId = $defaultLocation ? $defaultLocation->id : 1; } // Prepare delivery data with articleId from order positions $positions = json_decode($order->positions, true) ?: []; $deliveryDataWithArticleIds = []; foreach ($postData['deliveryData'] as $index => $delivery) { if (isset($positions[$index])) { $delivery['articleId'] = $positions[$index]['article']; } $deliveryDataWithArticleIds[] = $delivery; } $createdMovementIds = $this->createMovementsForDelivery( intval($postData['orderId']), $deliveryDataWithArticleIds, $locationId ); if (!empty($createdMovementIds)) { // Update order with linked movement IDs $existingMovementIds = $order->linkedMovementIds ? json_decode($order->linkedMovementIds, true) : []; $allMovementIds = array_merge($existingMovementIds, $createdMovementIds); $orderAsArray['linkedMovementIds'] = json_encode($allMovementIds); // Add movement info to log message $fullLogMessage .= ($fullLogMessage ? "\n\n" : "") . count($createdMovementIds) . " Lagerbewegung(en) erstellt."; $log['message'] = trim($fullLogMessage); } } // Store delivery note file IDs if (!empty($postData['deliveryNoteFileIds'])) { $existingFileIds = $order->deliveryNoteFileIds ? json_decode($order->deliveryNoteFileIds, true) : []; $allFileIds = array_merge($existingFileIds, $postData['deliveryNoteFileIds']); $orderAsArray['deliveryNoteFileIds'] = json_encode($allFileIds); } if ($postData['status'] !== 'noChanges') { $orderAsArray['status'] = $postData['status']; WarehouseOrderModel::update($orderAsArray); } elseif (!empty($orderAsArray['linkedMovementIds']) || !empty($orderAsArray['deliveryNoteFileIds'])) { // Update even if status didn't change but we added linked data WarehouseOrderModel::update($orderAsArray); } // Only create a log entry if there's actually something to log if ($postData['status'] !== 'noChanges' || !empty($log['message']) || !empty($log['fileIds'])) { WarehouseLogModel::create($log); } self::returnJson([ 'success' => true, 'message' => 'Log entry created', 'createdMovementIds' => $createdMovementIds ]); } catch (Exception $e) { self::returnJson(['error' => 'Error creating log entry: ' . $e->getMessage()]); } } protected function uploadFileAction() { $file = $_FILES['file'] ?? null; if (!$file || $file['error'] !== UPLOAD_ERR_OK) { self::returnJson(['error' => 'File upload failed']); exit; } try { $uploaded = mfUpload::handleFormUpload("file", false, "/WarehouseOrder"); self::returnJson(['success' => true, 'fileId' => $uploaded->id]); } catch (Exception $e) { self::returnJson(['error' => 'Upload error: ' . $e->getMessage()]); } } protected function getLogByIdAction() { $orderId = $this->request->id; if (empty($orderId)) { self::returnJson(['error' => 'Order ID is required']); return; } $log = WarehouseLogModel::getAll(['table' => 'WarehouseOrder', 'rowId' => $orderId]); self::returnJson($log); } protected function afterCreate($postData) { $this->updateLinkedOrders($postData['id'], 'add'); } protected function beforeDelete($postData): bool { $this->updateLinkedOrders($postData['id'], 'remove'); return true; } protected function updateLinkedOrders($orderId, $action) { $order = (array) WarehouseOrderModel::get($orderId); $requestIds = array_unique(array_filter(array_column( json_decode($order['positions'], true) ?: [], 'linkedOrderRequestId' ))); foreach ($requestIds as $requestId) { $request = (array) WarehouseOrderRequest::get($requestId); $linkedIds = $request['linkedOrderIds'] ? json_decode($request['linkedOrderIds'], true) : []; if ($action === 'add' && !in_array($orderId, $linkedIds)) { $linkedIds[] = $orderId; } elseif ($action === 'remove') { $index = array_search($orderId, $linkedIds); if ($index !== false) { unset($linkedIds[$index]); } } if ($request['linkedOrderIds'] !== $linkedIds) { $request['linkedOrderIds'] = $linkedIds; WarehouseOrderRequest::update($request); } } } protected function createMovementsForDelivery(int $orderId, array $deliveryData, int $locationId): array { $order = WarehouseOrderModel::get($orderId); $createdMovementIds = []; foreach ($deliveryData as $delivery) { $deliveredAmount = floatval($delivery['amount']); $articleId = intval($delivery['articleId']); // Only create movements for items actually delivered if ($deliveredAmount <= 0 || $articleId <= 0) { continue; } // Find or create WarehouseItem for article + location $existingItems = WarehouseItemModel::getAll([ 'articleId' => $articleId, 'warehouseLocationId' => $locationId ]); $warehouseItem = count($existingItems) > 0 ? $existingItems[0] : null; if (!$warehouseItem) { // Create new warehouse item with zero quantity $warehouseItemId = WarehouseItemModel::create([ 'articleId' => $articleId, 'warehouseLocationId' => $locationId, 'quantity' => 0 ]); $warehouseItem = WarehouseItemModel::get($warehouseItemId); } $quantityBefore = $warehouseItem->quantity; $quantityAfter = $quantityBefore + $deliveredAmount; // Create warehouse movement $movementData = [ 'movementNumber' => WarehouseMovementModel::generateMovementNumber(), 'movementType' => 'IN', 'articleId' => $articleId, 'warehouseLocationId' => $locationId, 'warehouseItemId' => $warehouseItem->id, 'quantity' => $deliveredAmount, 'quantityBefore' => $quantityBefore, 'quantityAfter' => $quantityAfter, 'reasonCategory' => 'Warenlieferung', 'linkedOrderId' => $orderId, 'note' => "Lagereingang aus Bestellung {$order->orderNumber}", 'userId' => $this->user->id, 'createBy' => $this->user->id, 'create' => time() ]; $movementId = WarehouseMovementModel::create($movementData); $createdMovementIds[] = $movementId; // Update warehouse item quantity $warehouseItem->quantity = $quantityAfter; WarehouseItemModel::update((array)$warehouseItem); } return $createdMovementIds; } protected function getLinkedMovementsAction() { $orderId = $this->request->orderId; if (empty($orderId)) { self::returnJson(['error' => 'Order ID is required']); return; } $order = WarehouseOrderModel::get($orderId); $linkedMovementIds = $order->linkedMovementIds ? json_decode($order->linkedMovementIds, true) : []; $movements = []; foreach ($linkedMovementIds as $movementId) { $movement = WarehouseMovementModel::get($movementId); if ($movement) { $article = $movement->getArticle(); $location = $movement->getLocation(); $movements[] = [ 'id' => $movement->id, 'movementNumber' => $movement->movementNumber, 'quantity' => $movement->quantity, 'articleName' => $article ? $article->title : 'Unbekannt', 'locationName' => $location ? $location->title : 'Unbekannt', 'create' => $movement->create ]; } } self::returnJson($movements); } protected function getLocationsAction() { $locations = WarehouseLocationModel::getAll(); $result = array_map(function($loc) { return [ 'value' => $loc->id, 'text' => $loc->title ]; }, $locations); self::returnJson($result); } }