diff --git a/app/Http/Controllers/AutomaticPurchaseController.php b/app/Http/Controllers/AutomaticPurchaseController.php index abea932..4c0eb20 100644 --- a/app/Http/Controllers/AutomaticPurchaseController.php +++ b/app/Http/Controllers/AutomaticPurchaseController.php @@ -16,35 +16,38 @@ class AutomaticPurchaseController extends Controller { public function index(Request $request, AutomaticPurchases $service) { - $actor=$request->user('web'); - abort_unless($actor instanceof User && $actor->hasFunctions(['admin','supply_read','supply_prepare','supply_review']),403); - $prepare=app(SupplyPlanning::class)->allows($actor,'supply_prepare'); - $ready=$service->ready(); - $planning=app(SupplyPlanning::class); + $actor = $request->user('web'); + abort_unless($actor instanceof User && $actor->hasFunctions(['admin', 'supply_read', 'supply_prepare', 'supply_review']), 403); + $prepare = app(SupplyPlanning::class)->allows($actor, 'supply_prepare'); + $ready = $service->ready(); + $planning = app(SupplyPlanning::class); Inertia::encryptHistory(); - return Inertia::render('stock/replenishment',[ - 'ready'=>$ready,'canPrepare'=>$prepare,'canReview'=>$planning->allows($actor,'supply_review'), - 'rules'=>$ready?$planning->latest('auto_rule')->latest('id')->limit(100)->get():[], - 'needs'=>$ready?$planning->latest('auto_need')->latest('id')->limit(100)->get():[], - 'history'=>$ready?SupplyRecord::query()->whereIn('kind',['auto_rule','auto_need'])->latest('id')->limit(100)->get():[], - 'policies'=>Schema::hasTable('supply_records') && $prepare?$planning->latest('policy')->where('state','approved')->where('data->purpose','internal')->get():[], - 'requesters'=>$prepare?User::query()->where('tenant_id',$actor->tenant_id)->get(['id','name','department_id','purchasing_role','tenant_id'])->filter(fn($u)=>in_array('requester',app(OrganizationAccess::class)->permissions($u,'procurement.internal'),true))->map(fn($u)=>$u->only(['id','name','department_id']))->values():[], - 'centers'=>$prepare?CostCenter::query()->orderBy('code')->get(['id','department_id','code','name']):[], - ])->toResponse($request)->withHeaders(['Cache-Control'=>'private, no-store, max-age=0']); + + return Inertia::render('stock/replenishment', [ + 'ready' => $ready, 'canPrepare' => $prepare, 'canReview' => $planning->allows($actor, 'supply_review'), + 'rules' => $ready ? $planning->latest('auto_rule')->latest('id')->limit(100)->get() : [], + 'needs' => $ready ? $planning->latest('auto_need')->latest('id')->limit(100)->get() : [], + 'history' => $ready ? SupplyRecord::query()->whereIn('kind', ['auto_rule', 'auto_need'])->latest('id')->limit(100)->get() : [], + 'policies' => Schema::hasTable('supply_records') && $prepare ? $planning->latest('policy')->where('state', 'approved')->where('data->purpose', 'internal')->get() : [], + 'requesters' => $prepare ? User::query()->where('tenant_id', $actor->tenant_id)->get(['id', 'name', 'department_id', 'purchasing_role', 'tenant_id'])->filter(fn ($u) => $service->eligibleRequester($u))->map(fn ($u) => $u->only(['id', 'name', 'department_id']))->values() : [], + 'centers' => $prepare ? CostCenter::query()->orderBy('code')->get(['id', 'department_id', 'code', 'name']) : [], + ])->toResponse($request)->withHeaders(['Cache-Control' => 'private, no-store, max-age=0']); } public function save(Request $request, AutomaticPurchases $service) { - $service->save($request->user('web'),$request->all()); + $service->save($request->user('web'), $request->all()); + return redirect('/stock/reposicao'); } public function evaluate(Request $request, int $id, AutomaticPurchases $service) { - $actor=$request->user('web'); - abort_unless($actor instanceof User && ($actor->hasFunctions(['supply_prepare','supply_review'])) && !app(OrganizationAccess::class)->maintenanceOnly($actor),403); - $need=SupplyRecord::query()->where('kind','auto_need')->findOrFail($id); - $service->evaluate($actor,$need->stock_item_id,$need->warehouse_id); + $actor = $request->user('web'); + abort_unless($actor instanceof User && ($actor->hasFunctions(['supply_prepare', 'supply_review'])) && ! app(OrganizationAccess::class)->maintenanceOnly($actor), 403); + $need = SupplyRecord::query()->where('kind', 'auto_need')->findOrFail($id); + $service->evaluate($actor, $need->stock_item_id, $need->warehouse_id); + return redirect('/stock/reposicao'); } } diff --git a/app/Services/AutomaticPurchases.php b/app/Services/AutomaticPurchases.php index 360111f..244bf08 100644 --- a/app/Services/AutomaticPurchases.php +++ b/app/Services/AutomaticPurchases.php @@ -17,6 +17,10 @@ use Illuminate\Validation\ValidationException; /** Event-driven draft preparation. Never submits, decides, orders, reserves budget or moves stock. */ class AutomaticPurchases { + public function eligibleRequester(User $user): bool + { + return in_array('requester', app(OrganizationAccess::class)->permissions($user, app(WorkContext::class)->enforced($user) ? 'procurement.internal' : null), true) && !app(OrganizationAccess::class)->maintenanceOnly($user); + } public function ready(): bool { return Schema::hasTable('supply_records') && app(WarehouseReservations::class)->ready(); @@ -29,7 +33,7 @@ class AutomaticPurchases $this->check($current !== null && $current->id === $rule->id && $current->tenant_id === $executor->tenant_id && $current->state === 'approved', 'A parametrização automática deixou de estar activa.'); $this->check(! app(OrganizationAccess::class)->maintenanceOnly($executor), 'A manutenção não pode executar compras automáticas.'); $user = User::query()->where('tenant_id', $executor->tenant_id)->lockForUpdate()->find($rule->data['requester_id']); - $this->check($user !== null && in_array('requester', app(OrganizationAccess::class)->permissions($user, 'procurement.internal'), true), 'Requisitante sem competência vigente no contexto de compras internas.'); + $this->check($user !== null && $this->eligibleRequester($user), 'Requisitante sem competência vigente no contexto de compras internas.'); $this->check(CostCenter::query()->whereKey($rule->data['cost_center_id'])->where('department_id', $user->department_id)->exists(), 'Centro de custo incompatível com o departamento actual do requisitante.'); $this->check($user->only(['id', 'name', 'department_id']) === $rule->snapshot['requester'], 'O requisitante mudou; reveja a parametrização.'); $center = CostCenter::query()->findOrFail($rule->data['cost_center_id']); @@ -69,7 +73,7 @@ class AutomaticPurchases $this->check($basis['policy']['purpose'] === 'internal', 'Esta parametrização destina-se às compras internas do SGC.'); $this->check($basis['catalogue']['purchase_type'] === 'consumable', 'Seleccione um consumível publicado; equipamento/peças exigem o seu destino técnico.'); $user = User::query()->where('tenant_id', $actor->tenant_id)->findOrFail($values['requester_id']); - $this->check(in_array('requester', app(OrganizationAccess::class)->permissions($user, 'procurement.internal'), true), 'Seleccione um requisitante com competência vigente em compras internas.'); + $this->check($this->eligibleRequester($user), 'Seleccione um requisitante com competência vigente em compras internas.'); $center = CostCenter::query()->where('department_id', $user->department_id)->findOrFail($values['cost_center_id']); $snapshot = ['requester' => $user->only(['id', 'name', 'department_id']), 'cost_center' => $center->only(['id', 'code', 'name', 'department_id']), 'prepared_by' => $actor->id, 'item' => $basis['item'], 'warehouse' => $basis['warehouse'], 'policy_version' => $basis['policy_version'], 'catalogue' => $basis['catalogue']]; @@ -176,7 +180,7 @@ continue; $this->check($line->catalogue_entry_id === $basis['catalogue']['id'] && ($line->catalogue_snapshot['version'] ?? null) === $basis['catalogue']['version'], 'O rascunho automático anterior mudou de artigo; reconcilie a necessidade.'); $received = 0; foreach (DB::table('goods_receipt_items as gi')->join('goods_receipts as g', 'g.id', '=', 'gi.goods_receipt_id')->join('purchase_order_items as oi', 'oi.id', '=', 'gi.purchase_order_item_id')->where('gi.tenant_id', $actor->tenant_id)->where('oi.requisition_item_id', $line->id)->where('g.warehouse_id', $warehouse)->where('gi.stock_item_id', $item)->get(['gi.id', 'gi.accepted_milli']) as $receipt) { - $voided = collect(app(InventoryReversals::class)->corrections('receipt'))->contains(fn ($correction) => ($correction->snapshot['source']['receipt']['id'] ?? null) === $receipt->id); + $voided = collect(app(InventoryReversals::class)->corrections('receipt'))->contains(fn ($correction) => ($correction->snapshot['source']['receipt']['item_id'] ?? null) === $receipt->id); if (! $voided) { $received += (int) $receipt->accepted_milli; } diff --git a/app/Services/InventoryLots.php b/app/Services/InventoryLots.php index caad559..bd36153 100644 --- a/app/Services/InventoryLots.php +++ b/app/Services/InventoryLots.php @@ -439,7 +439,7 @@ class InventoryLots 'data' => $data, 'snapshot' => [...$snapshot, 'actor' => $actor->only(['id', 'name'])], 'reason' => $reason, 'created_by' => $actor->id, 'operation_key' => $operation ?? (string) Str::uuid(), 'input_hash' => $hash ?? hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)), 'created_at' => now()]); app(RequisitionWorkflow::class)->audit($actor, 'inventory.'.$kind.'.'.$state, null, $old?->toArray(), $record->toArray()); - app(AutomaticPurchases::class)->signal($actor,$data['stock_item_id'],$data['warehouse_id']); + app(AutomaticPurchases::class)->signal($actor, $data['stock_item_id'], $data['warehouse_id']); return $record; } diff --git a/app/Services/ReceiptWorkflow.php b/app/Services/ReceiptWorkflow.php index 536b847..53cf7fe 100644 --- a/app/Services/ReceiptWorkflow.php +++ b/app/Services/ReceiptWorkflow.php @@ -224,7 +224,7 @@ class ReceiptWorkflow ]); $sourceInput = collect($inputs)->firstWhere('purchase_order_item_id', $line['purchase_order_item_id']); app(InventoryLots::class)->receive($user, $movement, $sourceInput['lots'] ?? [], $data['received_on']); - app(AutomaticPurchases::class)->signal($user,$line['stock_item_id'],$warehouse->id); + app(AutomaticPurchases::class)->signal($user, $line['stock_item_id'], $warehouse->id); } } $progress = $this->progress($order); diff --git a/database/migrations/2026_11_02_000049_create_warehouse_reservations.php b/database/migrations/2026_11_02_000049_create_warehouse_reservations.php index e8c334e..35c52ab 100644 --- a/database/migrations/2026_11_02_000049_create_warehouse_reservations.php +++ b/database/migrations/2026_11_02_000049_create_warehouse_reservations.php @@ -2,24 +2,39 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { - Schema::create('warehouse_reservations',function(Blueprint $table) { - $table->id(); $table->foreignId('tenant_id')->constrained()->restrictOnDelete(); +return new class extends Migration +{ + public function up(): void + { + Schema::create('warehouse_reservations', function (Blueprint $table) { + $table->id(); + $table->foreignId('tenant_id')->constrained()->restrictOnDelete(); $table->foreignId('warehouse_request_id')->constrained()->restrictOnDelete(); - $table->unsignedInteger('line_index'); $table->foreignId('stock_item_id')->constrained()->restrictOnDelete(); + $table->unsignedInteger('line_index'); + $table->foreignId('stock_item_id')->constrained()->restrictOnDelete(); $table->foreignId('warehouse_id')->constrained()->restrictOnDelete(); $table->foreignId('authorization_event_id')->constrained('warehouse_request_events')->restrictOnDelete(); $table->foreignId('actor_id')->constrained('users')->restrictOnDelete(); - $table->uuid('command_key'); $table->string('kind'); $table->bigInteger('quantity_milli'); $table->timestampTz('created_at'); - $table->unique(['warehouse_request_id','line_index','command_key','kind'],'warehouse_reservation_command_unique'); - $table->index(['tenant_id','stock_item_id','warehouse_id']); + $table->uuid('command_key'); + $table->string('kind'); + $table->bigInteger('quantity_milli'); + $table->timestampTz('created_at'); + $table->unique(['warehouse_request_id', 'line_index', 'command_key', 'kind'], 'warehouse_reservation_command_unique'); + $table->index(['tenant_id', 'stock_item_id', 'warehouse_id']); }); - if(DB::getDriverName()==='sqlite')DB::unprepared("CREATE TRIGGER warehouse_reservations_update BEFORE UPDATE ON warehouse_reservations BEGIN SELECT RAISE(ABORT, 'Preservar reservas'); END; CREATE TRIGGER warehouse_reservations_delete BEFORE DELETE ON warehouse_reservations BEGIN SELECT RAISE(ABORT, 'Preservar reservas'); END;"); - if(DB::getDriverName()==='pgsql')DB::unprepared("CREATE FUNCTION preserve_warehouse_reservations() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar reservas'; END $$; CREATE TRIGGER warehouse_reservations_immutable BEFORE UPDATE OR DELETE ON warehouse_reservations FOR EACH ROW EXECUTE FUNCTION preserve_warehouse_reservations();"); + if (DB::getDriverName() === 'sqlite') { + DB::unprepared("CREATE TRIGGER warehouse_reservations_update BEFORE UPDATE ON warehouse_reservations BEGIN SELECT RAISE(ABORT, 'Preservar reservas'); END; CREATE TRIGGER warehouse_reservations_delete BEFORE DELETE ON warehouse_reservations BEGIN SELECT RAISE(ABORT, 'Preservar reservas'); END;"); + } + if (DB::getDriverName() === 'pgsql') { + DB::unprepared("CREATE FUNCTION preserve_warehouse_reservations() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar reservas'; END $$; CREATE TRIGGER warehouse_reservations_immutable BEFORE UPDATE OR DELETE ON warehouse_reservations FOR EACH ROW EXECUTE FUNCTION preserve_warehouse_reservations();"); + } + } + + public function down(): void + { + Schema::dropIfExists('warehouse_reservations'); } - public function down(): void { Schema::dropIfExists('warehouse_reservations'); } }; diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index e0ff6ab..51ed5bf 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -1,5 +1,13 @@ # Desenvolvimento do SGC +## Composição de arranque e prova HTTP isolada SGF — 02/11/2026 + +Implementados `BootOverlay`, `Composition`, `RuntimeContext` e entry point local do bundle. A sobreposição do provider nativo carrega classes e rotas do módulo; descoberta de Compras reconhece `purchasing_requisitions.view`. Middleware específico é acrescentado depois da sessão/autenticação/contexto nativos e confere opção, destino/schema/âmbito, módulos adquiridos e selecção/geração do contexto. Autoridade inicial mantém âmbito administrativo, sem exigir direito funcional ainda não atribuído. A CLI aceita/verifica o entry point `bootstrap.php` do bundle. Candidatas anteriores não foram reescritas. + +Prova nova `scripts/sgf-composition-proof.php`: **27 controlos aprovados**, usando cópia descartável do SGF com dependências próprias, sem .env/dados/documentos/sessões da instalação existente, PostgreSQL `sgc_composition_<12hex>`/role restrita e APP_ENV production. Provider, Composer, kernel, rotas, autenticação/CSRF e administração nativos; router de prova não regista rotas ou classes do módulo. Conferidos arranque efectivo da cópia, CLI plan, configuração inicial, delegação, contexto, percurso de requisição/reserva/aprovação independente/consulta e bloqueios por revogação, opção diferente, contexto antigo, módulo não adquirido/desactivado. Sem browser e sem suites anteriores repetidas. [Prova](proofs/sgf-composition-delivery.json), candidata `OPERATIONAL-CANDIDATE-4b25f81932efd69d`. Base/role removidas; fonte/código/schema/concessões e ponteiro 0.1.1 do SGF de destino preservados. + +Próxima acção concreta: rotina de aplicação atómica das sobreposições e código, backup/reversão de ficheiros do host e gestão de caches/reinício. O ensaio HTTP usa caches não pré-compilados; não declarar a instalação operacional completa nem activar no destino sem decisão separada. A reorganização modular do SGF continua entrega distinta e sem resultado próprio apresentado. Matriz e limites actualizados em [transferencia-sgf.md](transferencia-sgf.md). + ## Composição orçamental e preparação de schema SGF — 01/11/2026 Continuação sem instalar/activar no destino: criada `integrations/sgf/operational`, com schema orçamental do contrato SGF existente, composição alternativa do mesmo BudgetAdapter/motor, verificação de destino/schema/âmbito e registo imutável `sgc_installations`. O preparador `scripts/prepare-sgf-operational.php` produz artefacto de revisão com sobreposições e suporte local, sem escrever na pasta SGF. `scripts/sgf-internal-install.php` oferece `plan` read-only e `migrate` de preparação do schema: confere hashes do host/manifesto/destino/backup, índice pg_restore, fundação e contrato de orçamento; DDL transaccional com lock e revisão/hashes/operador/role/backup. Não concede direitos, activa rotas ou troca o ponteiro. Sem down automático; schema parcial/incompatível é recusado. Fundação orçamental nova é criada vazia, sem fixtures. diff --git a/docs/orientacoes-continuidade.md b/docs/orientacoes-continuidade.md index 8d791d8..c29933f 100644 --- a/docs/orientacoes-continuidade.md +++ b/docs/orientacoes-continuidade.md @@ -1,5 +1,7 @@ # Continuidade do desenvolvimento do SGC +**Arranque e HTTP SGF — 02/11/2026:** candidata `OPERATIONAL-CANDIDATE-4b25f81932efd69d` integra bundle/rotas pelo provider nativo e associa `purchasing_requisitions.view` à descoberta de Compras. Novo middleware mantém contexto/opção/âmbito/schema, módulo adquirido/seleccionado e geração de contexto; autoridade inicial administrativa permanece sem outorga automática. 27 controlos novos HTTP aprovados em instalação com código e dependências copiados, APP_ENV production, base/role próprias removidas; sem repetir suites aceites ou usar browser. [Prova](proofs/sgf-composition-delivery.json). Nenhuma aplicação no destino, ponteiro 0.1.1 e candidatas antigas preservados. Pendência seguinte: aplicação atómica/reversão das sobreposições/código e caches/reinício, antes de propor activação. Não declarar instalação comercial concluída nem confundir esta integração com reorganização modular posterior, que mantém entrega própria pendente. + **Composição operacional/preparação de schema — 01/11/2026:** candidata `OPERATIONAL-CANDIDATE-29687a1c9bc0e0b5`, sem instalação no SGF. `scripts/prepare-sgf-operational.php` prepara artefacto; `scripts/sgf-internal-install.php` distingue plan read-only e migrate transaccional de schema, com destino/manifesto/backup conferidos, contrato orçamental e journal imutável. Mesmo motor e permissões, novo ramo operacional por destino/schema/âmbito explicitamente habilitados, sem obrigar nome de base QA ou ambiente de ensaio. 20 controlos novos + restauro em production/base não-QA aprovados em [proofs/sgf-operational-delivery.json](proofs/sgf-operational-delivery.json), sem repetir suites aceites. Não confundir preparação de schema com activação: não alterou código/provider/rotas/concessões/dados no SGF instalado; ponteiro 0.1.1 e candidatas anteriores preservados. Próximo passo é composição de arranque/rotas e prova HTTP numa cópia isolada. Não executar migrate no destino, activar 0.1.2/outra versão ou publicar sem a decisão separada. Reorganização modular mantém-se distinta e sem resultado próprio apresentado. **Preparação de acessos SGF — 01/11/2026:** criada sobreposição candidata de catálogo/gestão de permissões e atribuição inicial nominativa/auditada, com rotas/ecrã separados, sem alterações no host instalado. `scripts/prepare-sgf-access.php` produz hashes antes/depois e recusa divergências; não é instalador. 24 controlos novos PostgreSQL aprovados em [proofs/sgf-access-delivery.json](proofs/sgf-access-delivery.json), sem repetir suites aceites. Preservados os direitos anteriores na criação de unidades; nova autoridade inicial exige acção explícita de administrador autorizado e âmbito habilitado, nunca outorga automática. SGF mantém 0.1.1, candidata IR imutável e rotas não activadas. Próximo trabalho: composição orçamental operacional, instalador de schema/arranque, integração HTTP da preparação e pacote SGF específico; continuar sem instalar/publicar por arrasto. Reorganização modular não se confunde com este incremento e continua sem resultado próprio apresentado. diff --git a/docs/transferencia-sgf.md b/docs/transferencia-sgf.md index 898440e..0d22b41 100644 --- a/docs/transferencia-sgf.md +++ b/docs/transferencia-sgf.md @@ -1,5 +1,19 @@ # Transferência de Compras e actualizações do SGF +## Arranque e integração HTTP em cópia isolada — 02/11/2026 + +Preparada a candidata `OPERATIONAL-CANDIDATE-4b25f81932efd69d`, preservando os artefactos anteriores. A composição já inclui sobreposição do provider nativo para carregar o bundle local e registar as rotas, sem registo manual de rotas pelo harness HTTP. O bundle fica em `packages/internal-requisitions`, com classes e ficheiros próprios; a sobreposição do SGF reconhece `purchasing_requisitions.view` como autorização de descoberta do módulo Compras, sem conceder direitos nem substituir o catálogo/controlo funcional. + +`RuntimeContext` é acrescentado às rotas do bundle **depois** dos middlewares nativos de sessão/autenticação/empresa. Confere opção operacional habilitada, destino/schema/âmbito, módulos adquiridos e habilitados, selecção explícita de Compras e geração de contexto nos APIs. A atribuição inicial permanece no âmbito de Administração: não exige um direito funcional de requisição que ainda não foi outorgado, mas exige módulo adquirido, âmbito habilitado e os controlos nominativos/administrativos já implementados. Não cria autoridade a partir da selecção do módulo. A composição desactivada recusa a execução das rotas. + +O ensaio `scripts/sgf-composition-proof.php` usa uma instalação descartável com código, dependências e packages **copiados**, provider/configuração/rotas do próprio SGF e bootstrap nativo, sem `.env`, contas, documentos ou dados empresariais copiados. Usa base `sgc_composition_<12hex>` e role próprias, ambiente `production`, sessões privadas e acesso HTTP loopback com login/CSRF reais. A preparação de dados sintéticos e a activação ocorrem exclusivamente nessa cópia/base; não são passos aplicados ao SGF de destino. O router de teste só entrega o pedido ao kernel: não regista rotas, não substitui autenticação e não carrega classes da instalação original. + +Resultados efectivos, caminho do artefacto e verificações em [proofs/sgf-composition-delivery.json](proofs/sgf-composition-delivery.json). A prova verifica o incremento de composição/HTTP, incluindo autoridade inicial, administração nativa, contexto, submissão/reserva/aprovação/consulta e revogação; não repete as suites anteriores nem declara revisão visual por navegador. Os resultados históricos seguintes conservam o seu âmbito. + +**Resultado final: 27 controlos novos aprovados.** Confirmados provider e adaptador efectivamente carregados da cópia isolada; `plan` aceite pelo verificador de integridade do novo bundle; acesso anónimo e CSRF bloqueados; autoridade inicial sem direitos funcionais prévios; atribuição pela administração nativa; selecção explícita de Compras; recusa de token antigo, modalidade diferente, revogação, módulo não adquirido e composição desactivada; rascunho → submissão com uma reserva existente → aprovação por outra identidade → consulta com três versões/eventos. Base e role descartáveis removidas. Nenhum registo de dados ou direito do SGF de destino alterado. + +**Limites de entrega:** esta composição ainda não foi aplicada ao destino. Falta a rotina operacional de aplicação atómica das sobreposições/código, preservação/reversão dos ficheiros do host e gestão dos caches/reinício de serviços. O ensaio usa configuração sem cache; não declara validada a activação com cache de rotas/configuração pré-existente. As alterações de configuração da prova foram seguidas de reinício do serviço PHP. `plan`/`migrate` continuam a preparar/conferir schema, sem aplicar estas sobreposições ou activar rotas. Não substituir esse procedimento por extracção cega do bundle. SGF de destino mantém **0.1.1**, sem schema, permissões, provider ou rotas alterados. Activação continua dependente da decisão separada. A reorganização modular posterior é uma entrega distinta, não concluída por este incremento. + ## Objectivo e estado em 01/11/2026 O foco autorizado pelo proprietário é transferir as capacidades de Compras para o SGF e levar as melhorias seguintes por versões verificáveis. Não é sincronizar bases de desenvolvimento nem manter duas implementações. O SGC autónomo conserva todas as suas funcionalidades; no SGF, Pessoas e contratações destina-se apenas ao uso interno. Cada cliente recebe as capacidades adquiridas, com configuração e permissões próprias. @@ -30,7 +44,7 @@ Estados: **Transferido** identifica a capacidade exacta instalada e executada no | Pagamentos e afectações a fornecedores | **Parcialmente transferido** | Núcleo comum e entregas anteriores preservados | Adaptadores/autorizações delimitados nas respectivas entregas; não representam migração de contas ou histórico de cliente | Percurso anterior existe no âmbito documentado; integração completa com novas origens de Compras pendente | `SUPPLIER_SETTLEMENT_DELIVERY.md` e limites de histórico/origens; não afirmar financeiro integral | | Identidade, empresa/unidade, perfis e módulos adquiridos | **Parcialmente transferido** | Fundações próprias existentes nos dois hosts; não se fundiram tabelas-base | SGF mantém tenant/empresa fiscal/unidade/concessões; correspondência explícita com o contexto Purchasing por concluir | Sessão/contexto SGF existentes; selector SGC não foi incorporado automaticamente | Sem associação por igualdade de nome, email ou ID; parametrização comercial e permissões continuam distintas | | Reorganização modular do SGF pedida posteriormente | **Pendente — sem resultado apresentado** | Entrega distinta da integração candidata de requisições | Não avaliada nem concluída por esta conferência | Não inferir reorganização entregue a partir dos ecrãs candidatos | Exige apresentação própria do âmbito, alterações e resultado; não executada por arrasto | -| Requisição interna: rascunho, submissão, aprovação independente e consulta | **Parcialmente transferido — candidata; não activa** | Motor único `InternalRequisitions`, adaptadores dos dois hosts; recibos, versões e histórico; reserva existente na mesma transacção | Contexto interno, empresa/unidade, autorização renovada, identidade independente e fontes explicitamente associadas; sem novas concessões no destino | Rascunho/edição, lista, submissão, decisão, histórico e parametrização na candidata; sem novo menu/rota activa no SGF | HTTP, concorrência, revogação, falha/recuperação e schema em bases isoladas; equipamentos/peças e instalação comercial pendentes; destino 0.1.1 | +| Requisição interna: rascunho, submissão, aprovação independente e consulta | **Parcialmente transferido — candidata; não activa** | Motor único `InternalRequisitions`, adaptadores dos dois hosts; recibos, versões e histórico; reserva existente na mesma transacção | Contexto interno, empresa/unidade, autorização renovada e identidade independente; composição no provider e associação do novo direito de consulta ao módulo preparadas | Ecrãs e percurso integrados na cópia isolada pelo arranque nativo; nenhuma rota/menu novo activo no destino | Provas anteriores preservadas; novo ensaio de composição HTTP em `sgf-composition-delivery.json`; aplicação operacional do código/caches e equipamentos/peças pendentes; destino 0.1.1 | | Planeamento de compras | **Pendente** | Itens, marcos e associação a requisições permanecem em Compras | Adaptar unidades, centros, intervenientes e vínculos de origem no SGF | Planeamento e acompanhamento de marcos SGF do roteiro de Compras pendentes | Não gerar requisições duplicadas nem inferir reserva/decisão a partir de um plano | | Fornecedores, documentos e dados bancários | **Pendente** | Cadastros/versionamento de Compras ainda não extraídos | Definir correspondências autorizadas de fontes e permissões, sem fundir entidades por nome | Cadastro, validações e consulta SGF de todo o percurso pendentes | Preservar fotografias utilizadas e validação bancária independente | | Cotações, descontos e comparação económica | **Pendente** | Descontos e pontuação foram preparados na origem/candidata `0.1.2`; SGF activo não contém estas classes | Elegibilidade, conflitos, avaliações autorizadas e decisão continuam no host; adaptador do processo SGF não implementado | Recepção de cotações, grelha, empates, recomendação e decisão SGF pendentes | Testes locais não equivalem a transferência. Candidata não instalada; nenhuma adjudicação automática | diff --git a/resources/js/lib/procurement-navigation.ts b/resources/js/lib/procurement-navigation.ts index 2183e67..f140b8c 100644 --- a/resources/js/lib/procurement-navigation.ts +++ b/resources/js/lib/procurement-navigation.ts @@ -298,7 +298,12 @@ export function procurementNavigation( id: 'warehouse_replenishment', label: 'Reposição automática', path: '/stock/reposicao', - roles: ['admin', 'supply_read', 'supply_prepare', 'supply_review'], + roles: [ + 'admin', + 'supply_read', + 'supply_prepare', + 'supply_review', + ], }, { id: 'warehouse_requests', diff --git a/resources/js/pages/stock/availability.tsx b/resources/js/pages/stock/availability.tsx index 7b37bac..9cbf2d1 100644 --- a/resources/js/pages/stock/availability.tsx +++ b/resources/js/pages/stock/availability.tsx @@ -169,8 +169,17 @@ export default function Availability(props: Props) { ) : 'Sem acesso ao planeamento'} - {quantity(row.physical_commitment_milli)} / {quantity(row.free_milli)} - {quantity(row.minimum_milli)} + + {quantity( + row.physical_commitment_milli, + )}{' '} + / {quantity(row.free_milli)} + + + {quantity( + row.minimum_milli, + )} + {!row.reconciled ? 'Reconciliar repartição dos lotes' @@ -192,7 +201,13 @@ export default function Availability(props: Props) {

Sem artigos nos filtros seleccionados.

)}

- A compra automática usa a parametrização do artigo e armazém. Esta consulta não cria documentos. Consultar reposição e necessidades pendentes. + A compra automática usa a parametrização do + artigo e armazém. Esta consulta não cria + documentos.{' '} + + Consultar reposição e necessidades pendentes + + .

{props.rows.prev_page_url && ( diff --git a/resources/js/pages/stock/replenishment.tsx b/resources/js/pages/stock/replenishment.tsx index 97fc1a4..31e7bce 100644 --- a/resources/js/pages/stock/replenishment.tsx +++ b/resources/js/pages/stock/replenishment.tsx @@ -1,32 +1,435 @@ import { Head, Link, useForm, router } from '@inertiajs/react'; -import { PageTitle, Panel, FormErrors, controlClass } from '@/components/procurement'; +import { + PageTitle, + Panel, + FormErrors, + controlClass, +} from '@/components/procurement'; import { Button } from '@/components/ui/button'; -type RecordRow = { id:number; record_key:string; version:number; state:string; reason:string; created_at:string; data:{ policy_key?:string; requester_id?:number; cost_center_id?:number; unit_price?:string; tax_rate?:string; needed_days?:number; valid_until?:string; message?:string; purchases?:{id:number;number:string;quantity_milli:number}[] }; snapshot:{item:{id:number;code?:string;name?:string};warehouse:{id:number;name?:string};requester?:{name:string};cost_center?:{code:string;name:string};origins?:{warehouse_request_id:number;destination:string;equipment:{code:string;description:string}|null;remaining_milli:number}[]} }; -type Props={ready:boolean;canPrepare:boolean;canReview:boolean;rules:RecordRow[];needs:RecordRow[];history:RecordRow[];policies:RecordRow[];requesters:{id:number;name:string;department_id:number}[];centers:{id:number;code:string;name:string;department_id:number}[]}; -const states:Record={draft:'Por activar',approved:'Activa',suspended:'Suspensa',pending:'Necessidade pendente',created:'Rascunho criado',covered:'Reposição em curso',satisfied:'Disponibilidade suficiente'}; -function RuleForm({props,rule}:{props:Props;rule?:RecordRow}) { - const form=useForm({operation_key:crypto.randomUUID(),record_key:rule?.record_key??null,version:rule?.version??0,action:'save',reason:'',data:{policy_key:rule?.data.policy_key??'',requester_id:rule?.data.requester_id?.toString()??'',cost_center_id:rule?.data.cost_center_id?.toString()??'',unit_price:rule?.data.unit_price??'',tax_rate:rule?.data.tax_rate??'',needed_days:rule?.data.needed_days?.toString()??'',valid_until:rule?.data.valid_until??''}}); - const requester=props.requesters.find(u=>u.id.toString()===form.data.data.requester_id); - const set=(key:keyof typeof form.data.data,value:string)=>form.setData('data',{...form.data.data,[key]:value,...(key==='requester_id'?{cost_center_id:''}:{})}); - return
{e.preventDefault();form.post('/stock/reposicao',{preserveState:'errors'});}}> - - - - - - - -