<?php
namespace App\Controller\Admin\Sales;
use App\Dto\CalculatorProductsCostsDTO;
use App\Dto\NewStockDto\SalesProductsDto;
use App\Dto\PaginationDTO;
use App\Dto\SalesProductsDtoBuilder;
use App\Entity\Customer;
use App\Entity\MeasurementUnits;
use App\Entity\Payment;
use App\Entity\ProductsSold;
use App\Entity\Sales;
use App\Entity\SalesHistory;
use App\Entity\Stock;
use App\Entity\StockTransaction;
use App\Entity\User;
use App\Entity\Warehouse;
use App\Enum\SalesHistoryEventType;
use App\Enum\SalesStatus;
use App\Enum\SalesType;
use App\Exception\IncorrectAmountException;
use App\Exception\StockNotAvaibleException;
use App\Exception\StockNotFoundException;
use App\Form\CustomerType;
use App\Form\PaymentType;
use App\Form\ProductsSoldType;
use App\Form\SalesFormType;
use App\Repository\SalesRepository;
use App\Repository\StockRepository;
use App\Services\Customer\CustomerService;
use App\Services\MailService;
use App\Services\MeasurementConversions\MeasurementConversionsService;
use App\Services\PDFService;
use App\Services\LogService;
use App\Services\Payment\PaymentMethodService;
use App\Services\Payment\PaymentReminderService;
use App\Services\Payment\PaymentService;
use App\Services\Product\ProductService;
use App\Services\Sales\Impl\SalesProductsServiceImpl;
use App\Services\Sales\Impl\SalesServiceImpl;
use App\Services\Sales\InvoiceService;
use App\Services\Sales\SalesHistoryService;
use App\Services\Sales\SalesService;
use App\Repository\SalesReturnRepository;
use App\Services\Stock\StockInfoService;
use App\Services\Stock\StockService;
use App\Services\Stock\StockTransferService;
use App\Services\StockTransactionService;
use App\Services\Warehouse\WarehouseService;
use App\Utils\CurrencyHelper;
use App\Utils\MailTemplateHelper;
use Doctrine\DBAL\LockMode;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Exception;
use Knp\Bundle\SnappyBundle\Snappy\Response\PdfResponse;
use Knp\Snappy\Pdf;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class SalesController extends AbstractController
{
private const STOCK_EXEMPT_PRODUCT_TYPES = ['INSTALLATION_SERVICE', 'MAINTENANCE_SERVICE', 'CONSULTATION_SERVICE', 'TREE'];
private const NEGATIVE_PRICE_ALLOWED_PRODUCT_TYPES = [];
private const MONEY_SCALE = 2;
private StockService $stockService;
private SerializerInterface $serializer;
private WarehouseService $warehouseService;
private SalesService $salesService;
private $projectDir;
private StockTransferService $stockTransferService;
private ProductService $productService;
private StockInfoService $stockInfoService;
private EntityManagerInterface $entityManager;
private StockRepository $stockRepository;
private CustomerService $customerService;
private PaymentMethodService $paymentMethodService;
private PaymentService $paymentService;
private PaymentReminderService $paymentReminderService;
private LogService $logService;
private StockTransactionService $stockTransactionService;
private LoggerInterface $logger;
private SalesProductsServiceImpl $salesProductsServiceImpl;
private SalesHistoryService $salesHistoryService;
private SalesReturnRepository $salesReturnRepository;
private TranslatorInterface $translator;
private SalesServiceImpl $salesServiceImpl;
private Pdf $knpSnappyPdf;
private InvoiceService $invoiceService;
private PDFService $pdfService;
private MeasurementConversionsService $measurementConversionsService;
/**
* @param KernelInterface $kernel
* @param StockService $stockService
* @param SerializerInterface $serializer
* @param WarehouseService $warehouseService
* @param SalesService $salesService
* @param StockTransferService $stockTransferService
* @param ProductService $productService
* @param StockInfoService $stockInfoService
* @param EntityManagerInterface $entityManager
* @param StockRepository $stockRepository
* @param CustomerService $customerService
* @param PaymentMethodService $paymentMethodService
* @param PaymentService $paymentService
* @param PaymentReminderService $paymentReminderService
* @param LogService $logService
* @param StockTransactionService $stockTransactionService
* @param LoggerInterface $logger
* @param SalesProductsServiceImpl $salesProductsServiceImpl
* @param TranslatorInterface $translator
* @param SalesServiceImpl $salesServiceImpl
* @param Pdf $knpSnappyPdf
* @param InvoiceService $invoiceService
* @param PDFService $pdfService
* @param MeasurementConversionsService $measurementConversionsService
*/
public function __construct(
KernelInterface $kernel,
StockService $stockService,
SerializerInterface $serializer,
WarehouseService $warehouseService,
SalesService $salesService,
StockTransferService $stockTransferService,
ProductService $productService,
StockInfoService $stockInfoService,
EntityManagerInterface $entityManager,
StockRepository $stockRepository,
CustomerService $customerService,
PaymentMethodService $paymentMethodService,
PaymentService $paymentService,
PaymentReminderService $paymentReminderService,
LogService $logService,
StockTransactionService $stockTransactionService,
LoggerInterface $logger,
SalesProductsServiceImpl $salesProductsServiceImpl,
SalesHistoryService $salesHistoryService,
SalesReturnRepository $salesReturnRepository,
TranslatorInterface $translator,
SalesServiceImpl $salesServiceImpl,
Pdf $knpSnappyPdf,
InvoiceService $invoiceService,
PDFService $pdfService,
MeasurementConversionsService $measurementConversionsService
) {
$this->stockService = $stockService;
$this->serializer = $serializer;
$this->warehouseService = $warehouseService;
$this->salesService = $salesService;
$this->stockTransferService = $stockTransferService;
$this->productService = $productService;
$this->stockInfoService = $stockInfoService;
$this->entityManager = $entityManager;
$this->stockRepository = $stockRepository;
$this->customerService = $customerService;
$this->paymentMethodService = $paymentMethodService;
$this->paymentService = $paymentService;
$this->paymentReminderService = $paymentReminderService;
$this->logService = $logService;
$this->stockTransactionService = $stockTransactionService;
$this->logger = $logger;
$this->salesProductsServiceImpl = $salesProductsServiceImpl;
$this->salesHistoryService = $salesHistoryService;
$this->salesReturnRepository = $salesReturnRepository;
$this->translator = $translator;
$this->salesServiceImpl = $salesServiceImpl;
$this->projectDir = $kernel->getProjectDir();
$this->knpSnappyPdf = $knpSnappyPdf;
$this->invoiceService = $invoiceService;
$this->pdfService = $pdfService;
$this->measurementConversionsService = $measurementConversionsService;
}
/**
* @throws StockNotAvaibleException
* @throws StockNotFoundException
*/
#[Route('/sales', name: 'app_admin_sales')]
public function index(Request $request, SalesServiceImpl $salesServiceImpl, TranslatorInterface $translator): Response
{
$paymentMethods = $this->paymentMethodService->getPaymentMethods();
$sales = new Sales();
$sales->setInvoiceNumber($translator->trans('willBeCreatedAutomatically'));
$form = $this->createForm(SalesFormType::class, $sales);
$form->handleRequest($request);
$soldProducts = new ProductsSold();
$soldProductsFrom = $this->createForm(ProductsSoldType::class, $soldProducts);
$customer = new Customer();
$customer->setCode($this->customerService->createCustomerCode());
$customerForm = $this->createForm(CustomerType::class, $customer);
$customerForm->handleRequest($request);
$payment = new Payment();
$paymentForm = $this->createForm(PaymentType::class, $payment);
$paymentForm->handleRequest($request);
$stocks = $this->stockService->getAll();
$warehouses = $this->warehouseService->getAll();
if ($customerForm->isSubmitted() && $customerForm->isValid()) {
$customer = $customerForm->getData();
$customers = $this->customerService->findByFullName($customer->getFullName());
$this->addFlash('success', 'Müşteri başarıyla eklendi. Listeden seçebilirsiniz.');
$email = $customer->getEmail();
$customer->setEmail(mb_strtolower($email, 'UTF-8'));
$this->customerService->save($customerForm->getData());
}
return $this->render('admin/sales/index.html.twig', [
'controller_name' => 'SalesController',
'form' => $form->createView(),
'productsSoldForm' => $soldProductsFrom->createView(),
'customerForm' => $customerForm->createView(),
'paymentForm' => $paymentForm->createView(),
'warehouses' => $warehouses,
'paymentMethods' => $paymentMethods,
]);
}
// Duplicate proforma
#[Route('/sales/duplicate/{id}', name: 'app_admin_sales_duplicate')]
public function duplicateProforma(Request $request, SalesServiceImpl $salesServiceImpl, TranslatorInterface $translator)
{
$paymentMethods = $this->paymentMethodService->getPaymentMethods();
$sales = $this->salesService->getEntityById($request->get('id'));
// dd($sales->getProductsSolds()->get(0));
$sales->setInvoiceNumber($translator->trans('willBeCreatedAutomatically'));
$form = $this->createForm(SalesFormType::class, $sales);
$form->handleRequest($request);
$soldProducts = new ProductsSold();
$soldProductsFrom = $this->createForm(ProductsSoldType::class, $soldProducts);
$customer = new Customer();
$customer->setCode($this->customerService->createCustomerCode());
$customerForm = $this->createForm(CustomerType::class, $customer);
$customerForm->handleRequest($request);
$payment = new Payment();
$paymentForm = $this->createForm(PaymentType::class, $payment);
$paymentForm->handleRequest($request);
$stocks = $this->stockService->getAll();
$warehouses = $this->warehouseService->getAll();
if ($customerForm->isSubmitted() && $customerForm->isValid()) {
$customer = $customerForm->getData();
$customers = $this->customerService->findByFullName($customer->getFullName());
if (count($customers) > 0) {
$this->addFlash('error', 'Bu müşteri kaydı zaten var ! Listeden seçip devam edebilirsiniz.');
} else {
$this->addFlash('success', 'Müşteri başarıyla eklendi. Listeden seçebilirsiniz.');
$email = $customer->getEmail();
$customer->setEmail(mb_strtolower($email, 'UTF-8'));
$this->customerService->save($customerForm->getData());
}
}
return $this->render('admin/sales/index.html.twig', [
'sales' => $sales,
'controller_name' => 'SalesController',
'form' => $form->createView(),
'productsSoldForm' => $soldProductsFrom->createView(),
'customerForm' => $customerForm->createView(),
'paymentForm' => $paymentForm->createView(),
'warehouses' => $warehouses,
'paymentMethods' => $paymentMethods,
'sales'
]);
}
#[Route('/sales/create', name: 'app_admin_sales_create')]
public function create(Request $request, SalesServiceImpl $salesServiceImpl): Response
{
$this->logger->info("sales", [
'title' => 'Satış işlemi başlatıldı',
'request' => $request->request->all(),
]);
$this->entityManager->beginTransaction();
$products = $request->get('products');
$payments = $request->get('payments');
$soldList = $request->get('soldList');
$invoice = $request->get('invoice');
$warehouseId = $soldList[0]['warehouse'];
$salesType = $invoice['salesType'] ?? 'proforma';
$intendedStatus = $this->resolveSalesStatus($invoice['salesStatus'] ?? null);
try {
// Stok kontrolü: yalnızca COMPLETED veya PENDING_ITEMS durumunda yapılır
if ($this->shouldAffectStock($intendedStatus)) {
$this->checkStocks($soldList);
}
// Ürün bazlı indirim kdv gibi işlemler yapılıp total fiyatların frontend ile aynı olup olmadığı kontrol ediliyor.
foreach ($soldList as $soldProduct) {
$this->logger->info("sales", [
'title' => 'Ürün bazlı indirim kdv hesaplanıyor...',
'product' => $soldProduct,
]);
$this->assertUnitPriceAllowed($soldProduct);
$this->calculateTotalPrice($soldProduct);
}
// Ödeme planı ile ürünlerin toplam fiyatlarının eşitliği kontrol ediliyor.
$totalPrice = $this->checkPaymentAndAmountIsEqual($payments, $soldList);
$repo = $this->entityManager->getRepository(Sales::class);
$invoiceNumber = $salesServiceImpl->createProformaNumber();
$sale = $repo->findBy(['invoiceNumber' => $invoiceNumber]);
// dd($request);
if ($sale != null) {
throw new Exception(sprintf("%s fatura numarası ile kayıtlı bir fatura var. Lütfen kontrol ediniz.", $invoice['invoiceNumber']));
} else {
$soldProductsArray = new ArrayCollection();
for ($i = 0; $i < count($soldList); $i++) {
$product = $this->productService->getEntityId($soldList[$i]['productid']);
$calculatorDto = $this->stockRepository->calculateCost($soldList[$i]['productid'], $warehouseId);
$builder = new SalesProductsDtoBuilder();
$soldProductsArray->add(
$builder
->setProductName($soldList[$i]['productName'])
->setMeasurement($product->getMeasurement()?->getMeasurement())
->setQuantity($soldList[$i]['quantity'])
->setUnitPriceFob($calculatorDto->getPriceFob())
->setUnitPriceNavlun($calculatorDto->getPriceNavlun())
->setTotalUnitPrice($calculatorDto->getAverageCostPrice())
->setTotalUnitPurchasePrice($soldList[$i]['totalPurchasePrice'])
->setProductId($product->getId())
->setTotalUnitPrice($soldList[$i]['unitPrice'])
->setWarehouseId($warehouseId)
->setTax($soldList[$i]['tax'])
->setTotalSalePrice($soldList[$i]['totalPurchasePrice'])
->setDiscount($soldList[$i]['discount'])
->setUnAllocatedQuantity($soldList[$i]['unAllocatedQuantity'])
->setSelectedUnitId($soldList[$i]['selectedUnitId'])
->build()
);
}
$customer = $this->customerService->getCustomerById($invoice['customer']);
$sales = new Sales();
$sales->setSalesDate(new \DateTime($invoice['salesDate']));
$sales->setCustomer($customer);
$sales->setStatus($intendedStatus);
$userRepo = $this->entityManager->getRepository(User::class);
$sales->setSeller($userRepo->find($invoice['seller']));
$sales->setInvoiceNumber($invoice['invoiceNumber']);
$sales->setTotalPurchasePrice($totalPrice);
$sales->setDescription($invoice['description']);
$sales->setShippingNotes($invoice['shippingNotes']);
$sales->setPaymentNotes($invoice['paymentNotes']);
$performedBy = $this->getUser();
$sales = $this->salesService->create(
$sales,
$soldProductsArray,
$salesType,
$performedBy instanceof User ? $performedBy : null
);
$this->createPayments($payments, $customer, $sales);
$this->entityManager->commit();
return new JsonResponse(['success' => true, 'id' => $sales->getId()]);
}
} catch (Exception $e) {
$this->entityManager->rollback();
$message = $e->getMessage();
return new JsonResponse(['status' => 'error', 'message' => $message], 400);
}
return new JsonResponse(['status' => 'success']);
}
#[Route('/sales/un-allocated-products', name: 'un_allocated_products_index')]
public function getUnAllocatedProducts(Request $request)
{
$warehouses = $this->warehouseService->getAll();
if ($request->getMethod() == 'POST') {
if (isset($request->get('search')['value']))
$s = $request->get('search')['value'];
else
$s = '';
if (isset($request->get('order')[0]['column']))
$column = $request->get('order')[0]['column'];
else
$column = 0;
if (isset($request->get('order')[0]['dir']))
$dir = $request->get('order')[0]['dir'];
else
$dir = 'ASC';
$startDate = $request->get('startDate');
$finishDate = $request->get('finishDate');
$limit = $request->get('length');
if ($request->get('start'))
$page = 1 + ($request->get('start') / $limit);
else
$page = 1;
$warehouseid = $request->get('warehouseid');
$result = $this->salesProductsServiceImpl->getAllUnAllocatedProductsPaginate(
$s,
$column,
$dir,
$page,
$limit,
$startDate,
$finishDate,
$warehouseid
);
return $this->json($result);
}
return $this->render('admin/sales/unallocated_products.html.twig', [
'warehouses' => $warehouses
]);
}
/**
* @throws NonUniqueResultException
* @throws NoResultException
*/
#[Route('/sales/un-allocated-products/allocate', name: 'un_allocated_products_allocate')]
public function allocateProduct(Request $request)
{
$content = $request->getContent();
$data = json_decode($content, true);
$id = $data["id"];
$allocatedQuantity = $data["allocatedQuantity"];
$customer = $data["customer"];
$measurement = $data["measurement"];
$measurementUnit = $data["measurementUnit"];
$productCode = $data["productCode"];
$productName = $data["productName"];
$salesDate = $data["salesDate"];
$sellerName = $data["sellerName"];
$stockInfo = $data["stockInfo"];
$totalSold = $data["totalSold"];
$unAllocatedQuantity = $data["unAllocatedQuantity"];
$warehouseName = $data["warehouseName"];
$productSold = $this->salesProductsServiceImpl->getEntityById($id);
$quantityToAllocate = $allocatedQuantity - $productSold->getQuantity();
$stock = $this->stockInfoService->getStockInfoByProductAndWarehouse(
$productSold->getProduct(),
$productSold->getSales()->getWarehouse()
);
if ($quantityToAllocate > $stock->getTotalQuantity()) {
return $this->json([
'message' => sprintf('Stok miktarı yetersiz, depodaki stok miktarı = %s', $stock->getTotalQuantity())
], 400);
}
// dd($quantityToAllocate, CurrencyHelper::convertToFloat($productSold->getUnAllocatedQuantity()));
$unAllocatedQuantityFromProduct = round(CurrencyHelper::convertToFloat($productSold->getUnAllocatedQuantity()), 2);
$quantityToAllocate = round($quantityToAllocate, 2);
if ($quantityToAllocate > $unAllocatedQuantityFromProduct) {
return $this->json([
'message' => sprintf(
'Hatalı bilgi. Tahsis etmek istediğiniz miktar tahsis edilmemiş miktardan büyük olamaz. Tahsis edilmemiş miktar = %s',
$productSold->getUnAllocatedQuantity()
)
], 400);
}
return $this->json(
['message' => $this->salesProductsServiceImpl->allocateQuantity($productSold, $quantityToAllocate)]
);
}
#[Route('/sales/un-allocated-products/detail/ajax', name: 'un_allocated_products_detail_ajax')]
public function getUnAllocatedProduct(Request $request)
{
/**
* @var ProductsSold $entity
*/
$entity = $this->salesProductsServiceImpl->getEntityById($request->get('productSoldId'));
return $this->json([
'id' => $entity->getId(),
'productName' => $entity->getProduct()->getName(),
'productCode' => $entity->getProduct()->getCode(),
'measurement' => $entity->getProduct()->getMeasurement()->getName(),
'measurementUnit' => $entity->getProduct()->getMeasurementUnit()->getSymbol(),
'customer' => $entity->getSales()->getCustomer()->getFullName(),
'allocatedQuantity' => $entity->getQuantity(),
'unAllocatedQuantity' => $entity->getUnAllocatedQuantity(),
'salesDate' => $entity->getSales()->getSalesDate()->format('d-m-Y'),
'warehouseName' => $entity->getSales()->getWarehouse()->getName(),
'stockInfo' => $this->stockInfoService->getStockInfoByProductAndWarehouse(
$entity->getProduct(),
$entity->getSales()->getWarehouse()
)->getTotalQuantity(),
'sellerName' => $entity->getSales()->getSeller()->getFullname(),
'totalSold' => CurrencyHelper::convertToCurrency($entity->getQuantity() + ($entity->getUnAllocatedQuantity() == null ? 0 : $entity->getUnAllocatedQuantity())),
]);
}
#[Route('/proforma/index', name: 'proforma_index')]
public function getAllProformas(Request $request, EntityManagerInterface $entityManager)
{
$warehouses = $this->warehouseService->getAll();
return $this->render('admin/sales/proforma_index.html.twig', [
'controller_name' => 'StockController',
'warehouses' => $warehouses
]);
}
#[Route('/proforma/ajax', name: 'proforma_ajax')]
public function getAllProformasAjax(Request $request, SalesServiceImpl $salesServiceImpl)
{
$user = $this->getUser();
if (isset($request->get('search')['value']))
$s = $request->get('search')['value'];
else
$s = '';
$id = $request->get('id');
if (isset($request->get('order')[0]['column']))
$column = $request->get('order')[0]['column'];
else
$column = 0;
if (isset($request->get('order')[0]['dir']))
$dir = $request->get('order')[0]['dir'];
else
$dir = 'ASC';
$startDate = $request->get('startDate');
$finishDate = $request->get('finishDate');
$limit = $request->get('length');
if ($request->get('start'))
$page = 1 + ($request->get('start') / $limit);
else
$page = 1;
$warehouseid = $request->get('warehouseid');
$stocks = $salesServiceImpl->getAllProformasPaginate($user, $page, $limit, $warehouseid, $column, $dir, $s, $startDate, $finishDate);
return new JsonResponse($this->createArrayForDataTable($stocks));
}
private function createPayments($payments, $customer, $sales)
{
foreach ($payments as $payment) {
$paymentMethod = $this->paymentMethodService->getPaymentMethodById($payment['paymentMethod']);
$paymentObj = $this->paymentService->createPayment($customer, $paymentMethod, $sales, $payment['paymentDate'], $payment['amount'], $payment['status'], $payment['paymentDueDate'], $payment['description']);
$a = 0;
if ($paymentObj->getPaymentStatus() == 2) {
$paymentReminder = $this->paymentReminderService->create($paymentObj, $payment['paymentDate'], $payment['status'], $payment['paymentMethod']);
}
}
}
/**
* @throws StockNotAvaibleException
* @throws StockNotFoundException
*/
private function checkStocks($soldList): array
{
$warehouseId = $soldList[0]['warehouse'];
$quantities = [];
foreach ($soldList as $soldProduct) {
$productId = $soldProduct['productid'];
// Ürün tipine göre stok kontrolünü atla (service & tree)
$product = $this->productService->getEntityId($productId);
if ($product && $this->isStockExemptProductType($product->getProductTypeEnum())) {
continue; // bu ürünlerde stok kontrolü yapılmaz
}
// Birim dönüşümü: selectedUnitId farklı ise temel birime çevirerek topla
$rawQuantity = (float) $soldProduct['quantity'];
$selectedUnitId = $soldProduct['selectedUnitId'] ?? null;
if (!isset($quantities[$productId])) {
$quantities[$productId] = 0.0;
}
if ($selectedUnitId) {
// SalesServiceImpl içindeki dönüştürme mantığını kullan
$converted = $this->salesServiceImpl->getConvertedQuantity($product, $selectedUnitId, $rawQuantity);
$quantities[$productId] += (float) $converted;
} else {
$quantities[$productId] += $rawQuantity;
}
}
foreach ($quantities as $productId => $quantity) {
$this->stockInfoService->checkIsStockAvailable($quantity, $productId, $warehouseId);
}
return $quantities;
}
/**
* @throws Exception
*/
private function applyRoundingRule(float $value): float
{
return round($value, self::MONEY_SCALE, PHP_ROUND_HALF_UP);
}
/**
* @throws Exception
*/
private function checkPaymentAndAmountIsEqual($payments, $soldList)
{
$totalPriceRaw = 0.0;
$totalAmountOfPayments = 0.0;
foreach ($payments as $payment) {
$totalAmountOfPayments += CurrencyHelper::convertToFloat($payment['amount']);
}
foreach ($soldList as $soldProduct) {
$totalPriceRaw += $this->calculateTotalPrice($soldProduct);
}
$totalPrice = $this->applyRoundingRule($totalPriceRaw);
$totalAmountOfPayments = $this->applyRoundingRule($totalAmountOfPayments);
$diff = abs($totalPrice - $totalAmountOfPayments);
if ($diff > 1) {
throw new Exception("Ödemeler ile ürünlerin fiyatları farklı. Toplam Ürün Fiyatı: " . $totalPrice . ", Toplam Ödeme: " . $totalAmountOfPayments . ", Fark: " . $diff, 401);
}
return $totalPrice;
}
/**
* @throws Exception
*/
private function calculateTotalPrice($soldProduct): float
{
$quantity = CurrencyHelper::convertToFloat($soldProduct['quantity']);
$unAllocatedQuantity = CurrencyHelper::convertToFloat($soldProduct['unAllocatedQuantity']);
$tax = CurrencyHelper::convertToFloat($soldProduct['tax']);
$unitPrice = CurrencyHelper::convertToFloat($soldProduct['unitPrice']);
$discount = CurrencyHelper::convertToFloat($soldProduct['discount']);
$totalPurchasePrice = CurrencyHelper::convertToFloat($soldProduct['totalPurchasePrice']);
$totalQuantity = $quantity + $unAllocatedQuantity;
$subtotal = $totalQuantity * $unitPrice;
$discountAmount = $subtotal * ($discount / 100);
$afterDiscount = $subtotal - $discountAmount;
$taxAmount = $afterDiscount * ($tax / 100);
$lineTotal = $afterDiscount + $taxAmount;
$roundedLineTotal = $this->applyRoundingRule($lineTotal);
$roundedFrontend = $this->applyRoundingRule($totalPurchasePrice);
$difference = abs($roundedLineTotal - $roundedFrontend);
$rawDifference = abs($lineTotal - $totalPurchasePrice);
if ($difference > 1) {
throw new Exception("Fiyatlar arasında hesaplamada fark var. Backend: " . $roundedLineTotal . ", Frontend: " . $totalPurchasePrice . ", Fark: " . $difference, 401);
}
$this->logger->info("sales", [
'title' => 'Kullanıcının girdiği ödemeler ile ürün bazlı toplamlar karşılaştırılıyor',
'backend_rounded' => $roundedLineTotal,
'backend_raw' => $lineTotal,
'frontend' => $totalPurchasePrice,
'frontend_rounded' => $roundedFrontend,
'difference' => $difference,
'raw_difference' => $rawDifference,
]);
return $lineTotal;
}
private function roundUpToTwoDecimalsPrecise($number)
{
return round(ceil($number * 1000) / 1000, 2);
}
#[Route('/product-calculate-cost', name: 'product_calculate_cost')]
public function exampleCostCalcolator(Request $request, StockRepository $stockRepository)
{
$repo = $stockRepository->calculateCost(30);
return $this->json(['data' => 'data']);
}
#[Route('/stock-detail/{id}', name: 'app_admin_stock_detail')]
public function stockDetail(Request $request, $id)
{
$stock = $this->stockService->getStockById($id);
return $this->json($stock);
}
/**
* @throws Exception
*/
#[Route('/product/{id}/measurement-units', name: 'get_product_measurement_units', methods: ['GET'])]
public function getAvailableMeasurementUnits(int $id): JsonResponse
{
$product = $this->productService->getEntityId($id);
if (!$product)
throw new Exception(sprintf("Product not found with %s", $id), 404);
$availableUnits = $this->measurementConversionsService->getAvailableUnitsFor($product);
// dd($availableUnits);
return $this->json(
$availableUnits,
JsonResponse::HTTP_OK,
[],
['groups' => 'measurement:read']
);
}
#[Route('/sales/detail/{id}', name: 'app_admin_sales_detail')]
public function edit(Request $request, $id, SalesRepository $salesRepository)
{
$payments = $request->get('payments');
$soldList = $request->get('soldList');
$oldStatus = $this->salesService->getEntityById($id)->getStatus();
$newStatus = $request->get('invoice')['salesStatus'] ?? null;
$sales = $this->salesServiceImpl->getEntityById(3);
if (isset($soldList[0]['warehouse']))
$warehouseId = $soldList[0]['warehouse'];
else
$warehouseId = 0;
$invoice = $request->get('invoice');
// Log the edit operation
$this->logger->info("sales", [
'title' => 'Satış düzenleme işlemi başlatıldı',
'saleId' => $id,
'request' => $request->request->all(),
]);
$this->entityManager->beginTransaction();
try {
$sale = $this->salesService->getEntityById($id);
$form = $this->createForm(SalesFormType::class, $sale);
$soldProducts = new ProductsSold();
$soldProductsFrom = $this->createForm(ProductsSoldType::class, $soldProducts);
$form->handleRequest($request);
$customer = new Customer();
$customerForm = $this->createForm(CustomerType::class, $customer);
$customerForm->handleRequest($request);
if ($customerForm->isSubmitted() && $customerForm->isValid()) {
/**
* @var Customer $customer
*/
$customer = $customerForm->getData();
$this->addFlash('success', 'Müşteri başarıyla eklendi. Listeden seçebilirsiniz.');
$email = $customer->getEmail();
$customer->setEmail(mb_strtolower($email, 'UTF-8'));
$this->customerService->save($customerForm->getData());
$this->entityManager->commit();
$referer = $request->headers->get('referer');
return $this->redirect($referer ?: '/sales/detail/' . $id);
}
$payment = new Payment();
$paymentForm = $this->createForm(PaymentType::class, $payment);
$paymentMethods = $this->paymentMethodService->getPaymentMethods();
$warehouses = $this->warehouseService->getAll();
if ($request->getMethod() == 'POST') {
$sale = $this->salesService->getEntityById($id);
$salesType = $invoice['salesType'] ?? 'PROFORMA'; // Default to PROFORMA if not provided
$groupedTotalQuantitiesByProduct = $salesRepository->getGroupedProductsSoldBySale($sale);
// Stok kontrolü: yalnızca COMPLETED veya PENDING_ITEMS durumunda yapılır
$intendedStatus = $this->resolveSalesStatus($invoice['salesStatus'] ?? null);
if ($this->shouldAffectStock($intendedStatus)) {
$this->checkStocksIsAvailableForEditedSale($soldList, $groupedTotalQuantitiesByProduct);
}
// Ürün bazlı indirim kdv gibi işlemler yapılıp total fiyatların frontend ile aynı olup olmadığı kontrol ediliyor.
foreach ($soldList as $soldProduct) {
$this->logger->info("sales", [
'title' => 'Ürün bazlı indirim kdv hesaplanıyor (düzenleme)...',
'product' => $soldProduct,
]);
$this->assertUnitPriceAllowed($soldProduct);
$this->calculateTotalPrice($soldProduct);
}
// Ödeme planı ile ürünlerin toplam fiyatlarının eşitliği kontrol ediliyor.
$totalPrice = $this->checkPaymentAndAmountIsEqual($payments, $soldList);
$repo = $this->entityManager->getRepository(Sales::class);
$saleQ = $repo->createQueryBuilder('c')
->where('c.invoiceNumber = :invoiceNumber')
->andWhere('c.id != :id')
->setParameters(['invoiceNumber' => $invoice['invoiceNumber'], 'id' => $id])
->getQuery()
->getResult();
if ($saleQ) {
throw new Exception(sprintf("%s fatura numarasına kayıtlı bir satış mevcut.", $invoice['invoiceNumber']));
} else {
$soldProductsArray = new ArrayCollection();
for ($i = 0; $i < count($soldList); $i++) {
$product = $this->productService->getEntityId($soldList[$i]['productid']);
$calculatorDto = $this->stockRepository->calculateCost($soldList[$i]['productid'], $warehouseId);
$builder = new SalesProductsDtoBuilder();
$soldProductsArray->add(
$builder
->setProductName($soldList[$i]['productName'] ?? $product->getName())
->setMeasurement($product->getMeasurement()?->getMeasurement())
->setQuantity($soldList[$i]['quantity'])
->setUnitPriceFob($calculatorDto->getPriceFob())
->setUnitPriceNavlun($calculatorDto->getPriceNavlun())
->setTotalUnitPrice($soldList[$i]['unitPrice'])
->setTotalUnitPurchasePrice($soldList[$i]['totalPurchasePrice'])
->setProductId($product->getId())
->setWarehouseId($warehouseId)
->setTax($soldList[$i]['tax'])
->setTotalSalePrice($soldList[$i]['totalPurchasePrice'])
->setDiscount($soldList[$i]['discount'])
->setUnAllocatedQuantity($soldList[$i]['unAllocatedQuantity'])
->setSelectedUnitId($soldList[$i]['selectedUnitId'] ?? null)
->build()
);
}
$customer = $this->customerService->getCustomerById($invoice['customer']);
$sale->setSalesDate(new \DateTime($invoice['salesDate']));
$sale->setCustomer($customer);
$userRepo = $this->entityManager->getRepository(User::class);
$sale->setSeller($userRepo->find($invoice['seller']));
$sale->setInvoiceNumber($invoice['invoiceNumber']);
$sale->setTotalPurchasePrice($totalPrice);
$sale->setDescription($invoice['description']);
$sale->setShippingNotes($invoice['shippingNotes']);
$sale->setPaymentNotes($invoice['paymentNotes']);
$performedBy = $this->getUser();
$sale = $this->salesService->update(
$sale,
$soldProductsArray,
$intendedStatus,
$performedBy instanceof User ? $performedBy : null
);
$this->createPayments($payments, $customer, $sale);
$this->entityManager->commit();
return new JsonResponse(['success' => true]);
}
}
} catch (Exception $exception) {
$this->entityManager->rollback();
$message = $exception->getMessage();
$this->logger->error("sales", [
'title' => 'Satış düzenleme hatası',
'error' => $message,
'trace' => $exception->getTraceAsString()
]);
return new JsonResponse(['status' => 'error', 'message' => $message], 400);
}
$historyEntries = $sale->getHistoryEntries()->toArray();
usort(
$historyEntries,
static fn(SalesHistory $a, SalesHistory $b) => $b->getPerformedAt()->getTimestamp() <=> $a->getPerformedAt()->getTimestamp()
);
$returns = $this->salesReturnRepository->findBy(['sale' => $sale], ['requestedAt' => 'DESC']);
// Check if returns can be created for this sale
$canCreateReturn = $this->canCreateReturnForSale($sale);
return $this->render('admin/sales/edit.html.twig', [
'sale' => $sale,
'form' => $form->createView(),
'warehouses' => $warehouses,
'productsSoldForm' => $soldProductsFrom->createView(),
'customerForm' => $customerForm->createView(),
'paymentForm' => $paymentForm->createView(),
'paymentMethods' => $paymentMethods,
'historyEntries' => $historyEntries,
'returns' => $returns,
'canCreateReturn' => $canCreateReturn,
'defaultMailContent' => $this->getDefaultMailContent(),
]);
}
public function checkStocksIsAvailableForEditedSale($productToSolds, $soldQuantities)
{
$newProductSoldArr = [];
$warehouse = $productToSolds[0]['warehouse'];
foreach ($productToSolds as $productToSold) {
$productId = $productToSold['productid'];
// Ürün tipine göre stok kontrolünü atla (service & tree)
$product = $this->productService->getEntityId($productId);
if ($product && $this->isStockExemptProductType($product->getProductTypeEnum())) {
continue; // bu ürünlerde stok kontrolü yapılmaz
}
// Birim dönüşümü: selectedUnitId farklı ise temel birime çevirerek topla
$rawQuantity = (float) $productToSold['quantity'];
$selectedUnitId = $productToSold['selectedUnitId'] ?? null;
if (!isset($newProductSoldArr[$productId])) {
$newProductSoldArr[$productId] = 0.0;
}
if ($selectedUnitId) {
// SalesServiceImpl içindeki dönüştürme mantığını kullan
$converted = $this->salesServiceImpl->getConvertedQuantity($product, $selectedUnitId, $rawQuantity);
$newProductSoldArr[$productId] += (float) $converted;
} else {
$newProductSoldArr[$productId] += $rawQuantity;
}
}
foreach ($newProductSoldArr as $productId => $requiredQuantity) {
$availableStock = $this->stockInfoService->getStockInfoByProductAndWarehouse(
$this->productService->getEntityId($productId),
$this->warehouseService->getEntityById($warehouse)
);
$previouslySoldQuantity = $this->findStockInArray($soldQuantities, $productId);
$totalAvailableStock = $previouslySoldQuantity + $availableStock->getTotalQuantity();
if ($requiredQuantity > $totalAvailableStock) {
throw new Exception(sprintf(
"Yetersiz stok. %s id'li ürün için toplam kullanılabilir stok miktarı = %s, talep edilen = %s",
$productId,
$totalAvailableStock,
$requiredQuantity
));
}
}
return true;
}
public function findStockInArray($arr, $productId)
{
foreach ($arr as $item) {
if ($item['productId'] == $productId) {
return $item['totalQuantity'];
}
}
return 0;
}
private function normalizeProductTypeName($type): ?string
{
if ($type instanceof \UnitEnum) {
return $type->name;
}
if (is_array($type)) {
return $type['name'] ?? ($type['key'] ?? null);
}
if (is_string($type)) {
return strtoupper($type);
}
return null;
}
private function isStockExemptProductType($type): bool
{
$normalized = $this->normalizeProductTypeName($type);
return $normalized ? in_array($normalized, self::STOCK_EXEMPT_PRODUCT_TYPES, true) : false;
}
private function canProductTypeUseNegativePrice($type): bool
{
$normalized = $this->normalizeProductTypeName($type);
return $normalized ? in_array($normalized, self::NEGATIVE_PRICE_ALLOWED_PRODUCT_TYPES, true) : false;
}
private function assertUnitPriceAllowed(array $soldProduct): void
{
if (empty($soldProduct['productid']) || !array_key_exists('unitPrice', $soldProduct)) {
return;
}
$product = $this->productService->getEntityId($soldProduct['productid']);
if (!$product) {
return;
}
$unitPrice = CurrencyHelper::convertToFloat($soldProduct['unitPrice']);
$type = $product->getProductTypeEnum();
if ($this->canProductTypeUseNegativePrice($type)) {
if ($unitPrice >= 0) {
throw new Exception('Avoir ürünlerinde birim fiyat negatif olmalıdır.');
}
return;
}
}
private function resolveSalesStatus(?string $status): SalesStatus
{
if ($status === null) {
return SalesStatus::PENDING;
}
return SalesStatus::tryFrom($status) ?? SalesStatus::PENDING;
}
private function shouldAffectStock(SalesStatus $status): bool
{
return in_array($status, [SalesStatus::COMPLETED, SalesStatus::PENDING_ITEMS], true);
}
/**
* Check if returns can be created for this sale
* Returns can only be created for:
* 1. Sales with COMPLETED or PENDING_ITEMS status
* 2. Sales that have at least one physical product (excluding services, trees)
*/
private function canCreateReturnForSale(Sales $sale): bool
{
// Check sale status
$saleStatus = $sale->getStatus();
if (!$saleStatus || !in_array($saleStatus, [SalesStatus::COMPLETED, SalesStatus::PENDING_ITEMS], true)) {
return false;
}
// Check if there are any physical products
$productsSold = $sale->getProductsSolds();
foreach ($productsSold as $productSold) {
if (!$productSold instanceof ProductsSold) {
continue;
}
$product = $productSold->getProduct();
if (!$product) {
continue;
}
$type = $product->getProductTypeEnum();
if (!$this->isStockExemptProductType($type)) {
return true; // Found at least one physical product
}
}
return false; // No physical products found
}
#[Route('/delete-sales/{id}', name: 'delete-sales')]
public function delete(Request $request, $id, EntityManagerInterface $entityManager)
{
$sale = $entityManager->getRepository(Sales::class)->find($id);
if (!$sale) {
throw $this->createNotFoundException('Satış bulunamadı.');
}
$performedBy = $this->getUser();
$sale->setVisible(false);
$this->salesHistoryService->record(
$sale,
SalesHistoryEventType::VISIBILITY_CHANGED,
'Satış gizlendi (soft delete).',
['visible' => false],
$performedBy instanceof User ? $performedBy : null
);
$entityManager->persist($sale);
$entityManager->flush();
$this->addFlash('success', 'Satış Başarıyla silindi. Stoklar eklendi');
return $this->redirectToRoute('app_admin_sales');
/*
$sale = $entityManager->getRepository(Sales::class)->find($id);
$entityManager->beginTransaction();
try{
if($sale->getInvoice() != null){
$this->invoiceService->deleteInvoice($sale->getInvoice());
}
if($sale->getStatus() === SalesStatus::PENDING_ITEMS || $sale->getStatus() === SalesStatus::COMPLETED){
$warehouse = $sale->getWarehouse();
foreach ($sale->getProductsSolds() as $salesProductsDto) {
$productEntity = $this->productService->getEntityId($salesProductsDto->getProduct()->getId());
$beforeQuantity = $this->stockInfoService->getStockInfoByProductAndWarehouse($productEntity, $warehouse)->getTotalQuantity();
$stockTransaction = new StockTransaction();
$stockTransaction->setWarehouse($warehouse);
$stockTransaction->setProductId($productEntity);
$stockTransaction->setName($productEntity->getName() . " Code " . $productEntity->getCode());
$stockTransaction->setQuantity($salesProductsDto->getQuantity());
$stockTransaction->setAction("IN");
$stockTransaction->setDescription("Satış siliyor bu yüzden ürün stoğu EKLENİYOR");
$stockTransaction->setBeforeQuantity(
$beforeQuantity
);
$this->stockInfoService->addStock( $productEntity, $warehouse,$salesProductsDto->getQuantity());
$stockTransaction->setAfterQuantity(
$this->stockInfoService->getStockInfoByProductAndWarehouse($productEntity, $warehouse)->getTotalQuantity()
);
$stockTransaction->setSale($sale);
$this->entityManager->remove($salesProductsDto);
$this->entityManager->persist($stockTransaction);
}
}else{
$products = $sale->getProductsSolds();
foreach ($products as $product){
$this->entityManager->remove($product);
}
}
$payments = $sale->getPayments();
foreach ($payments as $payment){
$this->paymentService->deletePayment($payment);
}
$entityManager->remove($sale);
$entityManager->flush();
$entityManager->commit();
$this->addFlash('success', 'Satış Başarıyla silindi. Stoklar eklendi');
return $this->redirectToRoute('app_admin_sales');
}catch (\Exception $exception){
$entityManager->rollback();
throw $exception;
}
*/
}
#[Route('/warehouse-stocks/{warehouseId}', name: 'app_admin_warehouse_stock_information')]
public function getStockByWarehouse(Request $request, $warehouseId)
{
$stocks = $this->stockService->getStocksByWarehouse($warehouseId);
return $this->json($stocks);
}
// Tüm satışlar anasayfa
#[Route('/sales/all', name: 'app_admin_sales_all')]
public function showAll(Request $request, EntityManagerInterface $entityManager)
{
$warehouses = $this->warehouseService->getAll();
return $this->render('admin/sales/allsales.html.twig', [
'controller_name' => 'StockController',
'warehouses' => $warehouses,
'statuses' => SalesStatus::cases(),
'defaultMailContent' => $this->getDefaultMailContent(),
]);
}
// Tüm satışlar ajax ile dataların gönderiliyor
#[Route('/admin/sales/show', name: 'app_admin_sales_show')]
public function show(Request $request, EntityManagerInterface $entityManager)
{
if (isset($request->get('search')['value']))
$s = $request->get('search')['value'];
else
$s = '';
$id = $request->get('id');
if (isset($request->get('order')[0]['column']))
$column = $request->get('order')[0]['column'];
else
$column = 0;
if (isset($request->get('order')[0]['dir']))
$dir = $request->get('order')[0]['dir'];
else
$dir = 'ASC';
$startDate = $request->get('startDate');
$finishDate = $request->get('finishDate');
$limit = $request->get('length');
if ($request->get('start'))
$page = 1 + ($request->get('start') / $limit);
else
$page = 1;
$warehouseid = $request->get('warehouseid');
$statusParam = $request->get('status');
$status = null;
if (!empty($statusParam)) {
try {
$status = SalesStatus::from($statusParam);
} catch (\ValueError $exception) {
$status = null;
}
}
$stocks = $this->salesService->getAllSalesPaginate($page, $limit, $warehouseid, $column, $dir, $s, $startDate, $finishDate, $status, $this->getUser());
return new JsonResponse($this->createArrayForDataTable($stocks));
}
private function getPdfContentFromProforma($proformaId)
{
$logoPath = $this->projectDir . '/public/images/decopierrelogo.png';
$logoDataUri = '';
if (file_exists($logoPath)) {
$logoBase64 = base64_encode(file_get_contents($logoPath));
$logoDataUri = 'data:' . mime_content_type($logoPath) . ';base64,' . $logoBase64;
}
$sales = $this->salesServiceImpl->getEntityById($proformaId);
if (!$sales) {
return null; // Fatura bulunamazsa null döndür
}
$companyData = [
'name' => 'DECO PIERRE ET NATURE',
'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
'siren' => '948485271',
'tva' => 'FR13948485271',
'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com',
'bank_details' => [
'titulaire' => 'SAS FAV',
'bank_name' => 'BANQUE POPULAIRE',
'code_banque' => '16807',
'num_compte' => '37402602218',
'iban' => 'FR7616807004133740260221837',
'bic' => 'CCBPFRPPGRE'
]
];
$html = $this->renderView('/admin/sales/proforma.html.twig', [
'proforma' => $sales,
'logo_data_uri' => $logoDataUri,
'company' => $companyData
]);
$footerHtml = $this->renderView('/admin/sales/invoice_footer.html.twig', [
'company' => $companyData
]);
$options = [
'footer-html' => $footerHtml,
'margin-bottom' => 30,
'enable-local-file-access' => true,
'encoding' => 'UTF-8',
];
return [
'html' => $html,
'options' => $options,
];
}
private function getDefaultMailContent(): string
{
return MailTemplateHelper::getDefaultMailContent();
}
private function applyMailPlaceholders(string $mailContent, Sales $sale): string
{
$customer = $sale->getCustomer();
$placeholders = [
'[[client_name]]' => $customer ? (string) $customer->getFullName() : '',
'[[client_email]]' => $customer && $customer->getEmail() ? (string) $customer->getEmail() : '',
'[[client_phone]]' => $customer && $customer->getPhone() ? (string) $customer->getPhone() : '',
];
foreach ($placeholders as $placeholder => $value) {
$mailContent = str_replace($placeholder, $value ?? '', $mailContent);
}
return $mailContent;
}
private function getPdfContentFromInvoice(int $invoiceId): ?array
{
$logoPath = $this->projectDir . '/public/images/decopierrelogo.png';
$logoDataUri = '';
if (file_exists($logoPath)) {
$logoBase64 = base64_encode(file_get_contents($logoPath));
$logoDataUri = 'data:' . mime_content_type($logoPath) . ';base64,' . $logoBase64;
}
$invoice = $this->invoiceService->getEntityById($invoiceId);
if (!$invoice) {
return null;
}
$companyData = [
'name' => 'DECO PIERRE ET NATURE',
'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
'siren' => '948485271',
'tva' => 'FR13948485271',
'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com',
'bank_details' => [
'titulaire' => 'SAS FAV',
'bank_name' => 'BANQUE POPULAIRE',
'code_banque' => '16807',
'num_compte' => '37402602218',
'iban' => 'FR7616807004133740260221837',
'bic' => 'CCBPFRPPGRE'
]
];
$html = $this->renderView('/admin/sales/invoice.html.twig', [
'invoice' => $invoice,
'logo_data_uri' => $logoDataUri,
'company' => $companyData
]);
$footerHtml = $this->renderView('/admin/sales/invoice_footer.html.twig', [
'company' => $companyData
]);
$options = [
'footer-html' => $footerHtml,
'margin-bottom' => 30,
'enable-local-file-access' => true,
'encoding' => 'UTF-8',
];
return [
'html' => $html,
'options' => $options,
];
}
#[Route('/proforma/send-mail/{proformaId}', name: 'proforma_send_mail', methods: ['POST'])]
public function sendMailToCustomer(Request $request, $proformaId, MailService $mailService)
{
$sale = $this->salesService->getEntityById($proformaId);
if (!$sale) {
return new JsonResponse(['message' => 'sale_not_found'], 404);
}
$data = json_decode($request->getContent(), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
$data = $request->request->all();
}
$recipientsRaw = $data['recipients'] ?? [];
if (!is_array($recipientsRaw)) {
$recipientsRaw = explode(',', (string) $recipientsRaw);
}
if (empty($recipientsRaw) && $sale->getCustomer() && $sale->getCustomer()->getEmail()) {
$recipientsRaw = [$sale->getCustomer()->getEmail()];
}
$recipients = [];
foreach ($recipientsRaw as $recipient) {
$recipient = mb_strtolower(trim((string) $recipient));
if ($recipient && filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
$recipients[] = $recipient;
}
}
$recipients = array_values(array_unique($recipients));
if (!$recipients) {
return new JsonResponse(['message' => 'recipient_not_found'], 400);
}
$mailContent = $data['content'] ?? $this->getDefaultMailContent();
if (!$mailContent) {
$mailContent = $this->getDefaultMailContent();
}
$mailContent = $this->applyMailPlaceholders($mailContent, $sale);
$includeProforma = array_key_exists('includeProforma', $data)
? filter_var($data['includeProforma'], FILTER_VALIDATE_BOOLEAN)
: true;
$includeInvoice = array_key_exists('includeInvoice', $data)
? filter_var($data['includeInvoice'], FILTER_VALIDATE_BOOLEAN)
: ($sale->getInvoice() !== null);
$attachments = [];
if ($includeProforma) {
$proformaContent = $this->getPdfContentFromProforma($proformaId);
if ($proformaContent) {
$proformaPath = $this->pdfService->convertAndSave(
$proformaContent['html'],
$sale->getInvoiceNumber(),
'proforma',
$proformaContent['options']
);
if ($proformaPath && file_exists($proformaPath)) {
$attachments[] = $proformaPath;
}
}
}
if ($includeInvoice && $sale->getInvoice()) {
$invoiceContent = $this->getPdfContentFromInvoice($sale->getInvoice()->getId());
if ($invoiceContent) {
$invoicePath = $this->pdfService->convertAndSave(
$invoiceContent['html'],
$sale->getInvoice()->getInvoiceNumber(),
'invoice',
$invoiceContent['options']
);
if ($invoicePath && file_exists($invoicePath)) {
$attachments[] = $invoicePath;
}
}
}
$subject = ($includeInvoice && $sale->getInvoice())
? 'Facture ' . $sale->getInvoice()->getInvoiceNumber()
: 'Devis ' . $sale->getInvoiceNumber();
// dd($mailContent);
$result = $mailService->sendAdvancedMail([
'to' => $recipients,
'subject' => $subject,
'html' => $mailContent,
'bcc' => ['arsiv@decopierrenature.com'],
'replyTo' => 'decopierrefatih@gmail.com',
'attachments' => array_values(array_unique($attachments)),
'sale' => $sale,
'senderUser' => $this->getUser(),
]);
$message = $result ? 'success' : 'error';
return new JsonResponse(['message' => $message]);
}
/**
* @throws Exception
*/
#[Route('/proforma/convert-to-invoice/{salesId}', name: 'proforma_convert_to_invoice')]
public function convertToInvoice(int $salesId): JsonResponse
{
$this->entityManager->beginTransaction();
try {
/** @var Sales|null $sales */
$sales = $this->entityManager->find(Sales::class, $salesId, LockMode::PESSIMISTIC_WRITE);
if (!$sales) {
throw new Exception(sprintf("Proforma not found with %s id", $salesId));
}
if ($sales->getInvoice()) {
$invoiceId = $sales->getInvoice()->getId();
$this->entityManager->commit();
return new JsonResponse(
[
'success' => false,
'message' => 'invoice_already_exists',
'invoiceId' => $invoiceId,
],
Response::HTTP_CONFLICT
);
}
$invoice = $this->invoiceService->create($sales);
$this->entityManager->commit();
return new JsonResponse(
[
'success' => true,
'invoiceId' => $invoice->getId(),
],
Response::HTTP_CREATED
);
} catch (Exception $exception) {
$this->entityManager->rollback();
throw $exception;
}
}
private function createArrayForDataTable($stocks): array
{
$records = [];
$records['recordsTotal'] = $stocks->getTotalItemCount();
$records['recordsFiltered'] = $stocks->getTotalItemCount();
$records["data"] = [];
/**
* @var Sales $entity
*/
foreach ($stocks as $entity) {
$pdfLink = '/admin/sales/proforma/generate-pdf/' . $entity->getId();
$printLink = '/admin/sales/proforma/preview-print/' . $entity->getId();
$showProforma = '/admin/sales/proforma/' . $entity->getId();
$customer = $entity->getCustomer();
$customerName = $customer ? (string) $customer->getFullName() : '';
$customerEmail = $customer && $customer->getEmail() ? mb_strtolower((string) $customer->getEmail(), 'UTF-8') : '';
$customerPhone = $customer && $customer->getPhone() ? (string) $customer->getPhone() : '';
$proformaPreviewUrl = $this->generateUrl('admin_sales_proforma_preview_print', ['salesId' => $entity->getId()]);
$invoicePreviewUrl = $entity->getInvoice() ? $this->generateUrl('admin_sales_invoice_preview_print', ['invoiceId' => $entity->getInvoice()->getId()]) : '';
$sendMailUrl = $this->generateUrl('proforma_send_mail', ['proformaId' => $entity->getId()]);
$sendMailAttributes = sprintf(
'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"',
htmlspecialchars((string) $entity->getId(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars((string) $entity->getId(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($sendMailUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($customerEmail, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($customerName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($customerEmail, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($customerPhone, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($proformaPreviewUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($invoicePreviewUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
$entity->getInvoice() ? '1' : '0'
);
$actions = sprintf(
'<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>
<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>
<a href="%s" title="Print" target="_blank"><i class="fas fa-print fa-lg" style="color: #026309;"></i></a>
<a href="%s" title="Show" target="_blank"><i class="fas fa-eye fa-lg"></i></a>',
$pdfLink,
$sendMailAttributes,
$printLink,
$showProforma
);
$salesStatus = $entity->getStatus();
$statusLabel = $salesStatus?->getLabel() ?? 'Belirtilmedi';
$statusValue = $salesStatus?->value ?? 'UNSPECIFIED';
$statusCell = sprintf(
"<span class='sales-status-badge' data-status='%s'>%s</span>",
htmlspecialchars($statusValue, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
);
$records["data"][] = [
$entity->getId(),
$entity->getInvoiceNumber(),
$customerName,
CurrencyHelper::convertToCurrency($entity->getTotalPurchasePrice()) . ' ' . $this->translator->trans('currency.symbol'),
$entity->getSeller()->getFullname(),
$entity->getInvoice() ?
"<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>" :
"<a target='_blank' data-id='" . $entity->getId() . "' class='btn btn-warning btn-xs convertToInvoiceBtn'>" . $this->translator->trans('convertToInvoice') . "</a>",
$entity->getSalesDate()->format('d-m-Y'),
$statusCell,
$this->getPaymentStatus($entity),
$actions
];
}
return $records;
}
private function getPaymentStatus(Sales $sales)
{
$paymentStatus = '';
foreach ($sales->getPayments() as $payment) {
if ($payment->getPaymentStatus() == 2) {
$paymentStatus = "TERM_SALE-" . $this->translator->trans('payment_status.term_sale');
break;
}
if ($payment->getPaymentStatus() == 1) {
$paymentStatus = "UNPAID-" . $this->translator->trans('payment_status.not_paid');
;
break;
}
$paymentStatus = "PAID-" . $this->translator->trans('payment_status.paid');
;
}
return $paymentStatus;
}
// PROFORMAYI HTML SAYFASI OLARAK GÖSTERİYOR
#[Route('/admin/sales/proforma/{salesId}', name: 'admin_sales_proforma_show')]
public function showInvoice(Request $request, $salesId)
{
$logoPath = $this->projectDir . '/public/images/decopierrelogo.png';
$logoDataUri = '';
if (file_exists($logoPath)) {
$logoBase64 = base64_encode(file_get_contents($logoPath));
$logoDataUri = 'data:' . mime_content_type($logoPath) . ';base64,' . $logoBase64;
}
$sales = $this->salesServiceImpl->getEntityById($salesId);
$html = $this->render('/admin/sales/proforma.html.twig', [
'proforma' => $sales,
'logo_data_uri' => $logoDataUri,
'company' => [
'name' => 'DECO PIERRE & NATURE',
'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
'siren' => '948485271',
'tva' => 'FR13948485271',
'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com'
]
]);
return $html;
}
// PROFORMAYI PDF OLARAK İNDİRİYOR
#[Route('/admin/sales/proforma/generate-pdf/{salesId}', name: 'admin_sales_proforma_generate_pdf')]
public function generatePdf(Request $request, $salesId, Pdf $knpSnappyPdf)
{
$logoPath = $this->projectDir . '/public/images/decopierrelogo.png';
$logoDataUri = '';
if (file_exists($logoPath)) {
$logoBase64 = base64_encode(file_get_contents($logoPath));
$logoDataUri = 'data:' . mime_content_type($logoPath) . ';base64,' . $logoBase64;
}
$invoice = $this->salesServiceImpl->getEntityById($salesId);
// Şirket ve banka bilgilerini içeren bir array oluşturun
$companyData = [
'name' => 'DECO PIERRE ET NATURE',
'address' => '152 ROUTE DE GRENOBLE 69800 SAINT PRIEST, France',
'siren' => '948485271',
'tva' => 'FR13948485271',
'contact' => 'MATHIAS | 0652901882 | decopierreetnature@gmail.com',
'bank_details' => [
'titulaire' => 'SAS FAV',
'bank_name' => 'BANQUE POPULAIRE',
'code_banque' => '16807',
'num_compte' => '37402602218',
'iban' => 'FR7616807004133740260221837',
'bic' => 'CCBPFRPPGRE'
]
];
// Ana fatura içeriği için Twig render'ı
$html = $this->renderView('/admin/sales/proforma.html.twig', [
'proforma' => $invoice,
'logo_data_uri' => $logoDataUri,
'company' => $companyData
]);
// Footer içeriği için Twig render'ı
// NOT: footer.html.twig dosyasına sadece companyData'yı göndermeniz yeterlidir.
$footerHtml = $this->renderView('/admin/sales/invoice_footer.html.twig', [
'company' => $companyData
]);
$options = [
'footer-html' => $footerHtml,
'margin-bottom' => 30,
'enable-local-file-access' => true,
'encoding' => 'UTF-8',
];
return new PdfResponse(
$knpSnappyPdf->getOutputFromHtml($html, $options),
$invoice->getInvoiceNumber() . '.pdf',
'inline'
);
}
// PROFORMAYI PDF OLARAK KAYDEDİYOR DOSYA YOLU DÖNÜYOR
private function createAndSavePdf(int $invoiceId): ?string
{
$content = $this->getPdfContentFromProforma($invoiceId);
return $this->pdfService->convertAndSave($content['html'], $invoiceId, 'proforma', $content['options']);
}
// PROFORMAYI PDF OLARAK AÇAR ANCAK YAZDIRMA SEÇENEĞİ AÇILMAZ
#[Route('/admin/sales/proforma/send-pdf/{salesId}', name: 'admin_sales_proforma_send_pdf')]
public function sendPdf(Request $request, int $salesId): Response
{
$filePath = $this->createAndSavePdf($salesId);
if (!$filePath || !file_exists($filePath)) {
throw $this->createNotFoundException('PDF dosyası oluşturulamadı veya bulunamadı.');
}
$sales = $this->salesServiceImpl->getEntityById($salesId);
$fileName = $sales ? $sales->getInvoiceNumber() . '.pdf' : 'fatura.pdf';
$response = new BinaryFileResponse($filePath);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
$fileName
);
$response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', '0');
return $response;
}
// PROFORMAYI PDF OLARAK AÇAR VE YAZDIRMA SEÇENEĞİ ÇIKAR
#[Route('/admin/sales/proforma/preview-print/{salesId}', name: 'admin_sales_proforma_preview_print')]
public function previewAndPrint(int $salesId): Response
{
return $this->render('/admin/sales/proforma_print_preview.html.twig', [
'salesId' => $salesId,
'pdf_url' => $this->generateUrl('admin_sales_proforma_send_pdf', ['salesId' => $salesId]),
]);
}
public function findColumn($rank)
{
$columns = [
'id',
'invoiceNumber',
'salesCompany',
'totalPurchasePrice',
'seller',
'salesDate'
];
return $columns[$rank];
}
#[Route('/admin/excel')]
public function excelDetail()
{
$spreadsheet = IOFactory::load($this->getParameter('kernel.project_dir') . '/public/yeni.xlsx'); // Here we are able to read from the excel file
$row = $spreadsheet->getActiveSheet()->removeRow(1); // I added this to be able to remove the first file line
$sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true); // here, the read data is turned into an array
dd($sheetData);
}
#[Route('/product-cost-detail/{id}', name: 'product_cost_detail')]
public function getProductCost(Request $request, $id)
{
$warehouseId = $request->get('warehouse');
$product = $this->productService->getEntityId($id);
$warehouse = $this->entityManager->getRepository(Warehouse::class)->findOneBy(['id' => $warehouseId]);
/**
* @var CalculatorProductsCostsDTO $cost
*/
$cost = $this->stockRepository->calculateCost($id, $warehouse);
$stock = $this->stockInfoService->getStockInfoByProductAndWarehouse($product, $warehouse);
$info = [
'cost' => $cost->getAverageCostPrice(),
'stock' => $stock->getTotalQuantity(),
'measurement' => $product->getMeasurementUnit()?->getName(),
];
return $this->json($info);
}
#[Route('/hesapla/kar')]
public function calculateProfitOrDamage(EntityManagerInterface $entityManager)
{
$salesId = 13;
$productId = 16;
$quantity = 8;
$totalPrice = 55.1;
$sale = $this->salesService->getEntityById($salesId);
$product = $this->productService->getEntityId($productId);
$product->getStockTransferProducts();
$warehouse = $sale->getWarehouse();
$stockRepo = $entityManager->getRepository(Stock::class);
$lastStock = $stockRepo->findOneBy(['product' => $product, 'warehouse' => $warehouse], ['createdAt' => 'desc']);
$stockTransfer = $lastStock->getStockTransfer();
dd($stockTransfer);
dd($product->getStockTransferProducts());
// $transferProduct = $this->stockTransferService->getEntityById($sale-)
}
}