query('search', '')); $status = trim((string) $request->query('status', '')); $perPage = $this->perPage($request); $requestedSort = $request->string('sortBy')->toString(); $sortBy = in_array($requestedSort, self::SORT_COLUMNS, true) ? $requestedSort : 'created_at'; $orderBy = $requestedSort !== '' ? ($request->query('order') === 'desc' ? 'desc' : 'asc') : 'desc'; session(['gp_stock_orders' => $request->query()]); $query = StockOrder::query() ->select('stock_orders.*') ->with(['item.imageFile', 'user']) ->addSelect([ 'active_reservations_sum_quantity' => $this->reservedQuantitySubquery(), 'free_quantity_value' => $this->freeQuantitySubquery(), 'item_article_value' => CommonCatalogItem::query() ->select('article') ->whereColumn('common_catalog_items.id', 'stock_orders.common_catalog_item_id') ->limit(1), ]) ->when($search !== '', function (Builder $query) use ($search): void { $query->where(function (Builder $query) use ($search): void { $query->where('order_number', 'like', "%{$search}%") ->orWhere('note', 'like', "%{$search}%") ->orWhereHas('item', fn (Builder $query) => $query ->where('article', 'like', "%{$search}%") ->orWhere('calculator_name', 'like', "%{$search}%")); }); }) ->when(array_key_exists($status, StockOrder::STATUS_NAMES), fn (Builder $query) => $query->where('status', $status)); $this->applyColumnFilters($query, $request); $sortColumn = match ($sortBy) { 'item_article' => 'item_article_value', 'status_name' => 'status', 'reserved_quantity' => 'active_reservations_sum_quantity', 'free_quantity' => 'free_quantity_value', default => $sortBy, }; $query->orderBy($sortColumn, $orderBy)->orderBy('stock_orders.id'); $orders = $query ->paginate($perPage) ->withQueryString(); return view('stock.orders', [ 'active' => 'stock_orders', 'orders' => $orders, 'items' => CommonCatalogItem::query() ->orderBy('article') ->orderBy('calculator_name') ->get(['id', 'article', 'calculator_name']), 'orderedOrderNumbers' => StockOrder::query() ->where('status', StockOrder::STATUS_ORDERED) ->whereNotNull('order_number') ->where('order_number', '<>', '') ->distinct() ->orderBy('order_number') ->pluck('order_number'), 'statuses' => StockOrder::STATUS_NAMES, 'search' => $search, 'status' => $status, 'per_page' => $perPage, 'sortBy' => $sortBy, 'orderBy' => $orderBy, 'tableId' => 'stock_orders', 'columnFilters' => [ 'order_number' => [], 'item_article' => [], 'status_name' => [], 'ordered_quantity' => ['type' => 'ranges'], 'reserved_quantity' => ['type' => 'ranges'], 'free_quantity' => ['type' => 'ranges'], 'note' => [], 'created_at' => ['type' => 'dates'], ], ]); } public function store(StoreStockOrderRequest $request): RedirectResponse { /** @var User $actor */ $actor = $request->user(); $this->inventoryService->createOrder($request->validated(), $actor); return back()->with('success', 'Складской заказ создан.'); } public function update(StoreStockOrderRequest $request, StockOrder $stockOrder): RedirectResponse { /** @var User $actor */ $actor = $request->user(); try { $this->inventoryService->updateOrder($stockOrder, $request->validated(), $actor); return back()->with('success', 'Складской заказ обновлён.'); } catch (DomainException $exception) { return back()->with('error', $exception->getMessage()); } } public function setOrderInStock(Request $request): RedirectResponse { $validated = $request->validate([ 'bulk_order_number' => ['required', 'string', 'max:255'], ]); /** @var User $actor */ $actor = $request->user(); try { $count = $this->inventoryService->setOrderInStock( trim((string) $validated['bulk_order_number']), $actor, ); return back()->with('success', "Позиции заказа переведены в статус «На складе»: {$count}."); } catch (DomainException $exception) { return back() ->withErrors(['bulk_order_number' => $exception->getMessage()]) ->withInput(); } } public function destroy(StockOrder $stockOrder): RedirectResponse { if ($stockOrder->reservations()->exists()) { return back()->with('error', 'Нельзя удалить заказ с историей бронирований.'); } $stockOrder->delete(); return back()->with('success', 'Складской заказ удалён.'); } public function import(Request $request): RedirectResponse { $validated = $request->validate([ 'import_file' => ['required', 'file', 'mimes:xlsx', 'max:10240'], ]); /** @var UploadedFile $uploadedFile */ $uploadedFile = $validated['import_file']; $filename = Storage::disk('upload')->putFile('stock_orders', $uploadedFile); $import = Import::query()->create([ 'type' => 'stock_orders', 'year' => null, 'filename' => $filename, 'status' => 'new', 'user_id' => $request->user()?->id, 'file_path' => $filename, 'original_filename' => $uploadedFile->getClientOriginalName(), ]); ImportJob::dispatch($import, (int) $request->user()?->id); return back()->with('success', 'Импорт складских заказов поставлен в очередь.'); } private function perPage(Request $request): int { $allowed = [50, 100, 200, 500, 2000]; $perPage = (int) $request->query('per_page', session('per_page', 50)); $perPage = in_array($perPage, $allowed, true) ? $perPage : 50; session(['per_page' => $perPage]); return $perPage; } private function applyColumnFilters(Builder $query, Request $request): void { foreach (['order_number', 'note'] as $column) { $values = $this->filterValues($request, $column); if ($values !== []) { $query->where(function (Builder $query) use ($column, $values): void { $this->applyValuesCondition($query, $column, $values); }); } } $articles = $this->filterValues($request, 'item_article'); if ($articles !== []) { $query->whereHas('item', fn (Builder $query) => $query->whereIn('article', $articles)); } $statuses = $this->filterValues($request, 'status_name'); if ($statuses !== []) { $statusCodes = array_intersect_key(array_flip(StockOrder::STATUS_NAMES), array_flip($statuses)); $query->whereIn('status', array_values($statusCodes)); } $this->applyColumnRange($query, $request, 'ordered_quantity'); $this->applySubqueryRange($query, $request, 'reserved_quantity', $this->reservedQuantitySubquery()); $this->applySubqueryRange($query, $request, 'free_quantity', $this->freeQuantitySubquery()); $from = $request->input('filters.created_at_from'); $to = $request->input('filters.created_at_to'); if (is_string($from) && $from !== '') { $query->whereDate('stock_orders.created_at', '>=', $from); } if (is_string($to) && $to !== '') { $query->whereDate('stock_orders.created_at', '<=', $to); } } /** @return array */ private function filterValues(Request $request, string $column): array { $value = $request->input("filters.{$column}"); return is_string($value) && $value !== '' ? explode('||', $value) : []; } /** @param array $values */ private function applyValuesCondition(Builder $query, string $column, array $values): void { $nonEmpty = array_values(array_diff($values, ['-пусто-'])); if ($nonEmpty !== []) { $query->orWhereIn($column, $nonEmpty); } if (in_array('-пусто-', $values, true)) { $this->applyEmptyFilterCondition($query, $column); } } private function applyColumnRange(Builder $query, Request $request, string $column): void { $from = $request->input("filters.{$column}_from"); $to = $request->input("filters.{$column}_to"); if (is_numeric($from)) { $query->where("stock_orders.{$column}", '>=', (float) $from); } if (is_numeric($to)) { $query->where("stock_orders.{$column}", '<=', (float) $to); } } private function applySubqueryRange( Builder $query, Request $request, string $column, QueryBuilder $subquery, ): void { $from = $request->input("filters.{$column}_from"); $to = $request->input("filters.{$column}_to"); if (is_numeric($from)) { $query->where(clone $subquery, '>=', (float) $from); } if (is_numeric($to)) { $query->where(clone $subquery, '<=', (float) $to); } } private function reservedQuantitySubquery(): QueryBuilder { return DB::table('stock_reservations') ->selectRaw('COALESCE(SUM(quantity), 0)') ->whereColumn('stock_reservations.stock_order_id', 'stock_orders.id') ->where('stock_reservations.status', StockReservation::STATUS_ACTIVE); } private function freeQuantitySubquery(): QueryBuilder { $reserved = $this->reservedQuantitySubquery(); return DB::query()->selectRaw( 'GREATEST(0, stock_orders.available_quantity - ('.$reserved->toSql().'))', $reserved->getBindings(), ); } }