|
|
|
@@ -3,10 +3,13 @@
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Enums\PurchasingRole;
|
|
|
|
|
use App\Models\ProposalEvaluation;
|
|
|
|
|
use App\Models\PurchaseAward;
|
|
|
|
|
use App\Models\PurchaseOrder;
|
|
|
|
|
use App\Models\Quotation;
|
|
|
|
|
use App\Models\QuotationItem;
|
|
|
|
|
use App\Models\Requisition;
|
|
|
|
|
use App\Models\Supplier;
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
@@ -112,6 +115,11 @@ class PurchaseWorkflow
|
|
|
|
|
...$source->only(['unit', 'category', 'purchase_type']), 'tenant_id' => $user->tenant_id,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
$validUntil = $quote->attributesToArray()['valid_until'] ?? null;
|
|
|
|
|
$deliveryDate = $award->attributesToArray()['delivery_date'] ?? null;
|
|
|
|
|
if (! is_string($validUntil) || ! is_string($deliveryDate)) {
|
|
|
|
|
throw ValidationException::withMessages(['delivery_date' => 'As datas da proposta e da entrega devem estar completas antes da emissão.']);
|
|
|
|
|
}
|
|
|
|
|
$snapshot = [
|
|
|
|
|
'development_only' => true, 'budget_checked' => false,
|
|
|
|
|
'company' => Tenant::query()->findOrFail($user->tenant_id)->only(['id', 'name']),
|
|
|
|
@@ -120,12 +128,12 @@ class PurchaseWorkflow
|
|
|
|
|
'cost_center' => $requisition->costCenter->only(['id', 'code', 'name']),
|
|
|
|
|
'requester' => $requisition->requester?->only(['id', 'name']),
|
|
|
|
|
'supplier' => $supplier->only(['id', 'name', 'nuit', 'contact', 'email', 'phone', 'address']),
|
|
|
|
|
'quotation' => $quote->only(['id', 'reference', 'currency', 'payment_terms', 'delivery_days', 'valid_until']),
|
|
|
|
|
'award' => $award->only(['id', 'justification', 'delivery_date', 'delivery_location']),
|
|
|
|
|
'quotation' => [...$quote->only(['id', 'reference', 'currency', 'payment_terms', 'delivery_days']), 'valid_until' => $validUntil],
|
|
|
|
|
'award' => [...$award->only(['id', 'justification', 'delivery_location']), 'delivery_date' => $deliveryDate],
|
|
|
|
|
'issuer' => $user->only(['id', 'name']), 'approval_rules' => $award->approval_snapshot,
|
|
|
|
|
'estimate_approvals' => $requisition->steps()->get()->toArray(),
|
|
|
|
|
'final_approvals' => $award->approvals()->get()->toArray(),
|
|
|
|
|
'decision_matrix' => $award->decision_matrix, 'items' => $lines,
|
|
|
|
|
'decision_matrix' => $this->comparison->inspect($requisition)['rows'], 'items' => $lines,
|
|
|
|
|
];
|
|
|
|
|
$order = PurchaseOrder::query()->create([
|
|
|
|
|
'tenant_id' => $user->tenant_id, 'requisition_id' => $requisition->id, 'purchase_award_id' => $award->id,
|
|
|
|
@@ -161,6 +169,13 @@ class PurchaseWorkflow
|
|
|
|
|
/** @return array{rows: list<array<string, mixed>>, ready: bool, top_ids: list<int>, fingerprint: string} */
|
|
|
|
|
private function checkComparison(User $user, Requisition $requisition, Quotation $quote): array
|
|
|
|
|
{
|
|
|
|
|
// Also coordinate with supplier reviews, which lock supplier rows without the company mutex.
|
|
|
|
|
$quotes = Quotation::query()->where('requisition_id', $requisition->id)->orderBy('id')->lockForUpdate()->get(['id', 'supplier_id']);
|
|
|
|
|
Supplier::query()->whereIn('id', $quotes->pluck('supplier_id'))->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
QuotationItem::query()->whereIn('quotation_id', $quotes->pluck('id'))->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
ProposalEvaluation::query()->whereIn('quotation_id', $quotes->pluck('id'))->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
User::query()->where('tenant_id', $user->tenant_id)->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
$requisition->items()->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
$this->guard->assertParticipation($user, $requisition);
|
|
|
|
|
$this->guard->assertProcess($requisition, $quote->supplier_id, $user);
|
|
|
|
|
$result = $this->comparison->inspect($requisition);
|
|
|
|
@@ -213,7 +228,7 @@ class PurchaseWorkflow
|
|
|
|
|
throw ValidationException::withMessages(['approvals' => 'A requisição deve ter todas as aprovações obrigatórias da estimativa, preservadas no histórico.']);
|
|
|
|
|
}
|
|
|
|
|
foreach ($steps as $index => $step) {
|
|
|
|
|
if ($step->decision !== 'approved' || $step->role !== $roles[$index] || $step->approver_id === null || in_array($step->approver_id, $seen, true) ||
|
|
|
|
|
if ($step->decision !== 'approved' || $step->decided_at === null || $step->role !== $roles[$index] || $step->approver_id === null || in_array($step->approver_id, $seen, true) ||
|
|
|
|
|
! User::query()->where('tenant_id', $requisition->tenant_id)->whereKey($step->approver_id)->exists()) {
|
|
|
|
|
throw ValidationException::withMessages(['approvals' => 'O histórico de aprovação da estimativa está incompleto ou inconsistente.']);
|
|
|
|
|
}
|
|
|
|
@@ -232,7 +247,7 @@ class PurchaseWorkflow
|
|
|
|
|
}
|
|
|
|
|
foreach ($steps as $index => $step) {
|
|
|
|
|
$user = User::query()->where('tenant_id', $requisition->tenant_id)->find($step->approver_id);
|
|
|
|
|
if (! $user || $step->decision !== 'approved' || $step->role !== $roles[$index] || $user->purchasing_role->value !== $step->role ||
|
|
|
|
|
if (! $user || $step->decision !== 'approved' || $step->decided_at === null || $step->role !== $roles[$index] || $user->purchasing_role->value !== $step->role ||
|
|
|
|
|
($step->role === 'department_head' && $user->department_id !== $requisition->department_id) || in_array($user->id, $seen, true)) {
|
|
|
|
|
throw ValidationException::withMessages(['approvals' => 'As aprovações do valor final devem continuar elegíveis e ser de pessoas distintas.']);
|
|
|
|
|
}
|
|
|
|
|