Catégorisation des platformes et application de la fiscalité adapté (Impact PSAN , régime micro-BIC)
This commit is contained in:
1 parent
8953d0bb35
commit
33fee587aa
10 files changed
+600
-43
No files matched your search
@@ -2576,4 +2576,23 @@ if (!smtpCols.includes('documents_quota_count')) {
|
||||
db.exec('ALTER TABLE smtp_config ADD COLUMN documents_quota_count INTEGER NOT NULL DEFAULT 500');
|
||||
}
|
||||
|
||||
// ── Migration : catégorie de plateforme (PSFP / PSAN) ──────────────────────
|
||||
// Distingue les plateformes "Prestataire de Services de Financement
|
||||
// Participatif" (crowdfunding classique) des "Prestataire de Services sur
|
||||
// Actifs Numériques" (crypto). Valeur définie au niveau du référentiel admin
|
||||
// et répliquée sur les plateformes utilisateur à la création ; modifiable
|
||||
// ensuite par l'utilisateur (Paramètres > Plateformes) comme les autres
|
||||
// champs hérités (cf. HERITABLE_FIELDS / overridden_fields). Demande Olivier
|
||||
// 30/08/26.
|
||||
{
|
||||
const refColsCatPlat = db.prepare('PRAGMA table_info(plateformes_referentiel)').all().map(c => c.name);
|
||||
if (!refColsCatPlat.includes('categorie_plateforme')) {
|
||||
db.exec("ALTER TABLE plateformes_referentiel ADD COLUMN categorie_plateforme TEXT NOT NULL DEFAULT 'psfp'");
|
||||
}
|
||||
const platColsCatPlat = db.prepare('PRAGMA table_info(plateformes)').all().map(c => c.name);
|
||||
if (!platColsCatPlat.includes('categorie_plateforme')) {
|
||||
db.exec("ALTER TABLE plateformes ADD COLUMN categorie_plateforme TEXT NOT NULL DEFAULT 'psfp'");
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
@@ -54,6 +54,7 @@ const Schema = z.object({
|
||||
fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'),
|
||||
taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(),
|
||||
type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'),
|
||||
categorie_plateforme: z.enum(['psfp', 'psan']).default('psfp'),
|
||||
methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).default('portefeuille'),
|
||||
investisseur_id: z.number().int().positive().nullable().optional(),
|
||||
date_ouverture: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
|
||||
@@ -63,7 +64,7 @@ const Schema = z.object({
|
||||
});
|
||||
|
||||
// Champs hérités du référentiel (comparés pour calculer overridden_fields)
|
||||
const HERITABLE_FIELDS = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'icone_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
|
||||
const HERITABLE_FIELDS = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'categorie_plateforme', 'logo_filename', 'icone_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
|
||||
|
||||
/**
|
||||
* Calcule overridden_fields : champs dont la valeur diffère du référentiel.
|
||||
@@ -222,7 +223,7 @@ function syncCategories(platId, catIds) {
|
||||
router.get('/referentiel-list', (_req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT pr.id, pr.nom, pr.url, pr.domiciliation, pr.fiscalite,
|
||||
pr.taux_fiscalite_locale, pr.type_produit_fiscal, pr.logo_filename, pr.icone_filename,
|
||||
pr.taux_fiscalite_locale, pr.type_produit_fiscal, pr.categorie_plateforme, pr.logo_filename, pr.icone_filename,
|
||||
pr.methode_remboursement, pr.type_pret_defaut, pr.freq_interets_defaut
|
||||
FROM plateformes_referentiel pr
|
||||
ORDER BY pr.nom
|
||||
@@ -233,7 +234,7 @@ router.get('/referentiel-list', (_req, res) => {
|
||||
router.get('/', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT p.id, p.nom, p.url, p.notes, p.domiciliation, p.fiscalite, p.taux_fiscalite_locale,
|
||||
p.type_produit_fiscal,
|
||||
p.type_produit_fiscal, p.categorie_plateforme,
|
||||
p.methode_remboursement, p.investisseur_id, p.date_ouverture, p.logo_filename, p.icone_filename, p.created_at,
|
||||
p.type_pret_defaut, p.freq_interets_defaut,
|
||||
p.referentiel_id, p.overridden_fields,
|
||||
@@ -283,15 +284,15 @@ router.post('/', (req, res, next) => {
|
||||
|
||||
const r = db.prepare(`
|
||||
INSERT INTO plateformes
|
||||
(user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal,
|
||||
(user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, categorie_plateforme,
|
||||
methode_remboursement, investisseur_id, date_ouverture,
|
||||
type_pret_defaut, freq_interets_defaut,
|
||||
logo_filename, icone_filename,
|
||||
referentiel_id, overridden_fields)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`).run(
|
||||
req.user.id, body.nom, body.url || null, body.notes || null,
|
||||
body.domiciliation, fiscalite, taux, typeProduitFiscal,
|
||||
body.domiciliation, fiscalite, taux, typeProduitFiscal, body.categorie_plateforme,
|
||||
body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null,
|
||||
body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null,
|
||||
logoFilename, iconeFilename,
|
||||
@@ -358,6 +359,7 @@ router.get('/export', (req, res, next) => {
|
||||
fiscalite: r.fiscalite,
|
||||
taux_fiscalite_locale: r.taux_fiscalite_locale,
|
||||
type_produit_fiscal: r.type_produit_fiscal,
|
||||
categorie_plateforme: r.categorie_plateforme,
|
||||
methode_remboursement: r.methode_remboursement,
|
||||
date_ouverture: r.date_ouverture,
|
||||
type_pret_defaut: r.type_pret_defaut,
|
||||
@@ -426,6 +428,7 @@ router.get('/:id/export', (req, res, next) => {
|
||||
domiciliation: withAll.domiciliation, fiscalite: withAll.fiscalite,
|
||||
taux_fiscalite_locale: withAll.taux_fiscalite_locale,
|
||||
type_produit_fiscal: withAll.type_produit_fiscal,
|
||||
categorie_plateforme: withAll.categorie_plateforme,
|
||||
methode_remboursement: withAll.methode_remboursement,
|
||||
date_ouverture: withAll.date_ouverture,
|
||||
type_pret_defaut: withAll.type_pret_defaut, freq_interets_defaut: withAll.freq_interets_defaut,
|
||||
@@ -555,7 +558,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
const fields = [
|
||||
p.url || null, p.notes || null,
|
||||
p.domiciliation || 'france', fiscalite, taux,
|
||||
p.type_produit_fiscal || '2TT', p.methode_remboursement || 'portefeuille',
|
||||
p.type_produit_fiscal || '2TT', p.categorie_plateforme || 'psfp', p.methode_remboursement || 'portefeuille',
|
||||
investisseurId, p.date_ouverture || null,
|
||||
p.type_pret_defaut || null, p.freq_interets_defaut || null,
|
||||
p.logo_filename || null, p.icone_filename || null,
|
||||
@@ -564,7 +567,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
if (existing) {
|
||||
db.prepare(`
|
||||
UPDATE plateformes SET url=?, notes=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
|
||||
type_produit_fiscal=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?,
|
||||
type_produit_fiscal=?, categorie_plateforme=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?,
|
||||
type_pret_defaut=?, freq_interets_defaut=?,
|
||||
logo_filename=?, icone_filename=?
|
||||
WHERE id=? AND user_id=?
|
||||
@@ -575,10 +578,10 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
const r = db.prepare(`
|
||||
INSERT INTO plateformes
|
||||
(user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale,
|
||||
type_produit_fiscal, methode_remboursement, investisseur_id, date_ouverture,
|
||||
type_produit_fiscal, categorie_plateforme, methode_remboursement, investisseur_id, date_ouverture,
|
||||
type_pret_defaut, freq_interets_defaut,
|
||||
logo_filename, icone_filename, overridden_fields)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`).run(req.user.id, p.nom, ...fields, '[]');
|
||||
platId = r.lastInsertRowid;
|
||||
created++;
|
||||
@@ -629,6 +632,7 @@ router.put('/:id', (req, res, next) => {
|
||||
fiscalite,
|
||||
taux_fiscalite_locale: taux,
|
||||
type_produit_fiscal: typeProduitFiscal,
|
||||
categorie_plateforme: body.categorie_plateforme,
|
||||
logo_filename: current.logo_filename, // logo géré séparément
|
||||
methode_remboursement: body.methode_remboursement,
|
||||
type_pret_defaut: body.type_pret_defaut ?? null,
|
||||
@@ -640,13 +644,13 @@ router.put('/:id', (req, res, next) => {
|
||||
const r = db.prepare(`
|
||||
UPDATE plateformes
|
||||
SET nom=?, url=?, notes=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
|
||||
type_produit_fiscal=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?,
|
||||
type_produit_fiscal=?, categorie_plateforme=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?,
|
||||
type_pret_defaut=?, freq_interets_defaut=?,
|
||||
overridden_fields=?
|
||||
WHERE id=? AND user_id=?
|
||||
`).run(
|
||||
body.nom, body.url || null, body.notes || null, body.domiciliation, fiscalite, taux,
|
||||
typeProduitFiscal, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null,
|
||||
typeProduitFiscal, body.categorie_plateforme, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null,
|
||||
body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null,
|
||||
JSON.stringify(overriddenFields),
|
||||
req.params.id, req.user.id
|
||||
@@ -781,12 +785,12 @@ router.post('/:id/reset', (req, res, next) => {
|
||||
db.prepare(`
|
||||
UPDATE plateformes
|
||||
SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
|
||||
type_produit_fiscal=?, logo_filename=?, methode_remboursement=?,
|
||||
type_produit_fiscal=?, categorie_plateforme=?, logo_filename=?, methode_remboursement=?,
|
||||
type_pret_defaut=?, freq_interets_defaut=?, overridden_fields='[]'
|
||||
WHERE id=? AND user_id=?
|
||||
`).run(
|
||||
ref.nom, ref.url ?? null, ref.domiciliation, ref.fiscalite, ref.taux_fiscalite_locale ?? null,
|
||||
ref.type_produit_fiscal, ref.logo_filename ?? null, ref.methode_remboursement || 'portefeuille',
|
||||
ref.type_produit_fiscal, ref.categorie_plateforme || 'psfp', ref.logo_filename ?? null, ref.methode_remboursement || 'portefeuille',
|
||||
ref.type_pret_defaut ?? null, ref.freq_interets_defaut ?? null,
|
||||
req.params.id, req.user.id
|
||||
);
|
||||
|
||||
@@ -77,6 +77,7 @@ const Schema = z.object({
|
||||
fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'),
|
||||
taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(),
|
||||
type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'),
|
||||
categorie_plateforme: z.enum(['psfp', 'psan']).default('psfp'),
|
||||
methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).optional().default('portefeuille'),
|
||||
type_pret_defaut: z.enum(['in_fine', 'amortissable', 'differe']).nullable().optional(),
|
||||
freq_interets_defaut: z.enum(['mensuel', 'trimestriel', 'in_fine']).nullable().optional(),
|
||||
@@ -321,6 +322,7 @@ router.get('/export', (req, res, next) => {
|
||||
fiscalite: r.fiscalite,
|
||||
taux_fiscalite_locale: r.taux_fiscalite_locale,
|
||||
type_produit_fiscal: r.type_produit_fiscal,
|
||||
categorie_plateforme: r.categorie_plateforme,
|
||||
description: r.description,
|
||||
annee_creation: r.annee_creation,
|
||||
investisseurs_types: r.investisseurs_types,
|
||||
@@ -523,7 +525,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
|
||||
const fields = [
|
||||
p.url || null, p.domiciliation || 'france', p.fiscalite || 'flat_tax',
|
||||
p.taux_fiscalite_locale ?? null, p.type_produit_fiscal || '2TT',
|
||||
p.taux_fiscalite_locale ?? null, p.type_produit_fiscal || '2TT', p.categorie_plateforme || 'psfp',
|
||||
p.methode_remboursement || 'portefeuille', p.type_pret_defaut ?? null, p.freq_interets_defaut ?? null,
|
||||
p.description ?? null, p.annee_creation ?? null,
|
||||
p.investisseurs_types ?? null,
|
||||
@@ -541,7 +543,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
if (existing) {
|
||||
db.prepare(`
|
||||
UPDATE plateformes_referentiel SET
|
||||
url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, type_produit_fiscal=?,
|
||||
url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, type_produit_fiscal=?, categorie_plateforme=?,
|
||||
methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?,
|
||||
description=?, annee_creation=?, investisseurs_types=?,
|
||||
regulateur=?, numero_licence=?, is_regule=?,
|
||||
@@ -559,7 +561,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
} else {
|
||||
const r = db.prepare(`
|
||||
INSERT INTO plateformes_referentiel
|
||||
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal,
|
||||
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, categorie_plateforme,
|
||||
methode_remboursement, type_pret_defaut, freq_interets_defaut,
|
||||
description, annee_creation, investisseurs_types,
|
||||
regulateur, numero_licence, is_regule,
|
||||
@@ -569,7 +571,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
||||
garantie_rachat, statistiques_publiques, bonus_inscription, marche_secondaire, investissement_auto,
|
||||
url_trustpilot, url_linkedin,
|
||||
logo_filename, icone_filename, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
|
||||
`).run(p.nom, ...fields);
|
||||
refId = r.lastInsertRowid;
|
||||
created++;
|
||||
@@ -668,7 +670,7 @@ router.post('/', (req, res, next) => {
|
||||
|
||||
const r = db.prepare(`
|
||||
INSERT INTO plateformes_referentiel
|
||||
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal,
|
||||
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, categorie_plateforme,
|
||||
methode_remboursement, type_pret_defaut, freq_interets_defaut,
|
||||
logo_filename, description,
|
||||
annee_creation, investisseurs_types,
|
||||
@@ -680,7 +682,7 @@ router.post('/', (req, res, next) => {
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now'))
|
||||
`).run(
|
||||
data.nom, data.url || null, data.domiciliation, data.fiscalite,
|
||||
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal,
|
||||
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal, data.categorie_plateforme,
|
||||
data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null,
|
||||
data.logo_filename ?? null, data.description ?? null,
|
||||
data.annee_creation ?? null, data.investisseurs_types ?? null,
|
||||
@@ -722,7 +724,7 @@ router.put('/:id', (req, res, next) => {
|
||||
db.prepare(`
|
||||
UPDATE plateformes_referentiel
|
||||
SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
|
||||
type_produit_fiscal=?, methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?,
|
||||
type_produit_fiscal=?, categorie_plateforme=?, methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?,
|
||||
logo_filename=?, description=?,
|
||||
annee_creation=?, investisseurs_types=?,
|
||||
regulateur=?, numero_licence=?, is_regule=?,
|
||||
@@ -735,7 +737,7 @@ router.put('/:id', (req, res, next) => {
|
||||
WHERE id=?
|
||||
`).run(
|
||||
data.nom, data.url || null, data.domiciliation, data.fiscalite,
|
||||
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal,
|
||||
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal, data.categorie_plateforme,
|
||||
data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null,
|
||||
data.logo_filename ?? null, data.description ?? null,
|
||||
data.annee_creation ?? null, data.investisseurs_types ?? null,
|
||||
@@ -814,7 +816,7 @@ router.post('/:id/push', (req, res, next) => {
|
||||
const force = req.body?.force === true;
|
||||
|
||||
// Champs scalaires poussables
|
||||
const PUSHABLE = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
|
||||
const PUSHABLE = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'categorie_plateforme', 'logo_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
|
||||
|
||||
// Catégories/secteurs du référentiel
|
||||
const refCatIds = db.prepare(
|
||||
|
||||
@@ -117,6 +117,15 @@ router.get('/', (req, res) => {
|
||||
|
||||
const interetsNets = recap.interets_bruts - recap.prelev_sociaux - recap.prelev_forfaitaire;
|
||||
|
||||
// Au moins une plateforme PSAN (crypto) configurée, quelle que soit sa domiciliation — pour
|
||||
// exposer cerfa2042cpro (case 5HQ, micro-BNC) au frontend. Même logique que v1/taxreport.js
|
||||
// (Olivier 30/08/26) : ce handler interne '/' alimente le state `data` de TaxReport.jsx, donc
|
||||
// il doit porter ce champ lui aussi (l'agrégat /api/v1/taxreport est un endpoint distinct).
|
||||
const hasPsan = !!db.prepare(
|
||||
`SELECT 1 FROM plateformes WHERE user_id = ? AND categorie_plateforme = 'psan' LIMIT 1`
|
||||
).get(req.user.id);
|
||||
const cerfa2042cpro = hasPsan ? build2042CPro(annee, invCond, invCondBonus, invArg) : null;
|
||||
|
||||
const recapTotal = {
|
||||
interets_bruts: round2(recap.interets_bruts),
|
||||
prelev_sociaux: round2(recap.prelev_sociaux),
|
||||
@@ -135,7 +144,7 @@ router.get('/', (req, res) => {
|
||||
note: "Les cases sont indicatives. Verifiez avec votre situation fiscale reelle.",
|
||||
};
|
||||
|
||||
res.json({ annee, recap: recapTotal, pertes, pertesTotales: round2(pertesTotales), detail, corrections, cases });
|
||||
res.json({ annee, recap: recapTotal, pertes, pertesTotales: round2(pertesTotales), detail, corrections, cases, has_psan: hasPsan, cerfa2042cpro });
|
||||
});
|
||||
|
||||
router.get('/export', (req, res) => {
|
||||
@@ -512,8 +521,13 @@ router.get('/years', (req, res) => {
|
||||
|
||||
|
||||
|
||||
// Filtre commun 2047/2778-SD : seules les plateformes étrangères (domiciliation != FR)
|
||||
// ET de catégorie "Prestataire de Services de Financement Participatif" (PSFP) sont
|
||||
// concernées par ces CERFA. Les plateformes "Prestataire de Services sur Actifs
|
||||
// Numériques" (PSAN) sont exclues même si domiciliées hors de France (demande Olivier
|
||||
// 30/08/26).
|
||||
function build2778(annee, invCond, invCondBonus2778, invArg) {
|
||||
// Plateformes étrangères × investisseur avec au moins un remboursement sur l'année
|
||||
// Plateformes étrangères PSFP × investisseur avec au moins un remboursement sur l'année
|
||||
const platRows = db.prepare(`
|
||||
SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
|
||||
FROM remboursements r
|
||||
@@ -525,6 +539,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'normal'
|
||||
AND p.domiciliation != 'FR'
|
||||
AND p.categorie_plateforme = 'psfp'
|
||||
ORDER BY p.nom, inv.nom
|
||||
`).all(invArg, annee);
|
||||
|
||||
@@ -540,6 +555,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'interets_plateforme'
|
||||
AND p.domiciliation != 'FR'
|
||||
AND p.categorie_plateforme = 'psfp'
|
||||
`).all(invArg, annee);
|
||||
|
||||
const platRowsMap = {};
|
||||
@@ -569,6 +585,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'normal'
|
||||
AND p.domiciliation != 'FR'
|
||||
AND p.categorie_plateforme = 'psfp'
|
||||
GROUP BY p.id, i.investisseur_id, mois
|
||||
`).all(invArg, annee);
|
||||
|
||||
@@ -590,6 +607,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'interets_plateforme'
|
||||
AND p.domiciliation != 'FR'
|
||||
AND p.categorie_plateforme = 'psfp'
|
||||
GROUP BY p.id, r.bonus_investisseur_id, mois
|
||||
`).all(invArg, annee);
|
||||
|
||||
@@ -636,6 +654,7 @@ router.get('/2778', (req, res) => {
|
||||
res.json(build2778(annee, invCond, invCondBonus2778, invArg));
|
||||
});
|
||||
|
||||
// cf. commentaire au-dessus de build2778 : même filtre PSFP.
|
||||
function build2047(annee, invCond, invCondBonus2047, invArg) {
|
||||
const rows = db.prepare(`
|
||||
SELECT
|
||||
@@ -657,6 +676,7 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'normal'
|
||||
AND p.domiciliation != 'FR'
|
||||
AND p.categorie_plateforme = 'psfp'
|
||||
GROUP BY p.id, inv.id
|
||||
`).all(invArg, annee);
|
||||
|
||||
@@ -679,6 +699,7 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'interets_plateforme'
|
||||
AND p.domiciliation != 'FR'
|
||||
AND p.categorie_plateforme = 'psfp'
|
||||
GROUP BY p.id, inv.id
|
||||
`).all(invArg, annee);
|
||||
|
||||
@@ -793,6 +814,162 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
|
||||
É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). ── */
|
||||
/* ── Filtre commun 2042 C PRO (PSAN) : plateformes de catégorie "Prestataire de Services sur
|
||||
Actifs Numériques" (crypto), quelle que soit leur domiciliation (FR ou étrangère) — contrairement
|
||||
à 2047/2778-SD (PSFP étrangères uniquement), la doctrine BNC/micro-BNC pour les intérêts/rewards
|
||||
crypto (staking) s'applique en fonction de la nature du revenu, pas de la localisation de la
|
||||
plateforme. Pas de crédit d'impôt conventionnel ici (pas de mécanisme 2047) : les recettes sont
|
||||
déclarées directement en 2042-C PRO, case 5HQ (régime micro-BNC, abattement forfaitaire de 34 %
|
||||
appliqué automatiquement par l'administration). Demande Olivier 30/08/26. ── */
|
||||
function build2042CPro(annee, invCond, invCondBonus2042cpro, invArg) {
|
||||
// Plateformes PSAN × investisseur avec au moins un remboursement sur l'année
|
||||
const platRows = db.prepare(`
|
||||
SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
|
||||
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.categorie_plateforme = 'psan'
|
||||
ORDER BY p.nom, inv.nom
|
||||
`).all(invArg, annee);
|
||||
|
||||
// Intérêts plateforme (pas d'investissement) sur des plateformes PSAN — même logique de
|
||||
// fusion que pour le 2778/2047 : rattachés à la ligne plateforme × investisseur.
|
||||
const platRowsInteretsPlateforme = db.prepare(`
|
||||
SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
|
||||
FROM remboursements r
|
||||
JOIN plateformes p ON p.id = r.bonus_plateforme_id
|
||||
JOIN investisseurs inv ON inv.id = r.bonus_investisseur_id
|
||||
WHERE ${invCondBonus2042cpro}
|
||||
AND substr(r.date_remb,1,4) = ?
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'interets_plateforme'
|
||||
AND p.categorie_plateforme = 'psan'
|
||||
`).all(invArg, annee);
|
||||
|
||||
const platRowsMap = {};
|
||||
for (const p of [...platRows, ...platRowsInteretsPlateforme]) {
|
||||
platRowsMap[`${p.id}_${p.investisseur_id}`] = p;
|
||||
}
|
||||
const platRowsMerged = Object.values(platRowsMap).sort((a, b) =>
|
||||
a.nom.localeCompare(b.nom) || (a.investisseur_nom ?? '').localeCompare(b.investisseur_nom ?? ''));
|
||||
|
||||
// Montants mensuels par plateforme × investisseur — même base que 2047/2778 (intérêts bruts
|
||||
// avant retenue locale) : il n'y a pas de mécanisme de crédit d'impôt pour la partie BNC/PSAN,
|
||||
// mais la base de calcul de la recette brute reste cohérente avec le reste de l'app.
|
||||
const moisRows = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plateforme_id,
|
||||
i.investisseur_id,
|
||||
substr(r.date_remb,6,2) AS mois,
|
||||
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
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE ${invCond}
|
||||
AND substr(r.date_remb,1,4) = ?
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'normal'
|
||||
AND p.categorie_plateforme = 'psan'
|
||||
GROUP BY p.id, i.investisseur_id, mois
|
||||
`).all(invArg, annee);
|
||||
|
||||
const moisRowsInteretsPlateforme = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plateforme_id,
|
||||
r.bonus_investisseur_id AS investisseur_id,
|
||||
substr(r.date_remb,6,2) AS mois,
|
||||
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
|
||||
FROM remboursements r
|
||||
JOIN plateformes p ON p.id = r.bonus_plateforme_id
|
||||
WHERE ${invCondBonus2042cpro}
|
||||
AND substr(r.date_remb,1,4) = ?
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'interets_plateforme'
|
||||
AND p.categorie_plateforme = 'psan'
|
||||
GROUP BY p.id, r.bonus_investisseur_id, mois
|
||||
`).all(invArg, annee);
|
||||
|
||||
const moisMap = {};
|
||||
for (const m of moisRows) {
|
||||
const key = `${m.plateforme_id}_${m.investisseur_id}`;
|
||||
if (!moisMap[key]) moisMap[key] = {};
|
||||
moisMap[key][m.mois] = round2(m.montant);
|
||||
}
|
||||
for (const m of moisRowsInteretsPlateforme) {
|
||||
const key = `${m.plateforme_id}_${m.investisseur_id}`;
|
||||
if (!moisMap[key]) moisMap[key] = {};
|
||||
moisMap[key][m.mois] = round2((moisMap[key][m.mois] ?? 0) + m.montant);
|
||||
}
|
||||
|
||||
const plateformes = platRowsMerged.map(p => ({
|
||||
id: `${p.id}_${p.investisseur_id}`,
|
||||
plateforme_id: p.id,
|
||||
nom: p.nom,
|
||||
investisseur_id: p.investisseur_id,
|
||||
investisseur_nom: p.investisseur_nom,
|
||||
investisseur_prenom: p.investisseur_prenom,
|
||||
mois: moisMap[`${p.id}_${p.investisseur_id}`] ?? {},
|
||||
}));
|
||||
|
||||
// Données 2042 C PRO — régime micro-BNC (case 5HQ) : les recettes brutes sont déclarées
|
||||
// telles quelles, l'administration applique automatiquement l'abattement forfaitaire de 34 %
|
||||
// pour frais (CGI art. 102 ter). Au-delà du seuil, le régime réel (cases 5KO/5KP) s'applique —
|
||||
// affiché ici à titre indicatif seulement, l'app ne calcule pas le régime réel (charges réelles
|
||||
// non suivies dans l'app).
|
||||
const SEUIL_MICRO_BNC = 77700;
|
||||
const ABATTEMENT_MICRO_BNC = 0.34;
|
||||
const totalBrut = round2(plateformes.reduce(
|
||||
(s, p) => s + Object.values(p.mois).reduce((a, b) => a + b, 0), 0
|
||||
));
|
||||
const case_5HQ = Math.round(totalBrut);
|
||||
const montantImposable = Math.round(case_5HQ * (1 - ABATTEMENT_MICRO_BNC));
|
||||
|
||||
return {
|
||||
annee,
|
||||
plateformes,
|
||||
totalBrut,
|
||||
donnees2042cpro: {
|
||||
case_5HQ,
|
||||
abattement_taux: ABATTEMENT_MICRO_BNC,
|
||||
montant_imposable: montantImposable,
|
||||
seuil_micro_bnc: SEUIL_MICRO_BNC,
|
||||
depasse_seuil_micro: case_5HQ > SEUIL_MICRO_BNC,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/* ── 2042 C PRO — Micro-BNC (case 5HQ) — plateformes PSAN (staking / rewards crypto) ──
|
||||
Contrairement au 2047/2778-SD, ce n'est pas une déclaration de revenus de source étrangère :
|
||||
les recettes crypto sont des BNC quelle que soit la localisation de la plateforme, reportées
|
||||
directement en 2042-C PRO sans passer par le 2047 (pas de crédit d'impôt conventionnel). ── */
|
||||
router.get('/2042cpro', (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 invCondBonus2042cpro = scopeAll
|
||||
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'r.bonus_investisseur_id = ?';
|
||||
const invArg = scopeAll ? req.user.id : req.investisseur.id;
|
||||
res.json(build2042CPro(annee, invCond, invCondBonus2042cpro, invArg));
|
||||
});
|
||||
|
||||
router.get('/2047', (req, res) => {
|
||||
const annee = req.query.annee || String(new Date().getFullYear() - 1);
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
@@ -806,5 +983,5 @@ router.get('/2047', (req, res) => {
|
||||
res.json(build2047(annee, invCond, invCondBonus2047, invArg));
|
||||
});
|
||||
|
||||
export { buildCerfa2561, build2778, build2047 };
|
||||
export { buildCerfa2561, build2778, build2047, build2042CPro };
|
||||
export default router;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
import { buildCerfa2561, build2778, build2047 } from '../taxreport.js';
|
||||
import { buildCerfa2561, build2778, build2047, build2042CPro } from '../taxreport.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -21,12 +21,22 @@ const sumBy = (arr, key) => arr.reduce((s, x) => s + (x[key] ?? 0), 0);
|
||||
* `synthese_2042` reprend les cases 2TT / 2TR / 2BH / 2CK / 2TY telles qu'affichées
|
||||
* en haut de la page Fiscalité. `cerfa2561` détaille le récapitulatif IFU des
|
||||
* plateformes françaises soumises au PFU. `cerfa2047` (présent seulement si
|
||||
* `has_etranger`) détaille, par plateforme étrangère, l'éligibilité au crédit
|
||||
* d'impôt conventionnel (cases 231 à 238 vs 250) et le report vers 8VL/8PL.
|
||||
* `has_etranger`) détaille, par plateforme étrangère "Prestataire de Services de
|
||||
* Financement Participatif" (PSFP), l'éligibilité au crédit d'impôt conventionnel
|
||||
* (cases 231 à 238 vs 250) et le report vers 8VL/8PL.
|
||||
* `cerfa2778_mensuel` (présent seulement si `has_etranger`) donne le détail mensuel
|
||||
* des intérêts bruts par plateforme étrangère, base de calcul du prélèvement
|
||||
* des intérêts bruts par plateforme étrangère PSFP, base de calcul du prélèvement
|
||||
* forfaitaire obligatoire (PFO) — le PFO n'est légalement dû que si `pfo_assujetti`
|
||||
* est vrai (seuil de revenu fiscal de référence, cf. Paramétrage → Ma fiscalité).
|
||||
* Les plateformes "Prestataire de Services sur Actifs Numériques" (PSAN) ne sont
|
||||
* jamais concernées par les CERFA 2047/2778-SD, même si domiciliées hors de France.
|
||||
*
|
||||
* `cerfa2042cpro` (présent seulement si `has_psan`) donne les recettes brutes crypto
|
||||
* (staking/rewards) des plateformes PSAN, quelle que soit leur domiciliation — imposables
|
||||
* en BNC, régime micro par défaut (case 5HQ, abattement forfaitaire de 34 % appliqué
|
||||
* automatiquement par l'administration ; au-delà de 77 700 €, le régime réel 5KO/5KP
|
||||
* s'applique, non calculé ici). Ce n'est pas un revenu de source étrangère : aucun crédit
|
||||
* d'impôt, aucun report vers `synthese_2042`.
|
||||
*
|
||||
* Limite connue : `case_2BH` / `case_2CK` de `synthese_2042` supposent qu'aucune
|
||||
* plateforme étrangère n'a été exclue manuellement du calcul PFO dans l'application
|
||||
@@ -67,14 +77,25 @@ router.get('/', (req, res) => {
|
||||
).get(req.userId);
|
||||
const pfoAssujetti = prefRow ? prefRow.value === 'true' : false;
|
||||
|
||||
// Au moins une plateforme domiciliée hors de France, tous détenteurs du foyer confondus
|
||||
// et indépendamment de l'année — la table `plateformes` est scopée par `user_id` (pas
|
||||
// `investisseur_id`, qui n'est que le détenteur assigné) dans toute l'application (cf.
|
||||
// GET /api/plateformes), et hasEtranger côté frontend (TaxReport.jsx) est calculé sur
|
||||
// cette même liste non filtrée par investisseur actif — un comportement volontairement
|
||||
// répliqué ici, même avec une clé API mono-investisseur.
|
||||
// Au moins une plateforme domiciliée hors de France ET de catégorie "Prestataire de
|
||||
// Services de Financement Participatif" (PSFP), tous détenteurs du foyer confondus et
|
||||
// indépendamment de l'année — les plateformes "Prestataire de Services sur Actifs
|
||||
// Numériques" (PSAN) ne sont pas concernées par les CERFA 2047/2778-SD (demande Olivier
|
||||
// 30/08/26), même si elles sont domiciliées hors de France. La table `plateformes` est
|
||||
// scopée par `user_id` (pas `investisseur_id`, qui n'est que le détenteur assigné) dans
|
||||
// toute l'application (cf. GET /api/plateformes), et hasEtranger côté frontend
|
||||
// (TaxReport.jsx) est calculé sur cette même liste non filtrée par investisseur actif —
|
||||
// un comportement volontairement répliqué ici, même avec une clé API mono-investisseur.
|
||||
const hasEtranger = !!db.prepare(
|
||||
`SELECT 1 FROM plateformes WHERE user_id = ? AND domiciliation != 'FR' LIMIT 1`
|
||||
`SELECT 1 FROM plateformes WHERE user_id = ? AND domiciliation != 'FR' AND categorie_plateforme = 'psfp' LIMIT 1`
|
||||
).get(req.userId);
|
||||
|
||||
// Au moins une plateforme "Prestataire de Services sur Actifs Numériques" (PSAN), quelle que
|
||||
// soit sa domiciliation — les recettes crypto (staking/rewards) sont des BNC de par leur nature,
|
||||
// pas de par la localisation de la plateforme (contrairement à hasEtranger ci-dessus). Même
|
||||
// principe de réplication du calcul frontend (TaxReport.jsx) qu'hasEtranger.
|
||||
const hasPsan = !!db.prepare(
|
||||
`SELECT 1 FROM plateformes WHERE user_id = ? AND categorie_plateforme = 'psan' LIMIT 1`
|
||||
).get(req.userId);
|
||||
|
||||
const cerfa2561 = buildCerfa2561(annee, invCond, invCondBonus, invParam);
|
||||
@@ -86,6 +107,11 @@ router.get('/', (req, res) => {
|
||||
cerfa2778_mensuel = build2778(annee, invCond, invCondBonus, invParam);
|
||||
}
|
||||
|
||||
let cerfa2042cpro = null;
|
||||
if (hasPsan) {
|
||||
cerfa2042cpro = build2042CPro(annee, invCond, invCondBonus, invParam);
|
||||
}
|
||||
|
||||
// ── Synthèse 2042 — mêmes cases et mêmes formules que le pavé "Cases fiscales 2042 —
|
||||
// synthèse" en haut de la page Fiscalité (TaxReport.jsx, fonction load()) :
|
||||
// - 2TT : cumul direct des lignes 2561 françaises. 2TY (pertes en capital,
|
||||
@@ -115,11 +141,13 @@ router.get('/', (req, res) => {
|
||||
res.json({
|
||||
annee,
|
||||
has_etranger: hasEtranger,
|
||||
has_psan: hasPsan,
|
||||
pfo_assujetti: pfoAssujetti,
|
||||
synthese_2042,
|
||||
cerfa2561,
|
||||
cerfa2047,
|
||||
cerfa2778_mensuel,
|
||||
cerfa2042cpro,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -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,7 +369,9 @@ export default function TaxReport() {
|
||||
<div className="card text-muted">Chargement…</div>
|
||||
) : (
|
||||
<>
|
||||
{!detailExpanded && !cerfaExpanded && data2042 && <div className="card">
|
||||
{!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>}
|
||||
@@ -378,7 +383,21 @@ export default function TaxReport() {
|
||||
<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>}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{!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,
|
||||
|
||||
Reference in new issue
Block a user