feat(src/Service/Customer): Ajoute la génération de PDF pour impayés.

 feat(src/Command): Formatte les dates dans AutoCreatedAvisPaymentCommand.
 feat(templates): Ajoute un lien pour la liste des impayés.
 feat(src/Service/Docuseal): Corrige l'URL du fichier Docuseal.
 feat(src/Controller): Ajoute la génération de la liste des impayés.
📝 chore(translations): Ajoute des traductions pour les statuts de factures.
This commit is contained in:
Serreau Jovann
2025-11-05 15:41:32 +01:00
parent a44fae4ead
commit 5447be0167
9 changed files with 261 additions and 11 deletions

2
.env
View File

@@ -75,5 +75,5 @@ AMAZON_SES_SECRET=BD63dADmgFJJPnjlT9utRDlvcOh8pRH3eOZXsyhNL/F3
CLOUDFLARE_TOKEN=4mqx9d7ynvoeCaXonJA07U19rH8gGhctqp7j2Lch
MAILCOW_KEY=DF0E7E-0FD059-16226F-8ECFF1-E558B3
DEV_URL=https://a8afd3b350a5.ngrok-free.app
DEV_URL=https://086e682e904b.ngrok-free.app
SENTRY_BACKEND=https://dcf4ed12f5844686f088838f26082bf0@o4510197735948288.ingest.de.sentry.io/4510197737979984

View File

@@ -67,8 +67,8 @@ class AutoCreatedAvisPaymentCommand extends Command
unset($content[0]);
$desciption = implode("\n", $content);
$desciption = str_replace("{ndd}",$value->getNdd(),$desciption);
$desciption = str_replace("{date_start}",$expiredAt->format('M Y'),$desciption);
$desciption = str_replace("{date_stop}",$newLimitAt->format('M Y'),$desciption);
$desciption = str_replace("{date_start}",$expiredAt->format('d M Y'),$desciption);
$desciption = str_replace("{date_stop}",$newLimitAt->format('d M Y'),$desciption);
$avisLine = new CustomerAdvertPaymentLine();
$avisLine->setPos(0);
@@ -85,8 +85,8 @@ class AutoCreatedAvisPaymentCommand extends Command
unset($content[0]);
$desciption = implode("\n", $content);
$desciption = str_replace("{ndd}",$value->getNdd(),$desciption);
$desciption = str_replace("{date_start}",$expiredAt->format('M Y'),$desciption);
$desciption = str_replace("{date_stop}",$newLimitAt->format('M Y'),$desciption);
$desciption = str_replace("{date_start}",$expiredAt->format('d M Y'),$desciption);
$desciption = str_replace("{date_stop}",$newLimitAt->format('d M Y'),$desciption);
$avisLine2 = new CustomerAdvertPaymentLine();

View File

