src/Controller/Admin/Sales/SalesController.php line 1430

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Admin\Sales;
  3. use App\Dto\CalculatorProductsCostsDTO;
  4. use App\Dto\NewStockDto\SalesProductsDto;
  5. use App\Dto\PaginationDTO;
  6. use App\Dto\SalesProductsDtoBuilder;
  7. use App\Entity\Customer;
  8. use App\Entity\MeasurementUnits;
  9. use App\Entity\Payment;
  10. use App\Entity\ProductsSold;
  11. use App\Entity\Sales;
  12. use App\Entity\SalesHistory;
  13. use App\Entity\Stock;
  14. use App\Entity\StockTransaction;
  15. use App\Entity\User;
  16. use App\Entity\Warehouse;
  17. use App\Enum\SalesHistoryEventType;
  18. use App\Enum\SalesStatus;
  19. use App\Enum\SalesType;
  20. use App\Exception\IncorrectAmountException;
  21. use App\Exception\StockNotAvaibleException;
  22. use App\Exception\StockNotFoundException;
  23. use App\Form\CustomerType;
  24. use App\Form\PaymentType;
  25. use App\Form\ProductsSoldType;
  26. use App\Form\SalesFormType;
  27. use App\Repository\SalesRepository;
  28. use App\Repository\StockRepository;
  29. use App\Services\Customer\CustomerService;
  30. use App\Services\MailService;
  31. use App\Services\MeasurementConversions\MeasurementConversionsService;
  32. use App\Services\PDFService;
  33. use App\Services\LogService;
  34. use App\Services\Payment\PaymentMethodService;
  35. use App\Services\Payment\PaymentReminderService;
  36. use App\Services\Payment\PaymentService;
  37. use App\Services\Product\ProductService;
  38. use App\Services\Sales\Impl\SalesProductsServiceImpl;
  39. use App\Services\Sales\Impl\SalesServiceImpl;
  40. use App\Services\Sales\InvoiceService;
  41. use App\Services\Sales\SalesHistoryService;
  42. use App\Services\Sales\SalesService;
  43. use App\Repository\SalesReturnRepository;
  44. use App\Services\Stock\StockInfoService;
  45. use App\Services\Stock\StockService;
  46. use App\Services\Stock\StockTransferService;
  47. use App\Services\StockTransactionService;
  48. use App\Services\Warehouse\WarehouseService;
  49. use App\Utils\CurrencyHelper;
  50. use App\Utils\MailTemplateHelper;
  51. use Doctrine\DBAL\LockMode;
  52. use Doctrine\Common\Collections\ArrayCollection;
  53. use Doctrine\ORM\EntityManagerInterface;
  54. use Doctrine\ORM\NonUniqueResultException;
  55. use Doctrine\ORM\NoResultException;
  56. use Exception;
  57. use Knp\Bundle\SnappyBundle\Snappy\Response\PdfResponse;
  58. use Knp\Snappy\Pdf;
  59. use PhpOffice\PhpSpreadsheet\IOFactory;
  60. use Psr\Log\LoggerInterface;
  61. use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  62. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  63. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  64. use Symfony\Component\HttpFoundation\JsonResponse;
  65. use Symfony\Component\HttpFoundation\Request;
  66. use Symfony\Component\HttpFoundation\Response;
  67. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  68. use Symfony\Component\HttpKernel\KernelInterface;
  69. use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
  70. use Symfony\Component\Mailer\Mailer;
  71. use Symfony\Component\Mailer\MailerInterface;
  72. use Symfony\Component\Mailer\Transport;
  73. use Symfony\Component\Mime\Address;
  74. use Symfony\Component\Mime\Email;
  75. use Symfony\Component\Routing\Annotation\Route;
  76. use Symfony\Component\Security\Core\User\UserInterface;
  77. use Symfony\Component\Serializer\SerializerInterface;
  78. use Symfony\Contracts\Translation\TranslatorInterface;
  79. class SalesController extends AbstractController
  80. {
  81.     private const STOCK_EXEMPT_PRODUCT_TYPES = ['INSTALLATION_SERVICE''MAINTENANCE_SERVICE''CONSULTATION_SERVICE''TREE'];
  82.     private const NEGATIVE_PRICE_ALLOWED_PRODUCT_TYPES = [];
  83.     private const MONEY_SCALE 2;
  84.     private StockService $stockService;
  85.     private SerializerInterface $serializer;
  86.     private WarehouseService $warehouseService;
  87.     private SalesService $salesService;
  88.     private $projectDir;
  89.     private StockTransferService $stockTransferService;
  90.     private ProductService $productService;
  91.     private StockInfoService $stockInfoService;
  92.     private EntityManagerInterface $entityManager;
  93.     private StockRepository $stockRepository;
  94.     private CustomerService $customerService;
  95.     private PaymentMethodService $paymentMethodService;
  96.     private PaymentService $paymentService;
  97.     private PaymentReminderService $paymentReminderService;
  98.     private LogService $logService;
  99.     private StockTransactionService $stockTransactionService;
  100.     private LoggerInterface $logger;
  101.     private SalesProductsServiceImpl $salesProductsServiceImpl;
  102.     private SalesHistoryService $salesHistoryService;
  103.     private SalesReturnRepository $salesReturnRepository;
  104.     private \App\Services\GiftVoucherService $giftVoucherService;
  105.     private TranslatorInterface $translator;
  106.     private SalesServiceImpl $salesServiceImpl;
  107.     private Pdf $knpSnappyPdf;
  108.     private InvoiceService $invoiceService;
  109.     private PDFService $pdfService;
  110.     private MeasurementConversionsService $measurementConversionsService;
  111.     /**
  112.      * @param KernelInterface $kernel
  113.      * @param StockService $stockService
  114.      * @param SerializerInterface $serializer
  115.      * @param WarehouseService $warehouseService
  116.      * @param SalesService $salesService
  117.      * @param StockTransferService $stockTransferService
  118.      * @param ProductService $productService
  119.      * @param StockInfoService $stockInfoService
  120.      * @param EntityManagerInterface $entityManager
  121.      * @param StockRepository $stockRepository
  122.      * @param CustomerService $customerService
  123.      * @param PaymentMethodService $paymentMethodService
  124.      * @param PaymentService $paymentService
  125.      * @param PaymentReminderService $paymentReminderService
  126.      * @param LogService $logService
  127.      * @param StockTransactionService $stockTransactionService
  128.      * @param LoggerInterface $logger
  129.      * @param SalesProductsServiceImpl $salesProductsServiceImpl
  130.      * @param TranslatorInterface $translator
  131.      * @param SalesServiceImpl $salesServiceImpl
  132.      * @param Pdf $knpSnappyPdf
  133.      * @param InvoiceService $invoiceService
  134.      * @param PDFService $pdfService
  135.      * @param MeasurementConversionsService $measurementConversionsService
  136.      */
  137.     public function __construct(
  138.         KernelInterface $kernel,
  139.         StockService $stockService,
  140.         SerializerInterface $serializer,
  141.         WarehouseService $warehouseService,
  142.         SalesService $salesService,
  143.         StockTransferService $stockTransferService,
  144.         ProductService $productService,
  145.         StockInfoService $stockInfoService,
  146.         EntityManagerInterface $entityManager,
  147.         StockRepository $stockRepository,
  148.         CustomerService $customerService,
  149.         PaymentMethodService $paymentMethodService,
  150.         PaymentService $paymentService,
  151.         PaymentReminderService $paymentReminderService,
  152.         LogService $logService,
  153.         StockTransactionService $stockTransactionService,
  154.         LoggerInterface $logger,
  155.         SalesProductsServiceImpl $salesProductsServiceImpl,
  156.         SalesHistoryService $salesHistoryService,
  157.         SalesReturnRepository $salesReturnRepository,
  158.         TranslatorInterface $translator,
  159.         SalesServiceImpl $salesServiceImpl,
  160.         Pdf $knpSnappyPdf,
  161.         InvoiceService $invoiceService,
  162.         PDFService $pdfService,
  163.         MeasurementConversionsService $measurementConversionsService,
  164.         \App\Services\GiftVoucherService $giftVoucherService
  165.     ) {
  166.         $this->stockService $stockService;
  167.         $this->serializer $serializer;
  168.         $this->warehouseService $warehouseService;
  169.         $this->salesService $salesService;
  170.         $this->stockTransferService $stockTransferService;
  171.         $this->productService $productService;
  172.         $this->stockInfoService $stockInfoService;
  173.         $this->entityManager $entityManager;
  174.         $this->stockRepository $stockRepository;
  175.         $this->customerService $customerService;
  176.         $this->paymentMethodService $paymentMethodService;
  177.         $this->paymentService $paymentService;
  178.         $this->paymentReminderService $paymentReminderService;
  179.         $this->logService $logService;
  180.         $this->stockTransactionService $stockTransactionService;
  181.         $this->logger $logger;
  182.         $this->salesProductsServiceImpl $salesProductsServiceImpl;
  183.         $this->salesHistoryService $salesHistoryService;
  184.         $this->salesReturnRepository $salesReturnRepository;
  185.         $this->translator $translator;
  186.         $this->salesServiceImpl $salesServiceImpl;
  187.         $this->projectDir $kernel->getProjectDir();
  188.         $this->knpSnappyPdf $knpSnappyPdf;
  189.         $this->invoiceService $invoiceService;
  190.         $this->pdfService $pdfService;
  191.         $this->measurementConversionsService $measurementConversionsService;
  192.         $this->giftVoucherService $giftVoucherService;
  193.     }
  194.     /**
  195.      * @throws StockNotAvaibleException
  196.      * @throws StockNotFoundException
  197.      */
  198.     #[Route('/sales'name'app_admin_sales')]
  199.     public function index(Request $requestSalesServiceImpl $salesServiceImplTranslatorInterface $translator): Response
  200.     {
  201.         $paymentMethods $this->paymentMethodService->getPaymentMethods();
  202.         $sales = new Sales();
  203.         $sales->setInvoiceNumber($translator->trans('willBeCreatedAutomatically'));
  204.         $form $this->createForm(SalesFormType::class, $sales);
  205.         $form->handleRequest($request);
  206.         $soldProducts = new ProductsSold();
  207.         $soldProductsFrom $this->createForm(ProductsSoldType::class, $soldProducts);
  208.         $customer = new Customer();
  209.         $customer->setCode($this->customerService->createCustomerCode());
  210.         $customerForm $this->createForm(CustomerType::class, $customer);
  211.         $customerForm->handleRequest($request);
  212.         $payment = new Payment();
  213.         $paymentForm $this->createForm(PaymentType::class, $payment);
  214.         $paymentForm->handleRequest($request);
  215.         $stocks $this->stockService->getAll();
  216.         $warehouses $this->warehouseService->getAll();
  217.         if ($customerForm->isSubmitted() && $customerForm->isValid()) {
  218.             $customer $customerForm->getData();
  219.             $customers $this->customerService->findByFullName($customer->getFullName());
  220.             $this->addFlash('success''Müşteri başarıyla eklendi. Listeden seçebilirsiniz.');
  221.             $email $customer->getEmail();
  222.             $customer->setEmail(mb_strtolower($email'UTF-8'));
  223.             $this->customerService->save($customerForm->getData());
  224.         }
  225.         return $this->render('admin/sales/index.html.twig', [
  226.             'controller_name' => 'SalesController',
  227.             'form' => $form->createView(),
  228.             'productsSoldForm' => $soldProductsFrom->createView(),
  229.             'customerForm' => $customerForm->createView(),
  230.             'paymentForm' => $paymentForm->createView(),
  231.             'warehouses' => $warehouses,
  232.             'paymentMethods' => $paymentMethods,
  233.         ]);
  234.     }
  235.     // Duplicate proforma
  236.     #[Route('/sales/duplicate/{id}'name'app_admin_sales_duplicate')]
  237.     public function duplicateProforma(Request $requestSalesServiceImpl $salesServiceImplTranslatorInterface $translator)
  238.     {
  239.         $paymentMethods $this->paymentMethodService->getPaymentMethods();
  240.         $sales $this->salesService->getEntityById($request->get('id'));
  241.         //        dd($sales->getProductsSolds()->get(0));
  242.         $sales->setInvoiceNumber($translator->trans('willBeCreatedAutomatically'));
  243.         $form $this->createForm(SalesFormType::class, $sales);
  244.         $form->handleRequest($request);
  245.         $soldProducts = new ProductsSold();
  246.         $soldProductsFrom $this->createForm(ProductsSoldType::class, $soldProducts);
  247.         $customer = new Customer();
  248.         $customer->setCode($this->customerService->createCustomerCode());
  249.         $customerForm $this->createForm(CustomerType::class, $customer);
  250.         $customerForm->handleRequest($request);
  251.         $payment = new Payment();
  252.         $paymentForm $this->createForm(PaymentType::class, $payment);
  253.         $paymentForm->handleRequest($request);
  254.         $stocks $this->stockService->getAll();
  255.         $warehouses $this->warehouseService->getAll();
  256.         if ($customerForm->isSubmitted() && $customerForm->isValid()) {
  257.             $customer $customerForm->getData();
  258.             $customers $this->customerService->findByFullName($customer->getFullName());
  259.             if (count($customers) > 0) {
  260.                 $this->addFlash('error''Bu müşteri kaydı zaten var ! Listeden seçip devam edebilirsiniz.');
  261.             } else {
  262.                 $this->addFlash('success''Müşteri başarıyla eklendi. Listeden seçebilirsiniz.');
  263.                 $email $customer->getEmail();
  264.                 $customer->setEmail(mb_strtolower($email'UTF-8'));
  265.                 $this->customerService->save($customerForm->getData());
  266.             }
  267.         }
  268.         return $this->render('admin/sales/index.html.twig', [
  269.             'sales' => $sales,
  270.             'controller_name' => 'SalesController',
  271.             'form' => $form->createView(),
  272.             'productsSoldForm' => $soldProductsFrom->createView(),
  273.             'customerForm' => $customerForm->createView(),
  274.             'paymentForm' => $paymentForm->createView(),
  275.             'warehouses' => $warehouses,
  276.             'paymentMethods' => $paymentMethods,
  277.             'sales'
  278.         ]);
  279.     }
  280.     #[Route('/sales/create'name'app_admin_sales_create')]
  281.     public function create(Request $requestSalesServiceImpl $salesServiceImpl): Response
  282.     {
  283.         $this->logger->info("sales", [
  284.             'title' => 'Satış işlemi başlatıldı',
  285.             'request' => $request->request->all(),
  286.         ]);
  287.         $this->entityManager->beginTransaction();
  288.         $products $request->get('products');
  289.         $payments $request->get('payments');
  290.         $soldList $request->get('soldList');
  291.         $invoice $request->get('invoice');
  292.         $warehouseId $soldList[0]['warehouse'];
  293.         $salesType $invoice['salesType'] ?? 'proforma';
  294.         $intendedStatus $this->resolveSalesStatus($invoice['salesStatus'] ?? null);
  295.         try {
  296.             // Stok kontrolü: yalnızca COMPLETED veya PENDING_ITEMS durumunda yapılır
  297.             if ($this->shouldAffectStock($intendedStatus)) {
  298.                 $this->checkStocks($soldList);
  299.             }
  300.             // Ürün bazlı indirim kdv gibi işlemler yapılıp total fiyatların frontend ile aynı olup olmadığı kontrol ediliyor.
  301.             foreach ($soldList as $soldProduct) {
  302.                 $this->logger->info("sales", [
  303.                     'title' => 'Ürün bazlı indirim kdv hesaplanıyor...',
  304.                     'product' => $soldProduct,
  305.                 ]);
  306.                 $this->assertUnitPriceAllowed($soldProduct);
  307.                 $this->calculateTotalPrice($soldProduct);
  308.             }
  309.             // Ödeme planı ile ürünlerin toplam fiyatlarının eşitliği kontrol ediliyor.
  310.             $totalPrice $this->checkPaymentAndAmountIsEqual($payments$soldList);
  311.             $repo $this->entityManager->getRepository(Sales::class);
  312.             $invoiceNumber $salesServiceImpl->createProformaNumber();
  313.             $sale $repo->findBy(['invoiceNumber' => $invoiceNumber]);
  314.             //            dd($request);
  315.             if ($sale != null) {
  316.                 throw new Exception(sprintf("%s fatura numarası ile kayıtlı bir fatura var. Lütfen kontrol ediniz."$invoice['invoiceNumber']));
  317.             } else {
  318.                 $soldProductsArray = new ArrayCollection();
  319.                 for ($i 0$i count($soldList); $i++) {
  320.                     $product $this->productService->getEntityId($soldList[$i]['productid']);
  321.                     $calculatorDto $this->stockRepository->calculateCost($soldList[$i]['productid'], $warehouseId);
  322.                     $builder = new SalesProductsDtoBuilder();
  323.                     $soldProductsArray->add(
  324.                         $builder
  325.                             ->setProductName($soldList[$i]['productName'])
  326.                             ->setMeasurement($product->getMeasurement()?->getMeasurement())
  327.                             ->setQuantity($soldList[$i]['quantity'])
  328.                             ->setUnitPriceFob($calculatorDto->getPriceFob())
  329.                             ->setUnitPriceNavlun($calculatorDto->getPriceNavlun())
  330.                             ->setTotalUnitPrice($calculatorDto->getAverageCostPrice())
  331.                             ->setTotalUnitPurchasePrice($soldList[$i]['totalPurchasePrice'])
  332.                             ->setProductId($product->getId())
  333.                             ->setTotalUnitPrice($soldList[$i]['unitPrice'])
  334.                             ->setWarehouseId($warehouseId)
  335.                             ->setTax($soldList[$i]['tax'])
  336.                             ->setTotalSalePrice($soldList[$i]['totalPurchasePrice'])
  337.                             ->setDiscount($soldList[$i]['discount'])
  338.                             ->setUnAllocatedQuantity($soldList[$i]['unAllocatedQuantity'])
  339.                             ->setSelectedUnitId($soldList[$i]['selectedUnitId'])
  340.                             ->build()
  341.                     );
  342.                 }
  343.                 $customer $this->customerService->getCustomerById($invoice['customer']);
  344.                 $sales = new Sales();
  345.                 $sales->setSalesDate(new \DateTime($invoice['salesDate']));
  346.                 $sales->setDeliveryDate(!empty($invoice['deliveryDate']) ? new \DateTime($invoice['deliveryDate']) : null);
  347.                 $sales->setDueDate(!empty($invoice['dueDate']) ? new \DateTime($invoice['dueDate']) : null);
  348.                 $sales->setValidDate(!empty($invoice['validDate']) ? new \DateTime($invoice['validDate']) : null);
  349.                 $sales->setCustomer($customer);
  350.                 $sales->setStatus($intendedStatus);
  351.                 $userRepo $this->entityManager->getRepository(User::class);
  352.                 $sales->setSeller($userRepo->find($invoice['seller']));
  353.                 $sales->setInvoiceNumber($invoice['invoiceNumber']);
  354.                 $sales->setTotalPurchasePrice($totalPrice);
  355.                 $sales->setDescription($invoice['description']);
  356.                 $sales->setShippingNotes($invoice['shippingNotes']);
  357.                 $sales->setPaymentNotes($invoice['paymentNotes']);
  358.                 $performedBy $this->getUser();
  359.                 $sales $this->salesService->create(
  360.                     $sales,
  361.                     $soldProductsArray,
  362.                     $salesType,
  363.                     $performedBy instanceof User $performedBy null
  364.                 );
  365.                 $this->createPayments($payments$customer$sales);
  366.                 $this->entityManager->commit();
  367.                 return new JsonResponse(['success' => true'id' => $sales->getId()]);
  368.             }
  369.         } catch (Exception $e) {
  370.             $this->entityManager->rollback();
  371.             $message $e->getMessage();
  372.             return new JsonResponse(['status' => 'error''message' => $message], 400);
  373.         }
  374.     }
  375.     #[Route('/sales/un-allocated-products'name'un_allocated_products_index')]
  376.     public function getUnAllocatedProducts(Request $request)
  377.     {
  378.         $warehouses $this->warehouseService->getAll();
  379.         if ($request->getMethod() == 'POST') {
  380.             if (isset($request->get('search')['value']))
  381.                 $s $request->get('search')['value'];
  382.             else
  383.                 $s '';
  384.             if (isset($request->get('order')[0]['column']))
  385.                 $column $request->get('order')[0]['column'];
  386.             else
  387.                 $column 0;
  388.             if (isset($request->get('order')[0]['dir']))
  389.                 $dir $request->get('order')[0]['dir'];
  390.             else
  391.                 $dir 'ASC';
  392.             $startDate $request->get('startDate');
  393.             $finishDate $request->get('finishDate');
  394.             $limit $request->get('length');
  395.             if ($request->get('start'))
  396.                 $page + ($request->get('start') / $limit);
  397.             else
  398.                 $page 1;
  399.             $warehouseid $request->get('warehouseid');
  400.             $result $this->salesProductsServiceImpl->getAllUnAllocatedProductsPaginate(
  401.                 $s,
  402.                 $column,
  403.                 $dir,
  404.                 $page,
  405.                 $limit,
  406.                 $startDate,
  407.                 $finishDate,
  408.                 $warehouseid
  409.             );
  410.             return $this->json($result);
  411.         }
  412.         return $this->render('admin/sales/unallocated_products.html.twig', [
  413.             'warehouses' => $warehouses
  414.         ]);
  415.     }
  416.     /**
  417.      * @throws NonUniqueResultException
  418.      * @throws NoResultException
  419.      */
  420.     #[Route('/sales/un-allocated-products/allocate'name'un_allocated_products_allocate')]
  421.     public function allocateProduct(Request $request)
  422.     {
  423.         $content $request->getContent();
  424.         $data json_decode($contenttrue);
  425.         $id $data["id"];
  426.         $allocatedQuantity $data["allocatedQuantity"];
  427.         $customer $data["customer"];
  428.         $measurement $data["measurement"];
  429.         $measurementUnit $data["measurementUnit"];
  430.         $productCode $data["productCode"];
  431.         $productName $data["productName"];
  432.         $salesDate $data["salesDate"];
  433.         $sellerName $data["sellerName"];
  434.         $stockInfo $data["stockInfo"];
  435.         $totalSold $data["totalSold"];
  436.         $unAllocatedQuantity $data["unAllocatedQuantity"];
  437.         $warehouseName $data["warehouseName"];
  438.         $productSold $this->salesProductsServiceImpl->getEntityById($id);
  439.         $quantityToAllocate $allocatedQuantity $productSold->getQuantity();
  440.         $stock $this->stockInfoService->getStockInfoByProductAndWarehouse(
  441.             $productSold->getProduct(),
  442.             $productSold->getSales()->getWarehouse()
  443.         );
  444.         if ($quantityToAllocate $stock->getTotalQuantity()) {
  445.             return $this->json([
  446.                 'message' => sprintf('Stok miktarı yetersiz, depodaki stok miktarı = %s'$stock->getTotalQuantity())
  447.             ], 400);
  448.         }
  449.         //        dd($quantityToAllocate, CurrencyHelper::convertToFloat($productSold->getUnAllocatedQuantity()));
  450.         $unAllocatedQuantityFromProduct round(CurrencyHelper::convertToFloat($productSold->getUnAllocatedQuantity()), 2);
  451.         $quantityToAllocate round($quantityToAllocate2);
  452.         if ($quantityToAllocate $unAllocatedQuantityFromProduct) {
  453.             return $this->json([
  454.                 'message' => sprintf(
  455.                     'Hatalı bilgi. Tahsis etmek istediğiniz miktar tahsis edilmemiş miktardan büyük olamaz. Tahsis edilmemiş miktar = %s',
  456.                     $productSold->getUnAllocatedQuantity()
  457.                 )
  458.             ], 400);
  459.         }
  460.         return $this->json(
  461.             ['message' => $this->salesProductsServiceImpl->allocateQuantity($productSold$quantityToAllocate)]
  462.         );
  463.     }
  464.     #[Route('/sales/un-allocated-products/detail/ajax'name'un_allocated_products_detail_ajax')]
  465.     public function getUnAllocatedProduct(Request $request)
  466.     {
  467.         /**
  468.          * @var ProductsSold $entity
  469.          */
  470.         $entity $this->salesProductsServiceImpl->getEntityById($request->get('productSoldId'));
  471.         return $this->json([
  472.             'id' => $entity->getId(),
  473.             'productName' => $entity->getProduct()->getName(),
  474.             'productCode' => $entity->getProduct()->getCode(),
  475.             'measurement' => $entity->getProduct()->getMeasurement()->getName(),
  476.             'measurementUnit' => $entity->getProduct()->getMeasurementUnit()->getSymbol(),
  477.             'customer' => $entity->getSales()->getCustomer()->getFullName(),
  478.             'allocatedQuantity' => $entity->getQuantity(),
  479.             'unAllocatedQuantity' => $entity->getUnAllocatedQuantity(),
  480.             'salesDate' => $entity->getSales()->getSalesDate()->format('d.m.Y'),
  481.             'warehouseName' => $entity->getSales()->getWarehouse()->getName(),
  482.             'stockInfo' => $this->stockInfoService->getStockInfoByProductAndWarehouse(
  483.                 $entity->getProduct(),
  484.                 $entity->getSales()->getWarehouse()
  485.             )->getTotalQuantity(),
  486.             'sellerName' => $entity->getSales()->getSeller()->getFullname(),
  487.             'totalSold' => CurrencyHelper::convertToCurrency($entity->getQuantity() + ($entity->getUnAllocatedQuantity() == null $entity->getUnAllocatedQuantity())),
  488.         ]);
  489.     }
  490.     #[Route('/proforma/index'name'proforma_index')]
  491.     public function getAllProformas(Request $requestEntityManagerInterface $entityManager)
  492.     {
  493.         $warehouses $this->warehouseService->getAll();
  494.         return $this->render('admin/sales/proforma_index.html.twig', [
  495.             'controller_name' => 'StockController',
  496.             'warehouses' => $warehouses
  497.         ]);
  498.     }
  499.     #[Route('/proforma/ajax'name'proforma_ajax')]
  500.     public function getAllProformasAjax(Request $requestSalesServiceImpl $salesServiceImpl)
  501.     {
  502.         $user $this->getUser();
  503.         if (isset($request->get('search')['value']))
  504.             $s $request->get('search')['value'];
  505.         else
  506.             $s '';
  507.         $id $request->get('id');
  508.         if (isset($request->get('order')[0]['column']))
  509.             $column $request->get('order')[0]['column'];
  510.         else
  511.             $column 0;
  512.         if (isset($request->get('order')[0]['dir']))
  513.             $dir $request->get('order')[0]['dir'];
  514.         else
  515.             $dir 'ASC';
  516.         $startDate $request->get('startDate');
  517.         $finishDate $request->get('finishDate');
  518.         $limit $request->get('length');
  519.         if ($request->get('start'))
  520.             $page + ($request->get('start') / $limit);
  521.         else
  522.             $page 1;
  523.         $warehouseid $request->get('warehouseid');
  524.         $stocks $salesServiceImpl->getAllProformasPaginate($user$page$limit$warehouseid$column$dir$s$startDate$finishDate);
  525.         return new JsonResponse($this->createArrayForDataTable($stocks));
  526.     }
  527.     private function createPayments($payments$customer$sales)
  528.     {
  529.         foreach ($payments as $payment) {
  530.             $paymentMethod $this->paymentMethodService->getPaymentMethodById($payment['paymentMethod']);
  531.             $paymentObj $this->paymentService->createPayment($customer$paymentMethod$sales$payment['paymentDate'], $payment['amount'], $payment['status'], $payment['paymentDueDate'], $payment['description']);
  532.             $a 0;
  533.             if ($paymentObj->getPaymentStatus() == 2) {
  534.                 $paymentReminder $this->paymentReminderService->create($paymentObj$payment['paymentDate'], $payment['status'], $payment['paymentMethod']);
  535.             }
  536.             // Hediye Çeki Entegrasyonu
  537.             if ($paymentMethod->getName() === 'Hediye Çeki' || $paymentMethod->getName() === 'Gift Voucher') {
  538.                 if (!empty($payment['voucherCode'])) {
  539.                     // Özel durum: AUTO_DEDUCT_FROM_BALANCE - Müşterinin bakiyesinden otomatik düş
  540.                     if ($payment['voucherCode'] === 'AUTO_DEDUCT_FROM_BALANCE') {
  541.                         // Müşterinin aktif hediye sertifikalarından otomatik mahsup yap
  542.                         $this->giftVoucherService->deductFromCustomerBalance(
  543.                             $customer,
  544.                             (float) $payment['amount'],
  545.                             $sales,
  546.                             $paymentObj
  547.                         );
  548.                     } else {
  549.                         // Normal hediye çeki kodu kullanımı
  550.                         $this->giftVoucherService->useVoucher(
  551.                             $payment['voucherCode'],
  552.                             (float) $payment['amount'],
  553.                             $sales,
  554.                             $paymentObj
  555.                         );
  556.                     }
  557.                 }
  558.             }
  559.         }
  560.     }
  561.     /**
  562.      * @throws StockNotAvaibleException
  563.      * @throws StockNotFoundException
  564.      */
  565.     private function checkStocks($soldList): array
  566.     {
  567.         $warehouseId $soldList[0]['warehouse'];
  568.         $quantities = [];
  569.         foreach ($soldList as $soldProduct) {
  570.             $productId $soldProduct['productid'];
  571.             // Ürün tipine göre stok kontrolünü atla (service & tree)
  572.             $product $this->productService->getEntityId($productId);
  573.             if ($product && $this->isStockExemptProductType($product->getProductTypeEnum())) {
  574.                 continue; // bu ürünlerde stok kontrolü yapılmaz
  575.             }
  576.             // Birim dönüşümü: selectedUnitId farklı ise temel birime çevirerek topla
  577.             $rawQuantity = (float) $soldProduct['quantity'];
  578.             $selectedUnitId $soldProduct['selectedUnitId'] ?? null;
  579.             if (!isset($quantities[$productId])) {
  580.                 $quantities[$productId] = 0.0;
  581.             }
  582.             if ($selectedUnitId) {
  583.                 // SalesServiceImpl içindeki dönüştürme mantığını kullan
  584.                 $converted $this->salesServiceImpl->getConvertedQuantity($product$selectedUnitId$rawQuantity);
  585.                 $quantities[$productId] += (float) $converted;
  586.             } else {
  587.                 $quantities[$productId] += $rawQuantity;
  588.             }
  589.         }
  590.         foreach ($quantities as $productId => $quantity) {
  591.             $this->stockInfoService->checkIsStockAvailable($quantity$productId$warehouseId);
  592.         }
  593.         return $quantities;
  594.     }
  595.     /**
  596.      * @throws Exception
  597.      */
  598.     private function applyRoundingRule(float $value): float
  599.     {
  600.         return round($valueself::MONEY_SCALEPHP_ROUND_HALF_UP);
  601.     }
  602.     /**
  603.      * @throws Exception
  604.      */
  605.     private function checkPaymentAndAmountIsEqual($payments$soldList)
  606.     {
  607.         $totalPriceRaw 0.0;
  608.         $totalAmountOfPayments 0.0;
  609.         foreach ($payments as $payment) {
  610.             $totalAmountOfPayments += CurrencyHelper::convertToFloat($payment['amount']);
  611.         }
  612.         foreach ($soldList as $soldProduct) {
  613.             $totalPriceRaw += $this->calculateTotalPrice($soldProduct);
  614.         }
  615.         $totalPrice $this->applyRoundingRule($totalPriceRaw);
  616.         $totalAmountOfPayments $this->applyRoundingRule($totalAmountOfPayments);
  617.         $diff abs($totalPrice $totalAmountOfPayments);
  618.         if ($diff 1) {
  619.             throw new Exception("Ödemeler ile ürünlerin fiyatları farklı. Toplam Ürün Fiyatı: " $totalPrice ", Toplam Ödeme: " $totalAmountOfPayments ", Fark: " $diff401);
  620.         }
  621.         return $totalPrice;
  622.     }
  623.     /**
  624.      * @throws Exception
  625.      */
  626.     private function calculateTotalPrice($soldProduct): float
  627.     {
  628.         $quantity CurrencyHelper::convertToFloat($soldProduct['quantity']);
  629.         $unAllocatedQuantity CurrencyHelper::convertToFloat($soldProduct['unAllocatedQuantity']);
  630.         $tax CurrencyHelper::convertToFloat($soldProduct['tax']);
  631.         $unitPrice CurrencyHelper::convertToFloat($soldProduct['unitPrice']);
  632.         $discount CurrencyHelper::convertToFloat($soldProduct['discount']);
  633.         $totalPurchasePrice CurrencyHelper::convertToFloat($soldProduct['totalPurchasePrice']);
  634.         $totalQuantity $quantity $unAllocatedQuantity;
  635.         $subtotal $totalQuantity $unitPrice;
  636.         $discountAmount $subtotal * ($discount 100);
  637.         $afterDiscount $subtotal $discountAmount;
  638.         $taxAmount $afterDiscount * ($tax 100);
  639.         $lineTotal $afterDiscount $taxAmount;
  640.         $roundedLineTotal $this->applyRoundingRule($lineTotal);
  641.         $roundedFrontend $this->applyRoundingRule($totalPurchasePrice);
  642.         $difference abs($roundedLineTotal $roundedFrontend);
  643.         $rawDifference abs($lineTotal $totalPurchasePrice);
  644.         if ($difference 1) {
  645.             throw new Exception("Fiyatlar arasında hesaplamada fark var. Backend: " $roundedLineTotal ", Frontend: " $totalPurchasePrice ", Fark: " $difference401);
  646.         }
  647.         $this->logger->info("sales", [
  648.             'title' => 'Kullanıcının girdiği ödemeler ile ürün bazlı toplamlar karşılaştırılıyor',
  649.             'backend_rounded' => $roundedLineTotal,
  650.             'backend_raw' => $lineTotal,
  651.             'frontend' => $totalPurchasePrice,
  652.             'frontend_rounded' => $roundedFrontend,
  653.             'difference' => $difference,
  654.             'raw_difference' => $rawDifference,
  655.         ]);
  656.         return $lineTotal;
  657.     }
  658.     private function roundUpToTwoDecimalsPrecise($number)
  659.     {
  660.         return round(ceil($number 1000) / 10002);
  661.     }
  662.     #[Route('/product-calculate-cost'name'product_calculate_cost')]
  663.     public function exampleCostCalcolator(Request $requestStockRepository $stockRepository)
  664.     {
  665.         $repo $stockRepository->calculateCost(301);
  666.         return $this->json(['data' => 'data']);
  667.     }
  668.     #[Route('/stock-detail/{id}'name'app_admin_stock_detail')]
  669.     public function stockDetail(Request $request$id)
  670.     {
  671.         $stock $this->stockService->getStockById($id);
  672.         return $this->json($stock);
  673.     }
  674.     /**
  675.      * @throws Exception
  676.      */
  677.     #[Route('/product/{id}/measurement-units'name'get_product_measurement_units'methods: ['GET'])]
  678.     public function getAvailableMeasurementUnits(int $id): JsonResponse
  679.     {
  680.         $product $this->productService->getEntityId($id);
  681.         if (!$product)
  682.             throw new Exception(sprintf("Product not found with %s"$id), 404);
  683.         $availableUnits $this->measurementConversionsService->getAvailableUnitsFor($product);
  684.         //        dd($availableUnits);
  685.         return $this->json(
  686.             $availableUnits,
  687.             JsonResponse::HTTP_OK,
  688.             [],
  689.             ['groups' => 'measurement:read']
  690.         );
  691.     }
  692.     #[Route('/sales/detail/{id}'name'app_admin_sales_detail')]
  693.     public function edit(Request $request$idSalesRepository $salesRepository)
  694.     {
  695.         $payments $request->get('payments');
  696.         $soldList $request->get('soldList');
  697.         $oldStatus $this->salesService->getEntityById($id)->getStatus();
  698.         $newStatus $request->get('invoice')['salesStatus'] ?? null;
  699.         $sales $this->salesServiceImpl->getEntityById(3);
  700.         if (isset($soldList[0]['warehouse']))
  701.             $warehouseId $soldList[0]['warehouse'];
  702.         else
  703.             $warehouseId 0;
  704.         $invoice $request->get('invoice');
  705.         // Log the edit operation
  706.         $this->logger->info("sales", [
  707.             'title' => 'Satış düzenleme işlemi başlatıldı',
  708.             'saleId' => $id,
  709.             'request' => $request->request->all(),
  710.         ]);
  711.         $this->entityManager->beginTransaction();
  712.         try {
  713.             $sale $this->salesService->getEntityById($id);
  714.             $form $this->createForm(SalesFormType::class, $sale);
  715.             $soldProducts = new ProductsSold();
  716.             $soldProductsFrom $this->createForm(ProductsSoldType::class, $soldProducts);
  717.             $form->handleRequest($request);
  718.             $customer = new Customer();
  719.             $customerForm $this->createForm(CustomerType::class, $customer);
  720.             $customerForm->handleRequest($request);
  721.             if ($customerForm->isSubmitted() && $customerForm->isValid()) {
  722.                 /**
  723.                  * @var Customer $customer
  724.                  */
  725.                 $customer $customerForm->getData();
  726.                 $this->addFlash('success''Müşteri başarıyla eklendi. Listeden seçebilirsiniz.');
  727.                 $email $customer->getEmail();
  728.                 $customer->setEmail(mb_strtolower($email'UTF-8'));
  729.                 $this->customerService->save($customerForm->getData());
  730.                 $this->entityManager->commit();
  731.                 $referer $request->headers->get('referer');
  732.                 return $this->redirect($referer ?: '/sales/detail/' $id);
  733.             }
  734.             $payment = new Payment();
  735.             $paymentForm $this->createForm(PaymentType::class, $payment);
  736.             $paymentMethods $this->paymentMethodService->getPaymentMethods();
  737.             $warehouses $this->warehouseService->getAll();
  738.             if ($request->getMethod() == 'POST') {
  739.                 $sale $this->salesService->getEntityById($id);
  740.                 $salesType $invoice['salesType'] ?? 'PROFORMA'// Default to PROFORMA if not provided
  741.                 $groupedTotalQuantitiesByProduct $salesRepository->getGroupedProductsSoldBySale($sale);
  742.                 // Stok kontrolü: yalnızca COMPLETED veya PENDING_ITEMS durumunda yapılır
  743.                 $intendedStatus $this->resolveSalesStatus($invoice['salesStatus'] ?? null);
  744.                 if ($this->shouldAffectStock($intendedStatus)) {
  745.                     $this->checkStocksIsAvailableForEditedSale($soldList$groupedTotalQuantitiesByProduct);
  746.                 }
  747.                 // Ürün bazlı indirim kdv gibi işlemler yapılıp total fiyatların frontend ile aynı olup olmadığı kontrol ediliyor.
  748.                 foreach ($soldList as $soldProduct) {
  749.                     $this->logger->info("sales", [
  750.                         'title' => 'Ürün bazlı indirim kdv hesaplanıyor (düzenleme)...',
  751.                         'product' => $soldProduct,
  752.                     ]);
  753.                     $this->assertUnitPriceAllowed($soldProduct);
  754.                     $this->calculateTotalPrice($soldProduct);
  755.                 }
  756.                 // Ödeme planı ile ürünlerin toplam fiyatlarının eşitliği kontrol ediliyor.
  757.                 $totalPrice $this->checkPaymentAndAmountIsEqual($payments$soldList);
  758.                 $repo $this->entityManager->getRepository(Sales::class);
  759.                 $saleQ $repo->createQueryBuilder('c')
  760.                     ->where('c.invoiceNumber = :invoiceNumber')
  761.                     ->andWhere('c.id != :id')
  762.                     ->setParameters(['invoiceNumber' => $invoice['invoiceNumber'], 'id' => $id])
  763.                     ->getQuery()
  764.                     ->getResult();
  765.                 if ($saleQ) {
  766.                     throw new Exception(sprintf("%s fatura numarasına kayıtlı bir satış mevcut."$invoice['invoiceNumber']));
  767.                 } else {
  768.                     $soldProductsArray = new ArrayCollection();
  769.                     for ($i 0$i count($soldList); $i++) {
  770.                         $product $this->productService->getEntityId($soldList[$i]['productid']);
  771.                         $calculatorDto $this->stockRepository->calculateCost($soldList[$i]['productid'], $warehouseId);
  772.                         $builder = new SalesProductsDtoBuilder();
  773.                         $soldProductsArray->add(
  774.                             $builder
  775.                                 ->setProductName($soldList[$i]['productName'] ?? $product->getName())
  776.                                 ->setMeasurement($product->getMeasurement()?->getMeasurement())
  777.                                 ->setQuantity($soldList[$i]['quantity'])
  778.                                 ->setUnitPriceFob($calculatorDto->getPriceFob())
  779.                                 ->setUnitPriceNavlun($calculatorDto->getPriceNavlun())
  780.                                 ->setTotalUnitPrice($soldList[$i]['unitPrice'])
  781.                                 ->setTotalUnitPurchasePrice($soldList[$i]['totalPurchasePrice'])
  782.                                 ->setProductId($product->getId())
  783.                                 ->setWarehouseId($warehouseId)
  784.                                 ->setTax($soldList[$i]['tax'])
  785.                                 ->setTotalSalePrice($soldList[$i]['totalPurchasePrice'])
  786.                                 ->setDiscount($soldList[$i]['discount'])
  787.                                 ->setUnAllocatedQuantity($soldList[$i]['unAllocatedQuantity'])
  788.                                 ->setSelectedUnitId($soldList[$i]['selectedUnitId'] ?? null)
  789.                                 ->build()
  790.                         );
  791.                     }
  792.                     $customer $this->customerService->getCustomerById($invoice['customer']);
  793.                     $sale->setSalesDate(new \DateTime($invoice['salesDate']));
  794.                     $sale->setDeliveryDate(!empty($invoice['deliveryDate']) ? new \DateTime($invoice['deliveryDate']) : null);
  795.                     $sale->setDueDate(!empty($invoice['dueDate']) ? new \DateTime($invoice['dueDate']) : null);
  796.                     $sale->setValidDate(!empty($invoice['validDate']) ? new \DateTime($invoice['validDate']) : null);
  797.                     $sale->setCustomer($customer);
  798.                     $userRepo $this->entityManager->getRepository(User::class);
  799.                     $sale->setSeller($userRepo->find($invoice['seller']));
  800.                     $sale->setInvoiceNumber($invoice['invoiceNumber']);
  801.                     $sale->setTotalPurchasePrice($totalPrice);
  802.                     $sale->setDescription($invoice['description']);
  803.                     $sale->setShippingNotes($invoice['shippingNotes']);
  804.                     $sale->setPaymentNotes($invoice['paymentNotes']);
  805.                     $performedBy $this->getUser();
  806.                     $sale $this->salesService->update(
  807.                         $sale,
  808.                         $soldProductsArray,
  809.                         $intendedStatus,
  810.                         $performedBy instanceof User $performedBy null
  811.                     );
  812.                     $this->createPayments($payments$customer$sale);
  813.                     $this->entityManager->commit();
  814.                     return new JsonResponse(['success' => true]);
  815.                 }
  816.             }
  817.         } catch (Exception $exception) {
  818.             $this->entityManager->rollback();
  819.             $message $exception->getMessage();
  820.             $this->logger->error("sales", [
  821.                 'title' => 'Satış düzenleme hatası',
  822.                 'error' => $message,
  823.                 'trace' => $exception->getTraceAsString()
  824.             ]);
  825.             return new JsonResponse(['status' => 'error''message' => $message], 400);
  826.         }
  827.         $historyEntries $sale->getHistoryEntries()->toArray();
  828.         usort(
  829.             $historyEntries,
  830.             static fn(SalesHistory $aSalesHistory $b) => $b->getPerformedAt()->getTimestamp() <=> $a->getPerformedAt()->getTimestamp()
  831.         );
  832.         $returns $this->salesReturnRepository->findBy(['sale' => $sale], ['requestedAt' => 'DESC']);
  833.         // Check if returns can be created for this sale
  834.         $canCreateReturn $this->canCreateReturnForSale($sale);
  835.         return $this->render('admin/sales/edit.html.twig', [
  836.             'sale' => $sale,
  837.             'form' => $form->createView(),
  838.             'warehouses' => $warehouses,
  839.             'productsSoldForm' => $soldProductsFrom->createView(),
  840.             'customerForm' => $customerForm->createView(),
  841.             'paymentForm' => $paymentForm->createView(),
  842.             'paymentMethods' => $paymentMethods,
  843.             'historyEntries' => $historyEntries,
  844.             'returns' => $returns,
  845.             'canCreateReturn' => $canCreateReturn,
  846.             'defaultMailContent' => $this->getDefaultMailContent(),
  847.         ]);
  848.     }
  849.     public function checkStocksIsAvailableForEditedSale($productToSolds$soldQuantities)
  850.     {
  851.         $newProductSoldArr = [];
  852.         $warehouse $productToSolds[0]['warehouse'];
  853.         foreach ($productToSolds as $productToSold) {
  854.             $productId $productToSold['productid'];
  855.             // Ürün tipine göre stok kontrolünü atla (service & tree)
  856.             $product $this->productService->getEntityId($productId);
  857.             if ($product && $this->isStockExemptProductType($product->getProductTypeEnum())) {
  858.                 continue; // bu ürünlerde stok kontrolü yapılmaz
  859.             }
  860.             // Birim dönüşümü: selectedUnitId farklı ise temel birime çevirerek topla
  861.             $rawQuantity = (float) $productToSold['quantity'];
  862.             $selectedUnitId $productToSold['selectedUnitId'] ?? null;
  863.             if (!isset($newProductSoldArr[$productId])) {
  864.                 $newProductSoldArr[$productId] = 0.0;
  865.             }
  866.             if ($selectedUnitId) {
  867.                 // SalesServiceImpl içindeki dönüştürme mantığını kullan
  868.                 $converted $this->salesServiceImpl->getConvertedQuantity($product$selectedUnitId$rawQuantity);
  869.                 $newProductSoldArr[$productId] += (float) $converted;
  870.             } else {
  871.                 $newProductSoldArr[$productId] += $rawQuantity;
  872.             }
  873.         }
  874.         foreach ($newProductSoldArr as $productId => $requiredQuantity) {
  875.             $availableStock $this->stockInfoService->getStockInfoByProductAndWarehouse(
  876.                 $this->productService->getEntityId($productId),
  877.                 $this->warehouseService->getEntityById($warehouse)
  878.             );
  879.             $previouslySoldQuantity $this->findStockInArray($soldQuantities$productId);
  880.             $totalAvailableStock $previouslySoldQuantity $availableStock->getTotalQuantity();
  881.             if ($requiredQuantity $totalAvailableStock) {
  882.                 throw new Exception(sprintf(
  883.                     "Yetersiz stok. %s id'li ürün için toplam kullanılabilir stok miktarı = %s, talep edilen = %s",
  884.                     $productId,
  885.                     $totalAvailableStock,
  886.                     $requiredQuantity
  887.                 ));
  888.             }
  889.         }
  890.         return true;
  891.     }
  892.     public function findStockInArray($arr$productId)
  893.     {
  894.         foreach ($arr as $item) {
  895.             if ($item['productId'] == $productId) {
  896.                 return $item['totalQuantity'];
  897.             }
  898.         }
  899.         return 0;
  900.     }
  901.     private function normalizeProductTypeName($type): ?string
  902.     {
  903.         if ($type instanceof \UnitEnum) {
  904.             return $type->name;
  905.         }
  906.         if (is_array($type)) {
  907.             return $type['name'] ?? ($type['key'] ?? null);
  908.         }
  909.         if (is_string($type)) {
  910.             return strtoupper($type);
  911.         }
  912.         return null;
  913.     }
  914.     private function isStockExemptProductType($type): bool
  915.     {
  916.         $normalized $this->normalizeProductTypeName($type);
  917.         return $normalized in_array($normalizedself::STOCK_EXEMPT_PRODUCT_TYPEStrue) : false;
  918.     }
  919.     private function canProductTypeUseNegativePrice($type): bool
  920.     {
  921.         $normalized $this->normalizeProductTypeName($type);
  922.         return $normalized in_array($normalizedself::NEGATIVE_PRICE_ALLOWED_PRODUCT_TYPEStrue) : false;
  923.     }
  924.     private function assertUnitPriceAllowed(array $soldProduct): void
  925.     {
  926.         if (empty($soldProduct['productid']) || !array_key_exists('unitPrice'$soldProduct)) {
  927.             return;
  928.         }
  929.         $product $this->productService->getEntityId($soldProduct['productid']);
  930.         if (!$product) {
  931.             return;
  932.         }
  933.         $unitPrice CurrencyHelper::convertToFloat($soldProduct['unitPrice']);
  934.         $type $product->getProductTypeEnum();
  935.         if ($this->canProductTypeUseNegativePrice($type)) {
  936.             if ($unitPrice >= 0) {
  937.                 throw new Exception('Avoir ürünlerinde birim fiyat negatif olmalıdır.');
  938.             }
  939.             return;
  940.         }
  941.     }
  942.     private function resolveSalesStatus(?string $status): SalesStatus
  943.     {
  944.         if ($status === null) {
  945.             return SalesStatus::PENDING;
  946.         }
  947.         return SalesStatus::tryFrom($status) ?? SalesStatus::PENDING;
  948.     }
  949.     private function shouldAffectStock(SalesStatus $status): bool
  950.     {
  951.         return in_array($status, [SalesStatus::COMPLETEDSalesStatus::PENDING_ITEMS], true);
  952.     }
  953.     /**
  954.      * Check if returns can be created for this sale
  955.      * Returns can only be created for:
  956.      * 1. Sales with COMPLETED or PENDING_ITEMS status
  957.      * 2. Sales that have at least one physical product (excluding services, trees)
  958.      */
  959.     private function canCreateReturnForSale(Sales $sale): bool
  960.     {
  961.         // Check sale status
  962.         $saleStatus $sale->getStatus();
  963.         if (!$saleStatus || !in_array($saleStatus, [SalesStatus::COMPLETEDSalesStatus::PENDING_ITEMS], true)) {
  964.             return false;
  965.         }
  966.         // Check if there are any physical products
  967.         $productsSold $sale->getProductsSolds();
  968.         foreach ($productsSold as $productSold) {
  969.             if (!$productSold instanceof ProductsSold) {
  970.                 continue;
  971.             }
  972.             $product $productSold->getProduct();
  973.             if (!$product) {
  974.                 continue;
  975.             }
  976.             $type $product->getProductTypeEnum();
  977.             if (!$this->isStockExemptProductType($type)) {
  978.                 return true// Found at least one physical product
  979.             }
  980.         }
  981.         return false// No physical products found
  982.     }
  983.     #[Route('/delete-sales/{id}'name'delete-sales')]
  984.     public function delete(Request $request$idEntityManagerInterface $entityManager)
  985.     {
  986.         $sale $entityManager->getRepository(Sales::class)->find($id);
  987.         if (!$sale) {
  988.             throw $this->createNotFoundException('Satış bulunamadı.');
  989.         }
  990.         $performedBy $this->getUser();
  991.         $sale->setVisible(false);
  992.         $this->salesHistoryService->record(
  993.             $sale,
  994.             SalesHistoryEventType::VISIBILITY_CHANGED,
  995.             'Satış gizlendi (soft delete).',
  996.             ['visible' => false],
  997.             $performedBy instanceof User $performedBy null
  998.         );
  999.         $entityManager->persist($sale);
  1000.         $entityManager->flush();
  1001.         $this->addFlash('success''Satış Başarıyla silindi. Stoklar eklendi');
  1002.         return $this->redirectToRoute('app_admin_sales');
  1003.         /*
  1004.         $sale = $entityManager->getRepository(Sales::class)->find($id);
  1005.         $entityManager->beginTransaction();
  1006.         try{
  1007.             if($sale->getInvoice() != null){
  1008.                 $this->invoiceService->deleteInvoice($sale->getInvoice());
  1009.             }
  1010.             if($sale->getStatus() === SalesStatus::PENDING_ITEMS || $sale->getStatus() === SalesStatus::COMPLETED){
  1011.                 $warehouse = $sale->getWarehouse();
  1012.                 foreach ($sale->getProductsSolds() as $salesProductsDto) {
  1013.                         $productEntity = $this->productService->getEntityId($salesProductsDto->getProduct()->getId());
  1014.                         $beforeQuantity = $this->stockInfoService->getStockInfoByProductAndWarehouse($productEntity, $warehouse)->getTotalQuantity();
  1015.                         $stockTransaction = new StockTransaction();
  1016.                         $stockTransaction->setWarehouse($warehouse);
  1017.                         $stockTransaction->setProductId($productEntity);
  1018.                         $stockTransaction->setName($productEntity->getName() . " Code " . $productEntity->getCode());
  1019.                         $stockTransaction->setQuantity($salesProductsDto->getQuantity());
  1020.                         $stockTransaction->setAction("IN");
  1021.                         $stockTransaction->setDescription("Satış siliyor bu yüzden ürün stoğu EKLENİYOR");
  1022.                         $stockTransaction->setBeforeQuantity(
  1023.                             $beforeQuantity
  1024.                         );
  1025.                         $this->stockInfoService->addStock( $productEntity, $warehouse,$salesProductsDto->getQuantity());
  1026.                         $stockTransaction->setAfterQuantity(
  1027.                             $this->stockInfoService->getStockInfoByProductAndWarehouse($productEntity, $warehouse)->getTotalQuantity()
  1028.                         );
  1029.                         $stockTransaction->setSale($sale);
  1030.                         $this->entityManager->remove($salesProductsDto);
  1031.                         $this->entityManager->persist($stockTransaction);
  1032.                     }
  1033.             }else{
  1034.                 $products = $sale->getProductsSolds();
  1035.                 foreach ($products as $product){
  1036.                     $this->entityManager->remove($product);
  1037.                 }
  1038.             }
  1039.             $payments = $sale->getPayments();
  1040.             foreach ($payments as $payment){
  1041.                 $this->paymentService->deletePayment($payment);
  1042.             }
  1043.             $entityManager->remove($sale);
  1044.             $entityManager->flush();
  1045.             $entityManager->commit();
  1046.             $this->addFlash('success', 'Satış Başarıyla silindi. Stoklar eklendi');
  1047.             return $this->redirectToRoute('app_admin_sales');
  1048.         }catch (\Exception $exception){
  1049.             $entityManager->rollback();
  1050.             throw $exception;
  1051.         }
  1052.         */
  1053.     }
  1054.     #[Route('/admin/sales/permanently-delete/{id}'name'app_admin_sales_permanently_delete'methods: ['POST'])]
  1055.     public function permanentlyDelete(
  1056.         Request $request,
  1057.         int $id,
  1058.         SalesRepository $salesRepository,
  1059.         EntityManagerInterface $entityManager,
  1060.         InvoiceService $invoiceService,
  1061.         ProductService $productService,
  1062.         StockInfoService $stockInfoService,
  1063.         PaymentService $paymentService
  1064.     ) {
  1065.         $sale $salesRepository->find($id);
  1066.         if (!$sale) {
  1067.             return new JsonResponse(['success' => false'message' => 'Satış bulunamadı'], 404);
  1068.         }
  1069.         $user $this->getUser();
  1070.         if (!$user || strtolower($user->getUserIdentifier()) !== 'gizli@kullanici.com') {
  1071.             return new JsonResponse(['success' => false'message' => 'Yetkiniz yok'], 403);
  1072.         }
  1073.         $entityManager->beginTransaction();
  1074.         try {
  1075.             // 1. Fatura Kontrolü ve Silme
  1076.             if ($sale->getInvoice() != null) {
  1077.                 // Faturayı sil
  1078.                 $invoiceService->deleteInvoice($sale->getInvoice());
  1079.                 $sale->setInvoice(null);
  1080.             }
  1081.             // 2. Stok İadesi (Stock Reversal)
  1082.             if ($sale->getStatus() === SalesStatus::PENDING_ITEMS || $sale->getStatus() === SalesStatus::COMPLETED) {
  1083.                 $warehouse $sale->getWarehouse();
  1084.                 foreach ($sale->getProductsSolds() as $salesProductsDto) {
  1085.                     $productEntity $this->productService->getEntityId($salesProductsDto->getProduct()->getId());
  1086.                     if ($this->shouldSkipStockUpdate($productEntity)) {
  1087.                         continue;
  1088.                     }
  1089.                     $beforeQuantity $this->stockInfoService->getStockInfoByProductAndWarehouse($productEntity$warehouse)->getTotalQuantity();
  1090.                     $quantityToReverse $salesProductsDto->getBaseUnitQuantity();
  1091.                     if (($quantityToReverse === null || $quantityToReverse == 0) && $salesProductsDto->getSelectedUnit()) {
  1092.                         $quantityToReverse $this->salesServiceImpl->getConvertedQuantity(
  1093.                             $productEntity,
  1094.                             $salesProductsDto->getSelectedUnit()->getId(),
  1095.                             $salesProductsDto->getQuantity()
  1096.                         );
  1097.                     }
  1098.                     if ($quantityToReverse === null || $quantityToReverse == 0) {
  1099.                         $quantityToReverse $salesProductsDto->getQuantity();
  1100.                     }
  1101.                     $stockTransaction = new StockTransaction();
  1102.                     $stockTransaction->setWarehouse($warehouse);
  1103.                     $stockTransaction->setProductId($productEntity);
  1104.                     $stockTransaction->setName($productEntity->getName() . " Code " $productEntity->getCode());
  1105.                     $stockTransaction->setQuantity($quantityToReverse);
  1106.                     $stockTransaction->setAction("IN");
  1107.                     $stockTransaction->setDescription("Satış kalıcı olarak silindi (Soft Delete), stok iade edildi. Satış ID: " $sale->getId());
  1108.                     $stockTransaction->setBeforeQuantity($beforeQuantity);
  1109.                     $this->stockInfoService->addStock($productEntity$warehouse$quantityToReverse);
  1110.                     $stockTransaction->setAfterQuantity(
  1111.                         $this->stockInfoService->getStockInfoByProductAndWarehouse($productEntity$warehouse)->getTotalQuantity()
  1112.                     );
  1113.                     $stockTransaction->setSale($sale);
  1114.                     $entityManager->persist($stockTransaction);
  1115.                 }
  1116.             }
  1117.             // 3. Ödemelerin Silinmesi ve İadesi
  1118.             $payments $sale->getPayments();
  1119.             foreach ($payments as $payment) {
  1120.                 // Ek Kural: Hediye Çeki veya Müşteri Bakiyesi İadesi
  1121.                 $paymentMethod $payment->getPaymentMethod();
  1122.                 if (
  1123.                     $paymentMethod && (
  1124.                         stripos($paymentMethod->getName(), 'Hediye') !== false ||
  1125.                         stripos($paymentMethod->getName(), 'Gift') !== false ||
  1126.                         stripos($paymentMethod->getName(), 'Kupon') !== false
  1127.                     )
  1128.                 ) {
  1129.                     // Müşteriye yeni bir hediye çeki (bakiye) oluşturarak iade yapalım.
  1130.                     // Çünkü hangi çekten düştüğünü takip eden yapı (GiftVoucherUsage) invoice tarafında olabilir ama auto-deduct'ta yoktu.
  1131.                     // En güvenli yöntem: Müşteriye iade tutarı kadar yeni bir hediye çeki tanımlamak.
  1132.                     $refundDescription sprintf('İade: Silinen Satış #%s Ödemesi'$sale->getInvoiceNumber() ?? $sale->getId());
  1133.                     $this->giftVoucherService->createVoucher(
  1134.                         (float) $payment->getAmount(),
  1135.                         $user// İşlemi yapan admin
  1136.                         $sale->getCustomer(),
  1137.                         (new \DateTime())->modify('+1 year'// 1 yıl geçerli
  1138.                     );
  1139.                     // Loglama gerekirse buraya eklenebilir.
  1140.                 }
  1141.                 $entityManager->remove($payment);
  1142.             }
  1143.             // 4. Loglama
  1144.             $this->salesHistoryService->record(
  1145.                 $sale,
  1146.                 SalesHistoryEventType::DELETED,
  1147.                 'Satış kalıcı olarak silindi (Flagged as deleted).',
  1148.                 ['deleted' => true'visible' => false],
  1149.                 $user instanceof User $user null
  1150.             );
  1151.             // 5. Satış Kaydının İşlenmesi (Flag Update)
  1152.             $sale->setDeleted(true);
  1153.             $sale->setVisible(false);
  1154.             $entityManager->flush();
  1155.             $entityManager->commit();
  1156.             return new JsonResponse(['success' => true]);
  1157.         } catch (\Exception $e) {
  1158.             $entityManager->rollback();
  1159.             $this->logger->error('Kalıcı silme hatası: ' $e->getMessage());
  1160.             return new JsonResponse(['success' => false'message' => 'İşlem sırasında hata: ' $e->getMessage()], 500);
  1161.         }
  1162.     }
  1163.     #[Route('/admin/sales/restore/{id}'name'app_admin_sales_restore'methods: ['POST'])]
  1164.     public function restoreSales(Request $requestint $idSalesRepository $salesRepositoryEntityManagerInterface $entityManager)
  1165.     {
  1166.         $sale $salesRepository->find($id);
  1167.         if (!$sale) {
  1168.             return new JsonResponse(['success' => false'message' => 'Satış bulunamadı'], 404);
  1169.         }
  1170.         $user $this->getUser();
  1171.         if (!$user || strtolower($user->getUserIdentifier()) !== 'gizli@kullanici.com') {
  1172.             return new JsonResponse(['success' => false'message' => 'Yetkiniz yok'], 403);
  1173.         }
  1174.         try {
  1175.             $sale->setVisible(true);
  1176.             $sale->setDeleted(false); // Also unset deleted flag if it was set
  1177.             $this->salesHistoryService->record(
  1178.                 $sale,
  1179.                 SalesHistoryEventType::VISIBILITY_CHANGED// Or RESTORED if available
  1180.                 'Satış tekrar görünür yapıldı.',
  1181.                 ['visible' => true'deleted' => false],
  1182.                 $user instanceof User $user null
  1183.             );
  1184.             $entityManager->flush();
  1185.             return new JsonResponse(['success' => true]);
  1186.         } catch (\Exception $e) {
  1187.             return new JsonResponse(['success' => false'message' => 'Hata: ' $e->getMessage()], 500);
  1188.         }
  1189.     }
  1190.     private function shouldSkipStockUpdate($product): bool
  1191.     {
  1192.         // Service ve Tree gibi tiplerde stok tutulmuyorsa true dönmeli
  1193.         // ProductTypeEnum kontrolü gerekebilir. 
  1194.         // Mevcut kodda checkStocks fonksiyonunda: $this->isStockExemptProductType($product->getProductTypeEnum()) kullanılıyor.
  1195.         return $product && $this->isStockExemptProductType($product->getProductTypeEnum());
  1196.     }
  1197.     #[Route('/warehouse-stocks/{warehouseId}'name'app_admin_warehouse_stock_information')]
  1198.     public function getStockByWarehouse(Request $request$warehouseId)
  1199.     {
  1200.         $stocks $this->stockService->getStocksByWarehouse($warehouseId);
  1201.         return $this->json($stocks);
  1202.     }
  1203.     // Tüm satışlar anasayfa
  1204.     #[Route('/sales/all'name'app_admin_sales_all')]
  1205.     public function showAll(Request $requestEntityManagerInterface $entityManager)
  1206.     {
  1207.         $warehouses $this->warehouseService->getAll();
  1208.         return $this->render('admin/sales/allsales.html.twig', [
  1209.             'controller_name' => 'StockController',
  1210.             'warehouses' => $warehouses,
  1211.             'statuses' => SalesStatus::cases(),
  1212.             'defaultMailContent' => $this->getDefaultMailContent(),
  1213.         ]);
  1214.     }
  1215.     // Tüm satışlar ajax ile dataların gönderiliyor
  1216.     #[Route('/admin/sales/show'name'app_admin_sales_show')]
  1217.     public function show(Request $requestEntityManagerInterface $entityManager)
  1218.     {
  1219.         if (isset($request->get('search')['value']))
  1220.             $s $request->get('search')['value'];
  1221.         else
  1222.             $s '';
  1223.         $id $request->get('id');
  1224.         if (isset($request->get('order')[0]['column']))
  1225.             $column $request->get('order')[0]['column'];
  1226.         else
  1227.             $column 0;
  1228.         if (isset($request->get('order')[0]['dir']))
  1229.             $dir $request->get('order')[0]['dir'];
  1230.         else
  1231.             $dir 'ASC';
  1232.         $startDate $request->get('startDate');
  1233.         $finishDate $request->get('finishDate');
  1234.         $limit $request->get('length');
  1235.         if ($request->get('start'))
  1236.             $page + ($request->get('start') / $limit);
  1237.         else
  1238.             $page 1;
  1239.         $warehouseid $request->get('warehouseid');
  1240.         $statusParam $request->get('status');
  1241.         $status null;
  1242.         if (!empty($statusParam)) {
  1243.             try {
  1244.                 $status SalesStatus::from($statusParam);
  1245.             } catch (\ValueError $exception) {
  1246.                 $status null;
  1247.             }
  1248.         }
  1249.         $stocks $this->salesService->getAllSalesPaginate($page$limit$warehouseid$column$dir$s$startDate$finishDate$status$this->getUser());
  1250.         return new JsonResponse($this->createArrayForDataTable($stocks));
  1251.     }
  1252.     private function getPdfContentFromProforma($proformaId)
  1253.     {
  1254.         $logoPath $this->projectDir '/public/images/decopierrelogo.png';
  1255.         $logoDataUri '';
  1256.         if (file_exists($logoPath)) {
  1257.             $logoBase64 base64_encode(file_get_contents($logoPath));
  1258.             $logoDataUri 'data:' mime_content_type($logoPath) . ';base64,' $logoBase64;
  1259.         }
  1260.         $sales $this->salesServiceImpl->getEntityById($proformaId);
  1261.         if (!$sales) {
  1262.             return null// Fatura bulunamazsa null döndür
  1263.         }
  1264.         $companyData = [
  1265.             'name' => 'DECO PIERRE ET NATURE',
  1266.             'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
  1267.             'siren' => '948485271',
  1268.             'tva' => 'FR13948485271',
  1269.             'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com',
  1270.             'bank_details' => [
  1271.                 'titulaire' => 'SAS FAV',
  1272.                 'bank_name' => 'BANQUE POPULAIRE',
  1273.                 'code_banque' => '16807',
  1274.                 'num_compte' => '37402602218',
  1275.                 'iban' => 'FR7616807004133740260221837',
  1276.                 'bic' => 'CCBPFRPPGRE'
  1277.             ]
  1278.         ];
  1279.         $html $this->renderView('/admin/sales/proforma.html.twig', [
  1280.             'proforma' => $sales,
  1281.             'logo_data_uri' => $logoDataUri,
  1282.             'company' => $companyData
  1283.         ]);
  1284.         $footerHtml $this->renderView('/admin/sales/invoice_footer.html.twig', [
  1285.             'company' => $companyData
  1286.         ]);
  1287.         $options = [
  1288.             'footer-html' => $footerHtml,
  1289.             'margin-bottom' => 30,
  1290.             'enable-local-file-access' => true,
  1291.             'encoding' => 'UTF-8',
  1292.         ];
  1293.         return [
  1294.             'html' => $html,
  1295.             'options' => $options,
  1296.         ];
  1297.     }
  1298.     private function getDefaultMailContent(): string
  1299.     {
  1300.         return MailTemplateHelper::getDefaultMailContent();
  1301.     }
  1302.     private function applyMailPlaceholders(string $mailContentSales $sale): string
  1303.     {
  1304.         $customer $sale->getCustomer();
  1305.         $placeholders = [
  1306.             '[[client_name]]' => $customer ? (string) $customer->getFullName() : '',
  1307.             '[[client_email]]' => $customer && $customer->getEmail() ? (string) $customer->getEmail() : '',
  1308.             '[[client_phone]]' => $customer && $customer->getPhone() ? (string) $customer->getPhone() : '',
  1309.         ];
  1310.         foreach ($placeholders as $placeholder => $value) {
  1311.             $mailContent str_replace($placeholder$value ?? ''$mailContent);
  1312.         }
  1313.         return $mailContent;
  1314.     }
  1315.     private function getPdfContentFromInvoice(int $invoiceId): ?array
  1316.     {
  1317.         $logoPath $this->projectDir '/public/images/decopierrelogo.png';
  1318.         $logoDataUri '';
  1319.         if (file_exists($logoPath)) {
  1320.             $logoBase64 base64_encode(file_get_contents($logoPath));
  1321.             $logoDataUri 'data:' mime_content_type($logoPath) . ';base64,' $logoBase64;
  1322.         }
  1323.         $invoice $this->invoiceService->getEntityById($invoiceId);
  1324.         if (!$invoice) {
  1325.             return null;
  1326.         }
  1327.         $companyData = [
  1328.             'name' => 'DECO PIERRE ET NATURE',
  1329.             'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
  1330.             'siren' => '948485271',
  1331.             'tva' => 'FR13948485271',
  1332.             'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com',
  1333.             'bank_details' => [
  1334.                 'titulaire' => 'SAS FAV',
  1335.                 'bank_name' => 'BANQUE POPULAIRE',
  1336.                 'code_banque' => '16807',
  1337.                 'num_compte' => '37402602218',
  1338.                 'iban' => 'FR7616807004133740260221837',
  1339.                 'bic' => 'CCBPFRPPGRE'
  1340.             ]
  1341.         ];
  1342.         $html $this->renderView('/admin/sales/invoice.html.twig', [
  1343.             'invoice' => $invoice,
  1344.             'logo_data_uri' => $logoDataUri,
  1345.             'company' => $companyData
  1346.         ]);
  1347.         $footerHtml $this->renderView('/admin/sales/invoice_footer.html.twig', [
  1348.             'company' => $companyData
  1349.         ]);
  1350.         $options = [
  1351.             'footer-html' => $footerHtml,
  1352.             'margin-bottom' => 30,
  1353.             'enable-local-file-access' => true,
  1354.             'encoding' => 'UTF-8',
  1355.         ];
  1356.         return [
  1357.             'html' => $html,
  1358.             'options' => $options,
  1359.         ];
  1360.     }
  1361.     #[Route('/proforma/send-mail/{proformaId}'name'proforma_send_mail'methods: ['POST'])]
  1362.     public function sendMailToCustomer(Request $request$proformaIdMailService $mailService)
  1363.     {
  1364.         $sale $this->salesService->getEntityById($proformaId);
  1365.         if (!$sale) {
  1366.             return new JsonResponse(['message' => 'sale_not_found'], 404);
  1367.         }
  1368.         $data json_decode($request->getContent(), true);
  1369.         if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
  1370.             $data $request->request->all();
  1371.         }
  1372.         $recipientsRaw $data['recipients'] ?? [];
  1373.         if (!is_array($recipientsRaw)) {
  1374.             $recipientsRaw explode(',', (string) $recipientsRaw);
  1375.         }
  1376.         if (empty($recipientsRaw) && $sale->getCustomer() && $sale->getCustomer()->getEmail()) {
  1377.             $recipientsRaw = [$sale->getCustomer()->getEmail()];
  1378.         }
  1379.         $recipients = [];
  1380.         foreach ($recipientsRaw as $recipient) {
  1381.             $recipient mb_strtolower(trim((string) $recipient));
  1382.             if ($recipient && filter_var($recipientFILTER_VALIDATE_EMAIL)) {
  1383.                 $recipients[] = $recipient;
  1384.             }
  1385.         }
  1386.         $recipients array_values(array_unique($recipients));
  1387.         if (!$recipients) {
  1388.             return new JsonResponse(['message' => 'recipient_not_found'], 400);
  1389.         }
  1390.         $mailContent $data['content'] ?? $this->getDefaultMailContent();
  1391.         if (!$mailContent) {
  1392.             $mailContent $this->getDefaultMailContent();
  1393.         }
  1394.         $mailContent $this->applyMailPlaceholders($mailContent$sale);
  1395.         $includeProforma array_key_exists('includeProforma'$data)
  1396.             ? filter_var($data['includeProforma'], FILTER_VALIDATE_BOOLEAN)
  1397.             : true;
  1398.         $includeInvoice array_key_exists('includeInvoice'$data)
  1399.             ? filter_var($data['includeInvoice'], FILTER_VALIDATE_BOOLEAN)
  1400.             : ($sale->getInvoice() !== null);
  1401.         $attachments = [];
  1402.         if ($includeProforma) {
  1403.             $proformaContent $this->getPdfContentFromProforma($proformaId);
  1404.             if ($proformaContent) {
  1405.                 $proformaPath $this->pdfService->convertAndSave(
  1406.                     $proformaContent['html'],
  1407.                     $sale->getInvoiceNumber(),
  1408.                     'proforma',
  1409.                     $proformaContent['options']
  1410.                 );
  1411.                 if ($proformaPath && file_exists($proformaPath)) {
  1412.                     $attachments[] = $proformaPath;
  1413.                 }
  1414.             }
  1415.         }
  1416.         if ($includeInvoice && $sale->getInvoice()) {
  1417.             $invoiceContent $this->getPdfContentFromInvoice($sale->getInvoice()->getId());
  1418.             if ($invoiceContent) {
  1419.                 $invoicePath $this->pdfService->convertAndSave(
  1420.                     $invoiceContent['html'],
  1421.                     $sale->getInvoice()->getInvoiceNumber(),
  1422.                     'invoice',
  1423.                     $invoiceContent['options']
  1424.                 );
  1425.                 if ($invoicePath && file_exists($invoicePath)) {
  1426.                     $attachments[] = $invoicePath;
  1427.                 }
  1428.             }
  1429.         }
  1430.         $subject = ($includeInvoice && $sale->getInvoice())
  1431.             ? 'Facture ' $sale->getInvoice()->getInvoiceNumber()
  1432.             : 'Devis ' $sale->getInvoiceNumber();
  1433.         //        dd($mailContent);
  1434.         $result $mailService->sendAdvancedMail([
  1435.             'to' => $recipients,
  1436.             'subject' => $subject,
  1437.             'html' => $mailContent,
  1438.             'bcc' => ['arsiv@decopierrenature.com'],
  1439.             'replyTo' => 'decopierrefatih@gmail.com',
  1440.             'attachments' => array_values(array_unique($attachments)),
  1441.             'sale' => $sale,
  1442.             'senderUser' => $this->getUser(),
  1443.         ]);
  1444.         $message $result 'success' 'error';
  1445.         return new JsonResponse(['message' => $message]);
  1446.     }
  1447.     /**
  1448.      * @throws Exception
  1449.      */
  1450.     #[Route('/proforma/convert-to-invoice/{salesId}'name'proforma_convert_to_invoice')]
  1451.     public function convertToInvoice(int $salesId): JsonResponse
  1452.     {
  1453.         $this->entityManager->beginTransaction();
  1454.         try {
  1455.             /** @var Sales|null $sales */
  1456.             $sales $this->entityManager->getRepository(Sales::class)->find($salesIdLockMode::PESSIMISTIC_WRITE);
  1457.             if (!$sales) {
  1458.                 throw new Exception(sprintf("Proforma not found with %s id"$salesId));
  1459.             }
  1460.             if ($sales->getInvoice()) {
  1461.                 $invoiceId $sales->getInvoice()->getId();
  1462.                 $this->entityManager->commit();
  1463.                 return new JsonResponse(
  1464.                     [
  1465.                         'success' => false,
  1466.                         'message' => 'invoice_already_exists',
  1467.                         'invoiceId' => $invoiceId,
  1468.                     ],
  1469.                     Response::HTTP_CONFLICT
  1470.                 );
  1471.             }
  1472.             $invoice $this->invoiceService->create($sales);
  1473.             $this->entityManager->commit();
  1474.             return new JsonResponse(
  1475.                 [
  1476.                     'success' => true,
  1477.                     'invoiceId' => $invoice->getId(),
  1478.                 ],
  1479.                 Response::HTTP_CREATED
  1480.             );
  1481.         } catch (Exception $exception) {
  1482.             $this->entityManager->rollback();
  1483.             throw $exception;
  1484.         }
  1485.     }
  1486.     private function createArrayForDataTable($stocks): array
  1487.     {
  1488.         $records = [];
  1489.         $records['recordsTotal'] = $stocks->getTotalItemCount();
  1490.         $records['recordsFiltered'] = $stocks->getTotalItemCount();
  1491.         $records["data"] = [];
  1492.         /**
  1493.          * @var Sales $entity
  1494.          */
  1495.         foreach ($stocks as $entity) {
  1496.             $pdfLink '/admin/sales/proforma/generate-pdf/' $entity->getId();
  1497.             $printLink '/admin/sales/proforma/preview-print/' $entity->getId();
  1498.             $showProforma '/admin/sales/proforma/' $entity->getId();
  1499.             $customer $entity->getCustomer();
  1500.             $customerName $customer ? (string) $customer->getFullName() : '';
  1501.             $customerEmail $customer && $customer->getEmail() ? mb_strtolower((string) $customer->getEmail(), 'UTF-8') : '';
  1502.             $customerPhone $customer && $customer->getPhone() ? (string) $customer->getPhone() : '';
  1503.             $proformaPreviewUrl $this->generateUrl('admin_sales_proforma_preview_print', ['salesId' => $entity->getId()]);
  1504.             $invoicePreviewUrl $entity->getInvoice() ? $this->generateUrl('admin_sales_invoice_preview_print', ['invoiceId' => $entity->getInvoice()->getId()]) : '';
  1505.             $sendMailUrl $this->generateUrl('proforma_send_mail', ['proformaId' => $entity->getId()]);
  1506.             $sendMailAttributes sprintf(
  1507.                 'data-id="%s" data-sale-id="%s" data-send-url="%s" data-default-email="%s" data-customer-name="%s" data-customer-email="%s" data-customer-phone="%s" data-proforma-url="%s" data-invoice-url="%s" data-has-invoice="%s"',
  1508.                 htmlspecialchars((string) $entity->getId(), ENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1509.                 htmlspecialchars((string) $entity->getId(), ENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1510.                 htmlspecialchars($sendMailUrlENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1511.                 htmlspecialchars($customerEmailENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1512.                 htmlspecialchars($customerNameENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1513.                 htmlspecialchars($customerEmailENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1514.                 htmlspecialchars($customerPhoneENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1515.                 htmlspecialchars($proformaPreviewUrlENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1516.                 htmlspecialchars($invoicePreviewUrlENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1517.                 $entity->getInvoice() ? '1' '0'
  1518.             );
  1519.             $actionButtons '';
  1520.             // Hidden User Buttons for Hidden Sales
  1521.             if ($this->getUser() && strtolower($this->getUser()->getUserIdentifier()) === 'gizli@kullanici.com' && !$entity->isVisible()) {
  1522.                 $actionButtons .= '<a href="javascript:void(0);" class="restore-sale-btn" data-id="' $entity->getId() . '" title="Görünür Yap" style="margin-right: 8px;"><i class="fas fa-eye fa-lg" style="color: #17a2b8;"></i></a>';
  1523.                 $actionButtons .= '<a href="javascript:void(0);" class="permanently-delete-btn" data-id="' $entity->getId() . '" title="Kalıcı Olarak Sil" style="margin-right: 8px;"><i class="fas fa-trash-alt fa-lg" style="color: #d32f2f;"></i></a>';
  1524.             }
  1525.             // Existing Buttons
  1526.             $actionButtons .= sprintf(
  1527.                 '<a href="%s" target="_blank" title="Download PDF" style="margin-right: 8px;"><i class="far fa-file-pdf fa-lg" style="color: #d32f2f;"></i></a>
  1528.                         <a type="button" class="send-mail-btn" %s title="Send Mail" style="margin-right: 8px;"><i class="fas fa-mail-bulk fa-lg" style="color: #3498db;"></i></a>
  1529.                         <a href="%s" title="Print" target="_blank"><i class="fas fa-print fa-lg" style="color: #026309;"></i></a>
  1530.                         <a href="%s" title="Show" target="_blank"><i class="fas fa-eye fa-lg"></i></a>',
  1531.                 $pdfLink,
  1532.                 $sendMailAttributes,
  1533.                 $printLink,
  1534.                 $showProforma
  1535.             );
  1536.             $actions $actionButtons;
  1537.             $salesStatus $entity->getStatus();
  1538.             $statusLabel $salesStatus?->getLabel() ?? 'Belirtilmedi';
  1539.             $statusValue $salesStatus?->value ?? 'UNSPECIFIED';
  1540.             $statusCell sprintf(
  1541.                 "<span class='sales-status-badge' data-status='%s'>%s</span>",
  1542.                 htmlspecialchars($statusValueENT_QUOTES ENT_SUBSTITUTE'UTF-8'),
  1543.                 htmlspecialchars($statusLabelENT_QUOTES ENT_SUBSTITUTE'UTF-8')
  1544.             );
  1545.             $records["data"][] = [
  1546.                 $entity->getId(),
  1547.                 $entity->getInvoiceNumber(),
  1548.                 $customerName,
  1549.                 CurrencyHelper::convertToCurrency($entity->getTotalPurchasePrice()) . ' ' $this->translator->trans('currency.symbol'),
  1550.                 $entity->getSeller()->getFullname(),
  1551.                 $entity->getInvoice() ?
  1552.                 "<a target='_blank' href='" $this->generateUrl('admin_sales_invoice_preview_print', ['invoiceId' => $entity->getInvoice()->getId()]) . "' class='btn btn-primary btn-xs showInvoiceBtn'>" $this->translator->trans('showInvoice') . "</a>" :
  1553.                 "<a target='_blank' data-id='" $entity->getId() . "' class='btn btn-warning btn-xs convertToInvoiceBtn'>" $this->translator->trans('convertToInvoice') . "</a>",
  1554.                 $entity->getSalesDate()->format('d.m.Y'),
  1555.                 $statusCell,
  1556.                 $this->getPaymentStatus($entity),
  1557.                 $actions
  1558.             ];
  1559.         }
  1560.         return $records;
  1561.     }
  1562.     private function getPaymentStatus(Sales $sales)
  1563.     {
  1564.         $paymentStatus '';
  1565.         foreach ($sales->getPayments() as $payment) {
  1566.             if ($payment->getPaymentStatus() == 2) {
  1567.                 $paymentStatus "TERM_SALE-" $this->translator->trans('payment_status.term_sale');
  1568.                 break;
  1569.             }
  1570.             if ($payment->getPaymentStatus() == 1) {
  1571.                 $paymentStatus "UNPAID-" $this->translator->trans('payment_status.not_paid');
  1572.                 ;
  1573.                 break;
  1574.             }
  1575.             $paymentStatus "PAID-" $this->translator->trans('payment_status.paid');
  1576.             ;
  1577.         }
  1578.         return $paymentStatus;
  1579.     }
  1580.     // PROFORMAYI HTML SAYFASI OLARAK GÖSTERİYOR
  1581.     #[Route('/admin/sales/proforma/{salesId}'name'admin_sales_proforma_show')]
  1582.     public function showInvoice(Request $request$salesId)
  1583.     {
  1584.         $logoPath $this->projectDir '/public/images/decopierrelogo.png';
  1585.         $logoDataUri '';
  1586.         if (file_exists($logoPath)) {
  1587.             $logoBase64 base64_encode(file_get_contents($logoPath));
  1588.             $logoDataUri 'data:' mime_content_type($logoPath) . ';base64,' $logoBase64;
  1589.         }
  1590.         $sales $this->salesServiceImpl->getEntityById($salesId);
  1591.         $html $this->render('/admin/sales/proforma.html.twig', [
  1592.             'proforma' => $sales,
  1593.             'logo_data_uri' => $logoDataUri,
  1594.             'company' => [
  1595.                 'name' => 'DECO PIERRE & NATURE',
  1596.                 'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
  1597.                 'siren' => '948485271',
  1598.                 'tva' => 'FR13948485271',
  1599.                 'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com'
  1600.             ]
  1601.         ]);
  1602.         return $html;
  1603.     }
  1604.     // PROFORMAYI PDF OLARAK İNDİRİYOR
  1605.     #[Route('/admin/sales/proforma/generate-pdf/{salesId}'name'admin_sales_proforma_generate_pdf')]
  1606.     public function generatePdf(Request $request$salesIdPdf $knpSnappyPdf)
  1607.     {
  1608.         $logoPath $this->projectDir '/public/images/decopierrelogo.png';
  1609.         $logoDataUri '';
  1610.         if (file_exists($logoPath)) {
  1611.             $logoBase64 base64_encode(file_get_contents($logoPath));
  1612.             $logoDataUri 'data:' mime_content_type($logoPath) . ';base64,' $logoBase64;
  1613.         }
  1614.         $invoice $this->salesServiceImpl->getEntityById($salesId);
  1615.         // Şirket ve banka bilgilerini içeren bir array oluşturun
  1616.         $companyData = [
  1617.             'name' => 'DECO PIERRE ET NATURE',
  1618.             'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
  1619.             'siren' => '948485271',
  1620.             'tva' => 'FR13948485271',
  1621.             'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com',
  1622.             'bank_details' => [
  1623.                 'titulaire' => 'SAS FAV',
  1624.                 'bank_name' => 'BANQUE POPULAIRE',
  1625.                 'code_banque' => '16807',
  1626.                 'num_compte' => '37402602218',
  1627.                 'iban' => 'FR7616807004133740260221837',
  1628.                 'bic' => 'CCBPFRPPGRE'
  1629.             ]
  1630.         ];
  1631.         // Ana fatura içeriği için Twig render'ı
  1632.         $html $this->renderView('/admin/sales/proforma.html.twig', [
  1633.             'proforma' => $invoice,
  1634.             'logo_data_uri' => $logoDataUri,
  1635.             'company' => $companyData
  1636.         ]);
  1637.         // Footer içeriği için Twig render'ı
  1638.         // NOT: footer.html.twig dosyasına sadece companyData'yı göndermeniz yeterlidir.
  1639.         $footerHtml $this->renderView('/admin/sales/invoice_footer.html.twig', [
  1640.             'company' => $companyData
  1641.         ]);
  1642.         $options = [
  1643.             'footer-html' => $footerHtml,
  1644.             'margin-bottom' => 30,
  1645.             'enable-local-file-access' => true,
  1646.             'encoding' => 'UTF-8',
  1647.         ];
  1648.         return new PdfResponse(
  1649.             $knpSnappyPdf->getOutputFromHtml($html$options),
  1650.             $invoice->getInvoiceNumber() . '.pdf',
  1651.             'inline'
  1652.         );
  1653.     }
  1654.     // PROFORMAYI PDF OLARAK KAYDEDİYOR DOSYA YOLU DÖNÜYOR
  1655.     private function createAndSavePdf(int $invoiceId): ?string
  1656.     {
  1657.         $content $this->getPdfContentFromProforma($invoiceId);
  1658.         return $this->pdfService->convertAndSave($content['html'], $invoiceId'proforma'$content['options']);
  1659.     }
  1660.     // PROFORMAYI PDF OLARAK AÇAR ANCAK YAZDIRMA SEÇENEĞİ AÇILMAZ
  1661.     #[Route('/admin/sales/proforma/send-pdf/{salesId}'name'admin_sales_proforma_send_pdf')]
  1662.     public function sendPdf(Request $requestint $salesId): Response
  1663.     {
  1664.         $filePath $this->createAndSavePdf($salesId);
  1665.         if (!$filePath || !file_exists($filePath)) {
  1666.             throw $this->createNotFoundException('PDF dosyası oluşturulamadı veya bulunamadı.');
  1667.         }
  1668.         $sales $this->salesServiceImpl->getEntityById($salesId);
  1669.         $fileName $sales $sales->getInvoiceNumber() . '.pdf' 'fatura.pdf';
  1670.         $response = new BinaryFileResponse($filePath);
  1671.         $response->setContentDisposition(
  1672.             ResponseHeaderBag::DISPOSITION_INLINE,
  1673.             $fileName
  1674.         );
  1675.         $response->headers->set('Cache-Control''no-cache, no-store, must-revalidate');
  1676.         $response->headers->set('Pragma''no-cache');
  1677.         $response->headers->set('Expires''0');
  1678.         return $response;
  1679.     }
  1680.     // PROFORMAYI PDF OLARAK AÇAR VE YAZDIRMA SEÇENEĞİ ÇIKAR
  1681.     #[Route('/admin/sales/proforma/preview-print/{salesId}'name'admin_sales_proforma_preview_print')]
  1682.     public function previewAndPrint(int $salesId): Response
  1683.     {
  1684.         return $this->render('/admin/sales/proforma_print_preview.html.twig', [
  1685.             'salesId' => $salesId,
  1686.             'pdf_url' => $this->generateUrl('admin_sales_proforma_send_pdf', ['salesId' => $salesId]),
  1687.         ]);
  1688.     }
  1689.     public function findColumn($rank)
  1690.     {
  1691.         $columns = [
  1692.             'id',
  1693.             'invoiceNumber',
  1694.             'salesCompany',
  1695.             'totalPurchasePrice',
  1696.             'seller',
  1697.             'salesDate'
  1698.         ];
  1699.         return $columns[$rank];
  1700.     }
  1701.     #[Route('/admin/excel')]
  1702.     public function excelDetail()
  1703.     {
  1704.         $spreadsheet IOFactory::load($this->getParameter('kernel.project_dir') . '/public/yeni.xlsx'); // Here we are able to read from the excel file
  1705.         $row $spreadsheet->getActiveSheet()->removeRow(1); // I added this to be able to remove the first file line
  1706.         $sheetData $spreadsheet->getActiveSheet()->toArray(nulltruetruetrue); // here, the read data is turned into an array
  1707.         dd($sheetData);
  1708.     }
  1709.     #[Route('/product-cost-detail/{id}'name'product_cost_detail')]
  1710.     public function getProductCost(Request $request$id)
  1711.     {
  1712.         $warehouseId $request->get('warehouse');
  1713.         $product $this->productService->getEntityId($id);
  1714.         $warehouse $this->entityManager->getRepository(Warehouse::class)->findOneBy(['id' => $warehouseId]);
  1715.         /**
  1716.          * @var CalculatorProductsCostsDTO $cost
  1717.          */
  1718.         $cost $this->stockRepository->calculateCost($id$warehouse);
  1719.         $stock $this->stockInfoService->getStockInfoByProductAndWarehouse($product$warehouse);
  1720.         $info = [
  1721.             'cost' => $cost->getAverageCostPrice(),
  1722.             'stock' => $stock->getTotalQuantity(),
  1723.             'measurement' => $product->getMeasurementUnit()?->getName(),
  1724.         ];
  1725.         return $this->json($info);
  1726.     }
  1727.     #[Route('/hesapla/kar')]
  1728.     public function calculateProfitOrDamage(EntityManagerInterface $entityManager)
  1729.     {
  1730.         $salesId 13;
  1731.         $productId 16;
  1732.         $quantity 8;
  1733.         $totalPrice 55.1;
  1734.         $sale $this->salesService->getEntityById($salesId);
  1735.         $product $this->productService->getEntityId($productId);
  1736.         $product->getStockTransferProducts();
  1737.         $warehouse $sale->getWarehouse();
  1738.         $stockRepo $entityManager->getRepository(Stock::class);
  1739.         $lastStock $stockRepo->findOneBy(['product' => $product'warehouse' => $warehouse], ['createdAt' => 'desc']);
  1740.         $stockTransfer $lastStock->getStockTransfer();
  1741.         dd($stockTransfer);
  1742.         dd($product->getStockTransferProducts());
  1743.         //        $transferProduct = $this->stockTransferService->getEntityById($sale-)
  1744.     }
  1745. }