569 lines
23 KiB
TypeScript
569 lines
23 KiB
TypeScript
import { inputText } from './input-text.ts';
|
||
import {
|
||
irpcInterestVersion,
|
||
calculateIrpcInterest,
|
||
validateIrpcInterestRule,
|
||
type IrpcInterestRule,
|
||
type IrpcInterestCalculation,
|
||
} from './irpc-interest.ts';
|
||
import type { IrpcBook, IrpcResult, IrpcRow } from './irpc-withholdings.ts';
|
||
import {
|
||
addFractions,
|
||
decimalFraction,
|
||
fraction,
|
||
fractionPresentation,
|
||
subtractFractions,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
|
||
export type MrftaInput = {
|
||
version: 1;
|
||
period: string;
|
||
declaration: '' | 'initial' | 'replacement';
|
||
timing: '' | 'within' | 'late';
|
||
nuit: string;
|
||
name: string;
|
||
commercialName: string;
|
||
taxArea: string;
|
||
address: string;
|
||
contact: string;
|
||
completeness: '' | 'yes' | 'no';
|
||
completenessRef: string;
|
||
otherRubrics: '' | 'none' | 'pending';
|
||
otherRubricsRef: string;
|
||
codes: Array<{ rowId: string; cer: string; reference: string }>;
|
||
interest: {
|
||
mode: '' | 'assessment' | 'timely';
|
||
calculation?: 'automatic';
|
||
automatic?: IrpcInterestRule;
|
||
period: string;
|
||
amount: string;
|
||
reference: string;
|
||
date: string;
|
||
dueDate: string;
|
||
dueReference: string;
|
||
};
|
||
};
|
||
type MrftaLine = {
|
||
ids: string[];
|
||
documents: string[];
|
||
references: string[];
|
||
rubric: string;
|
||
cer: string;
|
||
rate: number;
|
||
base: FiscalFraction;
|
||
tax: FiscalFraction;
|
||
paid: FiscalFraction | null;
|
||
};
|
||
export type MrftaModel = {
|
||
automatic?: IrpcInterestCalculation;
|
||
period: string;
|
||
input: MrftaInput;
|
||
lines: MrftaLine[];
|
||
excluded: Array<{ id: string; reason: string }>;
|
||
pending: string[];
|
||
principal: FiscalFraction;
|
||
paid: FiscalFraction | null;
|
||
interest: FiscalFraction | null;
|
||
total: FiscalFraction | null;
|
||
interestReason: string;
|
||
source: string;
|
||
};
|
||
export type MrftaRecord = { key: string; createdAt: string; model: MrftaModel };
|
||
export type MrftaStore = {
|
||
mrfta?: MrftaInput;
|
||
mrftaResult?: MrftaRecord;
|
||
mrftaHistory?: MrftaRecord[];
|
||
};
|
||
export const newMrftaInput = (): MrftaInput => ({
|
||
version: 1,
|
||
period: '',
|
||
declaration: '',
|
||
timing: '',
|
||
nuit: '',
|
||
name: '',
|
||
commercialName: '',
|
||
taxArea: '',
|
||
address: '',
|
||
contact: '',
|
||
completeness: '',
|
||
completenessRef: '',
|
||
otherRubrics: '',
|
||
otherRubricsRef: '',
|
||
codes: [],
|
||
interest: {
|
||
mode: '',
|
||
period: '',
|
||
amount: '',
|
||
reference: '',
|
||
date: '',
|
||
dueDate: '',
|
||
dueReference: '',
|
||
},
|
||
});
|
||
export const mrftaSource =
|
||
'AT MRFTA19/39,PDF1–2:quadros1–7;um mês/ano por declaração. Decreto9/2008 arts25/35,PDF6/8,p121/123:juros compensatórios separados. MAIBOR12meses+2p.p.na data da liquidação;publicação MAIBOR descontinuada segundo BM. Sem substituir por MIMO/Prime nem presumir zero. CER exige referência específica;remissões antigas do M/39 não renumeradas no original.';
|
||
const date = (s: string) =>
|
||
/^20\d{2}-\d{2}-\d{2}$/.test(s) &&
|
||
Number.isFinite(Date.parse(s)) &&
|
||
new Date(s).toISOString().slice(0, 10) === s;
|
||
const zero = () => fraction(0n, 1n);
|
||
const isZero = (v: FiscalFraction) => BigInt(v.numerator) === 0n;
|
||
const negative = (v: FiscalFraction) => BigInt(v.numerator) < 0n;
|
||
const labels: Record<string, string> = {
|
||
period: 'mês/ano',
|
||
declaration: 'tipo de declaração',
|
||
timing: 'dentro/fora do prazo',
|
||
nuit: 'NUIT',
|
||
name: 'nome do sujeito passivo',
|
||
taxArea: 'área fiscal',
|
||
address: 'domicílio fiscal',
|
||
};
|
||
|
||
export function validateMrfta(b: MrftaStore) {
|
||
const i = b.mrfta;
|
||
if (
|
||
i?.interest?.calculation !== undefined &&
|
||
i.interest.calculation !== 'automatic'
|
||
)
|
||
throw Error('Método de cálculo de juros não recuperável.');
|
||
if (i?.interest?.automatic !== undefined)
|
||
validateIrpcInterestRule(i.interest.automatic);
|
||
if (
|
||
i &&
|
||
(!['', 'none', 'pending'].includes(i.otherRubrics) ||
|
||
typeof i.otherRubricsRef !== 'string')
|
||
)
|
||
throw Error('Âmbito de outras rubricas MRFTA não recuperável.');
|
||
if (
|
||
i &&
|
||
(i.version !== 1 ||
|
||
!['', 'initial', 'replacement'].includes(i.declaration) ||
|
||
!['', 'within', 'late'].includes(i.timing) ||
|
||
!['', 'yes', 'no'].includes(i.completeness) ||
|
||
[
|
||
'period',
|
||
'nuit',
|
||
'name',
|
||
'commercialName',
|
||
'taxArea',
|
||
'address',
|
||
'contact',
|
||
'completenessRef',
|
||
].some((k) => typeof i[k as keyof MrftaInput] !== 'string') ||
|
||
!Array.isArray(i.codes) ||
|
||
i.codes.some(
|
||
(c) =>
|
||
!c ||
|
||
['rowId', 'cer', 'reference'].some(
|
||
(k) => typeof c[k as keyof typeof c] !== 'string',
|
||
),
|
||
) ||
|
||
new Set(i.codes.map((c) => c.rowId)).size !== i.codes.length ||
|
||
!i.interest ||
|
||
!['', 'assessment', 'timely'].includes(i.interest.mode) ||
|
||
[
|
||
'period',
|
||
'amount',
|
||
'reference',
|
||
'date',
|
||
'dueDate',
|
||
'dueReference',
|
||
].some(
|
||
(k) =>
|
||
typeof i.interest[k as keyof typeof i.interest] !==
|
||
'string',
|
||
))
|
||
)
|
||
throw Error('Preparação MRFTA não recuperável;originais conservados.');
|
||
if (
|
||
(b.mrftaResult &&
|
||
(!b.mrftaResult.model ||
|
||
typeof b.mrftaResult.key !== 'string' ||
|
||
!Array.isArray(b.mrftaResult.model.lines) ||
|
||
!Array.isArray(b.mrftaResult.model.pending))) ||
|
||
(b.mrftaHistory !== undefined && !Array.isArray(b.mrftaHistory))
|
||
)
|
||
throw Error('Histórico MRFTA não recuperável.');
|
||
}
|
||
function rubric(r: IrpcRow, v: IrpcResult) {
|
||
if (v.nature === 'on_account')
|
||
return r.kind === 'rent'
|
||
? 'Taxa normal — Rendimentos prediais'
|
||
: r.kind === 'capital'
|
||
? 'Taxa normal — Rendimentos de capitais e mais valias'
|
||
: null;
|
||
if (v.nature !== 'definitive') return null;
|
||
return (
|
||
(
|
||
{
|
||
royalty:
|
||
'IRPC liberatório — Actividade intelectual ou industrial / prestação de informações',
|
||
equipment:
|
||
'IRPC liberatório — Uso ou concessão do uso de equipamento',
|
||
capital:
|
||
'IRPC liberatório — Aplicação de capitais e rendimentos prediais',
|
||
services:
|
||
'IRPC liberatório — Comissões por intermediação de contratos ou prestação de serviços',
|
||
} as Record<string, string>
|
||
)[r.kind] ?? null
|
||
);
|
||
}
|
||
function key(b: IrpcBook & MrftaStore, current: boolean) {
|
||
return JSON.stringify([
|
||
'MRFTA-L5/1',
|
||
b.result,
|
||
b.mrfta,
|
||
current,
|
||
...(b.mrfta?.interest.mode === '' &&
|
||
b.mrfta?.interest.calculation === 'automatic'
|
||
? [irpcInterestVersion]
|
||
: []),
|
||
]);
|
||
}
|
||
export function mrftaCurrent(b: IrpcBook & MrftaStore, current: boolean) {
|
||
return !!b.mrftaResult && b.mrftaResult.key === key(b, current);
|
||
}
|
||
export function prepareMrfta(
|
||
b: IrpcBook & MrftaStore,
|
||
current: boolean,
|
||
): MrftaModel {
|
||
validateMrfta(b);
|
||
const i = b.mrfta ?? newMrftaInput(),
|
||
pending: string[] = [],
|
||
excluded: MrftaModel['excluded'] = [],
|
||
lines: MrftaLine[] = [];
|
||
for (const k of Object.keys(labels))
|
||
if (!inputText(i[k as keyof MrftaInput]).trim())
|
||
pending.push('Falta ' + labels[k] + '.');
|
||
if (!/^2026-(0[1-9]|1[0-2])$/.test(i.period))
|
||
pending.push('Mês/ano inválido ou fora do recorte2026.');
|
||
if (i.nuit && !/^\d{9}$/.test(i.nuit))
|
||
pending.push(
|
||
'NUIT deve ter nove algarismos;confira a identidade fiscal.',
|
||
);
|
||
if (i.completeness !== 'yes' || !i.completenessRef.trim())
|
||
pending.push(
|
||
'Conferir completude das retenções deste sujeito passivo/período, incluindo os demais livros;este contexto não é automaticamente a declaração completa.',
|
||
);
|
||
if (i.otherRubrics !== 'none' || !i.otherRubricsRef.trim())
|
||
pending.push(
|
||
'IRPS,tributação autónoma e outras rubricas fora deste recorte não são zero presumido;documente a ausência de outros valores devidos ou mantenha o total indeterminado.',
|
||
);
|
||
if (!current || !b.result)
|
||
pending.push(
|
||
'Cálculo das retenções desactualizado ou inexistente;guarde o cálculo actual antes da preparação.',
|
||
);
|
||
for (const v of b.result?.rows ?? []) {
|
||
const r = b.result!.inputs.find((r) => r.id === v.id);
|
||
if (!r) {
|
||
pending.push('Entrada do resultado ausente: ' + v.id);
|
||
continue;
|
||
}
|
||
if (r.direction === 'received') {
|
||
excluded.push({
|
||
id: r.id,
|
||
reason: 'Retenção sofrida; não é imposto a entregar pelo titular.',
|
||
});
|
||
continue;
|
||
}
|
||
if (v.period && v.period !== i.period) {
|
||
excluded.push({ id: r.id, reason: 'Outro período: ' + v.period });
|
||
continue;
|
||
}
|
||
if (v.treatment === 'dispensed' && v.status === 'calculated') {
|
||
excluded.push({
|
||
id: r.id,
|
||
reason: 'Dispensa documentada;sem imposto a transcrever.',
|
||
});
|
||
continue;
|
||
}
|
||
if (
|
||
v.status !== 'calculated' ||
|
||
!v.tax ||
|
||
!v.base ||
|
||
v.rate === null ||
|
||
!v.period
|
||
) {
|
||
pending.push(
|
||
`${r.documentRef}: rendimento indeterminado;${v.reason}`,
|
||
);
|
||
continue;
|
||
}
|
||
const name = rubric(r, v);
|
||
if (!name) {
|
||
pending.push(
|
||
`${r.documentRef}: rubrica MRFTA por corresponder;categoria digital/especial não copiada para serviços comuns.`,
|
||
);
|
||
continue;
|
||
}
|
||
const code = i.codes.find((c) => c.rowId === r.id);
|
||
if (
|
||
!code?.cer.trim() ||
|
||
!/^\d+$/.test(code.cer.trim()) ||
|
||
!code.reference.trim()
|
||
)
|
||
pending.push(
|
||
`${r.documentRef}: CER numérico e referência aplicável por identificar;sem código presumido.`,
|
||
);
|
||
const cer = code?.cer.trim() ?? '',
|
||
line = lines.find(
|
||
(l) => l.rubric === name && l.rate === v.rate && l.cer === cer,
|
||
);
|
||
const reference = `CER:${code?.reference ?? ''};contraparte:${r.counterparty};qualificação:${r.qualificationRef};benefício:${r.reliefPreparation?.evidence ?? r.reliefRef};residência fiscal:${r.reliefPreparation?.residenceDocument ?? ''}`;
|
||
if (line) {
|
||
line.ids.push(r.id);
|
||
line.documents.push(r.documentRef);
|
||
line.references.push(reference);
|
||
line.base = addFractions([line.base, v.base]);
|
||
line.tax = addFractions([line.tax, v.tax]);
|
||
line.paid =
|
||
line.paid && v.settled
|
||
? addFractions([line.paid, v.settled])
|
||
: null;
|
||
} else
|
||
lines.push({
|
||
ids: [r.id],
|
||
documents: [r.documentRef],
|
||
references: [reference],
|
||
rubric: name,
|
||
cer,
|
||
rate: v.rate,
|
||
base: v.base,
|
||
tax: v.tax,
|
||
paid: v.settled,
|
||
});
|
||
}
|
||
for (const line of lines)
|
||
if (
|
||
lines.some(
|
||
(other) => other !== line && other.rubric === line.rubric,
|
||
)
|
||
)
|
||
pending.push(
|
||
line.rubric +
|
||
': taxas ou CER distintos na mesma rubrica;conferir transcrição sem fundir taxas nem perder parcelas.',
|
||
);
|
||
const principal = addFractions(lines.map((l) => l.tax)),
|
||
paid = lines.every((l) => l.paid !== null)
|
||
? addFractions(lines.map((l) => l.paid!))
|
||
: null;
|
||
if (paid === null)
|
||
pending.push(
|
||
'Valor já pago não reconciliado;pagamento ao fornecedor não é entrega à AT.',
|
||
);
|
||
let interest: FiscalFraction | null = null,
|
||
interestReason =
|
||
'Juros compensatórios indeterminados: taxa aplicável, âmbito e período por resolver;sem MAIBOR2026 nem substituição automática por MIMO/Prime. Juros de mora e tributação autónoma não apurados.';
|
||
if (i.interest.mode === 'assessment') {
|
||
try {
|
||
const a = decimalFraction(i.interest.amount);
|
||
if (
|
||
negative(a) ||
|
||
i.interest.period !== i.period ||
|
||
!i.interest.reference.trim() ||
|
||
!date(i.interest.date) ||
|
||
i.interest.date.slice(0, 7) < i.period
|
||
)
|
||
throw Error();
|
||
interest = a;
|
||
interestReason = `Juros compensatórios transcritos de liquidação documentada:${i.interest.reference};data:${i.interest.date};período:${i.interest.period};montante original em MZN:${i.interest.amount}. Não são juros calculados pelo portal.`;
|
||
} catch {
|
||
interestReason =
|
||
'Liquidação de juros incompleta,negativa ou incoerente;identifique montante,data e documento do mesmo período.';
|
||
}
|
||
} else if (i.interest.mode === 'timely') {
|
||
const ids = lines.flatMap((l) => l.ids),
|
||
inputs = (b.result?.inputs ?? []).filter((r) => ids.includes(r.id));
|
||
const dates = inputs.flatMap((r) =>
|
||
r.allocations.map(
|
||
(a) =>
|
||
b.result?.payments.find((p) => p.id === a.paymentId)
|
||
?.date ?? '',
|
||
),
|
||
);
|
||
if (
|
||
i.interest.period === i.period &&
|
||
date(i.interest.dueDate) &&
|
||
i.interest.dueReference.trim() &&
|
||
i.interest.reference.trim() &&
|
||
paid !== null &&
|
||
isZero(subtractFractions(principal, paid)) &&
|
||
inputs.length > 0 &&
|
||
inputs.every(
|
||
(r) => r.settlement === 'complete' && r.allocations.length > 0,
|
||
) &&
|
||
dates.every((d) => date(d) && d <= i.interest.dueDate) &&
|
||
i.interest.dueDate >= i.period + '-01'
|
||
) {
|
||
interest = zero();
|
||
interestReason = `Entregas integralmente reconciliadas até ao prazo documentado:${i.interest.dueDate};${i.interest.dueReference};${i.interest.reference}. Sem atraso neste recorte;sem declarar cumprimento global de obrigações.`;
|
||
} else
|
||
interestReason =
|
||
'Ausência de atraso não demonstrada: confira prazo aplicável, comprovativos completos, datas e montante entregue. Sem zero de juros presumido.';
|
||
}
|
||
let automatic: IrpcInterestCalculation | undefined;
|
||
if (i.interest.mode === '' && i.interest.calculation === 'automatic') {
|
||
if (
|
||
!i.interest.automatic ||
|
||
i.interest.period !== i.period ||
|
||
!i.interest.reference.trim()
|
||
)
|
||
interestReason =
|
||
'Cálculo automático sem parâmetros/ocorrência e período documentados;juros continuam indeterminados.';
|
||
else {
|
||
automatic = calculateIrpcInterest(
|
||
b,
|
||
current,
|
||
i.period,
|
||
i.interest.dueDate,
|
||
i.interest.dueReference,
|
||
i.interest.date,
|
||
i.interest.automatic,
|
||
);
|
||
interest = automatic.total;
|
||
interestReason = automatic.pending.length
|
||
? automatic.pending.join(' | ')
|
||
: 'Juros calculados por parâmetros documentados deste caso — simulação pelos pressupostos introduzidos, sem confirmação fiscal;sem taxa fiscal2026 pré-definida. Taxa anual:' +
|
||
automatic.rule.annualRate +
|
||
'%;fonte:' +
|
||
automatic.rule.rateSource +
|
||
';fundamento:' +
|
||
automatic.rule.fiscalBasis +
|
||
';vigência:' +
|
||
automatic.rule.validFrom +
|
||
'–' +
|
||
automatic.rule.validTo +
|
||
';base anual:' +
|
||
(automatic.rule.dayBasis === 'actual_year'
|
||
? 'dias efectivos de cada ano'
|
||
: automatic.rule.dayBasis + ' dias') +
|
||
';fonte:' +
|
||
automatic.rule.dayBasisRef +
|
||
';arredondamento do total aos cêntimos:' +
|
||
(automatic.rule.rounding === 'total_cent_half_up'
|
||
? 'metade por excesso'
|
||
: 'por defeito') +
|
||
';fonte:' +
|
||
automatic.rule.roundingRef +
|
||
';ocorrência:' +
|
||
automatic.rule.occurrenceRef +
|
||
';' +
|
||
automatic.rows
|
||
.map(
|
||
(r) =>
|
||
r.document +
|
||
':' +
|
||
r.segments
|
||
.map(
|
||
(s) =>
|
||
s.from +
|
||
'–' +
|
||
s.to +
|
||
':' +
|
||
s.days +
|
||
'dias/' +
|
||
s.denominator +
|
||
';principal ' +
|
||
money(s.principal) +
|
||
';juros ' +
|
||
money(s.interest),
|
||
)
|
||
.join(','),
|
||
)
|
||
.join(' | ');
|
||
}
|
||
}
|
||
if (!interest) pending.push(interestReason);
|
||
let total: FiscalFraction | null = null;
|
||
if (pending.length === 0 && interest && paid) {
|
||
const balance = subtractFractions(
|
||
addFractions([principal, interest]),
|
||
paid,
|
||
);
|
||
if (negative(balance))
|
||
pending.push(
|
||
'Valor já pago excede imposto e juros;reconcilie sem crédito/reembolso automático.',
|
||
);
|
||
else total = balance;
|
||
}
|
||
return {
|
||
...(automatic ? { automatic } : {}),
|
||
period: i.period,
|
||
input: structuredClone(i),
|
||
lines,
|
||
excluded,
|
||
pending: [...new Set(pending)],
|
||
principal,
|
||
paid,
|
||
interest,
|
||
total,
|
||
interestReason,
|
||
source: mrftaSource,
|
||
};
|
||
}
|
||
export function recordMrfta(
|
||
b: IrpcBook & MrftaStore,
|
||
current: boolean,
|
||
now = new Date().toISOString(),
|
||
): IrpcBook & MrftaStore {
|
||
const model = prepareMrfta(b, current);
|
||
return {
|
||
...b,
|
||
mrftaHistory: b.mrftaResult
|
||
? [...(b.mrftaHistory ?? []), structuredClone(b.mrftaResult)]
|
||
: (b.mrftaHistory ?? []),
|
||
mrftaResult: { key: key(b, current), createdAt: now, model },
|
||
};
|
||
}
|
||
const money = (f: FiscalFraction | null) => {
|
||
if (!f) return 'Indeterminado';
|
||
const p = fractionPresentation(f);
|
||
return (p.approximate ? '≈' : '') + p.decimal + ' MZN';
|
||
};
|
||
export function mrftaReportRows(
|
||
b: IrpcBook & MrftaStore,
|
||
current: boolean,
|
||
): Array<[string, string, string, string]> {
|
||
const r = b.mrftaResult;
|
||
if (!r) return [];
|
||
const m = r.model;
|
||
return [
|
||
[
|
||
'IRPC — retenções',
|
||
'MRFTA — ' + m.period,
|
||
`Subtotal das retenções correspondidas:${money(m.principal)};juros:${money(m.interest)};valor já pago:${money(m.paid)};total a pagar:${money(m.total)};${m.interestReason};pendências:${m.pending.join(' | ')}`,
|
||
`${mrftaCurrent(b, current) ? 'Preparação correspondente' : 'Histórico;preparação desactualizada'};${r.createdAt};completude:${m.input.completenessRef};outras rubricas:${m.input.otherRubricsRef};${m.source}`,
|
||
],
|
||
...m.lines.map(
|
||
(l) =>
|
||
[
|
||
'IRPC — retenções',
|
||
l.rubric,
|
||
`${l.documents.join(';')};base:${money(l.base)};taxa:${l.rate}%;imposto:${money(l.tax)};CER:${l.cer || 'por corresponder'};código introduzido,sem validação por catálogo oficial`,
|
||
`Período:${m.period};apenas rendimento pago;sem assinatura ou uso dos serviços;documentos:${l.ids.join(',')};suporte:${l.references.join(' | ')}`,
|
||
] as [string, string, string, string],
|
||
),
|
||
];
|
||
}
|
||
export function mrftaHtml(b: IrpcBook & MrftaStore, current: boolean): string {
|
||
if (!current || !mrftaCurrent(b, current))
|
||
throw Error(
|
||
'Calcule as retenções e prepare o MRFTA com as entradas actuais antes de exportar.',
|
||
);
|
||
const m = b.mrftaResult!.model,
|
||
esc = (v: unknown) =>
|
||
String(v).replace(
|
||
/[&<>"']/g,
|
||
(c) =>
|
||
({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
})[c]!,
|
||
);
|
||
return `<!doctype html><html lang="pt"><meta charset="utf-8"><title>MRFTA — preparação</title><body><h1>MRFTA — preenchimento de preparação</h1><p>Quadro1:retenção na fonte;${esc(m.input.declaration === 'initial' ? 'Inicial' : m.input.declaration === 'replacement' ? 'Substituição' : 'Tipo por identificar')}. Quadro2:${esc(m.period)};${esc(m.input.timing === 'within' ? 'Dentro do prazo' : m.input.timing === 'late' ? 'Fora do prazo' : 'Prazo por identificar')}.</p><p>Quadro3 NUIT:${esc(m.input.nuit)};área fiscal:${esc(m.input.taxArea)}. Quadro4:${esc(m.input.name)}. Quadro5:${esc(m.input.commercialName)}. Quadro6:${esc(m.input.address)};${esc(m.input.contact)}.</p><p>Conferência do período:${esc(m.input.completenessRef)}. Outras rubricas:${esc(m.input.otherRubricsRef)}.</p><p>CER introduzidos com referência do caso; sem validação automática por catálogo oficial. Códigos provisórios ou de simulação não são correspondências oficiais.</p><h2>Quadro7</h2><table><thead><tr><th>CER</th><th>Rubrica</th><th>Base</th><th>Taxa</th><th>Imposto</th><th>Documentos</th></tr></thead><tbody>${m.lines.map((l) => `<tr><td>${esc(l.cer || 'Por corresponder')}</td><td>${esc(l.rubric)}</td><td>${esc(money(l.base))}</td><td>${l.rate}%</td><td>${esc(money(l.tax))}</td><td>${esc(l.documents.join(';') + ';' + l.references.join(' | '))}</td></tr>`).join('')}</tbody></table><p>Subtotal correspondido:${esc(money(m.principal))};JUROS COMPENSATÓRIOS:${esc(money(m.interest))};VALOR JÁ PAGO:${esc(money(m.paid))};TOTAL A PAGAR:${esc(money(m.total))}.</p><p>${esc(m.interestReason)}</p><h2>Casos pendentes</h2><ul>${m.pending.map((p) => '<li>' + esc(p) + '</li>').join('')}</ul><h2>Casos separados</h2><ul>${m.excluded.map((p) => '<li>' + esc(p.id + ':' + p.reason) + '</li>').join('')}</ul><p>Quadros8/9 reservados,sem assinatura,carimbo,número de entrada ou aceitação AT. Tributação autónoma e juros de mora não apurados. Este documento de preparação não substitui o original oficial nem submete uma declaração.</p><small>${esc(m.source)}</small></body></html>`;
|
||
}
|