@@ -43,6 +43,7 @@ use App\Service\Customer\Billing\{
use App\Service\Customer\{CreateAvisEvent,
CreateCustomerNddEmailEvent,
CreateFactureEvent,
CreateListUnpaidEvent,
CustomerSendPasswordEmail,
DeleteCustomerEvent,
EventSpaceCustomerCreated,
@@ -673,7 +674,12 @@ class CustomerController extends AbstractController
return $this->redirectToRoute('artemis_intranet_customer_view', ['id' => $customer->getId()]);
}
if($request->query->has('extract')) {
if($request->query->get('extract') == "impaye") {
$event = new CreateListUnpaidEvent($customer);
$eventDispatcher->dispatch($event);
}
}
return $this->render('artemis/intranet/customer/edit.twig', [
'form' => $form->createView(),
'formNdd' => $formNdd->createView(),

View File

@@ -15,6 +15,7 @@ use App\Service\Mailer\Mailer;
use App\Service\Pdf\DevisPdf;
use App\Service\Pdf\FacturePdf;
use App\Service\Pdf\PaymentPdf;
use App\Service\Pdf\UnpaidPdf;
use App\Service\Stancer\Client;
use App\Service\Vault\VaultClient;
use Doctrine\ORM\EntityManagerInterface;
@@ -39,6 +40,7 @@ use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
#[AsEventListener(event: SiteconseilAdvertPaymentComplete::class, method: 'onSiteconseilAdvertPaymentComplete')]
#[AsEventListener(event: CustomerAdvertPaymentComplete::class, method: 'onCustomerAdvertPaymentComplete')]
#[AsEventListener(event: EventSpaceCustomerCreated::class, method: 'onEventSpaceCustomerCreated')]
#[AsEventListener(event: CreateListUnpaidEvent::class, method: 'onCreateListUnpaidEvent')]
class BillingEventSusbriber
{
@@ -53,6 +55,20 @@ class BillingEventSusbriber
private readonly KernelInterface $kernel
){
}
public function onCreateListUnpaidEvent(CreateListUnpaidEvent $createListUnpaidEvent)
{
$customer = $createListUnpaidEvent->getCustomer();
$advert = $customer->getCustomerAdvertPayments();
$lists =[];
foreach ($advert as $advertPayment) {
if($advertPayment->getState() == "send_avis" || $advertPayment->getState() =="unpaid") {
$lists[] = $advertPayment;
}
}
$pdf = New UnpaidPdf($lists,$customer,$this->kernel);
$pdf->generate();
$pdf->Output('I');
}
public function onEventSpaceCustomerCreated(EventSpaceCustomerCreated $event): void
{
@@ -178,7 +194,8 @@ class BillingEventSusbriber
$this->entityManager->persist($order);
$this->entityManager->flush();
$this->onCreateFactureEventSend(new CreateFactureEventSend($order));
if($isSend)
$this->onCreateFactureEventSend(new CreateFactureEventSend($order));
}
public function onCreatedAvisEventSend(CreateAvisEventSend $createAvisEventSend)
{

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Service\Customer;
use App\Entity\Customer;
class CreateListUnpaidEvent
{
private Customer $customer;
public function __construct(Customer $customer)
{
$this->customer = $customer;
}
/**
* @return Customer
*/
public function getCustomer(): Customer
{
return $this->customer;
}
}

View File

@@ -91,12 +91,13 @@ class SignClient
unlink($this->kernelInterface->getProjectDir()."/public/tmp-sign/facture.pdf");
file_put_contents($this->kernelInterface->getProjectDir()."/public/tmp-sign/facture.pdf",$content);
$submission = $this->docuseal->createSubmissionFromPdf([
'name' => 'Facture N°' . $customerOrder->getNumOrder(),
'documents' => [
[
'name' => 'facture_'.$customerOrder->getNumOrder(),
'file' => $this->requestStack->getCurrentRequest()."/tmp-sign/facture.pdf",
'file' => (($_ENV['APP_ENV'] == "dev")?$_ENV['DEV_URL']:$this->requestStack->getCurrentRequest()->getSchemeAndHttpHost())."/tmp-sign/facture.pdf",
],
],
'submitters' => [
@@ -109,6 +110,7 @@ class SignClient
]);
$submiter = $this->docuseal->getSubmitter($submission['submitters'][0]['id']);
$path = $submiter['documents'][0]['url'];
unlink($this->kernelInterface->getProjectDir()."/public/tmp-sign/facture.pdf");
return $path;
}

View File

@@ -0,0 +1,197 @@
<?php
namespace App\Service\Pdf;
use App\Entity\Customer;
use App\Entity\CustomerAdvertPayment;
use App\Kernel;
use Fpdf\Fpdf;
use IntlDateFormatter;
if (!defined('EURO_UNPAID')) {
define('EURO_UNPAID', chr(128));
}
class UnpaidPdf extends FPDF
{
private Customer $customer;
/**
* @var CustomerAdvertPayment[] $list
*/
private array $list;
private Kernel $kernel;
private function e(string $string): string
{
// Convert string from UTF-8 (source) to ISO-8859-1 (target)
return mb_convert_encoding($string, 'ISO-8859-1', 'UTF-8');
}
public function __construct(array $list, Customer $customer, Kernel $kernel)
{
$this->list = $list;
$this->customer = $customer;
$this->kernel = $kernel;
parent::__construct('P', 'mm', 'A4');
}
public function Header(): void
{
$this->SetFont('Arial', '', 10);
$this->Image($this->kernel->getProjectDir() . "/public/assets/logo_siteconseil.png", 90, 5, 25);
$formatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
'Europe/Paris',
IntlDateFormatter::GREGORIAN
);
$t = new \DateTimeImmutable();
$dateText = $this->e("Saint-Quentin, " . $formatter->format($t));
$this->Text(15, 85, $dateText);
$this->SetFont('Arial', 'B', 12);
$y = 60;
$customer = $this->customer;
$this->Text(110, $y, $this->e($customer->getRaisonSocial()));
$y += 5;
$this->Text(110, $y, $this->e($customer->getAddress()));
if ($address2 = $customer->getAddress2()) {
$y += 5;
$this->Text(110, $y, $this->e($address2));
}
if ($address3 = $customer->getAddress3()) {
$y += 5;
$this->Text(110, $y, $this->e($address3));
}
$y += 5;
$cityLine = $customer->getZipcode() . " " . $customer->getCity();
$this->Text(110, $y, $this->e($cityLine));
$this->body();
}
private function body(): void
{
$this->SetFont('Arial', 'B', 10);
// Les lignes décoratives sont maintenues
$this->Line(0, 100, 5, 100);
$this->Line(0, 200, 5, 200);
}
private function drawUnpaidInvoicesTable(): void
{
// Largeurs des colonnes (à ajuster selon vos données)
$w = [50, 50, 43, 43];
$header = ['Date', 'N° Avis de paiement', 'Montant HT', 'Montant TTC'];
$lineHeight = 7;
$totalTTC = 0;
// Entête du tableau
$this->SetFillColor(253, 130, 4); // Couleur de fond pour l'entête
$this->SetTextColor(0);
$this->SetDrawColor(128, 128, 128); // Couleur des bordures
$this->SetLineWidth(.3);
$this->SetFont('Arial', 'B', 10);
// Dessiner l'entête
for ($i = 0; $i < count($header); $i++) {
$this->Cell($w[$i], $lineHeight, $this->e($header[$i]), 1, 0, 'C', true);
}
$this->Ln();
// Données du tableau
$this->SetFillColor(231, 166, 100);
$this->SetTextColor(0);
$this->SetFont('Arial', '', 10);
$fill = false; // Alternance de couleurs
// Supposition de la structure des éléments dans $this->list
// Chaque élément de $this->list doit être un tableau/objet
// avec les clés 'date', 'numero', 'montant_ttc', 'a_regler_ttc'
foreach ($this->list as $row) {
$totalHt = 0;
$totalTTCTemp = 0;
foreach ($row->getCustomerAdvertPaymentLines() as $line) {
$totalHt = $line->getPriceHt();
$totalTTCTemp = $line->getPriceHt()*1.20;
}
$this->Cell($w[0], $lineHeight, $this->e($row->getCreateAt()->format('d/m/Y')), 'LR', 0, 'C', $fill);
$this->Cell($w[1], $lineHeight, $this->e($row->getNumAvis()), 'LR', 0, 'C', $fill);
$this->Cell($w[2], $lineHeight, $this->e($totalHt . " " ).EURO_UNPAID, 'LR', 0, 'R', $fill);
$this->Cell($w[3], $lineHeight, $this->e($totalTTCTemp . " ").EURO_UNPAID, 'LR', 0, 'R', $fill);
$this->Ln();
$totalTTC += $totalTTCTemp;
$fill = !$fill; // Alterner la couleur de fond
}
// Ligne de fin du tableau
$this->Cell(array_sum($w), 0, '', 'T');
$this->Ln();
// Ligne du total
$this->SetFont('Arial', 'B', 10);
$totalText = "TOTAL À RÉGLER : ";
$totalFormatted = number_format($totalTTC, 2, ',', ' ');
// Cellule vide pour aligner le total à droite
$this->Cell($w[0] + $w[1] + $w[2], $lineHeight, '', 0, 0, 'R');
// Cellule pour le texte du total
$this->Cell($w[3] - 20, $lineHeight, $this->e($totalText), 0, 0, 'R');
// Cellule pour le montant total
$this->Cell(20, $lineHeight, $this->e($totalFormatted)." ".EURO_UNPAID, 0, 1, 'R');
}
public function generate()
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
$startY = 110;
$this->SetY($startY);
$this->Cell(0, 10, $this->e("Madame, Monsieur,"), 0, 1);
$this->MultiCell(0, 5, $this->e("Sauf erreur ou omission de notre part, après vérification de vos comptes en nos livres, nous avons constaté que vous restez à nous devoir:"), 0, 1);
// --- Insertion du tableau des factures impayées ---
$this->Ln(5);
$this->drawUnpaidInvoicesTable();
$this->Ln(5);
// ---------------------------------------------------
$this->MultiCell(0, 5, $this->e("Nous pensons qu'il s'agit d'un oubli de votre part, et nous vous remercions de bien vouloir nous adresser par tout moyen à votre convenance le règlement correspondant."), 0, 'L');
$this->Ln(2);
$this->MultiCell(0, 5, $this->e("Dans cette attente, nous vous prions de croire en nos salutations distinguées."), 0, 'L');
$this->Ln(10);
$this->MultiCell(0, 5, $this->e("Le Service Comptabilité."), 0, 'R');
}
public function Footer()
{
$this->SetY(-40);
$this->Ln(10);
$this->SetFont('Arial', 'B', 8);
$this->SetTextColor(253, 140, 4);
$this->SetDrawColor(253, 140, 4);
$this->Cell(190, 5, $this->e("Partenaire de vos projects de puis 1997"), 0, 1, 'C');
$this->Line(15, $this->GetY(), 195, $this->GetY());
$this->SetFont('Arial', '', 8);
$this->SetTextColor(0, 0, 0);
$this->Ln(2);
$this->Cell(190, 4, $this->e("27, rue le Sérurier - 02100 SAINT-QUENTIN - Tél: 03 23 05 62 43"), 0, 1, 'C');
$this->Cell(190, 4, $this->e("e-mail : s.com@siteconseil.fr - www.siteconseil.fr"), 0, 1, 'C');
// Correction de la constante ici
$footerText = $this->e("S.A.R.L aux capital de 71400 ") . EURO_UNPAID . $this->e(" - N°SIRET 418 664 058 00025 - N° TVA FR 05 418 664 058 - CODE APE 6201 Z - R.C. St-Quentin 418 664 058");
$this->Cell(190, 4, $footerText, 0, 0, 'C');
}
}

View File

@@ -6,9 +6,12 @@
+ Crée un échéancier de paiement
</a>
{% else %}
<a href="{{ path('artemis_intranet_customer_orderAdd',{id:customer.id}) }}" class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
+ Crée un devis / avis de paiement / facture
</a>
<a href="{{ path('artemis_intranet_customer_orderAdd',{id:customer.id}) }}" class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
+ Crée un devis / avis de paiement / facture
</a>
<a target="_blank" href="{{ path('artemis_intranet_customer_view',{id:customer.id,extract:'impaye',current:'order'}) }}" class="pl-2 px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
Liste des impayée
</a>
{% endif %}
</div>
</div>

View File

@@ -83,3 +83,5 @@ dns_email: Nom de Domaine + Emails
esyWeb_created: Créer en attends de validation
esyWeb_validate: Validation - En Attends de déploiement
vadvert-ndd : Renouvellement nom de domaine
f-created: Facture Crée
f-send: Facture envoyée