commit de compras

This commit is contained in:
2026-11-04 09:37:20 +02:00
parent fcf31e8d48
commit 0b05a1192c
741 changed files with 114127 additions and 116 deletions
+3 -1
View File
@@ -17,7 +17,9 @@ export default function AccountingMenuWorkspace({section,context,can,navigate})
<section className="panel"><h2>Trabalho contabilístico no contexto seleccionado</h2><p>Ainda não há fonte agregada de documentos por validar, contas por reconciliar ou estado do fecho. A indisponibilidade não é apresentada como zero pendências.</p><div className="accounting-routes">{accountingSections.filter(s=>s.id!==section).map(s=><button key={s.id} onClick={()=>navigate(s.id)}>{s.label}<span>Consultar percursos e disponibilidade →</span></button>)}</div></section>
<section className="panel"><h2>Percursos já disponíveis</h2><div className="actions">{can('catalogue_accounting.view')&&<button onClick={()=>navigate('catalogue_accounting')}>Classificação contabilística</button>}{can('service_fiscal.view')&&<button onClick={()=>navigate('service_fiscal')}>Perfis fiscais do serviço</button>}{unit!==null&&can('service_receivables.view',unit)&&<button onClick={()=>navigate('service_receivables')}>Recebimentos e reconciliação de teste</button>}</div><p>Estas ligações conservam os direitos e os limites dos respectivos módulos; não representam uma contabilidade geral já integrada.</p></section>
</>}
{functions.map(f=><section className="panel" key={f.id}><h2>{f.label}</h2><p>{f.description}</p><p><strong>Disponibilidade:</strong> {f.status}</p>{action(f)}</section>)}
{context.accounting?.stock_valuation_read!==true&&unit!==null&&can('general_ledger.view',unit)&&<section className="panel"><h2>Primeiro lançamento contabilístico sintético</h2><p>Obrigação reconhecida → proposta → revisão independente → aplicação única. Duas contas e um período artificiais; sem utilização real, fecho ou contabilidade geral completa.</p><a href="/general-ledger">Abrir lançamento e extracto de ensaio</a></section>}
{context.accounting?.stock_valuation_read===true&&unit!==null&&can('stock.view',unit)&&can('general_ledger.view',unit)&&<section className="panel"><h2>Stock valorizado — consulta do livro de origem</h2><p>Quantidades físicas, valores contabilizados e pendências no mesmo livro QA. Esta consulta não executa movimentos ou lançamentos.</p><a href="/general-ledger/stock-valuation">Conferir stock e valorização</a></section>}
{functions.map(f=><section className="panel" key={f.id}><h2>{f.label}</h2><p>{f.description}</p><p><strong>Disponibilidade:</strong> {context.accounting?.stock_valuation_read===true&&['entries','balances'].includes(f.id)?'Consulta do mesmo livro de stock e valorização QA; confirmações e conferência completa na composição de origem.':unit!==null&&can('general_ledger.view',unit)&&['entries','balances'].includes(f.id)?'Primeiro percurso sintético de obrigação de fornecedor: lançamento equilibrado e extracto em duas contas artificiais. Não constitui contabilidade operacional, balancete legal, fecho ou demonstrações financeiras.':f.status}</p>{action(f)}</section>)}
{section==='accounting_daily'&&<section className="panel"><h2>Origem documental</h2><p>Documento recebido → preparação → classificação → conferência → validação → reconciliação → relatório → fecho. As etapas que ainda não têm implementação não são executadas por este menu.</p><div className="actions">{unit!==null&&can('billing_drafts.view',unit)&&<button onClick={()=>navigate('billing_drafts')}>Abrir rascunhos de facturação existentes</button>}{unit!==null&&can('supplier_payables.view',unit)&&<button onClick={()=>navigate('supplier_payables')}>Abrir obrigações a fornecedores de teste</button>}</div><p>Uma obrigação com principal reconhecido e vencimento desconhecido continua sem classificação automática de atraso. Preparar ou aprovar um pagamento não comprova execução bancária.</p></section>}
{section==='accounting_analysis'&&<section className="panel"><h2>Catálogo de relatórios previstos</h2><p>Catálogo para orientar a implementação. Não contém relatórios gerados, saldos, dados salariais ou bancários.</p><div className="form-grid"><label>Pesquisar relatório<input type="search" value={search} onChange={e=>setSearch(e.target.value)}/></label><label>Área<select value={family} onChange={e=>setFamily(e.target.value)}><option value="">Todas as áreas</option>{[...new Set(accountingReports.map(r=>r.family))].map(f=><option key={f}>{f}</option>)}</select></label></div>
<div className="accounting-report-list">{filterAccountingReports(search,family).map(r=><button key={r.id} aria-pressed={r.id===selected} onClick={()=>setSelected(r.id)}>{r.label}<small>{r.family} · Por implementar</small></button>)}</div>{!filterAccountingReports(search,family).length&&<p>Nenhuma definição de relatório corresponde à pesquisa. Isto não é uma consulta de movimentos.</p>}
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
export default function GeographyLink({enabled,kind,id,children}) {
if(enabled!==true||!['company','unit','client'].includes(kind)||!Number.isSafeInteger(Number(id))||Number(id)<1)return null;
return <a href={`/geography/${kind}/${Number(id)}`}>{children||'Localização para segmentação'}</a>;
}
+10 -2
View File
@@ -4,9 +4,17 @@ export default function PurchasingHome({context,can,navigate}) {
const unit=context.unit?.id??null;
const budget=unit!==null&&can('purchasing_budget.view',unit);
const payables=unit!==null&&can('supplier_payables.view',unit);
const requisitions=unit!==null&&context.purchasing?.internal_requisitions===true&&can('purchasing_requisitions.view',unit);
const sourceRequests=unit!==null&&context.purchasing?.source_requests===true&&can('purchasing_requests.view',unit);
const suppliers=unit!==null&&context.purchasing?.suppliers===true&&can('purchasing_suppliers.view',unit);
const sourcing=unit!==null&&context.purchasing?.sourcing===true&&can('purchasing_sourcing.view',unit);
const orders=unit!==null&&context.purchasing?.orders===true&&can('purchasing_orders.view',unit);
const obligations=unit!==null&&context.purchasing?.supplier_obligations===true&&can('purchasing_obligations.view',unit);
const invoices=unit!==null&&context.purchasing?.supplier_invoices===true&&can('purchasing_invoices.view',unit);
const receipts=unit!==null&&context.purchasing?.receipts===true&&can('purchasing_receipts.view',unit);
return <><div className="page-title"><p className="eyebrow">COMPRAS</p><h1>Área de Compras</h1><p>{context.company.name}{context.unit?` / ${context.unit.name}`:''}</p></div>
<section className="panel"><h2>Aquisição, recepção e obrigação</h2><p>Compras acompanha as aquisições e os seus documentos. A recepção de mercadoria, o consumo, a obrigação ao fornecedor e o pagamento são factos distintos; cada efeito mantém um único responsável.</p><p>Compras e Stock têm entradas separadas. Separar a navegação não remove os percursos existentes nem concede acesso a operações de outra empresa ou composição.</p></section>
<section className="panel"><h2>Percursos disponíveis neste contexto</h2>{!context.unit&&<p>Seleccione uma unidade autorizada para verificar os percursos de trabalho.</p>}<div className="actions">{budget&&<button onClick={()=>navigate('purchasing_budget')}>Controlo orçamental</button>}{payables&&<button onClick={()=>navigate('supplier_payables')}>Obrigações a fornecedores</button>}</div>{!budget&&!payables&&<p>Esta composição ainda não apresenta percursos operacionais de Compras autorizados para a unidade actual. As demonstrações próprias de orçamento e obrigações conservam os seus dados e acessos; não foram eliminadas.</p>}</section>
<section className="panel"><h2>Compras autónomo e ERP</h2><p>O módulo autónomo conserva requisições, fornecedores, cotações, adjudicação, ordens e recepções. Esses percursos ainda não estão incorporados integralmente nesta interface SGF. Não se apresenta esta entrada como substituto funcional dessa aplicação.</p><p>Orçamento e obrigações reutilizam o núcleo comum. A entrada neste mapa não permite pagamentos, movimentos de stock ou emissão fiscal.</p></section>
<section className="panel"><h2>Percursos disponíveis neste contexto</h2>{!context.unit&&<p>Seleccione uma unidade autorizada para verificar os percursos de trabalho.</p>}<div className="actions">{(sourceRequests||suppliers||sourcing||orders||receipts||invoices||obligations)&&<a href="/purchasing/workspace">Percurso de Compras</a>}{requisitions&&<a href="/purchasing/internal-requisitions">Requisições internas</a>}{sourceRequests&&<a href="/purchasing/requests">Requisições para compra</a>}{suppliers&&<a href="/purchasing/suppliers">Fornecedores e revisão</a>}{sourcing&&<a href="/purchasing/sourcing">Propostas e adjudicação</a>}{orders&&<a href="/purchasing/orders">Ordens de compra</a>}{receipts&&<a href="/purchasing/receipts">Recepção de mercadoria</a>}{obligations&&<a href="/purchasing/obligations">Reconhecimento de obrigações</a>}{invoices&&<a href="/purchasing/invoices">Facturas de fornecedor</a>}{budget&&<button onClick={()=>navigate('purchasing_budget')}>Controlo orçamental</button>}{payables&&<button onClick={()=>navigate('supplier_payables')}>Obrigações a fornecedores</button>}</div>{!requisitions&&!sourceRequests&&!suppliers&&!sourcing&&!orders&&!receipts&&!invoices&&!obligations&&!budget&&!payables&&<p>Esta composição ainda não apresenta percursos operacionais de Compras autorizados para a unidade actual. As demonstrações próprias de orçamento e obrigações conservam os seus dados e acessos; não foram eliminadas.</p>}</section>
<section className="panel"><h2>Compras autónomo e ERP</h2><p>O módulo autónomo conserva requisições, fornecedores, cotações, adjudicação, ordens e recepções. Os percursos disponíveis acima reutilizam os serviços de Compras e os mesmos documentos nesta instalação.</p><p>Orçamento e obrigações reutilizam o núcleo comum. A entrada neste mapa não permite pagamentos, movimentos de stock ou emissão fiscal.</p></section>
</>;
}
+2 -1
View File
@@ -1,3 +1,4 @@
import GeographyLink from './GeographyLink.jsx';
import React, { useEffect, useState } from 'react';
import DocumentPreview from './DocumentPreview.jsx';
import {visibleBillingSections} from './billing-menu.js';
@@ -99,7 +100,7 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
{selected.confirmation && <InternalDocument confirmation={selected.confirmation} />}
{area === 'billing_drafts' && (selected.confirmation ? <button onClick={() => setPreviewMode('history')}>Abrir confirmação interna histórica</button> : can('issuer_documents.view') && can('client_documents.view') && can('draft_documents.view',context.unit?.id) && <button className="primary" onClick={() => setPreviewMode('draft')}>Pré-visualizar rascunho</button>)}
</>}
<details className="a-history"><summary>Histórico e captura durável ({selected.history?.length || 0} versões)</summary>{selected.history?.map(entry => <div key={entry.id}><strong>Versão {entry.version}</strong><p className="help">{entry.created_at} · Autor #{entry.actor_id} · Evento #{entry.event?.id} · Capturado, sem processamento contabilístico</p><details><summary>Dados preservados nesta versão</summary><pre>{JSON.stringify(entry.snapshot, null, 2)}</pre></details></div>)}</details>
{area==='clients'&&<GeographyLink enabled={context.geography_enabled} kind="client" id={selected.id}/>}<details className="a-history"><summary>Histórico e captura durável ({selected.history?.length || 0} versões)</summary>{selected.history?.map(entry => <div key={entry.id}><strong>Versão {entry.version}</strong><p className="help">{entry.created_at} · Autor #{entry.actor_id} · Evento #{entry.event?.id} · Capturado, sem processamento contabilístico</p><details><summary>Dados preservados nesta versão</summary><pre>{JSON.stringify(entry.snapshot, null, 2)}</pre></details></div>)}</details>
</> : <><h2>Seleccione um registo</h2><p>Os dados e o histórico aparecem aqui. As operações disponíveis dependem das suas concessões explícitas.</p></>}
</section></div></>}
</>;
+5 -3
View File
@@ -1,8 +1,9 @@
import React,{useEffect,useState} from 'react';
import {stockSections,visibleStockSections,stockOperations,availableStockReports} from './stock-menu.js';
import StockReadWorkspace from './StockReadWorkspace.jsx';
import StockPeriodReportWorkspace from './StockPeriodReportWorkspace.jsx';
export default function StockMenuWorkspace({section,context,can,navigate,api}) {
export default function StockMenuWorkspace({section,context,can,navigate,api,openReceiptOrigin}) {
const [reportId,setReportId]=useState('balances');
useEffect(()=>{setReportId('balances');},[context.token]);
if(!can('company.view'))return <section className="panel"><h1>Stock</h1><p>Seleccione uma empresa autorizada.</p></section>;
@@ -10,6 +11,7 @@ export default function StockMenuWorkspace({section,context,can,navigate,api}) {
if(!active)return null;
const operation=stockOperations[section];
const canRead=Boolean(api&&context.unit&&can('stock.view',context.unit.id));
const periodAvailable=canRead&&context.stock?.period_report===true&&reportId==='movements';
// This SGF composition has no stock source, feature configuration or cost permission yet.
// Do not substitute workshop rights, company rights or purchasing roles for stock rights.
const reports=availableStockReports({},false),report=reports.find(r=>r.id===reportId)??reports[0];
@@ -20,7 +22,7 @@ export default function StockMenuWorkspace({section,context,can,navigate,api}) {
<section className="panel"><h2>Situação actual</h2><p>Não há indicadores de stock disponíveis nesta composição. Não é possível determinar produtos abaixo do mínimo, entradas/saídas, valor do stock ou mercadoria em trânsito.</p><p>O painel sinalizará situações actuais; os relatórios permitirão investigar os movimentos e o período. Nenhum indicador é calculado a partir de exemplos ilustrativos.</p></section>
<section className="panel"><h2>Escolher percurso</h2><div className="stock-route-grid">{stockSections.filter(s=>s.id!=='stock_panel').map(s=><button key={s.id} onClick={()=>navigate(s.id)}>{s.label}<span>Consultar o âmbito e o estado de disponibilidade →</span></button>)}</div></section>
</>}
{section==='stock_balances'&&canRead&&<StockReadWorkspace key={context.token} api={api} context={context}/>}
{section==='stock_balances'&&canRead&&<StockReadWorkspace key={context.token} api={api} context={context} openReceiptOrigin={openReceiptOrigin}/>}
{section==='stock_balances'&&!canRead&&<>
<section className="panel"><h2>Consulta de existências indisponível</h2><p>Falta a fonte autorizada de produtos, armazéns e saldos desta empresa. Não há uma lista vazia de existências nem um saldo editável.</p><dl className="stock-definitions"><div><dt>Físico</dt><dd>Quantidade efectivamente registada no armazém, incluindo reservado e bloqueado.</dd></div><div><dt>Reservado</dt><dd>Quantidade comprometida com documentos próprios, quando o controlo estiver activo.</dd></div><div><dt>Bloqueado</dt><dd>Quantidade indisponível por condição ou qualidade, sem sobreposição com reservas.</dd></div><div><dt>Disponível</dt><dd>Físico menos reservado e bloqueado. Mercadoria em trânsito não está disponível no destino.</dd></div></dl><p>Se uma dessas parcelas não estiver sustentada, o disponível fica indeterminado; não se usa zero como substituto.</p></section>
<section className="panel"><h2>Pesquisa e rastreabilidade previstas</h2><p>Produto por código, designação ou código de barras; armazém, categoria e situação de stock. Localização, lote, série e validade só serão apresentados se configurados.</p><p>A ficha dará acesso ao movimento e ao documento original. Diferenças físicas serão tratadas por contagem e ajuste autorizado em Inventário, nunca por edição directa do saldo.</p><button onClick={()=>navigate('stock_reports')}>Ver relatórios de controlo</button></section>
@@ -30,7 +32,7 @@ export default function StockMenuWorkspace({section,context,can,navigate,api}) {
<section className="panel"><h2>Etapas previstas</h2><ol>{operation.flow.map(step=><li key={step}>{step}</li>)}</ol><h3>O que já existe e o que falta</h3><p>{operation.existing}</p><p>{operation.missing}</p><p><strong>Prevenção de duplicações:</strong> {operation.control}</p></section>
</>}
{section==='stock_reports'&&<>
<section className="panel"><h2>Relatórios essenciais</h2><label>Relatório<select value={reportId} onChange={e=>setReportId(e.target.value)}>{reports.map(r=><option key={r.id} value={r.id}>{r.label}</option>)}</select></label><h3>{report.label}</h3><p><strong>Base temporal:</strong> {report.basis}.</p><p><strong>Informação prevista:</strong> {report.fields}</p><p role="status"><strong>Consulta indisponível.</strong> {report.missing}</p><p>Não é possível gerar, imprimir ou exportar um relatório sem a sua fonte. A falta de informação não é apresentada como ausência de movimentos.</p></section>
<section className="panel"><h2>Relatórios essenciais</h2><label>Relatório<select value={reportId} onChange={e=>setReportId(e.target.value)}>{reports.map(r=><option key={r.id} value={r.id}>{r.label}</option>)}</select></label><h3>{report.label}</h3><p><strong>Base temporal:</strong> {report.basis}.</p><p><strong>Informação prevista:</strong> {report.fields}</p>{periodAvailable?<StockPeriodReportWorkspace key={context.token} api={api} context={context}/>:<><p role="status"><strong>Consulta indisponível.</strong> {report.missing}</p><p>Não é possível gerar, imprimir ou exportar um relatório sem a sua fonte. A falta de informação não é apresentada como ausência de movimentos.</p></>}</section>
<section className="panel"><h2>Disponibilidade dos restantes relatórios</h2><p>Valorização exige custos, método configurado e permissão própria. Dados de custos não são carregados por este menu. Relatórios de trânsito, devoluções, lotes, inactividade e diferenças de inventário serão apresentados quando os respectivos percursos estiverem activos.</p><p>A consulta e a exportação terão de preservar os mesmos filtros, autorizações, abertura e fecho. Reservas e bloqueios históricos não serão substituídos por valores actuais.</p></section>
</>}
<section className="panel"><h2>Regras comuns do percurso</h2><p>Um rascunho não altera stock. A confirmação revalida permissões e quantidades, tem um único efeito e conserva actor, data, motivo e origem. Movimentos confirmados não são editados ou apagados; correcções exigem operação própria ligada ao original.</p><p>Acesso a este mapa não concede consulta de armazéns, custos ou movimentos. A integração operacional terá autorizações específicas e contexto explícito, preservando as diferenças entre empresa, unidade e armazém.</p></section>
@@ -0,0 +1,29 @@
import React,{useEffect,useState} from 'react';
import {milliQuantity} from './StockReadWorkspace.jsx';
import {requestStockPeriod} from './stock-period-request.js';
export function StockPeriodTable({rows}) {
return <div className="table-wrap"><table><thead><tr><th>Artigo / unidade</th><th>Armazém</th><th>Abertura</th><th>Entradas</th><th>Saídas</th><th>Fecho</th><th>Movimentos no período</th></tr></thead>
<tbody>{rows.map(row=><tr key={`${row.stock_item_id}:${row.warehouse_id}`}><td>{row.code} · {row.name} · {row.unit}</td><td>{row.warehouse_code} · {row.warehouse_name}</td>
{['opening_milli','incoming_milli','outgoing_milli','closing_milli'].map(key=><td key={key}>{milliQuantity(row[key])}</td>)}<td>{row.period_movements}</td></tr>)}</tbody></table></div>;
}
export default function StockPeriodReportWorkspace({api,context}) {
const [query,setQuery]=useState(null),[data,setData]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(false);
useEffect(()=>{if(!query)return;return requestStockPeriod({query,api,data:setData,error:setError,loading:setLoading});},[query,context.token]);
const submit=e=>{e.preventDefault();const values=Object.fromEntries(new FormData(e.currentTarget));setData(null);setQuery({view:'period',from:values.from,to:values.to,search:values.search,warehouse_id:values.warehouse_id||undefined,per_page:Number(values.per_page),page:1});};
return <section aria-label="Relatório físico por período"><h3>Resumo físico por artigo e armazém</h3>
<p>Escolha as datas do registo em UTC. Abertura antes do primeiro dia; fecho até ao fim do último dia. As designações são as actuais.</p>
<form onSubmit={submit}><label>Primeiro dia (UTC)<input name="from" type="date" min="1900-01-01" max="9998-12-31" required/></label><label>Último dia (UTC)<input name="to" type="date" min="1900-01-01" max="9998-12-31" required/></label>
<label>Artigo por código ou nome<input name="search" maxLength={100}/></label><label>Identificador do armazém autorizado<input name="warehouse_id" type="number" min="1"/></label>
<label>Linhas por página<select name="per_page" defaultValue="25"><option value="25">25</option><option value="100">100</option></select></label><button disabled={loading}>Consultar período</button></form>
{!query&&<p>Seleccione o período; não foi consultada nem escolhida uma data automaticamente.</p>}
{loading&&<p role="status">A consultar o período autorizado…</p>}
{error&&<div role="alert"><p>{error}</p><button onClick={()=>{setData(null);setQuery({...query,page:1,snapshot:undefined});}}>Actualizar relatório</button></div>}
{data&&<><p role="status">{data.notice}</p><p>{data.period.from} a {data.period.to} · {data.period.timezone} · referência {data.observed_at}. Página {data.pagination.page} de {data.pagination.last_page}; {data.pagination.total} pares artigo/armazém.</p>
<StockPeriodTable rows={data.rows}/>{data.rows.length===0&&<p>Sem pares com história elegível até ao fecho para estes filtros. Não se inventam existências zero.</p>}
<button disabled={loading||data.pagination.page<=1} onClick={()=>setQuery({...query,page:data.pagination.page-1,snapshot:data.snapshot})}>Anterior</button>
<button disabled={loading||data.pagination.page>=data.pagination.last_page} onClick={()=>setQuery({...query,page:data.pagination.page+1,snapshot:data.snapshot})}>Seguinte</button>
<button disabled={loading} onClick={()=>{setData(null);setQuery({...query,page:1,snapshot:undefined});}}>Nova referência de leitura</button></>}
<p>Fecho = abertura + entradas − saídas, por linha e unidade. Sem valorização, reservas, bloqueios ou disponibilidade calculada.</p>
</section>;
}
+8 -3
View File
@@ -1,12 +1,16 @@
import React,{useEffect,useRef,useState} from 'react';
const labels={items:'Artigos',warehouses:'Armazéns',balances:'Existências físicas',movements:'Movimentos'};
export function StockReceiptOrigin({row,enabled,onOpen,loading}) {
if(!enabled||row.movement_type!=='receipt_in'||!Number.isSafeInteger(row.id)||row.id<1||typeof onOpen!=='function')return null;
return <button type="button" disabled={loading} onClick={()=>onOpen(row.id)}>Conferir recepção em Compras</button>;
}
export function milliQuantity(value) {
if(value===null||value===undefined)return 'Indeterminado';
const n=BigInt(value),negative=n<0n,absolute=negative?-n:n;
return `${negative?'-':''}${absolute/1000n},${String(absolute%1000n).padStart(3,'0')}`;
}
export default function StockReadWorkspace({api,context}) {
export default function StockReadWorkspace({api,context,openReceiptOrigin}) {
const initial={view:'balances',page:1,per_page:25,search:''};
const [query,setQuery]=useState(initial),[data,setData]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(false);
const generation=useRef(0);
@@ -19,6 +23,7 @@ export default function StockReadWorkspace({api,context}) {
},[context.token,query]);
function filter(e){e.preventDefault();const f=Object.fromEntries(new FormData(e.currentTarget));setQuery({view:query.view,page:1,per_page:Number(f.per_page),search:f.search,warehouse_id:f.warehouse_id||undefined});}
function page(n){setQuery({...query,page:n,snapshot:data.snapshot});}
const trace=query.view==='movements'&&data?.capabilities?.receipt_trace===true&&typeof openReceiptOrigin==='function';
return <section className="panel"><h2>Consulta de Stock</h2>
<p>Somente leitura. As restantes operações de Stock continuam indisponíveis neste percurso.</p>
<nav aria-label="Consultas de stock">{Object.entries(labels).map(([id,label])=><button key={id} disabled={loading} aria-current={query.view===id?'page':undefined} onClick={()=>setQuery({...initial,view:id})}>{label}</button>)}</nav>
@@ -29,8 +34,8 @@ export default function StockReadWorkspace({api,context}) {
{loading&&<p role="status">A consultar a fonte autorizada…</p>}
{error&&<div role="alert"><p>{error}</p><button onClick={()=>setQuery({...query,page:1,snapshot:undefined})}>Actualizar consulta</button></div>}
{data&&<><p role="status">{data.notice}</p><p>Referência de leitura: {data.observed_at}. {data.pagination.total} registos; página {data.pagination.page} de {data.pagination.last_page}.</p>
<div className="table-wrap"><table><thead><tr><th>Código / origem</th><th>Designação</th>{query.view!=='warehouses'&&<th>Unidade</th>}{['balances','movements'].includes(query.view)&&<><th>Armazém</th><th>{query.view==='balances'?'Físico':'Quantidade'}</th><th>{query.view==='balances'?'Disponível':'Tipo / data'}</th></>}</tr></thead>
<tbody>{data.rows.map(row=><tr key={row.id??`${row.stock_item_id}:${row.warehouse_id}`}><td>#{row.id??row.stock_item_id} · {row.code}{row.source_reference&&<small> · {row.source_reference}</small>}</td><td>{row.name}</td>{query.view!=='warehouses'&&<td>{row.unit}</td>}{['balances','movements'].includes(query.view)&&<><td>#{row.warehouse_id} · {row.warehouse_code} · {row.warehouse_name}</td><td>{milliQuantity(query.view==='balances'?row.physical_milli:row.quantity_milli)}</td><td>{query.view==='balances'?'Indeterminado':`${row.movement_type === 'workshop_return_in' ? 'Devolução de excedente' : row.movement_type} · ${row.moved_at}`}</td></>}</tr>)}</tbody></table></div>
<div className="table-wrap"><table><thead><tr><th>Código / origem</th><th>Designação</th>{query.view!=='warehouses'&&<th>Unidade</th>}{['balances','movements'].includes(query.view)&&<><th>Armazém</th><th>{query.view==='balances'?'Físico':'Quantidade'}</th><th>{query.view==='balances'?'Disponível':'Tipo / data'}</th></>}{trace&&<th>Documento de origem</th>}</tr></thead>
<tbody>{data.rows.map(row=><tr key={row.id??`${row.stock_item_id}:${row.warehouse_id}`}><td>#{row.id??row.stock_item_id} · {row.code}{row.source_reference&&<small> · {row.source_reference}</small>}</td><td>{row.name}</td>{query.view!=='warehouses'&&<td>{row.unit}</td>}{['balances','movements'].includes(query.view)&&<><td>#{row.warehouse_id} · {row.warehouse_code} · {row.warehouse_name}</td><td>{milliQuantity(query.view==='balances'?row.physical_milli:row.quantity_milli)}</td><td>{query.view==='balances'?'Indeterminado':`${row.movement_type === 'workshop_return_in' ? 'Devolução de excedente' : row.movement_type} · ${row.moved_at}`}</td></>}{trace&&<td><StockReceiptOrigin row={row} enabled={trace} onOpen={openReceiptOrigin} loading={loading}/></td>}</tr>)}</tbody></table></div>
{data.rows.length===0&&<p>Sem registos elegíveis para estes filtros nesta fonte autorizada.</p>}
<button disabled={loading||query.page<=1} onClick={()=>page(query.page-1)}>Anterior</button><button disabled={loading||query.page>=data.pagination.last_page} onClick={()=>page(query.page+1)}>Seguinte</button>
<button disabled={loading} onClick={()=>setQuery({...query,page:1,snapshot:undefined})}>Nova referência de leitura</button></>}
+13 -3
View File
@@ -1,3 +1,4 @@
import GeographyLink from './GeographyLink.jsx';
import React, { useEffect, useState, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import '../css/app.css';
@@ -27,6 +28,7 @@ import {billingSections} from './billing-menu.js';
import WorkshopMenuWorkspace from './WorkshopMenuWorkspace.jsx';
import {workshopSections} from './workshop-menu.js';
import StockMenuWorkspace from './StockMenuWorkspace.jsx';
import {openStockReceipt} from './stock-receipt-trace.js';
import {stockSections} from './stock-menu.js';
import AccountingMenuWorkspace from './AccountingMenuWorkspace.jsx';
import {accountingSections} from './accounting-menu.js';
@@ -83,6 +85,14 @@ function App() {
};
return method==='GET' ? execute() : transitions.current.command(execute);
}
const openStockReceiptOrigin = movement => transition(async()=>{
setError('');
try { await openStockReceipt({movement,context,active:()=>currentToken.current===context.token,
select:body=>api('/api/context','POST',body),
commit:next=>{mountedToken.current=next.context.token;currentToken.current=next.context.token;setState(next);},
go:href=>location.assign(href)});
}catch(e){setError(e.message);}
});
async function refresh() {
const response = await fetch('/api/session', { credentials: 'same-origin', headers: { Accept: 'application/json' } });
if (!response.ok) throw new Error('Não foi possível carregar a sessão.');
@@ -113,11 +123,11 @@ function App() {
{page === 'module_records' && <ModuleRecordsWorkspace context={context} can={can} api={api}/>}
{page === 'workshop_billing' && <WorkshopBillingWorkspace context={context} can={can} api={api} busy={busy} />}
{page === 'workshop_corrections' && <WorkshopCorrectionsWorkspace context={context} can={can} api={api} busy={busy} />}
{page === 'company' && <section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Empresa</h1><NameForm key={context.company.id} label="Nome da empresa" initial={context.company.name} editable={can('company.edit')} busy={busy} onSave={name => perform(() => api(`/api/companies/${context.company.id}`, 'PATCH', { name }, context.token), 'Nome da empresa actualizado.')} />{!can('company.edit') && <p className="help">O seu acesso permite consulta. A edição exige uma concessão própria.</p>}</section>}
{page === 'units' && <><section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Unidades autorizadas</h1>{!context.units.length && <p>Não tem consulta concedida a nenhuma unidade desta empresa.</p>}<ul className="unit-list">{context.units.map(u => <li key={u.id}><strong>{u.name}</strong><button disabled={busy || context.unit?.id === u.id} onClick={() => select(context.company.id, u.id)}>{context.unit?.id === u.id ? 'Seleccionada' : 'Seleccionar'}</button></li>)}</ul>{context.unit && <NameForm label="Nome da unidade" initial={context.unit.name} editable={can('unit.edit', context.unit.id)} busy={busy} onSave={name => perform(() => api(`/api/units/${context.unit.id}`, 'PATCH', { name }, context.token), 'Nome da unidade actualizado.')} />}</section>{can('unit.create') && <section className="panel"><h2>Criar unidade</h2><p className="help">A unidade será criada nesta empresa. Receberá consulta e edição explícitas apenas para a nova unidade.</p><NameForm label="Nome da nova unidade" initial="" editable busy={busy} button="Criar unidade" onSave={name => perform(() => api('/api/units', 'POST', { name }, context.token), 'Unidade criada com os seus acessos explícitos.')} /></section>}</>}
{page === 'company' && <section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Empresa</h1><NameForm key={context.company.id} label="Nome da empresa" initial={context.company.name} editable={can('company.edit')} busy={busy} onSave={name => perform(() => api(`/api/companies/${context.company.id}`, 'PATCH', { name }, context.token), 'Nome da empresa actualizado.')} /><GeographyLink enabled={context.geography_enabled} kind="company" id={context.company.id}/>{!can('company.edit') && <p className="help">O seu acesso permite consulta. A edição exige uma concessão própria.</p>}</section>}
{page === 'units' && <><section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Unidades autorizadas</h1>{!context.units.length && <p>Não tem consulta concedida a nenhuma unidade desta empresa.</p>}<ul className="unit-list">{context.units.map(u => <li key={u.id}><strong>{u.name}</strong><button disabled={busy || context.unit?.id === u.id} onClick={() => select(context.company.id, u.id)}>{context.unit?.id === u.id ? 'Seleccionada' : 'Seleccionar'}</button></li>)}</ul>{context.unit && <NameForm label="Nome da unidade" initial={context.unit.name} editable={can('unit.edit', context.unit.id)} busy={busy} onSave={name => perform(() => api(`/api/units/${context.unit.id}`, 'PATCH', { name }, context.token), 'Nome da unidade actualizado.')} />}</section>{context.unit&&<GeographyLink enabled={context.geography_enabled} kind="unit" id={context.unit.id}/>}{can('unit.create') && <section className="panel"><h2>Criar unidade</h2><p className="help">A unidade será criada nesta empresa. Receberá consulta e edição explícitas apenas para a nova unidade.</p><NameForm label="Nome da nova unidade" initial="" editable busy={busy} button="Criar unidade" onSave={name => perform(() => api('/api/units', 'POST', { name }, context.token), 'Unidade criada com os seus acessos explícitos.')} /></section>}</>}
{serviceAreas[page] && <ServicesWorkspace key={page} area={page} context={context} can={can} api={api} perform={perform} busy={busy} initialId={aSelection?.area === page ? aSelection.id : null} initialCreate={aSelection?.area === page && aSelection.create} navigate={(area,id) => navigate(area,{area,id})} />}
{billingSections.some(s=>s.id===page)&&page!=='billing_drafts'&&<BillingMenuWorkspace key={JSON.stringify(context.grants)} section={page} context={context} can={can} api={api} navigate={(area,id=null,create=false)=>navigate(area,{area,id,create})}/>}
{stockSections.some(s=>s.id===page)&&<StockMenuWorkspace key={JSON.stringify(context.grants)} section={page} context={context} can={can} navigate={navigate} api={api}/>}
{stockSections.some(s=>s.id===page)&&<StockMenuWorkspace key={JSON.stringify(context.grants)} section={page} context={context} can={can} navigate={navigate} api={api} openReceiptOrigin={openStockReceiptOrigin}/>}
{accountingSections.some(s=>s.id===page)&&<AccountingMenuWorkspace key={JSON.stringify(context.grants)} section={page} context={context} can={can} navigate={navigate}/>}
{page === 'preview_model' && <DocumentPreviewConfiguration context={context} can={can} api={api} perform={perform} busy={busy} />}
{page === 'documents' && <DocumentPreparationWorkspace context={context} can={can} api={api} perform={perform} busy={busy} />}
+9
View File
@@ -0,0 +1,9 @@
/** One cancellable read: a late response cannot replace another period or context. */
export function requestStockPeriod({query,api,data,error,loading}) {
let cancelled=false;
data(null);error('');loading(true);
const params=new URLSearchParams(Object.entries(query).filter(([,value])=>value!==undefined&&value!==''));
Promise.resolve().then(()=>api('/api/stock-read?'+params)).then(value=>{if(!cancelled)data(value);})
.catch(e=>{if(!cancelled)error(e.message);}).finally(()=>{if(!cancelled)loading(false);});
return ()=>{cancelled=true;};
}
+18
View File
@@ -0,0 +1,18 @@
export function receiptMovementHref(id) {
if(!Number.isSafeInteger(id)||id<1)throw Error('Movimento inválido. Actualize a consulta.');
return `/purchasing/receipts?movement=${id}`;
}
/** Called only inside the host's existing dirty-form/context transition guard. */
export async function openStockReceipt({movement,context,active,select,commit,go}) {
const href=receiptMovementHref(movement),unit=context?.unit?.id;
const allowed=p=>context?.grants?.some(g=>g.permission===p&&g.unit_id===unit);
if(context?.module_id!=='stock'||!unit||!context?.purchasing?.receipts
||!context?.modules?.some(m=>m.id==='purchasing')||!allowed('stock.view')||!allowed('purchasing_receipts.view'))throw Error('Origem sem acesso autorizado neste contexto.');
if(!active())throw Error('O contexto mudou. Actualize a consulta.');
const next=await select({tenant_id:context.company.tenant_id,company_id:context.company.id,unit_id:unit,module_id:'purchasing'});
if(!active())throw Error('Resposta de um contexto anterior ignorada.');
if(next?.context?.module_id!=='purchasing'||next.context.company?.id!==context.company.id
||next.context.company?.tenant_id!==context.company.tenant_id||next.context.unit?.id!==unit||!next.context.token)throw Error('O destino não confirmou o contexto pedido.');
commit(next);go(href);
}
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Correcção de custo275→295 QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-cost-demo="{{ $tag }}"><h1>Correcção de custo directo275→295 — QA</h1><p>Ordem #{{ $order }}. O custo seleccionado original100+175=275 foi validado e lançado pelos workflows existentes. Uma taxa material sucessora60/UN e cálculo120+175=295 foram preparados e validados distintamente. O original, decisões e outbox permanecem; nenhuma correcção contabilística foi aplicada nesta demonstração inicial.</p><p>Ensaio com contas artificiais. Não é custo completo, stock valorizado, salário ou operação monetária. Não há reabertura ou uso real.</p><p><a href="/qa/cost-enter/prepare?open=cost">Preparar / aplicar par</a> · <a href="/qa/cost-enter/validate?open=cost">Conferir o par</a> · <a href="/qa/cost-enter/read?open=cost">Consultar</a></p><ol><li>Conferir original275, novo295, diferença20; preparador captura o par com fundamento.</li><li>Validador confere as quatro linhas: estorno275 e substituição295. Decide explicitamente, sem aplicação isolada.</li><li>Preparador revalida/aplica o grupo numa transacção; consultar os três lançamentos, balancete/razão com saldo295 e bruto845 por lado.</li><li>Capturar conferência do período, rever distintamente e fechar QA.</li></ol><p>Usar sessões separadas para os perfis. A demonstração8054 e todas as anteriores permanecem independentes e intactas. Não há reset automático. Revisão visual e capacidade de implantação continuam separadas.</p></main></html>
@@ -0,0 +1,11 @@
@php $original=$c['original_journal']; @endphp
@if($original&&$source)<section><h2>Correcção explícita do custo lançado</h2><p>Original #{{ $original->id }}: {{ $money($original->debit_minor) }}. Cálculo actual validado: {{ $money($source['principal_minor']) }}. Diferença: {{ $money($source['principal_minor']-$original->debit_minor) }}.</p><p>O original permanece imutável. O par contém estorno integral e substituição; nenhuma das partes pode ser revista ou aplicada isoladamente.</p>
@if(!$closed&&$c['accounting_state']==='requires_correction'&&$data['permissions']['correct_prepare']&&!collect($data['correction_results'])->contains('original_journal_id',$original->id))<form data-action="correct-prepare"><input type="hidden" name="journal_id" value="{{ $original->id }}"><label>Data explícita da correcção<input type="date" name="journal_on" value="2026-10-29" required></label><label>Período aberto do original<input name="period_key" value="QA-2026-10" required></label><label>Fundamento do par<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar estorno e substituição — sem aplicar</button></form>@endif
@foreach($data['corrections'] as $group)@php $reverse=json_decode($group->reverse_snapshot,true);$replacement=json_decode($group->replacement_snapshot,true); @endphp<article><h3>Grupo #{{ $group->id }} · original #{{ $group->original_journal_id }}</h3><p>Autor #{{ $group->prepared_by }} · {{ $group->reason }} · versão exacta <code>{{ $group->revision }}</code></p><p>Estorno {{ $money($reverse['impact']['debit_minor']) }} · substituição {{ $money($replacement['impact']['debit_minor']) }} · diferença {{ $money($replacement['impact']['debit_minor']-$reverse['impact']['debit_minor']) }}.</p><h4>Estorno</h4><ul>@foreach($reverse['lines'] as $line)<li>{{ $line['account_code'] }} — D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><h4>Substituição</h4><ul>@foreach($replacement['lines'] as $line)<li>{{ $line['account_code'] }} — D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><details><summary>Fotografias do original, fonte actual, contas e período</summary><pre>{{ json_encode(['reverse'=>$reverse,'replacement'=>$replacement],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>
@foreach([$group->reverse_review,$group->replacement_review] as $review)@if($review)<p>Proposta #{{ $review->proposal_id }} · {{ $review->decision }} · revisor #{{ $review->reviewed_by }} · {{ $review->reason }}</p>@endif @endforeach
@if($group->result)<p>Par aplicado: resultado #{{ $group->result->id }}, estorno #{{ $group->result->reverse_journal_id }} e substituição #{{ $group->result->replacement_journal_id }} · autor #{{ $group->result->actor_id }} · {{ $group->result->created_at }} · {{ $group->result->reason }}</p>
@elseif(!$group->current)<p class="notice">Captura superada; conservar o histórico e conferir o grupo actual.</p>
@elseif(!$closed)
@if($data['permissions']['correct_validate']&&!$group->reverse_review&&!$group->replacement_review&&$group->prepared_by!==auth()->id())<form data-action="correct-validate"><input type="hidden" name="correction_id" value="{{ $group->id }}"><input type="hidden" name="revision" value="{{ $group->revision }}"><label>Decisão do par completo<select name="decision" required><option value="">Escolher</option><option value="approved">Aprovar as quatro linhas exactas</option><option value="rejected">Recusar o par</option></select></label><label>Fundamento independente<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar conferência do par — sem aplicar</button></form>@endif
@if($data['permissions']['correct_post']&&$group->prepared_by===auth()->id()&&$group->reverse_review?->decision==='approved'&&$group->replacement_review?->decision==='approved')<form data-action="correct-post"><input type="hidden" name="correction_id" value="{{ $group->id }}"><input type="hidden" name="revision" value="{{ $group->revision }}"><label>Fundamento da aplicação conjunta<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar o par numa transacção</button></form>@endif
@endif</article>@endforeach</section>@endif
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Custo directo e diário QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-cost-demo="{{ $tag }}"><h1>Oficina → custo directo → diário e relatórios</h1><p>Ensaio sintético sem utilização operacional. Ordem #{{ $order }}: material2UN×50=100 e trabalho105MIN÷60×100=175; custo directo conhecido seleccionado275MZN. A ordem, verificação técnica, taxas e cálculo foram preparados e validados pelos workflows existentes. Não é custo completo, margem, stock valorizado ou salário.</p><p><a href="/qa/cost-enter/prepare?open=cost">Preparar / aplicar</a> · <a href="/qa/cost-enter/validate?open=cost">Rever distintamente</a> · <a href="/qa/cost-enter/read?open=cost">Consultar</a></p><ol><li>Conferir origens, taxas, decisões e versões; preparador captura impacto com fundamento.</li><li>Validador entra pela sua função, confere D QA-CUSTO-DIRECTO275 / C QA-COMPENSACAO-ENSAIO275 e decide explicitamente.</li><li>Preparador aplica uma vez; consultar diário, balancete e razão ligados à ordem.</li><li>Preparar fotografia do período, rever distintamente e fechar QA, sem reabrir.</li></ol><p>Usar sessões separadas para manter perfis em paralelo. Sem reset automático, publicação, direitos reais ou novos efeitos por validar o custo. A consulta de evidências de custo não concede verificação técnica.</p><p>Revisão visual e capacidade de implantação permanecem separadas. O histórico e a outbox originais são preservados.</p></main></html>
@@ -0,0 +1,23 @@
@php $c=$data['cost'];$id=$c['order_id'];$source=$c['source'];$facts=$c['facts'];$closed=collect($data['periods'])->where('period_key','QA-2026-10')->sortByDesc('version')->first()?->status==='closed';$money=fn($n)=>($n<0?'-':'').number_format(intdiv(abs($n),100),0,',',' ').','.str_pad((string)(abs($n)%100),2,'0',STR_PAD_LEFT).' MZN'; @endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Custo directo e diário QA</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28;margin:0}main{max-width:1100px;margin:auto;padding:24px}header,section{background:white;border:1px solid #d5e0dc;border-radius:12px;padding:24px;margin:18px 0}article{border-top:1px solid #ddd;padding:18px 0}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:10px;max-width:95%}textarea{display:block;width:90%}.notice{background:#fff2cc;padding:16px}pre{white-space:pre-wrap;overflow-wrap:anywhere}a{color:#125a4b}code{overflow-wrap:anywhere}</style></head><body><main>
<header><a href="/general-ledger">Diário</a> · <a href="/general-ledger/trial-balance?from_on=2026-10-01&amp;to_on=2026-10-31">Balancete e razão QA</a> · <a href="/qa/cost-demo">Perfis do ensaio</a><h1>Oficina → custo directo validado → Contabilidade</h1><p class="notice">EXCLUSIVAMENTE SINTÉTICO. Custo directo conhecido do perímetro seleccionado, não custo completo, margem, stock valorizado ou salário. Contas artificiais; nenhuma execução monetária.</p><p>Empresa #{{ $c['scope']['company_id'] }} / unidade #{{ $c['scope']['unit_id'] }}. Validar o custo não lança automaticamente. Capturar, rever e aplicar são decisões individuais.</p></header><p id="error" role="alert"></p>
<section><h2>Perímetros explicitamente compostos</h2>@foreach($c['orders'] as $o)<p><a href="?order_id={{ $o->order_id }}">Ordem #{{ $o->order_id }}</a> · {{ $o->reason }}</p>@endforeach</section>
@if($facts)<section><h2>Conferência da ordem #{{ $id }}</h2><p>{{ $facts['conference']['order']['reference'] }} · cálculo #{{ $facts['conference']['calculation']['id']??'pendente' }} / versão {{ $facts['conference']['calculation']['version']??'—' }} · decisão {{ $facts['conference']['decision']['decision']??'pendente' }}</p><p>Conhecidos {{ $facts['conference']['snapshot']['known_subtotal_exact']??'—' }} MZN; total seleccionado {{ $facts['conference']['snapshot']['selected_direct_total_exact']??'indisponível' }} MZN.</p>@if($c['pending'])<p class="notice">{{ $c['pending'] }}</p>@endif<details><summary>Origens verificadas, taxas, decisões e versões</summary><pre>{{ json_encode($facts,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>
@if($source)<p>Impacto proposto: D QA-CUSTO-DIRECTO {{ $money($source['principal_minor']) }} / C QA-COMPENSACAO-ENSAIO {{ $money($source['principal_minor']) }}. Sem alterar a origem ou criar dívida.</p>@if(!$closed&&$data['permissions']['prepare']&&!count($data['book']['journals']))<form data-action="effect-prepare"><input type="hidden" name="effect_type" value="workshop.direct_cost.validated"><input type="hidden" name="source_id" value="{{ $id }}"><input type="hidden" name="rule_key" value="QA-DIRECT-COST"><label>Data explícita<input type="date" name="journal_on" value="2026-10-29" required></label><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar fonte e impacto — sem lançar</button></form>@endif @endif
@if($closed)<p class="notice">Período QA fechado; conservar a consulta e os comprovativos. Sem reabertura.</p>@endif</section>@endif
@if($c['accounting_state']==='requires_correction')<p class="notice">A fonte actual difere do custo lançado ou está pendente. Não criar outro reconhecimento; a conferência do período exige uma resolução explícita.</p>@endif
@if(config('cost_ledger.correction_enabled')===true)@include('accounting.cost-correction')@endif
<section><h2>Propostas e conferência contabilística</h2>@foreach($data['book']['proposals'] as $p)@php $snap=json_decode($p->snapshot,true);$isPart=in_array($snap['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement'],true);$r=collect($data['book']['reviews'])->firstWhere('proposal_id',$p->id); @endphp<article><h3>Proposta #{{ $p->id }} · versão da captura {{ $p->capture_version }}</h3><p>{{ $p->reason }} · preparador #{{ $p->prepared_by }} · revisão exacta <code>{{ $p->revision }}</code></p><ul>@foreach($snap['lines'] as $l)<li>{{ $l['account_code'] }} — D {{ $money($l['debit_minor']) }} / C {{ $money($l['credit_minor']) }}</li>@endforeach</ul><details><summary>Fotografia imutável capturada</summary><pre>{{ json_encode($snap,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>@if($r)<p>{{ $r->decision }} por #{{ $r->reviewed_by }} · {{ $r->reason }}</p>@endif
@if(!$closed&&!$isPart&&$data['permissions']['validate']&&!$r&&$p->prepared_by!==auth()->id())<form data-action="validate"><input type="hidden" name="proposal_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Decisão<select name="decision" required><option value="">Escolher</option><option value="approved">Aprovar versão exacta</option><option value="rejected">Recusar</option></select></label><label>Fundamento independente<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar conferência — sem aplicar</button></form>@endif
@if(!$closed&&!$isPart&&$data['permissions']['post']&&$r?->decision==='approved'&&!$p->journal_id&&$p->prepared_by===auth()->id())<form data-action="post"><input type="hidden" name="proposal_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Fundamento da aplicação<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar este custo uma vez</button></form>@endif</article>@endforeach</section>
<section><h2>Lançamentos e histórico por origem</h2><p>Débitos {{ $money($data['book']['totals']['debit_minor']) }} · créditos {{ $money($data['book']['totals']['credit_minor']) }}.</p>@foreach($data['book']['journals'] as $j)<details><summary>Lançamento #{{ $j->id }} · {{ $j->journal_on }} · {{ $money($j->debit_minor) }}</summary><p>Proposta #{{ $j->proposal_id }} · revisão #{{ $j->review_id }} · aplicador #{{ $j->posted_by }} · {{ $j->reason }}</p><pre>{{ json_encode(json_decode($j->snapshot,true),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>@endforeach @foreach($data['book']['events'] as $e)<p>#{{ $e->id }} · {{ $e->kind }} · autor #{{ $e->actor_id }} · {{ $e->reason }}</p>@endforeach</section>
<section><h2>Conferência e fecho do período composto</h2><p>Inclui todas as fontes compostas da empresa/unidade. Custo pendente, alterado ou ainda não lançado impede o fecho; ocultar uma linha na página não a exclui da conferência.</p>@if(!$closed&&$data['permissions']['period_prepare'])<form data-action="period-prepare"><input type="hidden" name="period_key" value="QA-2026-10"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar conferência do período</button></form>@endif
@foreach($data['period_checks'] as $p)@php $r=collect($data['period_reviews'])->firstWhere('check_id',$p->id); @endphp<article><h3>Conferência #{{ $p->id }}</h3><p>{{ $p->reason }} · versão <code>{{ $p->revision }}</code></p><details><summary>Impacto e fotografia do período</summary><pre>{{ $p->snapshot }}</pre></details>@if($r)<p>{{ $r->decision }} · autor #{{ $r->actor_id }} · {{ $r->reason }}</p>@endif
@if(!$closed&&!$r&&$data['permissions']['period_validate']&&$p->prepared_by!==auth()->id())<form data-action="period-validate"><input type="hidden" name="check_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Decisão<select name="decision" required><option value="">Escolher</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Conferir período sem fechar</button></form>@endif
@if(!$closed&&$r?->decision==='approved'&&$data['permissions']['period_close']&&$p->prepared_by===auth()->id())<form data-action="period-close"><input type="hidden" name="check_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e fechar o período QA</button></form>@endif</article>@endforeach @foreach($data['period_closures'] as $c)<p>Fecho #{{ $c->id }} · {{ $c->period_key }} v{{ $c->period_version }} · {{ $c->reason }}</p>@endforeach</section>
@php $pages=array_merge([$data['cost']['pagination']['last_page'],$data['book']['pagination']['last_page']],array_column($data['pagination'],'last_page'),array_column($data['book']['evidence_pagination'],'last_page'));$page=$data['book']['pagination']['page'];$last=max($pages); @endphp<nav aria-label="Paginação das evidências"><p>Página {{ $page }} de {{ $last }} · cada lista limitada a25 registos</p>@foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label)@if($page+$delta>=1&&$page+$delta<=$last)<a href="?{{ http_build_query(array_filter(['order_id'=>$id,'page'=>$page+$delta],fn($v)=>$v!==null)) }}">{{ $label }}</a>@endif @endforeach</nav>
@include('accounting.receipt-recovery')
</main><script>
const csrf=@json(csrf_token()),context=@json(session('company_context.token'));
for(const form of document.querySelectorAll('[data-action]'))form.onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(form));for(const k of ['source_id','proposal_id','check_id','journal_id','correction_id'])if(k in data)data[k]=Number(data[k]);const body=JSON.stringify(data);if(form.dataset.body!==body){form.dataset.body=body;form.dataset.key=crypto.randomUUID();}const button=form.querySelector('button');button.disabled=true;document.querySelector('#error').textContent='';try{const r=await fetch('/api/general-ledger/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,'Idempotency-Key':form.dataset.key},body});const result=await r.json();if(!r.ok)throw Error(result.message||'Conferência recusada.');location.reload();}catch(err){document.querySelector('#error').textContent=err.message+' Chave do pedido: '+form.dataset.key+'. Confira a versão e o histórico antes de repetir.';}finally{button.disabled=false;}};
</script></body></html>
@@ -0,0 +1,33 @@
@php $ctx=app(\App\Shared\Context\CompanyContext::class);$supplierAccess=app(\App\Modules\Identity\Services\Permissions::class)->has(auth()->id(),$ctx->identifiers()['tenant_id'],$ctx->identifiers()['company_id'],'supplier_payables.view',$ctx->unitId());$money=static fn($n)=>($n<0?'-':'').number_format(intdiv(abs($n),100),0,',',' ').','.str_pad((string)(abs($n)%100),2,'0',STR_PAD_LEFT).' MZN'; @endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lançamento sintético — SGF</title><style>body{font:17px system-ui;color:#142b28;background:#f3f6f5;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;border:1px solid #d5e0dc;border-radius:12px;margin:18px 0;padding:24px}label{display:block;margin:14px 0}input,select,textarea,button{font:inherit;padding:10px;max-width:100%}textarea{display:block;width:90%}.notice{background:#fff2cc;padding:16px}pre{white-space:pre-wrap;overflow-wrap:anywhere}a{color:#125a4b}table{border-collapse:collapse;width:100%}td,th{text-align:left;border-bottom:1px solid #ddd;padding:10px}</style></head><body><main>
<header><a href="/">Voltar à navegação SGF / mudar módulo</a>@if(config('sales_ledger.synthetic_enabled')===true||config('cost_ledger.synthetic_enabled')===true)<h1>Contabilidade — diário de teste por origem</h1><p class="notice">EXCLUSIVAMENTE SINTÉTICO. Contas, regras e períodos artificiais. Sem uso real, contas PGC aprovadas ou execução monetária.</p><p>Fontes técnicas, comerciais e financeiras são efeitos distintos, com fotografia e revisão próprias. O lançamento não cria nem altera a dívida financeira.</p>@else<h1>Contabilidade — primeiro lançamento de ensaio</h1><p class="notice">EXCLUSIVAMENTE SINTÉTICO. Duas contas e um período artificiais. Sem uso real, contas PGC aprovadas, emissão, execução bancária ou fecho operacional.</p><p>Origem: obrigação reconhecida pelo Payables. Aplicar o lançamento não cria nem altera a dívida financeira. Vencimento e período contabilístico são factos separados.</p>@endif</header>
@if(config('general_ledger.composition_enabled')===true)<p><a href="/general-ledger/workspace">Painel conjunto de vendas, fornecedores e custos QA</a></p>@endif
<p id="result" role="status"></p><p id="error" role="alert"></p>
@if(config('cost_ledger.synthetic_enabled')===true)<p><a href="/general-ledger/costs">Oficina → custo directo validado → diário e relatórios QA</a></p>@endif
<p><a href="/general-ledger/trial-balance?from_on=2026-10-01&amp;to_on=2026-10-31">Balancete e razão por conta — Outubro de 2026 (QA)</a></p>
@if(config('general_ledger.cycle_enabled')===true&&$supplierAccess)<p><a href="/general-ledger/cycle">Ciclo integrado: afectação, reversão, correcção técnica e fecho QA</a></p>@endif
@if(config('sales_ledger.synthetic_enabled')===true)<p><a href="/general-ledger/sales">Comercial → recebíveis → contabilidade: origem, decisões e extracto</a></p>@endif
@if(session('ledger_demo'))<p><a href="/qa/ledger-demo">Trocar função no ensaio sintético</a></p>@endif
@if($data['permissions']['prepare']&&$supplierAccess)<section><h2>Preparar da origem reconhecida</h2><form data-action="prepare"><label>ID da obrigação de fornecedor<input name="obligation_id" type="number" min="1" value="{{ $data['query']['obligation_id']??'' }}" required></label><label>Data explícita<input name="journal_on" type="date" value="2026-10-29" required></label><label>Período de ensaio<input name="period_key" value="QA-2026-10" required></label><label>Regra de ensaio<input name="rule_key" value="QA-PAYABLE-JOURNAL" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar proposta e impacto — sem lançar</button></form></section>@endif
<section><h2>Propostas e conferência individual</h2>
@foreach($data['proposals'] as $p)
@php $snapshot=json_decode($p->snapshot,true);$isPart=in_array($snapshot['source']['effect_type'],['journal.reversal','journal.replacement','workshop.direct_cost.reversal','workshop.direct_cost.replacement','industry.cost.reversal','industry.cost.replacement'],true);$review=collect($data['reviews'])->firstWhere('proposal_id',$p->id); @endphp
@if(isset($snapshot['source']['industrial_lot_id']))<p><a href="/general-ledger/industrial?lot_id={{ $snapshot['source']['industrial_lot_id'] }}">Conferir lote e resultado industrial</a></p>@elseif(isset($snapshot['source']['cost_order_id']))<p><a href="/general-ledger/costs?order_id={{ $snapshot['source']['cost_order_id'] }}">Conferir ordem e custo directo seleccionado</a></p>@elseif(isset($snapshot['source']['invoice_id']))<p><a href="/general-ledger/sales?invoice_id={{ $snapshot['source']['invoice_id'] }}">Conferir percurso e origem comercial</a></p>@else<p><a href="/general-ledger/flow?source={{ $snapshot['source']['local_source_id'] }}">Conferir percurso e origem</a></p>@endif
<article><h3>Proposta #{{ $p->id }} · captura {{ $p->capture_version }}</h3><p>@if(isset($snapshot['source']['industrial_lot_id']))Lote #{{ $snapshot['source']['industrial_lot_id'] }}@elseif(isset($snapshot['source']['cost_order_id']))Ordem #{{ $snapshot['source']['cost_order_id'] }}@else Obrigação #{{ $p->obligation_id }}@endif · preparador #{{ $p->prepared_by }} · {{ $p->created_at }}</p><p>{{ $p->reason }}</p><p>Data {{ $snapshot['journal_on'] }} · período {{ $snapshot['period']['period_key'] }} v{{ $snapshot['period']['version'] }} · regra v{{ $snapshot['rule']['version'] }}</p>
<ul>@foreach($snapshot['lines'] as $line)<li>{{ $line['account_code'] }} — débito {{ $money($line['debit_minor']) }} / crédito {{ $money($line['credit_minor']) }}</li>@endforeach</ul><p>Impacto: lançamento equilibrado de {{ $money($snapshot['impact']['debit_minor']) }}. Sem novo pagamento ou alteração do saldo financeiro.</p><p>Revisão exacta <code>{{ $p->revision }}</code>@unless(isset($snapshot['source']['cost_order_id'])||isset($snapshot['source']['industrial_lot_id'])) · Vencimento da origem: {{ $snapshot['source']['due_on'] ?? 'desconhecido' }}@endunless</p>
<details><summary>Fotografia da fonte, regra e período</summary><pre>{{ json_encode($snapshot,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>
@if($review)<p>Decisão {{ $review->decision==='approved'?'aprovada':'recusada' }} por #{{ $review->reviewed_by }} · {{ $review->reason }}</p>@endif
@if($p->journal_id)<p>Aplicada uma vez no lançamento #{{ $p->journal_id }}. Consulte o comprovativo abaixo.</p>@endif
@if(!$isPart&&$data['permissions']['validate']&&!$review&&$p->prepared_by!==auth()->id())<form data-action="validate"><input type="hidden" name="proposal_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Decisão explícita<select name="decision"><option value="approved">Aprovar versão exacta</option><option value="rejected">Recusar</option></select></label><label>Fundamento independente<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar conferência — sem lançar</button></form>@endif
@if(!$isPart&&$data['permissions']['post']&&$review&&$review->decision==='approved'&&$p->prepared_by===auth()->id()&&!$p->journal_id)<form data-action="post"><input type="hidden" name="proposal_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Fundamento da aplicação<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar este impacto uma vez</button></form>@endif
</article>@endforeach</section>
<section><h2>Lançamentos e extracto sintético</h2><form method="get"><label>Período<input name="period_key" value="{{ $data['query']['period_key']??'QA-2026-10' }}"></label><label>Conta<select name="account_id"><option value="">Todas as contas</option>@foreach($data['accounts'] as $account)<option value="{{ $account->id }}" @selected(($data['query']['account_id']??'')==$account->id)>{{ $account->code }}</option>@endforeach</select></label><button>Consultar</button></form>
<p>Débitos {{ $money($data['totals']['debit_minor']) }} · Créditos {{ $money($data['totals']['credit_minor']) }} · Débito menos crédito {{ $money($data['totals']['net_minor']) }}</p><p>Os totais incluem todos os lançamentos do filtro; as contrapartidas dos lançamentos da página aparecem abaixo. Não são um balancete legal ou uma posição financeira por liquidar.</p>
@foreach($data['journals'] as $j)@php $journalSource=json_decode($j->snapshot,true)['source']; @endphp<article><h3>Lançamento #{{ $j->id }} · {{ $j->journal_on }}</h3><p>@if(isset($journalSource['industrial_lot_id']))Lote #{{ $journalSource['industrial_lot_id'] }}@elseif(isset($journalSource['cost_order_id']))Ordem #{{ $journalSource['cost_order_id'] }}@else Obrigação #{{ $j->obligation_id }}@endif · Proposta #{{ $j->proposal_id }} · Revisão #{{ $j->review_id }} · Aplicador #{{ $j->posted_by }}</p><p>{{ $j->reason }} · {{ $j->created_at }}</p><ul>@foreach($data['lines'] as $l)@if($l->journal_id===$j->id)<li>Conta #{{ $l->account_id }}: D {{ $money($l->debit_minor) }} / C {{ $money($l->credit_minor) }}</li>@endif @endforeach</ul><details><summary>Fotografia imutável da origem, regra e período aplicados</summary><pre>{{ json_encode(json_decode($j->snapshot,true),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>@if(isset($journalSource['cost_order_id']))<p><a href="/general-ledger/costs?order_id={{ $journalSource['cost_order_id'] }}">Conferir a ordem, o custo e as decisões</a>. A fotografia acima conserva o estado aplicado.</p>@else<p>Para conferir a posição financeira actual, volte à navegação, seleccione Compras e use a referência local da origem. A fotografia acima conserva o estado aplicado.</p>@endif</article>@endforeach
<nav aria-label="Paginação do diário"><p>Página {{ $data['pagination']['page'] }} de {{ $data['pagination']['last_page'] }} · {{ $data['pagination']['total'] }} lançamentos</p>@foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label)@php $target=$data['pagination']['page']+$delta; @endphp @if($target>=1&&$target<=$data['pagination']['last_page'])<a href="?{{ http_build_query(array_replace($data['query'],['page'=>$target])) }}">{{ $label }}</a>@endif @endforeach</nav></section>
<section><h2>Histórico do ensaio</h2>@foreach($data['events'] as $event)<details><summary>#{{ $event->id }} · {{ $event->kind }} · actor #{{ $event->actor_id }} · {{ $event->created_at }}</summary><p>{{ $event->reason }}</p><pre>{{ $event->previous }}</pre><pre>{{ $event->subsequent }}</pre></details>@endforeach</section>
@include('accounting.receipt-recovery')
</main><script>
const token=@json(csrf_token()),context=@json(session('company_context.token'));
for(const form of document.querySelectorAll('form[data-action]'))form.onsubmit=async e=>{e.preventDefault();const body=Object.fromEntries(new FormData(form));for(const key of ['obligation_id','proposal_id'])if(key in body)body[key]=Number(body[key]);const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}const button=form.querySelector('button');button.disabled=true;document.querySelector('#error').textContent='';try{const r=await fetch('/api/general-ledger/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':token,'X-SGF-Context':context,'Idempotency-Key':form.dataset.key},body:signature});const data=await r.json();if(!r.ok)throw new Error(data.message||'Não foi possível concluir.');location.reload();}catch(err){document.querySelector('#error').textContent=err.message+' Chave do pedido: '+form.dataset.key+'. Confira o histórico antes de repetir.';}finally{button.disabled=false;}};
</script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Correcção industrial1800→1820 QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-cost-demo="{{ $tag }}"><h1>Correcção industrial1800→1820 — QA</h1><p>Os workflows industriais validaram o original1800/10UN e a taxa sucessora51/KG para20KG:1020 material+500 trabalho+300 CIF=1820,182/UN. O residual300 permanece separado. O original1800 está lançado; nenhuma correcção industrial ou fecho inicial foi aplicado nesta demonstração.</p><p><a href="/qa/cost-enter/prepare?open=workspace">Preparar / aplicar</a> · <a href="/qa/cost-enter/validate?open=workspace">Conferir distintamente</a> · <a href="/qa/cost-enter/read?open=workspace">Consultar</a></p><ol><li>No painel conjunto, abrir o lote industrial. Conferir original1800, sucessor1820, diferença20, origens, taxa, critério e versões.</li><li>Preparador captura o par com fundamento. Validador confere as quatro linhas: estorno1800 e substituição1820.</li><li>Preparador revalida e aplica o grupo numa transacção. Consultar original, par, decisões, diário e razão com saldo industrial1820 e bruto5420 por lado; residual300 fora do diário.</li><li>Completar os percursos já autorizados de venda1000/recebimentos400+600/reversão400, fornecedor60 e correcção directa275→295.</li><li>Conferir o conjunto:11 lançamentos/23 linhas, bruto8725 por lado, cliente400/directo295/industrial1820/fornecedor60. Capturar/rever distintamente e fechar QA.</li></ol><p>Usar sessões separadas. A fixture começa com dois lançamentos originais: directo275 e industrial1800. Sem reposição automática, partes aplicadas isoladamente, cadeias adicionais ou reabertura. Nenhum movimento real, stock valorizado, folha salarial, activação de Compras ou publicação. Demos anteriores preservadas; revisão visual, carga e implantação separadas.</p></main></html>
@@ -0,0 +1,12 @@
@php $c=$i;$c['original_journal']=$i['journal'];$source=$i['source']; @endphp
@php $original=$c['original_journal']; @endphp
@if($original&&$source)<section><h2>Correcção explícita do resultado industrial lançado</h2><p>Original #{{ $original->id }}: {{ $money($original->debit_minor) }}. Resultado industrial actual validado: {{ $money($source['principal_minor']) }}. Diferença: {{ $money($source['principal_minor']-$original->debit_minor) }}.</p><p>O original permanece imutável. O par contém estorno integral e substituição; nenhuma das partes pode ser revista ou aplicada isoladamente.</p>
@if(!$closed&&$c['accounting_state']==='requires_future_correction'&&$data['permissions']['correct_prepare']&&!collect($data['correction_results'])->contains('original_journal_id',$original->id))<form data-action="correct-prepare"><input type="hidden" name="journal_id" value="{{ $original->id }}"><label>Data explícita da correcção<input type="date" name="journal_on" value="2026-10-29" required></label><label>Período aberto do original<input name="period_key" value="QA-2026-10" required></label><label>Fundamento do par<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar estorno e substituição — sem aplicar</button></form>@endif
@foreach($data['corrections'] as $group)@php $reverse=json_decode($group->reverse_snapshot,true);$replacement=json_decode($group->replacement_snapshot,true); @endphp<article><h3>Grupo #{{ $group->id }} · original #{{ $group->original_journal_id }}</h3><p>Autor #{{ $group->prepared_by }} · {{ $group->reason }} · versão exacta <code>{{ $group->revision }}</code></p><p>Estorno {{ $money($reverse['impact']['debit_minor']) }} · substituição {{ $money($replacement['impact']['debit_minor']) }} · diferença {{ $money($replacement['impact']['debit_minor']-$reverse['impact']['debit_minor']) }}.</p><h4>Estorno</h4><ul>@foreach($reverse['lines'] as $line)<li>{{ $line['account_code'] }} — D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><h4>Substituição</h4><ul>@foreach($replacement['lines'] as $line)<li>{{ $line['account_code'] }} — D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><details><summary>Fotografias do original, fonte actual, contas e período</summary><pre>{{ json_encode(['reverse'=>$reverse,'replacement'=>$replacement],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>
@foreach([$group->reverse_review,$group->replacement_review] as $review)@if($review)<p>Proposta #{{ $review->proposal_id }} · {{ $review->decision }} · revisor #{{ $review->reviewed_by }} · {{ $review->reason }}</p>@endif @endforeach
@if($group->result)<p>Par aplicado: resultado #{{ $group->result->id }}, estorno #{{ $group->result->reverse_journal_id }} e substituição #{{ $group->result->replacement_journal_id }} · autor #{{ $group->result->actor_id }} · {{ $group->result->created_at }} · {{ $group->result->reason }}</p>
@elseif(!$group->current)<p class="notice">Captura superada; conservar o histórico e conferir o grupo actual.</p>
@elseif(!$closed)
@if($data['permissions']['correct_validate']&&!$group->reverse_review&&!$group->replacement_review&&$group->prepared_by!==auth()->id())<form data-action="correct-validate"><input type="hidden" name="correction_id" value="{{ $group->id }}"><input type="hidden" name="revision" value="{{ $group->revision }}"><label>Decisão do par completo<select name="decision" required><option value="">Escolher</option><option value="approved">Aprovar as quatro linhas exactas</option><option value="rejected">Recusar o par</option></select></label><label>Fundamento independente<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar conferência do par — sem aplicar</button></form>@endif
@if($data['permissions']['correct_post']&&$group->prepared_by===auth()->id()&&$group->reverse_review?->decision==='approved'&&$group->replacement_review?->decision==='approved')<form data-action="correct-post"><input type="hidden" name="correction_id" value="{{ $group->id }}"><input type="hidden" name="revision" value="{{ $group->revision }}"><label>Fundamento da aplicação conjunta<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar o par numa transacção</button></form>@endif
@endif</article>@endforeach</section>@endif
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ciclo industrial integrado QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-cost-demo="{{ $tag }}"><h1>Resultado industrial1800 e diário conjunto QA</h1><p>Lote com material1000, trabalho500 e CIF imputado300:1800MZN,10UN,180/UN. Residual300 separado e não contabilizado. Entradas, taxas, critério e resultado validados pelos workflows; nenhum lançamento industrial inicial.</p><p><a href="/qa/cost-enter/prepare?open=workspace">Preparar / aplicar</a> · <a href="/qa/cost-enter/validate?open=workspace">Conferir distintamente</a> · <a href="/qa/cost-enter/read?open=workspace">Consultar</a></p><ol><li>No painel conjunto, abrir o lote industrial. Conferir resultado, origens, taxas, critério, versões e impacto1800 em duas contas artificiais.</li><li>Preparador captura com fundamento; validador confere a versão exacta; preparador revalida e aplica uma vez.</li><li>Completar os percursos já autorizados: factura1000, recebimentos/afectações400+600 e reversão400; fornecedor60; par de correcção de custo275→295.</li><li>Conferir9 lançamentos/19 linhas e bruto5105 por lado. Contas cliente400, custo directo295, industrial1800; fornecedor60. Residual300 fora do diário.</li><li>Capturar conferência conjunta, rever distintamente e fechar o período QA. Uma alteração de origem ou versão impede a confirmação.</li></ol><p>Na fixture inicial só o custo directo275 está lançado. Usar sessões separadas; sem reset automático. Sem stock valorizado, folha salarial, dinheiro, operações reais, activação de Compras ou publicação. Demos anteriores preservadas. Revisão visual, carga sustentada e implantação permanecem separadas.</p></main></html>
@@ -0,0 +1,17 @@
@php $i=$data['industrial'];$b=$data['book'];$closed=$data['closed'];$money=fn($n)=>number_format($n/100,2,',',' ').' MZN'; @endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Resultado industrial e diário QA</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;border:1px solid #d5e0dc;border-radius:12px;padding:24px;margin:18px 0}article{border-top:1px solid #ddd;padding:16px 0}a{color:#125a4b}label{display:block;margin:12px 0}input,textarea,select,button{font:inherit;padding:10px;max-width:95%}textarea{display:block;width:90%}pre,code{white-space:pre-wrap;overflow-wrap:anywhere}.notice{background:#fff2cc;padding:16px}</style></head><body><main>
<header><a href="/general-ledger/workspace">Painel conjunto e fecho QA</a> · <a href="/general-ledger">Diário</a> · <a href="/general-ledger/trial-balance?from_on=2026-10-01&amp;to_on=2026-10-31">Balancete e razão</a><h1>Lote industrial validado → Contabilidade QA</h1><p class="notice">Só ensaio sintético. Resultado seleccionado, residual separado, contas artificiais. Sem stock valorizado, folha salarial ou utilização real.</p></header><p id="error" role="alert"></p>
<section><h2>Perímetros compostos</h2>@foreach($i['lots'] as $l)<p><a href="?lot_id={{ $l->lot_id }}">Lote #{{ $l->lot_id }}</a> · {{ $l->period_key }}</p>@endforeach</section>
@if($i['facts'])@php $c=$i['facts']['conference'];$s=$c['snapshot']; @endphp<section><h2>Conferência actual do lote #{{ $i['lot_id'] }}</h2><p>{{ $c['lot']['reference'] }} · resultado #{{ $c['result']['id']??'pendente' }} / versão {{ $c['result']['version']??'—' }} · decisão {{ $c['decision']['decision']??'pendente' }}.</p><p>Material {{ $s['material_exact']??'indisponível' }} + trabalho {{ $s['labour_exact']??'indisponível' }} + CIF imputado {{ $s['allocated_exact']??'indisponível' }} MZN. Total seleccionado {{ $s['selected_total_exact']??'indisponível' }} MZN; {{ $s['good_units']??'—' }} UN; unitário {{ $s['unit_cost_exact']??'indisponível' }} MZN.</p><p>Residual {{ $s['residual_exact']??'indisponível' }} MZN separado; sem lançamento do residual.</p>@if($i['pending'])<p class="notice">{{ $i['pending'] }}</p>@endif @if($i['accounting_state']==='requires_future_correction')<p class="notice">A origem difere do resultado já lançado. Não reconhecer novamente. @if(config('industrial_ledger.correction_enabled')===true)Exige correcção explícita do par; o fecho fica impedido até à resolução.@else Correcção industrial futura por autorizar; o fecho fica impedido.@endif</p>@endif<details><summary>Entradas, taxas, critério, decisões, versões e resultado</summary><pre>{{ json_encode($i['facts'],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details>
@if($i['source'])<p>Impacto: D QA-CUSTO-INDUSTRIAL {{ $money($i['source']['principal_minor']) }} / C QA-COMPENSACAO-INDUSTRIAL {{ $money($i['source']['principal_minor']) }}. Sem outra dívida ou movimento de stock.</p>@if(!$closed&&!$i['journal']&&$b['permissions']['prepare'])<form data-action="effect-prepare"><input type="hidden" name="effect_type" value="industry.cost.validated"><input type="hidden" name="source_id" value="{{ $i['lot_id'] }}"><input type="hidden" name="rule_key" value="QA-INDUSTRIAL"><label>Data explícita<input type="date" name="journal_on" value="2026-10-29" required></label><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar fonte e impacto sem lançar</button></form>@endif @endif</section>@endif
@if(config('industrial_ledger.correction_enabled')===true)@include('accounting.industrial-correction')@endif
<section><h2>Conferência individual do impacto</h2>@foreach($b['proposals'] as $p)@php $snap=json_decode($p->snapshot,true);$isPart=in_array($snap['source']['effect_type'],['industry.cost.reversal','industry.cost.replacement'],true);$r=collect($b['reviews'])->firstWhere('proposal_id',$p->id); @endphp<article><h3>Proposta #{{ $p->id }} · captura {{ $p->capture_version }}</h3><p>Preparador #{{ $p->prepared_by }} · {{ $p->reason }} · revisão <code>{{ $p->revision }}</code></p><ul>@foreach($snap['lines'] as $line)<li>{{ $line['account_code'] }} · D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><details><summary>Fotografia capturada e impacto</summary><pre>{{ $p->snapshot }}</pre></details>@if($r)<p>{{ $r->decision }} · autor #{{ $r->reviewed_by }} · {{ $r->reason }}</p>@endif
@if(!$closed&&!$isPart&&!$r&&$b['permissions']['validate']&&$p->prepared_by!==auth()->id())<form data-action="validate"><input type="hidden" name="proposal_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Decisão<select name="decision" required><option value="">Escolher</option><option value="approved">Aprovar versão exacta</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar conferência independente</button></form>@endif
@if(!$closed&&!$isPart&&$r?->decision==='approved'&&!$p->journal_id&&$b['permissions']['post']&&$p->prepared_by===auth()->id())<form data-action="post"><input type="hidden" name="proposal_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar uma vez</button></form>@endif</article>@endforeach</section>
<section><h2>Aplicações e histórico</h2>@if($closed)<p>Período QA encerrado; consulta preservada, sem reabertura.</p>@endif<p>Débitos {{ $money($b['totals']['debit_minor']) }} · créditos {{ $money($b['totals']['credit_minor']) }}.</p>@foreach($b['journals'] as $j)<article>Lançamento #{{ $j->id }} · {{ $j->journal_on }} · aplicador #{{ $j->posted_by }} · proposta #{{ $j->proposal_id }} / revisão #{{ $j->review_id }} · {{ $j->reason }}</article>@endforeach @foreach($b['events'] as $e)<details><summary>#{{ $e->id }} · {{ $e->kind }} · autor #{{ $e->actor_id }}</summary><p>{{ $e->reason }}</p><pre>{{ $e->previous }}</pre><pre>{{ $e->subsequent }}</pre></details>@endforeach</section>
@php $last=max($i['pagination']['last_page'],$b['pagination']['last_page'],...array_column($b['evidence_pagination'],'last_page'),...array_column($data['pagination'],'last_page'));$page=$b['pagination']['page']; @endphp<nav aria-label="Páginas das evidências industriais">Página {{ $page }} de {{ $last }} · listas de25 registos @foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label)@if($page+$delta>=1&&$page+$delta<=$last)<a href="?{{ http_build_query(array_filter(['lot_id'=>$i['lot_id'],'page'=>$page+$delta],fn($v)=>$v!==null)) }}">{{ $label }}</a>@endif @endforeach</nav>
@include('accounting.receipt-recovery')
</main><script>
const csrf=@json(csrf_token()),context=@json(session('company_context.token'));
for(const form of document.querySelectorAll('[data-action]'))form.onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(form));for(const k of ['source_id','proposal_id','journal_id','correction_id'])if(k in data)data[k]=Number(data[k]);const body=JSON.stringify(data);if(form.dataset.body!==body){form.dataset.body=body;form.dataset.key=crypto.randomUUID();}const button=form.querySelector('button');button.disabled=true;document.querySelector('#error').textContent='';try{const r=await fetch('/api/general-ledger/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,'Idempotency-Key':form.dataset.key},body});const result=await r.json();if(!r.ok)throw Error(result.message||'Conferência recusada.');location.reload();}catch(err){document.querySelector('#error').textContent=err.message+' Chave do pedido: '+form.dataset.key+'. Confira novamente a origem, versões e histórico.';}finally{button.disabled=false;}};
</script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Percurso financeiro composto QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-cost-demo="{{ $tag }}"><h1>Percurso financeiro composto QA</h1><p>Uma empresa, unidade, diário e período artificiais. A fixture inicial contém factura de teste1000, obrigação de fornecedor60 e custo original275 lançado, com sucessor295 validado. Não contém abertura de recebíveis, contabilização da venda/fornecedor, correcção ou fecho conjuntos.</p><p><a href="/qa/cost-enter/prepare?open=workspace">Preparar e aplicar</a> · <a href="/qa/cost-enter/validate?open=workspace">Conferir distintamente</a> · <a href="/qa/cost-enter/read?open=workspace">Consultar</a></p><ol><li>Abra o painel e cada origem. No percurso Comercial, abra a obrigação documental e contabilize a factura1000 com revisão distinta.</li><li>Prepare, confirme e afecte os recebimentos400 e600. Contabilize cada afectação; peça e aprove a reversão400 e contabilize-a. Saldo final400.</li><li>Conferir fornecedor60 e aplicar o seu reconhecimento uma vez no diário.</li><li>Na ordem de oficina, conferir original275 e sucessor295; capturar, rever e aplicar o par indivisível de correcção.</li><li>Consultar diário8 lançamentos/17 linhas, bruto3305 por lado, contas cliente400/custo295. Capturar, rever distintamente e fechar o período conjunto.</li></ol><p>Use sessões separadas. Sem reposição automática. Contas, imposto documental e actores artificiais. Sem dinheiro, utilização real, activação de Compras ou publicação. Revisão visual e implantação continuam pendentes.</p></main></html>
@@ -0,0 +1,29 @@
@php
$closed=$data['period']?->status==='closed';
$money=fn($n)=>number_format($n/100,2,',',' ').' MZN';
$pager=function($key,$p)use($data){$q=$data['query'];$links=[];foreach([-1=>'Anterior',1=>'Seguinte'] as $d=>$label)if($p['page']+$d>=1&&$p['page']+$d<=$p['last_page'])$links[]=['label'=>$label,'url'=>'?'.http_build_query(array_merge($q,[$key=>$p['page']+$d]))];return $links;};
@endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Percurso financeiro composto QA</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;border:1px solid #d5e0dc;border-radius:12px;padding:24px;margin:18px 0}article{border-top:1px solid #ddd;padding:16px 0}a{color:#125a4b}label{display:block;margin:12px 0}textarea,select,button{font:inherit;padding:10px;max-width:95%}textarea{display:block;width:90%}pre,code{white-space:pre-wrap;overflow-wrap:anywhere}.notice{background:#fff2cc;padding:16px}nav a{margin-right:20px}</style></head><body><main>
<header><a href="/qa/cost-demo">Perfis e guia QA</a> · <a href="/general-ledger">Diário detalhado</a> · <a href="/general-ledger/trial-balance?from_on=2026-10-01&amp;to_on=2026-10-31">Balancete e razão</a><h1>Vendas, fornecedores e custos no mesmo diário</h1><p class="notice">Exclusivamente sintético. Empresa #{{ $data['scope']['company_id'] }} / unidade #{{ $data['scope']['unit_id'] }}. Sem emissão fiscal, dinheiro ou utilização real.</p><p>Os cartões mostram evidências registadas. Abra cada origem para conferir o estado actual, dependências, versões e impacto antes de decidir. Nenhuma associação ou operação é executada ao consultar este painel.</p></header><p id="error" role="alert"></p>
@foreach(array_merge(['sales'=>'Comercial e recebíveis','costs'=>'Oficina e custos directos','suppliers'=>'Obrigações a fornecedores'],config('industrial_ledger.synthetic_enabled')===true?['industrial'=>'Lote industrial e CIF seleccionado']:[]) as $kind=>$label)
@php $panel=$data['panels'][$kind]; @endphp
<section><h2>{{ $label }}</h2>@if(!$panel['allowed'])<p>Consulta da origem indisponível para este perfil.</p>@else
@forelse($panel['rows'] as $row)<article>
@if($kind==='sales')<h3><a href="/general-ledger/sales?invoice_id={{ $row['invoice_id'] }}">Factura {{ $row['number'] }}</a></h3><p>Abertura financeira confirmada: {{ $row['confirmed_opening']?'sim':'não' }}. Lançamento original: {{ $row['original_journal_id']??'pendente' }}.</p>
@elseif($kind==='costs')<h3><a href="/general-ledger/costs?order_id={{ $row['order_id'] }}">Ordem #{{ $row['order_id'] }}</a></h3><p>Cálculo #{{ $row['calculation_id']??'pendente' }} / versão {{ $row['calculation_version']??'—' }}. Decisão registada: {{ $row['recorded_decision']??'pendente' }}. Original #{{ $row['original_journal_id']??'pendente' }}; resultado de correcção #{{ $row['recorded_correction_result_id']??'pendente' }}.</p><p>Conferência actual das dependências exigida na origem.</p>
@elseif($kind==='industrial')<h3><a href="/general-ledger/industrial?lot_id={{ $row['lot_id'] }}">Lote #{{ $row['lot_id'] }}</a></h3><p>Resultado #{{ $row['result_id']??'pendente' }} / versão {{ $row['result_version']??'—' }} · decisão registada {{ $row['recorded_decision']??'pendente' }} · lançamento #{{ $row['original_journal_id']??'pendente' }}. Conferir dependências actuais na origem; residual separado.</p>
@else<h3><a href="/general-ledger?obligation_id={{ $row['obligation_id'] }}">Fornecedor · {{ $row['document_number'] }}</a></h3><p>Principal reconhecido {{ $money($row['principal_minor']) }}; este valor não representa saldo actual. Lançamento original #{{ $row['original_journal_id']??'pendente' }}.</p><a href="/general-ledger/flow?source={{ $row['source_id'] }}">Conferir origem e obrigação</a>@endif
</article>@empty<p>Nenhum registo neste contexto e página.</p>@endforelse
@php $key=['sales'=>'invoices_page','costs'=>'costs_page','suppliers'=>'suppliers_page','industrial'=>'industrial_page'][$kind]; @endphp<nav aria-label="Páginas de {{ $label }}"><p>Página {{ $panel['pagination']['page'] }} de {{ $panel['pagination']['last_page'] }} · {{ $panel['pagination']['total'] }} registos · 25 por página</p>@foreach($pager($key,$panel['pagination']) as $link)<a href="{{ $link['url'] }}">{{ $link['label'] }}</a>@endforeach</nav>@endif</section>@endforeach
<section><h2>Diário conjunto</h2><p>Débitos {{ $money($data['book']['totals']['debit_minor']) }} · créditos {{ $money($data['book']['totals']['credit_minor']) }}.</p>@foreach($data['book']['journals'] as $j)@php $source=json_decode($j->snapshot,true)['source']; @endphp<article><p>Lançamento #{{ $j->id }} · {{ $source['effect_type'] }} · {{ $j->journal_on }} · {{ $money($j->debit_minor) }}</p><p>Proposta #{{ $j->proposal_id }} · revisão #{{ $j->review_id }} · autor #{{ $j->posted_by }} · {{ $j->reason }}</p></article>@endforeach<nav aria-label="Páginas do diário">Página {{ $data['book']['pagination']['page'] }} de {{ $data['book']['pagination']['last_page'] }} @foreach($pager('journals_page',$data['book']['pagination']) as $link)<a href="{{ $link['url'] }}">{{ $link['label'] }}</a>@endforeach</nav></section>
<section><h2>Conferência e fecho conjunto</h2><p>Período {{ $data['period']?->period_key }} · {{ $data['period']?->status }}. A confirmação revalida todas as fontes compostas, incluindo as que não aparecem nesta página. Uma pendência impede o fecho.</p>@if($closed)<p class="notice">Período QA encerrado. Consulta preservada, sem reabertura.</p>@elseif(!$data['all_current_source_views'])<p>Este perfil não pode conferir todas as origens. Não há acções de fecho neste painel.</p>@endif
@if(!$closed&&$data['all_current_source_views']&&$data['period_permissions']['period_prepare'])<form data-action="period-prepare"><input type="hidden" name="period_key" value="{{ $data['period']->period_key }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar evidências e impacto do período</button></form>@endif
@foreach($data['checks']['rows'] as $item)@php $p=(object)$item['capture'];$r=$item['review']; @endphp<article><h3>Conferência #{{ $p->id }}</h3><p>Preparador #{{ $p->prepared_by }} · {{ $p->reason }} · versão <code>{{ $p->revision }}</code></p><details><summary>Evidências e impacto capturados</summary><pre>{{ $p->snapshot }}</pre></details>@if($r)<p>{{ $r->decision }} · autor #{{ $r->actor_id }} · {{ $r->reason }}</p>@endif
@if(!$closed&&$data['all_current_source_views']&&!$r&&$data['period_permissions']['period_validate']&&$p->prepared_by!==auth()->id())<form data-action="period-validate"><input type="hidden" name="check_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Decisão<select name="decision" required><option value="">Escolher</option><option value="approved">Aprovar versão exacta</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar conferência independente</button></form>@endif
@if(!$closed&&$data['all_current_source_views']&&$r?->decision==='approved'&&$data['period_permissions']['period_close']&&$p->prepared_by===auth()->id())<form data-action="period-close"><input type="hidden" name="check_id" value="{{ $p->id }}"><input type="hidden" name="revision" value="{{ $p->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e fechar este período QA</button></form>@endif
@if($item['closure'])<p>Fecho #{{ $item['closure']->id }} · {{ $item['closure']->reason }}</p>@endif</article>@endforeach<nav aria-label="Páginas das conferências">Página {{ $data['checks']['pagination']['page'] }} de {{ $data['checks']['pagination']['last_page'] }} @foreach($pager('checks_page',$data['checks']['pagination']) as $link)<a href="{{ $link['url'] }}">{{ $link['label'] }}</a>@endforeach</nav></section>
@include('accounting.receipt-recovery')
</main><script>
const csrf=@json(csrf_token()),context=@json(session('company_context.token'));
for(const form of document.querySelectorAll('[data-action]'))form.onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(form));if('check_id' in data)data.check_id=Number(data.check_id);const body=JSON.stringify(data);if(form.dataset.body!==body){form.dataset.body=body;form.dataset.key=crypto.randomUUID();}const button=form.querySelector('button');button.disabled=true;document.querySelector('#error').textContent='';try{const r=await fetch('/api/general-ledger/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,'Idempotency-Key':form.dataset.key},body});const result=await r.json();if(!r.ok)throw Error(result.message||'Conferência recusada.');location.reload();}catch(err){document.querySelector('#error').textContent=err.message+' Chave do pedido: '+form.dataset.key+'. Abra novamente as origens e confira as versões antes de repetir.';}finally{button.disabled=false;}};
</script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ciclo contabilístico integrado — QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-ledger-demo="{{ $tag }}"><h1>Ciclo financeiro e contabilístico sintético</h1><p>Obrigação #{{ $obligation }} de60MZN já reconhecida financeiramente. Nenhum lançamento ou pagamento nesta fixture inicial. Contas e período artificiais, sem uso real.</p><p><a href="/qa/ledger-enter/prepare?open=cycle">Preparar / aplicar</a> · <a href="/qa/ledger-enter/validate?open=cycle">Rever com outra pessoa</a> · <a href="/qa/ledger-enter/read?open=cycle">Consultar</a></p><ol><li>Capturar, rever e lançar o reconhecimento de60.</li><li>Em Compras, preparar pagamento sintético20, confirmação independente, afectação20 e sua confirmação. Saldo40.</li><li>Capturar, rever e contabilizar a afectação confirmada. O lançamento não volta a abater a dívida.</li><li>Preparar e confirmar reversão integral da afectação; saldo60. Conferir e contabilizar a reversão.</li><li>Capturar par de correcção técnica do reconhecimento original; revisão independente e aplicação conjunta de estorno60/substituição60.</li><li>Capturar conferência do períodoQA-2026-10, rever com outra pessoa e fechar. Novas gravações nesse período são recusadas.</li></ol><p>Referências sintéticas únicas SIM-… e data29/10/2026. Registos e decisões são conservados; sem reset automático. Não há reabertura, execução monetária ou reconciliação bancária. Revisão visual e implantação continuam separadas.</p></main></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Percurso integrado SGF — QA</title><main style="font:18px system-ui;max-width:900px;margin:40px auto;padding:24px" data-ledger-demo="{{ $tag }}"><h1>Da conferência financeira ao lançamento</h1><p>Ensaio integrado de 60 MZN. Origem local #{{ $source }}, duas contas e período artificiais. Começa sem obrigação reconhecida ou lançamento. As decisões da equipa ficam conservadas; não há reposição automática.</p><p><a href="/qa/ledger-enter/prepare?open=flow">Preparar / aplicar</a> · <a href="/qa/ledger-enter/validate?open=flow">Rever com outra identidade</a> · <a href="/qa/ledger-enter/read?open=flow">Consultar evidências</a></p><ol><li>Preparador abre a conferência e prepara a proposta financeira.</li><li>Revisor confere a mesma origem e reconhece a obrigação.</li><li>Preparador abre o percurso e captura a proposta contabilística.</li><li>Revisor aprova ou recusa a fotografia exacta e o impacto.</li><li>Preparador aplica uma vez; consulta mostra origem, posição, lançamento e extracto.</li></ol><p>Data explícita 29/10/2026. Sem pagamento, alteração de stock ou orçamento. Recibo documental sintético, sem ligação viva a Compras. Revisão visual e implantação pendentes.</p></main></html>
@@ -0,0 +1,26 @@
@php
$money=static fn($n)=>$n===null?'Indeterminado':($n<0?'-':'').number_format(intdiv(abs($n),100),0,',',' ').','.str_pad((string)(abs($n)%100),2,'0',STR_PAD_LEFT).' MZN';
$source=$flow['source_id'];$position=$flow['position'];$proposal=$flow['proposal'];$review=$flow['review'];$journal=$flow['journal'];
@endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Conferência até ao lançamento — SGF</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28}main{max-width:1000px;margin:auto;padding:24px}section,header{background:white;border:1px solid #d5e0dc;padding:24px;margin:18px 0;border-radius:12px}button{font:inherit;padding:10px;cursor:pointer}pre{white-space:pre-wrap;overflow-wrap:anywhere}.notice{padding:16px;background:#fff2cc}a{color:#125a4b}</style></head><body><main>
<header><a href="/general-ledger">Diário e extracto</a> · <a href="/">Navegação SGF</a><h1>Conferência → obrigação → lançamento</h1><p class="notice">Demonstração sintética. Cada decisão mantém o seu autor, fundamento e revisão. Abrir uma etapa não reconhece uma obrigação nem aplica um lançamento.</p><p>Empresa #{{ $flow['scope']['company_id'] }} / Unidade #{{ $flow['scope']['unit_id'] }} · Origem local #{{ $source }}</p>@if(session('ledger_demo'))<a href="/qa/ledger-demo">Trocar função sintética</a>@endif</header>
<p id="flow-error" role="alert"></p>
<section><h2>1. Documento e conferência financeira</h2><p>{{ $flow['document']['number'] }} · {{ $money($flow['document']['total_minor']) }}</p><p>A origem desta demonstração é um recibo sintético. Não comprova ligação operacional viva a Compras. Consulte a fotografia, pendências e propostas antes de decidir.</p><button data-module="purchasing" data-path="/supplier-payables?source={{ $source }}">Abrir conferência desta origem</button><details><summary>Fotografia documental</summary><pre>{{ json_encode($flow['document'],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details></section>
<section><h2>2. Reconhecimento independente e posição financeira</h2>
@if($position)<p>Obrigação #{{ $position['id'] }} · Principal {{ $money($position['principal_minor']) }} · Saldo {{ $money($position['balance_minor']) }}</p><p>Vencimento {{ $position['due_on']??'desconhecido' }}. O lançamento contabilístico não reduz o saldo.</p>@if($flow['source_conflict'])<p role="alert">Origem divergente. Exige nova conferência; a existência de uma proposta contabilística não resolve esta pendência.</p>@endif
@else<p>Ainda não reconhecida. Um documento ou uma proposta preparados não são uma obrigação validada. A contabilidade recusará uma origem sem reconhecimento.</p>@endif
<button data-module="purchasing" data-path="/supplier-payables/report?source={{ $source }}">Conferir posição financeira e totais</button>
@foreach($flow['financial_proposals'] as $p)<details><summary>Proposta financeira #{{ $p->id }} · captura {{ $p->capture_version }}</summary><p>{{ $p->reason }}</p><pre>{{ $p->snapshot }}</pre></details>@endforeach
@foreach($flow['financial_events'] as $event)<details><summary>Evidência #{{ $event->id }} · {{ $event->kind }} · autor #{{ $event->actor_id }} · {{ $event->created_at }}</summary><pre>{{ $event->payload }}</pre></details>@endforeach</section>
<section><h2>3. Preparação e revisão contabilística</h2>
@if($proposal)<p>Proposta #{{ $proposal->id }} · captura {{ $proposal->capture_version }} · autor #{{ $proposal->prepared_by }}</p><p>Revisão exacta <code>{{ $proposal->revision }}</code></p><p>{{ $review ? ($review->decision==='approved'?'Aprovada':'Recusada').' por #'.$review->reviewed_by : 'Conferência independente pendente' }}</p>
@else<p>Sem proposta contabilística. A preparação exige uma obrigação reconhecida e configuração compatível.</p>@endif
@if($position)<a href="/general-ledger?obligation_id={{ $position['id'] }}">Abrir proposta e impacto desta obrigação</a>@endif</section>
<section><h2>4. Aplicação única e extracto</h2>
@if($journal)<p>Lançamento #{{ $journal->id }} · {{ $journal->journal_on }} · Débito {{ $money($journal->debit_minor) }} / Crédito {{ $money($journal->credit_minor) }}</p><p>Aplicador #{{ $journal->posted_by }} · {{ $journal->created_at }} · {{ $journal->reason }}</p><a href="/general-ledger?obligation_id={{ $journal->obligation_id }}">Conferir comprovativo, linhas e histórico</a>
@else<p>Nenhum lançamento aplicado. A aprovação da proposta não o aplica automaticamente.</p>@endif</section>
<p>Revisão visual, carga de implantação, contas reais e composição autónoma contabilística continuam separadas. Esta página consulta o estado actual; as fotografias aplicadas conservam o histórico.</p>
</main><script>
const scope=@json($flow['scope']),token=@json(csrf_token());
for(const button of document.querySelectorAll('[data-module]'))button.onclick=async()=>{button.disabled=true;try{const r=await fetch('/api/context',{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':token},body:JSON.stringify({...scope,module_id:button.dataset.module})});const data=await r.json();if(!r.ok)throw Error(data.message||'Não foi possível seleccionar o módulo.');location.assign(button.dataset.path);}catch(e){document.querySelector('#flow-error').textContent=e.message;button.disabled=false;}};
</script></body></html>
@@ -0,0 +1,17 @@
<section aria-labelledby="receipt-title"><h2 id="receipt-title">Consultar comprovativo de um pedido</h2><p>Se perdeu a resposta, consulte a chave apresentada pela operação. Esta consulta mostra apenas comprovativos seus no contexto actual e não executa operações. A ausência de comprovativo nesta captura não prova falha nem autoriza repetição; confira o estado e as evidências antes de reenviar.</p><form id="receipt-lookup"><label>Domínio<select name="domain"><option value="ledger">Contabilidade</option><option value="recognition">Reconhecimento de fornecedor</option><option value="settlement">Liquidação de fornecedor</option></select></label><label>Chave do pedido<input name="command_key" required maxlength="36" pattern="[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" autocomplete="off"></label><button>Consultar sem executar</button></form><p id="receipt-status" role="status"></p><pre id="receipt-result"></pre></section>
<script>(()=>{
const form=document.getElementById('receipt-lookup'),status=document.getElementById('receipt-status'),result=document.getElementById('receipt-result'),button=form.querySelector('button');
let generation=0,active=null;
const invalidate=()=>{generation++;active?.abort();active=null;result.textContent='';status.textContent='Selecção alterada; consulte novamente a chave no domínio pretendido.';button.disabled=false;};
form.addEventListener('input',invalidate);form.addEventListener('change',invalidate);
form.onsubmit=async event=>{
event.preventDefault();active?.abort();const serial=++generation,controller=new AbortController();active=controller;
const key=form.elements.command_key.value.trim(),domain=form.elements.domain.value,current=()=>generation===serial&&form.elements.command_key.value.trim()===key&&form.elements.domain.value===domain;
button.disabled=true;status.textContent='A consultar…';result.textContent='';
try{const url=domain==='ledger'?'/api/general-ledger/receipts/'+encodeURIComponent(key):'/api/general-ledger/financial-receipts/'+encodeURIComponent(domain)+'/'+encodeURIComponent(key);
const response=await fetch(url,{credentials:'same-origin',signal:controller.signal,headers:{Accept:'application/json','X-SGF-Context':@json(session('company_context.token'))}});
if(!current())return;const data=await response.json();if(!current())return;
if(!response.ok)throw Error(data.message||'Consulta recusada.');status.textContent='Comprovativo histórico encontrado. Nenhuma operação foi executada por esta consulta.';result.textContent=JSON.stringify(data,null,2);
}catch(error){if(current()&&error.name!=='AbortError')status.textContent=error.message+' Confira o contexto e as evidências antes de repetir a operação.';}
finally{if(generation===serial){button.disabled=false;active=null;}}
};})();</script>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ciclo Comercial — QA</title><main style="font:18px system-ui;max-width:950px;margin:40px auto;padding:24px" data-sales-demo="{{ $tag }}"><h1>Comercial, recebíveis e contabilidade — ensaio integrado</h1><p>Factura de teste #{{ $invoice }} efectivamente emitida: base862,07, imposto documental137,93, total1000MZN. Sem validade fiscal ou movimentação de dinheiro. A emissão e as suas revisões ficam preservadas; abertura da dívida, recebimentos e lançamentos ainda exigem decisões.</p><p><a href="/qa/sales-enter/prepare?open=sales">Preparar / aplicar</a> · <a href="/qa/sales-enter/validate?open=sales">Rever com outra pessoa</a> · <a href="/qa/sales-enter/read?open=sales">Consultar</a></p><ol><li>Preparar abertura da obrigação1000; outra pessoa confirma.</li><li>Capturar três linhas contabilísticas; rever e aplicar uma vez.</li><li>Preparar recebimento400, confirmar independentemente e afectar400. Saldo600; conferir e contabilizar a afectação.</li><li>Repetir com600. Saldo0; contabilizar essa afectação.</li><li>Preparar reversão integral da primeira afectação400; outra pessoa aprova. Saldo400; conferir e contabilizar a reversão.</li><li>Conferir o períodoQA-2026-10, obter revisão distinta e fechar. Conta de clientes e posição financeira devem indicar400.</li></ol><p>Data explícita29/10/2026, contas artificiais. Não há reset ou reabertura. Demos e provas anteriores ficam separadas. Revisão visual e implantação permanecem pendentes.</p></main></html>
@@ -0,0 +1,47 @@
@php
$c=$data['commercial'];$inv=$c['invoice'];$id=$inv['id']??null;$p=$c['position']??null;$ob=$c['obligation_id']??null;
$money=static fn($n)=>($n<0?'-':'').number_format(intdiv(abs($n),100),0,',',' ').','.str_pad((string)(abs($n)%100),2,'0',STR_PAD_LEFT).' MZN';
$fp=$c['finance_permissions']??[];
$periodClosed=(collect($data['periods'])->where('period_key','QA-2026-10')->sortByDesc('version')->first()?->status)==='closed';
if($periodClosed){$fp=array_fill_keys(array_keys($fp),false);$data['permissions']=array_fill_keys(array_keys($data['permissions']),false);}
@endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Comercial, recebíveis e contabilidade — QA</title><style>body{font:17px system-ui;background:#f3f6f5;color:#142b28}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;border:1px solid #d5e0dc;border-radius:12px;padding:24px;margin:18px 0}article{border-top:1px solid #ddd;padding:18px 0}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:10px;max-width:95%}textarea{display:block;width:90%}.notice{padding:16px;background:#fff2cc}pre{white-space:pre-wrap;overflow-wrap:anywhere}code{overflow-wrap:anywhere}a{color:#125a4b}</style></head><body><main>
<header><a href="/general-ledger">Diário e extracto</a> · <a href="/">Navegação SGF</a>@if(session('sales_demo')) · <a href="/qa/sales-demo">Trocar função sintética</a>@endif<h1>Comercial → Recebíveis → Contabilidade</h1><p class="notice">TESTE — SEM VALIDADE FISCAL. Sem movimentação de dinheiro. Contas e período artificiais, sem reabertura. As decisões da emissão e da dívida permanecem separadas do lançamento.</p><p>Empresa #{{ $c['scope']['company_id'] }} / Unidade #{{ $c['scope']['unit_id'] }}. As operações financeiras abaixo seleccionam explicitamente Financeiro, usam os seus comandos existentes e regressam a Contabilidade. Cada envio exige fundamento; mudança de contexto invalida separadores antigos.</p></header><p id="error" role="alert"></p>
<section><h2>Escolher documento emitido</h2><form method="get"><label>Factura de teste<select name="invoice_id" required><option value="">Escolha uma origem</option>@foreach($c['invoices'] as $i)<option value="{{ $i->id }}" @selected($i->id===$id)>#{{ $i->id }} · {{ $i->series_code }}/{{ $i->number }}</option>@endforeach</select></label><button>Conferir esta origem</button></form></section>
@if($periodClosed)<p class="notice">Período QA encerrado. Este percurso conserva a consulta e os comprovativos; não propõe novos efeitos para o período fechado. Não existe reabertura ou reposição automática do exemplo.</p>@endif
@if($inv)
<section><h2>1. Documento original e cálculo conservado</h2><p>Factura #{{ $id }} · Base {{ $inv['snapshot']['calculation']['base_exact'] }} · Imposto documental {{ $inv['snapshot']['calculation']['vat_document_proposed'] }} · Total {{ $inv['snapshot']['calculation']['total_document_proposed'] }} MZN.</p><p>IVA exacto de teste {{ $inv['snapshot']['calculation']['vat_exact'] }}; diferença conservada {{ $inv['snapshot']['calculation']['discarded_difference'] }}. Não se recalcula o imposto ao contabilizar.</p><button id="pdf">Seleccionar Comercial e obter PDF original de teste</button><a id="pdf-link" hidden target="_blank" rel="noopener">Abrir PDF de teste obtido</a><details><summary>Fotografia de emissão, pressupostos e versões</summary><pre>{{ json_encode($inv['snapshot'],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details></section>
<section><h2>2. Obrigação e recebimentos sintéticos</h2>
@if($p)<p>Obrigação #{{ $ob }} · Principal {{ $p['original'] }} MZN · Saldo {{ $p['balance'] }} MZN · Recebimentos por afectar {{ $p['available'] }} MZN · revisão {{ $p['revision'] }}</p><p>Vencimento {{ $p['due_date']??'desconhecido' }}. As posições vêm do núcleo financeiro existente; um lançamento não abate o saldo novamente.</p>
@if(!$p['opened'])<p role="alert">Esta é uma proposta de abertura ainda sem confirmação. O montante proposto não constitui dívida aprovada; o reconhecimento contabilístico fica bloqueado.</p>@endif
@else<p>Nenhuma obrigação confirmada. Primeiro preparar a abertura e obter decisão independente; o documento sozinho não aprova a dívida.</p>@endif
@if(($fp['prepare']??false)&&!$ob)<form data-finance="propose"><input type="hidden" name="kind" value="obligation"><input type="hidden" name="revision" value="0"><label>Fundamento da abertura<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e preparar abertura1000 — sem confirmar</button></form>@endif
@if(($fp['prepare']??false)&&($p['opened']??false))<form data-finance="propose"><input type="hidden" name="kind" value="receipt"><input type="hidden" name="obligation_id" value="{{ $ob }}"><input type="hidden" name="revision" value="{{ $p['revision'] }}"><label>Montante do recebimento<input name="amount" required pattern="[0-9]+([.][0-9]{1,2})?" placeholder="400.00 ou 600.00"></label><label>Data explícita<input name="date" type="date" value="2026-10-29" required></label><label>Evidência sintética<textarea name="evidence" required minlength="3" maxlength="1000"></textarea></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e propor recebimento — sem confirmar</button></form>@endif
@foreach($c['records'] as $r)<article><h3>Registo financeiro #{{ $r->id }} · {{ $r->kind }} · {{ $r->amount }} MZN</h3><p>Autor #{{ $r->actor_id }} · {{ $r->created_at }} · {{ $r->decision_kind??'Decisão pendente' }}</p><details><summary>Origem, fundamento e versão</summary><pre>{{ $r->data }}</pre></details>
@if(!$r->decision_kind&&($r->kind==='correction'?($fp['correction_approve']??false):($fp['confirm']??false))&&$r->actor_id!==auth()->id())<form data-finance="decision" data-id="{{ $r->id }}"><input type="hidden" name="version" value="1"><input type="hidden" name="revision" value="{{ $p['revision']??0 }}"><label>Decisão independente<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="refused">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e registar decisão exacta</button></form>@endif
@if($r->kind==='receipt'&&($fp['allocate']??false)&&(($p['receipts'][$r->id]['state']??'')==='confirmed'))<p>Disponível deste recebimento: {{ $p['receipts'][$r->id]['available'] }} MZN.</p><form data-finance="allocate" data-id="{{ $r->id }}"><input type="hidden" name="version" value="1"><input type="hidden" name="revision" value="{{ $p['revision'] }}"><label>Montante explícito a afectar<input name="amount" required placeholder="400.00"></label><button>Seleccionar Financeiro e afectar este montante</button></form>@endif
</article>@endforeach</section>
<section><h2>3. Fontes contabilísticas e reversão financeira</h2>
@if(($p['opened']??false)&&$data['permissions']['prepare'])<form data-ledger="effect-prepare"><input type="hidden" name="effect_type" value="sales.invoice"><input type="hidden" name="source_id" value="{{ $id }}"><input type="hidden" name="rule_key" value="QA-SALES"><label>Data explícita<input name="journal_on" type="date" value="2026-10-29" required></label><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar o reconhecimento da factura em três linhas — sem lançar</button></form>@endif
@foreach($c['effects'] as $e)@if(in_array($e->kind,['allocate','reverse_allocation']))<article><h3>Efeito #{{ $e->id }} · {{ $e->kind }} · {{ $e->amount }} MZN</h3><p>Registo #{{ $e->record_id }} · autor #{{ $e->actor_id }} · {{ $e->created_at }}</p>
@if($data['permissions']['prepare'])<form data-ledger="effect-prepare"><input type="hidden" name="effect_type" value="{{ $e->kind==='allocate'?'receivable.allocated':'receivable.reversed' }}"><input type="hidden" name="source_id" value="{{ $e->id }}"><input type="hidden" name="rule_key" value="{{ $e->kind==='allocate'?'QA-RECEIVABLE':'QA-RECEIVABLE-REVERSE' }}"><label>Data explícita<input name="journal_on" type="date" value="2026-10-29" required></label><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar este efeito confirmado — sem lançar</button></form>@endif
@if($e->kind==='allocate'&&($fp['correction_request']??false))<form data-finance="propose"><input type="hidden" name="kind" value="correction"><input type="hidden" name="action" value="reverse_allocation"><input type="hidden" name="target_effect_id" value="{{ $e->id }}"><input type="hidden" name="obligation_id" value="{{ $ob }}"><input type="hidden" name="revision" value="{{ $p['revision'] }}"><label>Fundamento da reversão integral<textarea name="reason" required minlength="3" maxlength="1000"></textarea></label><button>Seleccionar Financeiro e propor reversão — sem aplicar</button></form>@endif</article>@endif @endforeach</section>
<section><h2>4. Conferência e aplicação contabilística</h2>@foreach($data['book']['proposals'] as $proposal)@php $snap=json_decode($proposal->snapshot,true);$review=collect($data['book']['reviews'])->firstWhere('proposal_id',$proposal->id); @endphp<article><h3>Proposta #{{ $proposal->id }} · {{ $snap['source']['effect_type'] }}</h3><p>Autor #{{ $proposal->prepared_by }} · revisão exacta <code>{{ $proposal->revision }}</code></p><p>Data {{ $snap['journal_on'] }} · regra {{ $snap['rule']['rule_key'] }} v{{ $snap['rule']['version'] }} · período v{{ $snap['period']['version'] }}.</p><ul>@foreach($snap['lines'] as $line)<li>{{ $line['account_code'] }} · D {{ $money($line['debit_minor']) }} / C {{ $money($line['credit_minor']) }}</li>@endforeach</ul><p>Impacto D/C {{ $money($snap['impact']['debit_minor']) }}; sem nova dívida ou alteração do saldo financeiro.</p><details><summary>Fotografia exacta da fonte, regra e período</summary><pre>{{ json_encode($snap,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details><p>{{ $review?'Decisão '.$review->decision.' por #'.$review->reviewed_by:'Revisão independente pendente' }} · {{ $proposal->journal_id?'Lançamento #'.$proposal->journal_id:'' }}</p>
@if($data['permissions']['validate']&&!$review&&$proposal->prepared_by!==auth()->id())<form data-ledger="validate"><input type="hidden" name="proposal_id" value="{{ $proposal->id }}"><input type="hidden" name="revision" value="{{ $proposal->revision }}"><label>Decisão<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Rever esta fotografia — sem lançar</button></form>@endif
@if($data['permissions']['post']&&$review&&$review->decision==='approved'&&!$proposal->journal_id&&$proposal->prepared_by===auth()->id())<form data-ledger="post"><input type="hidden" name="proposal_id" value="{{ $proposal->id }}"><input type="hidden" name="revision" value="{{ $proposal->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e aplicar uma vez</button></form>@endif</article>@endforeach</section>
<section><h2>5. Fecho QA, extracto e evidências</h2><p>Totais brutos desta factura: D {{ $money($data['book']['totals']['debit_minor']) }} / C {{ $money($data['book']['totals']['credit_minor']) }}. Não representam saldo por receber.</p><p>O fecho abaixo confere todos os efeitos reconhecidos da empresa/unidade, incluindo fornecedores quando existirem; não apenas a factura seleccionada.</p>@if($data['permissions']['period_prepare'])<form data-ledger="period-prepare"><label>Período<input name="period_key" value="QA-2026-10" required></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Capturar conferência completa do período</button></form>@endif
@if($data['client_account'])<p>Conta de clientes desta factura: débito menos crédito {{ $money($data['client_account']['net_minor']) }}. Compare com a posição financeira acima; efeitos confirmados ainda não lançados continuam pendentes de conferência.</p>@endif
@foreach($data['period_checks'] as $check)@php $periodReview=collect($data['period_reviews'])->firstWhere('check_id',$check->id);$closed=collect($data['period_closures'])->firstWhere('check_id',$check->id); @endphp<article><h3>Conferência #{{ $check->id }} · {{ $check->period_key }}</h3><pre>{{ $check->snapshot }}</pre><p>{{ $closed?'Fecho #'.$closed->id:($periodReview?'Revisão '.$periodReview->decision:'Revisão pendente') }}</p>
@if($data['permissions']['period_validate']&&!$periodReview&&!$closed)<form data-ledger="period-validate"><input type="hidden" name="check_id" value="{{ $check->id }}"><input type="hidden" name="revision" value="{{ $check->revision }}"><label>Decisão<select name="decision" required><option value="">Escolha</option><option value="approved">Aprovar</option><option value="rejected">Recusar</option></select></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Conferir período por outra pessoa</button></form>@endif
@if($data['permissions']['period_close']&&$periodReview&&$periodReview->decision==='approved'&&!$closed)<form data-ledger="period-close"><input type="hidden" name="check_id" value="{{ $check->id }}"><input type="hidden" name="revision" value="{{ $check->revision }}"><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Revalidar e fechar o período artificial</button></form>@endif</article>@endforeach
@foreach($data['book']['journals'] as $j)<details><summary>Lançamento #{{ $j->id }} · {{ $j->journal_on }} · D/C {{ $money((int)$j->debit_minor) }}</summary><p>Proposta #{{ $j->proposal_id }} · revisão #{{ $j->review_id }} · autor #{{ $j->posted_by }} · {{ $j->reason }}</p><pre>{{ $j->snapshot }}</pre></details>@endforeach
@foreach($data['book']['events'] as $event)<details><summary>Evidência #{{ $event->id }} · {{ $event->kind }} · autor #{{ $event->actor_id }} · {{ $event->created_at }}</summary><p>{{ $event->reason }}</p><pre>{{ $event->previous }}</pre><pre>{{ $event->subsequent }}</pre></details>@endforeach</section>
@endif
@php $pagers=$data['pagination']+['documents'=>$c['pagination']]+($id?['financial_records'=>$c['records_pagination'],'financial_effects'=>$c['effects_pagination']]+$data['book']['evidence_pagination']:[]);$current=$c['pagination']['page'];$max=max(array_column($pagers,'last_page')); @endphp<nav aria-label="Paginação das evidências"><p>Até25 registos por lista. Página {{ $current }}; maior página disponível {{ $max }}.</p>@foreach([-1=>'Anterior',1=>'Seguinte'] as $delta=>$label)@php $target=$current+$delta; @endphp @if($target>=1&&$target<=$max)<a href="?{{ http_build_query(array_filter(['invoice_id'=>$id,'page'=>$target],fn($v)=>$v!==null)) }}">{{ $label }}</a>@endif @endforeach</nav>
@include('accounting.receipt-recovery')
</main><script>
const scope=@json($c['scope']),invoice=@json($id),actor=@json(auth()->id()),csrf=@json(csrf_token());let context=@json(session('company_context.token'));
const select=async module=>{const current=await fetch('/api/session',{credentials:'same-origin',cache:'no-store',headers:{Accept:'application/json'}});const snapshot=await current.json();if(!current.ok||snapshot.user?.id!==actor||snapshot.context?.token!==context||snapshot.context?.company?.id!==scope.company_id||snapshot.context?.company?.tenant_id!==scope.tenant_id||snapshot.context?.unit?.id!==scope.unit_id)throw Error('O contexto mudou. Reabra o percurso antes de enviar esta decisão.');const r=await fetch('/api/context',{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf},body:JSON.stringify({...scope,module_id:module})});const d=await r.json();if(!r.ok)throw Error(d.message||'Módulo indisponível.');context=d.context.token;};
const pdf=document.querySelector('#pdf');if(pdf)pdf.onclick=async()=>{try{await select('commercial');const r=await fetch('/api/service-invoices/'+invoice+'/pdf',{credentials:'same-origin',headers:{'X-SGF-Context':context}});if(!r.ok)throw Error('PDF indisponivel; confira os acessos.');const blob=await r.blob();await select('accounting');const link=document.querySelector('#pdf-link');if(link.dataset.url)URL.revokeObjectURL(link.dataset.url);link.dataset.url=URL.createObjectURL(blob);link.href=link.dataset.url;link.hidden=false;}catch(e){document.querySelector('#error').textContent=e.message;try{await select('accounting');}catch{}}};
for(const form of document.querySelectorAll('[data-ledger],[data-finance]'))form.onsubmit=async e=>{e.preventDefault();let body=Object.fromEntries(new FormData(form));for(const k of ['obligation_id','source_id','proposal_id','check_id','revision','target_effect_id','version'])if(k in body&&(k!=='revision'||form.dataset.finance))body[k]=Number(body[k]);const finance=form.dataset.finance;const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}let url='/api/general-ledger/'+form.dataset.ledger;if(finance){if(finance==='propose'){body={...body,invoice_id:invoice,currency:'MZN',source:'TEST-MANUAL',external_id:form.dataset.key};url='/api/service-receivables/proposals';}else url='/api/service-receivables/'+form.dataset.id+'/'+(finance==='decision'?'decision':'allocate');}const button=form.querySelector('button');button.disabled=true;try{if(finance)await select('finance');const r=await fetch(url,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,'Idempotency-Key':form.dataset.key},body:JSON.stringify(body)});const d=await r.json();if(!r.ok)throw Error(d.message||'Estado alterado; confira a evidência.');if(finance)await select('accounting');location.reload();}catch(e){document.querySelector('#error').textContent=e.message+' Consulte o histórico antes de repetir.';if(finance){try{await select('accounting');}catch{}}}finally{button.disabled=false;}};
</script></body></html>
@@ -0,0 +1,3 @@
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Stock valorizado — consulta QA</title></head><body><main style="font:17px system-ui;max-width:1050px;margin:auto;padding:24px"><nav><a href="/">Voltar à navegação SGF</a></nav><h1>Stock valorizado — livro de origem QA</h1><p>Consulta do mesmo livro físico e contabilístico, com acesso SQL apenas de leitura. As confirmações pertencem à composição de origem; esta página não executa movimentos ou lançamentos.</p><p>Estado: {{ $data['status'] }} · Livro {{ $data['book']['id'] }} · artigo {{ $data['book']['stock_item_id'] }} · armazém {{ $data['book']['warehouse_id'] }} · {{ $data['book']['period_key'] }}</p><p>Quantidade física: {{ $data['physical_quantity_milli'] }} milésimos de {{ $data['book']['unit'] }}. Valor contabilizado de stock: {{ number_format($data['posted_stock_value_minor']/100,2,',',' ') }} MZN. Movimentos sem valorização: {{ $data['unposted_movements'] }}. Lançamentos com taxa anterior: {{ $data['stale_rate_journals'] }}.</p>@if($data['status']!=='reconciled')<p>Conferência incompleta. O valor contabilizado não é uma valorização actual confirmada de todo o stock.</p>@endif
<p>A indicação de reconciliação cobre os movimentos com lançamento e as versões de taxa. A conferência actual das restantes dependências e do período pertence à composição de origem; esta consulta não a substitui.</p><table><caption>Lançamentos de origem — página {{ $data['pagination']['page'] }}</caption><thead><tr><th>Identificador</th><th>Data</th><th>Valor débito/crédito MZN</th><th>Evidência imutável</th></tr></thead><tbody>@foreach($data['journals'] as $journal)<tr><td>{{ $journal->id }}</td><td>{{ $journal->journal_on }}</td><td>{{ number_format($journal->debit_minor/100,2,',',' ') }}</td><td><details><summary>Origem, taxa, revisão e dependências</summary><pre style="white-space:pre-wrap;overflow-wrap:anywhere">{{ json_encode(json_decode($journal->snapshot,true),JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) }}</pre></details></td></tr>@endforeach</tbody></table>
<nav>@if($data['pagination']['page']>1)<a href="?page={{ $data['pagination']['page']-1 }}&amp;per_page={{ $data['pagination']['per_page'] }}">Anterior</a>@endif @if($data['pagination']['page']*$data['pagination']['per_page']<$data['pagination']['total'])<a href="?page={{ $data['pagination']['page']+1 }}&amp;per_page={{ $data['pagination']['per_page'] }}">Seguinte</a>@endif Total {{ $data['pagination']['total'] }}</nav></main></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Localização e segmentação — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1120px;margin:auto;padding:24px}section,article{background:white;border:1px solid #bed0c9;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:8px;box-sizing:border-box;max-width:100%}button,a{margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}button[aria-pressed=true]{background:#d9ece4;border:2px solid #23705a}.muted{color:#52675e}</style><body><main id="geography" data-kind="{{ $kind }}" data-entity-id="{{ $entityId }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><h1>Localização para segmentação</h1><p>Indique apenas a precisão territorial conhecida. A selecção conserva a versão e os códigos da cartografia. Não altera endereços fiscais ou de entrega, direitos de acesso, revisão de fornecedores ou localizações de registos dependentes.</p><p id="status" role="status" aria-live="polite"></p><section id="current"></section><section id="picker"><h2>Escolher território da cartografia</h2><form id="search"><label>Pesquisar designação <input name="search" type="search" maxlength="100"></label><button>Pesquisar</button></form><button id="restart">Consultar províncias</button><div id="trail"></div><p id="chosen">Nenhum território escolhido.</p><div id="options"></div><div id="paging"></div></section><section id="decision" hidden><h2>Conferir esta indicação</h2><button id="clear">Deixar localização por indicar</button><form id="prepare"><label>Fundamento <textarea id="reason" required minlength="10" maxlength="2000"></textarea></label><button id="preview">Conferir impacto</button></form><div id="impact" hidden></div><button id="confirm" disabled>Confirmar esta localização</button></section><section id="results" hidden></section><section id="history"></section></main><script type="module" src="/geography-workspace.mjs"></script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ordens de compra — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36}main{max-width:1000px;margin:auto;padding:24px}section,article{background:#fff;border:1px solid #b8cec6;padding:20px;margin:16px 0;border-radius:8px}button,a{font:inherit;margin:8px;padding:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style><body>@include('partials.purchasing-navigation')<main id="orders-workspace" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><a href="/purchasing/receipts">Recepção de mercadoria</a><h1>Ordens de compra</h1><p>Ensaio sintético. Emitir uma ordem não recebe mercadoria, não cria uma factura e não executa um pagamento. Sem envio externo.</p><p id="status" role="status" aria-live="polite"></p><section><h2>Adjudicações autorizadas neste contexto</h2><div id="award-list"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="award-detail"></div></main><script type="module" src="/order-workspace.mjs"></script></body></html>
@@ -0,0 +1,14 @@
@php($purchasingLinks=app(\App\Modules\Purchasing\WorkspaceNavigation::class)->links())
@if(config('purchasing_workspace.single_installation')===true)
@php($activeProfile=collect(config('purchasing_workspace.profiles',[]))->firstWhere('user_id',auth()->id()))
<script type="module" src="/purchasing-exit.mjs"></script>
<header style="padding:14px 20px;background:#173e36;color:white"><strong>Compras · {{ $activeProfile['label'] ?? 'Função autorizada' }}</strong> · <a data-purchasing-entry style="color:white" href="/compras?{{ http_build_query(['return_to'=>request()->getRequestUri()]) }}">Entrada de Compras / trocar função</a> · {{ \Illuminate\Support\Facades\DB::table('companies')->where('id',app(\App\Shared\Context\CompanyContext::class)->identifiers()['company_id'])->value('name') }} · {{ \Illuminate\Support\Facades\DB::table('units')->where('id',app(\App\Shared\Context\CompanyContext::class)->unitId())->value('name') }}</header>
@endif
@if(count($purchasingLinks))
<nav aria-label="Etapas de Compras" style="padding:16px;background:#fff;border-bottom:1px solid #bed0c9;display:flex;gap:16px;flex-wrap:wrap">
<a href="/purchasing/workspace">Percurso de Compras</a>
@foreach($purchasingLinks as $step)
<a href="{{ $step['url'] }}" @if(request()->path()===ltrim($step['url'],'/'))aria-current="page"@endif>{{ $step['label'] }}</a>
@endforeach
</nav>
@endif
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Dossiê de Compras — SGF</title><style>body{font:16px system-ui;color:#173e36;background:#f3f7f5;margin:0}main{max-width:1100px;margin:auto;padding:24px}section{background:white;padding:18px;border:1px solid #bed0c9;margin:16px 0;border-radius:8px}button,input,select{font:inherit;padding:8px;margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}label{display:block}</style><body>@include('partials.purchasing-navigation')<main id="purchasing-dossier" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-context="{{ $context }}"><h1>Dossiê de Compras</h1><p>Consulta das origens e registos efectivos da requisição. Cada etapa exige o seu direito de consulta; uma etapa restrita não é apresentada como vazia. Esta página não prepara, confirma, paga ou contabiliza.</p><p id="status" role="status" aria-live="polite"></p><form id="filters"><label>Número contém <input name="search" maxlength="100"></label><label>Estado <select name="state"><option value="">Todos</option><option value="draft">Rascunho</option><option value="pending">Pendente</option><option value="approved">Aprovada</option><option value="rejected">Rejeitada</option></select></label><button>Pesquisar</button></form><section><div id="requests"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="detail"></div></main><script type="module" src="/purchasing-dossier.mjs"></script></body></html>
@@ -0,0 +1,5 @@
<!doctype html><html lang="pt-MZ"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Compras — SGF Tempo</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1100px;margin:auto;padding:24px}.profiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px}article,section{background:#fff;border:1px solid #bed0c9;border-radius:8px;padding:18px}label{display:block;margin:12px 0}select,button{font:inherit;padding:10px;max-width:100%}a{color:#174d71}p{line-height:1.5}</style><body><main><a href="/">Abrir o SGF</a><h1>Compras</h1><p><strong>{{ $company->name }} · {{ $unit->name }}</strong></p><p>Uma instalação para fornecedores, requisições, propostas, adjudicação, ordens, recepções, facturas e obrigações. Escolha a função que vai exercer; as revisões e aprovações exigem identidades distintas.</p><p>Ambiente sintético local. Os documentos ficam conservados nesta instalação. Não são pagamentos, emissão fiscal ou operações reais.</p>
<section><h2>Como percorrer uma compra</h2><p>Compras prepara o fornecedor; Finanças confere. O solicitante cria a necessidade e a chefia aprova. Compras regista propostas; Compras e Finanças avaliam. A chefia decide a adjudicação e Compras emite a ordem. Armazém recebe. Finanças regista a factura, o revisor valida. Finanças prepara a obrigação e o revisor reconhece.</p><p>Para trocar de função use «Entrada de Compras / trocar função», disponível em todas as etapas. As listas e os atalhos mantêm os mesmos documentos; não precisa de copiar identificadores.</p></section><h2>Entrar numa função</h2><div class="profiles">
@foreach($profiles as $key=>$profile)<article><h3>{{ $profile['label'] }}</h3><p>{{ $profile['description'] }}</p><form method="post" action="/compras/entrar">@csrf<input type="hidden" name="role" value="{{ $key }}"><label>Etapa<select name="stage" required>@foreach($profile['stages'] as $stage=>$label)<option value="{{ $stage }}">{{ $label }}</option>@endforeach</select></label><button>Entrar como {{ $profile['label'] }}</button></form>
@if($returnTo && preg_match('~^/purchasing/([a-z]+)(?:\?|$)~',$returnTo,$m) && isset($profile['stages'][$m[1]]))<form method="post" action="/compras/entrar">@csrf<input type="hidden" name="role" value="{{ $key }}"><input type="hidden" name="stage" value="{{ $m[1] }}"><input type="hidden" name="return_to" value="{{ $returnTo }}"><button>Continuar o documento actual como {{ $profile['label'] }}</button></form>@endif
</article>@endforeach</div><p>O orçamento e os cadastros de ensaio estão preparados. As decisões são individuais e conservam fundamento, versão e histórico. Nenhuma função recebe acesso real por esta entrada.</p></main></body></html>
@@ -0,0 +1,4 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Percurso de Compras — SGF</title><style>body{font:16px system-ui;color:#173e36;background:#f3f7f5;margin:0}main{max-width:1000px;margin:auto;padding:24px}article{padding:18px;background:#fff;border:1px solid #bed0c9;border-radius:8px;margin:16px 0}a{color:#174d71}p{line-height:1.5}</style><body>@include('partials.purchasing-navigation')<main><a href="/">Voltar ao SGF</a><h1>Percurso de Compras</h1><p>Continue as etapas disponíveis para o seu perfil na empresa e unidade actuais. Os documentos são os mesmos entre etapas; não é necessário copiar identificadores ou criar outro saldo.</p><p>Requisição aprovada → propostas comparadas e adjudicação aprovada → ordem emitida → recepção aceite → factura conferida → obrigação reconhecida. Cada decisão continua individual, fundamentada e sujeita às permissões actuais.</p>
@foreach($links as $step)<article><h2><a href="{{ $step['url'] }}">{{ $step['label'] }}</a></h2><p>{{ $step['description'] }}</p></article>@endforeach
@if(config('purchasing_workspace.single_installation')===true)<section id="purchasing-board" data-context="{{ session('company_context.token') }}"><h2>Documentos nesta instalação</h2><label>Etapa <select id="board-stage">@foreach($links as $step)@if($step['url']!=='/purchasing/dossiers')<option value="{{ basename($step['url']) }}">{{ $step['label'] }}</option>@endif @endforeach</select></label><button id="board-refresh">Actualizar lista</button><p id="board-status" role="status"></p><div id="board-records"></div><button id="board-previous">Anteriores</button><span id="board-page"></span><button id="board-next">Seguintes</button></section><script type="module" src="/purchasing-board.mjs"></script>@endif
<p>Ensaio sintético. As páginas apresentam os registos autorizados e a evidência efectiva. Este mapa não confirma operações, não atribui localizações, não paga e não contabiliza.</p></main></body></html>
@@ -0,0 +1,17 @@
@php
$money = static fn ($n) => $n === null ? 'Indeterminado' : number_format(intdiv($n, 100), 0, ',', ' ').','.str_pad((string) abs($n % 100), 2, '0', STR_PAD_LEFT).' MZN';
$query = http_build_query($report['criteria'], '', '&', PHP_QUERY_RFC3986);
$details = http_build_query($report['criteria'] + ['recognition' => 'recognised'], '', '&', PHP_QUERY_RFC3986);
@endphp
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Relatório de obrigações — ensaio SGF</title>
<style>body{font:17px system-ui;background:#f3f6f5;color:#142b28;margin:0}main{max-width:1000px;margin:auto;padding:24px}section,header{background:white;padding:24px;margin:16px 0;border:1px solid #d5e0dc;border-radius:12px}label{display:block;margin:12px 0}input,button{font:inherit;padding:10px}dt{margin-top:14px}dd{font-weight:600;margin-left:0}a{color:#125a4b}code{overflow-wrap:anywhere}.notice{background:#fff2cc;padding:16px}</style></head><body><main>
<header><a href="/supplier-payables?{{ $details }}">Conferir documentos, posições e evidências</a><h1>Obrigações reconhecidas — posição actual</h1><p class="notice">ENSAIO SINTÉTICO. Este relatório agrega as posições financeiras pelo núcleo existente. Não constitui razão contabilístico, balancete, reconciliação bancária ou relatório histórico por data de corte.</p><p>Históricos importados usam recibos de ensaio conferidos. Não há origem histórica SGF viva ou passagem operacional de responsabilidade validada.</p><p>Capturado em {{ $report['captured_at_utc'] }} UTC · Empresa #{{ $report['scope']['company_id'] }} / Unidade #{{ $report['scope']['unit_id'] }}</p></header>
<section><h2>Critérios</h2><form method="get" action="/supplier-payables/report">
@if(isset($report['criteria']['source']))<input type="hidden" name="source" value="{{ $report['criteria']['source'] }}"><p>Origem local #{{ $report['criteria']['source'] }} · <a href="/supplier-payables/report">Consultar todas as origens</a></p>@endif
<label>Número ou fornecedor<input name="q" maxlength="100" value="{{ $report['criteria']['q'] }}"></label><button>Gerar posição actual</button></form><p>Apenas obrigações reconhecidas dos documentos abrangidos. Documentos por reconhecer e pagamentos ainda por afectar não são abatidos como dívida liquidada.</p></section>
<section><h2>Posição abrangida</h2><dl><dt>Obrigações reconhecidas</dt><dd>{{ $report['totals']['count'] }}</dd><dt>Principal original reconhecido</dt><dd>{{ $money($report['totals']['principal_minor']) }}</dd><dt>Saldo global por liquidar</dt><dd>{{ $money($report['totals']['balance_minor']) }}</dd><dt>Posições com saldo indeterminado</dt><dd>{{ $report['totals']['unknown_balance_count'] }}</dd><dt>Obrigações com vencimento desconhecido</dt><dd>{{ $report['totals']['unknown_due_count'] }}</dd></dl>
@if($report['totals']['unknown_balance_count'])<p role="alert">Há evidência por conferir. O saldo global permanece indeterminado; o montante conhecido abaixo é parcial e não representa o total por liquidar.</p>@endif
</section>
<section><h2>Componentes das posições conhecidas</h2><dl><dt>Posições com saldo sustentado</dt><dd>{{ $report['known_positions']['count'] }}</dd><dt>Principal dessas posições</dt><dd>{{ $money($report['known_positions']['principal_minor']) }}</dd><dt>Confirmados históricos incorporados</dt><dd>{{ $money($report['known_positions']['historical_confirmed_minor']) }}</dd><dt>Afectações líquidas de reversões</dt><dd>{{ $money($report['known_positions']['net_allocated_minor']) }}</dd><dt>Saldo conhecido{{ $report['totals']['unknown_balance_count'] ? ' — parcial' : '' }}</dt><dd>{{ $money($report['known_positions']['balance_minor']) }}</dd></dl><p>Conferência: principal conhecido − confirmados históricos − afectações líquidas = saldo conhecido. Reversões preservam os originais e são consideradas pelo núcleo financeiro.</p></section>
<section><h2>Rastreabilidade</h2><p>Identificador de conteúdo das posições: <code>{{ $report['positions_sha256'] }}</code></p><p>O mesmo conteúdo e âmbito produzem o mesmo identificador. Uma nova consulta ou os documentos abertos depois podem já reflectir alterações posteriores à captura.</p><p><a href="/api/supplier-payables/report?{{ $query }}">Consultar os dados deste relatório em JSON</a></p><p>O link gera uma nova captura da posição actual; não recupera um documento arquivado. Não se guardou um relatório ou um lançamento por esta consulta.</p></section>
</main></body></html>
@@ -0,0 +1,3 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Recepção de mercadoria — SGF</title>
<style>body{font:16px system-ui;color:#173e36;background:#f3f7f5;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,article{background:white;border:1px solid #b8cec6;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:8px;max-width:100%;box-sizing:border-box}button,a{margin:6px}button{cursor:pointer}button:disabled{cursor:default}table{width:100%;border-collapse:collapse}td,th{text-align:left;border-bottom:1px solid #dae4df;padding:8px}.error{color:#8b2424}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style>
<body>@include('partials.purchasing-navigation')<main id="receipts" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-continuity-enabled="{{ config('receipt_qa.continuity_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><h1>Recepção de mercadoria</h1><p>Ensaio sintético — sem utilização operacional. Receber mercadoria não paga nem contabiliza a compra.</p><p id="message" role="status" aria-live="polite"></p><section><h2>Ordens autorizadas</h2><div id="orders"></div><button id="previous">Anterior</button><span id="pagination"></span><button id="next">Seguinte</button></section><div id="detail"></div></main><script type="module" src="/receipt-workspace.mjs"></script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Requisições para compra — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,article{background:white;border:1px solid #bed0c9;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:8px;box-sizing:border-box;max-width:100%}button,a{margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style><body>@include('partials.purchasing-navigation')<main id="requests" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><a href="/purchasing/suppliers">Fornecedores</a><a href="/purchasing/sourcing">Propostas e adjudicação</a><h1>Requisições para compra</h1><p>Ensaio sintético. Este percurso usa o mesmo livro de Compras desde o rascunho até à ordem. Submeter reserva uma vez; rejeitar liberta a reserva. A aprovação exige outra identidade. Não converte requisições antigas nem transfere os seus saldos.</p><p id="status" role="status" aria-live="polite"></p><section><form id="filters"><label>Estado <select name="status"><option value="">Todos</option><option value="draft">Rascunho</option><option value="pending">Pendente</option><option value="approved">Aprovado</option><option value="rejected">Rejeitado</option></select></label><button>Filtrar</button></form><button id="create" hidden>Preparar nova requisição</button><div id="list"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="detail"></div></main><script type="module" src="/request-workspace.mjs"></script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Propostas e adjudicação — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1150px;margin:auto;padding:24px}section,article{background:white;border:1px solid #bed0c9;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:8px;box-sizing:border-box;max-width:100%}button,a{margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}.hint{color:#52675e}h3{margin-top:0}</style><body>@include('partials.purchasing-navigation')<main id="sourcing" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><a href="/purchasing/orders">Ordens de compra</a>@if(config('receipt_qa.suppliers_enabled')===true)<a href="/purchasing/suppliers">Fornecedores e revis?o</a>@endif<h1>Propostas, comparação e adjudicação</h1><p>Ensaio sintético. Cada proposta, avaliação e decisão exige conferência individual. A pontuação não selecciona um fornecedor automaticamente. Sem envio externo.</p><p id="status" role="status" aria-live="polite"></p><section><h2>Processos autorizados</h2><div id="processes"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="process-detail"></div></main><script type="module" src="/sourcing-workspace.mjs"></script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Conferência de facturas de fornecedor — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1120px;margin:auto;padding:24px}section,article{background:white;border:1px solid #bed0c9;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,textarea,button{font:inherit;padding:8px;box-sizing:border-box;max-width:100%}button,a{margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style><body>@include('partials.purchasing-navigation')<main id="supplier-invoices" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-continuity-enabled="{{ config('receipt_qa.continuity_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a>@if($obligations)<a href="/purchasing/obligations">Reconhecer obrigação separadamente</a>@endif<h1>Conferência de facturas de fornecedor</h1><p>Ensaio sintético. Registe os dados do documento recebido e confira-os com a ordem e a recepção aceite. A validação exige outra identidade e consome o compromisso no mesmo livro orçamental. Não emite documentos fiscais, não paga nem lança na contabilidade.</p><p id="status" role="status" aria-live="polite"></p><section><h2>Ordens da origem autorizada</h2><div id="orders"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="detail"></div></main><script type="module" src="/supplier-invoice-workspace.mjs"></script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Reconhecimento de obrigações — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,article{background:white;border:1px solid #bed0c9;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:8px;max-width:100%}button,a{margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style><body>@include('partials.purchasing-navigation')<main id="supplier-obligations" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-continuity-enabled="{{ config('receipt_qa.continuity_enabled')===true?'true':'false' }}" data-evidence-enabled="{{ config('receipt_qa.obligation_evidence_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><a href="/purchasing/invoices">Conferir facturas</a><h1>Reconhecimento de obrigações</h1><p>Ensaio sintético. A factura conferida é a origem; preparar não reconhece dívida. Outra pessoa valida a versão exacta no mesmo núcleo. Vencimento desconhecido permanece pendente, com atraso indeterminado. Este percurso não paga, não contabiliza e não volta a consumir orçamento ou stock.</p><p id="status" role="status" aria-live="polite"></p><form id="filters"><label>Número contém <input name="search" maxlength="100"></label><label>Estado documental <select name="state"><option value="">Todos</option><option value="pending">Pendente</option><option value="validated">Validada</option></select></label><button>Pesquisar</button></form><section><div id="documents"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="detail"></div></main><script type="module" src="/supplier-obligation-workspace.mjs"></script></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Fornecedores — SGF</title><style>body{font:16px system-ui;background:#f3f7f5;color:#173e36;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,article{background:white;border:1px solid #bed0c9;padding:18px;margin:16px 0;border-radius:8px}label{display:block;margin:12px 0}input,select,textarea,button{font:inherit;padding:8px;box-sizing:border-box;max-width:100%}button,a{margin:8px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style><body>@include('partials.purchasing-navigation')<main id="suppliers" data-focus-enabled="{{ config('receipt_qa.focus_enabled')===true?'true':'false' }}" data-context="{{ $context }}" data-csrf="{{ $csrf }}"><a href="/">Voltar ao SGF</a><a href="/purchasing/sourcing">Propostas e adjudicação</a><h1>Fornecedores e revisão independente</h1><p>Ensaio sintético. Preparar uma ficha deixa-a pendente. Outra identidade confere os dados e decide a activação ou suspensão da versão exacta. Alterações voltam a exigir revisão. Documentos são referências declaradas; não há envio externo nem carregamento de anexos neste percurso.</p><p id="status" role="status" aria-live="polite"></p><section><form id="filters"><label>Pesquisar nome ou NUIT <input name="search" maxlength="100"></label><label>Estado <select name="status"><option value="">Todos</option><option value="pending">Pendente</option><option value="active">Activo</option><option value="suspended">Suspenso</option></select></label><button>Filtrar</button></form><button id="create" hidden>Preparar novo fornecedor</button><div id="list"></div><button id="previous">Anterior</button><span id="page"></span><button id="next">Seguinte</button></section><div id="detail"></div></main><script type="module" src="/supplier-workspace.mjs"></script></body></html>