219 lines
6.9 KiB
Java
219 lines
6.9 KiB
Java
package dev.lions.unionflow.server.entity;
|
|
|
|
import jakarta.persistence.*;
|
|
import jakarta.validation.constraints.*;
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
import lombok.AllArgsConstructor;
|
|
import lombok.Builder;
|
|
import lombok.Data;
|
|
import lombok.EqualsAndHashCode;
|
|
import lombok.NoArgsConstructor;
|
|
|
|
/**
|
|
* Entité Budget
|
|
*
|
|
* Représente un budget prévisionnel (mensuel/trimestriel/annuel) avec suivi de réalisation.
|
|
*
|
|
* @author UnionFlow Team
|
|
* @version 1.0
|
|
* @since 2026-03-13
|
|
*/
|
|
@Entity
|
|
@Table(name = "budgets", indexes = {
|
|
@Index(name = "idx_budget_organisation", columnList = "organisation_id"),
|
|
@Index(name = "idx_budget_status", columnList = "status"),
|
|
@Index(name = "idx_budget_period", columnList = "period"),
|
|
@Index(name = "idx_budget_year_month", columnList = "year, month"),
|
|
@Index(name = "idx_budget_created_by", columnList = "created_by_id")
|
|
})
|
|
@Data
|
|
@NoArgsConstructor
|
|
@AllArgsConstructor
|
|
@Builder
|
|
@EqualsAndHashCode(callSuper = true)
|
|
public class Budget extends BaseEntity {
|
|
|
|
/** Nom du budget */
|
|
@NotBlank
|
|
@Size(max = 200)
|
|
@Column(name = "name", nullable = false, length = 200)
|
|
private String name;
|
|
|
|
/** Description optionnelle */
|
|
@Size(max = 1000)
|
|
@Column(name = "description", length = 1000)
|
|
private String description;
|
|
|
|
/** Organisation concernée */
|
|
@NotNull
|
|
@ManyToOne(fetch = FetchType.LAZY)
|
|
@JoinColumn(name = "organisation_id", nullable = false)
|
|
private Organisation organisation;
|
|
|
|
/** Période (MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL) */
|
|
@NotBlank
|
|
@Pattern(regexp = "^(MONTHLY|QUARTERLY|SEMIANNUAL|ANNUAL)$")
|
|
@Column(name = "period", nullable = false, length = 20)
|
|
private String period;
|
|
|
|
/** Année du budget */
|
|
@NotNull
|
|
@Min(value = 2020, message = "L'année doit être >= 2020")
|
|
@Max(value = 2100, message = "L'année doit être <= 2100")
|
|
@Column(name = "year", nullable = false)
|
|
private Integer year;
|
|
|
|
/** Mois (1-12) pour budget mensuel, null sinon */
|
|
@Min(value = 1)
|
|
@Max(value = 12)
|
|
@Column(name = "month")
|
|
private Integer month;
|
|
|
|
/** Statut (DRAFT, ACTIVE, CLOSED, CANCELLED) */
|
|
@NotBlank
|
|
@Pattern(regexp = "^(DRAFT|ACTIVE|CLOSED|CANCELLED)$")
|
|
@Builder.Default
|
|
@Column(name = "status", nullable = false, length = 20)
|
|
private String status = "DRAFT";
|
|
|
|
/** Lignes budgétaires */
|
|
@OneToMany(mappedBy = "budget", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
|
@Builder.Default
|
|
private List<BudgetLine> lines = new ArrayList<>();
|
|
|
|
/** Total prévu (somme des montants prévus des lignes) */
|
|
@NotNull
|
|
@DecimalMin(value = "0.0")
|
|
@Digits(integer = 14, fraction = 2)
|
|
@Builder.Default
|
|
@Column(name = "total_planned", nullable = false, precision = 16, scale = 2)
|
|
private BigDecimal totalPlanned = BigDecimal.ZERO;
|
|
|
|
/** Total réalisé (somme des montants réalisés des lignes) */
|
|
@DecimalMin(value = "0.0")
|
|
@Digits(integer = 14, fraction = 2)
|
|
@Builder.Default
|
|
@Column(name = "total_realized", nullable = false, precision = 16, scale = 2)
|
|
private BigDecimal totalRealized = BigDecimal.ZERO;
|
|
|
|
/** Code devise ISO 3 lettres */
|
|
@NotBlank
|
|
@Pattern(regexp = "^[A-Z]{3}$")
|
|
@Builder.Default
|
|
@Column(name = "currency", nullable = false, length = 3)
|
|
private String currency = "XOF";
|
|
|
|
/** ID du créateur du budget */
|
|
@NotNull
|
|
@Column(name = "created_by_id", nullable = false)
|
|
private UUID createdById;
|
|
|
|
/** Date de création */
|
|
@NotNull
|
|
@Column(name = "created_at_budget", nullable = false)
|
|
private LocalDateTime createdAtBudget;
|
|
|
|
/** Date d'approbation */
|
|
@Column(name = "approved_at")
|
|
private LocalDateTime approvedAt;
|
|
|
|
/** ID de l'approbateur */
|
|
@Column(name = "approved_by_id")
|
|
private UUID approvedById;
|
|
|
|
/** Date de début de la période budgétaire */
|
|
@NotNull
|
|
@Column(name = "start_date", nullable = false)
|
|
private LocalDate startDate;
|
|
|
|
/** Date de fin de la période budgétaire */
|
|
@NotNull
|
|
@Column(name = "end_date", nullable = false)
|
|
private LocalDate endDate;
|
|
|
|
/** Métadonnées additionnelles (JSON) */
|
|
@Column(name = "metadata", columnDefinition = "TEXT")
|
|
private String metadata;
|
|
|
|
@PrePersist
|
|
protected void onCreate() {
|
|
super.onCreate();
|
|
if (createdAtBudget == null) {
|
|
createdAtBudget = LocalDateTime.now();
|
|
}
|
|
if (currency == null) {
|
|
currency = "XOF";
|
|
}
|
|
if (status == null) {
|
|
status = "DRAFT";
|
|
}
|
|
if (totalPlanned == null) {
|
|
totalPlanned = BigDecimal.ZERO;
|
|
}
|
|
if (totalRealized == null) {
|
|
totalRealized = BigDecimal.ZERO;
|
|
}
|
|
}
|
|
|
|
/** Méthode métier pour ajouter une ligne budgétaire */
|
|
public void addLine(BudgetLine line) {
|
|
lines.add(line);
|
|
line.setBudget(this);
|
|
recalculateTotals();
|
|
}
|
|
|
|
/** Méthode métier pour supprimer une ligne budgétaire */
|
|
public void removeLine(BudgetLine line) {
|
|
lines.remove(line);
|
|
line.setBudget(null);
|
|
recalculateTotals();
|
|
}
|
|
|
|
/** Méthode métier pour recalculer les totaux */
|
|
public void recalculateTotals() {
|
|
this.totalPlanned = lines.stream()
|
|
.map(BudgetLine::getAmountPlanned)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
this.totalRealized = lines.stream()
|
|
.map(BudgetLine::getAmountRealized)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
}
|
|
|
|
/** Méthode métier pour calculer le taux de réalisation (%) */
|
|
public double getRealizationRate() {
|
|
if (totalPlanned.compareTo(BigDecimal.ZERO) == 0) {
|
|
return 0.0;
|
|
}
|
|
return totalRealized.divide(totalPlanned, 4, java.math.RoundingMode.HALF_UP)
|
|
.multiply(new BigDecimal("100"))
|
|
.doubleValue();
|
|
}
|
|
|
|
/** Méthode métier pour calculer l'écart (réalisé - prévu) */
|
|
public BigDecimal getVariance() {
|
|
return totalRealized.subtract(totalPlanned);
|
|
}
|
|
|
|
/** Méthode métier pour vérifier si le budget est dépassé */
|
|
public boolean isOverBudget() {
|
|
return totalRealized.compareTo(totalPlanned) > 0;
|
|
}
|
|
|
|
/** Méthode métier pour vérifier si le budget est actif */
|
|
public boolean isActive() {
|
|
return "ACTIVE".equals(status);
|
|
}
|
|
|
|
/** Méthode métier pour vérifier si la période est en cours */
|
|
public boolean isCurrentPeriod() {
|
|
LocalDate now = LocalDate.now();
|
|
return !now.isBefore(startDate) && !now.isAfter(endDate);
|
|
}
|
|
}
|