Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 3m46s
Le commita72ab54(chore docker Dockerfile racine) a involontairement balayé des fichiers du commit31330d9(PI-SPI, KYC, RLS, mutuelle parts, comptabilité PDF) lors d'un git add -A trop large. Restauration de l'intégralité des fichiers depuis a72ab54^ : - 11 migrations Flyway V32-V42 (parts sociales, SYSCOHADA, Keycloak Org Id, KYC, RLS, Provider défaut, FCM, App DB Roles) - Package payment/pispi/ complet (PispiAuth, PispiClient, PispiIso20022Mapper, PispiSignatureVerifier, PispiWebhookResource, dto/Pacs008Request, dto/Pacs002Response, PispiPaymentProvider) - Package payment/{wave,orangemoney,mtnmomo}/* (PaymentProvider impls) - Package payment/orchestration/ (PaymentOrchestrator, PaymentProviderRegistry) - Entités KycDossier, mutuelle/parts/* (ComptePartsSociales, TransactionPartsSociales) - Mappers, repositories, resources associés - Services KycAmlService, ComptabilitePdfService, ReleveComptePdfService, InteretsEpargneService - AdminKeycloakOrganisationResource, KycResource, PaiementUnifieResource - Tests unitaires PI-SPI, KYC, mutuelle parts
436 lines
20 KiB
Java
436 lines
20 KiB
Java
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));
|
|
}
|
|
}
|