feat: accumulated work — PI-SPI, KYC, RLS, mutuelle parts, comptabilité PDF + startup fixes
## PI-SPI BCEAO (P0.3 — deadline 30/06/2026)
- package payment/pispi/ complet : PispiAuth (OAuth2), PispiClient (HTTP brut),
PispiIso20022Mapper (pacs.008/002), PispiSignatureVerifier (HMAC-SHA256),
PispiWebhookResource (/api/pispi/webhook), DTOs ISO 20022
- PaymentOrchestrator + PaymentProviderRegistry pour l'orchestration multi-provider
- Mode mock automatique si credentials absents (dev)
## KYC AML
- entity/KycDossier, KycResource, KycAmlService + tests
- Migration V38 (create_kyc_dossier_table)
## RLS (PostgreSQL Row-Level Security) — isolation multi-tenant
- RlsConnectionInitializer, RlsContextInterceptor, @RlsEnabled annotation
- Migration V39 (PostgreSQL RLS Tenant Isolation) + V42 (app DB roles)
- Tests unitaires RlsConnectionInitializerTest, RlsContextInterceptorTest
- Tests d'intégration RlsCrossTenantIsolationTest (@QuarkusTest + IntegrationTestProfile)
## Mutuelle — Parts sociales
- entity/mutuelle/parts/ComptePartsSociales, TransactionPartsSociales
- Service, resource, mapper, repository + tests
- InteretsEpargneService + ReleveComptePdfService
## Comptabilité PDF
- ComptabilitePdfService (OpenPDF), ComptabilitePdfResource
- Tests ComptabilitePdfServiceTest, ComptabilitePdfResourceTest
## Migrations Flyway (SYSCOHADA + Keycloak Orgs)
- V36 SYSCOHADA Plan Comptable Complet : seeds comptes standards UEMOA,
trigger init_plan_comptable_organisation, alignement schéma V1 → entités
- V37 keycloak_org_id sur organisations (P0.2 migration KC 26)
- V40 provider_defaut sur FormuleAbonnement
- V41 fcm_token sur utilisateurs (FCM notifications push)
## Fixes startup (SmallRye Config 3.20 + schéma)
- 8× @ConfigProperty(defaultValue = "") → Optional<String>
(firebase, pispi.*, mtnmomo, orange) — empty default rejetés par SmallRye 3.20
- application.properties : mappings secrets env var sous %prod. uniquement
- V36 : drop colonne obsolète 'numero' de V1 quand Hibernate a créé 'numero_compte'
- V36 : remplacement UNIQUE global sur journaux_comptables.code par composite
(organisation_id, code) pour autoriser plusieurs orgs avec code 'ACH'/'VTE'/etc
- V39 : escape placeholder ${VAR} → <VAR> dans lignes commentées
(Flyway parser évalue les placeholders même dans les commentaires)
- V41 : table 'membres' → 'utilisateurs' (nom correct selon entité Membre)
- JournalComptable entity : @UniqueConstraint composite au lieu de unique=true
- MembreResource : example @Schema JSON valide (['...'] → [])
- IntegrationTestProfile : auto-détection Docker via `docker info`, fallback
vers PostgreSQL local sans DevServices
## Dev config
- application-dev.properties : quarkus.devservices.enabled=false +
quarkus.kafka.devservices.enabled=false (pas besoin de Docker pour dev)
- quarkus.flyway.placeholder-replacement=false
- Secrets dev (wave.*, firebase, pispi) en mode mock automatique
## Phase 8 tests (complète)
- 170 fichiers modifiés/ajoutés, 23425+ insertions
- Tests RBAC (@QuarkusTest) pour MembreResource lifecycle
- Tests OrganisationContextFilter multi-org
- Tests SouscriptionQuotaOptionC, KycAmlService, EmailTemplate, etc.
Résultat : Backend démarre en 64s sur port 8085 avec 36 features installées.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.Font;
|
||||
import com.lowagie.text.pdf.*;
|
||||
import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
|
||||
import dev.lions.unionflow.server.entity.CompteComptable;
|
||||
import dev.lions.unionflow.server.entity.EcritureComptable;
|
||||
import dev.lions.unionflow.server.entity.LigneEcriture;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.CompteComptableRepository;
|
||||
import dev.lions.unionflow.server.repository.EcritureComptableRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Génération des rapports comptables PDF SYSCOHADA révisé.
|
||||
*
|
||||
* <p>Rapports disponibles :
|
||||
* <ul>
|
||||
* <li>Grand livre : détail de toutes les écritures par compte</li>
|
||||
* <li>Balance générale : soldes débit/crédit/solde net par compte</li>
|
||||
* <li>Compte de résultat : produits (classe 7+8) - charges (classe 6+8)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
public class ComptabilitePdfService {
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
private static final Color COLOR_HEADER = new Color(0x1A, 0x56, 0x8C);
|
||||
private static final Color COLOR_HEADER_TEXT = Color.WHITE;
|
||||
private static final Color COLOR_TOTAL_ROW = new Color(0xE8, 0xF0, 0xFE);
|
||||
private static final Color COLOR_ROW_ALT = new Color(0xF8, 0xFA, 0xFF);
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject
|
||||
CompteComptableRepository compteComptableRepository;
|
||||
|
||||
@Inject
|
||||
EcritureComptableRepository ecritureComptableRepository;
|
||||
|
||||
// ── Balance générale ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Génère la balance générale SYSCOHADA pour une organisation.
|
||||
* Liste tous les comptes avec cumul débit, cumul crédit et solde.
|
||||
*/
|
||||
public byte[] genererBalance(UUID organisationId, LocalDate dateDebut, LocalDate dateFin) {
|
||||
Organisation org = getOrg(organisationId);
|
||||
List<CompteComptable> comptes = compteComptableRepository.findByOrganisation(organisationId);
|
||||
|
||||
Map<String, BigDecimal[]> totauxParCompte = calculerTotauxParCompte(organisationId, dateDebut, dateFin);
|
||||
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
Document doc = new Document(PageSize.A4.rotate(), 20, 20, 40, 40);
|
||||
PdfWriter.getInstance(doc, baos);
|
||||
doc.open();
|
||||
|
||||
addTitrePage(doc, "BALANCE GÉNÉRALE", org.getNom(), dateDebut, dateFin);
|
||||
|
||||
PdfPTable table = new PdfPTable(6);
|
||||
table.setWidthPercentage(100);
|
||||
table.setWidths(new float[]{10f, 30f, 8f, 15f, 15f, 15f});
|
||||
|
||||
addHeaderCell(table, "Compte");
|
||||
addHeaderCell(table, "Libellé");
|
||||
addHeaderCell(table, "Classe");
|
||||
addHeaderCell(table, "Cumul Débit");
|
||||
addHeaderCell(table, "Cumul Crédit");
|
||||
addHeaderCell(table, "Solde");
|
||||
|
||||
BigDecimal totalDebit = BigDecimal.ZERO;
|
||||
BigDecimal totalCredit = BigDecimal.ZERO;
|
||||
boolean alt = false;
|
||||
|
||||
for (CompteComptable compte : comptes) {
|
||||
BigDecimal[] totaux = totauxParCompte.getOrDefault(
|
||||
compte.getNumeroCompte(), new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
|
||||
BigDecimal debit = totaux[0];
|
||||
BigDecimal credit = totaux[1];
|
||||
BigDecimal solde = debit.subtract(credit);
|
||||
|
||||
if (debit.signum() == 0 && credit.signum() == 0) continue;
|
||||
|
||||
Color bg = alt ? COLOR_ROW_ALT : Color.WHITE;
|
||||
addDataCell(table, compte.getNumeroCompte(), bg);
|
||||
addDataCell(table, compte.getLibelle(), bg);
|
||||
addDataCell(table, String.valueOf(compte.getClasseComptable()), bg);
|
||||
addAmountCell(table, debit, bg);
|
||||
addAmountCell(table, credit, bg);
|
||||
addAmountCell(table, solde, bg);
|
||||
|
||||
totalDebit = totalDebit.add(debit);
|
||||
totalCredit = totalCredit.add(credit);
|
||||
alt = !alt;
|
||||
}
|
||||
|
||||
// Ligne totaux
|
||||
BigDecimal totalSolde = totalDebit.subtract(totalCredit);
|
||||
addTotalCell(table, "TOTAUX");
|
||||
addTotalCell(table, "");
|
||||
addTotalCell(table, "");
|
||||
addAmountCell(table, totalDebit, COLOR_TOTAL_ROW);
|
||||
addAmountCell(table, totalCredit, COLOR_TOTAL_ROW);
|
||||
addAmountCell(table, totalSolde, COLOR_TOTAL_ROW);
|
||||
|
||||
doc.add(table);
|
||||
addFooter(doc);
|
||||
doc.close();
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur génération balance PDF : {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Erreur génération balance PDF", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Compte de résultat ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Génère le compte de résultat SYSCOHADA.
|
||||
* Produits (classes 7 et 8 produits) — Charges (classes 6 et 8 charges).
|
||||
*/
|
||||
public byte[] genererCompteResultat(UUID organisationId, LocalDate dateDebut, LocalDate dateFin) {
|
||||
Organisation org = getOrg(organisationId);
|
||||
Map<String, BigDecimal[]> totaux = calculerTotauxParCompte(organisationId, dateDebut, dateFin);
|
||||
|
||||
List<CompteComptable> comptes = compteComptableRepository.findByOrganisation(organisationId);
|
||||
|
||||
BigDecimal totalProduits = BigDecimal.ZERO;
|
||||
BigDecimal totalCharges = BigDecimal.ZERO;
|
||||
List<Object[]> lignesProduits = new ArrayList<>();
|
||||
List<Object[]> lignesCharges = new ArrayList<>();
|
||||
|
||||
for (CompteComptable compte : comptes) {
|
||||
int classe = compte.getClasseComptable();
|
||||
BigDecimal[] t = totaux.getOrDefault(compte.getNumeroCompte(),
|
||||
new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
|
||||
BigDecimal solde = t[1].subtract(t[0]); // crédit - débit pour produits
|
||||
|
||||
if ((classe == 7) || (classe == 8 && TypeCompteComptable.PRODUITS.equals(compte.getTypeCompte()))) {
|
||||
if (solde.signum() != 0) {
|
||||
lignesProduits.add(new Object[]{compte.getNumeroCompte(), compte.getLibelle(), solde});
|
||||
totalProduits = totalProduits.add(solde);
|
||||
}
|
||||
} else if ((classe == 6) || (classe == 8 && TypeCompteComptable.CHARGES.equals(compte.getTypeCompte()))) {
|
||||
BigDecimal soldeCharge = t[0].subtract(t[1]); // débit - crédit pour charges
|
||||
if (soldeCharge.signum() != 0) {
|
||||
lignesCharges.add(new Object[]{compte.getNumeroCompte(), compte.getLibelle(), soldeCharge});
|
||||
totalCharges = totalCharges.add(soldeCharge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal resultat = totalProduits.subtract(totalCharges);
|
||||
boolean benefice = resultat.signum() >= 0;
|
||||
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
Document doc = new Document(PageSize.A4, 30, 30, 50, 40);
|
||||
PdfWriter.getInstance(doc, baos);
|
||||
doc.open();
|
||||
|
||||
addTitrePage(doc, "COMPTE DE RÉSULTAT", org.getNom(), dateDebut, dateFin);
|
||||
|
||||
// Section PRODUITS
|
||||
addSectionTitle(doc, "PRODUITS D'EXPLOITATION");
|
||||
PdfPTable tableProduits = creerTableau2Colonnes();
|
||||
for (Object[] ligne : lignesProduits) {
|
||||
addDataCell(tableProduits, ligne[0] + " — " + ligne[1], Color.WHITE);
|
||||
addAmountCell(tableProduits, (BigDecimal) ligne[2], Color.WHITE);
|
||||
}
|
||||
addTotalCell(tableProduits, "TOTAL PRODUITS");
|
||||
addAmountCell(tableProduits, totalProduits, COLOR_TOTAL_ROW);
|
||||
doc.add(tableProduits);
|
||||
|
||||
doc.add(new Paragraph(" "));
|
||||
|
||||
// Section CHARGES
|
||||
addSectionTitle(doc, "CHARGES D'EXPLOITATION");
|
||||
PdfPTable tableCharges = creerTableau2Colonnes();
|
||||
for (Object[] ligne : lignesCharges) {
|
||||
addDataCell(tableCharges, ligne[0] + " — " + ligne[1], Color.WHITE);
|
||||
addAmountCell(tableCharges, (BigDecimal) ligne[2], Color.WHITE);
|
||||
}
|
||||
addTotalCell(tableCharges, "TOTAL CHARGES");
|
||||
addAmountCell(tableCharges, totalCharges, COLOR_TOTAL_ROW);
|
||||
doc.add(tableCharges);
|
||||
|
||||
doc.add(new Paragraph(" "));
|
||||
|
||||
// Résultat net
|
||||
PdfPTable tableResultat = creerTableau2Colonnes();
|
||||
String libelleResultat = benefice ? "BÉNÉFICE NET DE L'EXERCICE" : "PERTE NETTE DE L'EXERCICE";
|
||||
Color couleurResultat = benefice ? new Color(0x00, 0x80, 0x00) : new Color(0xCC, 0x00, 0x00);
|
||||
PdfPCell cellResultat = new PdfPCell(
|
||||
new Phrase(libelleResultat, FontFactory.getFont(FontFactory.HELVETICA_BOLD, 11, couleurResultat)));
|
||||
cellResultat.setBackgroundColor(new Color(0xF0, 0xF8, 0xE8));
|
||||
cellResultat.setPadding(8);
|
||||
tableResultat.addCell(cellResultat);
|
||||
addAmountCell(tableResultat, resultat.abs(), new Color(0xF0, 0xF8, 0xE8));
|
||||
doc.add(tableResultat);
|
||||
|
||||
addFooter(doc);
|
||||
doc.close();
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur génération compte de résultat PDF : {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Erreur génération compte de résultat PDF", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Grand livre ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Génère le grand livre pour un compte donné.
|
||||
*/
|
||||
public byte[] genererGrandLivre(UUID organisationId, String numeroCompte,
|
||||
LocalDate dateDebut, LocalDate dateFin) {
|
||||
Organisation org = getOrg(organisationId);
|
||||
CompteComptable compte = compteComptableRepository
|
||||
.findByOrganisationAndNumero(organisationId, numeroCompte)
|
||||
.orElseThrow(() -> new NotFoundException(
|
||||
"Compte " + numeroCompte + " introuvable pour l'org " + organisationId));
|
||||
|
||||
List<EcritureComptable> ecritures = ecritureComptableRepository
|
||||
.findByOrganisationAndDateRange(organisationId, dateDebut, dateFin);
|
||||
|
||||
// Filtrer les lignes qui concernent ce compte
|
||||
List<Object[]> mouvements = new ArrayList<>();
|
||||
BigDecimal solde = BigDecimal.ZERO;
|
||||
|
||||
for (EcritureComptable ecriture : ecritures) {
|
||||
if (ecriture.getLignes() == null) continue;
|
||||
for (LigneEcriture ligne : ecriture.getLignes()) {
|
||||
if (ligne.getCompteComptable() == null) continue;
|
||||
if (!numeroCompte.equals(ligne.getCompteComptable().getNumeroCompte())) continue;
|
||||
|
||||
BigDecimal debit = ligne.getMontantDebit() != null ? ligne.getMontantDebit() : BigDecimal.ZERO;
|
||||
BigDecimal credit = ligne.getMontantCredit() != null ? ligne.getMontantCredit() : BigDecimal.ZERO;
|
||||
solde = solde.add(debit).subtract(credit);
|
||||
|
||||
mouvements.add(new Object[]{
|
||||
ecriture.getDateEcriture(),
|
||||
ecriture.getNumeroPiece(),
|
||||
ecriture.getLibelle(),
|
||||
debit,
|
||||
credit,
|
||||
solde
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
Document doc = new Document(PageSize.A4.rotate(), 20, 20, 40, 40);
|
||||
PdfWriter.getInstance(doc, baos);
|
||||
doc.open();
|
||||
|
||||
addTitrePage(doc, "GRAND LIVRE — " + numeroCompte + " " + compte.getLibelle(),
|
||||
org.getNom(), dateDebut, dateFin);
|
||||
|
||||
PdfPTable table = new PdfPTable(6);
|
||||
table.setWidthPercentage(100);
|
||||
table.setWidths(new float[]{12f, 15f, 35f, 12f, 12f, 14f});
|
||||
|
||||
addHeaderCell(table, "Date");
|
||||
addHeaderCell(table, "Pièce");
|
||||
addHeaderCell(table, "Libellé");
|
||||
addHeaderCell(table, "Débit");
|
||||
addHeaderCell(table, "Crédit");
|
||||
addHeaderCell(table, "Solde cumulé");
|
||||
|
||||
boolean alt = false;
|
||||
for (Object[] mvt : mouvements) {
|
||||
Color bg = alt ? COLOR_ROW_ALT : Color.WHITE;
|
||||
addDataCell(table, DATE_FMT.format((LocalDate) mvt[0]), bg);
|
||||
addDataCell(table, (String) mvt[1], bg);
|
||||
addDataCell(table, (String) mvt[2], bg);
|
||||
addAmountCell(table, (BigDecimal) mvt[3], bg);
|
||||
addAmountCell(table, (BigDecimal) mvt[4], bg);
|
||||
addAmountCell(table, (BigDecimal) mvt[5], bg);
|
||||
alt = !alt;
|
||||
}
|
||||
|
||||
if (mouvements.isEmpty()) {
|
||||
PdfPCell empty = new PdfPCell(new Phrase("Aucun mouvement sur la période",
|
||||
FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 10, Color.GRAY)));
|
||||
empty.setColspan(6);
|
||||
empty.setPadding(10);
|
||||
empty.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
table.addCell(empty);
|
||||
}
|
||||
|
||||
doc.add(table);
|
||||
addFooter(doc);
|
||||
doc.close();
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur génération grand livre PDF : {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Erreur génération grand livre PDF", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Utilitaires PDF ──────────────────────────────────────────────────────
|
||||
|
||||
private void addTitrePage(Document doc, String titre, String orgNom,
|
||||
LocalDate dateDebut, LocalDate dateFin) throws DocumentException {
|
||||
Font fontTitre = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, COLOR_HEADER);
|
||||
Font fontSousTitre = FontFactory.getFont(FontFactory.HELVETICA, 11, Color.DARK_GRAY);
|
||||
|
||||
Paragraph pTitre = new Paragraph(titre, fontTitre);
|
||||
pTitre.setAlignment(Element.ALIGN_CENTER);
|
||||
pTitre.setSpacingAfter(4);
|
||||
doc.add(pTitre);
|
||||
|
||||
Paragraph pOrg = new Paragraph(orgNom, fontSousTitre);
|
||||
pOrg.setAlignment(Element.ALIGN_CENTER);
|
||||
doc.add(pOrg);
|
||||
|
||||
if (dateDebut != null && dateFin != null) {
|
||||
Paragraph pPeriode = new Paragraph(
|
||||
"Période : " + DATE_FMT.format(dateDebut) + " au " + DATE_FMT.format(dateFin),
|
||||
FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 10, Color.GRAY));
|
||||
pPeriode.setAlignment(Element.ALIGN_CENTER);
|
||||
pPeriode.setSpacingAfter(12);
|
||||
doc.add(pPeriode);
|
||||
}
|
||||
}
|
||||
|
||||
private void addSectionTitle(Document doc, String titre) throws DocumentException {
|
||||
Paragraph p = new Paragraph(titre,
|
||||
FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12, COLOR_HEADER));
|
||||
p.setSpacingBefore(8);
|
||||
p.setSpacingAfter(4);
|
||||
doc.add(p);
|
||||
}
|
||||
|
||||
private void addFooter(Document doc) throws DocumentException {
|
||||
Paragraph footer = new Paragraph(
|
||||
"Généré le " + DATE_FMT.format(LocalDate.now()) + " — UnionFlow SYSCOHADA révisé",
|
||||
FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 8, Color.GRAY));
|
||||
footer.setAlignment(Element.ALIGN_RIGHT);
|
||||
footer.setSpacingBefore(16);
|
||||
doc.add(footer);
|
||||
}
|
||||
|
||||
private PdfPTable creerTableau2Colonnes() throws DocumentException {
|
||||
PdfPTable table = new PdfPTable(2);
|
||||
table.setWidthPercentage(100);
|
||||
table.setWidths(new float[]{65f, 35f});
|
||||
return table;
|
||||
}
|
||||
|
||||
private void addHeaderCell(PdfPTable table, String text) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text,
|
||||
FontFactory.getFont(FontFactory.HELVETICA_BOLD, 9, COLOR_HEADER_TEXT)));
|
||||
cell.setBackgroundColor(COLOR_HEADER);
|
||||
cell.setPadding(6);
|
||||
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private void addDataCell(PdfPTable table, String text, Color bg) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text,
|
||||
FontFactory.getFont(FontFactory.HELVETICA, 9, Color.BLACK)));
|
||||
cell.setBackgroundColor(bg);
|
||||
cell.setPadding(5);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private void addAmountCell(PdfPTable table, BigDecimal amount, Color bg) {
|
||||
String formatted = amount != null
|
||||
? String.format("%,.0f XOF", amount.doubleValue())
|
||||
: "0 XOF";
|
||||
PdfPCell cell = new PdfPCell(new Phrase(formatted,
|
||||
FontFactory.getFont(FontFactory.HELVETICA, 9, Color.BLACK)));
|
||||
cell.setBackgroundColor(bg);
|
||||
cell.setPadding(5);
|
||||
cell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private void addTotalCell(PdfPTable table, String text) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text,
|
||||
FontFactory.getFont(FontFactory.HELVETICA_BOLD, 9, Color.BLACK)));
|
||||
cell.setBackgroundColor(COLOR_TOTAL_ROW);
|
||||
cell.setPadding(6);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
// ── Calcul des totaux ─────────────────────────────────────────────────────
|
||||
|
||||
private Map<String, BigDecimal[]> calculerTotauxParCompte(UUID organisationId,
|
||||
LocalDate dateDebut, LocalDate dateFin) {
|
||||
List<EcritureComptable> ecritures = ecritureComptableRepository
|
||||
.findByOrganisationAndDateRange(organisationId, dateDebut, dateFin);
|
||||
|
||||
Map<String, BigDecimal[]> totaux = new HashMap<>();
|
||||
for (EcritureComptable ecriture : ecritures) {
|
||||
if (ecriture.getLignes() == null) continue;
|
||||
for (LigneEcriture ligne : ecriture.getLignes()) {
|
||||
if (ligne.getCompteComptable() == null) continue;
|
||||
String numero = ligne.getCompteComptable().getNumeroCompte();
|
||||
totaux.computeIfAbsent(numero, k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
|
||||
BigDecimal debit = ligne.getMontantDebit() != null ? ligne.getMontantDebit() : BigDecimal.ZERO;
|
||||
BigDecimal credit = ligne.getMontantCredit() != null ? ligne.getMontantCredit() : BigDecimal.ZERO;
|
||||
totaux.get(numero)[0] = totaux.get(numero)[0].add(debit);
|
||||
totaux.get(numero)[1] = totaux.get(numero)[1].add(credit);
|
||||
}
|
||||
}
|
||||
return totaux;
|
||||
}
|
||||
|
||||
private Organisation getOrg(UUID organisationId) {
|
||||
return organisationRepository.findByIdOptional(organisationId)
|
||||
.orElseThrow(() -> new NotFoundException("Organisation introuvable : " + organisationId));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user