user('web'); abort_unless($actor instanceof User, 403); return $actor; } private function access(Request $request, bool $manage = false): void { $roles = $manage ? [PurchasingRole::Admin, PurchasingRole::Warehouse] : [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance]; abort_unless($this->actor($request)->hasFunctions($roles), 403); } public function index(Request $request): Response { $this->access($request); $filters = $request->validate([ 'search' => 'nullable|string|max:100', 'location' => 'nullable|string|regex:/^[0-9]+$/D|max:20', 'warehouse_id' => ['nullable', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $this->actor($request)->tenant_id)], ]); $items = StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']); $geography = app(AdministrativeGeography::class); $warehouseQuery = Warehouse::query(); if (! empty($filters['location'])) { abort_unless($geography->availableFor('warehouses'), 503); $warehouseQuery->where('geography_reference->catalogue', config('geography.source_sha256'))->whereIn('geography_reference->record_id', $geography->descendants($filters['location'])); } $columns = ['id', 'code', 'name']; if (Schema::hasColumn('warehouses', 'geography_reference')) { $columns[] = 'geography_reference'; } $warehouses = $warehouseQuery->orderBy('code')->get($columns); $warehouseGeography = $warehouses->mapWithKeys(fn (Warehouse $warehouse) => [$warehouse->id => $geography->describe($warehouse->geography_reference)])->all(); $selected = StockItem::query(); if (! empty($filters['search'])) { $selected->where(fn ($q) => $q->where('code', 'like', '%'.$filters['search'].'%')->orWhere('name', 'like', '%'.$filters['search'].'%')); } $movements = StockMovement::query()->whereIn('stock_item_id', $selected->select('id')); if (! empty($filters['location'])) { $movements->whereIn('warehouse_id', $warehouses->pluck('id')); } if (! empty($filters['warehouse_id'])) { $movements->where('warehouse_id', $filters['warehouse_id']); } $balances = (clone $movements)->selectRaw('stock_item_id, warehouse_id, SUM(quantity_milli) as quantity_milli')->groupBy('stock_item_id', 'warehouse_id')->get()->map(fn ($balance) => [ 'stock_item_id' => $balance->stock_item_id, 'warehouse_id' => $balance->warehouse_id, 'quantity_milli' => BigDecimal::of($balance->quantity_milli)->toInt(), 'lot_control' => app(InventoryLots::class)->summary($balance->stock_item_id, $balance->warehouse_id), ]); return Inertia::render('stock/index', [ 'items' => $items, 'warehouses' => $warehouses, 'balances' => $balances, 'geographyEnabled' => $geography->availableFor('warehouses'), 'warehouseGeography' => $warehouseGeography, 'geographyFilter' => empty($filters['location']) ? null : $geography->describe(['catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location'], 'country' => 'MZ']), 'movements' => $movements->latest('id')->paginate(20)->withQueryString(), 'filters' => $filters, 'canManage' => $this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse]), ]); } public function item(Request $request, RequisitionWorkflow $audit): RedirectResponse { $this->access($request, true); $request->merge(['code' => Str::upper(trim($request->string('code')->toString())), 'unit' => Str::lower(trim($request->string('unit')->toString()))]); $data = $request->validate([ 'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('stock_items')->where('tenant_id', $this->actor($request)->tenant_id)], 'name' => 'required|string|max:200', 'unit' => 'required|string|max:30', ]); DB::transaction(function () use ($request, $audit, $data) { $item = StockItem::query()->create([...$data, 'tenant_id' => $this->actor($request)->tenant_id, 'created_by' => $this->actor($request)->id]); $audit->audit($this->actor($request), 'stock.item_created', null, null, $item->toArray()); }); return back(); } public function warehouse(Request $request, RequisitionWorkflow $audit): RedirectResponse { $this->access($request, true); $request->merge(['code' => Str::upper(trim($request->string('code')->toString()))]); $data = $request->validate([ 'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('warehouses')->where('tenant_id', $this->actor($request)->tenant_id)], 'name' => 'required|string|max:200', ...app(AdministrativeGeography::class)->referenceRules('warehouses'), ]); DB::transaction(function () use ($request, $audit, $data) { app(ConflictGuard::class)->lockCompany($this->actor($request)); $actor = app(CurrentActor::class)->lock($this->actor($request), [PurchasingRole::Admin, PurchasingRole::Warehouse]); abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403); if (array_key_exists('geography_reference', $data)) { $data['geography_reference'] = app(AdministrativeGeography::class)->canonicalFor('warehouses', $data['geography_reference']); } $warehouse = Warehouse::query()->create([...$data, 'tenant_id' => $this->actor($request)->tenant_id, 'created_by' => $this->actor($request)->id]); $audit->audit($this->actor($request), 'stock.warehouse_created', null, null, $warehouse->toArray()); }); return back(); } public function warehouseGeography(Request $request, int $warehouse, RequisitionWorkflow $audit): RedirectResponse { $this->access($request, true); $geography = app(AdministrativeGeography::class); abort_unless($geography->availableFor('warehouses'), 503); $data = $request->validate([...$geography->referenceRules('warehouses'), 'geography_reference' => 'present|nullable|array:country,catalogue,record_id', 'expected_reference' => 'present|nullable|array:country,catalogue,record_id']); DB::transaction(function () use ($request, $warehouse, $audit, $data, $geography) { app(ConflictGuard::class)->lockCompany($this->actor($request)); $actor = app(CurrentActor::class)->lock($this->actor($request), [PurchasingRole::Admin, PurchasingRole::Warehouse]); abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403); $record = Warehouse::query()->lockForUpdate()->findOrFail($warehouse); if ($record->geography_reference != $data['expected_reference']) { throw ValidationException::withMessages(['geography_reference' => 'A localização mudou. Actualize antes de guardar.']); } $before = $record->toArray(); $after = $geography->canonicalFor('warehouses', $data['geography_reference']); if ($record->geography_reference != $after) { $record->update(['geography_reference' => $after]); $audit->audit($actor, 'stock.warehouse_geography_updated', null, $before, $record->toArray()); } }); return back(); } }