Amélioration de la tax report

This commit is contained in:
ocroguennec committed 2026-08-16 18:21:34 +02:00
1 parent 849e16de33
commit b43754fe19
2 files changed
+211 -6

No files matched your search

+210 -5
View File
@@ -6,6 +6,10 @@ const router = Router();
router.use(requireInvestisseur);
const round2 = v => Math.round((v ?? 0) * 100) / 100;
// Échappement CSV (séparateur ';') — remplace les caractères qui casseraient les colonnes.
// Bug préexistant : cette fonction était utilisée plus bas dans /export sans être définie
// (ReferenceError à l'exécution), corrigé au passage.
const esc = v => String(v ?? '').replace(/;/g, ',').replace(/[\r\n]+/g, ' ');
router.get('/', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
@@ -29,6 +33,32 @@ router.get('/', (req, res) => {
AND r.statut IN ('paye','partiel')
`).get(invArg, annee);
// Intérêts plateforme (type=interets_plateforme, pas d'investissement — rattachés via
// bonus_plateforme_id/bonus_investisseur_id) : mêmes règles fiscales que la plateforme
// associée, donc à additionner aux totaux comme des intérêts d'investissement classiques.
const invCondBonus = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const recapInteretsPlateforme = db.prepare(`
SELECT
COALESCE(SUM(r.interets_bruts),0) AS interets_bruts,
COALESCE(SUM(r.prelev_sociaux),0) AS prelev_sociaux,
COALESCE(SUM(r.prelev_forfaitaire),0) AS prelev_forfaitaire,
COALESCE(SUM(r.net_recu),0) AS net_recu,
COUNT(*) AS nb_remboursements
FROM remboursements r
WHERE ${invCondBonus}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
`).get(invArg, annee);
recap.interets_bruts += recapInteretsPlateforme.interets_bruts;
recap.prelev_sociaux += recapInteretsPlateforme.prelev_sociaux;
recap.prelev_forfaitaire += recapInteretsPlateforme.prelev_forfaitaire;
recap.net_recu += recapInteretsPlateforme.net_recu;
recap.nb_remboursements += recapInteretsPlateforme.nb_remboursements;
const pertes = db.prepare(`
SELECT i.id, i.nom_projet, p.nom AS plateforme_nom,
i.montant_investi,
@@ -125,6 +155,21 @@ router.get('/export', (req, res) => {
ORDER BY r.date_remb
`).all(invArg, annee);
const invCondBonusExp = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const interetsPlateformeRows = db.prepare(`
SELECT
'— Intérêts plateforme' || CASE WHEN NULLIF(r.notes, '') IS NOT NULL THEN ' — ' || r.notes ELSE '' END AS nom_projet,
p.nom AS plateforme,
r.date_remb, r.capital, r.interets_bruts,
r.prelev_sociaux, r.prelev_forfaitaire, r.net_recu, r.statut
FROM remboursements r
JOIN plateformes p ON p.id = r.bonus_plateforme_id
WHERE ${invCondBonusExp} AND substr(r.date_remb,1,4) = ? AND r.type = 'interets_plateforme'
ORDER BY r.date_remb
`).all(invArg, annee);
const corrCond2 = scopeAll
? 'c.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'c.investisseur_id = ?';
@@ -141,7 +186,7 @@ router.get('/export', (req, res) => {
`).all(invArg, annee);
const header = ['Projet','Plateforme','Date','Capital','Interets bruts','Prelev sociaux','Impot revenu','Net recu','Statut'];
const allRows = [...rows, ...corrRows];
const allRows = [...rows, ...interetsPlateformeRows, ...corrRows].sort((a, b) => (a.date_remb || '').localeCompare(b.date_remb || ''));
const csv = [
header.join(';'),
...allRows.map(r => [
@@ -190,6 +235,52 @@ router.get('/cerfa2561', (req, res) => {
ORDER BY p.nom, inv.nom
`).all(invArg, annee);
// Intérêts plateforme (pas d'investissement — rattachés via bonus_plateforme_id/
// bonus_investisseur_id) : fusionnés sur la même ligne plateforme × investisseur que
// les intérêts d'investissement classiques, car la fiscalité (domiciliation/fiscalite/
// type_produit_fiscal) dépend de la plateforme, pas de l'investissement précis.
const invCondBonus2561 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const rowsInteretsPlateforme = db.prepare(`
SELECT
p.id AS plateforme_id,
p.nom AS plateforme_nom,
p.domiciliation,
p.fiscalite,
p.type_produit_fiscal,
inv.id AS investisseur_id,
inv.nom AS investisseur_nom,
inv.prenom AS investisseur_prenom,
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.prelev_sociaux), 0) AS prelev_sociaux,
COALESCE(SUM(r.prelev_forfaitaire), 0) AS prelev_forfaitaire
FROM remboursements r
JOIN plateformes p ON p.id = r.bonus_plateforme_id
JOIN investisseurs inv ON inv.id = r.bonus_investisseur_id
WHERE ${invCondBonus2561}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
GROUP BY p.id, inv.id
`).all(invArg, annee);
// Fusion des deux sources sur la clé plateforme × investisseur
const rowsMap = {};
for (const r of rows) rowsMap[`${r.plateforme_id}_${r.investisseur_id}`] = { ...r };
for (const r of rowsInteretsPlateforme) {
const key = `${r.plateforme_id}_${r.investisseur_id}`;
if (rowsMap[key]) {
rowsMap[key].interets_bruts += r.interets_bruts;
rowsMap[key].prelev_sociaux += r.prelev_sociaux;
rowsMap[key].prelev_forfaitaire += r.prelev_forfaitaire;
} else {
rowsMap[key] = { ...r };
}
}
const rowsMerged = Object.values(rowsMap).sort((a, b) =>
a.plateforme_nom.localeCompare(b.plateforme_nom) || (a.investisseur_nom ?? '').localeCompare(b.investisseur_nom ?? ''));
// Pertes en capital par plateforme × investisseur
const pertesRows = db.prepare(`
SELECT
@@ -212,7 +303,7 @@ router.get('/cerfa2561', (req, res) => {
pertesMap[`${p.plateforme_id}_${p.investisseur_id}`] = Math.max(0, p.perte_capital);
}
const lignes = rows.map(r => {
const lignes = rowsMerged.map(r => {
const key = `${r.plateforme_id}_${r.investisseur_id}`;
const perteCapital = pertesMap[key] ?? 0;
const isFlatTax = r.domiciliation === 'FR' && r.fiscalite === 'flat_tax';
@@ -265,6 +356,23 @@ router.get('/cerfa2561', (req, res) => {
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(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.prelev_sociaux), 0) AS prelev_sociaux,
COALESCE(SUM(r.prelev_forfaitaire), 0) AS prelev_forfaitaire
FROM remboursements r
JOIN plateformes p ON p.id = r.bonus_plateforme_id
WHERE ${invCondBonus2561}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
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}`;
@@ -275,6 +383,16 @@ router.get('/cerfa2561', (req, res) => {
prelev_forfaitaire: round2(m.prelev_forfaitaire),
};
}
for (const m of moisRowsInteretsPlateforme) {
const key = `${m.plateforme_id}_${m.investisseur_id}`;
if (!moisMap[key]) moisMap[key] = {};
const existing = moisMap[key][m.mois];
moisMap[key][m.mois] = {
interets_bruts: round2((existing?.interets_bruts ?? 0) + m.interets_bruts),
prelev_sociaux: round2((existing?.prelev_sociaux ?? 0) + m.prelev_sociaux),
prelev_forfaitaire: round2((existing?.prelev_forfaitaire ?? 0) + m.prelev_forfaitaire),
};
}
const lignesWithMois = lignes.map(l => ({
...l,
@@ -318,7 +436,31 @@ router.get('/cerfa2561/remboursements', (req, res) => {
ORDER BY r.date_remb
`).all(invArg, ...platArgs, annee);
res.json(rows);
// Intérêts plateforme correspondant à la même plateforme (et investisseur si précisé)
const invCondBonusDetail = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const platCondBonus = investisseur_id
? 'AND p.id = ? AND r.bonus_investisseur_id = ?'
: 'AND p.id = ?';
const rowsInteretsPlateforme = db.prepare(`
SELECT
r.id, r.date_remb, r.capital, r.interets_bruts,
r.prelev_sociaux, r.prelev_forfaitaire, r.interets_nets, r.net_recu,
r.statut, r.notes,
'— Intérêts plateforme' || CASE WHEN NULLIF(r.notes, '') IS NOT NULL THEN ' — ' || r.notes ELSE '' END AS nom_projet
FROM remboursements r
JOIN plateformes p ON p.id = r.bonus_plateforme_id
WHERE ${invCondBonusDetail} ${platCondBonus}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
ORDER BY r.date_remb
`).all(invArg, ...platArgs, annee);
const allRows = [...rows, ...rowsInteretsPlateforme].sort((a, b) => (a.date_remb || '').localeCompare(b.date_remb || ''));
res.json(allRows);
});
@@ -346,7 +488,20 @@ router.get('/years', (req, res) => {
ORDER BY annee DESC
`).all(...invParams);
res.json(rows.map(r => r.annee));
const invWhereBonus = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const rowsInteretsPlateforme = db.prepare(`
SELECT DISTINCT strftime('%Y', r.date_remb) AS annee
FROM remboursements r
WHERE ${invWhereBonus}
AND r.type = 'interets_plateforme'
`).all(...invParams);
const years = [...new Set([...rows.map(r => r.annee), ...rowsInteretsPlateforme.map(r => r.annee)])]
.sort((a, b) => b.localeCompare(a));
res.json(years);
});
@@ -375,6 +530,30 @@ router.get('/2778', (req, res) => {
ORDER BY p.nom, inv.nom
`).all(invArg, annee);
// Intérêts plateforme (pas d'investissement) sur des plateformes étrangères — même
// logique de fusion que pour le 2561 : rattachés à la ligne plateforme × investisseur.
const invCondBonus2778 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
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 ${invCondBonus2778}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR'
`).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
const moisRows = db.prepare(`
SELECT
@@ -398,14 +577,40 @@ router.get('/2778', (req, res) => {
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 ${invCondBonus2778}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR'
GROUP BY p.id, 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 = platRows.map(p => ({
const plateformes = platRowsMerged.map(p => ({
id: `${p.id}_${p.investisseur_id}`,
plateforme_id: p.id,
nom: p.nom,
+1 -1
View File
@@ -385,7 +385,7 @@ export default function Cerfa2778Preview({ annee, activeView }) {
</div>
{/* Détail des plateformes incluses */}
{moisData.detail.length > 1 && (
{moisData.detail.length > 0 && (
<div style={{ marginBottom: 14, padding: '10px 12px', background: 'var(--surface-2)', borderRadius: 8 }}>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginBottom: 6, fontWeight: 500, textTransform: 'uppercase', letterSpacing: '.05em' }}>
Plateformes incluses