Catégorisation des platformes et application de la fiscalité adapté (Impact PSAN , régime micro-BIC)

This commit is contained in:
ocroguennec committed 2026-08-30 16:53:04 +02:00
1 parent 8953d0bb35
commit 33fee587aa
10 files changed
+610 -53

No files matched your search

@@ -0,0 +1,266 @@
import { useEffect, useState } from 'react';
import { api } from '../api.js';
import { MOIS_LABELS, MOIS_NUMS, r, fmtEUR } from './Cerfa2778Preview.jsx';
// Cases CERFA : 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 <span style={{ color: 'var(--text-muted)' }}>- €</span>;
return `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €`;
};
function CaseBadge({ code, tone }) {
const success = tone === 'success';
return (
<span style={{
display: 'inline-block', fontFamily: 'monospace', fontWeight: 800, fontSize: 'var(--fs-xs)',
color: success ? 'var(--success)' : 'var(--primary)',
background: success ? 'rgba(16,185,129,0.12)' : 'rgba(99,102,241,0.07)',
borderRadius: 6, padding: '2px 6px', letterSpacing: '.04em', whiteSpace: 'nowrap',
}}>{code}</span>
);
}
/* ── Suivi mensuel — table plateforme × mois, sans les lignes PFO/2778-SD (non applicables : les
recettes PSAN sont des BNC, pas des intérêts soumis au prélèvement forfaitaire obligatoire). ── */
function SuiviMensuelPsan({ annee, plateformes, totauxMois }) {
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 (
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Suivi mensuel des recettes crypto (BNC)</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Revenus {annee} — plateformes PSAN</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left', minWidth: 180, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Plateforme — Détenteur
</th>
{MOIS_LABELS.map(m => <th key={m} style={headStyle}>{m}</th>)}
<th style={{ ...headStyle, background: 'var(--surface-2)' }}>Total</th>
</tr>
</thead>
<tbody>
{plateformes.map(plat => {
const total = Object.values(plat.mois).reduce((a, b) => a + b, 0);
return (
<tr key={plat.id}>
<td style={{
...cellStyle, textAlign: 'left', position: 'sticky', left: 0,
background: 'var(--surface)', fontWeight: 500,
}}>
{plat.nom}
{(plat.investisseur_nom || plat.investisseur_prenom) && (
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6, fontWeight: 400 }}>
— {(() => { const n = plat.investisseur_nom ?? ''; const p = plat.investisseur_prenom ?? ''; return p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); })()}
</span>
)}
</td>
{MOIS_NUMS.map(m => <td key={m} style={cellStyle}>{fmtEUR(plat.mois[m] ?? null)}</td>)}
<td style={{ ...cellStyle, fontWeight: 600 }}>{fmtEUR(r(total))}</td>
</tr>
);
})}
<tr><td colSpan={14} style={{ height: 4, background: 'var(--surface-2)' }} /></tr>
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Total recettes brutes
</td>
{totauxMois.map((t, i) => <td key={i} style={{ ...cellStyle, fontWeight: 700 }}>{fmtEUR(t || null)}</td>)}
<td style={{ ...cellStyle, fontWeight: 800 }}>{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))}</td>
</tr>
</tbody>
</table>
<div style={{ padding: '10px 16px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', borderTop: '1px solid var(--border)' }}>
Base : intérêts/rewards bruts encaissés (valorisés en euros au jour de réception), avant tout abattement.
</div>
</div>
);
}
/* ── Composant principal ── */
export default function Cerfa2042CProPreview({ annee, activeView }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [view, setView] = useState('matrice'); // 'matrice' | 'donnees'
useEffect(() => {
setLoading(true);
setData(null);
const scopeParams = activeView === 'all' ? { scope: 'all' } : {};
api.get('/taxreport/2042cpro', { annee, ...scopeParams })
.then(setData)
.finally(() => setLoading(false));
}, [annee, activeView]);
if (loading || !data) return <div className="card text-muted">Chargement…</div>;
const plateformes = data.plateformes ?? [];
if (plateformes.length === 0) {
return (
<div className="card">
<p className="text-muted" style={{ margin: 0 }}>
Aucune recette crypto (plateforme PSAN) pour {annee}.
</p>
</div>
);
}
const totauxMois = MOIS_NUMS.map(m => r(plateformes.reduce((s, p) => s + (p.mois[m] ?? 0), 0)));
const d = data.donnees2042cpro;
const platLabel = (p) => {
const n = p.investisseur_nom ?? '';
const pr = p.investisseur_prenom ?? '';
const who = pr && n.startsWith(pr) ? n : [pr, n].filter(Boolean).join(' ');
return (
<>
<span style={{ fontWeight: 500 }}>{p.nom}</span>
{who && <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6 }}>— {who}</span>}
</>
);
};
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 (
<div className="card">
<div style={{ display: 'flex', gap: 0, alignItems: 'flex-start' }}>
<nav style={{
width: 200, flexShrink: 0, marginRight: 20,
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 10, padding: '8px 0', boxSizing: 'border-box',
}}>
<div style={{ padding: '6px 12px 8px', fontSize: 'var(--fs-xs)', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.06em' }}>
Sections
</div>
<button
className={`account-nav-item${view === 'matrice' ? ' active' : ''}`}
onClick={() => setView('matrice')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M3 3h18v2H3V3zm0 7h12v2H3v-2zm0 7h18v2H3v-2z'/>
</svg>
Suivi mensuel
</button>
<button
className={`account-nav-item${view === 'donnees' ? ' active' : ''}`}
onClick={() => setView('donnees')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'/>
</svg>
Données 2042 C PRO
</button>
<div style={{ margin: '10px 12px 6px', borderTop: '1px solid var(--border)' }} />
<div style={{ padding: '4px 12px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.5 }}>
Plateformes PSAN (crypto)<br/>BNC — régime micro, case 5HQ
</div>
</nav>
<div style={{ flex: 1, minWidth: 0 }}>
{/* ── Vue Suivi mensuel ── */}
{view === 'matrice' && (
<SuiviMensuelPsan annee={annee} plateformes={plateformes} totauxMois={totauxMois} />
)}
{/* ── Vue Données 2042 C PRO ── */}
{view === 'donnees' && (
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Bénéfices non commerciaux — Régime micro-BNC</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Vision annuelle {annee} — plateformes PSAN (staking / rewards crypto)</span>
</div>
<div style={{ padding: '0 14px 6px' }}>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="5HQ" /> Recettes brutes par plateforme
</div>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 700 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left' }}>Plateforme — Détenteur</th>
<th style={headStyle}>Recette brute {annee}</th>
</tr>
</thead>
<tbody>
{plateformes.map(p => {
const total = r(Object.values(p.mois).reduce((a, b) => a + b, 0));
return (
<tr key={p.id}>
<td style={{ ...cellStyle, textAlign: 'left' }}>{platLabel(p)}</td>
<td style={cellStyle}>{fmt0(Math.round(total))}</td>
</tr>
);
})}
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700 }}>Total</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(d.case_5HQ)}</td>
</tr>
</tbody>
</table>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '12px 14px', marginTop: 4, borderTop: '2px solid var(--border)',
background: 'rgba(99,102,241,0.05)',
}}>
<div>
<div style={{ fontWeight: 700, fontSize: 'var(--fs-sm)', display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="5HQ" tone="success" /> Micro-BNC — Recettes à reporter en 2042-C PRO
</div>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 4, maxWidth: 480, lineHeight: 1.5 }}>
Montant brut, sans déduire l'abattement — celui-ci ({Math.round(d.abattement_taux * 100)} %) est appliqué automatiquement par l'administration.
Bénéfice imposable indicatif : <strong>{fmt0(d.montant_imposable)}</strong>.
</div>
</div>
<div style={{ fontWeight: 800, fontSize: '1.2rem' }}>{fmt0(d.case_5HQ)}</div>
</div>
{d.depasse_seuil_micro && (
<div style={{
margin: '14px', padding: '10px 14px', borderRadius: 8,
background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.25)',
fontSize: 'var(--fs-xs)', color: 'var(--text)', lineHeight: 1.6,
}}>
⚠ Recettes supérieures au seuil du régime micro-BNC ({fmt0(d.seuil_micro_bnc)}) : le régime réel s'applique
(cases <strong>5KO</strong> bénéfice / <strong>5KP</strong> déficit), avec déduction des charges réelles
(non calculées par l'application) plutôt que l'abattement forfaitaire de {Math.round(d.abattement_taux * 100)} %.
</div>
)}
<div style={{ padding: '10px 14px 14px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.6 }}>
Les intérêts/rewards perçus via des plateformes de type « Prestataire de Services sur Actifs Numériques »
(staking, récompenses crypto) sont imposés en bénéfices non commerciaux (BNC), quelle que soit la
localisation de la plateforme — il ne s'agit pas d'un revenu de source étrangère : contrairement au 2047/2778-SD,
ce montant n'ouvre droit à aucun crédit d'impôt et ne se déclare pas via l'imprimé n° 2047. Il se reporte
directement sur la déclaration n° 2042-C PRO, rubrique « Régime micro BNC ».
</div>
</div>
)}
</div>
</div>
</div>
);
}
+11 -1
View File
@@ -44,7 +44,8 @@ function IconUpload() {
/* ── Constantes ───────────────────────────────────────────────── */
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
const FISCALITE_LABELS = { flat_tax: 'Flat tax (30%)', sans_fiscalite_locale: 'Sans fiscalité locale', avec_fiscalite_locale: 'Avec fiscalité locale' };
const EMPTY_REF = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', methode_remboursement: 'portefeuille', type_pret_defaut: '', freq_interets_defaut: '', description: '', categories: [], categories_inv_ids: [], secteurs_inv_ids: [] };
const CATEGORIE_PLATEFORME_LABELS = { psfp: 'Prestataire de Services de Financement Participatif', psan: 'Prestataire de Services sur Actifs Numériques' };
const EMPTY_REF = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', categorie_plateforme: 'psfp', methode_remboursement: 'portefeuille', type_pret_defaut: '', freq_interets_defaut: '', description: '', categories: [], categories_inv_ids: [], secteurs_inv_ids: [] };
/* ── Modal création / édition référentiel ─────────────────────── */
function RefModal({ row, onClose, onSaved }) {
@@ -59,6 +60,7 @@ function RefModal({ row, onClose, onSaved }) {
fiscalite: row.fiscalite || 'flat_tax',
taux_fiscalite_locale: row.taux_fiscalite_locale != null ? String(row.taux_fiscalite_locale) : '',
type_produit_fiscal: row.type_produit_fiscal || '2TT',
categorie_plateforme: row.categorie_plateforme || 'psfp',
methode_remboursement: row.methode_remboursement || 'portefeuille',
type_pret_defaut: row.type_pret_defaut || '',
freq_interets_defaut: row.freq_interets_defaut || '',
@@ -93,6 +95,7 @@ function RefModal({ row, onClose, onSaved }) {
taux_fiscalite_locale: fiscalite === 'avec_fiscalite_locale' && form.taux_fiscalite_locale !== ''
? Number(form.taux_fiscalite_locale) : null,
type_produit_fiscal: form.type_produit_fiscal,
categorie_plateforme: form.categorie_plateforme || 'psfp',
methode_remboursement: form.methode_remboursement || 'portefeuille',
type_pret_defaut: form.type_pret_defaut || null,
freq_interets_defaut: form.freq_interets_defaut || null,
@@ -181,6 +184,13 @@ function RefModal({ row, onClose, onSaved }) {
<option value="2TR">2TR — Revenus assimilés</option>
</select>
</div>
<div>
<label>Catégorie de plateforme</label>
<select value={form.categorie_plateforme || 'psfp'} onChange={e => set('categorie_plateforme', e.target.value)}>
<option value="psfp">{CATEGORIE_PLATEFORME_LABELS.psfp}</option>
<option value="psan">{CATEGORIE_PLATEFORME_LABELS.psan}</option>
</select>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<div>
<label>Méthode de remboursement</label>
+6
View File
@@ -35,6 +35,7 @@ const INV_TYPES = {
les_deux: 'Particulier & Entreprise',
};
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
const CATEGORIE_PLATEFORME_LABELS = { psfp: 'Prestataire de Services de Financement Participatif', psan: 'Prestataire de Services sur Actifs Numériques' };
// ── Composants helpers ────────────────────────────────────────────────────────
@@ -174,6 +175,7 @@ export default function PlatformeProfile() {
fiscalite: form.fiscalite,
taux_fiscalite_locale: form.taux_fiscalite_locale !== '' && form.taux_fiscalite_locale != null ? Number(form.taux_fiscalite_locale) : null,
type_produit_fiscal: form.type_produit_fiscal || '2TT',
categorie_plateforme: form.categorie_plateforme || 'psfp',
logo_filename: data.logo_filename ?? null,
description: form.description || null,
categories: data.categories || [],
@@ -275,6 +277,9 @@ export default function PlatformeProfile() {
{countryLabel(data.domiciliation)}
</span>
)}
<span title="Modifiable depuis la fiche du référentiel (liste Plateformes)" style={{ padding: '3px 10px', borderRadius: 20, background: 'var(--surface-2)', color: 'var(--text-muted)', fontSize: 12, flexShrink: 0 }}>
{CATEGORIE_PLATEFORME_LABELS[data.categorie_plateforme] ?? CATEGORIE_PLATEFORME_LABELS.psfp}
</span>
</div>
{editing
? <textarea value={form.description ?? ''} onChange={setInput('description')}
@@ -782,6 +787,7 @@ function ProfilImportBlock({ data, id, onApplied, catsInv = [], secteursInv = []
fiscalite: data.fiscalite,
taux_fiscalite_locale: data.taux_fiscalite_locale ?? null,
type_produit_fiscal: data.type_produit_fiscal || '2TT',
categorie_plateforme: data.categorie_plateforme || 'psfp',
logo_filename: data.logo_filename ?? null,
description: data.description || null,
categories: data.categories || [],
+45 -14
View File
@@ -13,6 +13,7 @@ import CerfaRecapTable from '../components/CerfaRecapTable.jsx';
import Cerfa2778Preview from '../components/Cerfa2778Preview.jsx';
import Cerfa2047Preview from '../components/Cerfa2047Preview.jsx';
import Cerfa2042Preview from '../components/Cerfa2042Preview.jsx';
import Cerfa2042CProPreview from '../components/Cerfa2042CProPreview.jsx';
/* ── YearSelector ────────────────────────────────────────────── */
function YearSelector({ annee, setAnnee, availableYears }) {
@@ -229,13 +230,15 @@ 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)
const [hasEtranger, setHasEtranger] = useState(false); // au moins une plateforme étrangère PSFP configurée (pour l'onglet CERFA 2047 — indépendant de l'année sélectionnée, même principe que le 2778-SD ; les plateformes PSAN ne sont pas concernées)
const [hasPsan, setHasPsan] = useState(false); // au moins une plateforme PSAN (crypto) configurée, quelle que soit sa domiciliation (pour l'onglet CERFA 2042 C PRO — indépendant de l'année sélectionnée, même principe que hasEtranger)
/* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */
useEffect(() => {
api.get('/plateformes').then(p => {
setHasPlateformes((p || []).length > 0);
setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR'));
setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR' && (pl.categorie_plateforme ?? 'psfp') === 'psfp'));
setHasPsan((p || []).some(pl => (pl.categorie_plateforme ?? 'psfp') === 'psan'));
}).catch(() => {});
}, []);
@@ -366,19 +369,35 @@ export default function TaxReport() {
<div className="card text-muted">Chargement…</div>
) : (
<>
{!detailExpanded && !cerfaExpanded && data2042 && <div className="card">
<h3 style={{ marginTop: 0 }}>Cases fiscales 2042 — synthèse {annee}</h3>
<div className="kpi-grid">
{data2042.case_2TT > 0 && <div className="kpi"><div className="label">Case 2TT — Prêts participatifs (FR)</div><div className="value">{fmtEUR(data2042.case_2TT)}</div></div>}
{data2042.case_2TR > 0 && <div className="kpi"><div className="label">Case 2TR — Revenus fixes (FR + étranger)</div><div className="value">{fmtEUR(data2042.case_2TR)}</div></div>}
<div className="kpi"><div className="label">Case 2BH — Base CSG/CRDS</div><div className="value">{fmtEUR(data2042.case_2BH)}</div></div>
<div className="kpi"><div className="label">Case 2CK — Crédit d'impôt</div><div className="value" style={{ color: 'var(--success)' }}>{fmtEUR(data2042.case_2CK)}</div></div>
{data2042.case_2TY > 0 && <div className="kpi"><div className="label">Case 2TY — Pertes en capital</div><div className="value danger">{fmtEUR(data2042.case_2TY)}</div></div>}
{!detailExpanded && !cerfaExpanded && data2042 && (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'stretch' }}>
<div className="card" style={{ flex: '3 1 420px' }}>
<h3 style={{ marginTop: 0 }}>Cases fiscales 2042 — synthèse {annee}</h3>
<div className="kpi-grid">
{data2042.case_2TT > 0 && <div className="kpi"><div className="label">Case 2TT — Prêts participatifs (FR)</div><div className="value">{fmtEUR(data2042.case_2TT)}</div></div>}
{data2042.case_2TR > 0 && <div className="kpi"><div className="label">Case 2TR — Revenus fixes (FR + étranger)</div><div className="value">{fmtEUR(data2042.case_2TR)}</div></div>}
<div className="kpi"><div className="label">Case 2BH — Base CSG/CRDS</div><div className="value">{fmtEUR(data2042.case_2BH)}</div></div>
<div className="kpi"><div className="label">Case 2CK — Crédit d'impôt</div><div className="value" style={{ color: 'var(--success)' }}>{fmtEUR(data2042.case_2CK)}</div></div>
{data2042.case_2TY > 0 && <div className="kpi"><div className="label">Case 2TY — Pertes en capital</div><div className="value danger">{fmtEUR(data2042.case_2TY)}</div></div>}
</div>
<p className="text-muted" style={{ marginTop: 12, fontSize: 12 }}>
⚠ Cases indicatives combinant plateformes françaises (IFU automatique) et étrangères. Référez-vous à la notice 2041-GFI.
</p>
</div>
{hasPsan && data?.cerfa2042cpro?.donnees2042cpro && (
<div className="card" style={{ flex: '1 1 220px' }}>
<h3 style={{ marginTop: 0 }}>Cases fiscales 2042 C PRO — synthèse {annee}</h3>
<div className="kpi-grid">
<div className="kpi"><div className="label">Case 5HQ — Micro-BNC (PSAN)</div><div className="value">{fmtEUR(data.cerfa2042cpro.donnees2042cpro.case_5HQ)}</div></div>
</div>
<p className="text-muted" style={{ marginTop: 12, fontSize: 12 }}>
⚠ Montant brut ; l'abattement de 34 % (régime micro-BNC) est appliqué automatiquement par l'administration.
</p>
</div>
)}
</div>
<p className="text-muted" style={{ marginTop: 12, fontSize: 12 }}>
⚠ Cases indicatives combinant plateformes françaises (IFU automatique) et étrangères. Référez-vous à la notice 2041-GFI.
</p>
</div>}
)}
{!detailExpanded && !cerfaExpanded && data2042 && (
<div className="card" style={{ marginTop: 16 }}>
@@ -409,6 +428,13 @@ export default function TaxReport() {
texte="Les intérêts perçus via des plateformes étrangères sont soumis à un prélèvement forfaitaire obligatoire (acompte) si vous avez un revenu fiscal de référence de l'avant-dernière année de 25 000 € pour un personne seule ou 50 000 € pour couple marié ou pacsé. Malgré la déclaration mensuelle aux autorités fiscales, la déclaration annuelle au travers du CERFA 2047 reste obligatoire."
/>
)}
{hasPsan && (
<InfoLigneCerfa
code="2042-C PRO"
titre="Intérêts d'emprunts sur une plateforme d'actif numérique (Crowdlending sur RWA)"
texte="Les intérêts perçus via des plateformes de type « Prestataire de Services sur Actifs Numériques » pour des prêts de type Crowdlending sur des RWA (Real World Asset) sont des bénéfices non commerciaux (BNC), quelle que soit leur localisation. Pas de déclaration CERFA 2047, ni de crédit d'impôt possible. Une déclaration directe en 2042-C PRO en régime micro-BNC (case 5HQ)."
/>
)}
</div>
<div style={{ flex: '1 1 320px', minWidth: 280, display: 'flex', justifyContent: 'center' }}>
<CerfaFlowDiagram hasEtranger={hasEtranger} />
@@ -445,6 +471,7 @@ export default function TaxReport() {
{!detailExpanded && !cerfaExpanded && (
<div className="dr-tabs">
<button className={`dr-tab${activeTab === '2042' ? ' active' : ''}`} onClick={() => setActiveTab('2042')}>CERFA 2042</button>
{hasPsan && <button className={`dr-tab${activeTab === '2042cpro' ? ' active' : ''}`} onClick={() => setActiveTab('2042cpro')}>CERFA 2042 C PRO</button>}
<button className={`dr-tab${activeTab === 'cerfa' ? ' active' : ''}`} onClick={() => setActiveTab('cerfa')}>CERFA 2561 (IFU)</button>
{hasEtranger && <button className={`dr-tab${activeTab === '2047' ? ' active' : ''}`} onClick={() => setActiveTab('2047')}>CERFA 2047</button>}
{pfoAssujetti && <button className={`dr-tab${activeTab === '2778' ? ' active' : ''}`} onClick={() => setActiveTab('2778')}>CERFA 2778-SD</button>}
@@ -473,6 +500,10 @@ export default function TaxReport() {
{activeTab === '2778' && pfoAssujetti && !detailExpanded && !cerfaExpanded && (
<Cerfa2778Preview annee={annee} activeView={activeView} />
)}
{activeTab === '2042cpro' && hasPsan && !detailExpanded && !cerfaExpanded && (
<Cerfa2042CProPreview annee={annee} activeView={activeView} />
)}
</>
);
}
@@ -24,6 +24,7 @@ function localToday() {
}
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
const CATEGORIE_PLATEFORME_LABELS = { psfp: 'Prestataire de Services de Financement Participatif', psan: 'Prestataire de Services sur Actifs Numériques' };
const FISCALITE_LABELS = {
flat_tax: 'Flat Tax',
sans_fiscalite_locale: 'Sans fiscalité locale',
@@ -51,7 +52,7 @@ function fmtFiscalite(p) {
return base;
}
const EMPTY_PLAT = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', methode_remboursement: 'portefeuille', investisseur_id: null, date_ouverture: '', logo_filename: null, type_pret_defaut: '', freq_interets_defaut: '', referentiel_id: null };
const EMPTY_PLAT = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', categorie_plateforme: 'psfp', methode_remboursement: 'portefeuille', investisseur_id: null, date_ouverture: '', logo_filename: null, type_pret_defaut: '', freq_interets_defaut: '', referentiel_id: null };
const LOGO_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/';
const logoUrl = (filename) => filename ? LOGO_BASE + filename : null;
@@ -211,6 +212,7 @@ function PlatDetailPanel({ plat, onEdit }) {
plat.date_ouverture && { label: "Date d'ouverture", value: fmtDate(plat.date_ouverture) },
{ label: 'Domiciliation', value: plat.domiciliation ? <span style={{ display:'inline-flex', alignItems:'center', gap:5 }}><FlagIcon code={plat.domiciliation} size={16} />{countryLabel(plat.domiciliation)}</span> : '—' },
{ label: 'Fiscalité', value: fmtFiscalite(plat) },
{ label: 'Catégorie de plateforme', value: CATEGORIE_PLATEFORME_LABELS[plat.categorie_plateforme] ?? CATEGORIE_PLATEFORME_LABELS.psfp },
plat.domiciliation === 'FR' && {
label: 'Déclaration 2561',
value: (plat.type_produit_fiscal ?? '2TT') === '2TR'
@@ -581,6 +583,15 @@ function PlatForm({ state, setter, logoFile, setLogoFile, logoPreview, setLogoPr
</select>
))}
{/* Catégorie de plateforme */}
{row('Catégorie de plateforme', (
<select value={state.categorie_plateforme || 'psfp'} onChange={e => setter(s => ({ ...s, categorie_plateforme: e.target.value }))}
style={{ width: '100%' }}>
<option value="psfp">{CATEGORIE_PLATEFORME_LABELS.psfp}</option>
<option value="psan">{CATEGORIE_PLATEFORME_LABELS.psan}</option>
</select>
))}
{/* Méthode de remboursement */}
{row('Versement des remboursements', (
<select value={state.methode_remboursement || 'portefeuille'} onChange={e => setter(s => ({ ...s, methode_remboursement: e.target.value }))}
@@ -848,6 +859,7 @@ export default function PlateformesSection() {
fiscalite: ref.fiscalite || 'flat_tax',
taux_fiscalite_locale: ref.taux_fiscalite_locale ?? null,
type_produit_fiscal: ref.type_produit_fiscal || '2TT',
categorie_plateforme: ref.categorie_plateforme || 'psfp',
methode_remboursement: ref.methode_remboursement || 'portefeuille',
type_pret_defaut: ref.type_pret_defaut || null,
freq_interets_defaut: ref.freq_interets_defaut || null,
@@ -952,6 +964,7 @@ export default function PlateformesSection() {
fiscalite: p.fiscalite || 'flat_tax',
taux_fiscalite_locale: p.taux_fiscalite_locale ?? '',
type_produit_fiscal: p.type_produit_fiscal || '2TT',
categorie_plateforme: p.categorie_plateforme || 'psfp',
methode_remboursement: p.methode_remboursement || 'portefeuille',
investisseur_id: p.investisseur_id ?? null,
date_ouverture: p.date_ouverture || '',
@@ -974,6 +987,7 @@ export default function PlateformesSection() {
taux_fiscalite_locale: editPlat.fiscalite === 'avec_fiscalite_locale' && editPlat.taux_fiscalite_locale !== ''
? Number(editPlat.taux_fiscalite_locale) : null,
type_produit_fiscal: editPlat.type_produit_fiscal || '2TT',
categorie_plateforme: editPlat.categorie_plateforme || 'psfp',
methode_remboursement: editPlat.methode_remboursement || 'portefeuille',
investisseur_id: editPlat.investisseur_id ? Number(editPlat.investisseur_id) : null,
date_ouverture: editPlat.date_ouverture || null,