feat: PHASE 4.1 - Entités et Repositories Comptables

Entités créées:
- CompteComptable: Plan comptable avec types, classes, soldes
- JournalComptable: Journaux (ACHATS, VENTES, BANQUE, CAISSE, OD)
- EcritureComptable: Écritures avec équilibre Débit=Crédit
- LigneEcriture: Lignes d'écriture avec validation

Enums créés (module API):
- TypeCompteComptable: ACTIF, PASSIF, CHARGES, PRODUITS, TRESORERIE, AUTRE
- TypeJournalComptable: ACHATS, VENTES, BANQUE, CAISSE, OD

Repositories créés:
- CompteComptableRepository: Recherche par numéro, type, classe
- JournalComptableRepository: Recherche par code, type, période
- EcritureComptableRepository: Recherche par journal, organisation, paiement, période
- LigneEcritureRepository: Recherche par écriture, compte

Fonctionnalités:
- Validation équilibre écritures (Débit = Crédit)
- Calcul automatique des totaux
- Génération automatique numéros de pièce
- Relations avec Organisation et Paiement
This commit is contained in:
dahoud
2025-11-30 11:16:24 +00:00
parent a62f401ab8
commit ced33a116e
10 changed files with 825 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
package dev.lions.unionflow.server.entity;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité CompteComptable pour le plan comptable
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "comptes_comptables",
indexes = {
@Index(name = "idx_compte_numero", columnList = "numero_compte", unique = true),
@Index(name = "idx_compte_type", columnList = "type_compte"),
@Index(name = "idx_compte_classe", columnList = "classe_comptable")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class CompteComptable extends BaseEntity {
/** Numéro de compte unique (ex: 411000, 512000) */
@NotBlank
@Column(name = "numero_compte", unique = true, nullable = false, length = 10)
private String numeroCompte;
/** Libellé du compte */
@NotBlank
@Column(name = "libelle", nullable = false, length = 200)
private String libelle;
/** Type de compte */
@NotNull
@Enumerated(EnumType.STRING)
@Column(name = "type_compte", nullable = false, length = 30)
private TypeCompteComptable typeCompte;
/** Classe comptable (1-7) */
@NotNull
@Min(value = 1, message = "La classe comptable doit être entre 1 et 7")
@Max(value = 7, message = "La classe comptable doit être entre 1 et 7")
@Column(name = "classe_comptable", nullable = false)
private Integer classeComptable;
/** Solde initial */
@Builder.Default
@DecimalMin(value = "0.0", message = "Le solde initial doit être positif ou nul")
@Digits(integer = 12, fraction = 2)
@Column(name = "solde_initial", precision = 14, scale = 2)
private BigDecimal soldeInitial = BigDecimal.ZERO;
/** Solde actuel (calculé) */
@Builder.Default
@Digits(integer = 12, fraction = 2)
@Column(name = "solde_actuel", precision = 14, scale = 2)
private BigDecimal soldeActuel = BigDecimal.ZERO;
/** Compte collectif (regroupe plusieurs sous-comptes) */
@Builder.Default
@Column(name = "compte_collectif", nullable = false)
private Boolean compteCollectif = false;
/** Compte analytique */
@Builder.Default
@Column(name = "compte_analytique", nullable = false)
private Boolean compteAnalytique = false;
/** Description du compte */
@Column(name = "description", length = 500)
private String description;
/** Lignes d'écriture associées */
@OneToMany(mappedBy = "compteComptable", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<LigneEcriture> lignesEcriture = new ArrayList<>();
/** Méthode métier pour obtenir le numéro formaté */
public String getNumeroFormate() {
return String.format("%-10s", numeroCompte);
}
/** Méthode métier pour vérifier si c'est un compte de trésorerie */
public boolean isTresorerie() {
return TypeCompteComptable.TRESORERIE.equals(typeCompte);
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
if (soldeInitial == null) {
soldeInitial = BigDecimal.ZERO;
}
if (soldeActuel == null) {
soldeActuel = soldeInitial;
}
if (compteCollectif == null) {
compteCollectif = false;
}
if (compteAnalytique == null) {
compteAnalytique = false;
}
}
}

View File

@@ -0,0 +1,172 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité EcritureComptable pour les écritures comptables
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "ecritures_comptables",
indexes = {
@Index(name = "idx_ecriture_numero_piece", columnList = "numero_piece", unique = true),
@Index(name = "idx_ecriture_date", columnList = "date_ecriture"),
@Index(name = "idx_ecriture_journal", columnList = "journal_id"),
@Index(name = "idx_ecriture_organisation", columnList = "organisation_id"),
@Index(name = "idx_ecriture_paiement", columnList = "paiement_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class EcritureComptable extends BaseEntity {
/** Numéro de pièce unique */
@NotBlank
@Column(name = "numero_piece", unique = true, nullable = false, length = 50)
private String numeroPiece;
/** Date de l'écriture */
@NotNull
@Column(name = "date_ecriture", nullable = false)
private LocalDate dateEcriture;
/** Libellé de l'écriture */
@NotBlank
@Column(name = "libelle", nullable = false, length = 500)
private String libelle;
/** Référence externe */
@Column(name = "reference", length = 100)
private String reference;
/** Lettrage (pour rapprochement) */
@Column(name = "lettrage", length = 20)
private String lettrage;
/** Pointage (pour rapprochement bancaire) */
@Builder.Default
@Column(name = "pointe", nullable = false)
private Boolean pointe = false;
/** Montant total débit (somme des lignes) */
@Builder.Default
@DecimalMin(value = "0.0")
@Digits(integer = 12, fraction = 2)
@Column(name = "montant_debit", precision = 14, scale = 2)
private BigDecimal montantDebit = BigDecimal.ZERO;
/** Montant total crédit (somme des lignes) */
@Builder.Default
@DecimalMin(value = "0.0")
@Digits(integer = 12, fraction = 2)
@Column(name = "montant_credit", precision = 14, scale = 2)
private BigDecimal montantCredit = BigDecimal.ZERO;
/** Commentaires */
@Column(name = "commentaire", length = 1000)
private String commentaire;
// Relations
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "journal_id", nullable = false)
private JournalComptable journal;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "paiement_id")
private Paiement paiement;
/** Lignes d'écriture */
@OneToMany(mappedBy = "ecriture", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@Builder.Default
private List<LigneEcriture> lignes = new ArrayList<>();
/** Méthode métier pour vérifier l'équilibre (Débit = Crédit) */
public boolean isEquilibree() {
if (montantDebit == null || montantCredit == null) {
return false;
}
return montantDebit.compareTo(montantCredit) == 0;
}
/** Méthode métier pour calculer les totaux à partir des lignes */
public void calculerTotaux() {
if (lignes == null || lignes.isEmpty()) {
montantDebit = BigDecimal.ZERO;
montantCredit = BigDecimal.ZERO;
return;
}
montantDebit =
lignes.stream()
.map(LigneEcriture::getMontantDebit)
.filter(amount -> amount != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
montantCredit =
lignes.stream()
.map(LigneEcriture::getMontantCredit)
.filter(amount -> amount != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/** Méthode métier pour générer un numéro de pièce unique */
public static String genererNumeroPiece(String prefixe, LocalDate date) {
return String.format(
"%s-%04d%02d%02d-%012d",
prefixe, date.getYear(), date.getMonthValue(), date.getDayOfMonth(),
System.currentTimeMillis() % 1000000000000L);
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
if (numeroPiece == null || numeroPiece.isEmpty()) {
numeroPiece = genererNumeroPiece("ECR", dateEcriture != null ? dateEcriture : LocalDate.now());
}
if (dateEcriture == null) {
dateEcriture = LocalDate.now();
}
if (montantDebit == null) {
montantDebit = BigDecimal.ZERO;
}
if (montantCredit == null) {
montantCredit = BigDecimal.ZERO;
}
if (pointe == null) {
pointe = false;
}
// Calculer les totaux si les lignes sont déjà présentes
if (lignes != null && !lignes.isEmpty()) {
calculerTotaux();
}
}
/** Callback JPA avant la mise à jour */
@PreUpdate
protected void onUpdate() {
calculerTotaux();
}
}

View File

@@ -0,0 +1,98 @@
package dev.lions.unionflow.server.entity;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité JournalComptable pour la gestion des journaux
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "journaux_comptables",
indexes = {
@Index(name = "idx_journal_code", columnList = "code", unique = true),
@Index(name = "idx_journal_type", columnList = "type_journal"),
@Index(name = "idx_journal_periode", columnList = "date_debut, date_fin")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class JournalComptable extends BaseEntity {
/** Code unique du journal */
@NotBlank
@Column(name = "code", unique = true, nullable = false, length = 10)
private String code;
/** Libellé du journal */
@NotBlank
@Column(name = "libelle", nullable = false, length = 100)
private String libelle;
/** Type de journal */
@NotNull
@Enumerated(EnumType.STRING)
@Column(name = "type_journal", nullable = false, length = 30)
private TypeJournalComptable typeJournal;
/** Date de début de la période */
@Column(name = "date_debut")
private LocalDate dateDebut;
/** Date de fin de la période */
@Column(name = "date_fin")
private LocalDate dateFin;
/** Statut du journal (OUVERT, FERME, ARCHIVE) */
@Builder.Default
@Column(name = "statut", length = 20)
private String statut = "OUVERT";
/** Description */
@Column(name = "description", length = 500)
private String description;
/** Écritures comptables associées */
@OneToMany(mappedBy = "journal", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<EcritureComptable> ecritures = new ArrayList<>();
/** Méthode métier pour vérifier si le journal est ouvert */
public boolean isOuvert() {
return "OUVERT".equals(statut);
}
/** Méthode métier pour vérifier si une date est dans la période */
public boolean estDansPeriode(LocalDate date) {
if (dateDebut == null || dateFin == null) {
return true; // Période illimitée
}
return !date.isBefore(dateDebut) && !date.isAfter(dateFin);
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
if (statut == null || statut.isEmpty()) {
statut = "OUVERT";
}
}
}

View File

@@ -0,0 +1,100 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité LigneEcriture pour les lignes d'une écriture comptable
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "lignes_ecriture",
indexes = {
@Index(name = "idx_ligne_ecriture_ecriture", columnList = "ecriture_id"),
@Index(name = "idx_ligne_ecriture_compte", columnList = "compte_comptable_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class LigneEcriture extends BaseEntity {
/** Numéro de ligne */
@NotNull
@Min(value = 1, message = "Le numéro de ligne doit être positif")
@Column(name = "numero_ligne", nullable = false)
private Integer numeroLigne;
/** Montant débit */
@DecimalMin(value = "0.0", message = "Le montant débit doit être positif ou nul")
@Digits(integer = 12, fraction = 2)
@Column(name = "montant_debit", precision = 14, scale = 2)
private BigDecimal montantDebit;
/** Montant crédit */
@DecimalMin(value = "0.0", message = "Le montant crédit doit être positif ou nul")
@Digits(integer = 12, fraction = 2)
@Column(name = "montant_credit", precision = 14, scale = 2)
private BigDecimal montantCredit;
/** Libellé de la ligne */
@Column(name = "libelle", length = 500)
private String libelle;
/** Référence */
@Column(name = "reference", length = 100)
private String reference;
// Relations
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ecriture_id", nullable = false)
private EcritureComptable ecriture;
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "compte_comptable_id", nullable = false)
private CompteComptable compteComptable;
/** Méthode métier pour vérifier que la ligne a soit un débit soit un crédit (pas les deux) */
public boolean isValide() {
boolean aDebit = montantDebit != null && montantDebit.compareTo(BigDecimal.ZERO) > 0;
boolean aCredit = montantCredit != null && montantCredit.compareTo(BigDecimal.ZERO) > 0;
return aDebit != aCredit; // XOR : soit débit, soit crédit, pas les deux
}
/** Méthode métier pour obtenir le montant (débit ou crédit) */
public BigDecimal getMontant() {
if (montantDebit != null && montantDebit.compareTo(BigDecimal.ZERO) > 0) {
return montantDebit;
}
if (montantCredit != null && montantCredit.compareTo(BigDecimal.ZERO) > 0) {
return montantCredit;
}
return BigDecimal.ZERO;
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
if (montantDebit == null) {
montantDebit = BigDecimal.ZERO;
}
if (montantCredit == null) {
montantCredit = BigDecimal.ZERO;
}
}
}

View File

@@ -0,0 +1,69 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
import dev.lions.unionflow.server.entity.CompteComptable;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
/**
* Repository pour l'entité CompteComptable
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class CompteComptableRepository implements PanacheRepository<CompteComptable> {
/**
* Trouve un compte par son numéro
*
* @param numeroCompte Numéro du compte
* @return Compte ou Optional.empty()
*/
public Optional<CompteComptable> findByNumeroCompte(String numeroCompte) {
return find("numeroCompte = ?1 AND actif = true", numeroCompte).firstResultOptional();
}
/**
* Trouve les comptes par type
*
* @param type Type de compte
* @return Liste des comptes
*/
public List<CompteComptable> findByType(TypeCompteComptable type) {
return find("typeCompte = ?1 AND actif = true ORDER BY numeroCompte ASC", type).list();
}
/**
* Trouve les comptes par classe comptable
*
* @param classe Classe comptable (1-7)
* @return Liste des comptes
*/
public List<CompteComptable> findByClasse(Integer classe) {
return find("classeComptable = ?1 AND actif = true ORDER BY numeroCompte ASC", classe).list();
}
/**
* Trouve tous les comptes actifs
*
* @return Liste des comptes actifs
*/
public List<CompteComptable> findAllActifs() {
return find("actif = true ORDER BY numeroCompte ASC").list();
}
/**
* Trouve les comptes de trésorerie
*
* @return Liste des comptes de trésorerie
*/
public List<CompteComptable> findComptesTresorerie() {
return find("typeCompte = ?1 AND actif = true ORDER BY numeroCompte ASC", TypeCompteComptable.TRESORERIE)
.list();
}
}

View File

@@ -0,0 +1,99 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.EcritureComptable;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour l'entité EcritureComptable
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class EcritureComptableRepository implements PanacheRepository<EcritureComptable> {
/**
* Trouve une écriture par son numéro de pièce
*
* @param numeroPiece Numéro de pièce
* @return Écriture ou Optional.empty()
*/
public Optional<EcritureComptable> findByNumeroPiece(String numeroPiece) {
return find("numeroPiece = ?1 AND actif = true", numeroPiece).firstResultOptional();
}
/**
* Trouve les écritures d'un journal
*
* @param journalId ID du journal
* @return Liste des écritures
*/
public List<EcritureComptable> findByJournalId(UUID journalId) {
return find("journal.id = ?1 AND actif = true ORDER BY dateEcriture DESC, numeroPiece ASC", journalId)
.list();
}
/**
* Trouve les écritures d'une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des écritures
*/
public List<EcritureComptable> findByOrganisationId(UUID organisationId) {
return find(
"organisation.id = ?1 AND actif = true ORDER BY dateEcriture DESC, numeroPiece ASC",
organisationId)
.list();
}
/**
* Trouve les écritures d'un paiement
*
* @param paiementId ID du paiement
* @return Liste des écritures
*/
public List<EcritureComptable> findByPaiementId(UUID paiementId) {
return find("paiement.id = ?1 AND actif = true ORDER BY dateEcriture DESC", paiementId).list();
}
/**
* Trouve les écritures dans une période
*
* @param dateDebut Date de début
* @param dateFin Date de fin
* @return Liste des écritures
*/
public List<EcritureComptable> findByPeriode(LocalDate dateDebut, LocalDate dateFin) {
return find(
"dateEcriture >= ?1 AND dateEcriture <= ?2 AND actif = true ORDER BY dateEcriture DESC, numeroPiece ASC",
dateDebut,
dateFin)
.list();
}
/**
* Trouve les écritures non pointées
*
* @return Liste des écritures non pointées
*/
public List<EcritureComptable> findNonPointees() {
return find("pointe = false AND actif = true ORDER BY dateEcriture ASC").list();
}
/**
* Trouve les écritures avec un lettrage spécifique
*
* @param lettrage Lettrage
* @return Liste des écritures
*/
public List<EcritureComptable> findByLettrage(String lettrage) {
return find("lettrage = ?1 AND actif = true ORDER BY dateEcriture DESC", lettrage).list();
}
}

View File

@@ -0,0 +1,72 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable;
import dev.lions.unionflow.server.entity.JournalComptable;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
/**
* Repository pour l'entité JournalComptable
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class JournalComptableRepository implements PanacheRepository<JournalComptable> {
/**
* Trouve un journal par son code
*
* @param code Code du journal
* @return Journal ou Optional.empty()
*/
public Optional<JournalComptable> findByCode(String code) {
return find("code = ?1 AND actif = true", code).firstResultOptional();
}
/**
* Trouve les journaux par type
*
* @param type Type de journal
* @return Liste des journaux
*/
public List<JournalComptable> findByType(TypeJournalComptable type) {
return find("typeJournal = ?1 AND actif = true ORDER BY code ASC", type).list();
}
/**
* Trouve les journaux ouverts
*
* @return Liste des journaux ouverts
*/
public List<JournalComptable> findJournauxOuverts() {
return find("statut = ?1 AND actif = true ORDER BY code ASC", "OUVERT").list();
}
/**
* Trouve les journaux pour une date donnée
*
* @param date Date à vérifier
* @return Liste des journaux actifs pour cette date
*/
public List<JournalComptable> findJournauxPourDate(LocalDate date) {
return find(
"(dateDebut IS NULL OR dateDebut <= ?1) AND (dateFin IS NULL OR dateFin >= ?1) AND actif = true ORDER BY code ASC",
date)
.list();
}
/**
* Trouve tous les journaux actifs
*
* @return Liste des journaux actifs
*/
public List<JournalComptable> findAllActifs() {
return find("actif = true ORDER BY code ASC").list();
}
}

View File

@@ -0,0 +1,40 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.LigneEcriture;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.UUID;
/**
* Repository pour l'entité LigneEcriture
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class LigneEcritureRepository implements PanacheRepository<LigneEcriture> {
/**
* Trouve toutes les lignes d'une écriture
*
* @param ecritureId ID de l'écriture
* @return Liste des lignes
*/
public List<LigneEcriture> findByEcritureId(UUID ecritureId) {
return find("ecriture.id = ?1 ORDER BY numeroLigne ASC", ecritureId).list();
}
/**
* Trouve toutes les lignes d'un compte comptable
*
* @param compteComptableId ID du compte comptable
* @return Liste des lignes
*/
public List<LigneEcriture> findByCompteComptableId(UUID compteComptableId) {
return find("compteComptable.id = ?1 ORDER BY ecriture.dateEcriture DESC, numeroLigne ASC", compteComptableId)
.list();
}
}