Files
btpxpress-backend/src/main/java/dev/lions/btpxpress/application/service/StockService.java
2025-10-01 01:37:34 +00:00

497 lines
18 KiB
Java

package dev.lions.btpxpress.application.service;
import dev.lions.btpxpress.domain.core.entity.*;
import dev.lions.btpxpress.domain.infrastructure.repository.*;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Service de gestion des stocks */
@ApplicationScoped
public class StockService {
private static final Logger logger = LoggerFactory.getLogger(StockService.class);
@Inject StockRepository stockRepository;
@Inject FournisseurRepository fournisseurRepository;
@Inject ChantierRepository chantierRepository;
/** Récupère tous les articles en stock */
public List<Stock> findAll() {
return stockRepository.listAll();
}
/** Récupère un article par son ID */
public Stock findById(UUID id) {
Stock stock = stockRepository.findById(id);
if (stock == null) {
throw new NotFoundException("Article en stock non trouvé avec l'ID: " + id);
}
return stock;
}
/** Récupère un article par sa référence */
public Stock findByReference(String reference) {
Stock stock = stockRepository.findByReference(reference);
if (stock == null) {
throw new NotFoundException("Article en stock non trouvé avec la référence: " + reference);
}
return stock;
}
/** Recherche des articles par désignation */
public List<Stock> searchByDesignation(String designation) {
return stockRepository.searchByDesignation(designation);
}
/** Récupère les articles par catégorie */
public List<Stock> findByCategorie(CategorieStock categorie) {
return stockRepository.findByCategorie(categorie);
}
/** Récupère les articles par fournisseur */
public List<Stock> findByFournisseur(UUID fournisseurId) {
return stockRepository.findByFournisseur(fournisseurId);
}
/** Récupère les articles par chantier */
public List<Stock> findByChantier(UUID chantierId) {
return stockRepository.findByChantier(chantierId);
}
/** Récupère les articles par statut */
public List<Stock> findByStatut(StatutStock statut) {
return stockRepository.findByStatut(statut);
}
/** Récupère les articles actifs */
public List<Stock> findActifs() {
return stockRepository.findActifs();
}
/** Récupère les articles en rupture de stock */
public List<Stock> findStocksEnRupture() {
return stockRepository.findStocksEnRupture();
}
/** Récupère les articles sous quantité minimum */
public List<Stock> findStocksSousQuantiteMinimum() {
return stockRepository.findStocksSousQuantiteMinimum();
}
/** Récupère les articles sous quantité de sécurité */
public List<Stock> findStocksSousQuantiteSecurite() {
return stockRepository.findStocksSousQuantiteSecurite();
}
/** Récupère les articles à commander */
public List<Stock> findStocksACommander() {
return stockRepository.findStocksACommander();
}
/** Récupère les articles périmés */
public List<Stock> findStocksPerimes() {
return stockRepository.findStocksPerimes();
}
/** Récupère les articles proches de la péremption */
public List<Stock> findStocksProchesPeremption(int nbJours) {
return stockRepository.findStocksProchesPeremption(nbJours);
}
/** Récupère les articles avec réservations */
public List<Stock> findStocksAvecReservations() {
return stockRepository.findStocksAvecReservations();
}
/** Crée un nouvel article en stock */
@Transactional
public Stock create(Stock stock) {
logger.info("Création d'un nouvel article en stock: {}", stock.getDesignation());
// Validation
validateStock(stock);
// Vérification de l'unicité de la référence
if (stockRepository.existsByReference(stock.getReference())) {
throw new IllegalArgumentException(
"Un article avec cette référence existe déjà: " + stock.getReference());
}
// Vérification que le fournisseur existe
if (stock.getFournisseurPrincipal() != null) {
if (fournisseurRepository.findById(stock.getFournisseurPrincipal().getId()) == null) {
throw new IllegalArgumentException("Le fournisseur spécifié n'existe pas");
}
}
// Vérification que le chantier existe
if (stock.getChantier() != null) {
if (chantierRepository.findById(stock.getChantier().getId()) == null) {
throw new IllegalArgumentException("Le chantier spécifié n'existe pas");
}
}
stockRepository.persist(stock);
logger.info("Article créé avec succès avec l'ID: {}", stock.getId());
return stock;
}
/** Met à jour un article en stock */
@Transactional
public Stock update(UUID id, Stock stockData) {
logger.info("Mise à jour de l'article en stock: {}", id);
Stock stock = findById(id);
// Validation
validateStock(stockData);
// Vérification de l'unicité de la référence si modifiée
if (!stock.getReference().equals(stockData.getReference())) {
if (stockRepository.existsByReference(stockData.getReference())) {
throw new IllegalArgumentException(
"Un article avec cette référence existe déjà: " + stockData.getReference());
}
}
// Mise à jour des champs
updateStockFields(stock, stockData);
logger.info("Article mis à jour avec succès: {}", id);
return stock;
}
/** Effectue une entrée de stock */
@Transactional
public Stock entreeStock(UUID stockId, BigDecimal quantite, String motif, String numeroDocument) {
logger.info("Entrée de stock pour l'article: {} - Quantité: {}", stockId, quantite);
Stock stock = findById(stockId);
if (quantite.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("La quantité d'entrée doit être positive");
}
// Mise à jour de la quantité
stock.setQuantiteStock(stock.getQuantiteStock().add(quantite));
// Mise à jour de la date
stock.setDateDerniereEntree(LocalDateTime.now());
logger.info("Entrée de stock effectuée avec succès: {} unités", quantite);
return stock;
}
/** Effectue une sortie de stock */
@Transactional
public Stock sortieStock(UUID stockId, BigDecimal quantite, String motif, String numeroDocument) {
logger.info("Sortie de stock pour l'article: {} - Quantité: {}", stockId, quantite);
Stock stock = findById(stockId);
if (quantite.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("La quantité de sortie doit être positive");
}
BigDecimal quantiteDisponible = stock.getQuantiteDisponible();
if (quantite.compareTo(quantiteDisponible) > 0) {
throw new IllegalArgumentException(
"Quantité insuffisante en stock. Disponible: " + quantiteDisponible);
}
// Mise à jour de la quantité
stock.setQuantiteStock(stock.getQuantiteStock().subtract(quantite));
stock.setDateDerniereSortie(LocalDateTime.now());
logger.info("Sortie de stock effectuée avec succès: {} unités", quantite);
return stock;
}
/** Réserve une quantité de stock */
@Transactional
public Stock reserverStock(UUID stockId, BigDecimal quantite, String motif) {
logger.info("Réservation de stock pour l'article: {} - Quantité: {}", stockId, quantite);
Stock stock = findById(stockId);
if (quantite.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("La quantité à réserver doit être positive");
}
BigDecimal quantiteDisponible = stock.getQuantiteDisponible();
if (quantite.compareTo(quantiteDisponible) > 0) {
throw new IllegalArgumentException(
"Quantité insuffisante disponible. Disponible: " + quantiteDisponible);
}
stock.setQuantiteReservee(stock.getQuantiteReservee().add(quantite));
logger.info("Réservation effectuée avec succès: {} unités", quantite);
return stock;
}
/** Libère une réservation de stock */
@Transactional
public Stock libererReservation(UUID stockId, BigDecimal quantite) {
logger.info("Libération de réservation pour l'article: {} - Quantité: {}", stockId, quantite);
Stock stock = findById(stockId);
if (quantite.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("La quantité à libérer doit être positive");
}
if (quantite.compareTo(stock.getQuantiteReservee()) > 0) {
throw new IllegalArgumentException("Quantité à libérer supérieure à la quantité réservée");
}
stock.setQuantiteReservee(stock.getQuantiteReservee().subtract(quantite));
logger.info("Libération de réservation effectuée avec succès: {} unités", quantite);
return stock;
}
/** Effectue un inventaire */
@Transactional
public Stock inventaireStock(UUID stockId, BigDecimal quantiteReelle, String motif) {
logger.info("Inventaire pour l'article: {} - Quantité réelle: {}", stockId, quantiteReelle);
Stock stock = findById(stockId);
if (quantiteReelle.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("La quantité réelle ne peut pas être négative");
}
BigDecimal ecart = quantiteReelle.subtract(stock.getQuantiteStock());
stock.setQuantiteStock(quantiteReelle);
stock.setDateDerniereInventaire(LocalDateTime.now());
if (motif != null && !motif.trim().isEmpty()) {
String commentaire =
stock.getCommentaires() != null
? stock.getCommentaires() + "\n[INVENTAIRE] " + motif
: "[INVENTAIRE] " + motif;
stock.setCommentaires(commentaire);
}
logger.info("Inventaire effectué avec succès. Écart: {} unités", ecart);
return stock;
}
/** Change le statut d'un article */
@Transactional
public Stock changerStatut(UUID stockId, StatutStock nouveauStatut, String motif) {
logger.info(
"Changement de statut pour l'article: {} - Nouveau statut: {}", stockId, nouveauStatut);
Stock stock = findById(stockId);
StatutStock ancienStatut = stock.getStatut();
stock.setStatut(nouveauStatut);
if (motif != null && !motif.trim().isEmpty()) {
String commentaire =
stock.getCommentaires() != null
? stock.getCommentaires()
+ "\n[STATUT] "
+ ancienStatut
+ " -> "
+ nouveauStatut
+ ": "
+ motif
: "[STATUT] " + ancienStatut + " -> " + nouveauStatut + ": " + motif;
stock.setCommentaires(commentaire);
}
logger.info("Statut changé avec succès de {} à {}", ancienStatut, nouveauStatut);
return stock;
}
/** Supprime un article (logiquement) */
@Transactional
public void delete(UUID id) {
logger.info("Suppression de l'article en stock: {}", id);
Stock stock = findById(id);
if (stock.getQuantiteStock().compareTo(BigDecimal.ZERO) > 0) {
throw new IllegalStateException("Impossible de supprimer un article qui a du stock");
}
if (stock.getQuantiteReservee().compareTo(BigDecimal.ZERO) > 0) {
throw new IllegalStateException("Impossible de supprimer un article qui a des réservations");
}
stock.setStatut(StatutStock.SUPPRIME);
logger.info("Article supprimé avec succès: {}", id);
}
/** Génère les statistiques de stock */
public Map<String, Object> getStatistiques() {
List<Stock> tousStocks = stockRepository.listAll();
Map<CategorieStock, Long> parCategorie =
tousStocks.stream()
.collect(Collectors.groupingBy(Stock::getCategorie, Collectors.counting()));
Map<StatutStock, Long> parStatut =
tousStocks.stream().collect(Collectors.groupingBy(Stock::getStatut, Collectors.counting()));
long articlesEnRupture = tousStocks.stream().mapToLong(s -> s.isEnRupture() ? 1 : 0).sum();
long articlesSousMinimum =
tousStocks.stream().mapToLong(s -> s.isSousQuantiteMinimum() ? 1 : 0).sum();
long articlesPerimes = tousStocks.stream().mapToLong(s -> s.isPerime() ? 1 : 0).sum();
BigDecimal valeurTotaleStock =
tousStocks.stream().map(Stock::getValeurStock).reduce(BigDecimal.ZERO, BigDecimal::add);
return Map.of(
"total", tousStocks.size(),
"parCategorie", parCategorie,
"parStatut", parStatut,
"articlesEnRupture", articlesEnRupture,
"articlesSousMinimum", articlesSousMinimum,
"articlesPerimes", articlesPerimes,
"valeurTotaleStock", valeurTotaleStock);
}
/** Génère la liste des articles à commander */
public List<Stock> getArticlesACommander() {
return stockRepository.findStocksACommander();
}
/** Calcule la valeur totale du stock */
public BigDecimal calculateValeurTotaleStock() {
return stockRepository.calculateValeurTotaleStock();
}
/** Recherche de stocks par multiple critères */
public List<Stock> searchStocks(String searchTerm) {
return stockRepository.searchStocks(searchTerm);
}
/** Récupère les top stocks par valeur */
public List<Stock> findTopStocksByValeur(int limit) {
return stockRepository.findTopStocksByValeur(limit);
}
/** Récupère les top stocks par quantité */
public List<Stock> findTopStocksByQuantite(int limit) {
return stockRepository.findTopStocksByQuantite(limit);
}
/** Validation des données d'un stock */
private void validateStock(Stock stock) {
if (stock.getReference() == null || stock.getReference().trim().isEmpty()) {
throw new IllegalArgumentException("La référence de l'article est obligatoire");
}
if (stock.getDesignation() == null || stock.getDesignation().trim().isEmpty()) {
throw new IllegalArgumentException("La désignation de l'article est obligatoire");
}
if (stock.getCategorie() == null) {
throw new IllegalArgumentException("La catégorie est obligatoire");
}
if (stock.getUniteMesure() == null) {
throw new IllegalArgumentException("L'unité de mesure est obligatoire");
}
if (stock.getQuantiteStock() != null
&& stock.getQuantiteStock().compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("La quantité en stock ne peut pas être négative");
}
if (stock.getQuantiteMinimum() != null
&& stock.getQuantiteMinimum().compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("La quantité minimum ne peut pas être négative");
}
if (stock.getPrixUnitaireHT() != null
&& stock.getPrixUnitaireHT().compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Le prix unitaire HT ne peut pas être négatif");
}
}
/** Met à jour les champs d'un stock */
private void updateStockFields(Stock stock, Stock stockData) {
stock.setReference(stockData.getReference());
stock.setDesignation(stockData.getDesignation());
stock.setDescription(stockData.getDescription());
stock.setCategorie(stockData.getCategorie());
stock.setSousCategorie(stockData.getSousCategorie());
stock.setUniteMesure(stockData.getUniteMesure());
stock.setQuantiteMinimum(stockData.getQuantiteMinimum());
stock.setQuantiteMaximum(stockData.getQuantiteMaximum());
stock.setQuantiteSecurite(stockData.getQuantiteSecurite());
stock.setPrixUnitaireHT(stockData.getPrixUnitaireHT());
stock.setTauxTVA(stockData.getTauxTVA());
stock.setEmplacementStockage(stockData.getEmplacementStockage());
stock.setCodeZone(stockData.getCodeZone());
stock.setCodeAllee(stockData.getCodeAllee());
stock.setCodeEtagere(stockData.getCodeEtagere());
stock.setFournisseurPrincipal(stockData.getFournisseurPrincipal());
stock.setMarque(stockData.getMarque());
stock.setModele(stockData.getModele());
stock.setReferenceFournisseur(stockData.getReferenceFournisseur());
stock.setCodeBarre(stockData.getCodeBarre());
stock.setCodeEAN(stockData.getCodeEAN());
stock.setPoidsUnitaire(stockData.getPoidsUnitaire());
stock.setLongueur(stockData.getLongueur());
stock.setLargeur(stockData.getLargeur());
stock.setHauteur(stockData.getHauteur());
stock.setVolume(stockData.getVolume());
stock.setDatePeremption(stockData.getDatePeremption());
stock.setGestionParLot(stockData.getGestionParLot());
stock.setTraçabiliteRequise(stockData.getTraçabiliteRequise());
stock.setArticlePerissable(stockData.getArticlePerissable());
stock.setControleQualiteRequis(stockData.getControleQualiteRequis());
stock.setArticleDangereux(stockData.getArticleDangereux());
stock.setClasseDanger(stockData.getClasseDanger());
stock.setCommentaires(stockData.getCommentaires());
stock.setNotesStockage(stockData.getNotesStockage());
stock.setConditionsStockage(stockData.getConditionsStockage());
stock.setTemperatureStockageMin(stockData.getTemperatureStockageMin());
stock.setTemperatureStockageMax(stockData.getTemperatureStockageMax());
stock.setHumiditeMax(stockData.getHumiditeMax());
}
/** Met à jour le coût moyen pondéré */
private void updateCoutMoyenPondere(
Stock stock, BigDecimal quantiteEntree, BigDecimal coutUnitaire) {
BigDecimal quantiteInitiale = stock.getQuantiteStock();
BigDecimal coutMoyenActuel =
stock.getCoutMoyenPondere() != null ? stock.getCoutMoyenPondere() : BigDecimal.ZERO;
if (quantiteInitiale.compareTo(BigDecimal.ZERO) == 0) {
// Premier approvisionnement
stock.setCoutMoyenPondere(coutUnitaire);
} else {
// Calcul du coût moyen pondéré
BigDecimal valeurInitiale = quantiteInitiale.multiply(coutMoyenActuel);
BigDecimal valeurEntree = quantiteEntree.multiply(coutUnitaire);
BigDecimal quantiteTotale = quantiteInitiale.add(quantiteEntree);
BigDecimal nouveauCoutMoyen =
valeurInitiale.add(valeurEntree).divide(quantiteTotale, 4, BigDecimal.ROUND_HALF_UP);
stock.setCoutMoyenPondere(nouveauCoutMoyen);
}
}
}