diff --git a/backend/src/routes/taxreport.js b/backend/src/routes/taxreport.js index 6711dec..008a01b 100644 --- a/backend/src/routes/taxreport.js +++ b/backend/src/routes/taxreport.js @@ -623,4 +623,173 @@ router.get('/2778', (req, res) => { res.json({ annee, plateformes }); }); + + +/* ── 2047 — Cadre 2, rubrique 230 (Intérêts) — vision annuelle par pays/plateforme ── + Détermine, pour chaque plateforme étrangère × investisseur, si les intérêts perçus + ouvrent droit à un crédit d'impôt conventionnel (cases 231-238) ou non (case 250), + en s'appuyant sur le référentiel pays "taux_credit_impot" (paramétrage admin). + Éligibilité = convention active + taux d'intérêt renseigné + non "exclusif résidence" + (le pictogramme c/ de la notice signifie imposable exclusivement en France : aucun + crédit d'impôt possible, même si la plateforme a réellement prélevé une taxe locale). ── */ +router.get('/2047', (req, res) => { + const annee = req.query.annee || String(new Date().getFullYear() - 1); + const scopeAll = req.query.scope === 'all'; + const invCond = scopeAll + ? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' + : 'i.investisseur_id = ?'; + const invArg = scopeAll ? req.user.id : req.investisseur.id; + + const rows = db.prepare(` + SELECT + p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation, + p.fiscalite, p.taux_fiscalite_locale, + inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom, + COALESCE(SUM( + CASE WHEN r.interets_bruts_avant_local IS NOT NULL AND r.interets_bruts_avant_local > 0 + THEN r.interets_bruts_avant_local ELSE r.interets_bruts END + ), 0) AS montant_avant_local, + COALESCE(SUM(r.interets_bruts), 0) AS montant_net, + COALESCE(SUM(r.taxe_locale), 0) AS impot_local + FROM remboursements r + JOIN investissements i ON i.id = r.investissement_id + JOIN plateformes p ON p.id = i.plateforme_id + JOIN investisseurs inv ON inv.id = i.investisseur_id + WHERE ${invCond} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'normal' + AND p.domiciliation != 'FR' + GROUP BY p.id, inv.id + `).all(invArg, annee); + + // Intérêts plateforme (bonus, sans investissement rattaché) — même logique de fusion que /2778 + const invCondBonus2047 = scopeAll + ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' + : 'r.bonus_investisseur_id = ?'; + const rowsInteretsPlateforme = db.prepare(` + SELECT + p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation, + p.fiscalite, p.taux_fiscalite_locale, + inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom, + COALESCE(SUM( + CASE WHEN r.interets_bruts_avant_local IS NOT NULL AND r.interets_bruts_avant_local > 0 + THEN r.interets_bruts_avant_local ELSE r.interets_bruts END + ), 0) AS montant_avant_local, + COALESCE(SUM(r.interets_bruts), 0) AS montant_net, + COALESCE(SUM(r.taxe_locale), 0) AS impot_local + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + JOIN investisseurs inv ON inv.id = r.bonus_investisseur_id + WHERE ${invCondBonus2047} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + AND p.domiciliation != 'FR' + GROUP BY p.id, inv.id + `).all(invArg, annee); + + const rowsMap2047 = {}; + for (const r0 of rows) rowsMap2047[`${r0.plateforme_id}_${r0.investisseur_id}`] = { ...r0 }; + for (const r0 of rowsInteretsPlateforme) { + const key = `${r0.plateforme_id}_${r0.investisseur_id}`; + if (rowsMap2047[key]) { + rowsMap2047[key].montant_avant_local += r0.montant_avant_local; + rowsMap2047[key].montant_net += r0.montant_net; + rowsMap2047[key].impot_local += r0.impot_local; + } else { + rowsMap2047[key] = { ...r0 }; + } + } + const rowsMerged2047 = Object.values(rowsMap2047).sort((a, b) => + a.plateforme_nom.localeCompare(b.plateforme_nom) || (a.investisseur_nom ?? '').localeCompare(b.investisseur_nom ?? '')); + + // Référentiel pays (taux crédit d'impôt) — jointure par code pays (= plateformes.domiciliation) + const paysRows = db.prepare('SELECT * FROM taux_credit_impot').all(); + const paysMap = {}; + for (const p of paysRows) { + if (p.code_pays) paysMap[p.code_pays.trim().toUpperCase()] = p; + } + + const lignes = rowsMerged2047.map(r0 => { + const pays = paysMap[(r0.domiciliation || '').trim().toUpperCase()] || null; + // Règle fiscale : les montants portés sur les cases d'une déclaration sont arrondis à + // l'euro le plus proche (même convention que les cases BA/IA/QR du 2778-SD — cf. computeCases + // côté frontend). On arrondit donc ici la base (net encaissé / impôt local) avant d'en dériver + // les cases 235/237/238, pour rester cohérent avec le mode de calcul du 2778-SD. + const montantNet = Math.round(r0.montant_net); + const impotLocal = Math.round(r0.impot_local); + + // Éligible aux cases 231-238 seulement si DEUX conditions sont réunies : + // 1) le pays ouvre droit à un crédit d'impôt (convention active, non "exclusif résidence" — + // pictogramme c/ = imposable exclusivement en France, donc aucun crédit possible) ; + // 2) la plateforme a réellement prélevé un impôt à la source sur l'année (impotLocal > 0). + // Sans retenue réelle, il n'y a rien à créditer : la ligne va en case 250 avec son montant + // net intégral (= montant brut, puisqu'aucun impôt étranger n'a été prélevé dessus), même + // si le pays serait par ailleurs éligible (ex. plateforme "sans fiscalité locale"). + const paysEligible = !!pays + && pays.statut_convention === 'active' + && !pays.int_exclusif_residence; + const eligible = paysEligible && impotLocal > 0; + // Certains pays (ex. Bénin, Saint-Barthélemy) n'ont pas de taux forfaitaire chiffré dans la + // notice ("le crédit d'impôt est égal à l'impôt prélevé", sans plafond en %) : le crédit est + // alors non plafonné et égal à l'impôt réellement supporté à l'étranger. + const tauxPlafonne = eligible && pays.int_taux != null; + + const tauxApplicable = tauxPlafonne ? pays.int_taux : null; + const resultat = tauxPlafonne ? Math.round(montantNet * pays.int_taux / 100) : (eligible ? impotLocal : 0); + const creditRetenu = eligible ? Math.round(Math.min(resultat, impotLocal)) : 0; + const interetsInclus = eligible ? montantNet + creditRetenu : 0; + + let motif = null; + if (!eligible) { + if (!pays) motif = "Aucune convention fiscale identifiée pour ce pays"; + else if (pays.statut_convention !== 'active') motif = `Convention fiscale ${pays.statut_convention} avec ce pays`; + else if (pays.int_exclusif_residence) motif = "Revenus imposables exclusivement en France (pas de crédit d'impôt)"; + else motif = "Aucune retenue à la source appliquée par cette plateforme (rien à créditer)"; + } + + return { + plateforme_id: r0.plateforme_id, + plateforme_nom: r0.plateforme_nom, + domiciliation: r0.domiciliation, + investisseur_id: r0.investisseur_id, + investisseur_nom: r0.investisseur_nom, + investisseur_prenom: r0.investisseur_prenom, + pays_nom: pays?.nom_pays ?? null, + statut_convention: pays?.statut_convention ?? null, + eligible_ci: eligible, + taux_plafonne: tauxPlafonne, + motif, + montant_avant_local: round2(r0.montant_avant_local), + case_233: eligible ? montantNet : 0, + case_234: tauxApplicable, + case_235: resultat, + case_236: eligible ? impotLocal : 0, + case_237: creditRetenu, + case_238: interetsInclus, + case_250: eligible ? 0 : montantNet, + }; + }); + + const totals = lignes.reduce((acc, l) => ({ + montant_avant_local: acc.montant_avant_local + l.montant_avant_local, + case_233: acc.case_233 + l.case_233, + case_236: acc.case_236 + l.case_236, + case_237: acc.case_237 + l.case_237, + case_238: acc.case_238 + l.case_238, + case_250: acc.case_250 + l.case_250, + }), { montant_avant_local: 0, case_233: 0, case_236: 0, case_237: 0, case_238: 0, case_250: 0 }); + + totals.case_251 = totals.case_238 + totals.case_250; // Total des intérêts imposables (238+240+250, 240 non utilisé ici) + totals.case_252 = totals.case_251; // dont intérêts et produits de placement à revenu fixe → 2042 case 2TR + totals.case_8VL = totals.case_237; // Crédit d'impôt conventionnel total (cadre 7) → 2042C case 8VL + totals.case_8PL = totals.case_233; // Montant net ouvrant droit à crédit d'impôt étranger → 2042C case 8PL + // case_233/236/237/238/250/251/252/8VL/8PL sont déjà des entiers (euros) — seul montant_avant_local + // (info complémentaire, non reportée sur une case du formulaire) reste arrondi au centime. + totals.montant_avant_local = round2(totals.montant_avant_local); + + res.json({ annee, lignes, totals }); +}); + export default router; diff --git a/frontend/src/components/Cerfa2042Preview.jsx b/frontend/src/components/Cerfa2042Preview.jsx index 61485ca..08baa01 100644 --- a/frontend/src/components/Cerfa2042Preview.jsx +++ b/frontend/src/components/Cerfa2042Preview.jsx @@ -92,6 +92,10 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) { const totalTaxRate = rates.pfo + rates.csg + rates.crds + rates.solidarite; /* ── Suivi mensuel combiné ── */ + const totalPrelevFR = r2(frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, mo) => ss + (mo.prelev_sociaux ?? 0) + (mo.prelev_forfaitaire ?? 0), 0)), 0)); + const totalPrelevEtr = r2(platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)) * totalTaxRate, 0)); + const totalDejaPreleve = r2(totalPrelevFR + (pfoAssujetti ? totalPrelevEtr : 0)); + const totalAPrelever = pfoAssujetti ? 0 : totalPrelevEtr; const matriceView = (
{loading &&
Chargement…
} @@ -218,6 +222,40 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) { {(totalTaxRate * 100).toFixed(1)} % + {platEtr.length > 0 && ( + <> + {/* Montant des prélèvements déjà prélevés */} + + + Montant des prélèvements déjà prélevés + + {MOIS_NUMS.map(m => { + const prelevFR = r2(frLignes.reduce((s, l) => s + ((l.mois?.[m]?.prelev_sociaux ?? 0) + (l.mois?.[m]?.prelev_forfaitaire ?? 0)), 0)); + const prelevEtr = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0) * totalTaxRate); + const dejaPreleve = r2(prelevFR + (pfoAssujetti ? prelevEtr : 0)); + return {dejaPreleve > 0 ? fmtN(dejaPreleve) + ' €' : '—'}; + })} + + {fmtN(totalDejaPreleve)} € + + + {/* Montant des prélèvements à prélever */} + + + Montant des prélèvements à prélever + {pfoAssujetti && (couvert par la 2778-SD)} + + {MOIS_NUMS.map(m => { + const prelevEtr = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0) * totalTaxRate); + const aPrelever = pfoAssujetti ? 0 : prelevEtr; + return 0 ? 'var(--danger)' : 'var(--text-muted)' }}>{aPrelever > 0 ? fmtN(aPrelever) + ' €' : '—'}; + })} + 0 ? 'var(--danger)' : 'var(--text-muted)' }}> + {fmtN(totalAPrelever)} € + + + + )} {/* Total intérêt net */} Total intérêt net @@ -284,7 +322,7 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) { ]); const total2BH = bd2BH.reduce((s, i) => s + i.val, 0); - // Breakdown 2CK : FR (PFNL retenu) + étrangères (acompte 2778-SD) + // Breakdown 2CK : FR (PFNL retenu) + étrangères (case 273 du 2047, alimentée par les acomptes 2778-SD) const bd2CK = applyFilter([ ...frLignes.filter(l => l.case_2CK > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2CK, badge: BADGE_AUTO })), ...platEtr.filter(p => etrBA(p) > 0).map(p => ({ nom: p.nom, val: Math.round(etrBA(p) * rates.pfo), badge: BADGE_DECL })), @@ -313,13 +351,13 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) { - +
Synthèse fiscale Les montants sont automatiquement reportés par la plateforme (IFU). - Les montants nécessitent une déclaration mensuelle 2778-SD et un report manuel sur la 2042. + Les montants nécessitent une déclaration annuelle 2047 et un report manuel sur la 2042.
); diff --git a/frontend/src/components/Cerfa2047Preview.jsx b/frontend/src/components/Cerfa2047Preview.jsx new file mode 100644 index 0000000..1f5c439 --- /dev/null +++ b/frontend/src/components/Cerfa2047Preview.jsx @@ -0,0 +1,447 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api.js'; +import { useUi } from '../context/UiContext.jsx'; +import { + SuiviMensuelMatrix, Report2042Block, + computeCases, getRatesForYear, MOIS_NUMS, r, +} from './Cerfa2778Preview.jsx'; + +const fmtPct = (v, nonPlafonne) => v == null + ? (nonPlafonne + ? Non plafonné + : —) + : `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %`; + +// Cases CERFA 2047 : montants arrondis à l'euro le plus proche (règle fiscale), sans décimales — +// contrairement au Suivi mensuel (intermédiaire, non déclaré tel quel) qui garde 2 décimales. +const fmt0 = v => { + if (v == null || v === 0) return - €; + return `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €`; +}; + +/* ── Badge de case CERFA — même habillage que Case2042Row / Case2042 (Report 2042) ── */ +function CaseBadge({ code, tone }) { + const success = tone === 'success'; + return ( + {code} + ); +} +function CaseHeader({ code, label, tone }) { + return ( +
+ + {label} +
+ ); +} + +/* ── Composant principal ── */ +export default function Cerfa2047Preview({ annee, activeView }) { + const LS_KEY = 'cl_2778_excluded_plats'; // partagé avec le 2778-SD (mêmes plateformes étrangères) + const { pfoAssujetti } = useUi(); + + const [data2778, setData2778] = useState(null); + const [data2047, setData2047] = useState(null); + const [loading, setLoading] = useState(false); + const [pfuList, setPfuList] = useState([]); + const [excluded, setExcluded] = useState(() => { + try { return new Set(JSON.parse(localStorage.getItem(LS_KEY)) ?? []); } + catch { return new Set(); } + }); + const [view, setView] = useState('matrice'); // 'matrice' | 'donnees' | 'report2042' + + useEffect(() => { + setLoading(true); + setData2778(null); + setData2047(null); + const scopeParams = activeView === 'all' ? { scope: 'all' } : {}; + Promise.all([ + api.get('/taxreport/2778', { annee, ...scopeParams }), + api.get('/taxreport/2047', { annee, ...scopeParams }), + pfuList.length === 0 ? api.get('/pfu') : Promise.resolve(null), + ]).then(([d2778, d2047, pfu]) => { + setData2778(d2778); + setData2047(d2047); + if (pfu) setPfuList(pfu); + }).finally(() => setLoading(false)); + }, [annee, activeView]); // eslint-disable-line + + if (loading || !data2778 || !data2047) return
Chargement…
; + + const plateformes = data2778.plateformes; + const rates = getRatesForYear(annee, pfuList); + const TOTAL_TAX = rates.pfo + rates.csg + rates.crds + rates.solidarite; + + if (plateformes.length === 0) { + return ( +
+

+ Aucun revenu de plateforme étrangère pour {annee}. +

+
+ ); + } + + const togglePlat = (id) => { + setExcluded(prev => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + localStorage.setItem(LS_KEY, JSON.stringify([...next])); + return next; + }); + }; + + const totauxMois = MOIS_NUMS.map(m => { + let sum = 0; + for (const plat of plateformes) { + if (!excluded.has(plat.id)) sum += plat.mois[m] ?? 0; + } + return r(sum); + }); + const totalBA = r(totauxMois.reduce((a, b) => a + b, 0)); + const totalIA = computeCases(totalBA, rates).IA; + + // Lignes 2047 filtrées sur les plateformes non exclues (cohérence avec le Suivi mensuel) + const lignes2047 = data2047.lignes.filter(l => !excluded.has(`${l.plateforme_id}_${l.investisseur_id}`)); + const lignesEligibles = lignes2047.filter(l => l.eligible_ci); + const lignesNonEligibles = lignes2047.filter(l => !l.eligible_ci); + // Regroupement par pays pour faciliter le report dans la déclaration 2047 + // (le formulaire officiel attend une ligne par pays, pas par plateforme) + const groupesEligibles = Object.values( + lignesEligibles.reduce((acc, l) => { + const key = l.domiciliation || l.pays_nom || '?'; + if (!acc[key]) acc[key] = { pays_nom: l.pays_nom, domiciliation: l.domiciliation, lignes: [] }; + acc[key].lignes.push(l); + return acc; + }, {}) + ).sort((a, b) => (a.pays_nom ?? '').localeCompare(b.pays_nom ?? '')); + const t = lignes2047.reduce((acc, l) => ({ + case_233: acc.case_233 + l.case_233, + case_236: acc.case_236 + l.case_236, + case_237: acc.case_237 + l.case_237, + case_238: acc.case_238 + l.case_238, + case_250: acc.case_250 + l.case_250, + }), { case_233: 0, case_236: 0, case_237: 0, case_238: 0, case_250: 0 }); + t.case_251 = r(t.case_238 + t.case_250); + t.case_8VL = r(t.case_237); + t.case_8PL = r(t.case_233); + + const platLabel = (l) => { + const n = l.investisseur_nom ?? ''; + const p = l.investisseur_prenom ?? ''; + const who = p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); + return ( + <> + {l.plateforme_nom} + {who && — {who}} + + ); + }; + + const cellStyle = { + textAlign: 'right', padding: '7px 10px', fontSize: 'var(--fs-sm)', + whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)', + }; + const headStyle = { + textAlign: 'center', padding: '7px 10px', fontSize: 'var(--fs-xs)', fontWeight: 600, + color: 'var(--text-muted)', background: 'var(--surface-2)', + borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap', + }; + + return ( +
+
+ + +
+ + {/* ── Vue Matrice (partagée avec le 2778-SD) ── */} + {view === 'matrice' && ( + + )} + + {/* ── Vue Données 2047 ── */} + {view === 'donnees' && ( +
+
+

Formulaire 2047 — Rubrique 230 (Intérêts)

+ Vision annuelle {annee} — plateformes étrangères +
+ + {/* Lignes 231-238 : éligibles au crédit d'impôt */} +
+
+ Intérêts ouvrant droit à un crédit d'impôt égal à l'impôt payé à l'étranger +
+
+ {lignesEligibles.length === 0 ? ( +
Aucune plateforme éligible pour {annee}.
+ ) : ( + + + + + + + + + + + + + + + {groupesEligibles.flatMap(g => { + const sub = g.lignes.reduce((acc, l) => ({ + case_233: acc.case_233 + l.case_233, + case_235: acc.case_235 + l.case_235, + case_236: acc.case_236 + l.case_236, + case_237: acc.case_237 + l.case_237, + case_238: acc.case_238 + l.case_238, + }), { case_233: 0, case_235: 0, case_236: 0, case_237: 0, case_238: 0 }); + const rows = g.lignes.map(l => ( + + + + + + + + + + + )); + if (g.lignes.length > 1) { + rows.push( + + + + + + + + + + ); + } + return rows; + })} + + + + + + + + + + +
Plateforme — DétenteurPays
{platLabel(l)}{l.pays_nom}{fmt0(l.case_233)}{fmtPct(l.case_234, !l.taux_plafonne)}{fmt0(l.case_235)}{fmt0(l.case_236)}{fmt0(l.case_237)}{fmt0(l.case_238)}
+ Sous-total {g.pays_nom} ({g.lignes.length} plateformes) + {fmt0(sub.case_233)}{fmt0(sub.case_235)}{fmt0(sub.case_236)}{fmt0(sub.case_237)}{fmt0(sub.case_238)}
Total{fmt0(t.case_233)}{fmt0(t.case_236)}{fmt0(t.case_237)}{fmt0(t.case_238)}
+ )} + + {/* Ligne 250 : n'ouvrant pas droit à crédit d'impôt */} +
+
+ Intérêts n'ouvrant pas droit à crédit d'impôt +
+
+ {lignesNonEligibles.length === 0 ? ( +
Aucune plateforme dans ce cas pour {annee}.
+ ) : ( + + + + + + + + + + + {lignesNonEligibles.map(l => ( + + + + + + + ))} + + + + + +
Plateforme — DétenteurPaysMotif
{platLabel(l)}{l.domiciliation}{l.motif}{fmt0(l.case_250)}
Total{fmt0(t.case_250)}
+ )} + + {/* Totaux 251 / 252 */} +
+
+
+ Total des intérêts imposables (238 + 250) +
+
+ dont intérêts et autres produits de placement à revenu fixe → reporté en case 2TR de la 2042 (voir onglet « Report 2042 ») +
+
+
{fmt0(t.case_251)}
+
+ +
+ L'éligibilité au crédit d'impôt (231-238 vs 250) est déterminée automatiquement à partir du pays de domiciliation + de chaque plateforme et du référentiel « 2047 — Taux crédit d'impôts » (Admin > Fiscalité). + Le crédit d'impôt retenu (237) est plafonné au plus petit des deux montants : taux conventionnel × montant net (235), + ou impôt réellement prélevé à l'étranger (236). +
+ + {/* Rubrique 9 : revenus déjà soumis aux prélèvements sociaux (2778-SD) */} +
+
+ Rubrique 9 — Revenus déjà soumis aux prélèvements sociaux (2778-SD) +
+
+ {pfoAssujetti ? ( +
+
+ + +
+
+ ) : ( +
+ Les cases 272 et 273 ne s'appliquent que si l'option de déclaration 2778-SD est activée + (Paramétrage > Ma fiscalité). +
+ )} +
+ )} + + {/* ── Vue Report 2042 ── */} + {view === 'report2042' && ( + <> + + +
+
+

Crédit d'impôt étranger (cadre 7 du 2047)

+ À reporter sur la 2042C +
+
+ + +
+
+ + )} + +
+
+
+ ); +} + +/* ── Ligne de case CERFA 2047 → report 2042/2042C ── */ +function Case2042Row({ code, label, note, value }) { + return ( +
+
+
{code}
+
+
{label}
+ {note &&
{note}
} +
+
+ {Number(value ?? 0).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} € +
+
+
+ ); +} diff --git a/frontend/src/components/Cerfa2778Preview.jsx b/frontend/src/components/Cerfa2778Preview.jsx index 322a78a..ea27710 100644 --- a/frontend/src/components/Cerfa2778Preview.jsx +++ b/frontend/src/components/Cerfa2778Preview.jsx @@ -1,13 +1,14 @@ import { useEffect, useState } from 'react'; import { api } from '../api.js'; +import { useUi } from '../context/UiContext.jsx'; -const MOIS_LABELS = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; -const MOIS_NUMS = ['01','02','03','04','05','06','07','08','09','10','11','12']; +export const MOIS_LABELS = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; +export const MOIS_NUMS = ['01','02','03','04','05','06','07','08','09','10','11','12']; // Taux par défaut (2026+) — remplacés par les données de /api/pfu const DEFAULT_RATES = { pfo: 0.128, csg: 0.106, crds: 0.005, solidarite: 0.075 }; -function getRatesForYear(annee, pfuList) { +export function getRatesForYear(annee, pfuList) { const yr = Number(annee); // Chercher l'année exacte, puis l'année précédente la plus proche const sorted = [...pfuList].sort((a, b) => b.annee - a.annee); @@ -21,18 +22,18 @@ function getRatesForYear(annee, pfuList) { }; } -const r = v => Math.round((v ?? 0) * 100) / 100; -const fmtEUR = v => { +export const r = v => Math.round((v ?? 0) * 100) / 100; +export const fmtEUR = v => { if (v == null || v === 0) return - €; return `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`; }; -const fmtInt = v => { +export const fmtInt = v => { if (v == null || v === 0) return -; return `${v} €`; }; /* ── Simulation cases CERFA pour un montant brut ── */ -function computeCases(ba, rates) { +export function computeCases(ba, rates) { const R = rates ?? DEFAULT_RATES; const BA = Math.round(ba); const IA = Math.round(BA * R.pfo); @@ -137,24 +138,6 @@ export default function Cerfa2778Preview({ annee, activeView }) { }); }; - const cellStyle = { - textAlign: 'right', - padding: '6px 10px', - fontSize: 'var(--fs-sm)', - whiteSpace: 'nowrap', - borderBottom: '1px solid var(--border)', - }; - const headStyle = { - textAlign: 'center', - padding: '6px 10px', - fontSize: 'var(--fs-xs)', - fontWeight: 600, - color: 'var(--text-muted)', - background: 'var(--surface-2)', - borderBottom: '1px solid var(--border)', - whiteSpace: 'nowrap', - }; - return (
{/* ── Navigation sidebar ── */} @@ -202,135 +185,15 @@ export default function Cerfa2778Preview({ annee, activeView }) {
{/* ── Vue Matrice ── */} {view === 'matrice' && ( - <> -
-
-

Suivi mensuel des intérêts bruts

- Revenus {annee} — plateformes étrangères -
- - - - - {MOIS_LABELS.map((m, i) => ( - - ))} - - - - - {plateformes.map(plat => { - const isExcluded = excluded.has(plat.id); - const total = Object.values(plat.mois).reduce((a, b) => a + b, 0); - return ( - - - {MOIS_NUMS.map(m => ( - - ))} - - - ); - })} - - {/* ── Séparateur ── */} - - - {/* ── Total brut mensuel ── */} - - - {totauxMois.map((t, i) => ( - - ))} - - - - {/* ── Taux Flat Tax ── */} - - - {MOIS_NUMS.map((_, i) => ( - - ))} - - - - {/* ── Total Flat Tax ── */} - - - {totauxMois.map((t, i) => { - const { QR } = computeCases(t, rates); - return ( - - ); - })} - - - - {/* ── Intérêt net ── */} - - - {totauxMois.map((t, i) => ( - - ))} - - - -
- Plateforme — Détenteur - {m}Total
- - {fmtEUR(plat.mois[m] ?? null)}{fmtEUR(r(total))}
- Total intérêt brut - {fmtEUR(t || null)} - {fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))} -
- Taux total prélevé ({(TOTAL_TAX * 100).toFixed(1)} %) - - {totauxMois[i] > 0 ? `${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %` : '—'} - {`${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %`}
- Montant total à payer (QR) - 0 ? 'var(--danger)' : undefined }}> - {t > 0 ? fmtInt(QR) : -} - - {fmtInt(computeCases(r(totauxMois.reduce((a, b) => a + b, 0)), rates).QR)} -
- Total intérêt net - 0 ? 'var(--success)' : undefined }}> - {fmtEUR(t > 0 ? r(t * (1 - TOTAL_TAX)) : null)} - - {fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0) * (1 - TOTAL_TAX)))} -
- -
- Taux {annee} : PFO {(rates.pfo*100).toFixed(1)} % + CSG {(rates.csg*100).toFixed(1)} % + CRDS {(rates.crds*100).toFixed(1)} % + Solidarité {(rates.solidarite*100).toFixed(1)} % = {(TOTAL_TAX*100).toFixed(1)} % - · - ⏱ Déclaration et paiement dus dans les 15 premiers jours du mois suivant l'encaissement - · - Base imposable : intérêts bruts après déduction de l'impôt prélevé à la source à l'étranger, avant déduction retenue "directive épargne" -
-
- - + )} {/* ── Vue Report 2042 ── */} @@ -421,12 +284,180 @@ export default function Cerfa2778Preview({ annee, activeView }) { ); } +/* ── Suivi mensuel (matrice) — partagé entre 2778-SD et 2047 ── */ +export function SuiviMensuelMatrix({ annee, plateformes, excluded, togglePlat, totauxMois, rates, TOTAL_TAX }) { + const { pfoAssujetti } = useUi(); + const cellStyle = { + textAlign: 'right', + padding: '6px 10px', + fontSize: 'var(--fs-sm)', + whiteSpace: 'nowrap', + borderBottom: '1px solid var(--border)', + }; + const headStyle = { + textAlign: 'center', + padding: '6px 10px', + fontSize: 'var(--fs-xs)', + fontWeight: 600, + color: 'var(--text-muted)', + background: 'var(--surface-2)', + borderBottom: '1px solid var(--border)', + whiteSpace: 'nowrap', + }; + + return ( +
+
+

Suivi mensuel des intérêts bruts

+ Revenus {annee} — plateformes étrangères +
+ + + + + {MOIS_LABELS.map((m, i) => ( + + ))} + + + + + {plateformes.map(plat => { + const isExcluded = excluded.has(plat.id); + const total = Object.values(plat.mois).reduce((a, b) => a + b, 0); + return ( + + + {MOIS_NUMS.map(m => ( + + ))} + + + ); + })} + + {/* ── Séparateur ── */} + + + {/* ── Total brut mensuel ── */} + + + {totauxMois.map((t, i) => ( + + ))} + + + + {pfoAssujetti && ( + <> + {/* ── Taux Flat Tax ── */} + + + {MOIS_NUMS.map((_, i) => ( + + ))} + + + + {/* ── Total Flat Tax ── */} + + + {totauxMois.map((t, i) => { + const { QR } = computeCases(t, rates); + return ( + + ); + })} + + + + {/* ── Intérêt net ── */} + + + {totauxMois.map((t, i) => ( + + ))} + + + + + )} + +
+ Plateforme — Détenteur + {m}Total
+ + {fmtEUR(plat.mois[m] ?? null)}{fmtEUR(r(total))}
+ Total intérêt brut + {fmtEUR(t || null)} + {fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))} +
+ Taux total prélevé ({(TOTAL_TAX * 100).toFixed(1)} %) + + {totauxMois[i] > 0 ? `${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %` : '—'} + {`${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %`}
+ Montant total à payer (QR) + 0 ? 'var(--danger)' : undefined }}> + {t > 0 ? fmtInt(QR) : -} + + {fmtInt(computeCases(r(totauxMois.reduce((a, b) => a + b, 0)), rates).QR)} +
+ Total intérêt net + 0 ? 'var(--success)' : undefined }}> + {fmtEUR(t > 0 ? r(t * (1 - TOTAL_TAX)) : null)} + + {fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0) * (1 - TOTAL_TAX)))} +
+ +
+ {pfoAssujetti && ( + <> + Taux {annee} : PFO {(rates.pfo*100).toFixed(1)} % + CSG {(rates.csg*100).toFixed(1)} % + CRDS {(rates.crds*100).toFixed(1)} % + Solidarité {(rates.solidarite*100).toFixed(1)} % = {(TOTAL_TAX*100).toFixed(1)} % + · + ⏱ Déclaration et paiement dus dans les 15 premiers jours du mois suivant l'encaissement + · + + )} + Base imposable : intérêts bruts après déduction de l'impôt prélevé à la source à l'étranger, avant déduction retenue "directive épargne" +
+
+ ); +} + /* ── Report annuel 2042 ── */ -function Report2042Block({ annee, totauxMois, rates, plateformes, excluded }) { +export function Report2042Block({ annee, totauxMois, rates, plateformes, excluded, source = '2778' }) { const totalBA = r(totauxMois.reduce((a, b) => a + b, 0)); if (totalBA === 0) return null; const totalIA = computeCases(totalBA, rates).IA; + const { pfoAssujetti } = useUi(); + const isFrom2047 = source === '2047'; + const noteBA = isFrom2047 + ? "Intérêts bruts totaux encaissés sur l'année (cases 233 + 250 des Données 2047)" + : "Intérêts bruts totaux encaissés sur l'année (somme des cases BA)"; + const noteIA = isFrom2047 + ? "Correspond à la case 273 des Données 2047 — s'impute sur l'IR définitif, l'excédent est restitué" + : "Somme des cases IA de vos 2778-SD — s'impute sur l'IR définitif, l'excédent est restitué"; // Détail par plateforme incluse (annuel) const platDetail = plateformes @@ -482,37 +513,60 @@ function Report2042Block({ annee, totauxMois, rates, plateformes, excluded }) { ({ nom: p.nom, val: Math.round(p.ba) }))} /> - ({ nom: p.nom, val: Math.round(p.ba) }))} - /> - ({ nom: p.nom, val: p.ia }))} - /> + {pfoAssujetti && ( + <> + ({ nom: p.nom, val: Math.round(p.ba) }))} + /> + ({ nom: p.nom, val: p.ia }))} + /> + + )}
-
- Comment fonctionne le PFO ? - Le prélèvement forfaitaire obligatoire (PFO, case 2CK) versé via la 2778-SD est un acompte sur l'impôt sur le revenu, non libératoire. - L'imposition définitive est calculée lors de votre 2042 : par défaut au taux de {(rates.pfo * 100).toFixed(1)} % (Flat Tax), - ou sur option expresse au barème progressif. L'acompte déjà versé (2CK) s'impute sur l'IR définitif — tout excédent vous est restitué. - Le montant 2BH est déclaré en sus de 2TR uniquement pour neutraliser les prélèvements sociaux déjà prélevés via la 2778-SD. -
+ {pfoAssujetti ? ( +
+ Comment fonctionne le PFO ? + {isFrom2047 ? ( + <>Le prélèvement forfaitaire obligatoire (PFO, case 2CK) correspond à la case 273 des Données 2047 : c'est un acompte sur l'impôt sur le revenu, non libératoire. + ) : ( + <>Le prélèvement forfaitaire obligatoire (PFO, case 2CK) versé via la 2778-SD est un acompte sur l'impôt sur le revenu, non libératoire. + )}{' '} + L'imposition définitive est calculée lors de votre 2042 : par défaut au taux de {(rates.pfo * 100).toFixed(1)} % (Flat Tax), + ou sur option expresse au barème progressif. L'acompte déjà versé (2CK) s'impute sur l'IR définitif — tout excédent vous est restitué.{' '} + {isFrom2047 ? ( + <>Le montant 2BH (case 272 des Données 2047) est déclaré en sus de 2TR uniquement pour neutraliser les prélèvements sociaux déjà prélevés. + ) : ( + <>Le montant 2BH est déclaré en sus de 2TR uniquement pour neutraliser les prélèvements sociaux déjà prélevés via la 2778-SD. + )} +
+ ) : ( +
+ Les cases 2BH et 2CK ne s'appliquent que si l'option de déclaration 2778-SD est activée + (Paramétrage > Ma fiscalité). +
+ )}
); } diff --git a/frontend/src/pages/Aide.jsx b/frontend/src/pages/Aide.jsx index 34f9f2c..e3618d7 100644 --- a/frontend/src/pages/Aide.jsx +++ b/frontend/src/pages/Aide.jsx @@ -343,6 +343,172 @@ export default function Aide() {

+ Fiscalité + + +

+ La page Fiscalité (menu de gauche) reprend, année par année, + les montants nécessaires pour préparer votre déclaration de revenus. Elle est organisée en quatre onglets, + chacun correspondant à un formulaire CERFA officiel : +

+ + + + + + + + + + + + + + + + + + + + +
+ 2561 + IFU — Imprimé fiscal unique. Récapitulatif annuel transmis par les plateformes françaises soumises au prélèvement forfaitaire unique (PFU).
+ 2047 + Revenus encaissés à l'étranger. Visible dès qu'une ou plusieurs de vos plateformes sont domiciliées hors de France ; détaille les intérêts perçus pays par pays et le crédit d'impôt associé.
+ 2778-SD + Prélèvement forfaitaire obligatoire (PFO), selon votre situation fiscale — déclaration mensuelle de l'acompte dû sur les intérêts de plateformes étrangères (voir la question suivante).
+ 2042 + Déclaration des revenus. La déclaration principale : reprend les cases calculées à partir des trois formulaires ci-dessus (2TT, 2TR, 2BH, 2CK, 2TY).
+ +

Le sens de circulation des données est toujours le même :

+ +

+ Un pavé « À quoi correspond chaque CERFA ? », avec un schéma récapitulatif, est affiché directement en haut de + l'onglet 2047 lorsque vous avez des plateformes étrangères. +

+ +

+ Important — ce module est une aide à la préparation de votre + déclaration, il ne la remplace pas. Vérifiez chaque montant avant de le reporter sur le site des impôts : + vous restez seul responsable de l'exactitude de votre déclaration fiscale, Crowdlending ne saurait être + tenu responsable des données déclarées. +

+
+ + +

+ Cette question concerne le formulaire 2778-SD (prélèvement + forfaitaire obligatoire, ou PFO). Elle ne se pose que si vous détenez au moins une plateforme domiciliée + hors de France. +

+ +

La règle

+

+ Le PFO s'applique aux personnes physiques fiscalement domiciliées en France dont le revenu + fiscal de référence (RFR) de l'avant-dernière année est égal ou supérieur à : +

+
+
+ 25 000 € — célibataire, divorcé ou veuf +
+
+ 50 000 € — couple marié ou pacsé +
+
+

+ Votre RFR figure sur votre avis d'imposition de l'année précédente + (ligne « Revenu fiscal de référence »). En dessous de ces seuils, vous êtes dispensé du PFO — mais les + prélèvements sociaux restent dus. +

+ +

Où l'activer dans l'application

+
    +
  1. Allez dans Paramétrage → Ma fiscalité.
  2. +
  3. Dépliez la carte « CERFA 2778-SD — Prélèvement Forfaitaire Obligatoire (PFO) pour des revenus de source étrangère » pour revoir le détail des seuils.
  4. +
  5. Activez le bouton si votre situation vous rend redevable du PFO.
  6. +
+

+ Ce réglage est global : une fois activé, l'onglet 2778-SD apparaît + dans la page Fiscalité, les cases 2BH / 2CK se + remplissent dans le report 2042, et les lignes de suivi mensuel du PFO (taux, montant à payer, intérêt net) + s'affichent dans les tableaux « Suivi mensuel » des onglets 2047 et 2778-SD. Si vous le désactivez, ces + éléments sont masqués car ils ne s'appliquent pas à votre situation. +

+ +

+ À retenir — que le PFO s'applique ou non à vous, la déclaration + annuelle via le CERFA 2047 reste obligatoire dès que vous avez + perçu des intérêts d'une plateforme étrangère. Ce paramètre reflète votre propre situation fiscale : en cas + de doute, vérifiez votre avis d'imposition ou consultez un professionnel. +

+
+ + +

+ Dans l'onglet 2047, vue + « Données 2047 », chaque plateforme étrangère est classée automatiquement selon la convention fiscale + signée entre la France et son pays de domiciliation. +

+ +

Éligible au crédit d'impôt (cases 233 à 238)

+

+ Le pays a une convention fiscale active et n'impose pas + l'imposition exclusivement dans le pays de résidence. Le crédit d'impôt (case 237) est égal à l'impôt + réellement prélevé à l'étranger (case 236), plafonné au taux conventionnel du pays lorsque celui-ci est + chiffré (case 234) — ou non plafonné pour certains pays dont + la convention prévoit un crédit égal à l'impôt prélevé sans limite de taux. +

+ +

Non éligible (case 250)

+

+ Le pays est absent des conventions fiscales référencées, sa convention est suspendue ou caduque, ou elle + prévoit une imposition exclusivement dans le pays de résidence. Dans ce cas, aucun crédit d'impôt n'est + calculé. +

+ +

Sous-totaux par pays

+

+ Le formulaire papier 2047 se remplit par pays, pas par + plateforme : si plusieurs de vos plateformes sont domiciliées dans le même pays, une ligne « Sous-total » + apparaît automatiquement sous le groupe pour faciliter le report. Les totaux 8VL et 8PL (report 2042) + ainsi que les cases 251/252 cumulent l'ensemble de vos plateformes étrangères, arrondis à l'euro comme + l'exige le formulaire officiel. +

+
+ + +

+ Chaque onglet CERFA propose généralement plusieurs vues : un suivi + mensuel (le détail plateforme par plateforme, mois par mois), une vue « Données » (les + cases du formulaire lui-même), et une vue « Report 2042 » (ce + qui doit être reporté sur la déclaration principale). +

+ +

Ordre suggéré

+
    +
  1. Sélectionnez l'année concernée en haut de la page.
  2. +
  3. Onglet 2561 si vous avez des plateformes françaises soumises au PFU.
  4. +
  5. Onglets 2778-SD puis 2047 si vous avez des plateformes étrangères — le suivi mensuel du 2778-SD documente le détail, la vue « Données 2047 » regroupe l'annuel par pays.
  6. +
  7. Onglet 2042 en dernier : sa vue « Données 2042 » et sa synthèse fiscale récapitulent toutes les cases (2TT, 2TR, 2BH, 2CK, 2TY) à saisir sur le site des impôts.
  8. +
+ +

+ Les montants affichés dans les vues « Données » et les cases officielles sont arrondis à l'euro, comme + l'exigent les formulaires CERFA (les montants intermédiaires du suivi mensuel restent au centime près, + pour la traçabilité). +

+ +

+ Comme rappelé sur la page elle-même : ce module est une aide à la + préparation de votre déclaration, il ne s'y substitue pas. Contrôlez chaque case avant de la + reporter — vous restez seul responsable de l'exactitude de votre déclaration. +

+
+ API et Serveur MCP pour l'IA diff --git a/frontend/src/pages/Plateformes.jsx b/frontend/src/pages/Plateformes.jsx index 1bd11b6..172ec66 100644 --- a/frontend/src/pages/Plateformes.jsx +++ b/frontend/src/pages/Plateformes.jsx @@ -313,7 +313,7 @@ export default function Plateformes() { setSearchParams(p => { const n = new URLSearchParams(p); n.set('plat', nom); return n; }, { replace: true }); }; const [selectedDetenteurId, setSelectedDetenteurId] = useState(() => { try { return localStorage.getItem('cl_plat_detenteur') || 'all'; } catch { return 'all'; } }); - const [selectedYear, setSelectedYear] = useState(() => { try { return localStorage.getItem('cl_plat_year') || String(new Date().getFullYear()); } catch { return String(new Date().getFullYear()); } }); + const [selectedYear, setSelectedYear] = useState(''); // toujours "Depuis le début" au démarrage de la page const [windowStart, setWindowStart] = useState(0); const [drDetaille, setDrDetaille] = useState(() => { try { return localStorage.getItem('cl_dr_plat_detaille') === 'true'; } catch { return false; } diff --git a/frontend/src/pages/TaxReport.jsx b/frontend/src/pages/TaxReport.jsx index c36d085..24abdb4 100644 --- a/frontend/src/pages/TaxReport.jsx +++ b/frontend/src/pages/TaxReport.jsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import PageIcon from '../components/PageIcon.jsx'; import EmptyState from '../components/EmptyState.jsx'; import Pagination from '../components/Pagination.jsx'; @@ -10,6 +11,7 @@ import { fmtEUR, fmtStatut } from '../utils/format.js'; import Cerfa2561Preview from '../components/Cerfa2561Preview.jsx'; import CerfaRecapTable from '../components/CerfaRecapTable.jsx'; import Cerfa2778Preview from '../components/Cerfa2778Preview.jsx'; +import Cerfa2047Preview from '../components/Cerfa2047Preview.jsx'; import Cerfa2042Preview from '../components/Cerfa2042Preview.jsx'; /* ── YearSelector ────────────────────────────────────────────── */ @@ -144,8 +146,78 @@ function ExportDropdown({ disabled, onCSV, onJSON }) { ); } +/* ── Ligne d'explication d'un formulaire CERFA ── */ +function InfoLigneCerfa({ code, titre, texte }) { + return ( +
+
{code}
+
+
{titre}
+
{texte}
+
+
+ ); +} + +/* ── Schéma : quel formulaire alimente quel formulaire ── */ +function CerfaFlowDiagram({ hasEtranger }) { + const boxFill = 'var(--surface-2)'; + const box = (x, y, w, h, code, label, highlight) => ( + + + {code} + {label} + + ); + const arrow = (x1, y1, x2, y2, key) => ( + + ); + const markerDef = ( + + + + + + ); + + if (!hasEtranger) { + return ( + + {markerDef} + {arrow(160, 40, 228, 40, 'a1')} + {box(20, 15, 140, 50, '2561', 'IFU')} + {box(230, 15, 170, 50, '2042', 'Déclaration revenus', true)} + + ); + } + + return ( + + {markerDef} + {arrow(520, 58, 520, 98, 'a1')} + {arrow(100, 148, 260, 190, 'a2')} + {arrow(520, 148, 362, 190, 'a3')} + + {box(440, 8, 160, 50, '2778-SD', 'PFO mensuel')} + {box(20, 98, 160, 50, '2561', 'IFU')} + {box(440, 98, 160, 50, '2047', 'Revenus étranger')} + {box(210, 190, 200, 54, '2042', 'Déclaration revenus', true)} + + ); +} + /* ── Composant principal ─────────────────────────────────────── */ export default function TaxReport() { + const navigate = useNavigate(); const { activeId, activeView } = useInvestisseur(); const { pfoAssujetti } = useUi(); const [annee, setAnnee] = useState(String(new Date().getFullYear())); @@ -157,10 +229,14 @@ export default function TaxReport() { const [detailExpanded, setDetailExpanded] = useState(false); const [cerfaExpanded, setCerfaExpanded] = useState(false); const [hasPlateformes, setHasPlateformes] = useState(true); // true par défaut pour éviter un flash du mauvais message + const [hasEtranger, setHasEtranger] = useState(false); // au moins une plateforme étrangère configurée (pour l'onglet CERFA 2047 — indépendant de l'année sélectionnée, même principe que le 2778-SD) /* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */ useEffect(() => { - api.get('/plateformes').then(p => setHasPlateformes((p || []).length > 0)).catch(() => {}); + api.get('/plateformes').then(p => { + setHasPlateformes((p || []).length > 0); + setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR')); + }).catch(() => {}); }, []); /* ── Chargement des années disponibles ── */ @@ -196,14 +272,14 @@ export default function TaxReport() { setData2042({ case_2TT: frLignes.reduce((s, l) => s + (l.case_2TT ?? 0), 0), case_2TR: frLignes.reduce((s, l) => s + (l.case_2TR ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)), - case_2BH: frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)), - case_2CK: frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0) * pfo), + case_2BH: frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0) + (pfoAssujetti ? Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)) : 0), + case_2CK: frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0) + (pfoAssujetti ? Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0) * pfo) : 0), case_2TY: frLignes.reduce((s, l) => s + (l.case_2TY ?? 0), 0), }); }).finally(() => setLoading(false)); }; - useEffect(load, [activeId, activeView, annee]); // eslint-disable-line + useEffect(load, [activeId, activeView, annee, pfoAssujetti]); // eslint-disable-line /* ── Pagination détail ── */ const detail = data?.detail ?? []; @@ -301,10 +377,73 @@ export default function TaxReport() {

} + {!detailExpanded && !cerfaExpanded && data2042 && ( +
+

À quoi correspond chaque CERFA ?

+
+
+ + + {hasEtranger && ( + + )} + {hasEtranger && ( + + )} +
+
+ +
+
+ + {hasEtranger && ( +
+
+ {pfoAssujetti ? ( + <>Vous avez des plateformes étrangères et l'option de déclaration 2778-SD est activée. + ) : ( + <>Vous avez des plateformes étrangères. Vérifiez si la déclaration mensuelle 2778-SD s'applique à votre situation. + )} +
+ +
+ )} + +

+ ⚠ Ce module est une aide à la préparation de votre déclaration ; il ne la remplace pas. Vérifiez chaque montant avant de le reporter. + Vous restez seul responsable de l'exactitude de votre déclaration fiscale — Crowdlending ne saurait être tenu responsable des données déclarées. +

+
+ )} + {!detailExpanded && !cerfaExpanded && (
+ {hasEtranger && } {pfoAssujetti && }
)} @@ -324,6 +463,10 @@ export default function TaxReport() { /> )} + {activeTab === '2047' && hasEtranger && !detailExpanded && !cerfaExpanded && ( + + )} + {activeTab === '2778' && pfoAssujetti && !detailExpanded && !cerfaExpanded && ( )}