Files
compras/app/Http/Controllers/PurchaseProcessController.php
T

118 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Enums\PurchasingRole;
use App\Models\AuditLog;
use App\Models\AwardCancellation;
use App\Models\BudgetEntry;
use App\Models\ExternalPayment;
use App\Models\GoodsReceipt;
use App\Models\Invoice;
use App\Models\PurchaseAward;
use App\Models\PurchaseOrder;
use App\Models\PurchaseRevision;
use App\Models\Quotation;
use App\Models\Requisition;
use App\Models\TechnicalReview;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class PurchaseProcessController extends Controller
{
public function show(Request $request, Requisition $requisition): Response
{
$actor = $request->user();
Requisition::query()->visibleTo($actor)->findOrFail($requisition->id);
$requisition->load(['requester:id,name', 'department:id,name', 'steps']);
$role = $actor->purchasing_role;
$purchases = in_array($role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]);
$finance = in_array($role, [PurchasingRole::Admin, PurchasingRole::Finance]);
$inventory = $purchases || $role === PurchasingRole::Warehouse;
$base = '/requisicoes/'.$requisition->id;
$stages = [];
$documents = [['label' => $requisition->number, 'kind' => 'Requisição', 'href' => $base]];
$decisions = [];
$stages[] = ['label' => 'Requisição e aprovação da necessidade', 'state' => $requisition->status, 'owner' => 'Solicitante e aprovadores', 'detail' => 'Estado do pedido; as decisões individuais constam abaixo.', 'href' => $base];
foreach ($requisition->steps as $step) {
$decisions[] = ['area' => 'Estimativa', 'role' => $step->role, 'decision' => $step->decision, 'actor_id' => $step->approver_id, 'date' => $step->decided_at];
}
$equipment = $requisition->items()->where('purchase_type', 'equipment')->exists();
$reviews = TechnicalReview::query()->where('requisition_id', $requisition->id)->orderBy('id')->get();
$stages[] = ['label' => 'Validação técnica', 'state' => $reviews->isNotEmpty() ? 'recorded' : ($equipment ? 'not_started' : 'not_required'), 'owner' => 'Responsável da área', 'detail' => 'O histórico não comprova validade da base actual; cada operação volta a verificá-la.', 'href' => $base];
foreach ($reviews as $review) {
$decisions[] = ['area' => 'Técnica', 'role' => 'department_head', 'decision' => $review->decision, 'actor_id' => $review->reviewed_by, 'date' => $review->reviewed_at];
}
$stages[] = ['label' => 'Controlo orçamental', 'state' => $finance ? (BudgetEntry::query()->where('requisition_id', $requisition->id)->exists() ? 'recorded' : 'not_started') : 'restricted', 'owner' => 'Finanças', 'detail' => 'Lançamentos associados ao processo. A presença de registos não certifica o orçamento nem a política da empresa.', 'href' => $finance ? '/orcamentos' : null];
$award = $purchases ? PurchaseAward::query()->where('requisition_id', $requisition->id)->with('approvals')->first() : null;
$order = $inventory ? PurchaseOrder::query()->visibleTo($actor)->where('requisition_id', $requisition->id)->first(['id', 'number']) : null;
$quoteCount = $purchases ? Quotation::query()->where('requisition_id', $requisition->id)->count() : 0;
$stages[] = ['label' => 'Propostas e comparação', 'state' => $purchases ? ($quoteCount ? 'recorded' : 'not_started') : 'restricted', 'owner' => 'Compras e avaliadores', 'detail' => $purchases ? $quoteCount.' propostas registadas. Quantidade não significa elegibilidade ou avaliação concluída.' : 'Disponível aos perfis autorizados para compras.', 'href' => $purchases ? $base.'/cotacoes' : null];
$stages[] = ['label' => 'Adjudicação', 'state' => $purchases ? ($award->status ?? 'not_started') : 'restricted', 'owner' => 'Compras e aprovadores do valor final', 'detail' => 'A decisão sobre a estimativa e a decisão sobre o valor adjudicado são distintas.', 'href' => $purchases ? $base.'/adjudicacao' : null];
if ($purchases && $quoteCount) {
$documents[] = ['label' => 'Propostas registadas', 'kind' => 'Comparação', 'href' => $base.'/cotacoes'];
}
if ($award) {
$documents[] = ['label' => 'Adjudicação #'.$award->id, 'kind' => 'Adjudicação', 'href' => $base.'/adjudicacao'];
foreach ($award->approvals as $step) {
$decisions[] = ['area' => 'Adjudicação', 'role' => $step->role, 'decision' => $step->decision, 'actor_id' => $step->approver_id, 'date' => $step->decided_at];
}
}
$stages[] = ['label' => 'Ordem de compra', 'state' => $inventory ? ($order ? 'issued' : 'not_started') : 'restricted', 'owner' => 'Compras', 'detail' => 'Documento emitido com os dados acordados preservados.', 'href' => $order && $purchases ? '/ordens/'.$order->id : null];
if ($order && $purchases) {
$documents[] = ['label' => $order->number, 'kind' => 'Ordem de compra', 'href' => '/ordens/'.$order->id];
}
$receipts = $order ? GoodsReceipt::query()->where('purchase_order_id', $order->id)->orderBy('id')->get(['id']) : collect();
$stages[] = ['label' => 'Recepção', 'state' => $inventory ? ($receipts->isNotEmpty() ? 'recorded' : 'not_started') : 'restricted', 'owner' => 'Armazém', 'detail' => $inventory ? $receipts->count().' recepções registadas. Podem existir entregas parciais ou rejeições.' : 'Disponível aos perfis autorizados para recepção.', 'href' => $order ? '/ordens/'.$order->id.'/recepcoes' : null];
foreach ($receipts as $receipt) {
$documents[] = ['label' => 'Recepção #'.$receipt->id, 'kind' => 'Recepção', 'href' => '/recepcoes/'.$receipt->id];
}
$invoices = $finance && $order ? Invoice::query()->where('purchase_order_id', $order->id)->get(['id', 'number', 'status']) : collect();
$payments = $finance ? ExternalPayment::query()->whereIn('invoice_id', $invoices->pluck('id'))->get(['id', 'status']) : collect();
$stages[] = ['label' => 'Facturas', 'state' => $finance ? ($invoices->isNotEmpty() ? 'recorded' : 'not_started') : 'restricted', 'owner' => 'Finanças e conferente independente', 'detail' => $finance ? $invoices->count().' registadas; '.$invoices->where('status', 'validated')->count().' validadas. O registo não equivale a validação.' : 'Disponível a Finanças e Administração.', 'href' => $finance ? '/facturas' : null];
$stages[] = ['label' => 'Pagamentos externos', 'state' => $finance ? ($payments->isNotEmpty() ? 'recorded' : 'not_started') : 'restricted', 'owner' => 'Finanças e conferente independente', 'detail' => $finance ? $payments->count().' registos; '.$payments->where('status', 'confirmed')->count().' confirmados. Não comprova liquidação integral e não executa transferências.' : 'Disponível a Finanças e Administração.', 'href' => $finance ? '/pagamentos' : null];
foreach ($invoices as $invoice) {
$documents[] = ['label' => $invoice->number, 'kind' => 'Factura', 'href' => '/facturas'];
}
$cancellation = $award ? AwardCancellation::query()->where('purchase_award_id', $award->id)->first(['id', 'status']) : null;
$actors = User::query()->where('tenant_id', $actor->tenant_id)->whereIn('id', array_column($decisions, 'actor_id'))->pluck('name', 'id');
foreach ($decisions as &$decision) {
$decision['actor'] = $decision['actor_id'] ? ($actors[$decision['actor_id']] ?? 'Utilizador do processo') : null;
unset($decision['actor_id']);
}
unset($decision);
$related = [];
foreach (PurchaseRevision::query()->where(fn ($query) => $query->where('source_requisition_id', $requisition->id)->orWhere('requisition_id', $requisition->id))->get() as $link) {
$otherId = $link->requisition_id === $requisition->id ? $link->source_requisition_id : $link->requisition_id;
$other = Requisition::query()->visibleTo($actor)->find($otherId, ['id', 'number']);
if ($other) {
$related[] = ['label' => $other->number, 'href' => '/requisicoes/'.$other->id.'/processo'];
}
}
$actions = ['requisition.created', 'requisition.updated', 'requisition.submitted', 'approval.approved', 'approval.rejected', 'equipment.technical_approved', 'equipment.technical_rejected', 'requisition.revision_created'];
if ($purchases) {
$actions = [...$actions, 'award.created', 'award.approved', 'award.rejected', 'purchase_order.issued', 'cancellation.requested', 'cancellation.approved', 'cancellation.rejected'];
}
if ($inventory) {
$actions[] = 'receipt.confirmed';
}
if ($finance) {
$actions = [...$actions, 'invoice.created', 'invoice.validated', 'payment.registered', 'payment.confirmed', 'payment.rejected', 'budget.reserve', 'budget.award', 'budget.commit', 'budget.release'];
}
Inertia::encryptHistory();
$response = Inertia::render('purchases/process', [
'process' => ['number' => $requisition->number, 'status' => $requisition->status, 'requester' => $requisition->requester?->name, 'department' => $requisition->department?->name, 'needed_by' => $requisition->needed_by, 'href' => $base],
'stages' => $stages, 'documents' => $documents, 'decisions' => $decisions, 'related' => $related,
'cancellation' => $cancellation ? ['status' => $cancellation->status, 'href' => '/revisao-compras'] : null,
'history' => AuditLog::query()->where('requisition_id', $requisition->id)->whereIn('action', $actions)->orderBy('id')->get(['id', 'action', 'created_at']),
'future' => ['Plano de compras e marcos previstos/realizados', 'Sessão de abertura e acta', 'Qualificação específica do concurso', 'Relatório de recomendação em etapas'],
])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
}