@@ -5,24 +5,39 @@ namespace App\Http\Controllers;
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\StockItem;
|
||||
use App\Models\StockMovement;
|
||||
use App\Models\User;
|
||||
use App\Models\Warehouse;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\CurrentActor;
|
||||
use App\Services\InventoryLots;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class StockController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$actor = $request->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($request->user()->hasFunctions($roles), 403);
|
||||
abort_unless($this->actor($request)->hasFunctions($roles), 403);
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
@@ -30,15 +45,30 @@ class StockController extends Controller
|
||||
$this->access($request);
|
||||
$filters = $request->validate([
|
||||
'search' => 'nullable|string|max:100',
|
||||
'warehouse_id' => ['nullable', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $request->user()->tenant_id)],
|
||||
'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']);
|
||||
$warehouses = Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']);
|
||||
$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']);
|
||||
}
|
||||
@@ -50,8 +80,10 @@ class StockController extends Controller
|
||||
|
||||
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' => $request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse]),
|
||||
'canManage' => $this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,12 +92,12 @@ class StockController extends Controller
|
||||
$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', $request->user()->tenant_id)],
|
||||
'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' => $request->user()->tenant_id, 'created_by' => $request->user()->id]);
|
||||
$audit->audit($request->user(), 'stock.item_created', null, null, $item->toArray());
|
||||
$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();
|
||||
@@ -76,12 +108,44 @@ class StockController extends Controller
|
||||
$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', $request->user()->tenant_id)],
|
||||
'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) {
|
||||
$warehouse = Warehouse::query()->create([...$data, 'tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id]);
|
||||
$audit->audit($request->user(), 'stock.warehouse_created', null, null, $warehouse->toArray());
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user