✨ feat(devis): Gère l'affichage et les calculs des promotions, cautions et formules
This commit is contained in:
6
.env
6
.env
@@ -83,9 +83,9 @@ STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE
|
||||
STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR
|
||||
STRIPE_WEBHOOKS_SECRET=
|
||||
|
||||
SIGN_URL=https://012d-212-114-31-239.ngrok-free.app
|
||||
STRIPE_BASEURL=https://012d-212-114-31-239.ngrok-free.app
|
||||
CONTRAT_BASEURL=https://012d-212-114-31-239.ngrok-free.app
|
||||
SIGN_URL=https://eefa-82-67-166-187.ngrok-free.app
|
||||
STRIPE_BASEURL=https://eefa-82-67-166-187.ngrok-free.app
|
||||
CONTRAT_BASEURL=https://eefa-82-67-166-187.ngrok-free.app
|
||||
|
||||
MINIO_S3_URL=
|
||||
MINIO_S3_CLIENT_ID=
|
||||
|
||||
@@ -43,7 +43,7 @@ class DevisController extends AbstractController
|
||||
private readonly ProductRepository $productRepository,
|
||||
private readonly CustomerAddressRepository $customerAddressRepository,
|
||||
private readonly CustomerRepository $customerRepository,
|
||||
private readonly PrestaireRepository $prestaireRepository
|
||||
private readonly PrestaireRepository $prestaireRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ class DevisController extends AbstractController
|
||||
return $this->handleResend($resendId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$this->appLogger->record('VIEW', 'Consultation de la liste des devis');
|
||||
|
||||
$pagination = $paginator->paginate(
|
||||
@@ -259,7 +261,7 @@ class DevisController extends AbstractController
|
||||
if (!empty($devisData['bill_address'])) $devis->setBillAddress($this->customerAddressRepository->find($devisData['bill_address']));
|
||||
if (!empty($devisData['ship_address'])) $devis->setAddressShip($this->customerAddressRepository->find($devisData['ship_address']));
|
||||
if (!empty($formData['customer'])) $devis->setCustomer($this->customerRepository->find($formData['customer']));
|
||||
|
||||
|
||||
if (!empty($devisData['paymentMethod'])) $devis->setPaymentMethod($devisData['paymentMethod']);
|
||||
if (!empty($devisData['prestataire'])) $devis->setPrestataire($this->prestaireRepository->find($devisData['prestataire']));
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Service;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class NotifuseClient
|
||||
{
|
||||
@@ -20,8 +21,10 @@ class NotifuseClient
|
||||
public function __construct(
|
||||
HttpClientInterface $httpClient,
|
||||
LoggerInterface $logger,
|
||||
?string $rootEmail = null, // From env
|
||||
?string $rootSecretKey = null // From env
|
||||
#[Autowire('%env(resolve:NOTIFUSE_ROOT_EMAIL)%')]
|
||||
?string $rootEmail = null,
|
||||
#[Autowire('%env(resolve:NOTIFUSE_SECRET_KEY)%')]
|
||||
?string $rootSecretKey = null
|
||||
) {
|
||||
$this->httpClient = $httpClient;
|
||||
$this->logger = $logger;
|
||||
|
||||
@@ -155,14 +155,24 @@ class DevisPdfService extends Fpdf
|
||||
|
||||
$totalCaution = 0;
|
||||
$totalHT = 0;
|
||||
$isFormuleLineAdded = false;
|
||||
|
||||
foreach ($this->devis->getDevisLines() as $line) {
|
||||
$p = $this->productRepository->findOneBy(['name'=>$line->getProduct()]);
|
||||
if($p instanceof Product) {
|
||||
if($p instanceof Product && !$formule) {
|
||||
$totalCaution += $p->getCaution();
|
||||
}
|
||||
|
||||
$price1Day = $line->getPriceHt();
|
||||
$priceSupHT = $line->getPriceHtSup() ?? 0;
|
||||
|
||||
// Si une formule est présente, on force les prix à 0 pour afficher "Inclus"
|
||||
// et on ajoute la ligne de la formule (géré plus bas)
|
||||
if ($formule) {
|
||||
$price1Day = 0;
|
||||
$priceSupHT = 0;
|
||||
}
|
||||
|
||||
$nbDays = $line->getDay();
|
||||
|
||||
$lineTotalHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT);
|
||||
@@ -173,6 +183,35 @@ class DevisPdfService extends Fpdf
|
||||
|
||||
$currentY = $this->GetY();
|
||||
|
||||
// Gestion de l'affichage de la ligne Formule si les prix sont à 0
|
||||
if ($formule && !$isFormuleLineAdded && $price1Day == 0 && $priceSupHT == 0) {
|
||||
$fPrice = $formule->getPrice1j() ?? 0;
|
||||
if ($nbDays >= 2 && $formule->getPrice2j()) $fPrice = $formule->getPrice2j();
|
||||
if ($nbDays >= 5 && $formule->getPrice5j()) $fPrice = $formule->getPrice5j();
|
||||
|
||||
$totalHT += $fPrice;
|
||||
$totalCaution += $formule->getCaution() ?? 0;
|
||||
|
||||
$this->SetXY(10, $currentY+2.5);
|
||||
$this->SetFont('Arial', 'B', 8);
|
||||
$this->Cell(70, 5, $this->clean("Formule : " . $formule->getName()), 0, 0, 'L');
|
||||
|
||||
$this->SetXY(80, $currentY);
|
||||
$this->SetFont('Arial', '', 8);
|
||||
$this->Cell(15, 10, $nbDays, 'LRB', 0, 'C');
|
||||
$this->Cell(25, 10, number_format($fPrice, 2, ',', ' ') . $this->euro(), 'RB', 0, 'R');
|
||||
$this->Cell(25, 10, "-", 'RB', 0, 'R');
|
||||
$this->Cell(15, 10, $tvaLabel, 'RB', 0, 'C');
|
||||
$this->Cell(40, 10, number_format($fPrice, 2, ',', ' ') . $this->euro(), 'RB', 1, 'R');
|
||||
|
||||
$this->Line(10, $currentY, 10, $currentY + 10);
|
||||
$this->Line(10, $currentY + 10, 80, $currentY + 10);
|
||||
|
||||
$isFormuleLineAdded = true;
|
||||
$currentY = $this->GetY(); // Update Y for the next line (the actual product line)
|
||||
if ($this->GetY() > 250) { $this->AddPage(); $currentY = $this->GetY(); }
|
||||
}
|
||||
|
||||
// Ligne Produit
|
||||
$this->SetXY(10, $currentY+2.5);
|
||||
$this->SetFont('Arial', 'B', 8);
|
||||
@@ -199,22 +238,57 @@ class DevisPdfService extends Fpdf
|
||||
$this->SetFillColor(230, 235, 245);
|
||||
$this->Cell(135, 8, $this->clean('Options & Services additionnels'), 1, 0, 'L', true);
|
||||
$this->Cell(15, 8, $this->clean('TVA'), 1, 0, 'C', true);
|
||||
$this->Cell(40, 8, $this->clean('Total HT'), 1, 1, 'R', true);
|
||||
$this->Cell(40, 8, $this->clean('i'), 1, 1, 'R', true);
|
||||
|
||||
$this->SetFont('Arial', '', 9);
|
||||
foreach ($this->devis->getDevisOptions() as $devisOption) {
|
||||
$priceHT = $devisOption->getPriceHt();
|
||||
$optionName = $devisOption->getOption();
|
||||
|
||||
// Si une formule est présente, les options sont incluses (prix 0) sauf les frais de livraison
|
||||
if ($formule) {
|
||||
$isDelivery = stripos($optionName, 'livraison') !== false || stripos($optionName, 'déplacement') !== false;
|
||||
if (!$isDelivery) {
|
||||
$priceHT = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$totalHT += $priceHT;
|
||||
|
||||
$this->Cell(135, 8, $this->clean($devisOption->getOption()." - ".$devisOption->getDetails()), 'LRB', 0, 'L');
|
||||
$this->Cell(135, 8, $this->clean($optionName." - ".$devisOption->getDetails()), 'LRB', 0, 'L');
|
||||
$this->Cell(15, 8, $tvaLabel, 'RB', 0, 'C');
|
||||
$this->Cell(40, 8, number_format($priceHT, 2, ',', ' ') . $this->euro(), 'RB', 1, 'R');
|
||||
$this->Cell(40, 8, ($priceHT == 0 ? "Inclus" : number_format($priceHT, 2, ',', ' ') . $this->euro()), 'RB', 1, 'R');
|
||||
|
||||
if ($this->GetY() > 260) { $this->AddPage(); }
|
||||
}
|
||||
}
|
||||
|
||||
// --- CALCULS FINAUX ---
|
||||
|
||||
// Gestion de la Promotion
|
||||
$promotion = $this->devis->getOrderSession()?->getPromotion();
|
||||
if ($promotion) {
|
||||
$percentage = $promotion['percentage'] ?? 0;
|
||||
$name = $promotion['name'] ?? 'Remise';
|
||||
$discountAmount = $totalHT * ($percentage / 100);
|
||||
|
||||
// Affichage du Total HT Avant Remise
|
||||
$this->SetFont('Arial', '', 9);
|
||||
$this->Cell(130);
|
||||
$this->SetTextColor(100, 100, 100);
|
||||
$this->Cell(30, 8, $this->clean("Total HT Avant Remise"), 0, 0, 'L');
|
||||
$this->Cell(30, 8, number_format($totalHT, 2, ',', ' ') . $this->euro(), 0, 1, 'R');
|
||||
|
||||
// Affichage de la ligne de promotion
|
||||
$this->Cell(130);
|
||||
$this->SetTextColor(37, 99, 235); // Bleu pour la promo
|
||||
$this->Cell(30, 8, $this->clean("Promotion : $name (-$percentage%)"), 0, 0, 'L');
|
||||
$this->Cell(30, 8, "- " . number_format($discountAmount, 2, ',', ' ') . $this->euro(), 0, 1, 'R');
|
||||
$this->SetTextColor(0, 0, 0); // Reset couleur
|
||||
|
||||
$totalHT -= $discountAmount;
|
||||
}
|
||||
|
||||
$amountTVA = $totalHT * $tvaRate;
|
||||
$totalTTC = $totalHT + $amountTVA;
|
||||
|
||||
@@ -222,7 +296,10 @@ class DevisPdfService extends Fpdf
|
||||
$this->Ln(5);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->Cell(130);
|
||||
$this->Cell(30, 8, 'TOTAL HT', 0, 0, 'L');
|
||||
|
||||
$lblTotal = $promotion ? 'TOTAL HT REMISE' : 'TOTAL HT';
|
||||
|
||||
$this->Cell(30, 8, $this->clean($lblTotal), 0, 0, 'L');
|
||||
$this->Cell(30, 8, number_format($totalHT, 2, ',', ' ') . $this->euro(), 0, 1, 'R');
|
||||
|
||||
$this->Cell(130);
|
||||
|
||||
@@ -34,6 +34,10 @@ class StripeExtension extends AbstractExtension
|
||||
{
|
||||
return [
|
||||
new TwigFilter('totalQuoto',[$this,'totalQuoto']),
|
||||
new TwigFilter('discountAmount',[$this,'discountAmount']),
|
||||
new TwigFilter('totalQuotoBeforeDiscount',[$this,'totalQuotoBeforeDiscount']),
|
||||
new TwigFilter('totalQuotoHT',[$this,'totalQuotoHT']),
|
||||
new TwigFilter('totalCaution',[$this,'totalCaution']),
|
||||
new TwigFilter('totalQuotoAccompte', [$this, 'totalQuotoAccompte']),
|
||||
new TwigFilter('totalContrat',[$this,'totalContrat']),
|
||||
new TwigFilter('totalPayContrat',[$this,'totalPayContrat']),
|
||||
@@ -55,51 +59,108 @@ class StripeExtension extends AbstractExtension
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le total HT du devis en tenant compte des tarifs dégressifs (J1 + Jours Sup)
|
||||
*/
|
||||
public function totalQuoto(Devis $devis): float
|
||||
private function calculateDevis(Devis $devis): array
|
||||
{
|
||||
$totalHT = 0;
|
||||
$totalCautions = 0;
|
||||
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
||||
$tvaRate = 0.20; // 20% de TVA
|
||||
$tvaRate = 0.20;
|
||||
|
||||
// 1. Calcul du montant HT des lignes et des cautions
|
||||
$formule = $devis->getFormule();
|
||||
|
||||
$nbDays = 1;
|
||||
if ($devis->getStartAt() && $devis->getEndAt()) {
|
||||
$diff = $devis->getEndAt()->diff($devis->getStartAt());
|
||||
$nbDays = $diff->days + 1;
|
||||
}
|
||||
|
||||
// 1. Products & Cautions
|
||||
foreach ($devis->getDevisLines() as $line) {
|
||||
$price1Day = (float) ($line->getPriceHt() ?? 0);
|
||||
$priceSupHT = (float) ($line->getPriceHtSup() ?? 0);
|
||||
$nbDays = (int) ($line->getDay() ?? 1);
|
||||
|
||||
// Calcul HT de la ligne
|
||||
$lineHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT);
|
||||
$totalHT += $lineHT;
|
||||
|
||||
// Récupération de la caution liée au produit
|
||||
$product = $this->em->getRepository(Product::class)->findOneBy([
|
||||
'name' => $line->getProduct()
|
||||
]);
|
||||
|
||||
if ($product) {
|
||||
$totalCautions += (float) ($product->getCaution() ?? 0);
|
||||
if (!$formule) {
|
||||
$price1Day = (float) ($line->getPriceHt() ?? 0);
|
||||
$priceSupHT = (float) ($line->getPriceHtSup() ?? 0);
|
||||
$lDays = (int) ($line->getDay() ?? 1);
|
||||
$totalHT += $price1Day + (max(0, $lDays - 1) * $priceSupHT);
|
||||
}
|
||||
|
||||
if (!$formule) {
|
||||
$product = $this->em->getRepository(Product::class)->findOneBy(['name' => $line->getProduct()]);
|
||||
if ($product) {
|
||||
$totalCautions += (float) ($product->getCaution() ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ajout des options au montant HT
|
||||
foreach ($devis->getDevisOptions() as $option) {
|
||||
$totalHT += (float) ($option->getPriceHt() ?? 0);
|
||||
// 2. Formule
|
||||
if ($formule) {
|
||||
$fPrice = $formule->getPrice1j() ?? 0;
|
||||
if ($nbDays >= 2 && $formule->getPrice2j()) $fPrice = $formule->getPrice2j();
|
||||
if ($nbDays >= 5 && $formule->getPrice5j()) $fPrice = $formule->getPrice5j();
|
||||
|
||||
$totalHT += $fPrice;
|
||||
$totalCautions += (float) ($formule->getCaution() ?? 0);
|
||||
}
|
||||
|
||||
// 3. Application de la TVA si activée
|
||||
// 3. Options
|
||||
foreach ($devis->getDevisOptions() as $option) {
|
||||
$price = (float) ($option->getPriceHt() ?? 0);
|
||||
$name = $option->getOption();
|
||||
if ($formule) {
|
||||
$isDelivery = stripos($name, 'livraison') !== false || stripos($name, 'déplacement') !== false;
|
||||
if (!$isDelivery) $price = 0;
|
||||
}
|
||||
$totalHT += $price;
|
||||
}
|
||||
|
||||
$totalBeforeDiscount = $totalHT;
|
||||
$discount = 0;
|
||||
|
||||
// 4. Promotion
|
||||
$promotion = $devis->getOrderSession()?->getPromotion();
|
||||
if ($promotion) {
|
||||
$percentage = $promotion['percentage'] ?? 0;
|
||||
$discount = $totalHT * ($percentage / 100);
|
||||
$totalHT -= $discount;
|
||||
}
|
||||
|
||||
// 5. TVA
|
||||
$montantPrestation = $totalHT;
|
||||
if ($tvaEnabled) {
|
||||
$montantPrestation = $totalHT * (1 + $tvaRate);
|
||||
}
|
||||
|
||||
// 4. Total final = Prestation (HT ou TTC) + Cautions
|
||||
$totalFinal = $montantPrestation + $totalCautions;
|
||||
return [
|
||||
'discount' => $discount,
|
||||
'final' => $montantPrestation + $totalCautions,
|
||||
'prestation' => $montantPrestation,
|
||||
'totalBeforeDiscount' => $totalBeforeDiscount,
|
||||
'caution' => $totalCautions
|
||||
];
|
||||
}
|
||||
|
||||
return round($totalFinal, 2);
|
||||
public function totalQuoto(Devis $devis): float
|
||||
{
|
||||
return round($this->calculateDevis($devis)['final'], 2);
|
||||
}
|
||||
|
||||
public function totalQuotoHT(Devis $devis): float
|
||||
{
|
||||
return round($this->calculateDevis($devis)['prestation'], 2);
|
||||
}
|
||||
|
||||
public function discountAmount(Devis $devis): float
|
||||
{
|
||||
return round($this->calculateDevis($devis)['discount'], 2);
|
||||
}
|
||||
|
||||
public function totalQuotoBeforeDiscount(Devis $devis): float
|
||||
{
|
||||
return round($this->calculateDevis($devis)['totalBeforeDiscount'], 2);
|
||||
}
|
||||
|
||||
public function totalCaution(Devis $devis): float
|
||||
{
|
||||
return round($this->calculateDevis($devis)['caution'], 2);
|
||||
}
|
||||
public function totalContratAccompte(Contrats $devis): float
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em]">Date</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em]">Statut</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] text-center">Total HT</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] text-center">Caution</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -92,9 +93,30 @@
|
||||
|
||||
{# MONTANT #}
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class="text-sm font-black text-white">
|
||||
{{ (quote|totalQuoto)|number_format(2, ',', ' ') }}€
|
||||
</span>
|
||||
<div class="flex flex-col items-center">
|
||||
{% if quote.orderSession and quote.orderSession.promotion %}
|
||||
<span class="text-sm font-black text-white">
|
||||
{{ (quote|totalQuotoHT)|number_format(2, ',', ' ') }}€
|
||||
</span>
|
||||
<span class="text-[10px] text-slate-500 line-through font-bold">
|
||||
{{ (quote|totalQuotoBeforeDiscount)|number_format(2, ',', ' ') }}€
|
||||
</span>
|
||||
<span class="text-[9px] text-emerald-400 font-bold uppercase tracking-wider mt-1">
|
||||
Promo -{{ quote.orderSession.promotion.percentage }}%
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-sm font-black text-white">
|
||||
{{ (quote|totalQuotoHT)|number_format(2, ',', ' ') }}€
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{# CAUTION #}
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class="text-xs font-bold text-slate-400">
|
||||
{{ (quote|totalCaution)|number_format(2, ',', ' ') }}€
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{# ACTIONS #}
|
||||
|
||||
@@ -23,9 +23,22 @@
|
||||
<th style="padding: 10px 0;">Référence :</th>
|
||||
<th style="padding: 10px 0; text-align:right;">#{{ datas.devis.num }}</th>
|
||||
</tr>
|
||||
{% if datas.devis.orderSession and datas.devis.orderSession.promotion %}
|
||||
{% set discount = datas.devis|discountAmount %}
|
||||
{% if discount > 0 %}
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #888888;">Total HT Initial :</td>
|
||||
<td style="padding: 10px 0; text-align:right; color: #888888; text-decoration: line-through;">{{ (datas.devis|totalQuotoBeforeDiscount)|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; color: #2563eb;">Promotion : {{ datas.devis.orderSession.promotion.name }} (-{{ datas.devis.orderSession.promotion.percentage }}%)</td>
|
||||
<td style="padding: 10px 0; text-align:right; color: #2563eb;">- {{ discount|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td style="padding: 10px 0;">Montant Total HT :</td>
|
||||
<td style="padding: 10px 0; text-align:right; font-weight:bold;">{{ (datas.devis|totalQuoto)|number_format(2, ',', ' ') }} €</td>
|
||||
<td style="padding: 10px 0;">Montant Total HT{% if datas.devis.orderSession and datas.devis.orderSession.promotion %} Remisé{% endif %} :</td>
|
||||
<td style="padding: 10px 0; text-align:right; font-weight:bold;">{{ (datas.devis|totalQuotoHT)|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px 0; color:#888888; font-size:12px;" colspan="2">
|
||||
|
||||
Reference in New Issue
Block a user