feat(Customer): Ajoute la gestion des échéances de paiement client

Ajoute l'entité CustomerSplit et les services associés pour gérer
les échéances de paiement des clients (PDF, envoi mail, etc.).
This commit is contained in:
Serreau Jovann
2025-10-09 09:18:01 +02:00
parent 57eac4d32a
commit aff07c97e1
40 changed files with 1453 additions and 45 deletions

1
.gitignore vendored
View File

@@ -36,3 +36,4 @@ script/demande/hosts.ini
backup/*.zip
backup/*.sql
discord_bot/node_modules
public/tmp-sign/*.pdf

View File

@@ -99,6 +99,7 @@
- "{{ path }}/var/log" # Specific for log, though var/log might be created by composer later
- "{{ path }}/public/media" # For uploads
- "{{ path }}/public/storage" # For uploads
- "{{ path }}/public/tmp-sign" # For uploads
- name: Exécuter 'composer install' dans le répertoire de l'application
ansible.builtin.command: composer install --no-dev --optimize-autoloader
@@ -253,3 +254,4 @@
- "{{ path }}/var/log"
- "{{ path }}/public/media"
- "{{ path }}/public/storage" # For uploads
- "{{ path }}/public/tmp-sign" # For uploads

View File

@@ -435,3 +435,8 @@ confirm-modal{
margin-bottom: 2rem;
}
}
.ech-created{
color: orange;
font-weight: bolder;
}

View File

@@ -0,0 +1,32 @@
<?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 Version20251008065523 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 customer_advert_payment ADD type_payment VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_advert_payment DROP type_payment');
}
}

View File

@@ -0,0 +1,32 @@
<?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 Version20251008084118 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 customer_order ADD is_lock BOOLEAN DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_order DROP is_lock');
}
}

View File

@@ -0,0 +1,42 @@
<?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 Version20251008124926 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('CREATE TABLE customer_split (id SERIAL NOT NULL, customer_id INT DEFAULT NULL, create_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, num VARCHAR(255) NOT NULL, amount DOUBLE PRECISION NOT NULL, state VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_7FC0EF2A9395C3F3 ON customer_split (customer_id)');
$this->addSql('COMMENT ON COLUMN customer_split.create_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE TABLE customer_split_line (id SERIAL NOT NULL, split_id INT DEFAULT NULL, date_prev TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, amount DOUBLE PRECISION NOT NULL, state VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_B054515F3DDC68C5 ON customer_split_line (split_id)');
$this->addSql('COMMENT ON COLUMN customer_split_line.date_prev IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE customer_split ADD CONSTRAINT FK_7FC0EF2A9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE customer_split_line ADD CONSTRAINT FK_B054515F3DDC68C5 FOREIGN KEY (split_id) REFERENCES customer_split (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_split DROP CONSTRAINT FK_7FC0EF2A9395C3F3');
$this->addSql('ALTER TABLE customer_split_line DROP CONSTRAINT FK_B054515F3DDC68C5');
$this->addSql('DROP TABLE customer_split');
$this->addSql('DROP TABLE customer_split_line');
}
}

View File

@@ -0,0 +1,32 @@
<?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 Version20251008125048 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 customer_split ADD description TEXT NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_split DROP description');
}
}

View File

@@ -0,0 +1,34 @@
<?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 Version20251008133426 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 customer_split ADD submission_id VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_split_line ADD payment_id VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE customer_split_line DROP payment_id');
$this->addSql('ALTER TABLE customer_split DROP submission_id');
}
}

0
public/tmp-sign/.gitignore vendored Normal file
View File

View File

@@ -5,6 +5,8 @@ namespace App\Controller\ApiInterne\Intranet;
use App\Entity\CustomerAdvertPayment;
use App\Repository\CustomerAdvertPaymentRepository;
use App\Service\Echeance\EventEcheanceCreated;
use App\Service\Logger\LoggerService;
use Doctrine\ORM\EntityManagerInterface;
use LuFiipe\InseeSierene\Sirene;
use Stancer\Config;
@@ -12,6 +14,7 @@ use Stancer\Customer;
use Stancer\Payment;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -38,7 +41,7 @@ class CustomerController extends AbstractController
return $this->json([]);
}
#[Route(path: '/api-interne/intranet/customer/payment/',name: 'api-interne-intranet-customer-payment')]
public function customerPayment(EntityManagerInterface $entityManager,Request $request,CustomerAdvertPaymentRepository $customerAdvertPayment): Response
public function customerPayment(LoggerService $loggerService,EventDispatcherInterface $eventDispatcher,EntityManagerInterface $entityManager,Request $request,CustomerAdvertPaymentRepository $customerAdvertPayment): Response
{
if(!$request->query->has('id'))
return $this->json([],Response::HTTP_FORBIDDEN);
@@ -77,6 +80,7 @@ class CustomerController extends AbstractController
$payment->setCapture(true);
$paimentId = $payment->send();
$advert->setPaymentId($paimentId);
$advert->setTypePayment($request->query->has('type')?"sepa":"card");
$entityManager->persist($advert);
$entityManager->flush();
}

View File

@@ -2,10 +2,10 @@
namespace App\Controller\Artemis\Intranet;
use App\Service\Echeance\EventEcheanceCreated;
use App\Service\Pdf\EmailListPdf;
use App\Service\Pdf\PaymentPdf;
use App\Entity\{
Customer,
use App\Entity\{Customer,
CustomerAdvertPayment,
CustomerAdvertPaymentLine,
CustomerContact,
@@ -15,8 +15,9 @@ use App\Entity\{
CustomerDnsEmail,
CustomerOrder,
CustomerOrderLine,
OrderNumberCurrent
};
CustomerSplit,
CustomerSplitLine,
OrderNumberCurrent};
use App\Form\Artemis\Intranet\{
CustomerDnsEmailType,
CustomerEditType,
@@ -258,6 +259,7 @@ class CustomerController extends AbstractController
$entityManager->persist($order);
}
$entityManager->flush();
$event = new CreateFactureEvent($order, false);
@@ -281,6 +283,16 @@ class CustomerController extends AbstractController
return $this->redirectToRoute('artemis_intranet_customer_view', ['id' => $customer->getId(), 'current' => 'order', 'currentOrder' => 'a']);
}
}
if ($request->query->has('idFacture') && $request->query->get('act') == 'send') {
$order = $customerOrderRepository->find($request->get('idFacture'));
$event = new CreateFactureEventSend($order);
$eventDispatcher->dispatch($event);
$order->setState("f-send");
$entityManager->persist($order);
$entityManager->flush();
$this->addFlash("success","Facture envoyée");
return $this->redirectToRoute('artemis_intranet_customer_view', ['id' => $customer->getId(),'current' => 'order', 'currentOrder' => 'f']);
}
if ($request->query->has('idDevis') && $request->query->has('act')) {
$idDevis = $request->query->getInt('idDevis');
@@ -606,6 +618,7 @@ class CustomerController extends AbstractController
$loggerService->log('CREATE', 'Création d\'une boîte mail ' . $nddEmail->getEmail(), $this->getUser());
}
$splitList = $entityManager->getRepository(CustomerSplit::class)->findBy(['customer' => $customer], ['id' => 'ASC']);
$orderDevis = $entityManager->getRepository(CustomerDevis::class)->findBy(['customer' => $customer], ['id' => 'ASC']);
$orderAdvert = $entityManager->getRepository(CustomerAdvertPayment::class)->findBy(['customer' => $customer], ['id' => 'ASC']);
$orderOrder = $entityManager->getRepository(CustomerOrder::class)->findBy(['customer' => $customer], ['id' => 'ASC']);
@@ -618,6 +631,9 @@ class CustomerController extends AbstractController
}
}
$ev = new EventEcheanceCreated($splitList[0]);
$eventDispatcher->dispatch($ev);
return $this->render('artemis/intranet/customer/edit.twig', [
'form' => $form->createView(),
'formNdd' => $formNdd->createView(),
@@ -625,6 +641,7 @@ class CustomerController extends AbstractController
'customer' => $customer,
'ndd' => $customerNdd,
'nddEmails' => $nddEmails,
'splitList' => $paginator->paginate($splitList, $request->query->getInt('page', 1), 10),
'orderDevis' => $paginator->paginate($orderDevis, $request->query->getInt('page', 1), 20),
'orderOrders' => $paginator->paginate($orderOrder, $request->query->getInt('page', 1), 20),
'orderAdverts' => $paginator->paginate($orderAdvert, $request->query->getInt('page', 1), 20),
@@ -696,6 +713,7 @@ class CustomerController extends AbstractController
$devis->setEvent($eventDispatcher);
if ($request->isMethod('POST')) {
$data = $request->request->all();
$data = $request->request->all();
$devis->setCreateAt(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $data['date']));
$entityManager->persist($devis);
@@ -843,5 +861,56 @@ class CustomerController extends AbstractController
'customer' => $customer,
]);
}
#[Route(path: '/artemis/intranet/customer/{id}/splitAdd', name: 'artemis_intranet_customer_splitAdd', methods: ['GET', 'POST'])]
public function customerSplitAdd(
?Customer $customer,
Request $request,
LoggerService $loggerService,
EntityManagerInterface $entityManager,
EventDispatcherInterface $eventDispatcher
): Response {
$num = "ECH-".sprintf('%05d',$entityManager->getRepository(CustomerSplit::class)->count([])+1);
if ($request->isMethod('POST')) {
$request = $request->request->all();
$total = 0;
$ech = new CustomerSplit();
$ech->setCustomer($customer);
$ech->setNum($request['num']);
$ech->setDescription($request['description']);
$ech->setCreateAt(new \DateTimeImmutable());
$ech->setState('created');
$entityManager->persist($ech);
foreach ($request['lines'] ?? [] as $pos => $line) {
$echLine = new CustomerSplitLine();
$echLine->setAmount(floatval($line['price']));
$echLine->setDatePrev(\DateTimeImmutable::createFromFormat('Y-m-d',$line['date']));
$echLine->setState("created");
$entityManager->persist($echLine);
$ech->addCustomerSplitLine($echLine);
$total = $total + floatval($line['price']);
}
$ech->setAmount($total);
$entityManager->persist($ech);
$entityManager->flush();
$event = new EventEcheanceCreated($ech);
$eventDispatcher->dispatch($event);
$this->addFlash("success","Création de l'échéance de paiement");
$loggerService->log("CREATE","Création de l'échéance de paiement - ".$customer->getRaisonSocial(),$this->getUser());
return $this->redirectToRoute('artemis_intranet_customer_view', [
'id' => $customer?->getId(),
'current' => 'order',
'currentOrder' => 'split',
]);
}
return $this->render('artemis/intranet/customer/slit-add.twig', [
'customer' => $customer,
'num' => $num
]);
}
}

View File

@@ -55,7 +55,7 @@ class PaymentController extends AbstractController
]);
if($advert->getState() == "pay") {
if($advert->getState() != "pay") {
$client = Config::init([$_ENV['STANCER_PUBLIC_KEY'], $_ENV['STANCER_PRIVATE_KEY']]);
$client->setMode($_ENV['STANCER_ENV']);
$payEdit = str_replace('"', "", $advert->getPaymentId());
@@ -74,30 +74,41 @@ class PaymentController extends AbstractController
$advert->setPayAt(new \DateTimeImmutable());
$entityManager->persist($advert);
$register = new CustomerAdvertPaymentRegister();
$register->setAmount(floatval($payment->amount/100));
$register->setType("CB");
$register->setAdvert($advert);
$register->setCreateAt(new \DateTimeImmutable());
$entityManager->persist($register);
$num = $advert->getNumAvis();
if(str_contains($num,"DIFF-")) {
$idRef = str_replace("DIFF-", "", $num);
$mainAdvert= $entityManager->getRepository(CustomerAdvertPayment::class)->find($idRef);
/*$register = new CustomerAdvertPaymentRegister();
$register->setAmount(floatval($payment->amount/100));
$register->setType("CB");
$register->setAdvert($mainAdvert);
$register->setCreateAt(new \DateTimeImmutable());
$entityManager->persist($register);*/
$advertSiteconseilPaymentComplete = new SiteconseilAdvertPaymentComplete($advert,$mainAdvert);
$registerSub = new CustomerAdvertPaymentRegister();
$registerSub->setAmount(floatval($payment->amount/100));
$registerSub->setType("CB");
$registerSub->setAdvert($mainAdvert);
$registerSub->setCreateAt(new \DateTimeImmutable());
$mainAdvert->setState("pay");
$entityManager->persist($registerSub);
$entityManager->persist($mainAdvert);
$advertSiteconseilPaymentComplete = new SiteconseilAdvertPaymentComplete($advert,$mainAdvert,$register);
} else {
$advertSiteconseilPaymentComplete = new SiteconseilAdvertPaymentComplete($advert);
$advertSiteconseilPaymentComplete = new SiteconseilAdvertPaymentComplete($advert,null,$register);
}
$entityManager->flush();
$advertCustomerePaymentComplete = new CustomerAdvertPaymentComplete($advert);
$advertCustomerePaymentComplete = new CustomerAdvertPaymentComplete($advert,$register);
$eventDispatcher->dispatch($advertCustomerePaymentComplete);
$eventDispatcher->dispatch($advertSiteconseilPaymentComplete);
return $this->render('admin/payement_complete.twig',[
'advert' => $advert,
]);
}
}
return $this->render('admin/payement_complete.twig',[
return $this->render('admin/payement_no_complete.twig',[
'advert' => $advert,
]);
}

View File

@@ -108,6 +108,12 @@ class Customer
#[ORM\OneToMany(targetEntity: Website::class, mappedBy: 'customer')]
private Collection $websites;
/**
* @var Collection<int, CustomerSplit>
*/
#[ORM\OneToMany(targetEntity: CustomerSplit::class, mappedBy: 'customer')]
private Collection $customerSplits;
public function __clone(): void
{
@@ -130,6 +136,7 @@ class Customer
$this->customerAdvertPayments = new ArrayCollection();
$this->customerOrders = new ArrayCollection();
$this->websites = new ArrayCollection();
$this->customerSplits = new ArrayCollection();
}
public function getId(): ?int
@@ -544,4 +551,34 @@ class Customer
return $this;
}
/**
* @return Collection<int, CustomerSplit>
*/
public function getCustomerSplits(): Collection
{
return $this->customerSplits;
}
public function addCustomerSplit(CustomerSplit $customerSplit): static
{
if (!$this->customerSplits->contains($customerSplit)) {
$this->customerSplits->add($customerSplit);
$customerSplit->setCustomer($this);
}
return $this;
}
public function removeCustomerSplit(CustomerSplit $customerSplit): static
{
if ($this->customerSplits->removeElement($customerSplit)) {
// set the owning side to null (unless already changed)
if ($customerSplit->getCustomer() === $this) {
$customerSplit->setCustomer(null);
}
}
return $this;
}
}

