From c05dc12aea131ed9d6212096b13d331b3c440622 Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 28 Oct 2026 14:06:03 +0200 Subject: [PATCH] Adiciona dotacoes e reservas orcamentais por empresa --- app/Http/Controllers/BudgetController.php | 69 +++++++ app/Models/Budget.php | 23 +++ app/Models/BudgetEntry.php | 16 ++ app/Services/BudgetWorkflow.php | 57 ++++++ app/Services/PurchaseWorkflow.php | 5 + app/Services/RequisitionWorkflow.php | 4 + .../2026_10_28_000009_create_budgets.php | 43 +++++ docs/compatibilidade.md | 24 ++- docs/desenvolvimento.md | 44 +++++ docs/proposta-regras-financeiras.md | 93 +++++++++ resources/js/components/app-sidebar.tsx | 14 ++ resources/js/pages/budgets/index.tsx | 178 ++++++++++++++++++ routes/web.php | 4 + tests/Feature/BudgetTest.php | 105 +++++++++++ 14 files changed, 676 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/BudgetController.php create mode 100644 app/Models/Budget.php create mode 100644 app/Models/BudgetEntry.php create mode 100644 app/Services/BudgetWorkflow.php create mode 100644 database/migrations/2026_10_28_000009_create_budgets.php create mode 100644 docs/proposta-regras-financeiras.md create mode 100644 resources/js/pages/budgets/index.tsx create mode 100644 tests/Feature/BudgetTest.php diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php new file mode 100644 index 0000000..b30c674 --- /dev/null +++ b/app/Http/Controllers/BudgetController.php @@ -0,0 +1,69 @@ +user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403); + } + + public function index(Request $request): Response + { + $this->access($request); + + return Inertia::render('budgets/index', ['centers' => CostCenter::query()->orderBy('code')->get(), 'budgets' => Budget::query()->latest('id')->get()->map(function (Budget $budget) { + $entries = BudgetEntry::query()->where('budget_id', $budget->id); + $reserved = (int) (clone $entries)->sum('reserved_minor'); + $committed = (int) $entries->sum('committed_minor'); + + return [...$budget->toArray(), 'reserved_minor' => $reserved, 'committed_minor' => $committed, 'available_minor' => $budget->amount_minor - $reserved - $committed]; + })]); + } + + public function store(Request $request): RedirectResponse + { + $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) { + 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']]); + app(RequisitionWorkflow::class)->audit($request->user(), 'budget.created', null, null, $budget->toArray()); + }); + + return back(); + } + + public function approve(Request $request, Budget $budget): RedirectResponse + { + $this->access($request); + DB::transaction(function () use ($request, $budget) { + app(ConflictGuard::class)->lockCompany($request->user()); + $budget = Budget::query()->lockForUpdate()->findOrFail($budget->id); + abort_if($budget->created_by === $request->user()->id, 403); + if ($budget->approved_by) { + return; + } + $budget->update(['approved_by' => $request->user()->id, 'approved_at' => now()]); + app(RequisitionWorkflow::class)->audit($request->user(), 'budget.approved', null, null, $budget->toArray()); + }); + + return back(); + } +} diff --git a/app/Models/Budget.php b/app/Models/Budget.php new file mode 100644 index 0000000..99f1c09 --- /dev/null +++ b/app/Models/Budget.php @@ -0,0 +1,23 @@ + 'boolean']; + } +} diff --git a/app/Models/BudgetEntry.php b/app/Models/BudgetEntry.php new file mode 100644 index 0000000..628a731 --- /dev/null +++ b/app/Models/BudgetEntry.php @@ -0,0 +1,16 @@ +|null + */ + public function record(User $user, Requisition $request, string $kind, int $amount = 0, ?int $subtotal = null): ?array + { + $existing = BudgetEntry::query()->where('requisition_id', $request->id)->orderBy('id')->first(); + $budget = $existing + ? Budget::query()->lockForUpdate()->findOrFail($existing->budget_id) + : Budget::query()->where('cost_center_id', $request->cost_center_id)->where('year', now()->year)->lockForUpdate()->first(); + // Unconfigured legacy processes remain development documents, explicitly unchecked. + if (! $budget) { + return null; + } + 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.']); + } + if (! $budget->includes_tax && in_array($kind, ['reserve', 'award', 'commit'])) { + $amount = $subtotal ?? throw new \LogicException('Subtotal necessário para a base sem IVA.'); + } + if (BudgetEntry::query()->where('requisition_id', $request->id)->where('kind', $kind)->exists()) { + return ['budget_id' => $budget->id, 'year' => (int) $budget->year]; + } + $own = BudgetEntry::query()->where('requisition_id', $request->id); + $reserved = (int) (clone $own)->sum('reserved_minor'); + $committed = (int) $own->sum('committed_minor'); + if ($committed !== 0 || ($kind !== 'reserve' && ! $existing)) { + throw ValidationException::withMessages(['budget' => 'Processo sem reserva válida ou já comprometido.']); + } + [$reserveDelta, $commitDelta] = match ($kind) { + 'reserve', 'award' => [$amount - $reserved, 0], + 'release' => [-$reserved, 0], + 'commit' => [-$reserved, $amount], + default => throw new \LogicException('Movimento orçamental inválido.'), + }; + $ledger = BudgetEntry::query()->where('budget_id', $budget->id); + $used = (int) (clone $ledger)->sum('reserved_minor') + (int) $ledger->sum('committed_minor'); + if ($used + $reserveDelta + $commitDelta > $budget->amount_minor) { + throw ValidationException::withMessages(['budget' => 'Saldo orçamental insuficiente. Não são permitidos excessos.']); + } + BudgetEntry::query()->create(['tenant_id' => $user->tenant_id, 'budget_id' => $budget->id, 'requisition_id' => $request->id, 'created_by' => $user->id, 'kind' => $kind, 'reserved_minor' => $reserveDelta, 'committed_minor' => $commitDelta, 'created_at' => now()]); + app(RequisitionWorkflow::class)->audit($user, 'budget.'.$kind, $request, null, ['budget_id' => $budget->id, 'reserved_minor' => $reserveDelta, 'committed_minor' => $commitDelta]); + + return ['budget_id' => $budget->id, 'year' => (int) $budget->year]; + } +} diff --git a/app/Services/PurchaseWorkflow.php b/app/Services/PurchaseWorkflow.php index 3c5e6c3..35e17d2 100644 --- a/app/Services/PurchaseWorkflow.php +++ b/app/Services/PurchaseWorkflow.php @@ -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); + 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, 'created_by' => $user->id, 'justification' => $data['justification'], 'delivery_date' => $data['delivery_date'], @@ -80,6 +81,9 @@ class PurchaseWorkflow $step->update(['decision' => $decision, 'approver_id' => $user->id, 'comment' => $comment, 'decided_at' => now()]); $status = $decision === 'rejected' ? 'rejected' : ($award->approvals()->where('decision', 'pending')->exists() ? 'pending' : 'approved'); $award->update(['status' => $status]); + if ($decision === 'rejected') { + app(BudgetWorkflow::class)->record($user, $requisition, 'release'); + } $this->audit->audit($user, 'award.'.$decision, $requisition, ['status' => 'pending'], ['award_id' => $award->id, 'status' => $status, 'approval' => $step->toArray()]); }); } @@ -125,6 +129,7 @@ class PurchaseWorkflow } $snapshot = [ 'development_only' => true, 'budget_checked' => false, + 'budget_control' => app(BudgetWorkflow::class)->record($user, $requisition, 'commit', $quote->total_minor, $quote->subtotal_minor), 'company' => Tenant::query()->findOrFail($user->tenant_id)->only(['id', 'name']), 'requisition' => $requisition->only(['id', 'number', 'justification']), 'department' => $requisition->department->only(['id', 'name']), diff --git a/app/Services/RequisitionWorkflow.php b/app/Services/RequisitionWorkflow.php index 8c856aa..796352d 100644 --- a/app/Services/RequisitionWorkflow.php +++ b/app/Services/RequisitionWorkflow.php @@ -81,6 +81,7 @@ class RequisitionWorkflow throw ValidationException::withMessages(['submission' => 'Actualize a data de entrega antes de submeter.']); } $tenant = Tenant::query()->findOrFail($user->tenant_id); + app(BudgetWorkflow::class)->record($user, $requisition, 'reserve', $requisition->total_minor, $requisition->subtotal_minor); $amount = $tenant->approval_includes_tax ? $requisition->total_minor : $requisition->subtotal_minor; $roles = ['department_head']; if ($amount > $tenant->first_limit_minor) { @@ -113,6 +114,9 @@ class RequisitionWorkflow $step->update(['decision' => $decision, 'approver_id' => $user->id, 'comment' => $comment, 'decided_at' => now()]); $status = $decision === 'rejected' ? 'rejected' : ($requisition->steps()->where('decision', 'pending')->exists() ? 'pending' : 'approved'); $requisition->update(['status' => $status]); + if ($decision === 'rejected') { + app(BudgetWorkflow::class)->record($user, $requisition, 'release'); + } $this->audit($user, 'approval.'.$decision, $requisition, ['status' => 'pending'], ['status' => $status, 'step_id' => $step->id, 'role' => $step->role, 'comment' => $comment]); }); } diff --git a/database/migrations/2026_10_28_000009_create_budgets.php b/database/migrations/2026_10_28_000009_create_budgets.php new file mode 100644 index 0000000..1179de7 --- /dev/null +++ b/database/migrations/2026_10_28_000009_create_budgets.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('tenant_id')->constrained()->restrictOnDelete(); + $table->foreignId('cost_center_id')->constrained()->restrictOnDelete(); + $table->unsignedSmallInteger('year'); + $table->bigInteger('amount_minor'); + $table->boolean('includes_tax'); + $table->text('reason'); + $table->foreignId('created_by')->constrained('users')->restrictOnDelete(); + $table->foreignId('approved_by')->nullable()->constrained('users')->restrictOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->timestamps(); + $table->unique(['tenant_id', 'cost_center_id', 'year']); + }); + Schema::create('budget_entries', function (Blueprint $table) { + $table->id(); + $table->foreignId('tenant_id')->constrained()->restrictOnDelete(); + $table->foreignId('budget_id')->constrained()->restrictOnDelete(); + $table->foreignId('requisition_id')->constrained()->restrictOnDelete(); + $table->foreignId('created_by')->constrained('users')->restrictOnDelete(); + $table->string('kind'); + $table->bigInteger('reserved_minor'); + $table->bigInteger('committed_minor'); + $table->timestamp('created_at'); + $table->unique(['requisition_id', 'kind']); + }); + } + + public function down(): void + { + throw new LogicException('Preservar dotações e movimentos orçamentais.'); + } +}; diff --git a/docs/compatibilidade.md b/docs/compatibilidade.md index 28140f4..4585d27 100644 --- a/docs/compatibilidade.md +++ b/docs/compatibilidade.md @@ -12,8 +12,8 @@ Estado actualizado em 28 de Outubro de 2026. Projecto activo: C:\laragon\www\com | Adjudicação | Escolha fundamentada, revalidação e aprovação do valor final | Excepções, revisão e cancelamento sem política ficam bloqueados | | Ordens | Emissão única/idempotente, snapshots, consulta e impressão | Orçamento não validado; documentos de desenvolvimento | | Recepção | Parcial/total, qualidade, divergências, pendentes, segregação do emitente e auditoria | Substituições e anexos; rejeições não autorizam receber acima do total | -| Stock | Artigos, armazéns, entradas, saídas fundamentadas, transferências atómicas, contagens e ajustes auditados por empresa | Revisão visual; mínimos, reposição, equipamento/peças e conversões | -| Equipamentos e peças | Tipos não suportados ficam bloqueados | Aprovação técnica e regras específicas | +| Stock | Artigos, armazéns, entradas, saídas fundamentadas, transferências atómicas, contagens e ajustes auditados por empresa | Revisão visual, reposição e conversões | +| Equipamentos e peças | Cadastro, compatibilidade, mínimos, alertas, validação técnica e rastreabilidade | Regra de investimento; adjudicação/emissão de equipamento bloqueadas | | Orçamento, facturas e pagamentos | Ainda não implementados | Definir regras antes do próximo incremento | | Início e navegação | Página inicial própria, menus por área/perfil e consulta pública por módulo | Revisão visual | | Auditoria | Histórico das operações implementadas | Exportações, retenção e protecção contra SQL administrativo | @@ -22,6 +22,24 @@ Preservar enums, políticas, modelos relacionais, Brick Math e isolamento por em ## Verificação actual +Referência consolidada: `75ffdb3`, com **262 testes aprovados, 3 ignorados e +2.327 asserções**. Nova verificação dirigida de equipamentos, adjudicação, +ordens e navegação: **87 testes aprovados, 1.163 asserções**. As onze migrações, +incluindo 000008 no lote 9, foram confirmadas aplicadas por `migrate:status`. +As páginas públicas inicial, consulta, equipamentos, recepções, stock e operações +responderam HTTP 200. Os acessos sem sessão a equipamentos, recepções, stock e +ordens responderam 302 para login. Estes resultados não são revisão visual. + +O navegador voltou a reportar `The browser access host is unavailable`. +Revisão visual pública, autenticada e de impressão permanece pendente. +Orçamento e facturas não estão implementados; as regras empresariais ainda +estão por definir. A [proposta de regras financeiras](proposta-regras-financeiras.md) +é um documento para decisão, não uma aprovação. **O orçamento continua sem validação.** +Sete cenários PostgreSQL de stock constam da evidência histórica, incluindo +saída de peça com equipamento; não foram novamente executados nesta revisão. + +## Histórico da entrega de stock anterior aos equipamentos + Na implementação, 233 testes passaram, 3 ignorados, 2.163 asserções em SQLite em memória; PHPStan, Pint, frontend, TypeScript e compilação aprovados. **Migrações até 000007 efectivamente aplicadas na base actual; 000007 no lote 8, sem migrações pendentes.** Antes da aplicação foi criado backup custom PostgreSQL e restaurado com sucesso numa base temporária exclusiva, depois eliminada. Contagens e hashes das 31 tabelas anteriores coincidiram antes/depois; restrições existentes preservadas. Mantêm-se 1 utilizador, 1 empresa, 1 departamento e 1 centro de custo. Sem seeds, recriação de contas ou movimentos fictícios; `stock_movements` e `stock_operations` vazias. `.env` inalterado. [Evidência da migração e recuperação](stock-migracao-000007.json). Backup privado: `storage/app/private/backups/stock-000007-20261028-111943-4deef2/`, com `database.dump`, manifesto, cópia do ambiente e instruções de recuperação para uma base nova. Nenhuma restauração foi feita sobre a base actual. A migração acrescenta a tabela e ligação de operações, relaxa a obrigatoriedade da recepção e preserva os valores existentes; o PostgreSQL já utilizava `bigint` assinado. `down()` permanece bloqueado para proteger o histórico. @@ -30,4 +48,4 @@ Quatro cenários simultâneos de recepção passaram numa base PostgreSQL separa Seis cenários simultâneos de stock aprovados em PostgreSQL separado, eliminado no final: excesso de saídas, repetição de saída/transferência/ajuste, transferências opostas e ajuste desactualizado. [Evidência](stock-concorrencia-postgresql.json). Apenas Armazém executa; Administração, Procurement e Finanças consultam. Contagens não movimentam stock; ajustes fundamentados exigem contagem compatível e sem movimentos posteriores. Periodicidade, tolerâncias e investigação/resolução das divergências exigem decisão da empresa; não foi inventado um circuito de aprovação. -Após a aplicação, `/consulta/operacoes-stock` respondeu HTTP 200 e `/stock/operacoes` redireccionou para login (302) no cliente sem sessão. Revisão visual pública e autenticada pendentes: a nova tentativa devolveu `The browser access host is unavailable`, impedindo também verificar a sessão existente. A revisão de impressão continua pendente. Os resultados de testes acima são os da implementação, não uma nova execução nesta aplicação. Resultados históricos, âmbito e limitações em [desenvolvimento.md](desenvolvimento.md). **O orçamento continua sem validação.** Equipamentos não foram iniciados. +Após a aplicação, `/consulta/operacoes-stock` respondeu HTTP 200 e `/stock/operacoes` redireccionou para login (302) no cliente sem sessão. Os resultados desta secção são históricos. A entrega posterior de equipamentos substitui as antigas indicações de que esse módulo ainda não tinha sido iniciado. Resultados e limitações em [desenvolvimento.md](desenvolvimento.md). diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index 8e9b9d1..11b4bdf 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -1,5 +1,49 @@ # Desenvolvimento do SGC +## Primeiro incremento de orçamento por empresa + +Implementados cadastro de dotação anual por empresa/centro de custo, montantes +explícitos em centavos MZN e escolha obrigatória da base com/sem IVA. Preparação +e aprovação por pessoas distintas de Administração/Finanças. Sem valores de +dotação predefinidos. A configuração pertence exclusivamente à empresa actual. + +Reservas na submissão, ajuste na adjudicação, libertação na rejeição e conversão +em compromisso na emissão reutilizam a transacção e o bloqueio por empresa. +Movimentos imutáveis, idempotência por processo/operação e auditoria transaccional. +Dotação pendente ou insuficiente bloqueia operações no centro/exercício configurado. +Centros sem dotação preservam o fluxo anterior de desenvolvimento, explicitamente +sem controlo orçamental. Não há activação global implícita para todos os clientes. +O snapshot mantém `budget_checked=false`, acrescentando a referência ao controlo +efectuado; não constitui validação financeira completa ou autorização de compra real. + +Interface em `/orcamentos`, restrita a Administração e Finanças. Não existem +reforços, edição de dotações, facturas, pagamentos ou migração financeira de +processos anteriores. Os limites de investimento continuam por definir por empresa; +equipamentos permanecem bloqueados para adjudicação/emissão. + +Migração aditiva 000009 preparada e testada apenas em SQLite isolado; **não aplicada +à base real**. Exige backup/restauro verificado antes da aplicação. Não foram +alteradas contas, configurações, credenciais ou dados de negócio. Dez testes novos +passaram, com 27 asserções; PHPStan e TypeScript aprovados. Revisão visual e +concorrência PostgreSQL deste módulo continuam pendentes. **O orçamento continua +sem validação operacional.** + +## Validação das pendências após consolidação + +Recolhido o resultado final da nova execução dirigida: **87 testes aprovados, +1.163 asserções**, abrangendo equipamentos/peças, adjudicação/ordens e navegação. +Não substitui o resultado da suite completa consolidada de 262 aprovados e 3 +ignorados. Onze migrações confirmadas aplicadas, sem pendentes. Seis entradas +públicas responderam HTTP 200; quatro rotas protegidas responderam 302 para login. + +O acesso ao navegador continua indisponível (`The browser access host is unavailable`). +A revisão visual pública, autenticada e de impressão não foi concluída. +O utilizador confirmou que as regras financeiras estão por definir. Foi preparada +uma [proposta de regras](proposta-regras-financeiras.md) para decisão empresarial, +sem activar orçamento, facturas ou desbloquear investimentos. **O orçamento +continua sem validação.** Corrigido o mapa de compatibilidade que ainda descrevia +equipamentos como não iniciados. + ## Revisão e consolidação — 28 de Outubro de 2026 - Recolhido o resultado final da execução da suite: **262 testes aprovados, diff --git a/docs/proposta-regras-financeiras.md b/docs/proposta-regras-financeiras.md new file mode 100644 index 0000000..8551b1c --- /dev/null +++ b/docs/proposta-regras-financeiras.md @@ -0,0 +1,93 @@ +# Proposta de regras financeiras do SGC + +Estado: proposta para decisão empresarial, ainda sem aprovação nem implementação. +Referência funcional: entrega `75ffdb3`. O orçamento continua sem validação. + +## Abordagem recomendada + +Implementar primeiro orçamento e reservas; depois compromisso de ordens e +conferência de facturas. Pagamentos ficam para um incremento posterior. +Cada etapa exige testes e auditoria antes de permitir utilização operacional. + +## Orçamento + +- Exercício anual, por empresa e centro de custo, em MZN. Guardar os montantes + em centavos. Datas de início/fim são explícitas; não transportar saldos + automaticamente para outro exercício. +- Proposta inicial: controlar o total com IVA, por prudência de tesouraria. + Finanças deve confirmar esta base e a política de IVA antes da implementação. +- Dotação inicial e reforços exigem preparação e aprovação por duas pessoas + distintas da empresa, com fundamento, data e histórico. Nenhuma alteração + substitui os registos anteriores ou cria saldo por omissão. +- Disponível = dotação aprovada − reservas abertas − compromissos abertos + − consumo líquido validado. As três parcelas são mutuamente exclusivas para + cada quantidade/valor: converter entre elas não duplica a despesa. +- Rascunho não reserva. Submissão reserva a estimativa integral, dentro de + transacção, com verificação do disponível. Sem dotação válida ou saldo, + impedir submissão; não criar autorização de excesso automática. +- Rejeição definitiva liberta a reserva com movimento inverso auditado. + Edição de pedido submetido exige fluxo explícito de revisão, ainda por construir. +- Adjudicação ajusta a reserva ao valor final apenas após nova verificação do + saldo; redução liberta a diferença. As aprovações do valor final mantêm-se. +- Emissão converte reserva em compromisso, sem reduzir novamente o disponível. + Falha na emissão reverte toda a operação financeira e documental. +- Não permitir transferências entre centros de custo, cancelamentos, reabertura + ou passagem de exercício enquanto os respectivos fluxos não forem aprovados. + +## Facturas + +- Factura ligada a fornecedor, ordem emitida e empresa; identidade e número + normalizado únicos por fornecedor/empresa. Impedir repetição da mesma operação. +- Finanças regista e outra pessoa de Finanças valida, distinta do autor. + Sem duas pessoas elegíveis, a factura permanece pendente. +- Conferir ordem, recepções aceites e factura por linha: quantidade, unidade, + preço, IVA e total. Aceitar facturação parcial até ao recebido/aceite ainda + não facturado. Serviços exigem uma aceitação registada antes da validação. +- Proposta inicial: tolerância zero para diferenças de preço, quantidade ou + imposto. Divergências suspendem a validação até existir procedimento aprovado; + não corrigir silenciosamente a ordem ou as recepções. +- Validar a factura converte a parcela correspondente do compromisso em consumo. + A parcela por facturar mantém-se comprometida; a soma não pode ultrapassar + o autorizado. O pagamento futuro liquida a dívida, sem consumir outra vez + o orçamento. +- Notas de crédito, adiantamentos, despesas sem ordem, diferenças cambiais e + facturação entre exercícios ficam bloqueados até definição dos respectivos fluxos. +- Não criar pagamentos automáticos nem enviar documentos a terceiros. + +## Investimento em equipamentos + +- Manter validação técnica do responsável da área e Finanças para qualquer + valor, com separação do solicitante e registo das decisões. +- Criar uma dotação de investimento identificada, distinguindo-a de despesa + corrente; a classificação exige decisão explícita, sem inferência pela descrição. +- O limite próprio de investimento deve ser configurado por empresa, com base + com/sem IVA, vigência, âmbito por aquisição/projecto e significado explícito: + tecto absoluto ou valor que exige aprovação adicional. São decisões em aberto. +- Não propor um valor monetário arbitrário. Enquanto a regra estiver ausente + ou não aprovada por duas pessoas distintas, manter adjudicação e emissão + de equipamento bloqueadas. Proibir fraccionamento para contornar o limite. + +## Validação de aceitação a implementar + +1. Duas submissões simultâneas disputando saldo: apenas as que cabem na dotação + podem reservar; saldo nunca negativo. +2. Repetição de submissão, adjudicação, emissão ou validação de factura não + duplica reserva, compromisso, consumo ou auditoria. +3. Falha de auditoria reverte a transacção inteira. Os lançamentos anteriores + não podem ser editados ou eliminados; correcções usam inversões explícitas. +4. Facturas parciais consomem só a parcela aceite, preservando compromisso + remanescente; excesso, duplicação e diferenças são recusados. +5. Rejeição liberta exactamente a reserva existente, uma única vez. +6. Isolamento entre empresas, centros de custo, exercícios e perfis; + ninguém aprova a própria dotação ou factura. +7. Equipamento sem regra de investimento aprovada permanece bloqueado, + incluindo nos limites e acima deles. Validar fronteiras após definição da regra. +8. Executar concorrência PostgreSQL em base isolada e revisão visual pública, + autenticada e de impressão; testes funcionais não substituem a revisão visual. + +## Decisões necessárias para activar o desenvolvimento financeiro + +Confirmar exercício anual, base com IVA, circuito de dupla aprovação, +reserva na submissão e tolerância zero. Definir dotação inicial por centro de +custo e os parâmetros/autoridade do investimento. Esta proposta não constitui +aprovação destas regras nem certificação para compras reais. diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index a828fd2..9e3d436 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -125,6 +125,20 @@ export function AppSidebar() { + {['admin', 'finance'].includes( + auth.user.purchasing_role ?? '', + ) && ( + + )} {inventoryItems.length > 0 && ( )} diff --git a/resources/js/pages/budgets/index.tsx b/resources/js/pages/budgets/index.tsx new file mode 100644 index 0000000..8a25a98 --- /dev/null +++ b/resources/js/pages/budgets/index.tsx @@ -0,0 +1,178 @@ +import { Head, router, useForm, usePage } from '@inertiajs/react'; +import { + Panel, + PageTitle, + FormErrors, + controlClass, +} from '@/components/procurement'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { money } from '@/types/procurement'; + +type Budget = { + id: number; + cost_center_id: number; + year: number; + amount_minor: number; + reserved_minor: number; + committed_minor: number; + available_minor: number; + created_by: number; + approved_by: number | null; + reason: string; + includes_tax: boolean; +}; +export default function Budgets({ + centers, + budgets, +}: { + centers: { id: number; code: string; name: string }[]; + budgets: Budget[]; +}) { + const { auth } = usePage().props; + const form = useForm({ + cost_center_id: '', + year: new Date().getFullYear(), + amount: '', + reason: '', + includes_tax: '', + }); + return ( + <> + +
+ +

+ Desenvolvimento. O orçamento continua sem validação + operacional. Centros sem dotação continuam no circuito + anterior, sem controlo orçamental. Facturas e pagamentos + ainda não implementados. +

+ +
{ + e.preventDefault(); + form.transform((data) => ({ + ...data, + includes_tax: data.includes_tax === '1', + })); + form.post('/orcamentos'); + }} + > + + + + + +