<?php
namespace App\Controller;
use DateTime;
use App\Entity\Vente;
use App\Entity\Client;
use App\Entity\Facture;
use App\Form\VenteType;
use App\Entity\SortieStock;
use App\Entity\DetailsVente;
use App\Repository\VenteRepository;
use App\Repository\ClientRepository;
use App\Repository\ProduitRepository;
use App\Repository\ProduitStockRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Core\Security;
#[Route('/vente')]
class VenteController extends AbstractController
{
#[Route('/', name: 'app_vente_index', methods: ['GET'])]
public function index(
VenteRepository $venteRepository,
Security $security
): Response {
/** @var \App\Entity\User $user */
$user = $security->getUser();
// Vérifier si l'utilisateur est connecté
if (!$user) {
throw $this->createAccessDeniedException('Vous devez être connecté pour accéder à cette page.');
}
// Récupérer les ventes selon le rôle de l'utilisateur
if ($security->isGranted('ROLE_SUPER_ADMIN')) {
// Si Super Admin, récupérer toutes les ventes
$ventes = $venteRepository->findBy([], ['id' => 'DESC']);
} else {
// Pour les autres utilisateurs, récupérer uniquement les ventes de leur boutique
$boutique = $user->getBoutique();
if (!$boutique) {
// Si l'utilisateur n'a pas de boutique associée
$ventes = [];
} else {
// Récupérer les ventes de la boutique de l'utilisateur
$ventes = $venteRepository->findBy(['boutique' => $boutique], ['id' => 'DESC']);
}
}
return $this->render('vente/index.html.twig', [
'ventes' => $ventes,
]);
}
#[Route('/{id}', name: 'app_vente_show', methods: ['GET'])]
public function show(Vente $vente): Response
{
$totalAPayer = 0;
foreach ($vente->getDetailsVentes() as $detailVente) {
$totalAPayer += $detailVente->getPrixTotal();
}
return $this->render('vente/show.html.twig', [
'vente' => $vente,
'totalAPayer' => $totalAPayer,
]);
}
#[Route('/edit/{id}', name: 'edit_vente')]
public function editVente(
Vente $vente,
SessionInterface $session,
ProduitRepository $productsRepository,
ClientRepository $clientRepository
): Response {
// Initialiser le panier avec les produits de la vente
$panier = [];
foreach ($vente->getDetailsVentes() as $detail) {
$panier[$detail->getProduit()->getId()] = [
'quantite' => $detail->getQuantiteVendu(),
'prixVenteUnitaire' => $detail->getPrixUnitaire(),
'remisePourcentage' => $detail->getRemisePourcentage() ?? 0
];
}
// Stocker le panier en session
$session->set("panier", $panier);
// Récupérer les clients pour le formulaire
$clients = $clientRepository->findAll();
// Préparer les données du panier pour l'affichage
$dataPanier = [];
$total = 0;
foreach ($panier as $id => $item) {
$product = $productsRepository->find($id);
$quantite = $item['quantite'];
$prixVenteUnitaire = $item['prixVenteUnitaire'];
$remisePourcentage = isset($item['remisePourcentage']) ? (float)$item['remisePourcentage'] : 0.0;
$dataPanier[] = [
"produit" => $product,
"quantite" => $quantite,
"prixVenteUnitaire" => $prixVenteUnitaire,
"remisePourcentage" => $remisePourcentage
];
$total += $prixVenteUnitaire * $quantite * (1 - ($remisePourcentage / 100));
}
// Ajouter l'ID de la vente en session
$session->set("edit_vente_id", $vente->getId());
return $this->render('panier/edit_vente.html.twig', [
'dataPanier' => $dataPanier,
'total' => $total,
'clients' => $clients,
'vente' => $vente,
'selectedClient' => $vente->getClient()
]);
}
#[Route('/update/{id}', name: 'update_vente', methods: ['POST'])]
public function updateVente(
Request $request,
Vente $vente,
SessionInterface $session,
EntityManagerInterface $entityManager,
ProduitRepository $produitRepository,
ProduitStockRepository $produitStockRepository,
Security $security
): Response {
$panier = $session->get('panier', []);
if (empty($panier)) {
$this->addFlash('error', 'Le panier est vide.');
return $this->redirectToRoute('app_vente_show', ['id' => $vente->getId()]);
}
$user = $security->getUser();
$boutique = $vente->getBoutique();
if (!$boutique) {
$this->addFlash('error', 'Aucune boutique associée à cette vente.');
return $this->redirectToRoute('app_vente_show', ['id' => $vente->getId()]);
}
// ÉTAPE 1: Sauvegarder les anciennes quantités avant suppression des détails
$anciensDetailsMap = [];
foreach ($vente->getDetailsVentes() as $detail) {
$anciensDetailsMap[$detail->getProduit()->getId()] = $detail->getQuantiteVendu();
}
// ÉTAPE 2: Démarrer une transaction atomique
$entityManager->beginTransaction();
try {
// ÉTAPE 3: Calculer les différences et vérifier le stock
$ajustementsStock = [];
// Vérifier les produits du nouveau panier
foreach ($panier as $produitId => $item) {
$produit = $produitRepository->find($produitId);
if (!$produit) {
throw new \Exception("Produit introuvable (ID: {$produitId})");
}
$nouvelleQuantite = is_array($item) ? $item['quantite'] : $item;
$ancienneQuantite = isset($anciensDetailsMap[$produitId]) ? $anciensDetailsMap[$produitId] : 0;
$difference = $nouvelleQuantite - $ancienneQuantite;
// Si la quantité a augmenté, vérifier le stock disponible
if ($difference > 0) {
$stockBoutique = $produitStockRepository->findOneBy([
'produit' => $produit,
'boutique' => $boutique
]);
if (!$stockBoutique) {
throw new \Exception("Le produit '{$produit->getNom()}' n'existe pas dans la boutique");
}
if ($stockBoutique->getQuantite() < $difference) {
throw new \Exception("Stock insuffisant pour '{$produit->getNom()}' (Disponible: {$stockBoutique->getQuantite()}, Nécessaire: {$difference})");
}
}
// Stocker l'ajustement à effectuer
if ($difference != 0) {
$ajustementsStock[$produitId] = [
'produit' => $produit,
'difference' => $difference,
'nouvelleQuantite' => $nouvelleQuantite
];
}
}
// ÉTAPE 4: Gérer les produits supprimés (restockage)
foreach ($anciensDetailsMap as $produitId => $ancienneQuantite) {
if (!isset($panier[$produitId])) {
$produit = $produitRepository->find($produitId);
if ($produit) {
// Produit supprimé = restockage complet
$ajustementsStock[$produitId] = [
'produit' => $produit,
'difference' => -$ancienneQuantite,
'nouvelleQuantite' => 0
];
}
}
}
// ÉTAPE 5: Appliquer les ajustements de stock
foreach ($ajustementsStock as $produitId => $ajustement) {
$produit = $ajustement['produit'];
$difference = $ajustement['difference'];
$stockBoutique = $produitStockRepository->findOneBy([
'produit' => $produit,
'boutique' => $boutique
]);
if (!$stockBoutique) {
throw new \Exception("Stock introuvable pour '{$produit->getNom()}' dans la boutique");
}
// Mettre à jour le stock
$ancienStock = $stockBoutique->getQuantite();
$nouveauStock = $ancienStock - $difference; // - car difference est du point de vue de la vente
if ($nouveauStock < 0) {
throw new \Exception("Erreur de calcul du stock pour '{$produit->getNom()}'");
}
$stockBoutique->setQuantite($nouveauStock);
$entityManager->persist($stockBoutique);
// ÉTAPE 6: Créer l'enregistrement de SortieStock pour tracer le mouvement
if ($difference != 0) {
$sortieStock = new SortieStock();
$sortieStock->setProduit($produit);
$sortieStock->setQuantite(abs($difference));
$sortieStock->setDateSortie(new DateTime());
$sortieStock->setBoutique($boutique);
$sortieStock->setUtilisateur($user);
// Récupérer le prix de vente depuis le panier
if (isset($panier[$produitId])) {
$prixVenteUnitaire = is_array($panier[$produitId]) && isset($panier[$produitId]['prixVenteUnitaire'])
? $panier[$produitId]['prixVenteUnitaire']
: $produit->getPrixUnitaire();
$sortieStock->setPrixVente($prixVenteUnitaire);
} else {
// Pour les produits supprimés, utiliser le prix par défaut
$sortieStock->setPrixVente($produit->getPrixUnitaire());
}
$entityManager->persist($sortieStock);
}
}
// Supprimer les anciens détails
foreach ($vente->getDetailsVentes() as $detail) {
$entityManager->remove($detail);
}
// Mettre à jour le client si nécessaire
$clientId = $request->request->get('client_id');
if ($clientId) {
$client = $entityManager->getRepository(Client::class)->find($clientId);
if ($client) {
$vente->setClient($client);
// Mettre à jour aussi le client de la facture associée
$facture = $entityManager->getRepository(Facture::class)->findOneBy(['vente' => $vente]);
if ($facture) {
$facture->setClient($client);
}
}
}
// Créer les nouveaux détails
$montantTotal = 0;
foreach ($panier as $produitId => $item) {
$produit = $produitRepository->find($produitId);
if (!$produit) continue;
$quantite = is_array($item) ? $item['quantite'] : $item;
$prixVenteUnitaire = is_array($item) && isset($item['prixVenteUnitaire'])
? $item['prixVenteUnitaire']
: $produit->getPrixUnitaire();
$remisePourcentage = is_array($item) && isset($item['remisePourcentage'])
? (float)$item['remisePourcentage']
: 0.0;
$prixTotal = (int) round($prixVenteUnitaire * $quantite * (1 - ($remisePourcentage / 100)));
$montantTotal += $prixTotal;
$detailVente = new DetailsVente();
$detailVente->setVente($vente);
$detailVente->setProduit($produit);
$detailVente->setQuantiteVendu($quantite);
$detailVente->setPrixUnitaire($prixVenteUnitaire);
$detailVente->setPrixTotal($prixTotal);
$detailVente->setRemisePourcentage($remisePourcentage);
$entityManager->persist($detailVente);
}
// Mettre à jour le montant total de la facture associée
$facture = $entityManager->getRepository(Facture::class)->findOneBy(['vente' => $vente]);
if ($facture) {
$facture->setMontantTotal($montantTotal);
}
// Valider la transaction complète
$entityManager->flush();
$entityManager->commit();
// Nettoyer la session
$session->remove('panier');
$session->remove('edit_vente_id');
$this->addFlash('success', 'La vente a été mise à jour avec succès !');
return $this->redirectToRoute('app_facture_show', ['id' => $facture->getId()]);
} catch (\Exception $e) {
// ROLLBACK en cas d'erreur
if ($entityManager->getConnection()->isTransactionActive()) {
$entityManager->rollback();
}
$this->addFlash('error', 'Erreur lors de la modification de la vente : ' . $e->getMessage());
return $this->redirectToRoute('app_vente_show', ['id' => $vente->getId()]);
}
}
}