Refonte export PDF, colonne commentaire et UX résumés
- Export PDF: nouveau nommage AAAAMM/AAAA_Trajets_MATRICULE_PLAQUE.pdf, logo carré embarqué en en-tête, km formatés selon Precision_Distance, restructuration en-têtes (Domicile déplacé, période/année fusionnées au titre) - Correction bug: le nom de fichier suggéré au téléchargement ne remontait pas au frontend (CORS WithExposedHeaders manquant + composants résumé ne lisaient pas Content-Disposition) - Résumé mensuel: colonne Commentaire, sélecteur temporel unifié (auto-chargement), km selon précision - Résumé annuel: bouton Export PDF repositionné, km selon précision - Panneau totaux: renommage "Remboursement indicatif" + infobulles d'aide (icône) au lieu du texte permanent - Ajout de PdfServiceIntegrationTests (3 tests), 65/65 tests backend OK - Mise à jour requirements.md et design.md en conséquence
This commit is contained in:
@@ -29,8 +29,12 @@ public class PdfService : IPdfService
|
||||
private readonly IBaremeService _baremeService;
|
||||
private readonly IMontantRegleService _montantRegleService;
|
||||
private readonly IRemboursementService _remboursementService;
|
||||
private readonly IPrecisionDistanceService _precisionDistanceService;
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
/// <summary>Logo carré, chargé une seule fois depuis la ressource embarquée (Assets/logo-carre.png).</summary>
|
||||
private static readonly byte[] LogoCarre = ChargerLogoCarre();
|
||||
|
||||
public PdfService(
|
||||
IResumeService resumeService,
|
||||
IProfilService profilService,
|
||||
@@ -38,6 +42,7 @@ public class PdfService : IPdfService
|
||||
IBaremeService baremeService,
|
||||
IMontantRegleService montantRegleService,
|
||||
IRemboursementService remboursementService,
|
||||
IPrecisionDistanceService precisionDistanceService,
|
||||
AppDbContext context)
|
||||
{
|
||||
_resumeService = resumeService;
|
||||
@@ -46,9 +51,32 @@ public class PdfService : IPdfService
|
||||
_baremeService = baremeService;
|
||||
_montantRegleService = montantRegleService;
|
||||
_remboursementService = remboursementService;
|
||||
_precisionDistanceService = precisionDistanceService;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>Charge les octets du logo carré depuis la ressource embarquée de l'assembly.</summary>
|
||||
private static byte[] ChargerLogoCarre()
|
||||
{
|
||||
var assembly = typeof(PdfService).Assembly;
|
||||
var nomRessource = assembly.GetManifestResourceNames()
|
||||
.FirstOrDefault(n => n.EndsWith("logo-carre.png", StringComparison.OrdinalIgnoreCase));
|
||||
if (nomRessource is null)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
using var flux = assembly.GetManifestResourceStream(nomRessource);
|
||||
if (flux is null)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
using var memoire = new MemoryStream();
|
||||
flux.CopyTo(memoire);
|
||||
return memoire.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PdfResult> GenererRapportMensuelAsync(
|
||||
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default)
|
||||
@@ -62,9 +90,10 @@ public class PdfService : IPdfService
|
||||
return await ExecuterAvecTimeoutAsync(cancellationToken, async token =>
|
||||
{
|
||||
var modele = await AssemblerModeleMensuelAsync(annee, mois, vehiculeId, token);
|
||||
var octets = RendreMensuel(modele, token);
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(token);
|
||||
var octets = RendreMensuel(modele, precision, token);
|
||||
|
||||
var nomFichier = $"trajets_{mois:D2}_{annee:D4}.pdf";
|
||||
var nomFichier = ConstruireNomFichier($"{annee:D4}{mois:D2}", modele.Profil.Matricule, modele.Vehicule.Plaque);
|
||||
return new PdfResult(octets, nomFichier);
|
||||
});
|
||||
}
|
||||
@@ -76,9 +105,11 @@ public class PdfService : IPdfService
|
||||
return await ExecuterAvecTimeoutAsync(cancellationToken, async token =>
|
||||
{
|
||||
var modele = await AssemblerModeleAnnuelAsync(annee, vehiculeId, token);
|
||||
var octets = RendreAnnuel(modele, token);
|
||||
var precision = await _precisionDistanceService.ObtenirAsync(token);
|
||||
var octets = RendreAnnuel(modele, precision, token);
|
||||
|
||||
var nomFichier = $"trajets_{annee:D4}.pdf";
|
||||
var nomFichier = ConstruireNomFichier(
|
||||
$"{annee:D4}", modele.Profil.Matricule, modele.Regularisation.Plaque);
|
||||
return new PdfResult(octets, nomFichier);
|
||||
});
|
||||
}
|
||||
@@ -274,7 +305,7 @@ public class PdfService : IPdfService
|
||||
/// <summary>
|
||||
/// Rend le document mensuel en octets PDF selon la maquette « PDF Mensuel » du design.
|
||||
/// </summary>
|
||||
private static byte[] RendreMensuel(ModeleRapportMensuel modele, CancellationToken cancellationToken)
|
||||
private static byte[] RendreMensuel(ModeleRapportMensuel modele, int precision, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -286,8 +317,8 @@ public class PdfService : IPdfService
|
||||
page.Margin(30);
|
||||
page.DefaultTextStyle(x => x.FontSize(9).FontColor(Colors.Black));
|
||||
|
||||
page.Header().Element(c => ComposerEntete(c, modele));
|
||||
page.Content().PaddingTop(10).Element(c => ComposerTableTrajets(c, modele));
|
||||
page.Header().Element(c => ComposerEntete(c, modele, precision));
|
||||
page.Content().PaddingTop(10).Element(c => ComposerTableTrajets(c, modele, precision));
|
||||
page.Footer().AlignCenter().Text(txt =>
|
||||
{
|
||||
txt.Span("Page ");
|
||||
@@ -306,86 +337,90 @@ public class PdfService : IPdfService
|
||||
/// Compose l'en-tête obligatoire (Exigences 7.6, 7.8, 7.10) : titre fixe, identité, véhicule,
|
||||
/// période, adresse de départ, totaux mois/année, TOTAL IK et Montant_Regle.
|
||||
/// </summary>
|
||||
private static void ComposerEntete(IContainer container, ModeleRapportMensuel modele)
|
||||
private static void ComposerEntete(IContainer container, ModeleRapportMensuel modele, int precision)
|
||||
{
|
||||
var periode = $"{NomMois(modele.Mois).ToUpper(FrCulture)} {modele.Annee}";
|
||||
var identite = FormatIdentite(modele.Profil);
|
||||
var matricule = string.IsNullOrWhiteSpace(modele.Profil.Matricule)
|
||||
? "—"
|
||||
: modele.Profil.Matricule!;
|
||||
var adresseDepart = string.IsNullOrWhiteSpace(modele.Profil.AdresseDomicile)
|
||||
var domicile = string.IsNullOrWhiteSpace(modele.Profil.AdresseDomicile)
|
||||
? "—"
|
||||
: modele.Profil.AdresseDomicile!;
|
||||
var vehiculeLibelle =
|
||||
$"{modele.Vehicule.Plaque} — {modele.Vehicule.PuissanceFiscale} CV " +
|
||||
$"(Coef. : {FormatDecimal(modele.Coefficient, 3)} / Base : {FormatDecimal(modele.Base, 0)})";
|
||||
|
||||
container.Column(col =>
|
||||
container.Row(ligne =>
|
||||
{
|
||||
col.Spacing(2);
|
||||
|
||||
// Titre fixe imposé par l'Exigence 7.6.
|
||||
col.Item().Text("Rapport de trajets").FontSize(16).Bold();
|
||||
col.Item().Text("Feuille d'indemnité kilométrique mensuelle").FontSize(11).SemiBold();
|
||||
|
||||
// Identité (nom, prénom, matricule) — Exigence 7.8.
|
||||
col.Item().PaddingTop(4).Text(txt =>
|
||||
// Logo carré en haut à gauche.
|
||||
if (LogoCarre.Length > 0)
|
||||
{
|
||||
txt.Span("Utilisateur : ").SemiBold();
|
||||
txt.Span($"{identite} — Matricule : {matricule}");
|
||||
});
|
||||
ligne.ConstantItem(45).Image(LogoCarre).FitWidth();
|
||||
}
|
||||
|
||||
// Véhicule (plaque, puissance fiscale, coefficient/base de la tranche annuelle) — Exigences 7.8/7.10.
|
||||
col.Item().Text(txt =>
|
||||
ligne.RelativeItem().PaddingLeft(LogoCarre.Length > 0 ? 10 : 0).Column(col =>
|
||||
{
|
||||
txt.Span("Véhicule : ").SemiBold();
|
||||
txt.Span(vehiculeLibelle);
|
||||
});
|
||||
col.Spacing(2);
|
||||
|
||||
// Période et adresse de départ du profil — Exigences 7.6/7.10.
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Période : ").SemiBold();
|
||||
txt.Span(periode);
|
||||
});
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Adresse de départ : ").SemiBold();
|
||||
txt.Span(adresseDepart);
|
||||
});
|
||||
// Titre fixe imposé par l'Exigence 7.6, période intégrée (remplace la ligne « Période »).
|
||||
col.Item().Text("Rapport de trajets").FontSize(16).Bold();
|
||||
col.Item().Text($"Feuille d'indemnité kilométrique {periode}").FontSize(11).SemiBold();
|
||||
|
||||
// Totaux : mois, année cumulée, TOTAL IK du mois, Montant_Regle — Exigences 7.6/7.10.
|
||||
col.Item().PaddingTop(6).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Column(c =>
|
||||
// Identité (nom, prénom, matricule) — Exigence 7.8.
|
||||
col.Item().PaddingTop(4).Text(txt =>
|
||||
{
|
||||
c.Item().Text(txt =>
|
||||
txt.Span("Utilisateur : ").SemiBold();
|
||||
txt.Span($"{identite} — Matricule : {matricule}");
|
||||
});
|
||||
|
||||
// Domicile, juste après l'identité (même position que sur le rapport annuel).
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Domicile : ").SemiBold();
|
||||
txt.Span(domicile);
|
||||
});
|
||||
|
||||
// Véhicule (plaque, puissance fiscale, coefficient/base de la tranche annuelle) — Exigences 7.8/7.10.
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Véhicule : ").SemiBold();
|
||||
txt.Span(vehiculeLibelle);
|
||||
});
|
||||
|
||||
// Totaux : mois, année cumulée, TOTAL IK du mois, Montant_Regle — Exigences 7.6/7.10.
|
||||
col.Item().PaddingTop(6).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Column(c =>
|
||||
{
|
||||
txt.Span("Total parcouru sur le mois : ").SemiBold();
|
||||
txt.Span($"{FormatKm(modele.TotalKmMois)} km");
|
||||
c.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Total parcouru sur le mois : ").SemiBold();
|
||||
txt.Span($"{FormatKm(modele.TotalKmMois, precision)} km");
|
||||
});
|
||||
c.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Total parcouru sur l'année : ").SemiBold();
|
||||
txt.Span($"{FormatKm(modele.TotalKmAnnuel, precision)} km");
|
||||
});
|
||||
});
|
||||
c.Item().Text(txt =>
|
||||
row.RelativeItem().Column(c =>
|
||||
{
|
||||
txt.Span("Total parcouru sur l'année : ").SemiBold();
|
||||
txt.Span($"{FormatKm(modele.TotalKmAnnuel)} km");
|
||||
c.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("TOTAL IK du mois : ").SemiBold();
|
||||
txt.Span($"{FormatMontant(modele.TotalIkMois)} €");
|
||||
});
|
||||
c.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Réglé : ").SemiBold();
|
||||
txt.Span($"{FormatMontant(modele.MontantRegle)} €");
|
||||
});
|
||||
});
|
||||
});
|
||||
row.RelativeItem().Column(c =>
|
||||
{
|
||||
c.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("TOTAL IK du mois : ").SemiBold();
|
||||
txt.Span($"{FormatMontant(modele.TotalIkMois)} €");
|
||||
});
|
||||
c.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Réglé : ").SemiBold();
|
||||
txt.Span($"{FormatMontant(modele.MontantRegle)} €");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -393,7 +428,7 @@ public class PdfService : IPdfService
|
||||
/// Compose la table des trajets du mois (Exigence 7.11) : date, motif (commentaire),
|
||||
/// adresse de départ → arrivée, kilomètres.
|
||||
/// </summary>
|
||||
private static void ComposerTableTrajets(IContainer container, ModeleRapportMensuel modele)
|
||||
private static void ComposerTableTrajets(IContainer container, ModeleRapportMensuel modele, int precision)
|
||||
{
|
||||
container.Table(table =>
|
||||
{
|
||||
@@ -433,12 +468,12 @@ public class PdfService : IPdfService
|
||||
table.Cell().Element(Cellule).Text(trajet.Date.ToString("dd/MM/yyyy", FrCulture));
|
||||
table.Cell().Element(Cellule).Text(motif);
|
||||
table.Cell().Element(Cellule).Text(itineraire);
|
||||
table.Cell().Element(Cellule).AlignRight().Text(FormatKm(km));
|
||||
table.Cell().Element(Cellule).AlignRight().Text(FormatKm(km, precision));
|
||||
}
|
||||
|
||||
// Ligne de total du mois.
|
||||
table.Cell().ColumnSpan(3).Element(CelluleTotal).AlignRight().Text("Total du mois :").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatKm(modele.TotalKmMois)}").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatKm(modele.TotalKmMois, precision)}").SemiBold();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -456,7 +491,7 @@ public class PdfService : IPdfService
|
||||
/// <summary>
|
||||
/// Rend le document de synthèse annuelle en octets PDF selon la maquette « PDF Annuel » du design.
|
||||
/// </summary>
|
||||
private static byte[] RendreAnnuel(ModeleRapportAnnuel modele, CancellationToken cancellationToken)
|
||||
private static byte[] RendreAnnuel(ModeleRapportAnnuel modele, int precision, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -468,13 +503,13 @@ public class PdfService : IPdfService
|
||||
page.Margin(30);
|
||||
page.DefaultTextStyle(x => x.FontSize(9).FontColor(Colors.Black));
|
||||
|
||||
page.Header().Element(c => ComposerEnteteAnnuel(c, modele));
|
||||
page.Header().Element(c => ComposerEnteteAnnuel(c, modele, precision));
|
||||
page.Content().PaddingTop(10).Column(col =>
|
||||
{
|
||||
col.Spacing(10);
|
||||
col.Item().Element(c => ComposerTableBaremeAnnuel(c, modele));
|
||||
col.Item().Element(c => ComposerTableSyntheseAnnuelle(c, modele));
|
||||
col.Item().Element(c => ComposerTotauxAnnuels(c, modele));
|
||||
col.Item().Element(c => ComposerTableSyntheseAnnuelle(c, modele, precision));
|
||||
col.Item().Element(c => ComposerTotauxAnnuels(c, modele, precision));
|
||||
});
|
||||
page.Footer().AlignCenter().Text(txt =>
|
||||
{
|
||||
@@ -494,7 +529,7 @@ public class PdfService : IPdfService
|
||||
/// Compose l'en-tête de la synthèse annuelle (Exigences 7.6, 7.7, 7.8, 7.12) : titre fixe,
|
||||
/// identité, année, domicile, véhicule (plaque, puissance fiscale) et tranche annuelle applicable.
|
||||
/// </summary>
|
||||
private static void ComposerEnteteAnnuel(IContainer container, ModeleRapportAnnuel modele)
|
||||
private static void ComposerEnteteAnnuel(IContainer container, ModeleRapportAnnuel modele, int precision)
|
||||
{
|
||||
var reg = modele.Regularisation;
|
||||
var identite = FormatIdentite(modele.Profil);
|
||||
@@ -505,55 +540,58 @@ public class PdfService : IPdfService
|
||||
? "—"
|
||||
: modele.Profil.AdresseDomicile!;
|
||||
|
||||
container.Column(col =>
|
||||
container.Row(ligne =>
|
||||
{
|
||||
col.Spacing(2);
|
||||
|
||||
// Titre fixe imposé par l'Exigence 7.6.
|
||||
col.Item().Text("Rapport de trajets").FontSize(16).Bold();
|
||||
col.Item().Text($"Synthèse {modele.Annee} d'indemnité kilométrique").FontSize(11).SemiBold();
|
||||
|
||||
// Identité (nom, prénom, matricule) — Exigence 7.8.
|
||||
col.Item().PaddingTop(4).Text(txt =>
|
||||
// Logo carré en haut à gauche.
|
||||
if (LogoCarre.Length > 0)
|
||||
{
|
||||
txt.Span("Utilisateur : ").SemiBold();
|
||||
txt.Span($"{identite} — Matricule : {matricule}");
|
||||
});
|
||||
ligne.ConstantItem(45).Image(LogoCarre).FitWidth();
|
||||
}
|
||||
|
||||
// Année (période couverte) — Exigence 7.6.
|
||||
col.Item().Text(txt =>
|
||||
ligne.RelativeItem().PaddingLeft(LogoCarre.Length > 0 ? 10 : 0).Column(col =>
|
||||
{
|
||||
txt.Span("Année : ").SemiBold();
|
||||
txt.Span(modele.Annee.ToString(CultureInfo.InvariantCulture));
|
||||
});
|
||||
col.Spacing(2);
|
||||
|
||||
// Adresse de domicile — Exigence 7.12.
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Domicile : ").SemiBold();
|
||||
txt.Span(domicile);
|
||||
});
|
||||
// Titre fixe imposé par l'Exigence 7.6.
|
||||
col.Item().Text("Rapport de trajets").FontSize(16).Bold();
|
||||
col.Item().Text($"Synthèse {modele.Annee} d'indemnité kilométrique").FontSize(11).SemiBold();
|
||||
|
||||
// Véhicule (plaque, puissance fiscale) et tranche annuelle applicable — Exigences 7.7/7.8/7.12.
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Véhicule : ").SemiBold();
|
||||
txt.Span($"{reg.Plaque} — {reg.PuissanceFiscale} CV");
|
||||
});
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Barème applicable : ").SemiBold();
|
||||
txt.Span(reg.TrancheLibelle);
|
||||
});
|
||||
// Identité (nom, prénom, matricule) — Exigence 7.8.
|
||||
col.Item().PaddingTop(4).Text(txt =>
|
||||
{
|
||||
txt.Span("Utilisateur : ").SemiBold();
|
||||
txt.Span($"{identite} — Matricule : {matricule}");
|
||||
});
|
||||
|
||||
// Total kilométrique de la période (Exigence 7.6).
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Total kilométrique de l'année : ").SemiBold();
|
||||
txt.Span($"{FormatKm(reg.TotalKmAnnuel)} km");
|
||||
});
|
||||
// Adresse de domicile — Exigence 7.12. (La ligne « Année » a été retirée : la
|
||||
// période figure déjà dans le titre « Synthèse {Année} d'indemnité kilométrique ».)
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Domicile : ").SemiBold();
|
||||
txt.Span(domicile);
|
||||
});
|
||||
|
||||
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
// Véhicule (plaque, puissance fiscale) et tranche annuelle applicable — Exigences 7.7/7.8/7.12.
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Véhicule : ").SemiBold();
|
||||
txt.Span($"{reg.Plaque} — {reg.PuissanceFiscale} CV");
|
||||
});
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Barème applicable : ").SemiBold();
|
||||
txt.Span(reg.TrancheLibelle);
|
||||
});
|
||||
|
||||
// Total kilométrique de la période (Exigence 7.6).
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Total kilométrique de l'année : ").SemiBold();
|
||||
txt.Span($"{FormatKm(reg.TotalKmAnnuel, precision)} km");
|
||||
});
|
||||
|
||||
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -618,7 +656,7 @@ public class PdfService : IPdfService
|
||||
/// Compose la table des douze mois (kilomètres parcourus, frais payé, frais réel) — Exigence 7.12.
|
||||
/// Les données proviennent de <see cref="RegularisationAnnuelleDto.Mois"/>.
|
||||
/// </summary>
|
||||
private static void ComposerTableSyntheseAnnuelle(IContainer container, ModeleRapportAnnuel modele)
|
||||
private static void ComposerTableSyntheseAnnuelle(IContainer container, ModeleRapportAnnuel modele, int precision)
|
||||
{
|
||||
var reg = modele.Regularisation;
|
||||
|
||||
@@ -643,14 +681,14 @@ public class PdfService : IPdfService
|
||||
foreach (var ligne in reg.Mois)
|
||||
{
|
||||
table.Cell().Element(Cellule).Text(NomMois(ligne.Mois));
|
||||
table.Cell().Element(Cellule).AlignRight().Text($"{FormatKm(ligne.KmMois)}");
|
||||
table.Cell().Element(Cellule).AlignRight().Text($"{FormatKm(ligne.KmMois, precision)}");
|
||||
table.Cell().Element(Cellule).AlignRight().Text($"{FormatMontant(ligne.FraisPaye)} €");
|
||||
table.Cell().Element(Cellule).AlignRight().Text($"{FormatMontant(ligne.FraisReelMensuel)} €");
|
||||
}
|
||||
|
||||
// Ligne de total annuel.
|
||||
table.Cell().Element(CelluleTotal).Text("TOTAL").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatKm(reg.TotalKmAnnuel)}").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatKm(reg.TotalKmAnnuel, precision)}").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatMontant(reg.TotalPaye)} €").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatMontant(reg.MontantReel)} €").SemiBold();
|
||||
});
|
||||
@@ -669,13 +707,13 @@ public class PdfService : IPdfService
|
||||
/// <summary>
|
||||
/// Compose le bloc « formule réelle » et la régularisation (réel − payé) — Exigence 7.12.
|
||||
/// </summary>
|
||||
private static void ComposerTotauxAnnuels(IContainer container, ModeleRapportAnnuel modele)
|
||||
private static void ComposerTotauxAnnuels(IContainer container, ModeleRapportAnnuel modele, int precision)
|
||||
{
|
||||
var reg = modele.Regularisation;
|
||||
|
||||
// Formule réelle : total_km × coefficient + base = Montant_Reel (tranche annuelle).
|
||||
var formule =
|
||||
$"{FormatKm(reg.TotalKmAnnuel)} km × {FormatDecimal(reg.CoefficientTranche, 3)} " +
|
||||
$"{FormatKm(reg.TotalKmAnnuel, precision)} km × {FormatDecimal(reg.CoefficientTranche, 3)} " +
|
||||
$"+ {FormatDecimal(reg.BaseTranche, 0)} = {FormatMontant(reg.MontantReel)} € " +
|
||||
$"(tranche {reg.TrancheLibelle}, {reg.PuissanceFiscale} CV)";
|
||||
|
||||
@@ -703,9 +741,37 @@ public class PdfService : IPdfService
|
||||
return identite.Length > 0 ? identite : "—";
|
||||
}
|
||||
|
||||
/// <summary>Kilomètres : séparateur de milliers, 2 décimales.</summary>
|
||||
private static string FormatKm(decimal km) =>
|
||||
Math.Round(km, 2, MidpointRounding.AwayFromZero).ToString("N2", FrCulture);
|
||||
/// <summary>
|
||||
/// Kilomètres : séparateur de milliers, nombre de décimales selon Precision_Distance
|
||||
/// (0, 1 ou 2 — voir la spec precision-distances-km).
|
||||
/// </summary>
|
||||
private static string FormatKm(decimal km, int precision) =>
|
||||
Math.Round(km, precision, MidpointRounding.AwayFromZero).ToString("N" + precision, FrCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Construit le nom de fichier d'export au format `AAAAMM_Trajets_MATRICULE_PLAQUE.pdf`
|
||||
/// (sans le segment mois pour l'export annuel, `periode` valant alors `AAAA`). Les segments
|
||||
/// Matricule et Plaque sont omis (avec leur séparateur) lorsqu'ils sont vides.
|
||||
/// </summary>
|
||||
private static string ConstruireNomFichier(string periode, string? matricule, string? plaque)
|
||||
{
|
||||
var segments = new List<string> { periode, "Trajets" };
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(matricule))
|
||||
{
|
||||
segments.Add(NettoyerSegmentNomFichier(matricule));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(plaque))
|
||||
{
|
||||
segments.Add(NettoyerSegmentNomFichier(plaque));
|
||||
}
|
||||
|
||||
return string.Join("_", segments) + ".pdf";
|
||||
}
|
||||
|
||||
/// <summary>Retire les espaces et tirets d'un segment de nom de fichier (ex. plaque « AB-123-CD » → « AB123CD »).</summary>
|
||||
private static string NettoyerSegmentNomFichier(string valeur) =>
|
||||
valeur.Replace(" ", "").Replace("-", "").Trim();
|
||||
|
||||
/// <summary>Montant : séparateur de milliers, 2 décimales.</summary>
|
||||
private static string FormatMontant(decimal montant) =>
|
||||
|
||||
Reference in New Issue
Block a user