Files
Investidor-V2/resources/js/components/simulation-irpc-costs.tsx
T

758 lines
33 KiB
TypeScript

import { useState } from 'react';
import {
costRules,
newNormalCosts,
newCostRow,
reusableCreditBalance,
type CostRow,
type NormalCostsInput,
} from '@/lib/irpc-normal-costs';
import { normalFinancialSource, type NormalBook } from '@/lib/irpc-normal';
import { fiscalMoney } from '@/lib/irpc-normal-schedules';
import {
addFractions,
subtractFractions,
fractionPresentation,
} from '@/lib/fiscal-rational';
import { type ProfitTopic } from '@/lib/irpc-normal-profit';
type Field = [
string,
string,
'money' | 'text' | 'choice' | 'method' | 'source',
];
const fields: Record<string, Field[]> = {
allowance: [
['billed', 'Parcela facturada a clientes', 'money'],
[
'taxedNotBilled',
'Parcela tributada emIRPS e não facturada a clientes',
'money',
],
],
advertising: [
[
'campaign',
'Campanha de lançamento com horizonte superior a um ano',
'choice',
],
],
vehicle: [
['passenger', 'Viatura ligeira de passageiros', 'choice'],
[
'publicOrRental',
'Afecta a serviço público de transportes ou aluguer na actividade normal',
'choice',
],
],
fuel: [
[
'supportedNormal',
'Parcela com activo/locação e consumo normal comprovados',
'money',
],
['passenger', 'Viatura ligeira de passageiros', 'choice'],
[
'publicOrRental',
'Afecta a serviço público de transportes ou aluguer na actividade normal',
'choice',
],
],
rental: [
[
'nonAcceptedQuota',
'Parcela documentada da quota não aceite (36h)',
'money',
],
['quotaReference', 'Documento e regra da quota não aceite', 'text'],
['passenger', 'Aplicação36(4) — ligeira de passageiros', 'choice'],
[
'publicOrRental',
'Afecta a serviço público de transportes ou aluguer na actividade normal',
'choice',
],
],
stock: [
['balanceKey', 'Identificação única do inventário/objecto', 'text'],
['lossObserved', 'Perda de valor efectivamente observada', 'choice'],
['observedLoss', 'Perda total observada documentada', 'money'],
['previous', 'Provisão fiscal anterior que subsiste', 'money'],
],
litigation: [
['balanceKey', 'Identificação única do processo/objecto', 'text'],
['caseOngoing', 'Processo judicial em curso', 'choice'],
[
'chargeDeductible',
'Obrigação daria lugar a custo fiscal dedutível',
'choice',
],
['obligation', 'Obrigação coberta documentada', 'money'],
['previous', 'Provisão fiscal anterior que subsiste', 'money'],
],
provision_reversal: [
['balanceKey', 'Identificação única da provisão/objecto', 'text'],
['previous', 'Parcela antes fiscalmente deduzida', 'money'],
[
'eventEnded',
'Evento não subsiste ou utilização para fim diferente',
'choice',
],
[
'inResult',
'Reversão já incluída em proveitos do resultado actual',
'choice',
],
],
taxed_reversal: [
['balanceKey', 'Identificação única da provisão/objecto', 'text'],
['previous', 'Parcela antes tributada e não deduzida', 'money'],
[
'eventEnded',
'Evento não subsiste ou utilização para fim diferente',
'choice',
],
[
'inResult',
'Reversão já incluída em proveitos do resultado actual',
'choice',
],
],
prior_cost: [
['originalYear', 'Exercício de origem', 'text'],
[
'unknownAtClose',
'Imprevisível ou manifestamente desconhecido no encerramento anterior',
'choice',
],
[
'closingEvidence',
'Suporte do conhecimento no encerramento anterior',
'text',
],
],
prior_income: [
['originalYear', 'Exercício de origem', 'text'],
[
'unknownAtClose',
'Imprevisível ou manifestamente desconhecido no encerramento anterior',
'choice',
],
[
'closingEvidence',
'Suporte do conhecimento no encerramento anterior',
'text',
],
],
work: [
['projectKey', 'Identificação única da obra', 'text'],
['method', 'Critério fiscal da obra', 'method'],
['price', 'Preço contratual estabelecido', 'money'],
['totalCost', 'Custo total estimado para concluir', 'money'],
['incurred', 'Custos efectivamente incorporados acumulados', 'money'],
[
'priorRevenue',
'Proveitos fiscais reconhecidos em exercícios anteriores',
'money',
],
[
'priorCost',
'Custos fiscais reconhecidos em exercícios anteriores',
'money',
],
['costSource', 'Linha de custos da obra no resultado actual', 'source'],
[
'bookCost',
'Parcela de custos da obra já contabilizada no exercício',
'money',
],
[
'partialBilling',
'Facturação parcial com grau de acabamento correspondente',
'choice',
],
[
'ownFractionalDelivery',
'Obra por conta própria vendida e entregue fraccionadamente',
'choice',
],
[
'consistentMethod',
'Critério uniforme para obras idênticas e mantido até ao final',
'choice',
],
['publicReception', 'Recepção provisória de obra pública', 'choice'],
[
'deferredReceipt',
'Receita antecipada19(5) proposta — zero conhecido se não aplicada',
'money',
],
],
};
export function SimulationIrpcCosts({
draft,
book,
save,
}: {
draft: Record<string, unknown>;
book: NormalBook;
save: (b: NormalBook) => void;
}) {
const [review, setReview] = useState(false),
[error, setError] = useState(''),
i = book.costs,
source = normalFinancialSource(draft);
if (!i)
return (
<button
type="button"
onClick={() => save({ ...book, costs: newNormalCosts() })}
>
Iniciar custos com limites, provisões e periodização
</button>
);
const patch = (p: Partial<NormalCostsInput>) => {
setReview(false);
save({ ...book, costs: { ...i, ...p } });
};
const row = (id: string, p: Partial<CostRow>) =>
patch({ rows: i.rows.map((r) => (r.id === id ? { ...r, ...p } : r)) });
const fact = (r: CostRow, k: string, v: string) =>
row(r.id, { facts: { ...r.facts, [k]: v } });
const summaries = () => {
const groups = new Map<ProfitTopic, string[]>();
for (const r of book.schedules?.rows ?? []) {
if (!r.rule) continue;
const k = (
{
tax_own: 'expenseCorrections',
tax_other: 'expenseCorrections',
undocumented: 'expenseCorrections',
capital_in: 'positivePatrimonial',
patrimonial_gain: 'positivePatrimonial',
capital_out: 'negativePatrimonial',
patrimonial_cost: 'negativePatrimonial',
future_cost: 'timing',
future_income: 'timing',
current_cost: 'timing',
current_income: 'timing',
} as Record<string, ProfitTopic>
)[r.rule];
groups.set(k, [...(groups.get(k) ?? []), r.amount]);
}
for (const r of i.rows)
if (r.rule) {
const k = costRules[r.rule][1];
groups.set(k, [...(groups.get(k) ?? []), r.amount]);
}
return [...groups].map(([key, values]) => ({
key,
amount: fractionPresentation(addFractions(values.map(fiscalMoney)))
.decimal,
}));
};
const reuse = (r: CostRow) => {
try {
const code = r.sourceCode,
l = source?.accountingLines.find((l) => l.code === code);
if (!l) throw Error('Seleccione uma origem.');
const claims = [
...book.rows,
...(book.schedules?.rows ?? []),
...i.rows.filter((q) => q.id !== r.id),
...i.rows
.filter((q) => q.rule === 'work')
.map((q) => ({
sourceCode: q.facts.costSource,
amount: q.facts.bookCost,
})),
].filter((q) => q.sourceCode === code);
const available = subtractFractions(
fiscalMoney(l.total),
addFractions(claims.map((q) => fiscalMoney(q.amount))),
);
if (BigInt(available.numerator) <= 0n)
throw Error('Não há parcela livre conhecida nesta origem.');
row(r.id, { amount: fractionPresentation(available).decimal });
setError('');
} catch (e) {
setError(String(e));
}
};
return (
<fieldset
aria-label="Custos com limites provisões periodização"
className="space-y-3 rounded border p-3"
>
<legend>
Custos com limites, provisões e periodização — exercício2026
</legend>
<p>
Reutiliza o resultado e as linhas já declaradas. Identifique só
a parcela de cada categoria e os parâmetros em falta. Os limites
anuais de publicidade e créditos são partilhados por todas as
parcelas; os saldos de provisões não são consumidos. A
divergência M20/M22 não bloqueia estes cálculos.
Benefícios/casos especiaisL7 e saldo anualL8 mantêm os seus
âmbitos.
</p>
{error && <p role="alert">{error}</p>}
<button
type="button"
onClick={() =>
patch({
rows: [...i.rows, newCostRow(crypto.randomUUID())],
})
}
>
Acrescentar custo, provisão ou periodização
</button>
{i.rows.some((r) => r.rule === 'advertising') && (
<fieldset>
<legend>Publicidade — limite único anual</legend>
<p>
Volume de vendas da origem:{' '}
{source?.accountingLines.find(
(l) => l.code === 'revenue',
)?.total ?? 'Em falta'}{' '}
MZN. O limite não se repete por factura.
</p>
<label>
Correspondência às vendas normais do exercício
<select
aria-label="Vendas normais para publicidade"
value={i.salesOrdinary}
onChange={(e) =>
patch({
salesOrdinary: e.target.value as '' | 'yes',
})
}
>
<option value="">Informação em falta</option>
<option value="yes">
Toda a receita da origem corresponde a
vendas/serviços normais
</option>
</select>
</label>
<label className="block">
Suporte do volume de vendas e completude da publicidade
<input
value={i.advertisingReference}
onChange={(e) =>
patch({ advertisingReference: e.target.value })
}
/>
</label>
<p>
Campanhas de lançamento com efeito superior a um ano
exigem36-A e articulação própria; permanecem
discriminadas, sem dedução ordinária presumida.
</p>
</fieldset>
)}
{i.rows.some((r) => r.rule === 'credit') && (
<fieldset>
<legend>Créditos — limites anual e acumulado</legend>
<label>
Regime da provisão de créditos
<select
aria-label="Regime da provisão de créditos"
value={i.creditRegime}
onChange={(e) =>
patch({
creditRegime: e.target
.value as NormalCostsInput['creditRegime'],
})
}
>
<option value="">Por identificar</option>
<option value="ordinary">
Ordinário — fora de supervisão
financeira/seguradora
</option>
<option value="regulated">
Provisão sujeita a supervisão — correspondência
própria
</option>
</select>
</label>
<label className="block">
Créditos da actividade normal existentes no fim do
exercício
<input
value={i.creditBase}
onChange={(e) =>
patch({ creditBase: e.target.value })
}
/>
</label>
{reusableCreditBalance(source) && (
<button
type="button"
onClick={() =>
patch({
creditBase: fractionPresentation(
reusableCreditBalance(source)!,
).decimal,
})
}
>
Reutilizar saldo financeiro de créditos:{' '}
{
fractionPresentation(
reusableCreditBalance(source)!,
).decimal
}{' '}
MZN — conferir âmbito
</button>
)}
<label className="block">
Provisão fiscal anterior que subsiste no termo do
exercício
<input
value={i.creditPrevious}
onChange={(e) =>
patch({ creditPrevious: e.target.value })
}
/>
</label>
<label className="block">
Suporte do saldo de créditos e provisão anterior
<input
value={i.creditReference}
onChange={(e) =>
patch({ creditReference: e.target.value })
}
/>
</label>
<p>
1,5% anual e6% acumulado; saldo anterior em falta não
significa zero. Não aplica regras bancárias/seguradoras
nem percentagens de antiguidade presumidas.
</p>
</fieldset>
)}
{i.rows.map((r, n) => {
const income = [
'provision_reversal',
'taxed_reversal',
'prior_income',
'work',
].includes(r.rule),
omitted =
r.rule === 'provision_reversal' &&
r.facts.inResult === 'no';
return (
<fieldset key={r.id} className="rounded border p-3">
<legend>
Parcela {n + 1} — {r.document || 'Por identificar'}
</legend>
<label>
Categoria
<select
aria-label={'Categoria custo ' + (n + 1)}
value={r.rule}
onChange={(e) =>
row(r.id, {
rule: e.target.value as CostRow['rule'],
})
}
>
<option value="">Por identificar</option>
{Object.entries(costRules).map(([k, v]) => (
<option key={k} value={k}>
{v[0]} — {v[2]}
</option>
))}
</select>
</label>
{(
['document', 'line', 'amount', 'reference'] as const
).map((k) => (
<label key={k} className="block">
{
{
document:
'Documento ou mapa discriminado',
line: 'Linha distinta',
amount:
r.rule === 'work'
? 'Proveitos da obra já contabilizados no exercício — zero se conhecido'
: 'Parcela contabilística MZN',
reference:
'Suporte da classificação, condições e período',
}[k]
}
<input
aria-label={'Custo ' + (n + 1) + ' ' + k}
value={r[k]}
onChange={(e) =>
row(r.id, { [k]: e.target.value })
}
/>
</label>
))}
{!omitted && (
<>
<label>
Origem contabilística
<select
aria-label={'Origem custo ' + (n + 1)}
value={r.sourceCode}
onChange={(e) =>
row(r.id, {
sourceCode: e.target.value,
})
}
>
<option value="">
Por corresponder
</option>
{source?.accountingLines
.filter(
(l) =>
l.accountingEffect ===
(income
? 'income'
: 'expense'),
)
.map((l) => (
<option
key={l.code}
value={l.code}
>
{l.origin}: {l.total} MZN
</option>
))}
</select>
</label>
<button type="button" onClick={() => reuse(r)}>
Reutilizar parcela livre da origem {n + 1}
</button>
</>
)}
{(fields[r.rule] ?? [])
.filter(
([k]) =>
!(
k === 'publicOrRental' &&
r.facts.passenger !== 'yes'
) &&
!(
k === 'publicReception' &&
r.facts.method !== 'closing'
) &&
!(
r.rule === 'stock' &&
r.facts.lossObserved === 'no' &&
['observedLoss', 'previous'].includes(k)
) &&
!(
r.rule === 'litigation' &&
r.facts.caseOngoing === 'no' &&
[
'obligation',
'previous',
'chargeDeductible',
].includes(k)
),
)
.map(([k, label, type]) => (
<label key={k} className="block">
{label}
{type === 'choice' ? (
<select
aria-label={
'Custo ' + (n + 1) + ' ' + k
}
value={r.facts[k] ?? ''}
onChange={(e) =>
fact(r, k, e.target.value)
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — documentado
</option>
<option value="no">
Não — documentado
</option>
</select>
) : type === 'method' ? (
<select
aria-label={
'Custo ' + (n + 1) + ' ' + k
}
value={r.facts[k] ?? ''}
onChange={(e) =>
fact(r, k, e.target.value)
}
>
<option value="">
Por identificar
</option>
<option value="percentage">
Percentagem de acabamento
</option>
<option value="closing">
Encerramento da obra
</option>
</select>
) : type === 'source' ? (
<select
aria-label={
'Custo ' + (n + 1) + ' ' + k
}
value={r.facts[k] ?? ''}
onChange={(e) =>
fact(r, k, e.target.value)
}
>
<option value="">
Por corresponder/zero fora de
linha
</option>
{source?.accountingLines
.filter(
(l) =>
l.accountingEffect ===
'expense',
)
.map((l) => (
<option
key={l.code}
value={l.code}
>
{l.origin}: {l.total}{' '}
MZN
</option>
))}
</select>
) : (
<input
aria-label={
'Custo ' + (n + 1) + ' ' + k
}
value={r.facts[k] ?? ''}
onChange={(e) =>
fact(r, k, e.target.value)
}
/>
)}
</label>
))}
<label>
Condições ordinárias e inclusão na origem
demonstradas
<select
aria-label={'Condições custo ' + (n + 1)}
value={r.conditions}
onChange={(e) =>
row(r.id, {
conditions: e.target.value as
| ''
| 'yes',
})
}
>
<option value="">Informação em falta</option>
<option value="yes">
Sim, nos documentos identificados
</option>
</select>
</label>
<button
type="button"
onClick={() =>
patch({
rows: i.rows.filter((q) => q.id !== r.id),
})
}
>
Retirar parcela {n + 1} das entradas actuais
</button>
</fieldset>
);
})}
{book.profit && (
<>
<button
type="button"
onClick={() => {
try {
summaries();
setReview(true);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Conferir correspondência dos temas com os mapas
</button>
{review && (
<div>
<p>
Reveja as somas brutas antes de aplicar; não
alteram os factos, o resultado financeiro ou os
históricos.
</p>
{summaries().map((t) => (
<p key={t.key}>
{
{
expenseCorrections:
'Correcções aos custos',
incomeCorrections:
'Correcções aos proveitos',
positivePatrimonial:
'Variações positivas',
negativePatrimonial:
'Variações negativas',
timing: 'Periodização',
special: 'Casos especiais',
}[t.key]
}
: actual{' '}
{book.profit!.topics[t.key].amount ||
book.profit!.topics[t.key].state ||
'Em falta'}{' '}
→ mapas {t.amount} MZN
</p>
))}
<button
type="button"
onClick={() => {
const topics = structuredClone(
book.profit!.topics,
);
for (const t of summaries())
topics[t.key] = {
state: 'known',
amount: t.amount,
reference:
'Correspondência aritmética aos documentos/linhas dos mapas de ajustamentos e custos do exercício.',
};
save({
...book,
profit: { ...book.profit!, topics },
});
setReview(false);
}}
>
Aplicar correspondências conferidas dos mapas
</button>
</div>
)}
</>
)}
<p>
Ao calcular e guardar, os novos resultados entram numa versão
própria; o resultado anterior permanece no histórico. Informação
em falta impede apenas os totais que dependem dessa parcela.
</p>
</fieldset>
);
}