Implementa regras de investimento por empresa com dupla aprovacao
This commit is contained in:
@@ -45,10 +45,11 @@ class BudgetController extends Controller
|
||||
{
|
||||
$this->access($request);
|
||||
$data = $request->validate(['cost_center_id' => ['required', 'integer', Rule::exists('cost_centers', 'id')->where('tenant_id', $request->user()->tenant_id)], 'year' => 'required|integer|min:2026|max:2100', 'includes_tax' => 'required|boolean', 'amount' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'reason' => 'required|string|max:4000']);
|
||||
DB::transaction(function () use ($request, $data) {
|
||||
$kind = $request->validate(['kind' => ['sometimes', Rule::in(['operating', 'investment'])]])['kind'] ?? 'operating';
|
||||
DB::transaction(function () use ($request, $data, $kind) {
|
||||
app(ConflictGuard::class)->lockCompany($request->user());
|
||||
abort_if(Budget::query()->where('cost_center_id', $data['cost_center_id'])->where('year', $data['year'])->exists(), 409, 'Já existe dotação para este centro e exercício.');
|
||||
$budget = Budget::query()->create(['tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id, 'cost_center_id' => $data['cost_center_id'], 'year' => $data['year'], 'includes_tax' => $data['includes_tax'], 'amount_minor' => BigDecimal::of($data['amount'])->multipliedBy(100)->toInt(), 'reason' => $data['reason']]);
|
||||
$budget = Budget::query()->create(['tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id, 'cost_center_id' => $data['cost_center_id'], 'year' => $data['year'], 'kind' => $kind, 'includes_tax' => $data['includes_tax'], 'amount_minor' => BigDecimal::of($data['amount'])->multipliedBy(100)->toInt(), 'reason' => $data['reason']]);
|
||||
app(RequisitionWorkflow::class)->audit($request->user(), 'budget.created', null, null, $budget->toArray());
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\InvestmentPolicy;
|
||||
use App\Models\InvestmentPolicyReview;
|
||||
use App\Services\InvestmentWorkflow;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class InvestmentPolicyController extends Controller
|
||||
{
|
||||
public function index(Request $request, InvestmentWorkflow $workflow): Response
|
||||
{
|
||||
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance, PurchasingRole::Procurement]), 403);
|
||||
$current = $workflow->current();
|
||||
|
||||
return Inertia::render('investment/index', ['policies' => InvestmentPolicy::query()->latest('revision')->get(), 'reviews' => InvestmentPolicyReview::query()->orderBy('id')->get(), 'currentPolicyId' => $current && $current->valid_until >= today()->format('Y-m-d') ? $current->id : null, 'canManage' => $request->user()->purchasing_role === PurchasingRole::Admin]);
|
||||
}
|
||||
|
||||
public function store(Request $request, InvestmentWorkflow $workflow): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->purchasing_role === PurchasingRole::Admin, 403);
|
||||
$data = $request->validate(['previous_id' => 'required|integer|min:0', 'mode' => 'required|in:ceiling', 'scope' => 'required|in:process_total', 'currency' => 'required|in:MZN', 'ceiling' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'includes_tax' => 'required|boolean', 'valid_from' => 'required|date_format:Y-m-d', 'valid_until' => 'required|date_format:Y-m-d|after_or_equal:valid_from', 'reason' => 'required|string|max:4000']);
|
||||
$workflow->propose($request->user(), $data);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function review(Request $request, InvestmentPolicy $policy, InvestmentWorkflow $workflow): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['decision' => 'required|in:approved,rejected', 'reason' => 'required|string|max:4000']);
|
||||
$workflow->review($request->user(), $policy, $data['decision'], $data['reason']);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
* @property int $amount_minor
|
||||
* @property int $created_by
|
||||
* @property int|null $approved_by
|
||||
* @property string $kind
|
||||
*/
|
||||
class Budget extends Model
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/** @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $created_by
|
||||
* @property int $revision
|
||||
* @property string $status
|
||||
*/
|
||||
class InvestmentPolicy extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $attributes = ['status' => 'pending'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['includes_tax' => 'boolean', 'ceiling_minor' => 'integer'];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updating(function (InvestmentPolicy $policy) {
|
||||
if ($policy->getRawOriginal('status') !== 'pending' || array_diff(array_keys($policy->getDirty()), ['status', 'decided_at', 'updated_at']) !== []) {
|
||||
throw new \LogicException('Preservar os parâmetros e as decisões da regra.');
|
||||
}
|
||||
});
|
||||
static::deleting(fn () => throw new \LogicException('Preservar versões de investimento.'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\AppendOnlyInventoryRecord;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvestmentPolicyReview extends Model
|
||||
{
|
||||
use AppendOnlyInventoryRecord, BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property string $comparison_hash
|
||||
* @property array<string, mixed> $approval_snapshot
|
||||
* @property list<array<string, mixed>> $decision_matrix
|
||||
* @property array<string,mixed>|null $investment_snapshot
|
||||
*/
|
||||
class PurchaseAward extends Model
|
||||
{
|
||||
@@ -25,7 +26,7 @@ class PurchaseAward extends Model
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['approval_snapshot' => 'array', 'decision_matrix' => 'array', 'delivery_date' => 'date:Y-m-d'];
|
||||
return ['approval_snapshot' => 'array', 'decision_matrix' => 'array', 'investment_snapshot' => 'array', 'delivery_date' => 'date:Y-m-d'];
|
||||
}
|
||||
|
||||
/** @return HasMany<PurchaseAwardApproval, $this> */
|
||||
|
||||
@@ -30,6 +30,10 @@ class BudgetWorkflow
|
||||
if (! $budget) {
|
||||
return null;
|
||||
}
|
||||
$requiredKind = $request->items()->where('purchase_type', 'equipment')->exists() ? 'investment' : 'operating';
|
||||
if ($budget->kind !== $requiredKind) {
|
||||
throw ValidationException::withMessages(['budget' => 'A dotação deve ter a classificação explícita adequada: investimento para processos com equipamento; corrente para os restantes.']);
|
||||
}
|
||||
if (! $budget->approved_by || ($kind !== 'release' && (int) $budget->year !== now()->year)) {
|
||||
throw ValidationException::withMessages(['budget' => 'Dotação pendente de aprovação ou exercício encerrado.']);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\Budget;
|
||||
use App\Models\BudgetEntry;
|
||||
use App\Models\InvestmentPolicy;
|
||||
use App\Models\InvestmentPolicyReview;
|
||||
use App\Models\Quotation;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\TechnicalReview;
|
||||
use App\Models\User;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class InvestmentWorkflow
|
||||
{
|
||||
private function admin(User $user): void
|
||||
{
|
||||
abort_unless(auth()->id() === $user->id && $user->tenant_id !== null && $user->purchasing_role === PurchasingRole::Admin, 403);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data */
|
||||
public function propose(User $user, array $data): InvestmentPolicy
|
||||
{
|
||||
$this->admin($user);
|
||||
|
||||
return DB::transaction(function () use ($user, $data) {
|
||||
app(ConflictGuard::class)->lockCompany($user);
|
||||
$user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id);
|
||||
$this->admin($user);
|
||||
$latest = InvestmentPolicy::query()->latest('revision')->first();
|
||||
if ((int) $data['previous_id'] !== ($latest->id ?? 0) || $data['mode'] !== 'ceiling' || $data['scope'] !== 'process_total' || $data['currency'] !== 'MZN') {
|
||||
throw ValidationException::withMessages(['investment' => 'Versão desactualizada ou regra não suportada. Não se presume um circuito de excepção.']);
|
||||
}
|
||||
$policy = InvestmentPolicy::query()->create(['tenant_id' => $user->tenant_id, 'created_by' => $user->id, 'revision' => ($latest->revision ?? 0) + 1, 'mode' => $data['mode'], 'scope' => $data['scope'], 'currency' => $data['currency'], 'ceiling_minor' => BigDecimal::of((string) $data['ceiling'])->multipliedBy(100)->toScale(0)->toInt(), 'includes_tax' => $data['includes_tax'], 'valid_from' => $data['valid_from'], 'valid_until' => $data['valid_until'], 'reason' => $data['reason']]);
|
||||
app(RequisitionWorkflow::class)->audit($user, 'investment.proposed', null, null, $policy->toArray());
|
||||
|
||||
return $policy;
|
||||
});
|
||||
}
|
||||
|
||||
public function review(User $user, InvestmentPolicy $policy, string $decision, string $reason): void
|
||||
{
|
||||
$this->admin($user);
|
||||
abort_unless($policy->tenant_id === $user->tenant_id && $policy->created_by !== $user->id, 403);
|
||||
if (! in_array($decision, ['approved', 'rejected']) || trim($reason) === '') {
|
||||
throw ValidationException::withMessages(['investment' => 'Decisão e fundamento obrigatórios.']);
|
||||
}
|
||||
DB::transaction(function () use ($user, $policy, $decision, $reason) {
|
||||
app(ConflictGuard::class)->lockCompany($user);
|
||||
$user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id);
|
||||
$this->admin($user);
|
||||
$policy = InvestmentPolicy::query()->lockForUpdate()->findOrFail($policy->id);
|
||||
$existing = InvestmentPolicyReview::query()->where('investment_policy_id', $policy->id)->where('reviewed_by', $user->id)->first();
|
||||
if ($existing) {
|
||||
if ($existing->decision !== $decision || $existing->reason !== $reason) {
|
||||
throw ValidationException::withMessages(['investment' => 'Decisão anterior imutável.']);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if ($policy->status !== 'pending' || InvestmentPolicy::query()->max('revision') !== $policy->revision) {
|
||||
throw ValidationException::withMessages(['investment' => 'Versão decidida ou substituída por proposta mais recente.']);
|
||||
}
|
||||
if ($decision === 'approved') {
|
||||
$ids = InvestmentPolicyReview::query()->where('investment_policy_id', $policy->id)->where('decision', 'approved')->pluck('reviewed_by')->push($policy->created_by)->push($user->id)->unique();
|
||||
$actors = User::query()->where('tenant_id', $user->tenant_id)->whereIn('id', $ids)->orderBy('id')->lockForUpdate()->get();
|
||||
if ($actors->count() !== $ids->count() || $actors->contains(fn (User $actor) => $actor->purchasing_role !== PurchasingRole::Admin)) {
|
||||
throw ValidationException::withMessages(['investment' => 'Proponente e validadores devem continuar elegíveis até à publicação.']);
|
||||
}
|
||||
}
|
||||
InvestmentPolicyReview::query()->create(['tenant_id' => $user->tenant_id, 'investment_policy_id' => $policy->id, 'reviewed_by' => $user->id, 'decision' => $decision, 'reason' => $reason, 'created_at' => now()]);
|
||||
if ($decision === 'rejected') {
|
||||
$policy->update(['status' => 'rejected', 'decided_at' => now()]);
|
||||
} elseif (InvestmentPolicyReview::query()->where('investment_policy_id', $policy->id)->where('decision', 'approved')->count() === 2) {
|
||||
$policy->update(['status' => 'approved', 'decided_at' => now()]);
|
||||
}
|
||||
app(RequisitionWorkflow::class)->audit($user, 'investment.'.$decision, null, null, ['policy_id' => $policy->id, 'status' => $policy->status]);
|
||||
});
|
||||
}
|
||||
|
||||
public function current(): ?InvestmentPolicy
|
||||
{
|
||||
return InvestmentPolicy::query()->where('status', 'approved')->where('valid_from', '<=', today()->format('Y-m-d'))->latest('revision')->first();
|
||||
}
|
||||
|
||||
public function requireCurrent(): InvestmentPolicy
|
||||
{
|
||||
$policy = $this->current();
|
||||
if (! $policy || $policy->valid_until < today()->format('Y-m-d') || $policy->mode !== 'ceiling' || $policy->scope !== 'process_total' || $policy->currency !== 'MZN') {
|
||||
throw ValidationException::withMessages(['purchase_type' => 'É necessária regra de investimento própria, com duas aprovações administrativas e vigência válida. Sem regra aplicável, adjudicação e emissão de equipamento bloqueadas.']);
|
||||
}
|
||||
|
||||
return $policy;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function check(Requisition $request, Quotation $quotation): ?array
|
||||
{
|
||||
if (! $request->items()->where('purchase_type', 'equipment')->exists()) {
|
||||
return null;
|
||||
}
|
||||
$policy = $this->requireCurrent();
|
||||
$amount = $policy->includes_tax ? $quotation->total_minor : $quotation->subtotal_minor;
|
||||
if ($amount > $policy->ceiling_minor || $quotation->currency !== 'MZN') {
|
||||
throw ValidationException::withMessages(['investment' => 'O total do processo excede o tecto de investimento da empresa ou usa moeda não suportada. Não há dispensa automática.']);
|
||||
}
|
||||
app(EquipmentWorkflow::class)->assertTechnical($request);
|
||||
$entry = BudgetEntry::query()->where('requisition_id', $request->id)->where('kind', 'reserve')->first();
|
||||
$budget = $entry ? Budget::query()->lockForUpdate()->find($entry->budget_id) : null;
|
||||
if (! $budget || ! $budget->approved_by || $budget->kind !== 'investment' || $budget->cost_center_id !== $request->cost_center_id || (int) $budget->year !== now()->year) {
|
||||
throw ValidationException::withMessages(['investment' => 'Exige reserva em dotação de investimento aprovada do centro e exercício. Não se atribui orçamento retroactivo.']);
|
||||
}
|
||||
$basis = app(EquipmentWorkflow::class)->basis($request);
|
||||
$technical = TechnicalReview::query()->where('requisition_id', $request->id)->where('basis_hash', $basis)->where('decision', 'approved')->firstOrFail();
|
||||
|
||||
return [...$policy->only(['id', 'revision', 'mode', 'scope', 'currency', 'ceiling_minor', 'includes_tax', 'valid_from', 'valid_until']), 'reviewer_ids' => InvestmentPolicyReview::query()->where('investment_policy_id', $policy->id)->orderBy('reviewed_by')->pluck('reviewed_by')->all(), 'budget_id' => $budget->id, 'technical_review_id' => $technical->id, 'technical_basis_hash' => $basis];
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class PurchaseWorkflow
|
||||
$this->sourceApproved($requisition, $rules);
|
||||
$quote = Quotation::query()->where('requisition_id', $requisition->id)->lockForUpdate()->findOrFail($data['quotation_id']);
|
||||
$result = $this->checkComparison($user, $requisition, $quote);
|
||||
$investment = app(InvestmentWorkflow::class)->check($requisition, $quote);
|
||||
app(BudgetWorkflow::class)->record($user, $requisition, 'award', $quote->total_minor, $quote->subtotal_minor);
|
||||
$award = PurchaseAward::query()->create([
|
||||
'tenant_id' => $user->tenant_id, 'requisition_id' => $requisition->id, 'quotation_id' => $quote->id,
|
||||
@@ -43,8 +44,9 @@ class PurchaseWorkflow
|
||||
'delivery_location' => $data['delivery_location'], 'status' => 'pending',
|
||||
'subtotal_minor' => $quote->subtotal_minor, 'tax_minor' => $quote->tax_minor, 'total_minor' => $quote->total_minor,
|
||||
'approval_snapshot' => $rules, 'decision_matrix' => $result['rows'], 'comparison_hash' => $result['fingerprint'],
|
||||
'investment_snapshot' => $investment,
|
||||
]);
|
||||
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $quote->total_minor : $quote->subtotal_minor);
|
||||
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $quote->total_minor : $quote->subtotal_minor, $requisition->items()->where('purchase_type', 'equipment')->exists());
|
||||
foreach ($roles as $position => $role) {
|
||||
$award->approvals()->create(['tenant_id' => $user->tenant_id, 'position' => $position + 1, 'role' => $role]);
|
||||
}
|
||||
@@ -130,6 +132,8 @@ class PurchaseWorkflow
|
||||
$snapshot = [
|
||||
'development_only' => true, 'budget_checked' => false,
|
||||
'budget_control' => app(BudgetWorkflow::class)->record($user, $requisition, 'commit', $quote->total_minor, $quote->subtotal_minor),
|
||||
'investment_rules' => $award->investment_snapshot,
|
||||
'equipment_requests' => $requisition->items()->where('purchase_type', 'equipment')->get()->map(fn ($line) => $line->only(['id', 'description', 'production_area']))->all(),
|
||||
'company' => Tenant::query()->findOrFail($user->tenant_id)->only(['id', 'name']),
|
||||
'requisition' => $requisition->only(['id', 'number', 'justification']),
|
||||
'department' => $requisition->department->only(['id', 'name']),
|
||||
@@ -166,6 +170,9 @@ class PurchaseWorkflow
|
||||
$this->supportedItems($requisition);
|
||||
$this->sourceApproved($requisition, $rules);
|
||||
$quote = Quotation::query()->where('requisition_id', $requisition->id)->lockForUpdate()->findOrFail($award->quotation_id);
|
||||
if (app(InvestmentWorkflow::class)->check($requisition, $quote) !== $award->investment_snapshot) {
|
||||
throw ValidationException::withMessages(['investment' => 'A regra de investimento ou a base técnica/orçamental mudou. As aprovações anteriores não autorizam uma emissão com outra base.']);
|
||||
}
|
||||
$result = $this->checkComparison($user, $requisition, $quote);
|
||||
if ($result['fingerprint'] !== $award->comparison_hash || $award->approval_snapshot !== $rules ||
|
||||
$quote->subtotal_minor !== $award->subtotal_minor || $quote->tax_minor !== $award->tax_minor || $quote->total_minor !== $award->total_minor) {
|
||||
@@ -214,13 +221,13 @@ class PurchaseWorkflow
|
||||
/** @param array{first_limit_minor: int, second_limit_minor: int, approval_includes_tax: bool, rules_provisional: bool} $rules
|
||||
* @return list<string>
|
||||
*/
|
||||
private function roles(array $rules, int $amount): array
|
||||
private function roles(array $rules, int $amount, bool $equipment = false): array
|
||||
{
|
||||
$roles = ['department_head'];
|
||||
if ($amount > $rules['first_limit_minor']) {
|
||||
$roles[] = 'procurement';
|
||||
}
|
||||
if ($amount > $rules['second_limit_minor']) {
|
||||
if ($amount > $rules['second_limit_minor'] || $equipment) {
|
||||
$roles[] = 'finance';
|
||||
}
|
||||
|
||||
@@ -230,7 +237,7 @@ class PurchaseWorkflow
|
||||
/** @param array{first_limit_minor: int, second_limit_minor: int, approval_includes_tax: bool, rules_provisional: bool} $rules */
|
||||
private function sourceApproved(Requisition $requisition, array $rules): void
|
||||
{
|
||||
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $requisition->total_minor : $requisition->subtotal_minor);
|
||||
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $requisition->total_minor : $requisition->subtotal_minor, $requisition->items()->where('purchase_type', 'equipment')->exists());
|
||||
$steps = $requisition->steps()->get();
|
||||
$seen = [$requisition->requester_id];
|
||||
if ($requisition->status !== 'approved' || $steps->count() !== count($roles)) {
|
||||
@@ -248,7 +255,7 @@ class PurchaseWorkflow
|
||||
private function validateFinalApprovals(PurchaseAward $award, Requisition $requisition): void
|
||||
{
|
||||
$rules = $this->rules($requisition);
|
||||
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $award->total_minor : $award->subtotal_minor);
|
||||
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $award->total_minor : $award->subtotal_minor, $requisition->items()->where('purchase_type', 'equipment')->exists());
|
||||
$steps = $award->approvals()->get();
|
||||
$seen = [$requisition->requester_id, $award->created_by];
|
||||
if ($steps->count() !== count($roles)) {
|
||||
@@ -268,10 +275,10 @@ class PurchaseWorkflow
|
||||
private function supportedItems(Requisition $requisition): void
|
||||
{
|
||||
if ($requisition->items()->where('purchase_type', 'equipment')->exists()) {
|
||||
throw ValidationException::withMessages(['purchase_type' => 'O limite próprio de investimento e a sua regra de aplicação ainda não foram definidos pela empresa. A validação técnica e Finanças não dispensam essa decisão; adjudicação e emissão de equipamento bloqueadas.']);
|
||||
app(InvestmentWorkflow::class)->requireCurrent();
|
||||
}
|
||||
foreach ($requisition->items()->get() as $line) {
|
||||
if (! in_array($line->purchase_type, ['consumable', 'service', 'other', 'spare_part']) || ($line->purchase_type === 'spare_part' && (! $line->equipment_id || ! $line->spare_part_id))) {
|
||||
if (! in_array($line->purchase_type, ['consumable', 'service', 'other', 'spare_part', 'equipment']) || ($line->purchase_type === 'spare_part' && (! $line->equipment_id || ! $line->spare_part_id))) {
|
||||
throw ValidationException::withMessages(['purchase_type' => 'Tipo não suportado ou peça sem equipamento/artigo associado. Complete o cadastro antes da compra.']);
|
||||
}
|
||||
app(EquipmentWorkflow::class)->line($line->toArray(), $requisition->department_id, $requisition->priority);
|
||||
|
||||
@@ -134,7 +134,7 @@ class ReceiptWorkflow
|
||||
throw ValidationException::withMessages(['items' => 'Descreva as reservas, danos ou divergências de qualidade.']);
|
||||
}
|
||||
$stockItem = ! empty($input['stock_item_id']) ? StockItem::query()->lockForUpdate()->findOrFail($input['stock_item_id']) : null;
|
||||
$physical = in_array($source->purchase_type, ['consumable', 'other', 'spare_part']);
|
||||
$physical = in_array($source->purchase_type, ['consumable', 'other', 'spare_part', 'equipment']);
|
||||
if ($source->purchase_type === 'spare_part') {
|
||||
if (! $source->spare_part_id || ! $source->equipment_id) {
|
||||
throw ValidationException::withMessages(['items' => 'Peça sem associação autorizada na ordem. A recepção está bloqueada.']);
|
||||
|
||||
Reference in New Issue
Block a user