feat(Devis/OrderSession): Crée le devis à partir de la session, inclut les PDFs.
```
This commit is contained in:
Serreau Jovann
2026-02-05 09:45:08 +01:00
parent 18c9598c7e
commit e33fc7eb47
4 changed files with 330 additions and 2 deletions

View 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');
}
}

View File

@@ -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;
}
} }

View File

@@ -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;

View File

@@ -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
{ {