Files
compras/app/Http/Controllers/PurchaseProcessController.php
2026-11-02 20:27:11 +02:00

141 lines
12 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\ProposalOpening;
use App\Models\PurchaseAward;
use App\Models\PurchaseOrder;
use App\Models\PurchasePlanItem;
use App\Models\PurchaseRevision;
use App\Models\QualificationSet;
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('web');
abort_unless($actor instanceof User, 403);
Requisition::query()->visibleTo($actor)->findOrFail($requisition->id);
$requisition->load(['requester:id,name', 'department:id,name', 'steps']);
$purchases = $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]);
$finance = $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance]);
$inventory = $purchases || $actor->hasFunctions([PurchasingRole::Warehouse]);
$base = '/requisicoes/'.$requisition->id;
$stages = [];
$documents = [['label' => $requisition->number, 'kind' => 'Requisição', 'href' => $base]];
if ($purchases) {
$documents[] = ['label' => 'Relatório de conferência', 'kind' => 'Relatório de consulta', 'href' => $base.'/relatorio'];
$documents[] = ['label' => 'Regime, decisões e conferências', 'kind' => 'Admissão de propostas', 'href' => $base.'/admissao'];
$documents[] = ['label' => 'Regime, tentativas e conferências', 'kind' => 'Recomendação', 'href' => $base.'/recomendacao'];
}
$qualification = $purchases ? QualificationSet::query()->where('requisition_id', $requisition->id)->latest('revision')->first(['id', 'revision']) : null;
if ($qualification) {
$documents[] = ['label' => 'Qualificação documental · Requisitos v'.$qualification->revision, 'kind' => 'Qualificação documental', 'href' => $base.'/qualificacao'];
}
$opening = $purchases ? ProposalOpening::query()->where('requisition_id', $requisition->id)->latest('revision')->first(['id', 'revision']) : null;
if ($opening) {
$documents[] = ['label' => 'Acta de abertura · Versão '.$opening->revision, 'kind' => 'Abertura', 'href' => $base.'/abertura'];
}
$planning = PurchasePlanItem::query()->visibleTo($actor)->where('requisition_id', $requisition->id)->first(['id', 'title']);
if ($planning) {
$documents[] = ['label' => $planning->title, 'kind' => 'Planeamento', 'href' => '/planeamento?item='.$planning->id];
}
$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 && $order ? '/facturas?purchase_order_id='.$order->id : 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?invoice_id='.$invoice->id.'#invoice-'.$invoice->id];
}
foreach ($payments as $payment) {
$documents[] = ['label' => 'Registo externo #'.$payment->id, 'kind' => 'Pagamento externo', 'href' => '/pagamentos?payment_id='.$payment->id.'#payment-'.$payment->id];
}
$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, 'recommendation.adopted', 'recommendation.proposed', 'recommendation.reviewed', 'admission.adopted', 'admission.proposed', 'admission.reviewed', 'qualification.defined', 'qualification.assessed', 'opening.recorded', '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' => [],
])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
}