diff --git a/app/config/services.yml b/app/config/services.yml index fd465e8b1..eec0e169f 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -173,9 +173,6 @@ services: Afup\Site\Corporate\Page: autowire: true - Afup\Site\Forum\Facturation: - autowire: true - Afup\Site\Association\Cotisations: factory: ['@Afup\Site\Association\CotisationsFactory', 'create'] diff --git a/sources/Afup/Forum/Facturation.php b/sources/Afup/Forum/Facturation.php deleted file mode 100644 index ac397ed5e..000000000 --- a/sources/Afup/Forum/Facturation.php +++ /dev/null @@ -1,455 +0,0 @@ -_bdd->echapper($reference); - return $this->_bdd->obtenirEnregistrement($requete); - } - - /** - * Renvoit la liste des inscriptions à facturer ou facturé au forum - * - * @param int $id_forum Id du forum - * @param string $champs Champs à renvoyer - * @param string $ordre Tri des enregistrements - * @param bool $associatif Renvoyer un tableau associatif ? - * @return array - */ - public function obtenirListe($id_forum = null, - string $champs = '*', - string $ordre = 'date_reglement', - $associatif = false, - $filtre = false) - { - $requete = 'SELECT'; - $requete .= ' ' . $champs . ' '; - $requete .= 'FROM'; - $requete .= ' afup_facturation_forum '; - $requete .= 'WHERE etat IN ( ' . Ticket::STATUS_PAID . ', ' . Ticket::STATUS_WAITING . ', ' . Ticket::STATUS_CONFIRMED . ') '; - $requete .= ' AND id_forum =' . $id_forum . ' '; - if ($filtre) { - $requete .= ' AND (societe LIKE \'%' . $filtre . '%\' OR reference LIKE \'%' . $filtre . '%\' ) '; - } - $requete .= 'ORDER BY ' . $ordre; - - if ($associatif) { - return $this->_bdd->obtenirAssociatif($requete); - } else { - return $this->_bdd->obtenirTous($requete); - } - } - - public function creerReference($id_forum, $label): string - { - $slugger = new AsciiSlugger(); - - $label = preg_replace('/[^A-Z0-9_\-\:\.;]/', '', strtoupper($slugger->slug($label)->toString())); - - return 'F' . date('Y') . sprintf('%02d', $id_forum) . '-' . date('dm') . '-' . substr((string) $label, 0, 5) . '-' . substr(md5(date('r') . $label), -5); - } - - public function estFacture($reference) - { - $facture = $this->obtenir($reference, 'etat, facturation'); - if ($facture['facturation'] == Ticket::INVOICE_TODO) { - $requete = 'UPDATE afup_inscription_forum '; - $requete .= 'SET facturation=' . Ticket::INVOICE_SENT . ' '; - $requete .= 'WHERE reference=' . $this->_bdd->echapper($reference); - $this->_bdd->executer($requete); - - $requete = 'UPDATE afup_facturation_forum '; - $requete .= 'SET facturation=' . Ticket::INVOICE_SENT . ', date_facture= ' . time() . ' '; - $requete .= 'WHERE reference=' . $this->_bdd->echapper($reference); - return $this->_bdd->executer($requete); - } - return true; - } - - public function genererDevis(string $reference, $chemin = null): void - { - $requete = 'SELECT aff.*, af.titre AS event_name - FROM afup_facturation_forum aff - LEFT JOIN afup_forum af ON af.id = aff.id_forum - WHERE reference=' . $this->_bdd->echapper($reference); - $facture = $this->_bdd->obtenirEnregistrement($requete); - - $requete = 'SELECT aif.*, aft.pretty_name - FROM afup_inscription_forum aif - LEFT JOIN afup_forum_tarif aft ON aft.id = aif.type_inscription - WHERE reference=' . $this->_bdd->echapper($reference); - $inscriptions = $this->_bdd->obtenirTous($requete); - - $dateFacture = isset($facture['date_facture']) && !empty($facture['date_facture']) - ? new \DateTimeImmutable('@' . $facture['date_facture']) - : new \DateTimeImmutable(); - - $bankAccountFactory = new BankAccountFactory(); - // Construction du PDF - $pdf = new PDF_Facture($bankAccountFactory->createApplyableAt($dateFacture)); - $pdf->AddPage(); - - $pdf->Cell(130, 5); - $pdf->Cell(60, 5, 'Le ' . $dateFacture->format('d/m/Y')); - - $pdf->Ln(); - $pdf->Ln(); - $pdf->Ln(); - - if (empty($facture['societe'])) { - $facture['societe'] = $facture['nom'] . " " . $facture['prenom']; - } - - // A l'attention du client [adresse] - $pdf->SetFont('Arial', 'BU', 10); - $pdf->Cell(130, 5, 'Objet : Devis n°' . $reference); - $pdf->SetFont('Arial', '', 10); - $pdf->Ln(10); - $pdf->MultiCell(130, 5, $facture['societe'] . "\n" . $facture['adresse'] . "\n" . $facture['code_postal'] . "\n" . $facture['ville'] . "\n" . $this->pays->obtenirNom($facture['id_pays'])); - - $pdf->Ln(15); - - $pdf->MultiCell(180, 5, sprintf("Devis concernant votre participation au %s organisé par l'Association Française des Utilisateurs de PHP (AFUP).", $facture['event_name'])); - // Cadre - $pdf->Ln(10); - $pdf->SetFillColor(200, 200, 200); - $pdf->Cell(50, 5, 'Type', 1, 0, 'L', 1); - $pdf->Cell(100, 5, 'Personne inscrite', 1, 0, 'L', 1); - $pdf->Cell(40, 5, 'Prix', 1, 0, 'L', 1); - - $total = 0; - foreach ($inscriptions as $inscription) { - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - - $pdf->Cell(50, 5, $this->truncate($inscription['pretty_name'], 27), 1); - $pdf->Cell(100, 5, $inscription['prenom'] . ' ' . $inscription['nom'], 1); - $pdf->Cell(40, 5, $inscription['montant'] . ' €', 1); - $total += $inscription['montant']; - } - - $pdf->Ln(); - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(150, 5, 'TOTAL', 1, 0, 'L', 1); - $pdf->Cell(40, 5, $total . ' €', 1, 0, 'L', 1); - - $pdf->Ln(15); - $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); - - if (is_null($chemin)) { - $pdf->Output('Devis - ' . ($facture['societe'] ?: $facture['nom'] . '_' . $facture['prenom']) . ' - ' . date('Y-m-d_H-i', (int) $facture['date_facture']) . '.pdf', 'D', true); - } else { - $pdf->Output($chemin, 'F', true); - } - } - - protected function truncate($value, $length) - { - if ($value <= $length) { - return $value; - } - - return substr((string) $value, 0, $length) . '...'; - } - - /** - * Génère une facture au format PDF - * - * @param string $reference Reference de la facture - * @param string $chemin Chemin du fichier PDF à générer. Si ce chemin est omi, le PDF est renvoyé au navigateur. - */ - public function genererFacture(string $reference, $chemin = null): string - { - $type = ''; - $requete = 'SELECT aff.*, af.titre AS event_name, af.has_prices_defined_with_vat as event_has_prices_defined_with_vat - FROM afup_facturation_forum aff - LEFT JOIN afup_forum af ON af.id = aff.id_forum - WHERE reference=' . $this->_bdd->echapper($reference); - $facture = $this->_bdd->obtenirEnregistrement($requete); - - $requete = 'SELECT aif.*, aft.pretty_name - FROM afup_inscription_forum aif - LEFT JOIN afup_forum_tarif aft ON aft.id = aif.type_inscription - WHERE reference=' . $this->_bdd->echapper($reference); - $inscriptions = $this->_bdd->obtenirTous($requete); - - // Construction du PDF - - $dateFacture = isset($facture['date_facture']) && !empty($facture['date_facture']) - ? new \DateTimeImmutable('@' . $facture['date_facture']) - : new \DateTimeImmutable(); - - - $isSubjectedToVat = Vat::isSubjectedToVat($dateFacture); - - $bankAccountFactory = new BankAccountFactory(); - // Construction du PDF - $pdf = new PDF_Facture($bankAccountFactory->createApplyableAt($dateFacture), $isSubjectedToVat); - $pdf->AddPage(); - - $pdf->Cell(130, 5); - $pdf->Cell(60, 5, 'Le ' . $dateFacture->format('d/m/Y')); - - $pdf->Ln(); - $pdf->Ln(); - $pdf->Ln(); - - if (empty($facture['societe'])) { - $facture['societe'] = $facture['nom'] . " " . $facture['prenom']; - } - - // A l'attention du client [adresse] - $pdf->SetFont('Arial', 'BU', 10); - $pdf->Cell(130, 5, 'Objet : Facture n°' . $reference); - $pdf->SetFont('Arial', '', 10); - $pdf->Ln(10); - $pdf->MultiCell(130, 5, $facture['societe'] . "\n" . $facture['adresse'] . "\n" . $facture['code_postal'] . "\n" . $facture['ville'] . "\n" . $this->pays->obtenirNom($facture['id_pays'])); - - $pdf->Ln(15); - - $pdf->MultiCell(180, 5, sprintf("Facture concernant votre participation au %s organisé par l'Association Française des Utilisateurs de PHP (AFUP).", $facture['event_name'])); - - if ($facture['informations_reglement']) { - $pdf->Ln(10); - $pdf->Cell(32, 5, 'Référence client : '); - $infos = explode("\n", (string) $facture['informations_reglement']); - foreach ($infos as $info) { - $pdf->Cell(100, 5, $info); - $pdf->Ln(); - $pdf->Cell(32, 5); - } - } - - // Cadre - $pdf->Ln(10); - $pdf->SetFillColor(200, 200, 200); - $pdf->Cell(50, 5, 'Type', 1, 0, 'L', 1); - $pdf->Cell(100 - ($isSubjectedToVat ? 35 : 0), 5, 'Personne inscrite', 1, 0, 'L', 1); - $pdf->Cell($isSubjectedToVat ? 30 : 40, 5, 'Prix' . ($isSubjectedToVat ? ' HT' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - if ($isSubjectedToVat) { - $pdf->Cell(15, 5, 'TVA', 1, 0, 'C', 1); - $pdf->Cell(30, 5, 'Prix TTC', 1, 0, 'R', 1); - } - - - $total = 0; - $totalHt = 0; - foreach ($inscriptions as $inscription) { - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - - if ($facture['event_has_prices_defined_with_vat']) { - $montantHt = Vat::getRoundedWithoutVatPriceFromPriceWithVat($inscription['montant'], Utils::TICKETING_VAT_RATE); - $montant = $inscription['montant']; - } else { - $montantHt = $inscription['montant']; - $montant = Vat::getRoundedWithVatPriceFromPriceWithoutVat($inscription['montant'], Utils::TICKETING_VAT_RATE); - } - - - $pdf->Cell(50, 5, $this->truncate($inscription['pretty_name'], 27), 1); - $pdf->Cell(100 - ($isSubjectedToVat ? 35 : 0), 5, $inscription['prenom'] . ' ' . $inscription['nom'], 1); - $pdf->Cell( - $isSubjectedToVat ? 30 : 40, 5, - $this->formatFactureValue($isSubjectedToVat ? $montantHt : $montant, $isSubjectedToVat) . ' €', - 1, - 0, - $isSubjectedToVat ? 'R' : '', - ); - - if ($isSubjectedToVat) { - $pdf->Cell(15, 5, '10%', 1, 0, 'C'); - $pdf->Cell(30, 5, $this->formatFactureValue($montant, $isSubjectedToVat) . ' €', 1, 0, 'R'); - } - - $totalHt += $montantHt; - $total += $montant; - } - - if ($facture['type_reglement'] == 1) { // Paiement par chèque - $pdf->Ln(); - $pdf->Cell(50, 5, 'FRAIS', 1); - $pdf->Cell(100, 5, 'Paiement par chèque', 1); - $pdf->Cell(40, 5, '25' . ' €', 1); - $total += 25; - } - - $totalLabel = 'TOTAL'; - if ($isSubjectedToVat) { - $totalLabel .= ' TTC'; - } - - if ($isSubjectedToVat) { - $pdf->Ln(); - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(160, 5, 'Total HT', 1, 0, 'R', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($totalHt, $isSubjectedToVat) . ' €', 1, 0, 'R', 1); - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(160, 5, 'Total TVA 10%', 1, 0, 'R', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($total - $totalHt, $isSubjectedToVat) . ' €', 1, 0, 'R', 1); - } - - $pdf->Ln(); - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell( - 150 + ($isSubjectedToVat ? 10 : 0), - 5, - $totalLabel, - 1, - 0, - $isSubjectedToVat ? 'R' : 'L', - 1, - ); - $pdf->Cell( - 40 - ($isSubjectedToVat ? 10 : 0), 5, - $this->formatFactureValue($total, $isSubjectedToVat) . ' €', - 1, - 0, - $isSubjectedToVat ? 'R' : 'L', - 1, - ); - - $pdf->Ln(15); - if ($facture['etat'] == 4) { - switch ($facture['type_reglement']) { - case 0: - $type = 'par CB'; - break; - case 1: - $type = 'par chèque'; - break; - case 2: - $type = 'par virement'; - break; - } - $pdf->SetTextColor(255, 0, 0); - $pdf->Cell(130, 5); - if ($facture['type_reglement'] != Ticket::PAYMENT_NONE) { - $pdf->Cell(60, 5, 'Payé ' . $type . ' le ' . date('d/m/Y', (int) $facture['date_reglement'])); - } - $pdf->SetTextColor(0, 0, 0); - } - $pdf->Ln(); - if (false === $isSubjectedToVat) { - $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); - } - - if (is_null($chemin)) { - $pdf->Output('Facture - ' . ($facture['societe'] ?: $facture['nom'] . '_' . $facture['prenom']) . ' - ' . date('Y-m-d_H-i', (int) $facture['date_facture']) . '.pdf', 'D', true); - } else { - $pdf->Output($chemin, 'F', true); - } - - return $reference; - } - - private function formatFactureValue($value, bool $isSubjectedToVat) - { - if (false === $isSubjectedToVat) { - return $value; - } - - return number_format((float) $value, 2, ',', ' '); - } - - /** - * Envoi par mail d'une facture au format PDF - * - * @param string|array $reference Invoicing reference as string, or the invoice itself - * @return bool Succès de l'envoi - */ - public function envoyerFacture($reference, $copyTresorier = true, $facturer = true) - { - if (is_array($reference)) { - $personne = $reference; - $reference = $personne['reference']; - } else { - $personne = $this->obtenir($reference, 'email, nom, prenom'); - } - - $cheminFacture = AFUP_CHEMIN_RACINE . 'cache' . DIRECTORY_SEPARATOR . 'fact' . $reference . '.pdf'; - $numeroFacture = $this->genererFacture($reference, $cheminFacture); - - $message = new Message( - 'Facture évènement AFUP', - MailUserFactory::afup(), - new MailUser($personne['email'], sprintf('%s %s', $personne['prenom'], $personne['nom'])), - ); - $mailer = Mail::createMailer(); - $mailer->renderTemplate($message,'mail_templates/facture-forum.html.twig', [ - 'raison_sociale' => AFUP_RAISON_SOCIALE, - 'adresse' => AFUP_ADRESSE, - 'ville' => AFUP_CODE_POSTAL . ' ' . AFUP_VILLE, - ]); - $message->addAttachment(new Attachment( - $cheminFacture, - 'facture-' . $numeroFacture . '.pdf', - 'base64', - 'application/pdf', - )); - if ($copyTresorier) { - $message->addBcc(MailUserFactory::tresorier()); - } - $ok = $mailer->send($message); - @unlink($cheminFacture); - - if ($ok && $facturer) { - $this->estFacture($reference); - } - - return $ok; - } - - /** - * Changement de la date de réglement d'une facture - * @param integer $reference - * @param integer $date_reglement - */ - public function changerDateReglement($reference, $date_reglement) - { - $requete = 'UPDATE '; - $requete .= ' afup_facturation_forum '; - $requete .= 'SET '; - $requete .= ' date_reglement = ' . intval($date_reglement) . ' '; - $requete .= 'WHERE'; - $requete .= ' reference=' . $this->_bdd->echapper($reference); - return $this->_bdd->executer($requete); - } -} diff --git a/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadDevisAction.php b/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadDevisAction.php index c115ebd3f..bd31c1b36 100644 --- a/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadDevisAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadDevisAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Event\Facturation; -use Afup\Site\Forum\Facturation; +use AppBundle\Event\Invoice\EventInvoicePdfGenerator; use AppBundle\Event\Model\Invoice; use AppBundle\Event\Model\Repository\InvoiceRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -15,7 +15,7 @@ class DownloadDevisAction extends AbstractController { public function __construct( - private readonly Facturation $facturation, + private readonly EventInvoicePdfGenerator $pdfGenerator, private readonly InvoiceRepository $invoiceRepository, ) {} @@ -27,12 +27,13 @@ public function __invoke(Request $request): Response throw new NotFoundHttpException("Ce devis n'existe pas"); } - ob_start(); - $this->facturation->genererDevis($reference); - $pdf = ob_get_clean(); + $date = $devis->getInvoiceDate() ?? new \DateTime(); + $label = $devis->getCompany() ?: ($devis->getLastname() . ' ' . $devis->getFirstname()); + $filename = 'Devis - ' . $label . ' - ' . $date->format('Y-m-d_H-i') . '.pdf'; - $response = new Response($pdf); + $response = new Response($this->pdfGenerator->generateQuote($reference)); $response->headers->set('Content-Type', 'application/pdf'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); return $response; } diff --git a/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadFactureAction.php b/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadFactureAction.php index 8072c4bc8..306ae6944 100644 --- a/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadFactureAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Facturation/DownloadFactureAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Event\Facturation; -use Afup\Site\Forum\Facturation; +use AppBundle\Event\Invoice\EventInvoicePdfGenerator; use AppBundle\Event\Model\Invoice; use AppBundle\Event\Model\Repository\InvoiceRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -15,7 +15,7 @@ class DownloadFactureAction extends AbstractController { public function __construct( - private readonly Facturation $facturation, + private readonly EventInvoicePdfGenerator $pdfGenerator, private readonly InvoiceRepository $invoiceRepository, ) {} @@ -27,12 +27,13 @@ public function __invoke(Request $request): Response throw new NotFoundHttpException("Cette facture n'existe pas"); } - ob_start(); - $this->facturation->genererFacture($reference); - $pdf = ob_get_clean(); + $date = $facture->getInvoiceDate() ?? new \DateTime(); + $label = $facture->getCompany() ?: ($facture->getLastname() . ' ' . $facture->getFirstname()); + $filename = 'Facture - ' . $label . ' - ' . $date->format('Y-m-d_H-i') . '.pdf'; - $response = new Response($pdf); + $response = new Response($this->pdfGenerator->generateInvoice($reference)); $response->headers->set('Content-Type', 'application/pdf'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); return $response; } diff --git a/sources/AppBundle/Controller/Admin/Event/Facturation/IssueFactureAction.php b/sources/AppBundle/Controller/Admin/Event/Facturation/IssueFactureAction.php index 988bf4ee2..315229d4a 100644 --- a/sources/AppBundle/Controller/Admin/Event/Facturation/IssueFactureAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Facturation/IssueFactureAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Admin\Event\Facturation; -use Afup\Site\Forum\Facturation; use AppBundle\AuditLog\Audit; +use AppBundle\Event\Invoice\InvoiceService; use AppBundle\Event\Model\Invoice; use AppBundle\Event\Model\Repository\InvoiceRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -17,7 +17,7 @@ class IssueFactureAction extends AbstractController { public function __construct( private readonly InvoiceRepository $invoiceRepository, - private readonly Facturation $facturation, + private readonly InvoiceService $invoiceService, private readonly Audit $audit, ) {} @@ -29,7 +29,7 @@ public function __invoke(Request $request): Response throw new NotFoundHttpException("Cette facture n'existe pas"); } - if ($this->facturation->estFacture($reference)) { + if ($this->invoiceService->markAsInvoiced($facture)) { $this->audit->log('Facturation => facture n°' . $reference); $this->addFlash('notice', 'La facture est prise en compte'); return $this->redirectToRoute('admin_event_factures'); diff --git a/sources/AppBundle/Controller/Admin/Event/Facturation/SendFactureAction.php b/sources/AppBundle/Controller/Admin/Event/Facturation/SendFactureAction.php index b2ebfd326..914aeb27f 100644 --- a/sources/AppBundle/Controller/Admin/Event/Facturation/SendFactureAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Facturation/SendFactureAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Admin\Event\Facturation; -use Afup\Site\Forum\Facturation; use AppBundle\AuditLog\Audit; +use AppBundle\Event\Invoice\EventInvoiceMailer; use AppBundle\Event\Model\Invoice; use AppBundle\Event\Model\Repository\InvoiceRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -16,7 +16,7 @@ class SendFactureAction extends AbstractController { public function __construct( - private readonly Facturation $facturation, + private readonly EventInvoiceMailer $invoiceMailer, private readonly InvoiceRepository $invoiceRepository, private readonly Audit $audit, ) {} @@ -29,7 +29,7 @@ public function __invoke(Request $request): Response throw new NotFoundHttpException("Cette facture n'existe pas"); } - if ($this->facturation->envoyerFacture($reference)) { + if ($this->invoiceMailer->send($reference)) { $this->audit->log('Facturation => facture n°' . $reference); $this->addFlash('notice', 'La facture a été envoyée'); return $this->redirectToRoute('admin_event_factures'); diff --git a/sources/AppBundle/Controller/Admin/Event/PendingBankwiresAction.php b/sources/AppBundle/Controller/Admin/Event/PendingBankwiresAction.php index 07b0e2076..cc838e9b2 100644 --- a/sources/AppBundle/Controller/Admin/Event/PendingBankwiresAction.php +++ b/sources/AppBundle/Controller/Admin/Event/PendingBankwiresAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Admin\Event; -use Afup\Site\Forum\Facturation; use AppBundle\Email\Emails; +use AppBundle\Event\Invoice\EventInvoiceMailer; use AppBundle\Email\Mailer\MailUser; use AppBundle\Event\AdminEventSelection; use AppBundle\Event\Model\Event; @@ -30,7 +30,7 @@ public function __construct( private readonly Emails $emails, private readonly EventDispatcherInterface $eventDispatcher, private readonly CsrfTokenManagerInterface $csrfTokenManager, - private readonly Facturation $facturation, + private readonly EventInvoiceMailer $invoiceMailer, ) {} public function __invoke(Request $request, AdminEventSelection $eventSelection): Response @@ -70,7 +70,7 @@ private function setInvoicePaid(Event $event, Invoice $invoice): void $this->invoiceRepository->save($invoice); $tickets = $this->ticketRepository->getByReference($invoice->getReference()); - $this->facturation->envoyerFacture($invoice->getReference()); + $this->invoiceMailer->send($invoice->getReference()); $this->addFlash('notice', sprintf('La facture %s a été marquée comme payée', $invoice->getReference())); foreach ($tickets as $ticket) { diff --git a/sources/AppBundle/Controller/Admin/Event/Ticket/AddAction.php b/sources/AppBundle/Controller/Admin/Event/Ticket/AddAction.php index 54f4d9a2c..6257438d3 100644 --- a/sources/AppBundle/Controller/Admin/Event/Ticket/AddAction.php +++ b/sources/AppBundle/Controller/Admin/Event/Ticket/AddAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Admin\Event\Ticket; -use Afup\Site\Forum\Facturation; use AppBundle\AuditLog\Audit; +use AppBundle\Event\Invoice\EventInvoiceReferenceGenerator; use AppBundle\Event\Form\TicketAdminWithInvoiceType; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Invoice; @@ -27,7 +27,7 @@ public function __construct( private readonly EventRepository $eventRepository, private readonly InvoiceRepository $invoiceRepository, private readonly Audit $audit, - private readonly Facturation $facturation, + private readonly EventInvoiceReferenceGenerator $referenceGenerator, ) {} public function __invoke(Request $request): Response @@ -63,7 +63,7 @@ public function __invoke(Request $request): Response throw $this->createNotFoundException(sprintf('Offer not found with ticketTypeId "%s"', $ticketTypeId)); } - $reference = $this->facturation->creerReference($event->getId(), $ticket->getLabel()); + $reference = $this->referenceGenerator->generate($event->getId(), $ticket->getLabel()); if ($offer->ticketEventType) { $ticket->setTicketEventType($offer->ticketEventType); } diff --git a/sources/AppBundle/Controller/Event/Ticket/PayboxCallbackAction.php b/sources/AppBundle/Controller/Event/Ticket/PayboxCallbackAction.php index ba70eb977..72cead0bb 100644 --- a/sources/AppBundle/Controller/Event/Ticket/PayboxCallbackAction.php +++ b/sources/AppBundle/Controller/Event/Ticket/PayboxCallbackAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Event\Ticket; -use Afup\Site\Forum\Facturation; use AppBundle\Controller\Event\EventActionHelper; +use AppBundle\Event\Invoice\EventInvoiceMailer; use AppBundle\Email\Emails; use AppBundle\Email\Mailer\MailUser; use AppBundle\Event\Model\Repository\InvoiceRepository; @@ -30,7 +30,7 @@ public function __construct( private readonly InvoiceRepository $invoiceRepository, private readonly TicketRepository $ticketRepository, private readonly EventActionHelper $eventActionHelper, - private readonly Facturation $facturation, + private readonly EventInvoiceMailer $invoiceMailer, ) {} public function __invoke(string $eventSlug, Request $request): Response @@ -67,7 +67,7 @@ public function __invoke(string $eventSlug, Request $request): Response $tickets = $this->ticketRepository->getByReference($invoice->getReference()); if ($paymentStatus === Ticket::STATUS_PAID) { - $this->facturation->envoyerFacture($invoice->getReference()); + $this->invoiceMailer->send($invoice->getReference()); } foreach ($tickets as $ticket) { diff --git a/sources/AppBundle/Controller/Event/Ticket/PaymentAction.php b/sources/AppBundle/Controller/Event/Ticket/PaymentAction.php index 68df6c9b1..917126a51 100644 --- a/sources/AppBundle/Controller/Event/Ticket/PaymentAction.php +++ b/sources/AppBundle/Controller/Event/Ticket/PaymentAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Event\Ticket; -use Afup\Site\Forum\Facturation; use Afup\Site\Utils\Utils; +use AppBundle\Event\Invoice\EventInvoiceMailer; use Afup\Site\Utils\Vat; use AppBundle\Compta\BankAccount\BankAccountFactory; use AppBundle\Controller\Event\EventActionHelper; @@ -32,7 +32,7 @@ public function __construct( private readonly TicketRepository $ticketRepository, private readonly PayboxFactory $payboxFactory, private readonly EventActionHelper $eventActionHelper, - private readonly Facturation $facturation, + private readonly EventInvoiceMailer $invoiceMailer, ) {} public function __invoke($eventSlug, Request $request): Response @@ -79,7 +79,7 @@ public function __invoke($eventSlug, Request $request): Response $invoiceRepository->save($invoice); - $this->facturation->envoyerFacture($invoice->getReference()); + $this->invoiceMailer->send($invoice->getReference()); $ticketRepository = $this->ticketRepository; $tickets = $ticketRepository->getByInvoiceWithDetail($invoice); @@ -102,7 +102,7 @@ public function __invoke($eventSlug, Request $request): Response $params['bankAccount'] = $bankAccountFactory->createApplyableAt($invoice->getinvoiceDate()); // For bankwire, companies need to retrieve the invoice - $this->facturation->envoyerFacture($invoiceRef); + $this->invoiceMailer->send($invoiceRef); } return $this->render('event/ticket/payment.html.twig', $params); diff --git a/sources/AppBundle/Controller/Event/Ticket/TicketAction.php b/sources/AppBundle/Controller/Event/Ticket/TicketAction.php index df4ef0e0a..1ff977a5b 100644 --- a/sources/AppBundle/Controller/Event/Ticket/TicketAction.php +++ b/sources/AppBundle/Controller/Event/Ticket/TicketAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Event\Ticket; -use Afup\Site\Forum\Facturation; use Afup\Site\Utils\Vat; +use AppBundle\Event\Invoice\EventInvoiceReferenceGenerator; use AppBundle\Association\MemberType; use AppBundle\Association\Model\User; use AppBundle\Controller\Event\EventActionHelper; @@ -28,7 +28,7 @@ public function __construct( private readonly TicketRepository $ticketRepository, private readonly EventActionHelper $eventActionHelper, private readonly TicketEventTypeRepository $ticketEventTypeRepository, - private readonly Facturation $facturation, + private readonly EventInvoiceReferenceGenerator $referenceGenerator, private readonly Authentication $authentication, ) {} @@ -84,7 +84,7 @@ public function __invoke($eventSlug, Request $request): Response $invoice->setTickets($tickets); - $reference = $this->facturation->creerReference($event->getId(), $invoice->getLabel()); + $reference = $this->referenceGenerator->generate($event->getId(), $invoice->getLabel()); $invoice->setReference($reference); $invoiceRepository->saveWithTickets($invoice); diff --git a/sources/AppBundle/Event/Invoice/EventInvoiceMailer.php b/sources/AppBundle/Event/Invoice/EventInvoiceMailer.php new file mode 100644 index 000000000..3468ce0aa --- /dev/null +++ b/sources/AppBundle/Event/Invoice/EventInvoiceMailer.php @@ -0,0 +1,65 @@ +invoiceRepository->getByReference($reference); + if ($invoice === null) { + return false; + } + + $cheminFacture = AFUP_CHEMIN_RACINE . 'cache' . DIRECTORY_SEPARATOR . 'fact' . $reference . '.pdf'; + $this->pdfGenerator->generateInvoice($reference, $cheminFacture); + + $message = new Message( + 'Facture évènement AFUP', + MailUserFactory::afup(), + new MailUser($invoice->getEmail(), sprintf('%s %s', $invoice->getFirstname(), $invoice->getLastname())), + ); + + $this->mailer->renderTemplate($message, 'mail_templates/facture-forum.html.twig', [ + 'raison_sociale' => AFUP_RAISON_SOCIALE, + 'adresse' => AFUP_ADRESSE, + 'ville' => AFUP_CODE_POSTAL . ' ' . AFUP_VILLE, + ]); + + $message->addAttachment(new Attachment( + $cheminFacture, + 'facture-' . $reference . '.pdf', + 'base64', + 'application/pdf', + )); + + if ($copyTresorier) { + $message->addBcc(MailUserFactory::tresorier()); + } + + $ok = $this->mailer->send($message); + @unlink($cheminFacture); + + if ($ok && $facturer) { + $this->invoiceService->markAsInvoiced($invoice); + } + + return $ok; + } +} diff --git a/sources/AppBundle/Event/Invoice/EventInvoicePdfGenerator.php b/sources/AppBundle/Event/Invoice/EventInvoicePdfGenerator.php new file mode 100644 index 000000000..c868a13a1 --- /dev/null +++ b/sources/AppBundle/Event/Invoice/EventInvoicePdfGenerator.php @@ -0,0 +1,248 @@ +invoiceRepository->getWithEventDataByReference($reference); + $inscriptions = $this->ticketRepository->getWithTicketTypeByReference($reference); + + $dateFacture = isset($facture['date_facture']) && !empty($facture['date_facture']) + ? new \DateTimeImmutable('@' . $facture['date_facture']) + : new \DateTimeImmutable(); + + $pdf = new PDF_Facture($this->bankAccountFactory->createApplyableAt($dateFacture)); + $pdf->AddPage(); + + $pdf->Cell(130, 5); + $pdf->Cell(60, 5, 'Le ' . $dateFacture->format('d/m/Y')); + + $pdf->Ln(); + $pdf->Ln(); + $pdf->Ln(); + + if (empty($facture['societe'])) { + $facture['societe'] = $facture['nom'] . ' ' . $facture['prenom']; + } + + $pdf->SetFont('Arial', 'BU', 10); + $pdf->Cell(130, 5, 'Objet : Devis n°' . $reference); + $pdf->SetFont('Arial', '', 10); + $pdf->Ln(10); + $pdf->MultiCell(130, 5, $facture['societe'] . "\n" . $facture['adresse'] . "\n" . $facture['code_postal'] . "\n" . $facture['ville'] . "\n" . $this->pays->obtenirNom($facture['id_pays'])); + + $pdf->Ln(15); + $pdf->MultiCell(180, 5, sprintf("Devis concernant votre participation au %s organisé par l'Association Française des Utilisateurs de PHP (AFUP).", $facture['event_name'])); + + $pdf->Ln(10); + $pdf->SetFillColor(200, 200, 200); + $pdf->Cell(50, 5, 'Type', 1, 0, 'L', 1); + $pdf->Cell(100, 5, 'Personne inscrite', 1, 0, 'L', 1); + $pdf->Cell(40, 5, 'Prix', 1, 0, 'L', 1); + + $total = 0; + foreach ($inscriptions as $inscription) { + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(50, 5, $this->truncate($inscription['pretty_name'], 27), 1); + $pdf->Cell(100, 5, $inscription['prenom'] . ' ' . $inscription['nom'], 1); + $pdf->Cell(40, 5, $inscription['montant'] . ' €', 1); + $total += $inscription['montant']; + } + + $pdf->Ln(); + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(150, 5, 'TOTAL', 1, 0, 'L', 1); + $pdf->Cell(40, 5, $total . ' €', 1, 0, 'L', 1); + + $pdf->Ln(15); + $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); + + if ($path !== null) { + $pdf->Output($path, 'F'); + return ''; + } + + return (string) $pdf->Output('', 'S'); + } + + public function generateInvoice(string $reference, ?string $path = null): string + { + $facture = $this->invoiceRepository->getWithEventDataByReference($reference); + $inscriptions = $this->ticketRepository->getWithTicketTypeByReference($reference); + + $dateFacture = isset($facture['date_facture']) && !empty($facture['date_facture']) + ? new \DateTimeImmutable('@' . $facture['date_facture']) + : new \DateTimeImmutable(); + + $isSubjectedToVat = Vat::isSubjectedToVat($dateFacture); + + $pdf = new PDF_Facture($this->bankAccountFactory->createApplyableAt($dateFacture), $isSubjectedToVat); + $pdf->AddPage(); + + $pdf->Cell(130, 5); + $pdf->Cell(60, 5, 'Le ' . $dateFacture->format('d/m/Y')); + + $pdf->Ln(); + $pdf->Ln(); + $pdf->Ln(); + + if (empty($facture['societe'])) { + $facture['societe'] = $facture['nom'] . ' ' . $facture['prenom']; + } + + $pdf->SetFont('Arial', 'BU', 10); + $pdf->Cell(130, 5, 'Objet : Facture n°' . $reference); + $pdf->SetFont('Arial', '', 10); + $pdf->Ln(10); + $pdf->MultiCell(130, 5, $facture['societe'] . "\n" . $facture['adresse'] . "\n" . $facture['code_postal'] . "\n" . $facture['ville'] . "\n" . $this->pays->obtenirNom($facture['id_pays'])); + + $pdf->Ln(15); + $pdf->MultiCell(180, 5, sprintf("Facture concernant votre participation au %s organisé par l'Association Française des Utilisateurs de PHP (AFUP).", $facture['event_name'])); + + if ($facture['informations_reglement']) { + $pdf->Ln(10); + $pdf->Cell(32, 5, 'Référence client : '); + $infos = explode("\n", (string) $facture['informations_reglement']); + foreach ($infos as $info) { + $pdf->Cell(100, 5, $info); + $pdf->Ln(); + $pdf->Cell(32, 5); + } + } + + $pdf->Ln(10); + $pdf->SetFillColor(200, 200, 200); + $pdf->Cell(50, 5, 'Type', 1, 0, 'L', 1); + $pdf->Cell(100 - ($isSubjectedToVat ? 35 : 0), 5, 'Personne inscrite', 1, 0, 'L', 1); + $pdf->Cell($isSubjectedToVat ? 30 : 40, 5, 'Prix' . ($isSubjectedToVat ? ' HT' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + if ($isSubjectedToVat) { + $pdf->Cell(15, 5, 'TVA', 1, 0, 'C', 1); + $pdf->Cell(30, 5, 'Prix TTC', 1, 0, 'R', 1); + } + + $total = 0; + $totalHt = 0; + foreach ($inscriptions as $inscription) { + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + + if ($facture['event_has_prices_defined_with_vat']) { + $montantHt = Vat::getRoundedWithoutVatPriceFromPriceWithVat($inscription['montant'], Utils::TICKETING_VAT_RATE); + $montant = $inscription['montant']; + } else { + $montantHt = $inscription['montant']; + $montant = Vat::getRoundedWithVatPriceFromPriceWithoutVat($inscription['montant'], Utils::TICKETING_VAT_RATE); + } + + $pdf->Cell(50, 5, $this->truncate($inscription['pretty_name'], 27), 1); + $pdf->Cell(100 - ($isSubjectedToVat ? 35 : 0), 5, $inscription['prenom'] . ' ' . $inscription['nom'], 1); + $pdf->Cell( + $isSubjectedToVat ? 30 : 40, 5, + $this->formatValue($isSubjectedToVat ? $montantHt : $montant, $isSubjectedToVat) . ' €', + 1, 0, $isSubjectedToVat ? 'R' : '', + ); + + if ($isSubjectedToVat) { + $pdf->Cell(15, 5, '10%', 1, 0, 'C'); + $pdf->Cell(30, 5, $this->formatValue($montant, $isSubjectedToVat) . ' €', 1, 0, 'R'); + } + + $totalHt += $montantHt; + $total += $montant; + } + + if ($facture['type_reglement'] == 1) { + $pdf->Ln(); + $pdf->Cell(50, 5, 'FRAIS', 1); + $pdf->Cell(100, 5, 'Paiement par chèque', 1); + $pdf->Cell(40, 5, '25 €', 1); + $total += 25; + } + + $totalLabel = 'TOTAL' . ($isSubjectedToVat ? ' TTC' : ''); + + if ($isSubjectedToVat) { + $pdf->Ln(); + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(160, 5, 'Total HT', 1, 0, 'R', 1); + $pdf->Cell(30, 5, $this->formatValue($totalHt, $isSubjectedToVat) . ' €', 1, 0, 'R', 1); + + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(160, 5, 'Total TVA 10%', 1, 0, 'R', 1); + $pdf->Cell(30, 5, $this->formatValue($total - $totalHt, $isSubjectedToVat) . ' €', 1, 0, 'R', 1); + } + + $pdf->Ln(); + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(150 + ($isSubjectedToVat ? 10 : 0), 5, $totalLabel, 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + $pdf->Cell(40 - ($isSubjectedToVat ? 10 : 0), 5, $this->formatValue($total, $isSubjectedToVat) . ' €', 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + + $pdf->Ln(15); + if ($facture['etat'] == 4) { + $type = match ($facture['type_reglement']) { + 0 => 'par CB', + 1 => 'par chèque', + 2 => 'par virement', + default => '', + }; + $pdf->SetTextColor(255, 0, 0); + $pdf->Cell(130, 5); + if ($facture['type_reglement'] != Ticket::PAYMENT_NONE) { + $pdf->Cell(60, 5, 'Payé ' . $type . ' le ' . date('d/m/Y', (int) $facture['date_reglement'])); + } + $pdf->SetTextColor(0, 0, 0); + } + + $pdf->Ln(); + if (false === $isSubjectedToVat) { + $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); + } + + if ($path !== null) { + $pdf->Output($path, 'F'); + return ''; + } + + return (string) $pdf->Output('', 'S'); + } + + private function truncate(string $value, int $length): string + { + if (strlen($value) <= $length) { + return $value; + } + + return substr($value, 0, $length) . '...'; + } + + private function formatValue(mixed $value, bool $isSubjectedToVat): string + { + if (false === $isSubjectedToVat) { + return (string) $value; + } + + return number_format((float) $value, 2, ',', ' '); + } +} diff --git a/sources/AppBundle/Event/Invoice/EventInvoiceReferenceGenerator.php b/sources/AppBundle/Event/Invoice/EventInvoiceReferenceGenerator.php new file mode 100644 index 000000000..dd105e1dd --- /dev/null +++ b/sources/AppBundle/Event/Invoice/EventInvoiceReferenceGenerator.php @@ -0,0 +1,18 @@ +slug($label)->toString())); + + return 'F' . date('Y') . sprintf('%02d', $forumId) . '-' . date('dm') . '-' . substr((string) $label, 0, 5) . '-' . substr(md5(date('r') . $label), -5); + } +} diff --git a/sources/AppBundle/Event/Invoice/InvoiceService.php b/sources/AppBundle/Event/Invoice/InvoiceService.php index 7d5d09b6a..e464bedc2 100644 --- a/sources/AppBundle/Event/Invoice/InvoiceService.php +++ b/sources/AppBundle/Event/Invoice/InvoiceService.php @@ -8,6 +8,7 @@ use AppBundle\Event\Model\Repository\InvoiceRepository; use AppBundle\Event\Model\Repository\TicketRepository; use AppBundle\Event\Model\Ticket; +use DateTime; class InvoiceService { @@ -80,6 +81,24 @@ public function handleInvoicing( } } + public function markAsInvoiced(Invoice $invoice): bool + { + if ($invoice->getInvoice()) { + return true; + } + + $tickets = $this->ticketRepository->getByReference($invoice->getReference()); + foreach ($tickets as $ticket) { + $ticket->setInvoiceStatus(Ticket::INVOICE_SENT); + $this->ticketRepository->save($ticket); + } + + $invoice->setInvoice(true)->setInvoiceDate(new DateTime()); + $this->invoiceRepository->save($invoice); + + return true; + } + public function deleteInvoice(Invoice $invoice): void { $tickets = $this->ticketRepository->getByReference($invoice->getReference()); diff --git a/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php b/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php index e92f4bd3a..251edd0ca 100644 --- a/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php +++ b/sources/AppBundle/Event/Model/Repository/InvoiceRepository.php @@ -7,6 +7,7 @@ use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Invoice; use AppBundle\Event\Model\Ticket; +use AppBundle\Ting\DateTimeWithTimeZoneSerializer; use AppBundle\Ting\JoinHydrator; use Aura\SqlQuery\Mysql\Select; use CCMBenchmark\Ting\Driver\Exception; @@ -67,6 +68,22 @@ public function getByReference(mixed $reference): ?Invoice return $this->getOneBy(['reference' => $reference]); } + public function getWithEventDataByReference(string $reference): ?array + { + $query = $this->getQuery( + 'SELECT aff.*, af.titre AS event_name, af.has_prices_defined_with_vat AS event_has_prices_defined_with_vat + FROM afup_facturation_forum aff + LEFT JOIN afup_forum af ON af.id = aff.id_forum + WHERE aff.reference = :reference', + ); + $query->setParams(['reference' => $reference]); + foreach ($query->query($this->getCollection(new HydratorArray())) as $row) { + return $row; + } + + return null; + } + public function getPendingBankwires(Event $event) { /** @@ -212,6 +229,7 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor 'columnName' => 'date_reglement', 'fieldName' => 'paymentDate', 'type' => 'datetime', + 'serializer' => DateTimeWithTimeZoneSerializer::class, 'serializer_options' => [ 'unserialize' => ['unSerializeUseFormat' => true, 'format' => 'U'], 'serialize' => ['format' => 'U'], @@ -221,6 +239,7 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor 'columnName' => 'date_facture', 'fieldName' => 'invoiceDate', 'type' => 'datetime', + 'serializer' => DateTimeWithTimeZoneSerializer::class, 'serializer_options' => [ 'unserialize' => ['unSerializeUseFormat' => true, 'format' => 'U'], 'serialize' => ['format' => 'U'], diff --git a/sources/AppBundle/Event/Model/Repository/TicketRepository.php b/sources/AppBundle/Event/Model/Repository/TicketRepository.php index 9d5af7645..6ada1361c 100644 --- a/sources/AppBundle/Event/Model/Repository/TicketRepository.php +++ b/sources/AppBundle/Event/Model/Repository/TicketRepository.php @@ -89,6 +89,23 @@ public function getRegistrationsForEventsWithNewsletterAllowed(\Traversable $eve ; } + public function getWithTicketTypeByReference(string $reference): array + { + $query = $this->getQuery( + 'SELECT aif.*, aft.pretty_name + FROM afup_inscription_forum aif + LEFT JOIN afup_forum_tarif aft ON aft.id = aif.type_inscription + WHERE aif.reference = :reference', + ); + $query->setParams(['reference' => $reference]); + $results = []; + foreach ($query->query($this->getCollection(new HydratorArray())) as $row) { + $results[] = $row; + } + + return $results; + } + public function getByInvoiceWithDetail(Invoice $invoice) { return $this->getPreparedQuery(