src/Controller/Admin/ProduitDeclinationValueController.php line 1274

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Admin;
  3. use App\Service\ActivityService;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  10. use Symfony\Component\HttpFoundation\JsonResponse;
  11. use App\Entity\ProduitDeclinationValue;
  12. use App\Entity\GroupDeclinationValue;
  13. use App\Entity\Produit;
  14. use App\Repository\ProduitRepository;
  15. use App\Repository\DeclinationRepository;
  16. use App\Repository\ValueDeclinationRepository;
  17. use App\Repository\ProduitDeclinationValueRepository;
  18. use App\Repository\GroupDeclinationValueRepository;
  19. use App\Repository\FileRepository;
  20. use App\Entity\Stock;
  21. use App\Entity\File;
  22. use App\Entity\Warehouse;
  23. use App\Form\UploadFileProduitDecType;
  24. use App\Form\FilterProduitDeclinationType;
  25. use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
  26. use App\Form\FilterDocumentType;
  27. use App\Service\RightService;
  28. use App\Service\StockExchangeIncomingService;
  29. use App\Service\WarehouseContextService;
  30. use Symfony\Component\ExpressionLanguage\Expression;
  31. use Symfony\Component\Security\Http\Attribute\IsGranted;
  32. #[IsGranted(new Expression("is_granted('ROLE_ADMIN') or is_granted('ROLE_RESELLER')"))]
  33. /**
  34.  * @Route("/produit/declination/value")
  35.  */
  36. class ProduitDeclinationValueController extends AbstractController {
  37.     use ImageControllerTrait;
  38.     use AccessTrait;
  39.     private $produitRepository;
  40.     private $declinationRepository;
  41.     private $valueDeclinationRepository;
  42.     private $produitDeclinationValueRepository;
  43.     private $groupDeclinationValueRepository;
  44.     private $fileRepository;
  45.     private $uploaderHelper;
  46.     private $rightService;
  47.     private $activityService;
  48.     private WarehouseContextService $warehouseContextService;
  49.     public function __construct(
  50.             ProduitRepository $produitRepository,
  51.             ValueDeclinationRepository $valueDeclinationRepository,
  52.             DeclinationRepository $declinationRepository,
  53.             ProduitDeclinationValueRepository $produitDeclinationValueRepository,
  54.             FileRepository $fileRepository,
  55.             GroupDeclinationValueRepository $groupDeclinationValueRepository,
  56.             UploaderHelper $uploaderHelper,
  57.             RightService $rightService,
  58.             ActivityService $activityService,
  59.             WarehouseContextService $warehouseContextService
  60.     ) {
  61.         $this->produitRepository $produitRepository;
  62.         $this->valueDeclinationRepository $valueDeclinationRepository;
  63.         $this->declinationRepository $declinationRepository;
  64.         $this->produitDeclinationValueRepository $produitDeclinationValueRepository;
  65.         $this->groupDeclinationValueRepository $groupDeclinationValueRepository;
  66.         $this->fileRepository $fileRepository;
  67.         $this->uploaderHelper $uploaderHelper;
  68.         $this->rightService $rightService;
  69.         $this->activityService $activityService;
  70.         $this->warehouseContextService $warehouseContextService;
  71.     }
  72.     private function assignDefaultWarehouseToStock(Stock $stockEntityManagerInterface $em): void
  73.     {
  74.         $warehouse $this->warehouseContextService->resolveOperationWarehouse(null$this->getUser());
  75.         if ($warehouse instanceof Warehouse) {
  76.             $stock->setWarehouse($warehouse);
  77.         }
  78.     }
  79.     private function slugify(string $text): string
  80.     {
  81.         $text trim($text);
  82.         if ($text === '') {
  83.             return 'produit';
  84.         }
  85.         $text iconv('UTF-8''ASCII//TRANSLIT//IGNORE'$text) ?: $text;
  86.         $text preg_replace('/[^A-Za-z0-9]+/''-'$text) ?? $text;
  87.         $text trim($text'-');
  88.         $text strtolower($text);
  89.         return $text !== '' $text 'produit';
  90.     }
  91.     private function canRecalculateExchangeIncoming(array $rights): bool
  92.     {
  93.         return $this->isGranted('ROLE_SUPER_ADMIN') || \in_array('STOCK_UPDATE'$rightstrue);
  94.     }
  95.     private function buildExchangeIncomingDetailsUrl(?ProduitDeclinationValue $declinaison, ?int $warehouseId null): ?string
  96.     {
  97.         if (!$declinaison instanceof ProduitDeclinationValue) {
  98.             return null;
  99.         }
  100.         $parameters = ['id' => $declinaison->getId()];
  101.         if (($warehouseId ?? 0) > 0) {
  102.             $parameters['warehouse'] = (int) $warehouseId;
  103.         }
  104.         return $this->generateUrl('document_exchange_incoming_declination_modal'$parameters);
  105.     }
  106. /**
  107.  * @Route("/show/{id}", name="produit_declination_value_show", methods={"GET"}, options={"expose"=true})
  108.  */
  109. public function show(ProduitDeclinationValue $declinaisonRequest $request): Response {
  110.     $rights $this->rightService->getAllRights($this->getUser());
  111.     if( !in_array('PRODUIT'$rights)) {
  112.         $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  113.         return $this->redirect($this->generateUrl('index'));
  114.     }
  115.     $formFile $this->createForm(UploadFileProduitDecType::class, $declinaison);
  116.     $form $this->createForm(FilterDocumentType::class);
  117.     return $this->render('@admin/produit_declination_value/fiche_declinaison_produit.html.twig', [
  118.                 'declinaison' => $declinaison,
  119.                 'formFile' => $formFile->createView(),
  120.                 'form' => $form->createView(),
  121.                 'rights' => $rights
  122.     ]);
  123. }
  124. /**
  125.  * @Route("/", name="produit_declination_value_index", methods={"GET"},options = { "expose" =  true})
  126.  */
  127. public function index(Request $request): Response {
  128.     $rights $this->rightService->getAllRights($this->getUser());
  129.     if (!in_array('PRODUIT'$rights)) {
  130.         $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  131.         return $this->redirect($this->generateUrl('index'));
  132.     }
  133.     $form $this->createForm(FilterProduitDeclinationType::class);
  134.     // ⚠️ on ne touche pas à l’existant
  135.     $declinations        $this->declinationRepository->findAll();
  136.     // ✅ nouvelle liste pour le filtre uniquement
  137.     $declinationsFilter  $this->declinationRepository->findAllForFilter();
  138.     return $this->render('@admin/produit_declination_value/list_declinaisons_produits.html.twig', [
  139.         'form'               => $form->createView(),
  140.         'declinations'       => $declinations,        // reste disponible ailleurs
  141.         'declinationsFilter' => $declinationsFilter,  // utilisé par le filtre UI
  142.         'rights'             => $rights,
  143.         'canRecalculateExchangeIncoming' => $this->canRecalculateExchangeIncoming($rights),
  144.     ]);
  145. }
  146. /**
  147.  * @Route("/add", name="add_produit_declination_value", methods={"GET","POST"}, options={"expose"=true})
  148.  */
  149. public function add(Request $requestEntityManagerInterface $em): JsonResponse
  150. {
  151.     try {
  152.         $listDeclinations = [];
  153.         $valueDeclinations = (array) $request->get('value_declination', []);
  154.         foreach ($valueDeclinations as $item) {
  155.             $status = (string) ($item['status'] ?? 'new');
  156.             // Ignore les lignes supprimées
  157.             if ($status === 'delete') {
  158.                 continue;
  159.             }
  160.             $declinations = (array) ($item['declinations'] ?? []);
  161.             if (empty($declinations)) {
  162.                 continue;
  163.             }
  164.             $listDeclinations[] = $declinations;
  165.         }
  166.         $duplicate $this->checkDuplicate($listDeclinations);
  167.         if ($duplicate) {
  168.             return new JsonResponse(["success" => false"message" => $duplicate]);
  169.         }
  170.         $this->createEntity($request$em);
  171.         $request->getSession()->getFlashBag()->add('success'"Les changements ont été enregistrés");
  172.         return new JsonResponse([
  173.             "success" => true,
  174.             "path" => $this->generateUrl('produit_show', ['id' => $request->get('id_produit')])
  175.         ]);
  176.     } catch (\RuntimeException $e) {
  177.         return new JsonResponse([
  178.             "success" => false,
  179.             "message" => $e->getMessage()
  180.         ], 422);
  181.     } catch (\Throwable $e) {
  182.         return new JsonResponse([
  183.             "success" => false,
  184.             "message" => "Erreur technique lors de l'enregistrement."
  185.         ], 500);
  186.     }
  187. }
  188. public function createEntity($request,EntityManagerInterface $em) {
  189.     foreach ($request->get('value_declination') as $item) {
  190.         if( $item['status'] === 'new') {
  191.             $entity = new ProduitDeclinationValue();
  192.             $entity->setBuyingPriceTtc($item['buyingPriceTtc'])
  193.                     ->setDescription($item['description'])
  194.                     ->setName($item['name'])
  195.                     ->setPriceHt($item['price_ht'])
  196.                     ->setReference($item['reference'])
  197.                     ->setCreatedAt(new \DateTime('now'));
  198.             $stock = new Stock();
  199.             if( $request->get('id_produit')) {
  200.                 $produit $this->produitRepository->find($request->get('id_produit'));
  201.                 $entity->setProduit($produit);
  202.                 $stock->setProduit($produit);
  203.             }
  204.             $stock->setQtReserved(0)
  205.                     ->setQtStock(0)
  206.                     ->setDeclinationProduit($entity);
  207.             $this->assignDefaultWarehouseToStock($stock$em);
  208.             $em->persist($stock);
  209.             $em->persist($entity);
  210.             if( $item['declinations']) {
  211.                 foreach ($item['declinations'] as $key => $value) {
  212.                     $group = new GroupDeclinationValue();
  213.                     if( $value && $key) {
  214.                         $valueDeclination $this->valueDeclinationRepository->find($value);
  215.                         $declination $this->declinationRepository->find($key);
  216.                         $group->setValue($valueDeclination);
  217.                         $group->setDeclination($declination);
  218.                     }
  219.                     $group->setProduitDeclination($entity);
  220.                     $em->persist($group);
  221.                 }
  222.             }
  223.         } elseif( $item['status'] === 'update') {
  224.             $entity $this->produitDeclinationValueRepository->find($item['id']);
  225.             $entity->setBuyingPriceTtc($item['buyingPriceTtc'])
  226.                     ->setDescription($item['description'])
  227.                     ->setName($item['name'])
  228.                     ->setPriceHt($item['price_ht'])
  229.                     ->setReference($item['reference']);
  230.         } elseif (isset($item['id']) && $item['status'] === 'delete') {
  231.             $entity $this->produitDeclinationValueRepository->find($item['id']);
  232.             if (!$entity) {
  233.                 continue;
  234.             }
  235.             // Interdire suppression si utilisée dans des documents
  236.             if (count($entity->getDocumentDeclinationProduits()) > 0) {
  237.                 throw new \RuntimeException(sprintf(
  238.                     "La déclinaison '%s' (%s) est utilisée dans des documents. Suppression impossible.",
  239.                     $entity->getName(),
  240.                     $entity->getReference()
  241.                 ));
  242.             }
  243.             foreach ($entity->getGroupDeclinationValues() as $group) {
  244.                 $em->remove($group);
  245.             }
  246.             foreach ($entity->getStocks() as $stock) {
  247.                 foreach ($stock->getActivities() as $activity) {
  248.                     $em->remove($activity);
  249.                 }
  250.                 $em->remove($stock);
  251.             }
  252.             foreach ($entity->getActivities() as $activity) {
  253.                 $em->remove($activity);
  254.             }
  255.             $em->remove($entity);
  256.         }
  257.     }
  258.     $em->flush();
  259. }
  260. private function comboKey(array $combo): string
  261. {
  262.     $norm = [];
  263.     foreach ($combo as $declinationId => $valueId) {
  264.         $d = (int) $declinationId;
  265.         $v = (int) $valueId;
  266.         if ($d && $v 0) {
  267.             $norm[$d] = $v;
  268.         }
  269.     }
  270.     ksort($norm);
  271.     return json_encode($norm);
  272. }
  273. private function normalizeDeclinationGroup(array $group): array
  274. {
  275.     $normalized = [];
  276.     foreach ($group as $declinationKey => $valueId) {
  277.         $vId = (int) $valueId;
  278.         if ($vId <= 0) {
  279.             continue;
  280.         }
  281.         // clé peut être un id (ex: "12") ou un nom (ex: "Couleur")
  282.         $dId ctype_digit((string) $declinationKey) ? (int) $declinationKey 0;
  283.         if ($dId <= 0) {
  284.             $decl $this->declinationRepository->findOneBy(['name' => (string) $declinationKey]);
  285.             if ($decl) {
  286.                 $dId = (int) $decl->getId();
  287.             }
  288.         }
  289.         if ($dId 0) {
  290.             $normalized[$dId] = $vId;
  291.         }
  292.     }
  293.     ksort($normalized);
  294.     return $normalized;
  295. }
  296. public function checkDuplicate(array $listDeclinations): ?string
  297. {
  298.     $seen = [];
  299.     foreach ($listDeclinations as $group) {
  300.         if (!is_array($group)) {
  301.             continue;
  302.         }
  303.         $normalized $this->normalizeDeclinationGroup($group);
  304.         // IMPORTANT: on compare seulement une vraie combinaison (au moins 2 dimensions)
  305.         if (count($normalized) < 2) {
  306.             continue;
  307.         }
  308.         $key json_encode($normalized);
  309.         if (isset($seen[$key])) {
  310.             $parts = [];
  311.             foreach ($normalized as $declinationId => $valueId) {
  312.                 $decl $this->declinationRepository->find((int) $declinationId);
  313.                 $val  $this->valueDeclinationRepository->find((int) $valueId);
  314.                 if ($decl && $val) {
  315.                     $parts[] = $decl->getName() . ': ' $val->getName();
  316.                 }
  317.             }
  318.             $label = !empty($parts) ? implode(' | '$parts) : 'Cette combinaison';
  319.             return $label " existe déjà, veuillez sélectionner une combinaison différente.";
  320.         }
  321.         $seen[$key] = true;
  322.     }
  323.     return null;
  324. }
  325. public function return_dup($arr) {
  326.     $dups = array();
  327.     $temp $arr;
  328.     foreach ($arr as $key => $item) {
  329.         unset($temp[$key]);
  330.         if( in_array($item$temp)) {
  331.             $dups[] = $item;
  332.         }
  333.     }
  334.     return $dups;
  335. }
  336. /**
  337.  * @Route("/listData/{idProduit}", name="list_produit_declination_value", methods={"GET","POST"}, options = { "expose" =  true})
  338.  */
  339. public function listData($idProduitRequest $request): JsonResponse
  340. {
  341.     $draw   = (int) $request->get('draw'1);
  342.     $start  = (int) $request->get('start'0);
  343.     $length = (int) $request->get('length'20);
  344.     $page  = (int) floor($start max(1$length));
  345.     $limit max(1$length);
  346.     $reference = (string) $request->get('reference''');
  347.     $declinations $request->get('declinations', []);
  348.     if (is_string($declinations)) {
  349.         $decoded json_decode($declinationstrue);
  350.         $declinations = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
  351.     }
  352.     $declinations array_combine(
  353.         array_map('intval'array_keys($declinations)),
  354.         array_map(static function ($v) { return is_array($v) ? array_map('intval'$v) : (int) $v; }, array_values($declinations))
  355.     );
  356.     $entities $this->produitDeclinationValueRepository->findProduitGroup($page$limit, (int) $idProduit$reference$declinations);
  357.     $count    = (int) $this->produitDeclinationValueRepository->countProduitGroup((int) $idProduit$reference$declinations);
  358.     $data = [];
  359.     foreach ($entities as $entity) {
  360.         $baseTtc  = (float) $entity->getProduit()->getPriceTtc();
  361.         $finalTtc $baseTtc;
  362.         $promotion $entity->getProduit()->getPromotion();
  363.         if ($promotion) {
  364.             $now = new \DateTimeImmutable('now');
  365.             if ($promotion->getStartAt() <= $now && $promotion->getEndAt() >= $now) {
  366.                 $promoType  $promotion->getDiscountType();
  367.                 $promoValue = (float) $promotion->getDiscountValue();
  368.                 if ($promoType === 'percent') {
  369.                     $finalTtc round($baseTtc * ($promoValue 100), 3);
  370.                 } else {
  371.                     $finalTtc round(max(0$baseTtc $promoValue), 3);
  372.                 }
  373.             }
  374.         }
  375.         $picture $entity->getPicture()->filter(static fn($pic) => $pic->getIsSelected() == 1)->first();
  376.         if (!$picture) {
  377.             $picture $entity->getPicture()->first();
  378.         }
  379.         $imageName $picture $picture->getImageName() : 'no-image-50px.png';
  380.         $imageSrc  $request->getSchemeAndHttpHost() . '/images/' $imageName;
  381.         $imageHtml '<img src="' htmlspecialchars($imageSrcENT_QUOTES ENT_SUBSTITUTE'UTF-8') . '" alt="" style="width:44px;height:44px;object-fit:cover;border-radius:6px;">';
  382.         $row = [
  383.             'id'             => $entity->getId(),
  384.             'name'           => $entity->getName(),
  385.             'reference'      => $entity->getReference(),
  386.             'image'          => $imageHtml,
  387.             'quick_order_url' => $this->generateUrl('product_quick_order', [
  388.                 'id' => $entity->getProduit()->getId(),
  389.                 'name' => $entity->getProduit()->getName() ? $this->slugify($entity->getProduit()->getName()) : 'produit',
  390.             ], UrlGeneratorInterface::ABSOLUTE_URL),
  391.             'buyingPriceTtc' => ($this->isGranted('ROLE_SUPER_ADMIN')) ? number_format((float) $entity->getBuyingPriceTtc(), 3) : '',
  392.             'price_ttc'      => number_format($finalTtc3),
  393.             'actions'        =>
  394.                 '<button class="btn btn-sm btn-clean text-info" id="updateDeclination" data-id="' . (int) $entity->getId() . '" data-bs-toggle="tooltip" title="Modifier"><i class="fas fa-edit"></i></button>'
  395.                 '<a href="' $this->generateUrl('produit_declination_value_show', ['id' => $entity->getId()]) . '" class="btn btn-sm btn-clean text-secondary" data-bs-toggle="tooltip" title="Afficher" target="_blank" rel="noopener"><i class="fa fa-clipboard"></i></a>',
  396.         ];
  397.         $groups $entity->getGroupDeclinationValues()->toArray();
  398.         usort($groups, static function ($a$b) {
  399.             return (int) $a->getDeclination()->getPosition() <=> (int) $b->getDeclination()->getPosition();
  400.         });
  401.        foreach ($groups as $group) {
  402.             $key   strtolower((string) $group->getDeclination()->getName());
  403.             $value = (string) $group->getValue()->getName();
  404.             $row[$key] = $value;
  405.         }
  406.         $data[] = $row;
  407.     }
  408.     return $this->json([
  409.         'draw'            => $draw,
  410.         'recordsTotal'    => $count,
  411.         'recordsFiltered' => $count,
  412.         'data'            => $data,
  413.     ]);
  414. }
  415. /**
  416.  * @Route("/columns/{idProduit}", name="columns_produit_declination_value", methods={"GET","POST"}, options = { "expose" =  true})
  417.  */
  418. public function listColumns($idProduit): JsonResponse
  419. {
  420.     $produit $this->produitRepository->find($idProduit);
  421.     $output = [
  422.         [
  423.             'field'    => 'reference',
  424.             'sortable' => 'asc',
  425.             'width'    => 140,
  426.             'title'    => 'Référence'
  427.         ],
  428.         [
  429.             'field'    => 'name',
  430.             'sortable' => 'asc',
  431.             'title'    => 'Nom commercial',
  432.             'width'    => 250
  433.         ]
  434.     ];
  435.     // CORRECTION CLÉ : tri par position métier
  436.     $declinations $produit->getDeclinations()->toArray();
  437.     usort($declinations, static function ($a$b) {
  438.         return $a->getPosition() <=> $b->getPosition();
  439.     });
  440.     foreach ($declinations as $entity) {
  441.         $output[] = [
  442.             'field'     => strtolower($entity->getName()),
  443.             'width'     => 60,
  444.             'textAlign' => 'center',
  445.             'title'     => $entity->getName()
  446.         ];
  447.     }
  448.     if ($this->isGranted('ROLE_SUPER_ADMIN')) {
  449.         $output[] = [
  450.             'field'     => 'buyingPriceTtc',
  451.             'width'     => 80,
  452.             'textAlign' => 'center',
  453.             'title'     => 'Prix Achat <small>(TTC)</small>'
  454.         ];
  455.     }
  456.     $output[] = [
  457.         'field'     => 'price_ttc',
  458.         'width'     => 80,
  459.         'textAlign' => 'center',
  460.         'title'     => 'Prix Vente <small>(TTC)</small>'
  461.     ];
  462.     $output[] = [
  463.         'field'     => 'actions',
  464.         'width'     => 70,
  465.         'textAlign' => 'center',
  466.         'title'     => 'Actions'
  467.     ];
  468.     return new JsonResponse($output);
  469. }
  470. /**
  471.  * @Route("/edit/{id}", name="produit_declination_value_edit", methods={"GET","POST"}, options={"expose"=true})
  472.  */
  473. public function edit(Request $requestProduitDeclinationValue $produitDeclinationValueEntityManagerInterface $em): Response
  474. {
  475.     if ($request->isMethod('POST')) {
  476.         $item = (array) $request->request->get('produit_declination_value', []);
  477.         $reference trim((string) ($item['reference'] ?? ''));
  478.         $name trim((string) ($item['name'] ?? ''));
  479.         $description array_key_exists('description'$item) ? (string) $item['description'] : null;
  480.         $buyingPriceHt max(0, (float) ($item['buyingPriceHt'] ?? 0));
  481.         $priceHt max(0, (float) ($item['priceHt'] ?? 0));
  482.         $tvaRate 0.0;
  483.         if ($produitDeclinationValue->getProduit() && $produitDeclinationValue->getProduit()->getTva()) {
  484.             $tvaRate = (float) $produitDeclinationValue->getProduit()->getTva()->getNumber();
  485.         }
  486.         $buyingPriceTtc $buyingPriceHt * (+ ($tvaRate 100));
  487.         $produitDeclinationValue
  488.             ->setReference($reference)
  489.             ->setName($name)
  490.             ->setBarcode(isset($item['barcode']) && trim((string) $item['barcode']) !== '' trim((string) $item['barcode']) : null)
  491.             ->setDescription($description)
  492.             ->setBuyingPriceHt(round($buyingPriceHt3))
  493.             ->setBuyingPriceTtc(round($buyingPriceTtc3))
  494.             ->setPriceHt(round($priceHt3));
  495.         $em->flush();
  496.         $request->getSession()->getFlashBag()->add('success''Déclinaison modifiée avec succés');
  497.         return $this->redirectToRoute('produit_declination_value_show', [
  498.             'id' => $produitDeclinationValue->getId(),
  499.             'tab' => 'content_pricing_promo'
  500.         ]);
  501.     }
  502.     return $this->render('@admin/produit_declination_value/_formModalEditDeclinaison.html.twig', [
  503.         'produitDeclinationValue' => $produitDeclinationValue
  504.     ]);
  505. }
  506. /**
  507.  * @Route("/multi/add/{id}", name="produit_declination_value_multi_add", methods={"GET","POST"}, options={"expose"=true})
  508.  */
  509. public function multiAdd(Request $requestProduit $produitEntityManagerInterface $em)
  510. {
  511.     if ($request->isMethod('POST')) {
  512.         try {
  513.             $multiAdd = (array) $request->request->get('multi_add', []);
  514.             $declinationFixeRaw  = (array) ($multiAdd['declinationFixe'] ?? []);
  515.             $declinationMultiRaw = (array) ($multiAdd['declinationMulti'] ?? []);
  516.             $normalizeDeclinationMap = static function (array $raw): array {
  517.                 $out = [];
  518.                 foreach ($raw as $declinationId => $values) {
  519.                     $declinationId = (int) $declinationId;
  520.                     if ($declinationId <= 0) {
  521.                         continue;
  522.                     }
  523.                     $bucket = [];
  524.                     if (is_array($values)) {
  525.                         array_walk_recursive($values, static function ($v) use (&$bucket) {
  526.                             $iv = (int) $v;
  527.                             if ($iv 0) {
  528.                                 $bucket[] = $iv;
  529.                             }
  530.                         });
  531.                     } else {
  532.                         $iv = (int) $values;
  533.                         if ($iv 0) {
  534.                             $bucket[] = $iv;
  535.                         }
  536.                     }
  537.                     $bucket array_values(array_unique($bucket));
  538.                     if (!empty($bucket)) {
  539.                         $out[$declinationId] = $bucket;
  540.                     }
  541.                 }
  542.                 return $out;
  543.             };
  544.             $declinationFixe  $normalizeDeclinationMap($declinationFixeRaw);
  545.             $declinationMulti $normalizeDeclinationMap($declinationMultiRaw);
  546.             if (empty($declinationFixe) || empty($declinationMulti)) {
  547.                 return new JsonResponse([
  548.                     'success' => false,
  549.                     'message' => 'Veuillez sélectionner au moins une déclinaison fixe et une déclinaison multiple.'
  550.                 ], 422);
  551.             }
  552.             $declinationPositions = [];
  553.             foreach ($produit->getDeclinations() as $d) {
  554.                 $declinationPositions[(int) $d->getId()] = (int) $d->getPosition();
  555.             }
  556.             // 1) Vérifier que fixe et multiple ne contiennent pas la même déclinaison
  557.             $dupDeclinations array_intersect(array_keys($declinationFixe), array_keys($declinationMulti));
  558.             if (!empty($dupDeclinations)) {
  559.                 return new JsonResponse([
  560.                     'success' => false,
  561.                     'message' => 'Une même déclinaison ne peut pas être à la fois fixe et multiple.'
  562.                 ], 422);
  563.             }
  564.             // 2) Fusion sans perdre les clés (IDs déclinaisons)
  565.             $dimensions = [];
  566.             foreach ($declinationFixe as $declinationId => $valueIds) {
  567.                 $dimensions[(int) $declinationId] = array_values(array_unique(array_map('intval', (array) $valueIds)));
  568.             }
  569.             foreach ($declinationMulti as $declinationId => $valueIds) {
  570.                 $dimensions[(int) $declinationId] = array_values(array_unique(array_map('intval', (array) $valueIds)));
  571.             }
  572.             // Nettoyage final
  573.             foreach ($dimensions as $dId => $vals) {
  574.                 $vals array_values(array_filter($vals, static fn($v) => (int)$v 0));
  575.                 if (empty($vals)) {
  576.                     unset($dimensions[$dId]);
  577.                 } else {
  578.                     $dimensions[$dId] = $vals;
  579.                 }
  580.             }
  581.             if (empty($dimensions)) {
  582.                 return new JsonResponse([
  583.                     'success' => false,
  584.                     'message' => 'Aucune valeur de déclinaison sélectionnée.'
  585.                 ], 422);
  586.             }
  587.             // Tri par position métier
  588.             uksort($dimensions, static function ($a$b) use ($declinationPositions) {
  589.                 $pa $declinationPositions[(int) $a] ?? 999;
  590.                 $pb $declinationPositions[(int) $b] ?? 999;
  591.                 return $pa <=> $pb;
  592.             });
  593.             $combinations = [[]];
  594.             foreach ($dimensions as $declinationId => $valueIds) {
  595.                 $next = [];
  596.                 foreach ($combinations as $base) {
  597.                     foreach ($valueIds as $valueId) {
  598.                         $row $base;
  599.                         $row[(int) $declinationId] = (int) $valueId;
  600.                         $next[] = $row;
  601.                     }
  602.                 }
  603.                 $combinations $next;
  604.             }
  605.             if (empty($combinations)) {
  606.                 return new JsonResponse([
  607.                     'success' => false,
  608.                     'message' => 'Aucune combinaison générée.'
  609.                 ], 422);
  610.             }
  611.             $expectedDeclinationIds array_map('intval'array_keys($dimensions));
  612.             sort($expectedDeclinationIds);
  613.             $comboKey = static function (array $combo): string {
  614.                 $norm = [];
  615.                 foreach ($combo as $dId => $vId) {
  616.                     $d = (int) $dId;
  617.                     $v = (int) $vId;
  618.                     if ($d && $v 0) {
  619.                         $norm[$d] = $v;
  620.                     }
  621.                 }
  622.                 ksort($norm);
  623.                 return json_encode($norm);
  624.             };
  625.             $comboLabel = function (array $combo): string {
  626.                 $parts = [];
  627.                 foreach ($combo as $declinationId => $valueId) {
  628.                     $decl $this->declinationRepository->find((int) $declinationId);
  629.                     $val  $this->valueDeclinationRepository->find((int) $valueId);
  630.                     if ($decl && $val) {
  631.                         $parts[] = $decl->getName() . ': ' $val->getName();
  632.                     }
  633.                 }
  634.                 return !empty($parts) ? implode(' | '$parts) : 'combinaison invalide';
  635.             };
  636.             $existing $this->listGroupDeclination($produit->getProduitDeclinationValues());
  637.             $seen = [];
  638.             foreach ($existing as $ex) {
  639.                 $seen[$comboKey($ex)] = true;
  640.             }
  641.             $selectedImages $multiAdd['images'] ?? [];
  642.             if (!is_array($selectedImages)) {
  643.                 $selectedImages = [$selectedImages];
  644.             }
  645.             $selectedImages array_values(array_unique(array_filter(array_map('intval'$selectedImages))));
  646.             $addedCount 0;
  647.             $skippedLabels = [];
  648.             foreach ($combinations as $combo) {
  649.                 $comboDeclIds array_map('intval'array_keys($combo));
  650.                 sort($comboDeclIds);
  651.                 // sécurité: combo complète (fixe + multiple)
  652.                 if ($comboDeclIds !== $expectedDeclinationIds) {
  653.                     $skippedLabels[] = $comboLabel($combo);
  654.                     continue;
  655.                 }
  656.                 $key $comboKey($combo);
  657.                 if (isset($seen[$key])) {
  658.                     $skippedLabels[] = $comboLabel($combo);
  659.                     continue;
  660.                 }
  661.                 // Résolution stricte: si une dimension est invalide, on ignore la ligne
  662.                 $resolved = [];
  663.                 $invalid false;
  664.                 foreach ($combo as $declinationId => $valueId) {
  665.                     $valueDeclination $this->valueDeclinationRepository->find((int) $valueId);
  666.                     $declination      $this->declinationRepository->find((int) $declinationId);
  667.                     if (!$valueDeclination || !$declination) {
  668.                         $invalid true;
  669.                         break;
  670.                     }
  671.                     $resolved[] = [
  672.                         'declination' => $declination,
  673.                         'value'       => $valueDeclination,
  674.                         'position'    => (int) $declination->getPosition(),
  675.                         'name'        => (string) $valueDeclination->getName(),
  676.                     ];
  677.                 }
  678.                 if ($invalid || count($resolved) !== count($expectedDeclinationIds)) {
  679.                     $skippedLabels[] = $comboLabel($combo);
  680.                     continue;
  681.                 }
  682.                 usort($resolved, static fn($a$b) => $a['position'] <=> $b['position']);
  683.                 $entity = new ProduitDeclinationValue();
  684.                 $entity->setBuyingPriceTtc($produit->getBuyingPriceTtc())
  685.                     ->setDescription($produit->getDescription())
  686.                     ->setPriceHt($produit->getPriceHt())
  687.                     ->setCreatedAt(new \DateTime('now'))
  688.                     ->setProduit($produit);
  689.                 $reference $produit->getReference();
  690.                 $name      $produit->getName();
  691.                 foreach ($resolved as $item) {
  692.                     $group = new GroupDeclinationValue();
  693.                     $group->setValue($item['value']);
  694.                     $group->setDeclination($item['declination']);
  695.                     $group->setProduitDeclination($entity);
  696.                     $em->persist($group);
  697.                     $reference .= '-' mb_strtolower($item['name']);
  698.                     $name      .= ' ' mb_strtolower($item['name']);
  699.                 }
  700.                 $entity->setReference($reference)->setName($name);
  701.                 foreach ($selectedImages as $imgId) {
  702.                     $imageSelected $this->fileRepository->find((int) $imgId);
  703.                     if ($imageSelected) {
  704.                         $entity->addPicture($imageSelected);
  705.                     }
  706.                 }
  707.                 $stock = new Stock();
  708.                 $stock->setQtReserved(0)
  709.                     ->setProduit($produit)
  710.                     ->setQtStock(0)
  711.                     ->setDeclinationProduit($entity);
  712.                 $this->assignDefaultWarehouseToStock($stock$em);
  713.                 $em->persist($stock);
  714.                 $em->persist($entity);
  715.                 $seen[$key] = true;
  716.                 $addedCount++;
  717.             }
  718.             $em->flush();
  719.             $skippedLabels array_values(array_unique(array_filter($skippedLabels)));
  720.             $message $addedCount ' déclinaison(s) ajoutée(s).';
  721.             if (!empty($skippedLabels)) {
  722.                 $message .= ' Ignorées (déjà existantes/invalides): ' implode(' ; '$skippedLabels);
  723.             }
  724.             return new JsonResponse([
  725.                 'success' => true,
  726.                 'message' => $message
  727.             ]);
  728.         } catch (\Throwable $e) {
  729.             return new JsonResponse([
  730.                 'success' => false,
  731.                 'message' => 'Erreur technique: ' $e->getMessage()
  732.             ], 500);
  733.         }
  734.     }
  735.     $fixe = [];
  736.     $multi = [];
  737.     $declinations $produit->getDeclinations()->toArray();
  738.     usort($declinations, static function ($a$b) {
  739.         return (int) $a->getPosition() <=> (int) $b->getPosition();
  740.     });
  741.     // Par défaut:
  742.     // - position la plus petite => fixe
  743.     // - toutes les autres => multiples
  744.     if (!empty($declinations)) {
  745.         $fixe[] = $declinations[0];
  746.         for ($i 1$i count($declinations); $i++) {
  747.             $multi[] = $declinations[$i];
  748.         }
  749.     }
  750.     return $this->render('admin/produit_declination_value/multi-add.html.twig', [
  751.         'produit' => $produit,
  752.         'fixe'    => $fixe,
  753.         'multi'   => $multi,
  754.     ]);
  755. }
  756. /**
  757.  * @Route("/modal/{id}", name="produit_declinaisons_modal_refresh", methods={"GET"}, options={"expose"=true})
  758.  */
  759. public function refreshDeclinaisonsModal(Produit $produit): Response
  760. {
  761.     $rights $this->rightService->getAllRights($this->getUser());
  762.     return $this->render('@admin/includes/modals/_produit_declinaisons_modal.html.twig', [
  763.         'produit' => $produit,
  764.         'rights'  => $rights,
  765.     ]);
  766. }
  767. /**
  768.  * @Route("/threshold/{id}", name="produit_declination_threshold_update", methods={"POST"}, options={"expose"=true})
  769.  */
  770. public function updateThreshold(ProduitDeclinationValue $decliRequest $requestEntityManagerInterface $em): JsonResponse
  771. {
  772.     $threshold max(0, (int) $request->request->get('threshold'0));
  773.     $decli->setAlertStockMin($threshold);
  774.     $em->flush();
  775.     return new JsonResponse([
  776.         'success' => true,
  777.         'message' => 'Seuil mis à jour.',
  778.         'threshold' => $threshold
  779.     ]);
  780. }
  781. /**
  782.  * @Route("/threshold/bulk/{produit}", name="produit_declination_threshold_bulk_update", methods={"POST"}, options={"expose"=true})
  783.  */
  784. public function updateThresholdBulk(Produit $produitRequest $requestEntityManagerInterface $em): JsonResponse
  785. {
  786.     $threshold max(0, (int) $request->request->get('threshold'0));
  787.     foreach ($produit->getProduitDeclinationValues() as $decli) {
  788.         $decli->setAlertStockMin($threshold);
  789.     }
  790.     $em->flush();
  791.     return new JsonResponse([
  792.         'success' => true,
  793.         'message' => 'Seuil appliqué à toutes les déclinaisons.',
  794.         'threshold' => $threshold
  795.     ]);
  796. }
  797. /**
  798.  * @Route("/multi/select", name="select_multi_add", methods={"GET","POST"}, options={"expose"=true})
  799.  */
  800. public function selectMulti(Request $request): Response
  801. {
  802.     $multiAdd = (array) $request->request->get('multi_add', []);
  803.     $fixeIds  array_values(array_filter(array_map('intval', (array) ($multiAdd['fixe'] ?? []))));
  804.     $multiIds array_values(array_filter(array_map('intval', (array) ($multiAdd['multiple'] ?? []))));
  805.     $fixe = [];
  806.     $multi = [];
  807.     foreach ($fixeIds as $id) {
  808.         $declination $this->declinationRepository->find($id);
  809.         if ($declination) {
  810.             $fixe[] = $declination;
  811.         }
  812.     }
  813.     foreach ($multiIds as $id) {
  814.         $declination $this->declinationRepository->find($id);
  815.         if ($declination) {
  816.             $multi[] = $declination;
  817.         }
  818.     }
  819.     return $this->render('@admin/produit_declination_value/formule.html.twig', [
  820.         'fixe'  => $fixe,
  821.         'multi' => $multi,
  822.     ]);
  823. }
  824. public function listGroupDeclination($declinations) {
  825.     $list = [];
  826.     foreach ($declinations as $declination) {
  827.         $listGroup = [];
  828.         foreach ($declination->getGroupDeclinationValues() as $group) {
  829.             $listGroup[$group->getDeclination()->getId()] = (string) $group->getValue()->getId();
  830.         }
  831.         $list[] = $listGroup;
  832.     }
  833.     return $list;
  834. }
  835. public function sortAssociativeArrayByKey($array$key$direction) {
  836.     switch ($direction) {
  837.         case "ASC":
  838.             usort($array, function ($first$second) use ($key) {
  839.                 return $first[$key] <=> $second[$key];
  840.             });
  841.             break;
  842.         case "DESC":
  843.             usort($array, function ($first$second) use ($key) {
  844.                 return $second[$key] <=> $first[$key];
  845.             });
  846.             break;
  847.         default:
  848.             break;
  849.     }
  850.     return $array;
  851. }
  852. /**
  853.  * @Route("/search", name="search_item", methods={"GET","POST"}, options = { "expose" =  true})
  854.  */
  855. public function searchItem(Request $request,EntityManagerInterface $em) {
  856.     $types=$request->get("type")??["declinaison"];
  857.     $entitiesproduit=$entities=[];
  858.     $hideSupplierReceptionPrices $request->get('document_category') === 'fournisseur'
  859.         && !$this->currentUserHasRight('DOCUMENT_SUPPLIER_RECEPTION_PRICE');
  860.     $isAvailable filter_var($request->get('is_available'false), FILTER_VALIDATE_BOOLEANFILTER_NULL_ON_FAILURE);
  861.     $isAvailable $isAvailable === true;
  862.     $restrictSupplier filter_var($request->get('restrict_supplier'false), FILTER_VALIDATE_BOOLEAN);
  863.     $supplierId $restrictSupplier ? (int) $request->get('supplier_id'0) : 0;
  864.     if($types){
  865.         if(in_array("declinaison",$types)){
  866.             $entities $this->produitDeclinationValueRepository->searchItem(
  867.                 $request->get('query'),
  868.                 $isAvailable,
  869.                 count($types) == 12,
  870.                 false,
  871.                 $supplierId $supplierId null
  872.             );
  873.             foreach ($entities as &$entity) {
  874.                 $qtReserved=$quantity 0;
  875.                 $incomingQuantity 0;
  876.                 $incomingDateEstimated null;
  877.                 $exchangeIncomingQuantity 0;
  878.                 $entity['type'] = 'produitDeclination';
  879.                 $declinaison $this->produitDeclinationValueRepository->find($entity['id']);
  880.                 $produit $declinaison $declinaison->getProduit() : null;
  881.                 $stocks $em->getRepository(Stock::class)->findByDeclinationProduit($entity['id']);
  882.                 foreach ($stocks as $stock) {
  883.                     $quantity $quantity + ($stock->getQtStock() - $stock->getQtReserved());
  884.                     $qtReserved+=$stock->getQtReserved();
  885.                     $incomingQuantity += (int) $stock->getIncomingQuantity();
  886.                     $exchangeIncomingQuantity += (int) $stock->getExchangeIncomingQuantity();
  887.                     $candidateIncomingDate $stock->getIncomingDateEstimated();
  888.                     if ($candidateIncomingDate && ($incomingDateEstimated === null || $candidateIncomingDate $incomingDateEstimated)) {
  889.                         $incomingDateEstimated $candidateIncomingDate;
  890.                     }
  891.                 }
  892.                 $entity['quantity'] =( $quantity<=3  or $this->isGranted('ROLE_SUPER_ADMIN') or $this->currentUserHasRight('STOCK_SEE_REAL_STOCK') )?$quantity:"3<sup>+</sup>";
  893.                 $entity['qtReserved'] =$qtReserved;
  894.                 $entity['incomingQuantity'] = $incomingQuantity;
  895.                 $entity['incomingRemainingQuantity'] = $declinaison $declinaison->getQtyIncomingRemaining() : $incomingQuantity;
  896.                 $entity['incomingDateEstimated'] = $incomingDateEstimated?->format('d/m/Y H:i');
  897.                 $entity['exchangeIncomingQuantity'] = $exchangeIncomingQuantity;
  898.                 $entity['exchangeIncomingDetailsUrl'] = $exchangeIncomingQuantity 0
  899.                     $this->buildExchangeIncomingDetailsUrl($declinaison)
  900.                     : null;
  901.                 $tvaEntity $produit $produit->getTva() : null;
  902.                 $entity['tva_id'] = $tvaEntity $tvaEntity->getId() : null;
  903.                 $entity['tva'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
  904.                 $entity['tva_number'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
  905.                 $promotion=$produit->getPromotion();
  906.                 $entity['value'] = null;
  907.                 $entity['promotionType'] = null;
  908.                 $in_promo=false;
  909.                 $entity['price_ttc'] =$entity['price_ttc_without_promo'] = $produit->getPriceTtc();
  910.                 if( $promotion) {
  911.                     $date = new \DateTime('now');
  912.                     if( $promotion->getStartAt() <= $date && $promotion->getEndAt() >= $date) {
  913.                         $entity['value'] = $promotion->getDiscountValue();
  914.                         $entity['promotionType'] = $promotion->getDiscountType();
  915.                         $entity['price_ttc'] =($promotion->getDiscountType() == 'percent')?
  916.                             $produit->getPriceTtc() - ((($produit->getPriceTtc() / 100) * $promotion->getDiscountValue()))
  917.                             : $produit->getPriceTtc() - $promotion->getDiscountValue();
  918.                         $in_promo=true;
  919.                     }
  920.                 }
  921.                 $entity['in_promo'] = $in_promo;
  922.                 $entity['material'] = $produit $produit->getMaterial() : null;
  923.                 $picture $this->getPicture($this->produitDeclinationValueRepository->find($entity['id']), $request);
  924.                 $entity['picture'] = $picture;
  925.                 if ($hideSupplierReceptionPrices) {
  926.                     $entity['buyingPriceHt'] = null;
  927.                     $entity['buyingPriceTtc'] = null;
  928.                 }
  929.             }
  930.             unset($entity);
  931.             usort($entities, function ($left$right) {
  932.                 $leftEntity $this->produitDeclinationValueRepository->find($left['id'] ?? null);
  933.                 $rightEntity $this->produitDeclinationValueRepository->find($right['id'] ?? null);
  934.                 $extractValues = function ($declinationEntity) {
  935.                     $position1 '';
  936.                     $position2 '';
  937.                     if (!$declinationEntity) {
  938.                         return [$position1$position2];
  939.                     }
  940.                     foreach ($declinationEntity->getGroupDeclinationValues() as $groupValue) {
  941.                         $declination $groupValue->getDeclination();
  942.                         $value $groupValue->getValue();
  943.                         if (!$declination || !$value) {
  944.                             continue;
  945.                         }
  946.                         $declinationId = (int) $declination->getId();
  947.                         $valueName = (string) $value->getName();
  948.                         if ($declinationId === 2) {
  949.                             $position1 $valueName;
  950.                         } else {
  951.                             $position2 $valueName;
  952.                         }
  953.                     }
  954.                     return [$position1$position2];
  955.                 };
  956.                 [$leftPosition1$leftPosition2] = $extractValues($leftEntity);
  957.                 [$rightPosition1$rightPosition2] = $extractValues($rightEntity);
  958.                 $position1Compare strcasecmp($leftPosition1$rightPosition1);
  959.                 if ($position1Compare !== 0) {
  960.                     return $position1Compare;
  961.                 }
  962.                 $position2Compare strnatcasecmp($leftPosition2$rightPosition2);
  963.                 if ($position2Compare !== 0) {
  964.                     return $position2Compare;
  965.                 }
  966.                 return strcasecmp((string) ($left['reference'] ?? ''), (string) ($right['reference'] ?? ''));
  967.             });
  968.         }
  969.         if(in_array("product",$types)) {
  970.             $entitiesproduit $this->produitRepository->searchItem($request->get('query'),count($types)==2?6:12);
  971.             foreach ($entitiesproduit as &$entity) {
  972.                 $qtReserved $quantity 0;
  973.                 $incomingQuantity 0;
  974.                 $incomingDateEstimated null;
  975.                 $exchangeIncomingQuantity 0;
  976.                 $stocks $em->getRepository(Stock::class)->findByProduit($entity['id']);
  977.                 foreach ($stocks as $stock) {
  978.                     $quantity $quantity + ($stock->getQtStock() - $stock->getQtReserved());
  979.                     $qtReserved += $stock->getQtReserved();
  980.                     $incomingQuantity += (int) $stock->getIncomingQuantity();
  981.                     $exchangeIncomingQuantity += (int) $stock->getExchangeIncomingQuantity();
  982.                     $candidateIncomingDate $stock->getIncomingDateEstimated();
  983.                     if ($candidateIncomingDate && ($incomingDateEstimated === null || $candidateIncomingDate $incomingDateEstimated)) {
  984.                         $incomingDateEstimated $candidateIncomingDate;
  985.                     }
  986.                 }
  987.                 $entity['quantity'] =( $quantity<=3  or $this->isGranted('ROLE_SUPER_ADMIN') or $this->currentUserHasRight('STOCK_SEE_REAL_STOCK') )?$quantity:"3<sup>+</sup>";
  988.                 $entity['qtReserved'] = $qtReserved;
  989.                 $entity['incomingQuantity'] = $incomingQuantity;
  990.                 $entity['incomingRemainingQuantity'] = $produit?->getIncomingRemainingQuantity() ?? $incomingQuantity;
  991.                 $entity['incomingDateEstimated'] = $incomingDateEstimated?->format('d/m/Y H:i');
  992.                 $entity['exchangeIncomingQuantity'] = $exchangeIncomingQuantity;
  993.                 $produit $this->produitRepository->find($entity['id']);
  994.                 $tvaEntity $produit $produit->getTva() : null;
  995.                 $entity['tva_id'] = $tvaEntity $tvaEntity->getId() : null;
  996.                 $entity['tva'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
  997.                 $entity['tva_number'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
  998.                 $entity['material'] = $produit $produit->getMaterial() : null;
  999.                 $promotion $produit->getPromotion();
  1000.                 $in_promo false;
  1001.                 $entity['value'] = null;
  1002.                 $entity['promotionType'] = null;
  1003.                 $entity['price_ttc'] = $entity['price_ttc_without_promo'] = $produit->getPriceTtc();
  1004.                 if( $promotion) {
  1005.                     $date = new \DateTime('now');
  1006.                     if( $promotion->getStartAt() <= $date && $promotion->getEndAt() >= $date) {
  1007.                         $entity['value'] = $promotion->getDiscountValue();
  1008.                         $entity['promotionType'] = $promotion->getDiscountType();
  1009.                         $entity['price_ttc'] = ($promotion->getDiscountType() == 'percent') ?
  1010.                             $produit->getPriceTtc() - ((($produit->getPriceTtc() / 100) * $promotion->getDiscountValue()))
  1011.                             : $produit->getPriceTtc() - $promotion->getDiscountValue();
  1012.                         $in_promo true;
  1013.                     }
  1014.                 }
  1015.                 $entity['in_promo'] = $in_promo;
  1016.                 $entity['type'] = 'produit';
  1017.                 $picture $this->getPicture($this->produitRepository->find($entity['id']), $request);
  1018.                 $entity['picture'] = $picture;
  1019.             }
  1020.         }
  1021.     }
  1022.     $data array_merge($entitiesproduit$entities);
  1023.     return new JsonResponse($data);
  1024. }
  1025. /**
  1026.  * @Route("/list", name="list_produit_declination_table", methods={"GET","POST"}, options = { "expose" =  true})
  1027.  */
  1028. public function listDatatable(Request $requestProduitDeclinationValueRepository $produitDecRepository): JsonResponse
  1029. {
  1030.     $draw   = (int) $request->get('draw'1);
  1031.     $start  = (int) $request->get('start'0);
  1032.     $length = (int) $request->get('length'20);
  1033.     $page  = (int) floor($start max(1$length));
  1034.     $limit max(1$length);
  1035.     $order   = (array) $request->get('order', []);
  1036.     $columns = (array) $request->get('columns', []);
  1037.     $sortField 'declinationPositions';
  1038.     $sortType  'ASC';
  1039.     $allowedFields = ['reference''name''createdAt''qtStock'];
  1040.     if (!empty($order) && isset($columns[$order[0]['column']]['data'])) {
  1041.         $candidate = (string) $columns[$order[0]['column']]['data'];
  1042.         $dir strtoupper((string) ($order[0]['dir'] ?? 'DESC'));
  1043.         if (in_array($candidate$allowedFieldstrue)) $sortField $candidate;
  1044.         $sortType in_array($dir, ['ASC''DESC'], true) ? $dir 'DESC';
  1045.     }
  1046.     $rootParams  = (array) $request->query->all();
  1047.     $queryParams is_array($rootParams['query'] ?? null) ? $rootParams['query'] : (array) $request->get('query', []);
  1048.     $getParam = function (string $key$default null) use ($rootParams$queryParams) {
  1049.         if (array_key_exists($key$rootParams)) return $rootParams[$key];
  1050.         if (array_key_exists($key$queryParams)) return $queryParams[$key];
  1051.         return $default;
  1052.     };
  1053.     $declinationFilters $getParam('declinations'$request->get('declinations', []));
  1054.     if (is_string($declinationFilters)) {
  1055.         $decoded json_decode($declinationFilterstrue);
  1056.         $declinationFilters = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
  1057.     }
  1058.     if (!is_array($declinationFilters)) $declinationFilters = [];
  1059.     $normalizedDeclinationFilters = [];
  1060.     foreach ($declinationFilters as $k => $v) {
  1061.         $key = (int) $k;
  1062.         $normalizedDeclinationFilters[$key] = is_array($v) ? array_map('intval'$v) : (int) $v;
  1063.     }
  1064.     $declinationFilters $normalizedDeclinationFilters;
  1065.     $reference      $getParam('reference');
  1066.     $name           $getParam('name');
  1067.     $categories     $getParam('categories');
  1068.     $isAvailable    $getParam('isAvailable');
  1069.     $inPromo        $getParam('inPromo');
  1070.     $qtMin          $getParam('qtMin');
  1071.     $qtMax          $getParam('qtMax');
  1072.     $buyingPriceMin $getParam('buyingPriceMin');
  1073.     $buyingPriceMax $getParam('buyingPriceMax');
  1074.     $priceMin       $getParam('priceMin');
  1075.     $priceMax       $getParam('priceMax');
  1076.     $withDeleted    $getParam('withDeleted') == 'true' || $getParam('withDeleted') === true || $getParam('withDeleted') === || $getParam('withDeleted') === '1';
  1077.     $hasIsAvailable array_key_exists('isAvailable'$rootParams) || array_key_exists('isAvailable'$queryParams);
  1078.     if (!$hasIsAvailable$isAvailable '1';
  1079.     $warehouseRaw $getParam('warehouse'$request->get('warehouse'));
  1080.     $currentWarehouseId = ($warehouseRaw !== null && $warehouseRaw !== '')
  1081.         ? (int) $warehouseRaw
  1082.         : ($this->warehouseContextService->getCurrentWarehouse($this->getUser())?->getId() ?? null);
  1083.     $result $produitDecRepository->searchAndCountProduitDeclinations(
  1084.         $page,
  1085.         $limit,
  1086.         $reference,
  1087.         $name,
  1088.         $categories,
  1089.         $isAvailable,
  1090.         $inPromo,
  1091.         $declinationFilters,
  1092.         $qtMin,
  1093.         $qtMax,
  1094.         $buyingPriceMin,
  1095.         $buyingPriceMax,
  1096.         $priceMin,
  1097.         $priceMax,
  1098.         $sortField,
  1099.         $sortType,
  1100.         $withDeleted,
  1101.         $currentWarehouseId
  1102.     );
  1103.     $entities $result['data'] ?? [];
  1104.     $count    = (int) ($result['total'] ?? 0);
  1105.     $metaDecli1Name null;
  1106.     $metaDecli2Name null;
  1107.     $data = [];
  1108.     foreach ($entities as $entity) {
  1109.         $quantity 0;
  1110.         $qtReserved 0;
  1111.         $incomingQuantity 0;
  1112.         $incomingRemainingQuantity 0;
  1113.         $incomingDateEstimated null;
  1114.         $exchangeIncomingQuantity 0;
  1115.         foreach ($entity->getStocks() as $stock) {
  1116.             if ($currentWarehouseId && (int) ($stock->getWarehouse()?->getId() ?? 0) !== (int) $currentWarehouseId) {
  1117.                 continue;
  1118.             }
  1119.             //$quantity   += max(0, (int) $stock->getQtStock() - (int) $stock->getQtReserved());
  1120.             $quantity += ((int) $stock->getQtStock() - (int) $stock->getQtReserved());
  1121.             $qtReserved += (int) $stock->getQtReserved();
  1122.             $incomingQuantity += (int) $stock->getIncomingQuantity();
  1123.             $incomingRemainingQuantity += (int) $stock->getIncomingRemainingQuantity();
  1124.             $exchangeIncomingQuantity += (int) $stock->getExchangeIncomingQuantity();
  1125.             $candidateIncomingDate $stock->getIncomingDateEstimated();
  1126.             if ($candidateIncomingDate && ($incomingDateEstimated === null || $candidateIncomingDate $incomingDateEstimated)) {
  1127.                 $incomingDateEstimated $candidateIncomingDate;
  1128.             }
  1129.         }
  1130.         $declinationGroups $entity->getGroupDeclinationValues()->toArray();
  1131.         usort($declinationGroups, function ($a$b) {
  1132.             return $a->getDeclination()->getPosition() <=> $b->getDeclination()->getPosition();
  1133.         });
  1134.         $decli1 = isset($declinationGroups[0]) ? $declinationGroups[0] : null;
  1135.         $decli2 = isset($declinationGroups[1]) ? $declinationGroups[1] : null;
  1136.         $decli1Name $decli1 $decli1->getDeclination()->getName() : null;
  1137.         $decli1Val  $decli1 $decli1->getValue()->getName() : null;
  1138.         $decli2Name $decli2 $decli2->getDeclination()->getName() : null;
  1139.         $decli2Val  $decli2 $decli2->getValue()->getName() : null;
  1140.         if ($metaDecli1Name === null && $decli1Name$metaDecli1Name $decli1Name;
  1141.         if ($metaDecli2Name === null && $decli2Name$metaDecli2Name $decli2Name;
  1142.         $base  = (float) $entity->getProduit()->getPriceTtc();
  1143.         $final $base;
  1144.         $promoActive  false;
  1145.         $promoType    null;
  1146.         $promoValue   null;
  1147.         $promoPercent null;
  1148.         $promotion $entity->getProduit()->getPromotion();
  1149.         if ($promotion) {
  1150.             $now = new \DateTime('now');
  1151.             if ($promotion->getStartAt() <= $now && $promotion->getEndAt() >= $now) {
  1152.                 $promoActive true;
  1153.                 $promoType   $promotion->getDiscountType();
  1154.                 $promoValue  = (float) $promotion->getDiscountValue();
  1155.                 if ($promoType === 'percent') {
  1156.                     $final        round($base * ($promoValue 100), 3);
  1157.                     $promoPercent = (int) round($promoValue);
  1158.                 } else {
  1159.                     $final        round(max(0$base $promoValue), 3);
  1160.                     $promoPercent $base ? (int) round((($base $final) / $base) * 100) : 0;
  1161.                 }
  1162.             }
  1163.         }
  1164.         $pictures $entity->getPicture()->filter(fn($p) => $p->getIsSelected() == 1);
  1165.         $pictureName $pictures->isEmpty() ? null $pictures->first()->getImageName();
  1166.         if (
  1167.             !$pictures->isEmpty()
  1168.             && $pictureName
  1169.             && file_exists($this->getParameter('kernel.project_dir') . "/public/images/" $pictureName)
  1170.             && !file_exists($this->getParameter('kernel.project_dir') . "/public/images/web/" $pictureName)
  1171.         ) {
  1172.             $this->resizeImage($pictureName);
  1173.         }
  1174.         $canSeeRealStock $this->isGranted('ROLE_SUPER_ADMIN') || $this->currentUserHasRight('STOCK_SEE_REAL_STOCK');
  1175.         $quantityDisplay = ($quantity <= || $canSeeRealStock) ? $quantity "3<sup>+</sup>";
  1176.         $isResellerView $this->isGranted('ROLE_RESELLER') && !$this->isGranted('ROLE_SUPER_ADMIN');
  1177.         $displayPrice $isResellerView ? (float) ($entity->getProduit()->getResellerPriceTtc() ?? $final) : $final;
  1178.         $displayBasePrice $isResellerView ? (float) ($entity->getProduit()->getResellerPriceTtc() ?? $base) : $base;
  1179.         $data[] = [
  1180.             'id'                 => $entity->getId(),
  1181.             'image'              => $pictureName,
  1182.             'name'               => $entity->getName(),
  1183.             'reference'          => $entity->getReference(),
  1184.             'material'           => $entity->getProduit()?->getMaterial(),
  1185.             'parent'             => $entity->getProduit()->getReference(),
  1186.             'parent_id'          => $entity->getProduit()->getId(),
  1187.             'parent_name'        => $entity->getProduit()->getName(),
  1188.             'quick_order_url'    => $this->generateUrl('product_quick_order', [
  1189.                 'id' => $entity->getProduit()->getId(),
  1190.                 'name' => $entity->getProduit()->getName() ? $this->slugify($entity->getProduit()->getName()) : 'produit',
  1191.             ], UrlGeneratorInterface::ABSOLUTE_URL),
  1192.             'buyingPriceTtc'     => $this->isGranted('ROLE_SUPER_ADMIN') ? (float) $entity->getBuyingPriceTtc() : null,
  1193.             'price_ht'           => (float) $entity->getPriceHt(),
  1194.             'price_ttc'          => $displayPrice,
  1195.             'price_ttc_final'    => $displayPrice,
  1196.             'price_ttc_original' => $displayBasePrice,
  1197.             'resellerPriceTtc'   => (float) ($entity->getProduit()->getResellerPriceTtc() ?? 0),
  1198.             'promo_active'       => $promoActive,
  1199.             'promo_type'         => $promoType,
  1200.             'promo_value'        => $promoValue,
  1201.             'promo_percent'      => $promoPercent,
  1202.             'decli1_name'        => $decli1Name,
  1203.             'decli1_value'       => $decli1Val,
  1204.             'decli2_name'        => $decli2Name,
  1205.             'decli2_value'       => $decli2Val,
  1206.             'qtStock'           => $quantity,
  1207.             'quantity'           => $quantityDisplay,
  1208.             'qtReserved'         => $qtReserved,
  1209.             'incomingQuantity'   => $incomingQuantity,
  1210.             'incomingRemainingQuantity' => $incomingRemainingQuantity,
  1211.             'incomingDateEstimated' => $incomingDateEstimated?->format('d/m/Y H:i'),
  1212.             'exchangeIncomingQuantity' => $exchangeIncomingQuantity,
  1213.             'exchangeIncomingDetailsUrl' => $exchangeIncomingQuantity 0
  1214.                 $this->buildExchangeIncomingDetailsUrl($entity$currentWarehouseId $currentWarehouseId null)
  1215.                 : null,
  1216.             'createdAt'          => $entity->getCreatedAt()?->format('Y-m-d\TH:i:s'),
  1217.             'inPromo'            => $promoActive,
  1218.         ];
  1219.     }
  1220.     return $this->json([
  1221.         'draw'            => $draw,
  1222.         'recordsTotal'    => $count,
  1223.         'recordsFiltered' => $count,
  1224.         'data'            => $data,
  1225.         'meta'            => [
  1226.             'decli1_name' => $metaDecli1Name,
  1227.             'decli2_name' => $metaDecli2Name,
  1228.         ],
  1229.     ]);
  1230. }
  1231. /**
  1232.  * @Route("/exchange-incoming/recalculate", name="produit_declination_exchange_incoming_recalculate", methods={"POST"}, options={"expose"=true})
  1233.  */
  1234. public function recalculateExchangeIncoming(
  1235.     Request $request,
  1236.     StockExchangeIncomingService $stockExchangeIncomingService,
  1237.     EntityManagerInterface $em
  1238. ): JsonResponse {
  1239.     $rights $this->rightService->getAllRights($this->getUser());
  1240.     if (!$this->canRecalculateExchangeIncoming($rights)) {
  1241.         return new JsonResponse([
  1242.             'success' => false,
  1243.             'message' => 'Accès refusé.',
  1244.         ], 403);
  1245.     }
  1246.     $result $stockExchangeIncomingService->recalculateAll();
  1247.     $em->flush();
  1248.     return new JsonResponse([
  1249.         'success' => true,
  1250.         'message' => sprintf(
  1251.             'Recalcul terminé : %d bon(s), %d ligne(s), %d stock(s) mis à jour.',
  1252.             (int) ($result['documents'] ?? 0),
  1253.             (int) ($result['lines'] ?? 0),
  1254.             (int) ($result['stocks'] ?? 0)
  1255.         ),
  1256.         'summary' => $result,
  1257.     ]);
  1258. }
  1259. /**
  1260.  * @Route("/stock/edit/{id}", name="stock_edit", methods={"GET","POST"}, options = { "expose" =  true})
  1261.  */
  1262. public function editStock(Request $requestStock $stock,EntityManagerInterface $em) {
  1263.     if( $request->isMethod('POST')) {
  1264.         $stock->setQtStock((int) $request->get('edit_stock')['qt_total']);
  1265.         $em->flush();
  1266.         if ($request->isXmlHttpRequest()) {
  1267.             return new JsonResponse([
  1268.                 'success' => true,
  1269.                 'message' => 'Stock modifie avec succes.'
  1270.             ]);
  1271.         }
  1272.         $request->getSession()->getFlashBag()->add('success''Stock modifie avec succes');
  1273.     }
  1274.     return $this->redirectToRoute('produit_declination_value_show', ['id' => $stock->getDeclinationProduit()->getId()]);
  1275. }
  1276. public function getPicture($entity$request): string
  1277. {
  1278.     $baseurl $request->getScheme() . '://' $request->getHttpHost() . $request->getBasePath();
  1279.     if ($entity->getPicture()) {
  1280.         foreach ($entity->getPicture() as $item) {
  1281.             if ($item->getIsSelected()) {
  1282.                 $path $this->uploaderHelper->asset($item'file');
  1283.                 if ($path) {
  1284.                     return $baseurl $path;
  1285.                 }
  1286.             }
  1287.         }
  1288.     }
  1289.     // Aucune image sélectionnée
  1290.     return '';
  1291. }
  1292. /**
  1293.  * @Route("/file/{id}", name="produit_declination_value_file", methods={"GET","POST"}, options={"expose"=true})
  1294.  */
  1295. public function addFile(Request $requestProduitDeclinationValue $produitEntityManagerInterface $em): Response
  1296. {
  1297.     $selectFileId $request->request->get('selectFile');
  1298.     $isAjax $request->isXmlHttpRequest();
  1299.     if ($isAjax && $selectFileId) {
  1300.         foreach ($produit->getPicture() as $picture) {
  1301.             $picture->setIsSelected(false);
  1302.         }
  1303.         $imageSelected $this->fileRepository->find($selectFileId);
  1304.         if (!$imageSelected) {
  1305.             return new JsonResponse(['success' => false'message' => 'Image introuvable'], 404);
  1306.         }
  1307.         $imageSelected->setIsSelected(true);
  1308.         $em->flush();
  1309.         return new JsonResponse([
  1310.             'success' => true,
  1311.             'message' => 'Image principale mise a jour.'
  1312.         ]);
  1313.     }
  1314.     $form $this->createForm(UploadFileProduitDecType::class, $produit);
  1315.     $form->handleRequest($request);
  1316.     if ($form->isSubmitted() && $form->isValid()) {
  1317.         $filesBag $request->files->get('upload_file_produit_dec', []);
  1318.         $pictures $filesBag['picture'] ?? [];
  1319.         if (!is_array($pictures)) {
  1320.             $pictures = [$pictures];
  1321.         }
  1322.         $hasNewImages false;
  1323.         foreach ($pictures as $key => $picture) {
  1324.             if (!$picture) {
  1325.                 continue;
  1326.             }
  1327.             $file = new File();
  1328.             $file->setFile($picture);
  1329.             if (!$selectFileId && $key === 0) {
  1330.                 foreach ($produit->getPicture() as $existingPicture) {
  1331.                     $existingPicture->setIsSelected(false);
  1332.                 }
  1333.                 $file->setIsSelected(true);
  1334.             }
  1335.             $em->persist($file);
  1336.             $produit->addPicture($file);
  1337.             $hasNewImages true;
  1338.         }
  1339.         if ($hasNewImages) {
  1340.             $em->flush();
  1341.             if ($isAjax) {
  1342.                 return new JsonResponse([
  1343.                     'success' => true,
  1344.                     'message' => 'Images ajoutees avec succes.'
  1345.                 ]);
  1346.             }
  1347.             $request->getSession()->getFlashBag()->add('success''Images ajoutees avec succes');
  1348.         }
  1349.         return $this->redirectToRoute('produit_declination_value_show', [
  1350.             'id' => $produit->getId(),
  1351.             'tab' => 'content_galery'
  1352.         ]);
  1353.     }
  1354.     if ($isAjax) {
  1355.         return new JsonResponse(['success' => false'message' => 'Requete invalide'], 400);
  1356.     }
  1357.     return $this->redirectToRoute('produit_declination_value_show', [
  1358.         'id' => $produit->getId(),
  1359.         'tab' => 'content_galery'
  1360.     ]);
  1361. }
  1362. /**
  1363.  * @Route("/searchgroup", name="get_group_produit", methods={"GET","POST"}, options = { "expose" =  true})
  1364.  */
  1365. public function searchGroupProduit(Request $request,EntityManagerInterface $em) {
  1366.     $result = [];
  1367.         $hideSupplierReceptionPrices $request->get('document_category') === 'fournisseur'
  1368.         && !$this->currentUserHasRight('DOCUMENT_SUPPLIER_RECEPTION_PRICE');
  1369.     $restrictSupplier filter_var($request->get('restrict_supplier'false), FILTER_VALIDATE_BOOLEAN);
  1370.     $supplierId $restrictSupplier ? (int) $request->get('supplier_id'0) : 0;
  1371.     $entities $this->produitDeclinationValueRepository->findDeclinationValueWithDeclination(
  1372.         $request->get('color'),
  1373.         $request->get('produit'),
  1374.         $supplierId $supplierId null
  1375.     );
  1376.     $entities $this->orderWithSise($entities);
  1377.     foreach ($entities as $entity) {
  1378.         $quantity 0;
  1379.         $array['type'] = 'produitDeclination';
  1380.         $stocks $em->getRepository(Stock::class)->findByDeclinationProduit($entity->getId());
  1381.         foreach ($stocks as $stock) {
  1382.             $quantity $quantity + ($stock->getQtStock() - $stock->getQtReserved());
  1383.         }
  1384.         $promotion $this->produitDeclinationValueRepository->find($entity->getId())->getProduit()->getPromotion();
  1385.         $array['value'] = null;
  1386.         $array['promotionType'] = null;
  1387.         if( $promotion) {
  1388.             $date = new \DateTime('now');
  1389.             if( $promotion->getStartAt() <= $date && $promotion->getEndAt() >= $date) {
  1390.                 $array['value'] = $promotion->getDiscountValue();
  1391.                 $array['promotionType'] = $promotion->getDiscountType();
  1392.             }
  1393.         }
  1394.         $picture $this->getPicture($this->produitDeclinationValueRepository->find($entity->getId()), $request);
  1395.         $array['picture'] = $picture;
  1396.         $array['quantity'] = $quantity;
  1397.         $array['id'] = $entity->getId();
  1398.         $array['name'] = $entity->getName();
  1399.         $array['reference'] = $entity->getReference();
  1400.         $array['description'] = $entity->getDescription();
  1401.         $array['price_ht'] = $entity->getPriceHt();
  1402.         $array['price_ttc'] = $entity->getProduit()->getPriceTtc();
  1403.         $array['unit'] = $entity->getProduit()->getUnit();
  1404.         $array['buyingPriceHt'] = $hideSupplierReceptionPrices null $entity->getBuyingPriceHt();
  1405.         $array['buyingPriceTtc'] = $hideSupplierReceptionPrices null $entity->getBuyingPriceTtc();
  1406.         $array['tva'] = $entity->getProduit()->getTva() ? $entity->getProduit()->getTva()->getNumber() : 0;
  1407.         $array['tva_id'] = $entity->getProduit()->getTva() ? $entity->getProduit()->getTva()->getId() : 1;
  1408.         $result[] = $array;
  1409.     }
  1410.     return new JsonResponse($result);
  1411. }
  1412. public function orderWithSise($entities) {
  1413.     $allSize = ["XS""S""M""L""XL""XXL""XXXL""XXXXL"];
  1414.     $isLetter false;
  1415.     if($entities)
  1416.         foreach ($entities[0]->getGroupDeclinationValues() as $group) {
  1417.             if( $group->getDeclination()->getName() == "Taille") {
  1418.                 if( in_array($group->getValue()->getName(), $allSize)) $isLetter true;
  1419.             }
  1420.         }
  1421.     if( $isLetter == true) {
  1422.         usort($entities, function ($a$b) use ($allSize) {
  1423.             foreach ($a->getGroupDeclinationValues() as $group) {
  1424.                 if( $group->getDeclination()->getName() == "Taille")
  1425.                     $pos_a array_search($group->getValue()->getName(), $allSize);
  1426.             }
  1427.             foreach ($b->getGroupDeclinationValues() as $group) {
  1428.                 if( $group->getDeclination()->getName() == "Taille")
  1429.                     $pos_b array_search($group->getValue()->getName(), $allSize);
  1430.             }
  1431.             return $pos_a $pos_b;
  1432.         });
  1433.     }
  1434.     return $entities;
  1435. }
  1436. /**
  1437.  * @Route("/info/edit/{id}", name="produit_dec_info_edit", methods={"GET","POST"}, options={"expose"=true})
  1438.  */
  1439. public function editInfo(Request $requestProduitDeclinationValue $produit,EntityManagerInterface $em): Response {
  1440.     $this->hasRight($request,'PRODUIT_UPDATE');
  1441.     if( $request->isMethod('POST') && $request->get('form_info')) {
  1442.         $produit->setDescription($request->get('form_info')['description']);
  1443.         $produit->setName($request->get('form_info')['name']);
  1444.         $produit->setBarcode(isset($request->get('form_info')['barcode']) && trim((string) $request->get('form_info')['barcode']) !== '' trim((string) $request->get('form_info')['barcode']) : null);
  1445.         $message $this->getUser()->getFirstName() . " a modifié l'info de Déclinaison  " $produit->getReference();
  1446.         $this->activityService->addActivity('info'$message$produit->getProduit(), $this->getUser(), 'produit');
  1447.         $em->flush();
  1448.         if ($request->isXmlHttpRequest()) {
  1449.             return new JsonResponse([
  1450.                 'result' => 1,
  1451.                 'message' => 'Informations de la declinaison mises a jour.',
  1452.             ]);
  1453.         }
  1454.         return $this->redirectToRoute('produit_declination_value_show', ['id' => $produit->getId(),"tab"=>"content_informations"]);
  1455.     }
  1456.     return false;
  1457. }
  1458. }