View File

@@ -73,6 +73,9 @@ class CustomerAdvertPayment
*/
#[ORM\OneToMany(targetEntity: CustomerAdvertPaymentRegister::class, mappedBy: 'advert')]
private Collection $customerAdvertPaymentRegisters;
#[ORM\Column(length: 255, nullable: true)]
private ?string $typePayment = null;
public function __construct()
{
$this->customerAdvertPaymentLines = new ArrayCollection();
@@ -376,4 +379,16 @@ class CustomerAdvertPayment
return $this;
}
public function getTypePayment(): ?string
{
return $this->typePayment;
}
public function setTypePayment(?string $typePayment): static
{
$this->typePayment = $typePayment;
return $this;
}
}

View File

@@ -54,6 +54,9 @@ class CustomerOrder
private ?string $factureMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $factureOriginalName = null;
#[ORM\Column(nullable: true)]
private ?bool $isLock = null;
public function __construct()
{
$this->customerOrderLines = new ArrayCollection();
@@ -270,4 +273,16 @@ class CustomerOrder
{
$this->factureSize = $factureSize;
}
public function isLock(): ?bool
{
return $this->isLock;
}
public function setIsLock(?bool $isLock): static
{
$this->isLock = $isLock;
return $this;
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace App\Entity;
use App\Repository\CustomerSplitRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomerSplitRepository::class)]
class CustomerSplit
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'customerSplits')]
private ?Customer $customer = null;
#[ORM\Column]
private ?\DateTimeImmutable $createAt = null;
#[ORM\Column(length: 255)]
private ?string $num = null;
#[ORM\Column]
private ?float $amount = null;
#[ORM\Column(length: 255)]
private ?string $state = null;
/**
* @var Collection<int, CustomerSplitLine>
*/
#[ORM\OneToMany(targetEntity: CustomerSplitLine::class, mappedBy: 'split')]
private Collection $customerSplitLines;
#[ORM\Column(type: Types::TEXT)]
private ?string $description = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $submissionId = null;
public function __construct()
{
$this->customerSplitLines = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getCustomer(): ?Customer
{
return $this->customer;
}
public function setCustomer(?Customer $customer): static
{
$this->customer = $customer;
return $this;
}
public function getCreateAt(): ?\DateTimeImmutable
{
return $this->createAt;
}
public function setCreateAt(\DateTimeImmutable $createAt): static
{
$this->createAt = $createAt;
return $this;
}
public function getNum(): ?string
{
return $this->num;
}
public function setNum(string $num): static
{
$this->num = $num;
return $this;
}
public function getAmount(): ?float
{
return $this->amount;
}
public function setAmount(float $amount): static
{
$this->amount = $amount;
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
/**
* @return Collection<int, CustomerSplitLine>
*/
public function getCustomerSplitLines(): Collection
{
return $this->customerSplitLines;
}
public function addCustomerSplitLine(CustomerSplitLine $customerSplitLine): static
{
if (!$this->customerSplitLines->contains($customerSplitLine)) {
$this->customerSplitLines->add($customerSplitLine);
$customerSplitLine->setSplit($this);
}
return $this;
}
public function removeCustomerSplitLine(CustomerSplitLine $customerSplitLine): static
{
if ($this->customerSplitLines->removeElement($customerSplitLine)) {
// set the owning side to null (unless already changed)
if ($customerSplitLine->getSplit() === $this) {
$customerSplitLine->setSplit(null);
}
}
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
public function getSubmissionId(): ?string
{
return $this->submissionId;
}
public function setSubmissionId(string $submissionId): static
{
$this->submissionId = $submissionId;
return $this;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Entity;
use App\Repository\CustomerSplitLineRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomerSplitLineRepository::class)]
class CustomerSplitLine
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'customerSplitLines')]
private ?CustomerSplit $split = null;
#[ORM\Column]
private ?\DateTimeImmutable $datePrev = null;
#[ORM\Column]
private ?float $amount = null;
#[ORM\Column(length: 255)]
private ?string $state = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $paymentId = null;
public function getId(): ?int
{
return $this->id;
}
public function getSplit(): ?CustomerSplit
{
return $this->split;
}
public function setSplit(?CustomerSplit $split): static
{
$this->split = $split;
return $this;
}
public function getDatePrev(): ?\DateTimeImmutable
{
return $this->datePrev;
}
public function setDatePrev(\DateTimeImmutable $datePrev): static
{
$this->datePrev = $datePrev;
return $this;
}
public function getAmount(): ?float
{
return $this->amount;
}
public function setAmount(float $amount): static
{
$this->amount = $amount;
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
public function getPaymentId(): ?string
{
return $this->paymentId;
}
public function setPaymentId(string $paymentId): static
{
$this->paymentId = $paymentId;
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\CustomerSplitLine;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<CustomerSplitLine>
*/
class CustomerSplitLineRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CustomerSplitLine::class);
}
// /**
// * @return CustomerSplitLine[] Returns an array of CustomerSplitLine objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?CustomerSplitLine
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\CustomerSplit;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<CustomerSplit>
*/
class CustomerSplitRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CustomerSplit::class);
}
// /**
// * @return CustomerSplit[] Returns an array of CustomerSplit objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?CustomerSplit
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -3,10 +3,19 @@
namespace App\Service\Customer\Billing;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerAdvertPaymentRegister;
class CustomerAdvertPaymentComplete
{
public function __construct(private CustomerAdvertPayment $currentAdvertPayment){
public function __construct(private CustomerAdvertPayment $currentAdvertPayment,private CustomerAdvertPaymentRegister $customerAdvertPaymentRegister){
}
/**
* @return CustomerAdvertPaymentRegister
*/
public function getCustomerAdvertPaymentRegister(): CustomerAdvertPaymentRegister
{
return $this->customerAdvertPaymentRegister;
}
/**

View File

@@ -3,12 +3,20 @@
namespace App\Service\Customer\Billing;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerAdvertPaymentRegister;
class SiteconseilAdvertPaymentComplete
{
public function __construct(private CustomerAdvertPayment $currentAdvertPayment,private ?CustomerAdvertPayment $parentAdvert=null){
public function __construct(private CustomerAdvertPayment $currentAdvertPayment,private ?CustomerAdvertPayment $parentAdvert=null,private ?CustomerAdvertPaymentRegister $customerAdvertPaymentRegister = null){
}
/**
* @return CustomerAdvertPaymentRegister|null
*/
public function getCustomerAdvertPaymentRegister(): ?CustomerAdvertPaymentRegister
{
return $this->customerAdvertPaymentRegister;
}
/**
* @return CustomerAdvertPayment
*/

View File

@@ -7,6 +7,7 @@ use App\Entity\CustomerAdvertPaymentLine;
use App\Service\Customer\Billing\CreateAvisEventSend;
use App\Service\Customer\Billing\CreateDevisCustomerEvent;
use App\Service\Customer\Billing\CreateDevisCustomerEventSend;
use App\Service\Customer\Billing\CreateFactureEventSend;
use App\Service\Customer\Billing\CustomerAdvertPaymentComplete;
use App\Service\Customer\Billing\SiteconseilAdvertPaymentComplete;
use App\Service\Docuseal\SignClient;
@@ -34,6 +35,7 @@ use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
#[AsEventListener(event: CreateAvisEvent::class, method: 'onCreatedAvisEvent')]
#[AsEventListener(event: CreateAvisEventSend::class, method: 'onCreatedAvisEventSend')]
#[AsEventListener(event: CreateFactureEvent::class, method: 'onCreateFactureEvent')]
#[AsEventListener(event: CreateFactureEventSend::class, method: 'onCreateFactureEventSend')]
#[AsEventListener(event: SiteconseilAdvertPaymentComplete::class, method: 'onSiteconseilAdvertPaymentComplete')]
#[AsEventListener(event: CustomerAdvertPaymentComplete::class, method: 'onCustomerAdvertPaymentComplete')]
class BillingEventSusbriber
@@ -57,11 +59,28 @@ class BillingEventSusbriber
$currentAdvert = $advertPaymentComplete->getCurrentAdvertPayment();
/** @var CustomerAdvertPayment $parent */
$parent = $advertPaymentComplete->getParentAdvert();
$register = $advertPaymentComplete->getCustomerAdvertPaymentRegister();
$customer = $currentAdvert->getCustomer();
$this->mailer->sendMulti(["jovann@siteconseil.fr","legrand@siteconseil.fr"],"[SARL SITECONSEIL] - Validation d'un paiement par le client","mails/customer/validate_paiement_customer.twig",[
'customer' => $customer,
'advert' => $currentAdvert,
'parent' => $parent,
'register' => $register,
]);
}
public function onCustomerAdvertPaymentComplete(CustomerAdvertPaymentComplete $advertPaymentComplete): void
{
$currentAdvert = $advertPaymentComplete->getCurrentAdvertPayment();
$register = $advertPaymentComplete->getCustomerAdvertPaymentRegister();
$customer = $currentAdvert->getCustomer();
$this->mailer->send($customer->mainContact()->getEmail(),$customer->getRaisonSocial(),"[SARL SITECONSEIL] - Validation de votre paiement","mails/customer/validate_paiement.twig",[
'customer' => $customer,
'advert' => $currentAdvert,
'register' => $register,
]);
}
public function onCustomerSendPasswordEmail(CustomerSendPasswordEmail $customerSendPasswordEmail)
@@ -97,14 +116,55 @@ class BillingEventSusbriber
]);
}
public function onCreateFactureEventSend(CreateFactureEventSend $createFactureEventSend): void
{
$createAvis = $createFactureEventSend->getCustomerOrder();
$contentCgv = file_get_contents($this->kernel->getProjectDir()."/public/cgv.pdf");
$files =[];
$files[] = new DataPart(file_get_contents($this->kernel->getProjectDir()."/public".$this->uploaderHelper->asset($createAvis,"facture")),"FACTURE - ".$createAvis->getNumOrder().".pdf");
$files[] = new DataPart($contentCgv,"Conditions Générale de Vente.pdf");
$amountHt=0;
$amount=0;
foreach ($createAvis->getCustomerOrderLines() as $line) {
$amount = $amount + ($line->getPriceHt()*1.20);
$amountHt = $amountHt + floatval($line->getPriceHt());
}
$this->mailer->send($createAvis->getCustomer()->mainContact()->getEmail(),$createAvis->getCustomer()->getRaisonSocial(),"[SARL SITECONSEIL] - Votre facture n° ".$createAvis->getNumOrder(),"mails/customer/facture-payment.twig",[
'createAvis' => $createAvis,
'paymentNotice' => [
'number' => $createAvis->getNumOrder(),
'amountHt' => $amountHt,
'amount' => $amount,
],
'customer' => $createAvis->getCustomer(),
],$files);
}
public function onCreateFactureEvent(CreateFactureEvent $event)
{
$order = $event->getCustomerOrder();
$isSend = $event->isSend();
$pdf = New FacturePdf($this->kernel,$order);
$pdf = New FacturePdf($this->kernel,$order,true);
$tmpname = Uuid::v4().".pdf";
$dir = sys_get_temp_dir().'/'.$tmpname;
$pdf->generate();
$pdf->Output('I');
$content = $pdf->Output('S');
file_put_contents($dir,$content);
$pathSign = $this->signClient->signOrder($content,$order);
unlink($dir);
file_put_contents($dir,file_get_contents($pathSign));
$upload = new UploadedFile($dir,"avis-".$order->getNumOrder().".pdf","application/pdf",0,true);
$order->setFacture($upload);
$this->entityManager->persist($order);
$this->entityManager->flush();
$this->onCreateFactureEventSend(new CreateFactureEventSend($order));
}
public function onCreatedAvisEventSend(CreateAvisEventSend $createAvisEventSend)
{

View File

@@ -3,8 +3,10 @@
namespace App\Service\Docuseal;
use App\Entity\CustomerDevis;
use App\Entity\CustomerOrder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
@@ -16,7 +18,8 @@ class SignClient
private readonly RequestStack $requestStack,
private readonly UploaderHelper $uploaderHelper,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly EntityManagerInterface $entityManager
private readonly EntityManagerInterface $entityManager,
private readonly KernelInterface $kernelInterface,
) {
$key = $_ENV['DOCUSIGN_KEY'] ?? '';
$url = $_ENV['DOCUSIGN_URL'] ?? '';
@@ -62,4 +65,34 @@ class SignClient
return "https://signature.esy-web.dev/s/" . $submissionData['slug'];
}
public function signOrder(string $content,CustomerOrder $customerOrder)
{
$currentRequest = $this->requestStack->getCurrentRequest();
if(file_exists($this->kernelInterface->getProjectDir()."/public/tmp-sign/facture.pdf"))
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',
'file' => $this->requestStack->getCurrentRequest()."/tmp-sign/facture.pdf",
],
],
'submitters' => [
[
'role' => 'SARL SITECONSEIL',
'completed' => true,
'email' => 's.com@siteconseil.fr'
],
],
]);
$submiter = $this->docuseal->getSubmitter($submission['submitters'][0]['id']);
$path = $submiter['documents'][0]['url'];
return $path;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Service\Echeance;
use App\Service\Docuseal\SignClient;
use App\Service\Pdf\EchPdfDetails;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\KernelInterface;
#[AsEventListener(event: EventEcheanceCreated::class, method: 'onEventEcheanceCreated')]
class EcheanceEventSusbriber
{
public function __construct(
private readonly SignClient $signClient,
private readonly KernelInterface $kernel
){
}
public function onEventEcheanceCreated(EventEcheanceCreated $event) {
$ech = $event->getCustomerSplit();
$pdf = new EchPdfDetails($this->kernel,$ech);
$pdf->generated();
$content = $pdf->Output('I');
dd($content);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Service\Echeance;
use App\Entity\CustomerSplit;
class EventEcheanceCreated
{
public function __construct(private CustomerSplit $customerSplit)
{
}
/**
* @return CustomerSplit
*/
public function getCustomerSplit(): CustomerSplit
{
return $this->customerSplit;
}
}

View File

@@ -38,6 +38,7 @@ class DevisPdf extends FPDF
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',

View File

@@ -0,0 +1,288 @@
<?php
namespace App\Service\Pdf;
use App\Entity\CustomerSplit;
use Fpdf\Fpdf;
use IntlDateFormatter;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Entity\Account; // Import inutile, laissé pour garder la cohérence avec le code fourni
// Define the EURO symbol as it's often missing in standard character sets for FPDF's ISO-8859-1
if (!defined('EURO_ECH')) {
define('EURO_ECH', chr(128));
}
class EchPdfDetails extends FPDF
{
private array $items;
public function __construct(
private readonly KernelInterface $kernel,
private readonly CustomerSplit $customerSplit,
)
{
parent::__construct();
$items = [];
foreach ($this->customerSplit->getCustomerSplitLines() as $line) {
$items[] = [
'dates' => $line->getDatePrev(),
'amount' => $line->getAmount(),
];
}
// Assuming ksort was intended to sort by date, but it sorts by key.
// Given $items is a simple array of indexed arrays, ksort does nothing useful here.
// If sorting by date is needed, a custom sort function should be used.
// For now, I'll keep the original logic but note the intent.
// ksort($items);
$this->items = $items;
$title = mb_convert_encoding("FACILITE DE PAIEMENT N° " . $this->customerSplit->getNum(), "ISO-8859-1", "UTF-8");
$this->SetTitle($title);
}
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
);
// Uniformisation de l'encodage en 'ISO-8859-1'
$numDevisText = mb_convert_encoding("FACILITE DE PAIEMENT N° " . $this->customerSplit->getNum(), 'ISO-8859-1', 'UTF-8');
$dateText = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerSplit->getCreateAt()), 'ISO-8859-1', 'UTF-8');
$this->Text(15, 80, $numDevisText);
$this->Text(15, 85, $dateText);
$this->SetFont('Arial', 'B', 12);
$y = 60;
$customer = $this->customerSplit->getCustomer();
// Uniformisation de l'encodage en 'ISO-8859-1'
$this->Text(110, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
$this->Text(110, $y, mb_convert_encoding($customer->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($address2 = $customer->getAddress2()) {
$y += 5;
$this->Text(110, $y, mb_convert_encoding($address2, 'ISO-8859-1', 'UTF-8'));
}
if ($address3 = $customer->getAddress3()) {
$y += 5;
$this->Text(110, $y, mb_convert_encoding($address3, 'ISO-8859-1', 'UTF-8'));
}
$y += 5;
$cityLine = $customer->getZipcode() . " " . $customer->getCity();
$this->Text(110, $y, mb_convert_encoding($cityLine, 'ISO-8859-1', 'UTF-8'));
// Add a line break after the header to start the content below.
$this->SetY(100);
}
public function generated(): void
{
$this->AddPage(); // Ensure a page is available
$this->SetY(105); // Start position after the header
$this->SetFont('Arial', '', 10);
$this->SetTextColor(0, 0, 0);
// Calculate total amount and number of installments
$totalAmount = 0.0;
$installmentsCount = count($this->items);
foreach ($this->items as $item) {
$totalAmount += $item['amount'];
}
// Format total amount with two decimals
$formattedTotalAmount = number_format($totalAmount, 2, ',', ' ');
// Main text introduction
$introText = "Voici la facilité de paiement qui vous a été accordée :";
$this->Write(5, mb_convert_encoding($introText, 'ISO-8859-1', 'UTF-8'));
$this->Ln(8);
// ----------------------------------------------
// --- CLAUSE : Rétractation ---
// ----------------------------------------------
$this->SetFont('Arial', 'U', 10); // Souligné
$retractTitle = "Droit de Rétractation";
$this->Write(5, mb_convert_encoding($retractTitle, 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Ln(5);
// Correction du problème de mise en gras de l'email
$this->Write(5, mb_convert_encoding("Pour vous rétracter de cette facilité de paiement, vous devez envoyer un mail à l'adresse ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10); // DÉBUT GRAS
$this->Write(5, mb_convert_encoding("s.com@siteconseil.fr", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10); // FIN GRAS
$retractTextEnd = " pour en demander l'annulation, à condition qu'aucun paiement n'ait déjà été effectué.";
$this->Write(5, mb_convert_encoding($retractTextEnd, 'ISO-8859-1', 'UTF-8')); // Utilisation de Write() pour continuer sur la même ligne
$this->Ln(8);
// ----------------------------------------------
// --- CLAUSE : Limitation et Impayé ---
// ----------------------------------------------
$this->SetFont('Arial', 'I', 10); // Italique
$limitationText = "Attention : Une seule facilité de paiement peut être accordée à la fois. Tout impayé entraînera l'impossibilité de demander une nouvelle facilité de paiement.";
$this->MultiCell(180, 5, mb_convert_encoding($limitationText, 'ISO-8859-1', 'UTF-8'), 0, 'L');
$this->Ln(8);
$this->SetFont('Arial', '', 10); // Retour à la police normale
// Summary details - Displayed in parts to allow for bolding
$this->Write(5, mb_convert_encoding("Cette facilité de paiement est répartie sur ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding($installmentsCount . " mensualité(s)", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Write(5, mb_convert_encoding(" pour un montant total de ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding($formattedTotalAmount. " ", 'ISO-8859-1', 'UTF-8').EURO_ECH);
$this->Ln(10);
// Reset font for the rest of the content
$this->SetFont('Arial', '', 10);
// Add installment details table
if (!empty($this->items)) {
$this->SetFont('Arial', 'B', 10);
$this->Cell(50, 7, mb_convert_encoding('Échéance', 'ISO-8859-1', 'UTF-8'), 1, 0, 'C');
$this->Cell(50, 7, mb_convert_encoding('Montant', 'ISO-8859-1', 'UTF-8'), 1, 1, 'C');
$this->SetFont('Arial', '', 10);
$formatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::SHORT,
IntlDateFormatter::NONE,
'Europe/Paris',
IntlDateFormatter::GREGORIAN
);
foreach ($this->items as $item) {
// Assuming 'dates' is a \DateTimeInterface object
$dateText = $formatter->format($item['dates']);
$amountText = number_format($item['amount'], 2, ',', ' ');
$this->Cell(50, 6, mb_convert_encoding($dateText, 'ISO-8859-1', 'UTF-8'), 1, 0, 'C');
$this->Cell(50, 6, mb_convert_encoding($amountText, 'ISO-8859-1', 'UTF-8')." ".EURO_ECH, 1, 1, 'R');
}
$this->Ln(10);
}
// --- SECTION: Default Payment Clause & Services Suspension ---
$this->SetFont('Arial', 'B', 10);
$this->SetTextColor(255, 0, 0); // Red color for emphasis
$defaultTitle = "Conséquences en cas de défaut de paiement :";
$this->Write(5, mb_convert_encoding($defaultTitle, 'ISO-8859-1', 'UTF-8'));
$this->Ln(7);
$this->SetFont('Arial', '', 10);
$this->SetTextColor(0, 0, 0); // Reset to black
// 1. Regularization Clause (7 days) with manual bolding
$this->Write(5, mb_convert_encoding("En cas de défaut de paiement à l'échéance convenue, vous disposerez d'un délai de ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding("7 jours calendaires", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Write(5, mb_convert_encoding(" pour régulariser votre situation. Passé ce délai, la présente facilité de paiement sera ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding("immédiatement rompue", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Write(5, mb_convert_encoding(" et l'intégralité des sommes restant dues deviendra exigible, ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding("sans qu'aucun remboursement ne puisse être accordé", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Write(5, mb_convert_encoding(".", 'ISO-8859-1', 'UTF-8'));
$this->Ln(5);
// 2. Service Suspension Clause and Collection with manual bolding
$this->Write(5, mb_convert_encoding("Tout défaut de paiement entraînera la ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding("suspension immédiate des services associés", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Write(5, mb_convert_encoding(" et la créance pourra être transmise à une ", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 10);
$this->Write(5, mb_convert_encoding("société de recouvrement", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 10);
$this->Write(5, mb_convert_encoding(".", 'ISO-8859-1', 'UTF-8'));
$this->Ln(15);
// ----------------------------------------------
// --- Signature Blocks ---
// ----------------------------------------------
$this->SetFont('Arial', 'B', 10);
// Signature Block for the Client
$this->Cell(80, 5, mb_convert_encoding('Le Client', 'ISO-8859-1', 'UTF-8'), 0, 0, 'C');
// Separator/Spacer
$this->Cell(30, 5, '', 0, 0, 'C');
// Signature Block for SARL SITECONSEIL
$this->Cell(80, 5, mb_convert_encoding('SARL SITECONSEIL', 'ISO-8859-1', 'UTF-8'), 0, 1, 'C'); // 1 = Go to next line
$this->Ln(5); // Vertical space for the signature line and placeholder
// Signature Line for the Client
$this->Cell(80, 0, '', 'T', 0, 'C'); // 'T' for Top border (the line)
// Separator/Spacer
$this->Cell(30, 0, '', 0, 0, 'C');
// Signature Line for SARL SITECONSEIL
$this->Cell(80, 0, '', 'T', 1, 'C'); // 1 = Go to next line
$this->SetY($this->GetY() - 5); // Remonte d'une ligne pour aligner les marqueurs
// Add Signature Placeholder for the Client
$this->SetX(15);
$this->Cell(70, 5, '{{Signature;type=signature;role=Client}}', 0, 0, 'C');
// Separator/Spacer
$this->Cell(30, 5, '', 0, 0, 'C');
$t = new \DateTimeImmutable();
// Add Acceptance Text for SARL SITECONSEIL (Restored previous logic)
$this->Cell(80, 5, mb_convert_encoding("Accepter le ".$t->format('d/m/y H:i'), 'ISO-8859-1', 'UTF-8'), 0, 1, 'C');
}
public function Footer()
{
// This is where FPDF positions the footer content, regardless of the page content length.
// SetY(-30) ensures the pointer is 30mm from the bottom, placing the footer reliably.
$this->SetY(-30);
$this->SetFont('Arial', 'B', 8);
$this->SetTextColor(253, 140,4);
$this->SetDrawColor(253, 140,4);
$this->Cell(190, 5, mb_convert_encoding("Partenaire de vos projects de puis 1997", 'ISO-8859-1', 'UTF-8'), 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, mb_convert_encoding("27, rue le Sérurier - 02100 SAINT-QUENTIN - Tél: 03 23 05 62 43 - Réglement par virement: FR76 1020 6006 7198 7497 7981 061", 'ISO-8859-1', 'UTF-8'), 0, 1,'C');
$this->Cell(190, 4, mb_convert_encoding("e-mail : s.com@siteconseil.fr - www.siteconseil.fr", 'ISO-8859-1', 'UTF-8'), 0, 1,'C');
$this->Cell(190, 4, mb_convert_encoding("S.A.R.L aux captial de 71400 ", 'ISO-8859-1', 'UTF-8').EURO_ECH." - ".mb_convert_encoding("N°SIRET 418 664 058 00025 - N° TVA FR 05 418 664 058 - CODE APE 6201 Z - R.C. St-Quentin 418 664 058", 'ISO-8859-1', 'UTF-8'), 0,0 ,'C');
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Service\Pdf;
use App\Entity\Customer;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerOrder;
use Fpdf\Fpdf;
use IntlDateFormatter;
@@ -15,12 +17,13 @@ class FacturePdf extends Fpdf
public function __construct(
private readonly KernelInterface $kernel,
private readonly CustomerOrder $customerDevis
private readonly CustomerOrder $customerOrder,
private readonly bool $showSign
) {
parent::__construct();
$items = [];
foreach ($this->customerDevis->getCustomerOrderLines() as $line) {
foreach ($this->customerOrder->getCustomerOrderLines() as $line) {
$items[$line->getPo()] = [
'title' => $line->getName(),
'content' => $line->getContent(),
@@ -28,17 +31,18 @@ class FacturePdf extends Fpdf
'priceTTC' => round(1.20 * $line->getPriceHT(), 2),
];
}
ksort($items);
$this->items = $items;
$title = mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), "ISO-8859-1", "UTF-8");
$title = mb_convert_encoding("Facture N° " . $this->customerOrder->getNumOrder(), "ISO-8859-1", "UTF-8");
$this->SetTitle($title);
}
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,
@@ -47,15 +51,16 @@ class FacturePdf extends Fpdf
IntlDateFormatter::GREGORIAN
);
$factureNumber = mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), 'ISO-8859-1', 'UTF-8');
$dateFacture = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerDevis->getCreateAt()), 'ISO-8859-1', 'UTF-8');
$numDevisText = mb_convert_encoding("Facture N° " . $this->customerOrder->getNumOrder(), 'ISO-8859-1', 'UTF-8');
$dateText = mb_convert_encoding("Saint-Quentin, " . $formatter->format($this->customerOrder->getCreateAt()), 'ISO-8859-1', 'UTF-8');
$this->Text(15, 80, $factureNumber);
$this->Text(15, 85, $dateFacture);
$this->Text(15, 80, $numDevisText);
$this->Text(15, 85, $dateText);
$this->SetFont('Arial', 'B', 12);
$y = 60;
$customer = $this->customerDevis->getCustomer();
$customer = $this->customerOrder->getCustomer();
$this->Text(110, $y, mb_convert_encoding($customer->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y += 5;
@@ -72,13 +77,9 @@ class FacturePdf extends Fpdf
}
$y += 5;
$this->Text(110, $y, mb_convert_encoding(
$customer->getZipcode() . " " . $customer->getCity(),
'ISO-8859-1',
'UTF-8'
));
$cityLine = $customer->getZipcode() . " " . $customer->getCity();
$this->Text(110, $y, mb_convert_encoding($cityLine, 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 12);
$this->body();
}
@@ -108,7 +109,7 @@ class FacturePdf extends Fpdf
foreach ($this->items as $item) {
if ($this->GetY() + 30 > $contentBottomLimit) {
$this->AddPage();
$this->body(); // redraw headers
$this->body(); // redraw table headers
$this->SetY($startY);
}
@@ -119,9 +120,10 @@ class FacturePdf extends Fpdf
$this->SetFont('Arial', 'B', 11);
$this->Cell(95, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
// Price HT
$this->SetFont('Arial', 'B', 11);
// Prices
$this->SetFont('Arial', '', 11);
$this->SetXY(142, $currentY);
$this->SetFont('Arial', 'B', 11);
$this->Cell(39, 8, number_format($item['priceHt'], 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->SetFont('Arial', '', 11);
@@ -135,6 +137,22 @@ class FacturePdf extends Fpdf
$this->displaySummary();
}
public function Footer()
{
$this->Ln(10);
$this->SetFont('Arial', 'B', 8);
$this->SetTextColor(253, 140,4);
$this->SetDrawColor(253, 140,4);
$this->Cell(190, 5, mb_convert_encoding("Partenaire de vos projects de puis 1997", 'ISO-8859-1', 'UTF-8'), 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, mb_convert_encoding("27, rue le Sérurier - 02100 SAINT-QUENTIN - Tél: 03 23 05 62 43", 'ISO-8859-1', 'UTF-8'), 0, 1,'C');
$this->Cell(190, 4, mb_convert_encoding("e-mail : s.com@siteconseil.fr - www.siteconseil.fr", 'ISO-8859-1', 'UTF-8'), 0, 1,'C');
$this->Cell(190, 4, mb_convert_encoding("S.A.R.L aux captial de 71400 ", 'ISO-8859-1', 'UTF-8').EURO_FACTURE." - ".mb_convert_encoding("N°SIRET 418 664 058 00025 - N° TVA FR 05 418 664 058 - CODE APE 6201 Z - R.C. St-Quentin 418 664 058", 'ISO-8859-1', 'UTF-8'), 0,0 ,'C');
}
private function displaySummary(): void
{
$totalHT = array_sum(array_column($this->items, 'priceHt'));
@@ -142,6 +160,7 @@ class FacturePdf extends Fpdf
$totalTTC = $totalHT + $totalTVA;
$this->SetY(-60);
$this->SetFont('Arial', '', 10);
$this->SetFont('Arial', '', 12);
$this->Cell(135, 10, mb_convert_encoding('Total HT :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');

View File

@@ -7,6 +7,8 @@ use App\Entity\Customer;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerDevis;
use App\Entity\CustomerDns;
use App\Entity\CustomerOrder;
use App\Entity\CustomerSplit;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -19,8 +21,19 @@ class TwigOrderExtensions extends AbstractExtension
new TwigFilter('totalOrder',[$this,'totalOrder']),
new TwigFilter('countEmail',[$this,'countEmail']),
new TwigFilter('skFormat',[$this,'skFormat']),
new TwigFilter('noCompletedEch',[$this,'noCompletedEch']),
];
}
public function noCompletedEch(CustomerSplit $customerSplit)
{
$nbEch = 0;
foreach ($customerSplit->getCustomerSplitLines() as $customerSplitLine) {
if($customerSplitLine->getState() != "completed")
$nbEch++;
}
return $nbEch;
}
public function getFunctions()
{
return [
@@ -59,6 +72,14 @@ class TwigOrderExtensions extends AbstractExtension
public function totalOrder($object): float|int
{
if($object instanceof CustomerOrder) {
$total = 0;
foreach ($object->getCustomerOrderLines() as $customerDevisLine){
$total = $total + (1.20*$customerDevisLine->getPriceHT());
}
return $total;
}
if($object instanceof CustomerAdvertPayment) {
$total = 0;
foreach ($object->getCustomerAdvertPaymentLines() as $customerDevisLine){

View File

@@ -0,0 +1,20 @@
{% extends 'admin/base.twig' %}
{% block content %}
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8 p-10 rounded-xl shadow-lg bg-gray-700">
<div class="text-center">
<h2 class="mt-6 text-3xl font-extrabold text-white">
Le paiement n'a pas été effectuée
</h2>
<a href="{{ path('app_payment',{id:advert.id}) }}" style="margin-top: 5rem" class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md text-center transition duration-300 ease-in-out">
Retenter le paiement
</a>
</div>
</div>
</div>
{% endblock %}
{% block title %}
Paiement réussi !
{% endblock %}

View File

@@ -47,7 +47,7 @@
transform: rotate(90deg);
}
</style>
<script src="https://signature.esy-web.dev/js/form.js"></script>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">

View File

@@ -1,9 +1,15 @@
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">Factures / Devis / Avis de Paiement</h2>
<div>
{% if currentOrder == "split" %}
<a href="{{ path('artemis_intranet_customer_splitAdd',{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 é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>
{% endif %}
</div>
</div>
@@ -20,6 +26,9 @@
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',currentOrder:'d'}) }}" class="w-33 px-4 py-2 font-semibold {% if currentOrder== "d" %}{{ active }}{% else %}{{ desactive }}{% endif %}">
Devis
</a>
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',currentOrder:'split'}) }}" class="w-33 px-4 py-2 font-semibold {% if currentOrder== "split" %}{{ active }}{% else %}{{ desactive }}{% endif %}">
Facturation en X Fois
</a>
</div>
{% include 'artemis/intranet/customer/order/'~currentOrder~".twig" %}

View File

@@ -12,6 +12,7 @@
</thead>
<tbody>
{% for orderAdvert in orderAdverts %}
{% if "DIFF-" not in orderAdvert.numAvis %}
<tr class="hover:bg-gray-700">
<td class="px-6 py-4">Avis de paiement</td>
<td class="px-6 py-4">{{ orderAdvert.numAvis }}</td>
@@ -22,14 +23,18 @@
{% if orderAdvert.state == "created" %}
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,currentOrder:'a',current:'order',idAvis:orderAdvert.id,act:'send'}) }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Envoyée l'avis de paiement</a>
{% endif %}
<a target="_blank" href="{{ vich_uploader_asset(orderAdvert,'file') }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Téléchager</a>
{% if orderAdvert.state == "pay" and orderAdvert.customerOrder is null %}
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',idAvis:orderAdvert.id,act:'createFacture'}) }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Crée la facture</a>
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',idAvis:orderAdvert.id,act:'createFacture'}) }}" style="margin-top: 1rem" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Crée la facture</a>
{% endif %}
{% if orderAdvert.state == "wait-bank" or orderAdvert.state == "wait-virement" or orderAdvert.state == "p-payment" %}
<button is="register-payment" id="{{ orderAdvert.id }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Enregistrée un paiement</button>
{% if orderAdvert.state == "wait-bank" or orderAdvert.state == "wait-virement" or orderAdvert.state == "p-payment" or orderAdvert.state == "pay" %}
<button is="register-payment" id="{{ orderAdvert.id }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded" style="margin-top: 1rem; width: 100%">Enregistrée un paiement</button>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

View File

@@ -25,7 +25,7 @@
{% if orderOrder.state == "f-send" %}
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',idFacture:orderOrder.id,act:'resend'}) }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Réenvoyée la facture</a>
{% endif %}
<a href="{{ vich_uploader_asset(orderOrder,'facture') }}" download="facture-{{ orderOrder.numOrder }}.pdf" class="block w-full mt-1 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Télécharger la facture</a>
<a target="_blank" href="{{ vich_uploader_asset(orderOrder,'facture') }}" class="block w-full mt-1 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Télécharger la facture</a>
</td>
</tr>

View File

@@ -0,0 +1,29 @@
<div class="overflow-x-auto bg-gray-800 rounded-lg shadow">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">N°</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Montant</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Statut</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Total Échéances</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Restant Échéances</th>
<th class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{% for splitItem in splitList %}
<tr class="hover:bg-gray-700">
<td class="px-6 py-4">{{ splitItem.createAt|date('d/m/Y') }}</td>
<td class="px-6 py-4">{{ splitItem.num }}</td>
<td class="px-6 py-4">{{ splitItem.amount|format_currency('EUR') }} </td>
<td class="px-6 py-4 ech-{{ splitItem.state }}">{{ ('ech_'~splitItem.state)|trans }} </td>
<td class="px-6 py-4">{{ splitItem.customerSplitLines.count }}</td>
<td class="px-6 py-4">{{ splitItem|noCompletedEch() }}</td>
<td class="px-6 py-4"></td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(splitList) }}
</div>

View File

@@ -0,0 +1,82 @@
{% extends 'artemis/base.twig' %}
{% block content %}
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">Client - {{ customer.raisonSocial }} -Création échéancier de paiement</h2>
</div>
<form method="post" class="mt-5 bg-gray-800 rounded-lg shadow-lg p-6 space-y-4">
<div class="flex space-x-4">
<div class="flex-1">
<div class="mb-1">
<label for="num" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Numéro</label>
<input type="text" name="num" id="num_devis" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required value="{{ num }}" />
</div>
</div>
</div>
<div class="mb-5">
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea rows="7" type="text" name="description" id="description" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required></textarea>
</div>
<fieldset class="form-section">
<legend>
<h2>Mensualité</h2>
</legend>
<div class="form-repeater" data-component="repeater" is="repeat-line">
<ol class="form-repeater__rows" data-ref="rows" tabindex="0">
<!-- This element will be repeated: -->
<li class="form-repeater__row">
<fieldset class="form-group form-group--horizontal">
<div class="flex space-x-4">
<div class="flex-1">
<div class="form-field">
<div class="mb-1">
<label for="lines[0][date]" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Date du prélévement</label>
<input type="date" name="lines[0][date]" id="lines[0][date]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
</div>
</div>
<div class="flex-1">
<div class="mb-1">
<label for="price" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Montant TTC</label>
<input type="number" step="0.1" name="lines[0][price]" id="lines[0][price]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
</div>
<div class="flex-1">
<button
style="margin-top: 2rem"
class="w-full form-repeater__remove-button bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
data-ref="removeButton"
type="button"
>
Supprimer la ligne
</button>
</div>
</div>
</fieldset>
</li>
</ol>
<button
class="form-repeater__add-button w-full bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded"
data-ref="addButton"
type="button"
>
+ Ajouter une ligne
</button>
</div>
</fieldset>
<button
class="w-full bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded"
type="submit"
>
Enregistrer
</button>
</form>
{% endblock %}
{% block title %}Intranet - Client - {{ customer.raisonSocial }} - Création échéancier de paiement{% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-column>
<mj-text font-size="20px" font-weight="bold" color="#333333" padding-bottom="10px">
Votre nouvelle facture est disponible
</mj-text>
<mj-text font-size="16px" color="#555555" padding-bottom="20px">
Bonjour,
</mj-text>
<mj-text font-size="16px" color="#555555" padding-bottom="10px">
Nous vous informons que votre nouvelle <strong>facture</strong> n°<strong>{{ datas.paymentNotice.number }}</strong>) d'un montant de <strong>{{ datas.paymentNotice.amount | number_format(2, ',', ' ') }} € TTC</strong> est maintenant disponible.
</mj-text>
<mj-text font-size="16px" color="#555555" padding-bottom="20px" font-weight="bold">
Veuillez trouver la facture en pièce jointe de cet e-mail (format PDF).
</mj-text>
<mj-divider border-color="#cccccc" />
<mj-text font-size="14px" color="#555555" padding-top="20px">
Si vous avez des questions ou besoin d'assistance concernant cette facture, n'hésitez pas à nous contacter.
</mj-text>
<mj-text font-size="14px" color="#333333" padding-top="30px">
Cordialement,<br/>
Léquipe Support
</mj-text>
</mj-column>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-column>
<mj-text font-size="20px" font-weight="bold" color="#333333" padding-bottom="10px">
Notification de Paiement Client Validé
</mj-text>
<mj-text font-size="16px" color="#555555" padding-bottom="20px">
Bonjour<br/><br/>
Ce courriel confirme la **réception et la validation de votre paiement** pour l'avis de paiement {{ datas.advert.numAvis }}
</mj-text>
<mj-divider border-color="#cccccc" />
<mj-text font-size="16px" font-weight="bold" color="#333333" padding-top="15px" padding-bottom="5px">
Détails de la Transaction :
</mj-text>
<mj-text font-size="14px" color="#555555" line-height="1.5">
- **Numéro de commande / Dossier** : <strong>{{ datas.advert.numAvis }}</strong><br/>
- **Client concerné** : {{ datas.customer.raisonSocial }}<br/>
- **Montant total encaissé** : <strong>{{ datas.register.amount|format_currency('EUR') }}</strong><br/>
- **Date de validation du paiement** : {{ datas.register.createAt|date('d/m/Y') }}<br/>
- **Méthode de paiement** : {{ datas.register.type|trans }}<br/>
</mj-text>
<mj-text>
Une fois le paiement vérifiée de notre coté, une facture vous sera envoyée
</mj-text>
<mj-text font-size="14px" color="#333333" padding-top="30px">
Cordialement,<br/>
Le Support Commercial
</mj-text>
</mj-column>
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-column>
<mj-text font-size="20px" font-weight="bold" color="#333333" padding-bottom="10px">
Notification de Paiement Client Validé
</mj-text>
<mj-text font-size="16px" color="#555555" padding-bottom="20px">
Bonjour<br/><br/>
Ce courriel confirme la **réception et la validation d'un paiement client** pour avis de paiement {{ datas.advert.numAvis }} {% if datas.parent is not null %}(Différence de l'avis de paiement {{ datas.parent.numAvis }}){% endif %}.
</mj-text>
<mj-divider border-color="#cccccc" />
<mj-text font-size="16px" font-weight="bold" color="#333333" padding-top="15px" padding-bottom="5px">
Détails de la Transaction :
</mj-text>
<mj-text font-size="14px" color="#555555" line-height="1.5">
- **Numéro de commande / Dossier** : <strong>{{ datas.advert.numAvis }}</strong><br/>
- **Client concerné** : {{ datas.customer.raisonSocial }}<br/>
- **Montant total encaissé** : <strong>{{ datas.register.amount|format_currency('EUR') }}</strong><br/>
- **Date de validation du paiement** : {{ datas.register.createAt|date('d/m/Y') }}<br/>
- **Méthode de paiement** : {{ datas.register.type|trans }}<br/>
</mj-text>
<mj-text font-size="14px" color="#333333" padding-top="30px">
Cordialement,<br/>
Le Support Commercial
</mj-text>
</mj-column>
{% endblock %}

View File

@@ -76,3 +76,4 @@ customer: Client
administrator: Adminisateur
customer_group: Groupe d'accée
customer_settings: Paramétres
ech_created: Crée - En attends de validation