<?php
namespace App\Controller\Admin;
use App\Service\ActivityService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Entity\ProduitDeclinationValue;
use App\Entity\GroupDeclinationValue;
use App\Entity\Produit;
use App\Repository\ProduitRepository;
use App\Repository\DeclinationRepository;
use App\Repository\ValueDeclinationRepository;
use App\Repository\ProduitDeclinationValueRepository;
use App\Repository\GroupDeclinationValueRepository;
use App\Repository\FileRepository;
use App\Entity\Stock;
use App\Entity\File;
use App\Entity\Warehouse;
use App\Form\UploadFileProduitDecType;
use App\Form\FilterProduitDeclinationType;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
use App\Form\FilterDocumentType;
use App\Service\RightService;
use App\Service\StockExchangeIncomingService;
use App\Service\WarehouseContextService;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted(new Expression("is_granted('ROLE_ADMIN') or is_granted('ROLE_RESELLER')"))]
/**
* @Route("/produit/declination/value")
*/
class ProduitDeclinationValueController extends AbstractController {
use ImageControllerTrait;
use AccessTrait;
private $produitRepository;
private $declinationRepository;
private $valueDeclinationRepository;
private $produitDeclinationValueRepository;
private $groupDeclinationValueRepository;
private $fileRepository;
private $uploaderHelper;
private $rightService;
private $activityService;
private WarehouseContextService $warehouseContextService;
public function __construct(
ProduitRepository $produitRepository,
ValueDeclinationRepository $valueDeclinationRepository,
DeclinationRepository $declinationRepository,
ProduitDeclinationValueRepository $produitDeclinationValueRepository,
FileRepository $fileRepository,
GroupDeclinationValueRepository $groupDeclinationValueRepository,
UploaderHelper $uploaderHelper,
RightService $rightService,
ActivityService $activityService,
WarehouseContextService $warehouseContextService
) {
$this->produitRepository = $produitRepository;
$this->valueDeclinationRepository = $valueDeclinationRepository;
$this->declinationRepository = $declinationRepository;
$this->produitDeclinationValueRepository = $produitDeclinationValueRepository;
$this->groupDeclinationValueRepository = $groupDeclinationValueRepository;
$this->fileRepository = $fileRepository;
$this->uploaderHelper = $uploaderHelper;
$this->rightService = $rightService;
$this->activityService = $activityService;
$this->warehouseContextService = $warehouseContextService;
}
private function assignDefaultWarehouseToStock(Stock $stock, EntityManagerInterface $em): void
{
$warehouse = $this->warehouseContextService->resolveOperationWarehouse(null, $this->getUser());
if ($warehouse instanceof Warehouse) {
$stock->setWarehouse($warehouse);
}
}
private function slugify(string $text): string
{
$text = trim($text);
if ($text === '') {
return 'produit';
}
$text = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text) ?: $text;
$text = preg_replace('/[^A-Za-z0-9]+/', '-', $text) ?? $text;
$text = trim($text, '-');
$text = strtolower($text);
return $text !== '' ? $text : 'produit';
}
private function canRecalculateExchangeIncoming(array $rights): bool
{
return $this->isGranted('ROLE_SUPER_ADMIN') || \in_array('STOCK_UPDATE', $rights, true);
}
private function buildExchangeIncomingDetailsUrl(?ProduitDeclinationValue $declinaison, ?int $warehouseId = null): ?string
{
if (!$declinaison instanceof ProduitDeclinationValue) {
return null;
}
$parameters = ['id' => $declinaison->getId()];
if (($warehouseId ?? 0) > 0) {
$parameters['warehouse'] = (int) $warehouseId;
}
return $this->generateUrl('document_exchange_incoming_declination_modal', $parameters);
}
/**
* @Route("/show/{id}", name="produit_declination_value_show", methods={"GET"}, options={"expose"=true})
*/
public function show(ProduitDeclinationValue $declinaison, Request $request): Response {
$rights = $this->rightService->getAllRights($this->getUser());
if( !in_array('PRODUIT', $rights)) {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));
}
$formFile = $this->createForm(UploadFileProduitDecType::class, $declinaison);
$form = $this->createForm(FilterDocumentType::class);
return $this->render('@admin/produit_declination_value/fiche_declinaison_produit.html.twig', [
'declinaison' => $declinaison,
'formFile' => $formFile->createView(),
'form' => $form->createView(),
'rights' => $rights
]);
}
/**
* @Route("/", name="produit_declination_value_index", methods={"GET"},options = { "expose" = true})
*/
public function index(Request $request): Response {
$rights = $this->rightService->getAllRights($this->getUser());
if (!in_array('PRODUIT', $rights)) {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));
}
$form = $this->createForm(FilterProduitDeclinationType::class);
// ⚠️ on ne touche pas à l’existant
$declinations = $this->declinationRepository->findAll();
// ✅ nouvelle liste pour le filtre uniquement
$declinationsFilter = $this->declinationRepository->findAllForFilter();
return $this->render('@admin/produit_declination_value/list_declinaisons_produits.html.twig', [
'form' => $form->createView(),
'declinations' => $declinations, // reste disponible ailleurs
'declinationsFilter' => $declinationsFilter, // utilisé par le filtre UI
'rights' => $rights,
'canRecalculateExchangeIncoming' => $this->canRecalculateExchangeIncoming($rights),
]);
}
/**
* @Route("/add", name="add_produit_declination_value", methods={"GET","POST"}, options={"expose"=true})
*/
public function add(Request $request, EntityManagerInterface $em): JsonResponse
{
try {
$listDeclinations = [];
$valueDeclinations = (array) $request->get('value_declination', []);
foreach ($valueDeclinations as $item) {
$status = (string) ($item['status'] ?? 'new');
// Ignore les lignes supprimées
if ($status === 'delete') {
continue;
}
$declinations = (array) ($item['declinations'] ?? []);
if (empty($declinations)) {
continue;
}
$listDeclinations[] = $declinations;
}
$duplicate = $this->checkDuplicate($listDeclinations);
if ($duplicate) {
return new JsonResponse(["success" => false, "message" => $duplicate]);
}
$this->createEntity($request, $em);
$request->getSession()->getFlashBag()->add('success', "Les changements ont été enregistrés");
return new JsonResponse([
"success" => true,
"path" => $this->generateUrl('produit_show', ['id' => $request->get('id_produit')])
]);
} catch (\RuntimeException $e) {
return new JsonResponse([
"success" => false,
"message" => $e->getMessage()
], 422);
} catch (\Throwable $e) {
return new JsonResponse([
"success" => false,
"message" => "Erreur technique lors de l'enregistrement."
], 500);
}
}
public function createEntity($request,EntityManagerInterface $em) {
foreach ($request->get('value_declination') as $item) {
if( $item['status'] === 'new') {
$entity = new ProduitDeclinationValue();
$entity->setBuyingPriceTtc($item['buyingPriceTtc'])
->setDescription($item['description'])
->setName($item['name'])
->setPriceHt($item['price_ht'])
->setReference($item['reference'])
->setCreatedAt(new \DateTime('now'));
$stock = new Stock();
if( $request->get('id_produit')) {
$produit = $this->produitRepository->find($request->get('id_produit'));
$entity->setProduit($produit);
$stock->setProduit($produit);
}
$stock->setQtReserved(0)
->setQtStock(0)
->setDeclinationProduit($entity);
$this->assignDefaultWarehouseToStock($stock, $em);
$em->persist($stock);
$em->persist($entity);
if( $item['declinations']) {
foreach ($item['declinations'] as $key => $value) {
$group = new GroupDeclinationValue();
if( $value && $key) {
$valueDeclination = $this->valueDeclinationRepository->find($value);
$declination = $this->declinationRepository->find($key);
$group->setValue($valueDeclination);
$group->setDeclination($declination);
}
$group->setProduitDeclination($entity);
$em->persist($group);
}
}
} elseif( $item['status'] === 'update') {
$entity = $this->produitDeclinationValueRepository->find($item['id']);
$entity->setBuyingPriceTtc($item['buyingPriceTtc'])
->setDescription($item['description'])
->setName($item['name'])
->setPriceHt($item['price_ht'])
->setReference($item['reference']);
} elseif (isset($item['id']) && $item['status'] === 'delete') {
$entity = $this->produitDeclinationValueRepository->find($item['id']);
if (!$entity) {
continue;
}
// Interdire suppression si utilisée dans des documents
if (count($entity->getDocumentDeclinationProduits()) > 0) {
throw new \RuntimeException(sprintf(
"La déclinaison '%s' (%s) est utilisée dans des documents. Suppression impossible.",
$entity->getName(),
$entity->getReference()
));
}
foreach ($entity->getGroupDeclinationValues() as $group) {
$em->remove($group);
}
foreach ($entity->getStocks() as $stock) {
foreach ($stock->getActivities() as $activity) {
$em->remove($activity);
}
$em->remove($stock);
}
foreach ($entity->getActivities() as $activity) {
$em->remove($activity);
}
$em->remove($entity);
}
}
$em->flush();
}
private function comboKey(array $combo): string
{
$norm = [];
foreach ($combo as $declinationId => $valueId) {
$d = (int) $declinationId;
$v = (int) $valueId;
if ($d > 0 && $v > 0) {
$norm[$d] = $v;
}
}
ksort($norm);
return json_encode($norm);
}
private function normalizeDeclinationGroup(array $group): array
{
$normalized = [];
foreach ($group as $declinationKey => $valueId) {
$vId = (int) $valueId;
if ($vId <= 0) {
continue;
}
// clé peut être un id (ex: "12") ou un nom (ex: "Couleur")
$dId = ctype_digit((string) $declinationKey) ? (int) $declinationKey : 0;
if ($dId <= 0) {
$decl = $this->declinationRepository->findOneBy(['name' => (string) $declinationKey]);
if ($decl) {
$dId = (int) $decl->getId();
}
}
if ($dId > 0) {
$normalized[$dId] = $vId;
}
}
ksort($normalized);
return $normalized;
}
public function checkDuplicate(array $listDeclinations): ?string
{
$seen = [];
foreach ($listDeclinations as $group) {
if (!is_array($group)) {
continue;
}
$normalized = $this->normalizeDeclinationGroup($group);
// IMPORTANT: on compare seulement une vraie combinaison (au moins 2 dimensions)
if (count($normalized) < 2) {
continue;
}
$key = json_encode($normalized);
if (isset($seen[$key])) {
$parts = [];
foreach ($normalized as $declinationId => $valueId) {
$decl = $this->declinationRepository->find((int) $declinationId);
$val = $this->valueDeclinationRepository->find((int) $valueId);
if ($decl && $val) {
$parts[] = $decl->getName() . ': ' . $val->getName();
}
}
$label = !empty($parts) ? implode(' | ', $parts) : 'Cette combinaison';
return $label . " existe déjà, veuillez sélectionner une combinaison différente.";
}
$seen[$key] = true;
}
return null;
}
public function return_dup($arr) {
$dups = array();
$temp = $arr;
foreach ($arr as $key => $item) {
unset($temp[$key]);
if( in_array($item, $temp)) {
$dups[] = $item;
}
}
return $dups;
}
/**
* @Route("/listData/{idProduit}", name="list_produit_declination_value", methods={"GET","POST"}, options = { "expose" = true})
*/
public function listData($idProduit, Request $request): JsonResponse
{
$draw = (int) $request->get('draw', 1);
$start = (int) $request->get('start', 0);
$length = (int) $request->get('length', 20);
$page = (int) floor($start / max(1, $length));
$limit = max(1, $length);
$reference = (string) $request->get('reference', '');
$declinations = $request->get('declinations', []);
if (is_string($declinations)) {
$decoded = json_decode($declinations, true);
$declinations = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
}
$declinations = array_combine(
array_map('intval', array_keys($declinations)),
array_map(static function ($v) { return is_array($v) ? array_map('intval', $v) : (int) $v; }, array_values($declinations))
);
$entities = $this->produitDeclinationValueRepository->findProduitGroup($page, $limit, (int) $idProduit, $reference, $declinations);
$count = (int) $this->produitDeclinationValueRepository->countProduitGroup((int) $idProduit, $reference, $declinations);
$data = [];
foreach ($entities as $entity) {
$baseTtc = (float) $entity->getProduit()->getPriceTtc();
$finalTtc = $baseTtc;
$promotion = $entity->getProduit()->getPromotion();
if ($promotion) {
$now = new \DateTimeImmutable('now');
if ($promotion->getStartAt() <= $now && $promotion->getEndAt() >= $now) {
$promoType = $promotion->getDiscountType();
$promoValue = (float) $promotion->getDiscountValue();
if ($promoType === 'percent') {
$finalTtc = round($baseTtc * (1 - $promoValue / 100), 3);
} else {
$finalTtc = round(max(0, $baseTtc - $promoValue), 3);
}
}
}
$picture = $entity->getPicture()->filter(static fn($pic) => $pic->getIsSelected() == 1)->first();
if (!$picture) {
$picture = $entity->getPicture()->first();
}
$imageName = $picture ? $picture->getImageName() : 'no-image-50px.png';
$imageSrc = $request->getSchemeAndHttpHost() . '/images/' . $imageName;
$imageHtml = '<img src="' . htmlspecialchars($imageSrc, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" alt="" style="width:44px;height:44px;object-fit:cover;border-radius:6px;">';
$row = [
'id' => $entity->getId(),
'name' => $entity->getName(),
'reference' => $entity->getReference(),
'image' => $imageHtml,
'quick_order_url' => $this->generateUrl('product_quick_order', [
'id' => $entity->getProduit()->getId(),
'name' => $entity->getProduit()->getName() ? $this->slugify($entity->getProduit()->getName()) : 'produit',
], UrlGeneratorInterface::ABSOLUTE_URL),
'buyingPriceTtc' => ($this->isGranted('ROLE_SUPER_ADMIN')) ? number_format((float) $entity->getBuyingPriceTtc(), 3) : '',
'price_ttc' => number_format($finalTtc, 3),
'actions' =>
'<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>'
. '<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>',
];
$groups = $entity->getGroupDeclinationValues()->toArray();
usort($groups, static function ($a, $b) {
return (int) $a->getDeclination()->getPosition() <=> (int) $b->getDeclination()->getPosition();
});
foreach ($groups as $group) {
$key = strtolower((string) $group->getDeclination()->getName());
$value = (string) $group->getValue()->getName();
$row[$key] = $value;
}
$data[] = $row;
}
return $this->json([
'draw' => $draw,
'recordsTotal' => $count,
'recordsFiltered' => $count,
'data' => $data,
]);
}
/**
* @Route("/columns/{idProduit}", name="columns_produit_declination_value", methods={"GET","POST"}, options = { "expose" = true})
*/
public function listColumns($idProduit): JsonResponse
{
$produit = $this->produitRepository->find($idProduit);
$output = [
[
'field' => 'reference',
'sortable' => 'asc',
'width' => 140,
'title' => 'Référence'
],
[
'field' => 'name',
'sortable' => 'asc',
'title' => 'Nom commercial',
'width' => 250
]
];
// CORRECTION CLÉ : tri par position métier
$declinations = $produit->getDeclinations()->toArray();
usort($declinations, static function ($a, $b) {
return $a->getPosition() <=> $b->getPosition();
});
foreach ($declinations as $entity) {
$output[] = [
'field' => strtolower($entity->getName()),
'width' => 60,
'textAlign' => 'center',
'title' => $entity->getName()
];
}
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$output[] = [
'field' => 'buyingPriceTtc',
'width' => 80,
'textAlign' => 'center',
'title' => 'Prix Achat <small>(TTC)</small>'
];
}
$output[] = [
'field' => 'price_ttc',
'width' => 80,
'textAlign' => 'center',
'title' => 'Prix Vente <small>(TTC)</small>'
];
$output[] = [
'field' => 'actions',
'width' => 70,
'textAlign' => 'center',
'title' => 'Actions'
];
return new JsonResponse($output);
}
/**
* @Route("/edit/{id}", name="produit_declination_value_edit", methods={"GET","POST"}, options={"expose"=true})
*/
public function edit(Request $request, ProduitDeclinationValue $produitDeclinationValue, EntityManagerInterface $em): Response
{
if ($request->isMethod('POST')) {
$item = (array) $request->request->get('produit_declination_value', []);
$reference = trim((string) ($item['reference'] ?? ''));
$name = trim((string) ($item['name'] ?? ''));
$description = array_key_exists('description', $item) ? (string) $item['description'] : null;
$buyingPriceHt = max(0, (float) ($item['buyingPriceHt'] ?? 0));
$priceHt = max(0, (float) ($item['priceHt'] ?? 0));
$tvaRate = 0.0;
if ($produitDeclinationValue->getProduit() && $produitDeclinationValue->getProduit()->getTva()) {
$tvaRate = (float) $produitDeclinationValue->getProduit()->getTva()->getNumber();
}
$buyingPriceTtc = $buyingPriceHt * (1 + ($tvaRate / 100));
$produitDeclinationValue
->setReference($reference)
->setName($name)
->setBarcode(isset($item['barcode']) && trim((string) $item['barcode']) !== '' ? trim((string) $item['barcode']) : null)
->setDescription($description)
->setBuyingPriceHt(round($buyingPriceHt, 3))
->setBuyingPriceTtc(round($buyingPriceTtc, 3))
->setPriceHt(round($priceHt, 3));
$em->flush();
$request->getSession()->getFlashBag()->add('success', 'Déclinaison modifiée avec succés');
return $this->redirectToRoute('produit_declination_value_show', [
'id' => $produitDeclinationValue->getId(),
'tab' => 'content_pricing_promo'
]);
}
return $this->render('@admin/produit_declination_value/_formModalEditDeclinaison.html.twig', [
'produitDeclinationValue' => $produitDeclinationValue
]);
}
/**
* @Route("/multi/add/{id}", name="produit_declination_value_multi_add", methods={"GET","POST"}, options={"expose"=true})
*/
public function multiAdd(Request $request, Produit $produit, EntityManagerInterface $em)
{
if ($request->isMethod('POST')) {
try {
$multiAdd = (array) $request->request->get('multi_add', []);
$declinationFixeRaw = (array) ($multiAdd['declinationFixe'] ?? []);
$declinationMultiRaw = (array) ($multiAdd['declinationMulti'] ?? []);
$normalizeDeclinationMap = static function (array $raw): array {
$out = [];
foreach ($raw as $declinationId => $values) {
$declinationId = (int) $declinationId;
if ($declinationId <= 0) {
continue;
}
$bucket = [];
if (is_array($values)) {
array_walk_recursive($values, static function ($v) use (&$bucket) {
$iv = (int) $v;
if ($iv > 0) {
$bucket[] = $iv;
}
});
} else {
$iv = (int) $values;
if ($iv > 0) {
$bucket[] = $iv;
}
}
$bucket = array_values(array_unique($bucket));
if (!empty($bucket)) {
$out[$declinationId] = $bucket;
}
}
return $out;
};
$declinationFixe = $normalizeDeclinationMap($declinationFixeRaw);
$declinationMulti = $normalizeDeclinationMap($declinationMultiRaw);
if (empty($declinationFixe) || empty($declinationMulti)) {
return new JsonResponse([
'success' => false,
'message' => 'Veuillez sélectionner au moins une déclinaison fixe et une déclinaison multiple.'
], 422);
}
$declinationPositions = [];
foreach ($produit->getDeclinations() as $d) {
$declinationPositions[(int) $d->getId()] = (int) $d->getPosition();
}
// 1) Vérifier que fixe et multiple ne contiennent pas la même déclinaison
$dupDeclinations = array_intersect(array_keys($declinationFixe), array_keys($declinationMulti));
if (!empty($dupDeclinations)) {
return new JsonResponse([
'success' => false,
'message' => 'Une même déclinaison ne peut pas être à la fois fixe et multiple.'
], 422);
}
// 2) Fusion sans perdre les clés (IDs déclinaisons)
$dimensions = [];
foreach ($declinationFixe as $declinationId => $valueIds) {
$dimensions[(int) $declinationId] = array_values(array_unique(array_map('intval', (array) $valueIds)));
}
foreach ($declinationMulti as $declinationId => $valueIds) {
$dimensions[(int) $declinationId] = array_values(array_unique(array_map('intval', (array) $valueIds)));
}
// Nettoyage final
foreach ($dimensions as $dId => $vals) {
$vals = array_values(array_filter($vals, static fn($v) => (int)$v > 0));
if (empty($vals)) {
unset($dimensions[$dId]);
} else {
$dimensions[$dId] = $vals;
}
}
if (empty($dimensions)) {
return new JsonResponse([
'success' => false,
'message' => 'Aucune valeur de déclinaison sélectionnée.'
], 422);
}
// Tri par position métier
uksort($dimensions, static function ($a, $b) use ($declinationPositions) {
$pa = $declinationPositions[(int) $a] ?? 999;
$pb = $declinationPositions[(int) $b] ?? 999;
return $pa <=> $pb;
});
$combinations = [[]];
foreach ($dimensions as $declinationId => $valueIds) {
$next = [];
foreach ($combinations as $base) {
foreach ($valueIds as $valueId) {
$row = $base;
$row[(int) $declinationId] = (int) $valueId;
$next[] = $row;
}
}
$combinations = $next;
}
if (empty($combinations)) {
return new JsonResponse([
'success' => false,
'message' => 'Aucune combinaison générée.'
], 422);
}
$expectedDeclinationIds = array_map('intval', array_keys($dimensions));
sort($expectedDeclinationIds);
$comboKey = static function (array $combo): string {
$norm = [];
foreach ($combo as $dId => $vId) {
$d = (int) $dId;
$v = (int) $vId;
if ($d > 0 && $v > 0) {
$norm[$d] = $v;
}
}
ksort($norm);
return json_encode($norm);
};
$comboLabel = function (array $combo): string {
$parts = [];
foreach ($combo as $declinationId => $valueId) {
$decl = $this->declinationRepository->find((int) $declinationId);
$val = $this->valueDeclinationRepository->find((int) $valueId);
if ($decl && $val) {
$parts[] = $decl->getName() . ': ' . $val->getName();
}
}
return !empty($parts) ? implode(' | ', $parts) : 'combinaison invalide';
};
$existing = $this->listGroupDeclination($produit->getProduitDeclinationValues());
$seen = [];
foreach ($existing as $ex) {
$seen[$comboKey($ex)] = true;
}
$selectedImages = $multiAdd['images'] ?? [];
if (!is_array($selectedImages)) {
$selectedImages = [$selectedImages];
}
$selectedImages = array_values(array_unique(array_filter(array_map('intval', $selectedImages))));
$addedCount = 0;
$skippedLabels = [];
foreach ($combinations as $combo) {
$comboDeclIds = array_map('intval', array_keys($combo));
sort($comboDeclIds);
// sécurité: combo complète (fixe + multiple)
if ($comboDeclIds !== $expectedDeclinationIds) {
$skippedLabels[] = $comboLabel($combo);
continue;
}
$key = $comboKey($combo);
if (isset($seen[$key])) {
$skippedLabels[] = $comboLabel($combo);
continue;
}
// Résolution stricte: si une dimension est invalide, on ignore la ligne
$resolved = [];
$invalid = false;
foreach ($combo as $declinationId => $valueId) {
$valueDeclination = $this->valueDeclinationRepository->find((int) $valueId);
$declination = $this->declinationRepository->find((int) $declinationId);
if (!$valueDeclination || !$declination) {
$invalid = true;
break;
}
$resolved[] = [
'declination' => $declination,
'value' => $valueDeclination,
'position' => (int) $declination->getPosition(),
'name' => (string) $valueDeclination->getName(),
];
}
if ($invalid || count($resolved) !== count($expectedDeclinationIds)) {
$skippedLabels[] = $comboLabel($combo);
continue;
}
usort($resolved, static fn($a, $b) => $a['position'] <=> $b['position']);
$entity = new ProduitDeclinationValue();
$entity->setBuyingPriceTtc($produit->getBuyingPriceTtc())
->setDescription($produit->getDescription())
->setPriceHt($produit->getPriceHt())
->setCreatedAt(new \DateTime('now'))
->setProduit($produit);
$reference = $produit->getReference();
$name = $produit->getName();
foreach ($resolved as $item) {
$group = new GroupDeclinationValue();
$group->setValue($item['value']);
$group->setDeclination($item['declination']);
$group->setProduitDeclination($entity);
$em->persist($group);
$reference .= '-' . mb_strtolower($item['name']);
$name .= ' ' . mb_strtolower($item['name']);
}
$entity->setReference($reference)->setName($name);
foreach ($selectedImages as $imgId) {
$imageSelected = $this->fileRepository->find((int) $imgId);
if ($imageSelected) {
$entity->addPicture($imageSelected);
}
}
$stock = new Stock();
$stock->setQtReserved(0)
->setProduit($produit)
->setQtStock(0)
->setDeclinationProduit($entity);
$this->assignDefaultWarehouseToStock($stock, $em);
$em->persist($stock);
$em->persist($entity);
$seen[$key] = true;
$addedCount++;
}
$em->flush();
$skippedLabels = array_values(array_unique(array_filter($skippedLabels)));
$message = $addedCount . ' déclinaison(s) ajoutée(s).';
if (!empty($skippedLabels)) {
$message .= ' Ignorées (déjà existantes/invalides): ' . implode(' ; ', $skippedLabels);
}
return new JsonResponse([
'success' => true,
'message' => $message
]);
} catch (\Throwable $e) {
return new JsonResponse([
'success' => false,
'message' => 'Erreur technique: ' . $e->getMessage()
], 500);
}
}
$fixe = [];
$multi = [];
$declinations = $produit->getDeclinations()->toArray();
usort($declinations, static function ($a, $b) {
return (int) $a->getPosition() <=> (int) $b->getPosition();
});
// Par défaut:
// - position la plus petite => fixe
// - toutes les autres => multiples
if (!empty($declinations)) {
$fixe[] = $declinations[0];
for ($i = 1; $i < count($declinations); $i++) {
$multi[] = $declinations[$i];
}
}
return $this->render('admin/produit_declination_value/multi-add.html.twig', [
'produit' => $produit,
'fixe' => $fixe,
'multi' => $multi,
]);
}
/**
* @Route("/modal/{id}", name="produit_declinaisons_modal_refresh", methods={"GET"}, options={"expose"=true})
*/
public function refreshDeclinaisonsModal(Produit $produit): Response
{
$rights = $this->rightService->getAllRights($this->getUser());
return $this->render('@admin/includes/modals/_produit_declinaisons_modal.html.twig', [
'produit' => $produit,
'rights' => $rights,
]);
}
/**
* @Route("/threshold/{id}", name="produit_declination_threshold_update", methods={"POST"}, options={"expose"=true})
*/
public function updateThreshold(ProduitDeclinationValue $decli, Request $request, EntityManagerInterface $em): JsonResponse
{
$threshold = max(0, (int) $request->request->get('threshold', 0));
$decli->setAlertStockMin($threshold);
$em->flush();
return new JsonResponse([
'success' => true,
'message' => 'Seuil mis à jour.',
'threshold' => $threshold
]);
}
/**
* @Route("/threshold/bulk/{produit}", name="produit_declination_threshold_bulk_update", methods={"POST"}, options={"expose"=true})
*/
public function updateThresholdBulk(Produit $produit, Request $request, EntityManagerInterface $em): JsonResponse
{
$threshold = max(0, (int) $request->request->get('threshold', 0));
foreach ($produit->getProduitDeclinationValues() as $decli) {
$decli->setAlertStockMin($threshold);
}
$em->flush();
return new JsonResponse([
'success' => true,
'message' => 'Seuil appliqué à toutes les déclinaisons.',
'threshold' => $threshold
]);
}
/**
* @Route("/multi/select", name="select_multi_add", methods={"GET","POST"}, options={"expose"=true})
*/
public function selectMulti(Request $request): Response
{
$multiAdd = (array) $request->request->get('multi_add', []);
$fixeIds = array_values(array_filter(array_map('intval', (array) ($multiAdd['fixe'] ?? []))));
$multiIds = array_values(array_filter(array_map('intval', (array) ($multiAdd['multiple'] ?? []))));
$fixe = [];
$multi = [];
foreach ($fixeIds as $id) {
$declination = $this->declinationRepository->find($id);
if ($declination) {
$fixe[] = $declination;
}
}
foreach ($multiIds as $id) {
$declination = $this->declinationRepository->find($id);
if ($declination) {
$multi[] = $declination;
}
}
return $this->render('@admin/produit_declination_value/formule.html.twig', [
'fixe' => $fixe,
'multi' => $multi,
]);
}
public function listGroupDeclination($declinations) {
$list = [];
foreach ($declinations as $declination) {
$listGroup = [];
foreach ($declination->getGroupDeclinationValues() as $group) {
$listGroup[$group->getDeclination()->getId()] = (string) $group->getValue()->getId();
}
$list[] = $listGroup;
}
return $list;
}
public function sortAssociativeArrayByKey($array, $key, $direction) {
switch ($direction) {
case "ASC":
usort($array, function ($first, $second) use ($key) {
return $first[$key] <=> $second[$key];
});
break;
case "DESC":
usort($array, function ($first, $second) use ($key) {
return $second[$key] <=> $first[$key];
});
break;
default:
break;
}
return $array;
}
/**
* @Route("/search", name="search_item", methods={"GET","POST"}, options = { "expose" = true})
*/
public function searchItem(Request $request,EntityManagerInterface $em) {
$types=$request->get("type")??["declinaison"];
$entitiesproduit=$entities=[];
$hideSupplierReceptionPrices = $request->get('document_category') === 'fournisseur'
&& !$this->currentUserHasRight('DOCUMENT_SUPPLIER_RECEPTION_PRICE');
$isAvailable = filter_var($request->get('is_available', false), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$isAvailable = $isAvailable === true;
$restrictSupplier = filter_var($request->get('restrict_supplier', false), FILTER_VALIDATE_BOOLEAN);
$supplierId = $restrictSupplier ? (int) $request->get('supplier_id', 0) : 0;
if($types){
if(in_array("declinaison",$types)){
$entities = $this->produitDeclinationValueRepository->searchItem(
$request->get('query'),
$isAvailable,
count($types) == 2 ? 6 : 12,
false,
$supplierId > 0 ? $supplierId : null
);
foreach ($entities as &$entity) {
$qtReserved=$quantity = 0;
$incomingQuantity = 0;
$incomingDateEstimated = null;
$exchangeIncomingQuantity = 0;
$entity['type'] = 'produitDeclination';
$declinaison = $this->produitDeclinationValueRepository->find($entity['id']);
$produit = $declinaison ? $declinaison->getProduit() : null;
$stocks = $em->getRepository(Stock::class)->findByDeclinationProduit($entity['id']);
foreach ($stocks as $stock) {
$quantity = $quantity + ($stock->getQtStock() - $stock->getQtReserved());
$qtReserved+=$stock->getQtReserved();
$incomingQuantity += (int) $stock->getIncomingQuantity();
$exchangeIncomingQuantity += (int) $stock->getExchangeIncomingQuantity();
$candidateIncomingDate = $stock->getIncomingDateEstimated();
if ($candidateIncomingDate && ($incomingDateEstimated === null || $candidateIncomingDate < $incomingDateEstimated)) {
$incomingDateEstimated = $candidateIncomingDate;
}
}
$entity['quantity'] =( $quantity<=3 or $this->isGranted('ROLE_SUPER_ADMIN') or $this->currentUserHasRight('STOCK_SEE_REAL_STOCK') )?$quantity:"3<sup>+</sup>";
$entity['qtReserved'] =$qtReserved;
$entity['incomingQuantity'] = $incomingQuantity;
$entity['incomingRemainingQuantity'] = $declinaison ? $declinaison->getQtyIncomingRemaining() : $incomingQuantity;
$entity['incomingDateEstimated'] = $incomingDateEstimated?->format('d/m/Y H:i');
$entity['exchangeIncomingQuantity'] = $exchangeIncomingQuantity;
$entity['exchangeIncomingDetailsUrl'] = $exchangeIncomingQuantity > 0
? $this->buildExchangeIncomingDetailsUrl($declinaison)
: null;
$tvaEntity = $produit ? $produit->getTva() : null;
$entity['tva_id'] = $tvaEntity ? $tvaEntity->getId() : null;
$entity['tva'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
$entity['tva_number'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
$promotion=$produit->getPromotion();
$entity['value'] = null;
$entity['promotionType'] = null;
$in_promo=false;
$entity['price_ttc'] =$entity['price_ttc_without_promo'] = $produit->getPriceTtc();
if( $promotion) {
$date = new \DateTime('now');
if( $promotion->getStartAt() <= $date && $promotion->getEndAt() >= $date) {
$entity['value'] = $promotion->getDiscountValue();
$entity['promotionType'] = $promotion->getDiscountType();
$entity['price_ttc'] =($promotion->getDiscountType() == 'percent')?
$produit->getPriceTtc() - ((($produit->getPriceTtc() / 100) * $promotion->getDiscountValue()))
: $produit->getPriceTtc() - $promotion->getDiscountValue();
$in_promo=true;
}
}
$entity['in_promo'] = $in_promo;
$entity['material'] = $produit ? $produit->getMaterial() : null;
$picture = $this->getPicture($this->produitDeclinationValueRepository->find($entity['id']), $request);
$entity['picture'] = $picture;
if ($hideSupplierReceptionPrices) {
$entity['buyingPriceHt'] = null;
$entity['buyingPriceTtc'] = null;
}
}
unset($entity);
usort($entities, function ($left, $right) {
$leftEntity = $this->produitDeclinationValueRepository->find($left['id'] ?? null);
$rightEntity = $this->produitDeclinationValueRepository->find($right['id'] ?? null);
$extractValues = function ($declinationEntity) {
$position1 = '';
$position2 = '';
if (!$declinationEntity) {
return [$position1, $position2];
}
foreach ($declinationEntity->getGroupDeclinationValues() as $groupValue) {
$declination = $groupValue->getDeclination();
$value = $groupValue->getValue();
if (!$declination || !$value) {
continue;
}
$declinationId = (int) $declination->getId();
$valueName = (string) $value->getName();
if ($declinationId === 2) {
$position1 = $valueName;
} else {
$position2 = $valueName;
}
}
return [$position1, $position2];
};
[$leftPosition1, $leftPosition2] = $extractValues($leftEntity);
[$rightPosition1, $rightPosition2] = $extractValues($rightEntity);
$position1Compare = strcasecmp($leftPosition1, $rightPosition1);
if ($position1Compare !== 0) {
return $position1Compare;
}
$position2Compare = strnatcasecmp($leftPosition2, $rightPosition2);
if ($position2Compare !== 0) {
return $position2Compare;
}
return strcasecmp((string) ($left['reference'] ?? ''), (string) ($right['reference'] ?? ''));
});
}
if(in_array("product",$types)) {
$entitiesproduit = $this->produitRepository->searchItem($request->get('query'),count($types)==2?6:12);
foreach ($entitiesproduit as &$entity) {
$qtReserved = $quantity = 0;
$incomingQuantity = 0;
$incomingDateEstimated = null;
$exchangeIncomingQuantity = 0;
$stocks = $em->getRepository(Stock::class)->findByProduit($entity['id']);
foreach ($stocks as $stock) {
$quantity = $quantity + ($stock->getQtStock() - $stock->getQtReserved());
$qtReserved += $stock->getQtReserved();
$incomingQuantity += (int) $stock->getIncomingQuantity();
$exchangeIncomingQuantity += (int) $stock->getExchangeIncomingQuantity();
$candidateIncomingDate = $stock->getIncomingDateEstimated();
if ($candidateIncomingDate && ($incomingDateEstimated === null || $candidateIncomingDate < $incomingDateEstimated)) {
$incomingDateEstimated = $candidateIncomingDate;
}
}
$entity['quantity'] =( $quantity<=3 or $this->isGranted('ROLE_SUPER_ADMIN') or $this->currentUserHasRight('STOCK_SEE_REAL_STOCK') )?$quantity:"3<sup>+</sup>";
$entity['qtReserved'] = $qtReserved;
$entity['incomingQuantity'] = $incomingQuantity;
$entity['incomingRemainingQuantity'] = $produit?->getIncomingRemainingQuantity() ?? $incomingQuantity;
$entity['incomingDateEstimated'] = $incomingDateEstimated?->format('d/m/Y H:i');
$entity['exchangeIncomingQuantity'] = $exchangeIncomingQuantity;
$produit = $this->produitRepository->find($entity['id']);
$tvaEntity = $produit ? $produit->getTva() : null;
$entity['tva_id'] = $tvaEntity ? $tvaEntity->getId() : null;
$entity['tva'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
$entity['tva_number'] = $tvaEntity ? (float) $tvaEntity->getNumber() : null;
$entity['material'] = $produit ? $produit->getMaterial() : null;
$promotion = $produit->getPromotion();
$in_promo = false;
$entity['value'] = null;
$entity['promotionType'] = null;
$entity['price_ttc'] = $entity['price_ttc_without_promo'] = $produit->getPriceTtc();
if( $promotion) {
$date = new \DateTime('now');
if( $promotion->getStartAt() <= $date && $promotion->getEndAt() >= $date) {
$entity['value'] = $promotion->getDiscountValue();
$entity['promotionType'] = $promotion->getDiscountType();
$entity['price_ttc'] = ($promotion->getDiscountType() == 'percent') ?
$produit->getPriceTtc() - ((($produit->getPriceTtc() / 100) * $promotion->getDiscountValue()))
: $produit->getPriceTtc() - $promotion->getDiscountValue();
$in_promo = true;
}
}
$entity['in_promo'] = $in_promo;
$entity['type'] = 'produit';
$picture = $this->getPicture($this->produitRepository->find($entity['id']), $request);
$entity['picture'] = $picture;
}
}
}
$data = array_merge($entitiesproduit, $entities);
return new JsonResponse($data);
}
/**
* @Route("/list", name="list_produit_declination_table", methods={"GET","POST"}, options = { "expose" = true})
*/
public function listDatatable(Request $request, ProduitDeclinationValueRepository $produitDecRepository): JsonResponse
{
$draw = (int) $request->get('draw', 1);
$start = (int) $request->get('start', 0);
$length = (int) $request->get('length', 20);
$page = (int) floor($start / max(1, $length));
$limit = max(1, $length);
$order = (array) $request->get('order', []);
$columns = (array) $request->get('columns', []);
$sortField = 'declinationPositions';
$sortType = 'ASC';
$allowedFields = ['reference', 'name', 'createdAt', 'qtStock'];
if (!empty($order) && isset($columns[$order[0]['column']]['data'])) {
$candidate = (string) $columns[$order[0]['column']]['data'];
$dir = strtoupper((string) ($order[0]['dir'] ?? 'DESC'));
if (in_array($candidate, $allowedFields, true)) $sortField = $candidate;
$sortType = in_array($dir, ['ASC', 'DESC'], true) ? $dir : 'DESC';
}
$rootParams = (array) $request->query->all();
$queryParams = is_array($rootParams['query'] ?? null) ? $rootParams['query'] : (array) $request->get('query', []);
$getParam = function (string $key, $default = null) use ($rootParams, $queryParams) {
if (array_key_exists($key, $rootParams)) return $rootParams[$key];
if (array_key_exists($key, $queryParams)) return $queryParams[$key];
return $default;
};
$declinationFilters = $getParam('declinations', $request->get('declinations', []));
if (is_string($declinationFilters)) {
$decoded = json_decode($declinationFilters, true);
$declinationFilters = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
}
if (!is_array($declinationFilters)) $declinationFilters = [];
$normalizedDeclinationFilters = [];
foreach ($declinationFilters as $k => $v) {
$key = (int) $k;
$normalizedDeclinationFilters[$key] = is_array($v) ? array_map('intval', $v) : (int) $v;
}
$declinationFilters = $normalizedDeclinationFilters;
$reference = $getParam('reference');
$name = $getParam('name');
$categories = $getParam('categories');
$isAvailable = $getParam('isAvailable');
$inPromo = $getParam('inPromo');
$qtMin = $getParam('qtMin');
$qtMax = $getParam('qtMax');
$buyingPriceMin = $getParam('buyingPriceMin');
$buyingPriceMax = $getParam('buyingPriceMax');
$priceMin = $getParam('priceMin');
$priceMax = $getParam('priceMax');
$withDeleted = $getParam('withDeleted') == 'true' || $getParam('withDeleted') === true || $getParam('withDeleted') === 1 || $getParam('withDeleted') === '1';
$hasIsAvailable = array_key_exists('isAvailable', $rootParams) || array_key_exists('isAvailable', $queryParams);
if (!$hasIsAvailable) $isAvailable = '1';
$warehouseRaw = $getParam('warehouse', $request->get('warehouse'));
$currentWarehouseId = ($warehouseRaw !== null && $warehouseRaw !== '')
? (int) $warehouseRaw
: ($this->warehouseContextService->getCurrentWarehouse($this->getUser())?->getId() ?? null);
$result = $produitDecRepository->searchAndCountProduitDeclinations(
$page,
$limit,
$reference,
$name,
$categories,
$isAvailable,
$inPromo,
$declinationFilters,
$qtMin,
$qtMax,
$buyingPriceMin,
$buyingPriceMax,
$priceMin,
$priceMax,
$sortField,
$sortType,
$withDeleted,
$currentWarehouseId
);
$entities = $result['data'] ?? [];
$count = (int) ($result['total'] ?? 0);
$metaDecli1Name = null;
$metaDecli2Name = null;
$data = [];
foreach ($entities as $entity) {
$quantity = 0;
$qtReserved = 0;
$incomingQuantity = 0;
$incomingRemainingQuantity = 0;
$incomingDateEstimated = null;
$exchangeIncomingQuantity = 0;
foreach ($entity->getStocks() as $stock) {
if ($currentWarehouseId && (int) ($stock->getWarehouse()?->getId() ?? 0) !== (int) $currentWarehouseId) {
continue;
}
//$quantity += max(0, (int) $stock->getQtStock() - (int) $stock->getQtReserved());
$quantity += ((int) $stock->getQtStock() - (int) $stock->getQtReserved());
$qtReserved += (int) $stock->getQtReserved();
$incomingQuantity += (int) $stock->getIncomingQuantity();
$incomingRemainingQuantity += (int) $stock->getIncomingRemainingQuantity();
$exchangeIncomingQuantity += (int) $stock->getExchangeIncomingQuantity();
$candidateIncomingDate = $stock->getIncomingDateEstimated();
if ($candidateIncomingDate && ($incomingDateEstimated === null || $candidateIncomingDate < $incomingDateEstimated)) {
$incomingDateEstimated = $candidateIncomingDate;
}
}
$declinationGroups = $entity->getGroupDeclinationValues()->toArray();
usort($declinationGroups, function ($a, $b) {
return $a->getDeclination()->getPosition() <=> $b->getDeclination()->getPosition();
});
$decli1 = isset($declinationGroups[0]) ? $declinationGroups[0] : null;
$decli2 = isset($declinationGroups[1]) ? $declinationGroups[1] : null;
$decli1Name = $decli1 ? $decli1->getDeclination()->getName() : null;
$decli1Val = $decli1 ? $decli1->getValue()->getName() : null;
$decli2Name = $decli2 ? $decli2->getDeclination()->getName() : null;
$decli2Val = $decli2 ? $decli2->getValue()->getName() : null;
if ($metaDecli1Name === null && $decli1Name) $metaDecli1Name = $decli1Name;
if ($metaDecli2Name === null && $decli2Name) $metaDecli2Name = $decli2Name;
$base = (float) $entity->getProduit()->getPriceTtc();
$final = $base;
$promoActive = false;
$promoType = null;
$promoValue = null;
$promoPercent = null;
$promotion = $entity->getProduit()->getPromotion();
if ($promotion) {
$now = new \DateTime('now');
if ($promotion->getStartAt() <= $now && $promotion->getEndAt() >= $now) {
$promoActive = true;
$promoType = $promotion->getDiscountType();
$promoValue = (float) $promotion->getDiscountValue();
if ($promoType === 'percent') {
$final = round($base * (1 - $promoValue / 100), 3);
$promoPercent = (int) round($promoValue);
} else {
$final = round(max(0, $base - $promoValue), 3);
$promoPercent = $base > 0 ? (int) round((($base - $final) / $base) * 100) : 0;
}
}
}
$pictures = $entity->getPicture()->filter(fn($p) => $p->getIsSelected() == 1);
$pictureName = $pictures->isEmpty() ? null : $pictures->first()->getImageName();
if (
!$pictures->isEmpty()
&& $pictureName
&& file_exists($this->getParameter('kernel.project_dir') . "/public/images/" . $pictureName)
&& !file_exists($this->getParameter('kernel.project_dir') . "/public/images/web/" . $pictureName)
) {
$this->resizeImage($pictureName);
}
$canSeeRealStock = $this->isGranted('ROLE_SUPER_ADMIN') || $this->currentUserHasRight('STOCK_SEE_REAL_STOCK');
$quantityDisplay = ($quantity <= 3 || $canSeeRealStock) ? $quantity : "3<sup>+</sup>";
$isResellerView = $this->isGranted('ROLE_RESELLER') && !$this->isGranted('ROLE_SUPER_ADMIN');
$displayPrice = $isResellerView ? (float) ($entity->getProduit()->getResellerPriceTtc() ?? $final) : $final;
$displayBasePrice = $isResellerView ? (float) ($entity->getProduit()->getResellerPriceTtc() ?? $base) : $base;
$data[] = [
'id' => $entity->getId(),
'image' => $pictureName,
'name' => $entity->getName(),
'reference' => $entity->getReference(),
'material' => $entity->getProduit()?->getMaterial(),
'parent' => $entity->getProduit()->getReference(),
'parent_id' => $entity->getProduit()->getId(),
'parent_name' => $entity->getProduit()->getName(),
'quick_order_url' => $this->generateUrl('product_quick_order', [
'id' => $entity->getProduit()->getId(),
'name' => $entity->getProduit()->getName() ? $this->slugify($entity->getProduit()->getName()) : 'produit',
], UrlGeneratorInterface::ABSOLUTE_URL),
'buyingPriceTtc' => $this->isGranted('ROLE_SUPER_ADMIN') ? (float) $entity->getBuyingPriceTtc() : null,
'price_ht' => (float) $entity->getPriceHt(),
'price_ttc' => $displayPrice,
'price_ttc_final' => $displayPrice,
'price_ttc_original' => $displayBasePrice,
'resellerPriceTtc' => (float) ($entity->getProduit()->getResellerPriceTtc() ?? 0),
'promo_active' => $promoActive,
'promo_type' => $promoType,
'promo_value' => $promoValue,
'promo_percent' => $promoPercent,
'decli1_name' => $decli1Name,
'decli1_value' => $decli1Val,
'decli2_name' => $decli2Name,
'decli2_value' => $decli2Val,
'qtStock' => $quantity,
'quantity' => $quantityDisplay,
'qtReserved' => $qtReserved,
'incomingQuantity' => $incomingQuantity,
'incomingRemainingQuantity' => $incomingRemainingQuantity,
'incomingDateEstimated' => $incomingDateEstimated?->format('d/m/Y H:i'),
'exchangeIncomingQuantity' => $exchangeIncomingQuantity,
'exchangeIncomingDetailsUrl' => $exchangeIncomingQuantity > 0
? $this->buildExchangeIncomingDetailsUrl($entity, $currentWarehouseId > 0 ? $currentWarehouseId : null)
: null,
'createdAt' => $entity->getCreatedAt()?->format('Y-m-d\TH:i:s'),
'inPromo' => $promoActive,
];
}
return $this->json([
'draw' => $draw,
'recordsTotal' => $count,
'recordsFiltered' => $count,
'data' => $data,
'meta' => [
'decli1_name' => $metaDecli1Name,
'decli2_name' => $metaDecli2Name,
],
]);
}
/**
* @Route("/exchange-incoming/recalculate", name="produit_declination_exchange_incoming_recalculate", methods={"POST"}, options={"expose"=true})
*/
public function recalculateExchangeIncoming(
Request $request,
StockExchangeIncomingService $stockExchangeIncomingService,
EntityManagerInterface $em
): JsonResponse {
$rights = $this->rightService->getAllRights($this->getUser());
if (!$this->canRecalculateExchangeIncoming($rights)) {
return new JsonResponse([
'success' => false,
'message' => 'Accès refusé.',
], 403);
}
$result = $stockExchangeIncomingService->recalculateAll();
$em->flush();
return new JsonResponse([
'success' => true,
'message' => sprintf(
'Recalcul terminé : %d bon(s), %d ligne(s), %d stock(s) mis à jour.',
(int) ($result['documents'] ?? 0),
(int) ($result['lines'] ?? 0),
(int) ($result['stocks'] ?? 0)
),
'summary' => $result,
]);
}
/**
* @Route("/stock/edit/{id}", name="stock_edit", methods={"GET","POST"}, options = { "expose" = true})
*/
public function editStock(Request $request, Stock $stock,EntityManagerInterface $em) {
if( $request->isMethod('POST')) {
$stock->setQtStock((int) $request->get('edit_stock')['qt_total']);
$em->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'success' => true,
'message' => 'Stock modifie avec succes.'
]);
}
$request->getSession()->getFlashBag()->add('success', 'Stock modifie avec succes');
}
return $this->redirectToRoute('produit_declination_value_show', ['id' => $stock->getDeclinationProduit()->getId()]);
}
public function getPicture($entity, $request): string
{
$baseurl = $request->getScheme() . '://' . $request->getHttpHost() . $request->getBasePath();
if ($entity->getPicture()) {
foreach ($entity->getPicture() as $item) {
if ($item->getIsSelected()) {
$path = $this->uploaderHelper->asset($item, 'file');
if ($path) {
return $baseurl . $path;
}
}
}
}
// Aucune image sélectionnée
return '';
}
/**
* @Route("/file/{id}", name="produit_declination_value_file", methods={"GET","POST"}, options={"expose"=true})
*/
public function addFile(Request $request, ProduitDeclinationValue $produit, EntityManagerInterface $em): Response
{
$selectFileId = $request->request->get('selectFile');
$isAjax = $request->isXmlHttpRequest();
if ($isAjax && $selectFileId) {
foreach ($produit->getPicture() as $picture) {
$picture->setIsSelected(false);
}
$imageSelected = $this->fileRepository->find($selectFileId);
if (!$imageSelected) {
return new JsonResponse(['success' => false, 'message' => 'Image introuvable'], 404);
}
$imageSelected->setIsSelected(true);
$em->flush();
return new JsonResponse([
'success' => true,
'message' => 'Image principale mise a jour.'
]);
}
$form = $this->createForm(UploadFileProduitDecType::class, $produit);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$filesBag = $request->files->get('upload_file_produit_dec', []);
$pictures = $filesBag['picture'] ?? [];
if (!is_array($pictures)) {
$pictures = [$pictures];
}
$hasNewImages = false;
foreach ($pictures as $key => $picture) {
if (!$picture) {
continue;
}
$file = new File();
$file->setFile($picture);
if (!$selectFileId && $key === 0) {
foreach ($produit->getPicture() as $existingPicture) {
$existingPicture->setIsSelected(false);
}
$file->setIsSelected(true);
}
$em->persist($file);
$produit->addPicture($file);
$hasNewImages = true;
}
if ($hasNewImages) {
$em->flush();
if ($isAjax) {
return new JsonResponse([
'success' => true,
'message' => 'Images ajoutees avec succes.'
]);
}
$request->getSession()->getFlashBag()->add('success', 'Images ajoutees avec succes');
}
return $this->redirectToRoute('produit_declination_value_show', [
'id' => $produit->getId(),
'tab' => 'content_galery'
]);
}
if ($isAjax) {
return new JsonResponse(['success' => false, 'message' => 'Requete invalide'], 400);
}
return $this->redirectToRoute('produit_declination_value_show', [
'id' => $produit->getId(),
'tab' => 'content_galery'
]);
}
/**
* @Route("/searchgroup", name="get_group_produit", methods={"GET","POST"}, options = { "expose" = true})
*/
public function searchGroupProduit(Request $request,EntityManagerInterface $em) {
$result = [];
$hideSupplierReceptionPrices = $request->get('document_category') === 'fournisseur'
&& !$this->currentUserHasRight('DOCUMENT_SUPPLIER_RECEPTION_PRICE');
$restrictSupplier = filter_var($request->get('restrict_supplier', false), FILTER_VALIDATE_BOOLEAN);
$supplierId = $restrictSupplier ? (int) $request->get('supplier_id', 0) : 0;
$entities = $this->produitDeclinationValueRepository->findDeclinationValueWithDeclination(
$request->get('color'),
$request->get('produit'),
$supplierId > 0 ? $supplierId : null
);
$entities = $this->orderWithSise($entities);
foreach ($entities as $entity) {
$quantity = 0;
$array['type'] = 'produitDeclination';
$stocks = $em->getRepository(Stock::class)->findByDeclinationProduit($entity->getId());
foreach ($stocks as $stock) {
$quantity = $quantity + ($stock->getQtStock() - $stock->getQtReserved());
}
$promotion = $this->produitDeclinationValueRepository->find($entity->getId())->getProduit()->getPromotion();
$array['value'] = null;
$array['promotionType'] = null;
if( $promotion) {
$date = new \DateTime('now');
if( $promotion->getStartAt() <= $date && $promotion->getEndAt() >= $date) {
$array['value'] = $promotion->getDiscountValue();
$array['promotionType'] = $promotion->getDiscountType();
}
}
$picture = $this->getPicture($this->produitDeclinationValueRepository->find($entity->getId()), $request);
$array['picture'] = $picture;
$array['quantity'] = $quantity;
$array['id'] = $entity->getId();
$array['name'] = $entity->getName();
$array['reference'] = $entity->getReference();
$array['description'] = $entity->getDescription();
$array['price_ht'] = $entity->getPriceHt();
$array['price_ttc'] = $entity->getProduit()->getPriceTtc();
$array['unit'] = $entity->getProduit()->getUnit();
$array['buyingPriceHt'] = $hideSupplierReceptionPrices ? null : $entity->getBuyingPriceHt();
$array['buyingPriceTtc'] = $hideSupplierReceptionPrices ? null : $entity->getBuyingPriceTtc();
$array['tva'] = $entity->getProduit()->getTva() ? $entity->getProduit()->getTva()->getNumber() : 0;
$array['tva_id'] = $entity->getProduit()->getTva() ? $entity->getProduit()->getTva()->getId() : 1;
$result[] = $array;
}
return new JsonResponse($result);
}
public function orderWithSise($entities) {
$allSize = ["XS", "S", "M", "L", "XL", "XXL", "XXXL", "XXXXL"];
$isLetter = false;
if($entities)
foreach ($entities[0]->getGroupDeclinationValues() as $group) {
if( $group->getDeclination()->getName() == "Taille") {
if( in_array($group->getValue()->getName(), $allSize)) $isLetter = true;
}
}
if( $isLetter == true) {
usort($entities, function ($a, $b) use ($allSize) {
foreach ($a->getGroupDeclinationValues() as $group) {
if( $group->getDeclination()->getName() == "Taille")
$pos_a = array_search($group->getValue()->getName(), $allSize);
}
foreach ($b->getGroupDeclinationValues() as $group) {
if( $group->getDeclination()->getName() == "Taille")
$pos_b = array_search($group->getValue()->getName(), $allSize);
}
return $pos_a - $pos_b;
});
}
return $entities;
}
/**
* @Route("/info/edit/{id}", name="produit_dec_info_edit", methods={"GET","POST"}, options={"expose"=true})
*/
public function editInfo(Request $request, ProduitDeclinationValue $produit,EntityManagerInterface $em): Response {
$this->hasRight($request,'PRODUIT_UPDATE');
if( $request->isMethod('POST') && $request->get('form_info')) {
$produit->setDescription($request->get('form_info')['description']);
$produit->setName($request->get('form_info')['name']);
$produit->setBarcode(isset($request->get('form_info')['barcode']) && trim((string) $request->get('form_info')['barcode']) !== '' ? trim((string) $request->get('form_info')['barcode']) : null);
$message = $this->getUser()->getFirstName() . " a modifié l'info de Déclinaison " . $produit->getReference();
$this->activityService->addActivity('info', $message, $produit->getProduit(), $this->getUser(), 'produit');
$em->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'result' => 1,
'message' => 'Informations de la declinaison mises a jour.',
]);
}
return $this->redirectToRoute('produit_declination_value_show', ['id' => $produit->getId(),"tab"=>"content_informations"]);
}
return false;
}
}