86 lines
5.4 KiB
PHP
86 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\GoodsReceipt;
|
|
use App\Models\GoodsReceiptItem;
|
|
use App\Models\StockOperation;
|
|
use App\Models\SupplyRecord;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
/** Read projection only; callers retain their native module authority and commands. */
|
|
class InventoryCorrectionPage
|
|
{
|
|
private function actor(): User
|
|
{
|
|
$actor = auth()->user();
|
|
abort_unless($actor instanceof User && $actor->tenant_id !== null, 403);
|
|
|
|
return User::query()->where('tenant_id', $actor->tenant_id)->findOrFail($actor->id);
|
|
}
|
|
|
|
/** @param Collection<array-key,mixed> $selected
|
|
* @return Collection<int,GoodsReceiptItem>
|
|
*/
|
|
public function receipts(Collection $selected, string $search = ''): Collection
|
|
{
|
|
$actor = $this->actor();
|
|
abort_unless(app(InventoryDispositions::class)->allows($actor, 'prepare') || app(InventoryReversals::class)->allows($actor, 'prepare'), 403);
|
|
$query = GoodsReceiptItem::query()->whereNotNull('stock_item_id')->where('accepted_milli', '>', 0)
|
|
->whereNotIn('id', array_keys(app(InventoryReversals::class)->corrections('receipt')));
|
|
$recent = (clone $query)->when(trim($search) !== '', fn ($q) => $q->whereIn('goods_receipt_id', GoodsReceipt::query()->whereLike('number', '%'.trim($search).'%')->select('id')))->latest('id')->limit(200)->pluck('id');
|
|
|
|
return $query->where(fn ($q) => $q->whereIn('id', $recent)->orWhereIn('id', $selected->filter()->unique()))->latest('id')->get();
|
|
}
|
|
|
|
/** @param Collection<array-key,mixed> $selected
|
|
* @return Collection<int,StockOperation>
|
|
*/
|
|
public function operations(Collection $selected, string $search = ''): Collection
|
|
{
|
|
abort_unless(app(InventoryReversals::class)->allows($this->actor(), 'prepare'), 403);
|
|
$query = StockOperation::query()->whereIn('kind', ['issue', 'transfer', 'adjustment', 'supplier_return', 'write_off'])
|
|
->whereNotIn('id', array_keys(app(InventoryReversals::class)->corrections('operation')));
|
|
$recent = (clone $query)->when(trim($search) !== '', fn ($q) => $q->whereLike('reference', '%'.trim($search).'%'))->latest('id')->limit(200)->pluck('id');
|
|
|
|
return $query->where(fn ($q) => $q->whereIn('id', $recent)->orWhereIn('id', $selected->filter()->unique()))->latest('id')->get();
|
|
}
|
|
|
|
/** @return array{ready:bool,focus:SupplyRecord|null,records:Collection<int,SupplyRecord>,filters:array{search:string,state:string},lookup:array<string,mixed>,pagination:array<string,mixed>|null} */
|
|
public function read(Request $request, User $actor, string $kind): array
|
|
{
|
|
abort_unless(auth()->id() === $actor->id && $actor->tenant_id !== null && in_array($kind, ['stock_event', 'stock_reversal'], true), 403);
|
|
$actor = $this->actor();
|
|
$workflow = $kind === 'stock_event' ? app(InventoryDispositions::class) : app(InventoryReversals::class);
|
|
abort_unless(collect(['read', 'prepare', 'review', 'execute'])->contains(fn ($capability) => $workflow->allows($actor, $capability)), 403);
|
|
$filters = $request->validate([
|
|
'record_id' => ['nullable', 'integer', 'min:1', Rule::exists('supply_records', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', $kind)],
|
|
'records_page' => 'nullable|integer|min:1|max:1000000',
|
|
'search' => 'nullable|string|max:100',
|
|
'state' => 'nullable|in:draft,returned,approved,executed',
|
|
'source_search' => 'nullable|string|max:100', 'source_form_key' => 'nullable|string|max:80',
|
|
'selected_receipt_id' => ['nullable', 'integer', 'min:1', Rule::exists('goods_receipt_items', 'id')->where('tenant_id', $actor->tenant_id)],
|
|
'selected_operation_id' => ['nullable', 'integer', 'min:1', Rule::exists('stock_operations', 'id')->where('tenant_id', $actor->tenant_id)],
|
|
]);
|
|
$ready = Schema::hasTable('supply_records');
|
|
$focus = isset($filters['record_id']) ? SupplyRecord::query()->where('tenant_id', $actor->tenant_id)->where('kind', $kind)->findOrFail((int) $filters['record_id']) : null;
|
|
$query = $ready ? app(SupplyPlanning::class)->latest($kind)->where('tenant_id', $actor->tenant_id)->latest('id') : null;
|
|
$search = trim($filters['search'] ?? '');
|
|
$state = $filters['state'] ?? '';
|
|
$page = $focus === null ? $query?->when($search !== '', fn ($q) => $q->whereLike('data->reference', '%'.$search.'%'))
|
|
->when($state !== '', fn ($q) => $q->where('state', $state))->paginate(25, ['*'], 'records_page')->withQueryString() : null;
|
|
|
|
return ['ready' => $ready, 'focus' => $focus,
|
|
'lookup' => ['search' => trim($filters['source_search'] ?? ''), 'form' => $filters['source_form_key'] ?? null,
|
|
'receipt' => $filters['selected_receipt_id'] ?? null, 'operation' => $filters['selected_operation_id'] ?? null],
|
|
'records' => $focus !== null ? $query?->where('record_key', $focus->record_key)->get() ?? collect() : $page?->getCollection() ?? collect(),
|
|
'filters' => ['search' => $search, 'state' => $state],
|
|
'pagination' => $page === null ? null : ['current_page' => $page->currentPage(), 'last_page' => $page->lastPage(), 'total' => $page->total(),
|
|
'prev_page_url' => $page->previousPageUrl(), 'next_page_url' => $page->nextPageUrl()]];
|
|
}
|
|
}
|