```
✨ feat(Devis/OrderSession): Crée le devis à partir de la session, inclut les PDFs.
```
This commit is contained in:
41
migrations/Version20260205084002.php
Normal file
41
migrations/Version20260205084002.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260205084002 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE devis ADD distance DOUBLE PRECISION DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE devis ADD price_ship DOUBLE PRECISION DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE devis ADD payment_method VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE devis ADD order_session_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE devis ADD CONSTRAINT FK_8B27C52BD2E5118A FOREIGN KEY (order_session_id) REFERENCES order_session (id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_8B27C52BD2E5118A ON devis (order_session_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE devis DROP CONSTRAINT FK_8B27C52BD2E5118A');
|
||||||
|
$this->addSql('DROP INDEX UNIQ_8B27C52BD2E5118A');
|
||||||
|
$this->addSql('ALTER TABLE devis DROP distance');
|
||||||
|
$this->addSql('ALTER TABLE devis DROP price_ship');
|
||||||
|
$this->addSql('ALTER TABLE devis DROP payment_method');
|
||||||
|
$this->addSql('ALTER TABLE devis DROP order_session_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,21 +2,36 @@
|
|||||||
|
|
||||||
namespace App\Controller\Dashboard;
|
namespace App\Controller\Dashboard;
|
||||||
|
|
||||||
|
use App\Entity\CustomerAddress;
|
||||||
|
use App\Entity\Devis;
|
||||||
|
use App\Entity\DevisLine;
|
||||||
|
use App\Entity\DevisOptions;
|
||||||
use App\Logger\AppLogger;
|
use App\Logger\AppLogger;
|
||||||
|
use App\Repository\DevisRepository;
|
||||||
|
use App\Repository\OptionsRepository;
|
||||||
use App\Repository\OrderSessionRepository;
|
use App\Repository\OrderSessionRepository;
|
||||||
|
use App\Repository\ProductRepository;
|
||||||
|
use App\Service\Pdf\DevisPdfService;
|
||||||
use Knp\Component\Pager\PaginatorInterface;
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
#[Route('/crm/flow')]
|
#[Route('/crm/flow')]
|
||||||
class FlowController extends AbstractController
|
class FlowController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AppLogger $appLogger,
|
private readonly AppLogger $appLogger,
|
||||||
private readonly OrderSessionRepository $orderSessionRepository,
|
private readonly OrderSessionRepository $orderSessionRepository,
|
||||||
private readonly HttpClientInterface $client
|
private readonly HttpClientInterface $client,
|
||||||
|
private readonly DevisRepository $devisRepository,
|
||||||
|
private readonly ProductRepository $productRepository,
|
||||||
|
private readonly OptionsRepository $optionsRepository,
|
||||||
|
private readonly KernelInterface $kernel
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +94,162 @@ class FlowController extends AbstractController
|
|||||||
#[Route('/allow/{id}', name: 'app_crm_flow_allow', methods: ['GET'])]
|
#[Route('/allow/{id}', name: 'app_crm_flow_allow', methods: ['GET'])]
|
||||||
public function allow(\App\Entity\OrderSession $session, \Doctrine\ORM\EntityManagerInterface $em): Response
|
public function allow(\App\Entity\OrderSession $session, \Doctrine\ORM\EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
|
// 1. Create Devis
|
||||||
|
$devis = new Devis();
|
||||||
|
$devisNumber = "DEVIS-" . sprintf('%05d', $this->devisRepository->count() + 1);
|
||||||
|
$devis->setNum($devisNumber)
|
||||||
|
->setState("wait-send")
|
||||||
|
->setCreateA(new \DateTimeImmutable())
|
||||||
|
->setUpdateAt(new \DateTimeImmutable());
|
||||||
|
|
||||||
|
// 2. Customer
|
||||||
|
$devis->setCustomer($session->getCustomer());
|
||||||
|
|
||||||
|
// 2.1 Set additional Devis fields from OrderSession
|
||||||
|
$devis->setDistance($session->getDeliveryDistance());
|
||||||
|
$devis->setPriceShip($session->getDeliveryPrice());
|
||||||
|
$devis->setPaymentMethod($session->getTypePaiement());
|
||||||
|
$devis->setOrderSession($session);
|
||||||
|
|
||||||
|
// 3. Addresses
|
||||||
|
// Billing Address
|
||||||
|
$billAddr = $this->findOrCreateAddress(
|
||||||
|
$session->getCustomer(),
|
||||||
|
$session->getBillingAddress() ?: $session->getAdressEvent(),
|
||||||
|
$session->getBillingZipCode() ?: $session->getZipCodeEvent(),
|
||||||
|
$session->getBillingTown() ?: $session->getTownEvent(),
|
||||||
|
$em
|
||||||
|
);
|
||||||
|
$devis->setBillAddress($billAddr);
|
||||||
|
|
||||||
|
// Shipping Address
|
||||||
|
$shipAddr = $this->findOrCreateAddress(
|
||||||
|
$session->getCustomer(),
|
||||||
|
$session->getAdressEvent(),
|
||||||
|
$session->getZipCodeEvent(),
|
||||||
|
$session->getTownEvent(),
|
||||||
|
$em,
|
||||||
|
$session->getAdress2Event()
|
||||||
|
);
|
||||||
|
$devis->setAddressShip($shipAddr);
|
||||||
|
|
||||||
|
// 4. Dates
|
||||||
|
$productsData = $session->getProducts();
|
||||||
|
$start = null;
|
||||||
|
$end = null;
|
||||||
|
try {
|
||||||
|
if (!empty($productsData['start'])) {
|
||||||
|
$start = new \DateTimeImmutable($productsData['start']);
|
||||||
|
$devis->setStartAt($start);
|
||||||
|
}
|
||||||
|
if (!empty($productsData['end'])) {
|
||||||
|
$end = new \DateTimeImmutable($productsData['end']);
|
||||||
|
$devis->setEndAt($end);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Ignore date parsing errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Duration
|
||||||
|
$days = 1;
|
||||||
|
if ($start && $end) {
|
||||||
|
$days = max(1, $start->diff($end)->days + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Products & Linked Options
|
||||||
|
$pos = 0;
|
||||||
|
if (isset($productsData['ids']) && is_array($productsData['ids'])) {
|
||||||
|
foreach ($productsData['ids'] as $prodId) {
|
||||||
|
$product = $this->productRepository->find($prodId);
|
||||||
|
if ($product) {
|
||||||
|
$line = new DevisLine();
|
||||||
|
$line->setDevi($devis);
|
||||||
|
$line->setPos($pos++);
|
||||||
|
$line->setProduct($product->getName());
|
||||||
|
$line->setDay($days);
|
||||||
|
$line->setPriceHt($product->getPrice1day());
|
||||||
|
|
||||||
|
if (method_exists($product, 'getPriceSup')) {
|
||||||
|
$line->setPriceHtSup($product->getPriceSup());
|
||||||
|
} else {
|
||||||
|
$line->setPriceHtSup(0);
|
||||||
|
}
|
||||||
|
$em->persist($line);
|
||||||
|
|
||||||
|
// Linked Options
|
||||||
|
if (isset($productsData['options'][$prodId])) {
|
||||||
|
foreach ($productsData['options'][$prodId] as $optId) {
|
||||||
|
$option = $this->optionsRepository->find($optId);
|
||||||
|
if ($option) {
|
||||||
|
$devisOpt = new DevisOptions();
|
||||||
|
$devisOpt->setDevis($devis);
|
||||||
|
$devisOpt->setOption($option->getName());
|
||||||
|
$devisOpt->setPriceHt($option->getPriceHt());
|
||||||
|
$em->persist($devisOpt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Orphan Options
|
||||||
|
$sessionOptions = $session->getOptions();
|
||||||
|
$productIds = $productsData['ids'] ?? [];
|
||||||
|
|
||||||
|
if ($sessionOptions && is_array($sessionOptions)) {
|
||||||
|
foreach ($sessionOptions as $prodId => $opts) {
|
||||||
|
if (!in_array($prodId, $productIds) && is_array($opts)) {
|
||||||
|
foreach ($opts as $optId) {
|
||||||
|
$option = $this->optionsRepository->find($optId);
|
||||||
|
if ($option) {
|
||||||
|
$devisOpt = new DevisOptions();
|
||||||
|
$devisOpt->setDevis($devis);
|
||||||
|
$devisOpt->setOption($option->getName());
|
||||||
|
$devisOpt->setPriceHt($option->getPriceHt());
|
||||||
|
$em->persist($devisOpt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Delivery Fee
|
||||||
|
if ($session->getDeliveryPrice() > 0) {
|
||||||
|
$devisOpt = new DevisOptions();
|
||||||
|
$devisOpt->setDevis($devis);
|
||||||
|
$devisOpt->setOption("Frais de livraison");
|
||||||
|
$dist = number_format($session->getDeliveryDistance(), 1, ',', ' ');
|
||||||
|
$town = $session->getBillingTown() ?: 'Ville inconnue';
|
||||||
|
$devisOpt->setDetails("Livraison ($dist km) - $town");
|
||||||
|
$devisOpt->setPriceHt($session->getDeliveryPrice());
|
||||||
|
$em->persist($devisOpt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Persist & Flush to generate IDs
|
||||||
|
$em->persist($devis);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
// 9. Generate PDFs
|
||||||
|
try {
|
||||||
|
// DocuSeal
|
||||||
|
$docusealService = new DevisPdfService($this->kernel, $devis, $this->productRepository, true);
|
||||||
|
$this->savePdfFile($devis, $docusealService->generate(), 'dc_', 'setDevisDocuSealFile');
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
$devisService = new DevisPdfService($this->kernel, $devis, $this->productRepository, false);
|
||||||
|
$this->savePdfFile($devis, $devisService->generate(), 'devis_', 'setDevisFile');
|
||||||
|
|
||||||
|
$em->flush();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->appLogger->record('ERROR', 'Erreur génération PDF Devis auto: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Update Session
|
||||||
$session->setState('allow');
|
$session->setState('allow');
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->addFlash('success', 'La réservation a été validée.');
|
$this->addFlash('success', 'La réservation a été validée et le devis ' . $devis->getNum() . ' a été créé.');
|
||||||
|
|
||||||
return $this->redirectToRoute('app_crm_flow');
|
return $this->redirectToRoute('app_crm_flow');
|
||||||
}
|
}
|
||||||
@@ -159,4 +326,40 @@ class FlowController extends AbstractController
|
|||||||
// Log error or silent fail
|
// Log error or silent fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function savePdfFile(Devis $devis, string $content, string $prefix, string $setterMethod): void
|
||||||
|
{
|
||||||
|
$tmpPath = sys_get_temp_dir() . '/' . $prefix . uniqid() . '.pdf';
|
||||||
|
file_put_contents($tmpPath, $content);
|
||||||
|
|
||||||
|
$file = new UploadedFile($tmpPath, $prefix . $devis->getNum() . '.pdf', 'application/pdf', null, true);
|
||||||
|
$devis->$setterMethod($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findOrCreateAddress(\App\Entity\Customer $customer, ?string $address, ?string $zip, ?string $city, \Doctrine\ORM\EntityManagerInterface $em, ?string $address2 = null): CustomerAddress
|
||||||
|
{
|
||||||
|
$address = $address ?: 'Adresse inconnue';
|
||||||
|
$zip = $zip ?: '00000';
|
||||||
|
$city = $city ?: 'Ville inconnue';
|
||||||
|
|
||||||
|
foreach ($customer->getCustomerAddresses() as $existingAddr) {
|
||||||
|
if ($existingAddr->getAddress() === $address &&
|
||||||
|
$existingAddr->getZipcode() === $zip &&
|
||||||
|
$existingAddr->getCity() === $city &&
|
||||||
|
$existingAddr->getAddress2() === $address2) {
|
||||||
|
return $existingAddr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$newAddr = new CustomerAddress();
|
||||||
|
$newAddr->setCustomer($customer)
|
||||||
|
->setAddress($address)
|
||||||
|
->setZipcode($zip)
|
||||||
|
->setCity($city)
|
||||||
|
->setAddress2($address2)
|
||||||
|
->setCountry('France');
|
||||||
|
|
||||||
|
$em->persist($newAddr);
|
||||||
|
return $newAddr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ class Devis
|
|||||||
#[ORM\OneToOne(mappedBy: 'devis', cascade: ['persist', 'remove'])]
|
#[ORM\OneToOne(mappedBy: 'devis', cascade: ['persist', 'remove'])]
|
||||||
private ?Contrats $contrats = null;
|
private ?Contrats $contrats = null;
|
||||||
|
|
||||||
|
#[ORM\OneToOne(inversedBy: 'devis', cascade: ['persist', 'remove'])]
|
||||||
|
private ?OrderSession $orderSession = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?float $distance = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?float $priceShip = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $paymentMethod = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?\DateTimeImmutable $startAt = null;
|
private ?\DateTimeImmutable $startAt = null;
|
||||||
@@ -103,6 +114,54 @@ class Devis
|
|||||||
$this->devisOptions = new ArrayCollection();
|
$this->devisOptions = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getOrderSession(): ?OrderSession
|
||||||
|
{
|
||||||
|
return $this->orderSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOrderSession(?OrderSession $orderSession): static
|
||||||
|
{
|
||||||
|
$this->orderSession = $orderSession;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDistance(): ?float
|
||||||
|
{
|
||||||
|
return $this->distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDistance(?float $distance): static
|
||||||
|
{
|
||||||
|
$this->distance = $distance;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriceShip(): ?float
|
||||||
|
{
|
||||||
|
return $this->priceShip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPriceShip(?float $priceShip): static
|
||||||
|
{
|
||||||
|
$this->priceShip = $priceShip;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPaymentMethod(): ?string
|
||||||
|
{
|
||||||
|
return $this->paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPaymentMethod(?string $paymentMethod): static
|
||||||
|
{
|
||||||
|
$this->paymentMethod = $paymentMethod;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function isIsNotAddCaution(): ?bool
|
public function isIsNotAddCaution(): ?bool
|
||||||
{
|
{
|
||||||
return $this->isNotAddCaution;
|
return $this->isNotAddCaution;
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ class OrderSession
|
|||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $typePaiement = null;
|
private ?string $typePaiement = null;
|
||||||
|
|
||||||
|
#[ORM\OneToOne(mappedBy: 'orderSession', cascade: ['persist', 'remove'])]
|
||||||
|
private ?Devis $devis = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->createdAt = new \DateTimeImmutable();
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
@@ -98,6 +101,28 @@ class OrderSession
|
|||||||
$this->state = 'created';
|
$this->state = 'created';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDevis(): ?Devis
|
||||||
|
{
|
||||||
|
return $this->devis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDevis(?Devis $devis): static
|
||||||
|
{
|
||||||
|
// unset the owning side of the relation if necessary
|
||||||
|
if ($devis === null && $this->devis !== null) {
|
||||||
|
$this->devis->setOrderSession(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the owning side of the relation if necessary
|
||||||
|
if ($devis !== null && $devis->getOrderSession() !== $this) {
|
||||||
|
$devis->setOrderSession($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->devis = $devis;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user