commit21
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Models\PurchaseOrder;
|
||||
use App\Models\Quotation;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\User;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\ProcurementOperationalInsights;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -19,6 +20,14 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ProcurementReportController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
private function snapshot(\Closure $read): mixed
|
||||
{
|
||||
if (DB::connection()->getDriverName() === 'pgsql' && DB::transactionLevel() !== 0) {
|
||||
@@ -37,8 +46,10 @@ class ProcurementReportController extends Controller
|
||||
/** @return array<string,mixed> */
|
||||
private function filters(Request $request): array
|
||||
{
|
||||
$actor = User::query()->findOrFail($request->user()->id);
|
||||
abort_unless($actor->tenant_id === $request->user()->tenant_id && $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
||||
$current = $request->user('web');
|
||||
abort_unless($current instanceof User, 403);
|
||||
$actor = User::query()->where('tenant_id', $current->tenant_id)->findOrFail($current->id);
|
||||
abort_unless(! app(OrganizationAccess::class)->maintenanceOnly($actor) && $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
||||
$request->setUserResolver(fn () => $actor);
|
||||
|
||||
$filters = $request->validate([
|
||||
@@ -47,6 +58,7 @@ class ProcurementReportController extends Controller
|
||||
'from' => 'nullable|date_format:Y-m-d',
|
||||
'to' => 'nullable|date_format:Y-m-d',
|
||||
'department_id' => 'nullable|integer|min:1',
|
||||
'attention' => 'nullable|in:unapproved_needed,approved_without_quotes,approved_without_order',
|
||||
]);
|
||||
if (! empty($filters['from']) && ! empty($filters['to']) && $filters['from'] > $filters['to']) {
|
||||
throw ValidationException::withMessages(['to' => 'A data final deve ser igual ou posterior à data inicial.']);
|
||||
@@ -55,10 +67,22 @@ class ProcurementReportController extends Controller
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/** @return Builder<Quotation> */
|
||||
private function registeredQuotes(): Builder
|
||||
{
|
||||
return Quotation::query()->whereColumn('quotations.requisition_id', 'requisitions.id');
|
||||
}
|
||||
|
||||
/** @return Builder<PurchaseOrder> */
|
||||
private function visibleOrders(User $actor): Builder
|
||||
{
|
||||
return PurchaseOrder::query()->visibleTo($actor)->whereColumn('purchase_orders.requisition_id', 'requisitions.id');
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters
|
||||
* @return Builder<Requisition>
|
||||
*/
|
||||
private function query(User $actor, array $filters): Builder
|
||||
private function query(User $actor, array $filters, string $today): Builder
|
||||
{
|
||||
$query = Requisition::query()->where('tenant_id', $actor->tenant_id)->visibleTo($actor);
|
||||
if (isset($filters['search']) && $filters['search'] !== '') {
|
||||
@@ -69,6 +93,15 @@ class ProcurementReportController extends Controller
|
||||
$query->where($field, $filters[$field]);
|
||||
}
|
||||
}
|
||||
if (($filters['attention'] ?? '') === 'unapproved_needed') {
|
||||
$query->whereIn('status', ['draft', 'pending'])->whereDate('needed_by', '<', $today);
|
||||
}
|
||||
if (($filters['attention'] ?? '') === 'approved_without_quotes') {
|
||||
$query->where('status', 'approved')->whereNotExists($this->registeredQuotes()->selectRaw('1'));
|
||||
}
|
||||
if (($filters['attention'] ?? '') === 'approved_without_order') {
|
||||
$query->where('status', 'approved')->whereNotExists($this->visibleOrders($actor)->selectRaw('1'));
|
||||
}
|
||||
if (! empty($filters['from'])) {
|
||||
$query->where('created_at', '>=', $filters['from'].' 00:00:00');
|
||||
}
|
||||
@@ -82,11 +115,11 @@ class ProcurementReportController extends Controller
|
||||
/** @param Builder<Requisition> $query
|
||||
* @return Builder<Requisition>
|
||||
*/
|
||||
private function records(Builder $query): Builder
|
||||
private function records(Builder $query, User $actor): Builder
|
||||
{
|
||||
return $query->select('requisitions.*')->with(['department:id,name', 'requester:id,name'])->addSelect([
|
||||
'quotations_count' => Quotation::query()->selectRaw('COUNT(*)')->whereColumn('requisition_id', 'requisitions.id'),
|
||||
'order_number' => PurchaseOrder::query()->visibleTo(request()->user())->select('number')->whereColumn('requisition_id', 'requisitions.id')->orderBy('id')->limit(1),
|
||||
'quotations_count' => $this->registeredQuotes()->selectRaw('COUNT(*)'),
|
||||
'order_number' => $this->visibleOrders($actor)->select('number')->orderBy('id')->limit(1),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,6 +129,7 @@ class ProcurementReportController extends Controller
|
||||
return [
|
||||
'id' => $record->id, 'number' => $record->number, 'status' => $record->status,
|
||||
'created_at' => $record->created_at?->toIso8601String(),
|
||||
'needed_by' => $record->needed_by?->toDateString(),
|
||||
'department' => $record->department?->name, 'requester' => $record->requester?->name,
|
||||
'total_minor' => (string) $record->total_minor,
|
||||
'quotations_count' => (int) $record->getAttribute('quotations_count'),
|
||||
@@ -106,14 +140,15 @@ class ProcurementReportController extends Controller
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$props = $this->snapshot(function () use ($request): array {
|
||||
$instant = CarbonImmutable::now(config('app.timezone'));
|
||||
$filters = $this->filters($request);
|
||||
$query = $this->query($request->user(), $filters);
|
||||
$actor = $this->actor($request);
|
||||
$query = $this->query($actor, $filters, $instant->toDateString());
|
||||
$counts = (clone $query)->selectRaw('status, COUNT(*) AS count')->groupBy('status')->pluck('count', 'status')->map(fn ($value) => (int) $value);
|
||||
$records = $this->records(clone $query)->orderByDesc('id')->paginate(25)->withQueryString()->through(fn (Requisition $record) => $this->row($record));
|
||||
$departments = Department::query()->when($request->user()->departmentScoped(), fn ($q) => $q->whereKey($request->user()->department_id))->orderBy('name')->get(['id', 'name']);
|
||||
$records = $this->records(clone $query, $actor)->orderByDesc('id')->paginate(25)->withQueryString()->through(fn (Requisition $record) => $this->row($record));
|
||||
$departments = Department::query()->when($actor->departmentScoped(), fn ($q) => $q->whereKey($actor->department_id))->orderBy('name')->get(['id', 'name']);
|
||||
|
||||
$instant = CarbonImmutable::now();
|
||||
$insights = app(ProcurementOperationalInsights::class)->build($this->records(clone $query), $instant);
|
||||
$insights = app(ProcurementOperationalInsights::class)->build($this->records(clone $query, $actor), $instant);
|
||||
|
||||
return ['records' => $records, 'counts' => $counts, 'filters' => $filters, 'departments' => $departments, 'generatedAt' => $instant->toIso8601String(), 'insights' => $insights];
|
||||
});
|
||||
@@ -127,23 +162,26 @@ class ProcurementReportController extends Controller
|
||||
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
$query = $this->records($this->query($request->user(), $filters));
|
||||
$this->filters($request);
|
||||
$text = fn (?string $value) => preg_match('/^[\s]*[=+\-@]|^[\t\r\n]/u', $value ?? '') ? "'".$value : ($value ?? '');
|
||||
|
||||
return response()->streamDownload(fn () => $this->snapshot(function () use ($query, $text, $filters) {
|
||||
return response()->streamDownload(fn () => $this->snapshot(function () use ($request, $text) {
|
||||
$instant = CarbonImmutable::now(config('app.timezone'));
|
||||
$filters = $this->filters($request);
|
||||
$actor = $this->actor($request);
|
||||
$query = $this->records($this->query($actor, $filters, $instant->toDateString()), $actor);
|
||||
$output = fopen('php://output', 'w');
|
||||
if ($output === false) {
|
||||
throw new \RuntimeException('Não foi possível gerar o ficheiro.');
|
||||
}
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
fputcsv($output, ['Relatório de compras', 'Criado em', now()->toIso8601String()], ';', '"', '');
|
||||
fputcsv($output, ['Relatório de compras', 'Criado em', $instant->toIso8601String()], ';', '"', '');
|
||||
fputcsv($output, ['Filtros', $text(json_encode($filters, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))], ';', '"', '');
|
||||
fputcsv($output, ['Requisição', 'Criada em', 'Departamento', 'Solicitante', 'Estado da requisição', 'Estimativa total (centavos MZN)', 'Propostas registadas', 'Ordem de compra', 'Processo', 'Relatório de conferência'], ';', '"', '');
|
||||
fputcsv($output, ['Requisição', 'Criada em', 'Departamento', 'Solicitante', 'Estado da requisição', 'Estimativa total (centavos MZN)', 'Propostas registadas', 'Ordem de compra', 'Processo', 'Relatório de conferência', 'Data de necessidade'], ';', '"', '');
|
||||
foreach ($query->lazyById(200) as $record) {
|
||||
$row = $this->row($record);
|
||||
$base = url('/requisicoes/'.$record->id);
|
||||
fputcsv($output, [$text($record->number), $row['created_at'], $text($row['department']), $text($row['requester']), $record->status, $row['total_minor'], $row['quotations_count'], $text($row['order_number']), $base.'/processo', $base.'/relatorio'], ';', '"', '');
|
||||
fputcsv($output, [$text($record->number), $row['created_at'], $text($row['department']), $text($row['requester']), $record->status, $row['total_minor'], $row['quotations_count'], $text($row['order_number']), $base.'/processo', $base.'/relatorio', $row['needed_by']], ';', '"', '');
|
||||
}
|
||||
fclose($output);
|
||||
}), 'relatorio-compras-'.now()->format('Ymd-His').'.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store, max-age=0']);
|
||||
|
||||
Reference in New Issue
Block a user