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é. * *

Rapports disponibles : *

*/ @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 comptes = compteComptableRepository.findByOrganisation(organisationId); Map 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 totaux = calculerTotauxParCompte(organisationId, dateDebut, dateFin); List comptes = compteComptableRepository.findByOrganisation(organisationId); BigDecimal totalProduits = BigDecimal.ZERO; BigDecimal totalCharges = BigDecimal.ZERO; List lignesProduits = new ArrayList<>(); List 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 ecritures = ecritureComptableRepository .findByOrganisationAndDateRange(organisationId, dateDebut, dateFin); // Filtrer les lignes qui concernent ce compte List 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 calculerTotauxParCompte(UUID organisationId, LocalDate dateDebut, LocalDate dateFin) { List ecritures = ecritureComptableRepository .findByOrganisationAndDateRange(organisationId, dateDebut, dateFin); Map 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)); } }