497 lines
18 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|