699 lines
28 KiB
TypeScript
699 lines
28 KiB
TypeScript
import { displayValue } from './display-value.ts';
|
||
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
|
||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||
import { csvCellWithDelimiter } from './csv.js';
|
||
import {
|
||
restoreCashflowDraftSnapshot,
|
||
projectMonthlyCashflow,
|
||
} from './monthly-cashflow.ts';
|
||
import {
|
||
calculateFinancialTools,
|
||
restoreFinancialToolsSnapshot,
|
||
} from './financial-calculators.ts';
|
||
import {
|
||
forecastBusinessPlan,
|
||
restoreBusinessPlanSnapshot,
|
||
} from './business-financial-plan.ts';
|
||
import {
|
||
getProjectReadiness,
|
||
readPlannedRiskResponses,
|
||
} from './project-readiness.ts';
|
||
|
||
export type TeamNeed = {
|
||
competency: string;
|
||
state: 'available' | 'gap' | 'unknown';
|
||
action: string;
|
||
estimated_cost: string;
|
||
due_date: string;
|
||
};
|
||
|
||
export type ProjectMilestone = {
|
||
title: string;
|
||
due_date: string;
|
||
status: 'planned' | 'in_progress' | 'completed';
|
||
planned_amount: string;
|
||
actual_amount: string;
|
||
};
|
||
|
||
export function readRows<T>(value: string, allowed: readonly string[]): T[] {
|
||
try {
|
||
const parsed: unknown = JSON.parse(value || '[]');
|
||
if (!Array.isArray(parsed)) return [];
|
||
return parsed.filter((row): row is T =>
|
||
Boolean(
|
||
row &&
|
||
typeof row === 'object' &&
|
||
!Array.isArray(row) &&
|
||
allowed.every(
|
||
(key) =>
|
||
typeof (row as Record<string, unknown>)[key] ===
|
||
'string',
|
||
),
|
||
),
|
||
);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export type ProjectDossierRow = [
|
||
section: string,
|
||
field: string,
|
||
value: string,
|
||
origin: string,
|
||
];
|
||
|
||
export function buildProjectDossierRows(
|
||
draft: Record<string, string | string[]>,
|
||
): ProjectDossierRow[] {
|
||
const rows: ProjectDossierRow[] = [
|
||
['Secção', 'Campo', 'Valor', 'Origem / estado'],
|
||
[
|
||
'Projecto',
|
||
'Título',
|
||
String(draft.project_title ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Versão do questionário',
|
||
String(draft.project_questionnaire_version ?? 'legacy-unversioned'),
|
||
'Metadado do questionário; não altera resultados guardados',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Resumo',
|
||
String(draft.project_summary ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Problema',
|
||
String(draft.project_problem ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Clientes/beneficiários',
|
||
String(draft.target_customers ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Proposta de valor',
|
||
String(draft.value_proposition ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Tipo de projecto',
|
||
String(draft.project_type_label ?? draft.project_type ?? ''),
|
||
'Declarado/seleccionado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Sector',
|
||
String(draft.sector_id_label ?? draft.sector_id ?? ''),
|
||
'Selecção declarada pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Actividade económica',
|
||
String(draft.activity_id_label ?? draft.activity_id ?? ''),
|
||
'Selecção declarada pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Produtos e serviços',
|
||
String(draft.products_services ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Fase de maturidade',
|
||
String(draft.maturity_stage_label ?? draft.maturity_stage ?? ''),
|
||
'Declarada pelo utilizador',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Data prevista de início',
|
||
String(draft.expected_start_date ?? ''),
|
||
'Declarada pelo utilizador; data não verificada',
|
||
],
|
||
[
|
||
'Projecto',
|
||
'Duração prevista',
|
||
draft.project_duration_years
|
||
? `${displayValue(draft.project_duration_years)} anos`
|
||
: '',
|
||
'Declarada pelo utilizador',
|
||
],
|
||
[
|
||
'Mercado',
|
||
'Hipótese',
|
||
String(draft.market_assumption ?? ''),
|
||
'Informação declarada, não verificada',
|
||
],
|
||
[
|
||
'Mercado',
|
||
'Fonte e data',
|
||
String(draft.market_source ?? ''),
|
||
'Opcional; não verificada',
|
||
],
|
||
[
|
||
'Mercado',
|
||
'Concorrência e alternativas',
|
||
String(draft.competitor_assumption ?? ''),
|
||
'Hipótese declarada; não verificada',
|
||
],
|
||
[
|
||
'Mercado',
|
||
'Canal de venda',
|
||
String(draft.sales_channel_assumption ?? ''),
|
||
'Planeamento declarado; não confirmado',
|
||
],
|
||
[
|
||
'Mercado',
|
||
'Acções de divulgação',
|
||
String(draft.marketing_approach ?? ''),
|
||
'Planeamento declarado; não confirmado',
|
||
],
|
||
[
|
||
'Localização',
|
||
'Província',
|
||
String(draft.province_id_label ?? ''),
|
||
'Selecção de catálogo territorial',
|
||
],
|
||
[
|
||
'Localização',
|
||
'Distrito',
|
||
String(draft.district_id_label ?? ''),
|
||
'Selecção de catálogo territorial',
|
||
],
|
||
[
|
||
'Localização',
|
||
'Posto administrativo',
|
||
String(draft.admin_post_id_label ?? ''),
|
||
'Selecção de catálogo territorial',
|
||
],
|
||
[
|
||
'Localização',
|
||
'Localidade',
|
||
String(draft.locality_id_label ?? ''),
|
||
'Selecção de catálogo territorial',
|
||
],
|
||
[
|
||
'Localização',
|
||
'Critérios e pontos a confirmar',
|
||
String(draft.location_rationale ?? ''),
|
||
'Declaração do utilizador; não confirma disponibilidade',
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Estado do plano detalhado',
|
||
draft.business_plan_snapshot
|
||
? 'Snapshot versionado disponível; usar também a exportação do plano financeiro'
|
||
: 'Sem plano empresarial detalhado preenchido',
|
||
'Pressupostos declarados; não é demonstração contabilística',
|
||
],
|
||
[
|
||
'Equipa',
|
||
'Nota anterior sobre competências',
|
||
String(draft.skills_needed ?? ''),
|
||
'Texto declarado existente',
|
||
],
|
||
[
|
||
'Execução',
|
||
'Nota anterior sobre marcos',
|
||
String(draft.project_milestones ?? ''),
|
||
'Texto declarado existente',
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Estado de pré-elegibilidade',
|
||
'Informação insuficiente',
|
||
'Sem catálogo documentado de produtos e critérios',
|
||
],
|
||
[
|
||
'Necessidade de negócio',
|
||
'Descrição',
|
||
String(draft.opportunity_description ?? ''),
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Necessidade de negócio',
|
||
'Infra-estrutura necessária',
|
||
(Array.isArray(draft.infrastructure_needs)
|
||
? draft.infrastructure_needs
|
||
: []
|
||
).join(', '),
|
||
'Declarado/seleccionado pelo utilizador',
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Montante pretendido',
|
||
String(draft.opportunity_value ?? ''),
|
||
'Declarado pelo utilizador; ver moeda separada',
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Moeda do montante',
|
||
String(
|
||
draft.opportunity_currency_label ??
|
||
draft.opportunity_currency ??
|
||
'',
|
||
),
|
||
'Declarada/seleccionada; sem avaliação de produto',
|
||
],
|
||
];
|
||
const plannedResponses = readPlannedRiskResponses(draft);
|
||
for (const item of getProjectReadiness(draft)) {
|
||
const response = plannedResponses.find(
|
||
(entry) => entry.risk_id === item.id,
|
||
);
|
||
const responseText = response
|
||
? [
|
||
response.action,
|
||
response.owner ? `Responsável: ${response.owner}` : '',
|
||
response.estimated_cost
|
||
? `Custo previsto: ${response.estimated_cost} ${String(draft.currency ?? '')}`
|
||
: '',
|
||
response.due_date ? `Prazo: ${response.due_date}` : '',
|
||
]
|
||
.filter(Boolean)
|
||
.join(' · ')
|
||
: 'Resposta ainda não registada';
|
||
rows.push([
|
||
'Risco',
|
||
item.title,
|
||
`${item.origin} ${item.affected} Consequência possível: ${item.consequence} Acção: ${item.nextAction} Resposta planeada: ${responseText}`,
|
||
'Derivado das respostas e campos em falta; estimativa qualitativa, sem probabilidade ou gravidade atribuída',
|
||
]);
|
||
}
|
||
const finance = restoreFinancialToolsSnapshot(
|
||
String(draft.financial_tools_snapshot ?? ''),
|
||
);
|
||
if (finance) {
|
||
const calculation = calculateFinancialTools({
|
||
loan: {
|
||
principal: finance.loan.principal,
|
||
annualRatePercent: finance.loan.rate,
|
||
rateType: finance.loan.rateType,
|
||
termMonths: Number(finance.loan.months),
|
||
periodMonths: Number(finance.loan.periodMonths),
|
||
gracePeriods: Number(finance.loan.gracePeriods),
|
||
upfrontFee: finance.loan.upfrontFee,
|
||
recurringFee: finance.loan.recurringFee,
|
||
},
|
||
breakEven: {
|
||
fixedCosts: finance.breakEven.fixed,
|
||
unitPrice: finance.breakEven.price,
|
||
variableCostPerUnit: finance.breakEven.variable,
|
||
},
|
||
funding: {
|
||
totalInvestment: finance.funding.total,
|
||
securedFunding: finance.funding.secured,
|
||
},
|
||
incomes: finance.incomes.map((row) => row.amount),
|
||
expenses: finance.expenses.map((row) => row.amount),
|
||
});
|
||
const currency = String(draft.currency ?? '') || 'moeda não indicada';
|
||
const source =
|
||
'Estimativa matemática baseada em pressupostos declarados; não é oferta de crédito nem resultado oficial';
|
||
rows.push(
|
||
[
|
||
'Plano financeiro',
|
||
'Periodicidade do orçamento',
|
||
finance.budgetPeriod,
|
||
'Declarada pelo utilizador',
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Capital pressuposto',
|
||
`${finance.loan.principal} ${currency}`,
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Taxa anual pressuposta',
|
||
`${finance.loan.rate}% (${finance.loan.rateType === 'nominal' ? 'nominal anual' : 'efectiva anual'})`,
|
||
'Declarada pelo utilizador; não é taxa de produto',
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Prazo pressuposto',
|
||
`${finance.loan.months} meses`,
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Método e pressupostos substituídos',
|
||
`Saldo decrescente; capital ${finance.loan.principal} ${currency}; taxa ${finance.loan.rate}% ${finance.loan.rateType === 'nominal' ? 'nominal anual (dividida pelos períodos do ano)' : 'efectiva anual (convertida para o período)'}; prazo ${finance.loan.months} meses; periodicidade ${finance.loan.periodMonths} meses; carência ${finance.loan.gracePeriods} períodos; comissão inicial ${finance.loan.upfrontFee} ${currency}; comissão periódica ${finance.loan.recurringFee} ${currency}. Montantes a duas casas decimais; juros e prestação arredondados à unidade monetária mínima, metade para cima; última prestação ajusta o saldo residual.`,
|
||
'Cronograma do simulador; taxa declarada, sem correspondência a produto',
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Prestação do primeiro período',
|
||
calculation.loan.value?.schedule[0]?.totalPayment
|
||
? `${calculation.loan.value.schedule[0].totalPayment} ${currency}`
|
||
: 'Indeterminado',
|
||
source,
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Juros totais estimados',
|
||
calculation.loan.value?.totalInterest
|
||
? `${calculation.loan.value.totalInterest} ${currency}`
|
||
: 'Indeterminado',
|
||
source,
|
||
],
|
||
[
|
||
'Empréstimo educativo',
|
||
'Estado/erro do cálculo',
|
||
calculation.loan.error ??
|
||
(calculation.loan.value
|
||
? 'Calculado pelo cronograma do simulador'
|
||
: 'Indeterminado'),
|
||
source,
|
||
],
|
||
[
|
||
'Ponto de equilíbrio',
|
||
'Fórmula e valores substituídos',
|
||
`${finance.breakEven.fixed} / (${finance.breakEven.price} − ${finance.breakEven.variable}); unidades arredondadas para cima`,
|
||
'Custos fixos ÷ margem de contribuição unitária; pressupostos declarados',
|
||
],
|
||
[
|
||
'Ponto de equilíbrio',
|
||
'Unidades estimadas',
|
||
calculation.breakEven.value?.breakEvenUnits ?? 'Indeterminado',
|
||
source,
|
||
],
|
||
[
|
||
'Ponto de equilíbrio',
|
||
'Vendas correspondentes',
|
||
calculation.breakEven.value?.breakEvenRevenue
|
||
? `${calculation.breakEven.value.breakEvenRevenue} ${currency}`
|
||
: 'Indeterminado',
|
||
source,
|
||
],
|
||
[
|
||
'Ponto de equilíbrio',
|
||
'Estado/erro do cálculo',
|
||
calculation.breakEven.error ??
|
||
(calculation.breakEven.value
|
||
? 'Calculado pelo simulador'
|
||
: 'Indeterminado'),
|
||
source,
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Investimento total pressuposto',
|
||
`${finance.funding.total} ${currency}`,
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Montante já mobilizado pressuposto',
|
||
`${finance.funding.secured} ${currency}`,
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Fórmula e valores substituídos',
|
||
`${finance.funding.total} − ${finance.funding.secured}`,
|
||
'Investimento total declarado − financiamento mobilizado declarado',
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Diferença por financiar',
|
||
calculation.funding.value?.gap
|
||
? `${calculation.funding.value.gap} ${currency}`
|
||
: calculation.funding.value?.gap === '0.00'
|
||
? `0.00 ${currency}`
|
||
: 'Indeterminado',
|
||
source,
|
||
],
|
||
[
|
||
'Financiamento',
|
||
'Estado/erro do cálculo',
|
||
calculation.funding.error ??
|
||
(calculation.funding.value
|
||
? 'Calculado pelo simulador'
|
||
: 'Indeterminado'),
|
||
source,
|
||
],
|
||
[
|
||
'Orçamento',
|
||
'Fórmula e valores substituídos',
|
||
`${calculation.budget.value?.incomeTotal ?? 'Não informado'} − ${calculation.budget.value?.expenseTotal ?? 'Não informado'}`,
|
||
'Receitas declaradas − despesas declaradas; valores ausentes não são zero',
|
||
],
|
||
[
|
||
'Orçamento',
|
||
'Receitas declaradas',
|
||
calculation.budget.value?.incomeTotal
|
||
? `${calculation.budget.value.incomeTotal} ${currency}`
|
||
: 'Indeterminado',
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Orçamento',
|
||
'Despesas declaradas',
|
||
calculation.budget.value?.expenseTotal
|
||
? `${calculation.budget.value.expenseTotal} ${currency}`
|
||
: 'Indeterminado',
|
||
'Declarado pelo utilizador',
|
||
],
|
||
[
|
||
'Orçamento',
|
||
'Saldo operacional estimado',
|
||
calculation.budget.value?.operatingBalance
|
||
? `${calculation.budget.value.operatingBalance} ${currency}`
|
||
: 'Indeterminado',
|
||
source,
|
||
],
|
||
[
|
||
'Orçamento',
|
||
'Estado/erro do cálculo',
|
||
calculation.budget.error ??
|
||
(calculation.budget.value
|
||
? 'Calculado pelo simulador'
|
||
: 'Indeterminado'),
|
||
source,
|
||
],
|
||
);
|
||
for (const income of finance.incomes) {
|
||
rows.push([
|
||
'Orçamento',
|
||
`Receita declarada: ${income.label}`,
|
||
`${income.amount} ${currency}`,
|
||
'Declarado pelo utilizador',
|
||
]);
|
||
}
|
||
for (const expense of finance.expenses) {
|
||
rows.push([
|
||
'Orçamento',
|
||
`Despesa declarada: ${expense.label}`,
|
||
`${expense.amount} ${currency}`,
|
||
'Declarado pelo utilizador',
|
||
]);
|
||
}
|
||
}
|
||
const businessPlan = restoreBusinessPlanSnapshot(
|
||
String(draft.business_plan_snapshot ?? ''),
|
||
);
|
||
if (businessPlan) {
|
||
try {
|
||
const forecast = forecastBusinessPlan(businessPlan);
|
||
const source =
|
||
'Estimativa calculada pelo plano financeiro a partir de pressupostos declarados; não é demonstração contabilística nem resultado confirmado';
|
||
rows.push(
|
||
[
|
||
'Plano financeiro',
|
||
'Moeda do plano',
|
||
businessPlan.currency,
|
||
'Declarada pelo utilizador',
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Horizonte',
|
||
`${businessPlan.periods ?? 12} meses`,
|
||
'Pressuposto declarado',
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Facturação no horizonte',
|
||
`${forecast.totalRevenue} ${businessPlan.currency}`,
|
||
source,
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Fórmula e valores substituídos',
|
||
`Facturação: quantidade mensal × preço unitário por produto; caixa final: saldo de abertura + cobranças e fontes − pagamentos. Horizonte: ${businessPlan.periods ?? 12} meses; moeda: ${businessPlan.currency}.`,
|
||
'Métodos do plano financeiro; agendas e pressupostos declarados no snapshot',
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Cobranças no horizonte',
|
||
`${forecast.totalCashCollections} ${businessPlan.currency}`,
|
||
source,
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Pagamentos no horizonte',
|
||
`${forecast.totalPayments} ${businessPlan.currency}`,
|
||
source,
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Resultado antes de impostos',
|
||
`${forecast.totalProfitBeforeTax} ${businessPlan.currency}`,
|
||
source,
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Caixa final projectada',
|
||
`${forecast.endingCash} ${businessPlan.currency}`,
|
||
source,
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Reconciliação do financiamento',
|
||
forecast.fundingStatus,
|
||
'Comparação calculada entre usos e fontes declarados',
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Diferença entre usos e fontes',
|
||
`${forecast.fundingGap} ${businessPlan.currency}`,
|
||
source,
|
||
],
|
||
[
|
||
'Plano financeiro',
|
||
'Taxa de câmbio de apresentação',
|
||
businessPlan.fxRate
|
||
? `${businessPlan.fxRate}; ${businessPlan.fxDate ?? 'data não informada'}; ${businessPlan.fxSource ?? 'fonte não informada'}`
|
||
: 'Não utilizada',
|
||
businessPlan.fxRate
|
||
? 'Pressuposto de planeamento declarado pelo utilizador; não verificado'
|
||
: 'Sem conversão',
|
||
],
|
||
);
|
||
} catch {
|
||
rows.push([
|
||
'Plano financeiro',
|
||
'Estado do cálculo',
|
||
'Indeterminado',
|
||
'Snapshot inválido ou pressupostos incompatíveis; reveja o plano antes de exportar',
|
||
]);
|
||
}
|
||
}
|
||
for (const [index, need] of readRows<TeamNeed>(
|
||
String(draft.project_team_snapshot ?? ''),
|
||
['competency', 'state', 'action', 'estimated_cost', 'due_date'],
|
||
).entries()) {
|
||
rows.push([
|
||
'Equipa',
|
||
`Necessidade ${index + 1}`,
|
||
`${need.competency}; ${need.state}; resposta: ${need.action}; custo: ${need.estimated_cost}; prazo: ${need.due_date}`,
|
||
'Declarado pelo utilizador; sem certificação ou correspondência',
|
||
]);
|
||
}
|
||
for (const [index, milestone] of readRows<ProjectMilestone>(
|
||
String(draft.project_execution_snapshot ?? ''),
|
||
['title', 'due_date', 'status', 'planned_amount', 'actual_amount'],
|
||
).entries()) {
|
||
rows.push([
|
||
'Execução',
|
||
`Marco ${index + 1}`,
|
||
`${milestone.title}; ${milestone.status}; prazo: ${milestone.due_date}; planeado: ${milestone.planned_amount}; realizado: ${milestone.actual_amount}`,
|
||
'Planeado/realizado declarado pelo utilizador',
|
||
]);
|
||
}
|
||
if (draft.monthly_iva_snapshot) {
|
||
const saved = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot));
|
||
let current = false;
|
||
try { current = !!saved && monthlyIvaKey(monthlyIvaInput(draft, readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? '')))) === monthlyIvaKey(saved.input); } catch { /* An old fiscal version is never silently made current. */ }
|
||
rows.push(['IVA mensal', 'Estado da versão', current ? 'Entradas correspondentes à projecção guardada' : 'Histórico desactualizado face às vendas ou pressupostos actuais; não aplicar como resultado actual', 'Execução versionada no servidor; sem apuramento final']);
|
||
if (saved?.execution.result_snapshot) for (const [label,value] of fiscalQaRows(saved.execution.result_snapshot)) rows.push(['IVA mensal', label, value, 'Snapshot fiscal preservado; sem recálculo na exportação']);
|
||
else rows.push(['IVA mensal', 'Resultado', 'Indeterminado ou execução ainda não concluída', 'Sem substituição por zero']);
|
||
try {
|
||
for (const purchase of modeloAPurchaseReferences(draft)) rows.push(['IVA mensal', `Compras/encomendas — mês ${purchase.month}`, purchase.declared ?? 'Sem informação', 'Pressuposto actual do plano, não resultado fiscal histórico; base/IVA incluído por classificar; IVA suportado e dedutível indeterminados; não é data de factura ou pagamento']);
|
||
} catch {
|
||
rows.push(['IVA mensal','Aquisições projectadas','Não foi possível recuperar; sem substituição por zero','Pressupostos guardados preservados; não calcula deduções']);
|
||
}
|
||
}
|
||
if (draft.cashflow_snapshot) {
|
||
const saved = restoreCashflowDraftSnapshot(
|
||
String(draft.cashflow_snapshot),
|
||
);
|
||
const origin =
|
||
'Estimativa de planeamento; pressupostos guardados da ferramenta que podem incluir exemplos iniciais. Não confirma impostos nem recebimentos.';
|
||
try {
|
||
if (!saved)
|
||
throw new Error(
|
||
'Pressupostos de tesouraria não recuperáveis; não foram substituídos por valores alternativos.',
|
||
);
|
||
const currency = String(draft.currency ?? '');
|
||
if (!currency)
|
||
throw new Error(
|
||
'Indique uma moeda para interpretar os montantes de tesouraria.',
|
||
);
|
||
const input = {
|
||
...saved.inputs,
|
||
currency,
|
||
...(saved.taxMode === 'fixed'
|
||
? { monthlyTaxSchedule: undefined }
|
||
: { monthlyTaxAssessed: '' }),
|
||
};
|
||
const projection = projectMonthlyCashflow(input);
|
||
const amount = (value: string | null) =>
|
||
value === null ? 'Indeterminado' : `${value} ${currency}`;
|
||
rows.push([
|
||
'Tesouraria',
|
||
'Pressupostos e período',
|
||
`12 meses; moeda ${currency}; saldo inicial ${saved.inputs.openingCash}; receita mensal ${saved.inputs.monthlyRevenue}; custos mensais ${saved.inputs.monthlyOperatingCosts}; dívida mensal ${saved.inputs.monthlyDebtService}; imposto ${saved.taxMode === 'fixed' ? 'mensal fixo: ' + (saved.inputs.monthlyTaxAssessed || 'não informado') : 'variável por mês (vazio não é zero)'}. Crédito estimado ${saved.inputs.vatCreditEstimated}; pedido ${saved.inputs.vatRefundRequested}; reconhecido ${saved.inputs.vatRefundRecognized}; recebimento previsto ${saved.inputs.vatRefundExpectedReceipt}; mês previsto ${saved.inputs.vatRefundExpectedMonth || 'não informado'}.`,
|
||
origin,
|
||
]);
|
||
rows.push([
|
||
'Tesouraria',
|
||
'Saldo final total',
|
||
amount(projection.endingCash),
|
||
origin,
|
||
]);
|
||
rows.push([
|
||
'Tesouraria',
|
||
'Caixa conhecida antes de imposto em falta',
|
||
amount(projection.endingCashBeforeUnassessedTax),
|
||
origin,
|
||
]);
|
||
for (const row of projection.rows)
|
||
rows.push([
|
||
'Tesouraria',
|
||
`Mês ${row.month}`,
|
||
`Imposto assumido: ${amount(row.taxAssessed)}; recebimento de IVA previsto: ${amount(row.vatRefundExpectedReceipt)}; fluxo líquido: ${amount(row.netFlow)}; saldo final total: ${amount(row.closingCash)}.`,
|
||
origin,
|
||
]);
|
||
} catch (cause) {
|
||
rows.push([
|
||
'Tesouraria',
|
||
'Estado da tesouraria',
|
||
cause instanceof Error
|
||
? cause.message
|
||
: 'Tesouraria indeterminada; reveja os pressupostos.',
|
||
origin,
|
||
]);
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
export function exportProjectDossierCsv(
|
||
draft: Record<string, string | string[]>,
|
||
): string {
|
||
const rows = buildProjectDossierRows(draft);
|
||
// Semicolon is the active list separator in the target Excel locale (pt-MZ).
|
||
return `\uFEFF${rows.map((row) => row.map((value) => csvCellWithDelimiter(value, ';')).join(';')).join('\r\n')}`;
|
||
}
|