From 778f538b08ea34d1746f6503643b5d8dd4db118f Mon Sep 17 00:00:00 2001 From: vgreb Date: Thu, 14 May 2026 14:34:42 +0200 Subject: [PATCH] Refonte classe Afup\Site\Comptabilite\Facture A merger avant #2210 resolves #2239 --- app/config/services.yml | 3 - sources/Afup/Comptabilite/Facture.php | 743 ------------------ .../AppBundle/Accounting/InvoicingMailer.php | 51 ++ .../Accounting/InvoicingNumberGenerator.php | 35 + .../Accounting/InvoicingPdfGenerator.php | 299 +++++++ .../Model/Repository/InvoicingRepository.php | 97 ++- .../Invoice/DownloadInvoiceAction.php | 8 +- .../Invoice/SendInvoiceEmailAction.php | 11 +- .../Quotation/AddQuotationAction.php | 6 +- .../Quotation/ConvertQuotationAction.php | 6 +- .../Quotation/DownloadQuotationAction.php | 8 +- .../Website/Payment/InvoiceAction.php | 28 +- .../Website/Payment/InvoiceDownloadAction.php | 16 +- .../Website/Payment/InvoiceRedirectAction.php | 12 +- 14 files changed, 509 insertions(+), 814 deletions(-) delete mode 100644 sources/Afup/Comptabilite/Facture.php create mode 100644 sources/AppBundle/Accounting/InvoicingMailer.php create mode 100644 sources/AppBundle/Accounting/InvoicingNumberGenerator.php create mode 100644 sources/AppBundle/Accounting/InvoicingPdfGenerator.php diff --git a/app/config/services.yml b/app/config/services.yml index ef0e873f1..b1a287310 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -179,9 +179,6 @@ services: Afup\Site\Association\Cotisations: factory: ['@Afup\Site\Association\CotisationsFactory', 'create'] - Afup\Site\Comptabilite\Facture: - autowire: true - Afup\Site\Droits: autowire: true diff --git a/sources/Afup/Comptabilite/Facture.php b/sources/Afup/Comptabilite/Facture.php deleted file mode 100644 index 91d8aa1d8..000000000 --- a/sources/Afup/Comptabilite/Facture.php +++ /dev/null @@ -1,743 +0,0 @@ -= (select date_debut from compta_periode where id = %s)', $this->_bdd->echapper($idPeriode)); - $requete .= sprintf(' AND acf.date_devis <= (select date_fin from compta_periode where id = %s)', $this->_bdd->echapper($idPeriode)); - } - - $requete .= 'GROUP BY '; - $requete .= ' acf.id, date_devis, numero_devis, date_facture, numero_facture, societe, service, adresse, code_postal, ville, id_pays, email, observation, ref_clt1, ref_clt2, ref_clt3, nom, prenom, tel, etat_paiement, date_paiement, devise_facture '; - $requete .= 'ORDER BY '; - $requete .= ' acf.date_devis DESC'; - - return $this->_bdd->obtenirTous($requete); - } - - public function obtenirDevisDetails($id) - { - $requete = 'SELECT '; - $requete .= 'afup_compta_facture.*, '; - $requete .= 'afup_compta_facture_details.ref,afup_compta_facture_details.designation,afup_compta_facture_details.quantite,afup_compta_facture_details.pu '; - $requete .= 'FROM '; - $requete .= 'afup_compta_facture, '; - $requete .= 'afup_compta_facture_details '; - $requete .= 'WHERE '; - $requete .= ' numero_devis != "" '; - $requete .= 'afup_compta_facture.id = afup_compta_facture_details.idafup_compta_facture '; - $requete .= 'ORDER BY '; - $requete .= 'compta.date_devis '; - - return $this->_bdd->obtenirTous($requete); - } - - public function obtenirFacture($idPeriode = null) - { - $requete = 'SELECT '; - $requete .= ' acf.*, sum(quantite * pu) prix '; - $requete .= 'FROM '; - $requete .= ' afup_compta_facture acf '; - $requete .= 'LEFT JOIN '; - $requete .= ' afup_compta_facture_details acfd '; - $requete .= 'ON '; - $requete .= ' acfd.idafup_compta_facture = acf.id '; - $requete .= 'WHERE '; - $requete .= ' numero_facture != "" '; - - if (null !== $idPeriode) { - $requete .= sprintf(' AND acf.date_facture >= (select date_debut from compta_periode where id = %s)', $this->_bdd->echapper($idPeriode)); - $requete .= sprintf(' AND acf.date_facture <= (select date_fin from compta_periode where id = %s)', $this->_bdd->echapper($idPeriode)); - } - - $requete .= 'GROUP BY '; - $requete .= ' acf.id, date_devis, numero_devis, date_facture, numero_facture, societe, service, adresse, code_postal, ville, id_pays, email, observation, ref_clt1, ref_clt2, ref_clt3, nom, prenom, tel, etat_paiement, date_paiement, devise_facture '; - $requete .= 'ORDER BY '; - $requete .= ' acf.date_facture DESC'; - - return $this->_bdd->obtenirTous($requete); - } - - public function obtenirFactureDetails($id) - { - $requete = 'SELECT '; - $requete .= 'afup_compta_facture.*, '; - $requete .= 'afup_compta_facture_details.ref,afup_compta_facture_details.designation,afup_compta_facture_details.quantite,afup_compta_facture_details.pu '; - $requete .= 'FROM '; - $requete .= 'afup_compta_facture, '; - $requete .= 'afup_compta_facture_details '; - $requete .= 'WHERE '; - $requete .= ' numero_facture != "" '; - $requete .= 'afup_compta_facture.id = afup_compta_facture_details.idafup_compta_facture '; - $requete .= 'ORDER BY '; - $requete .= 'compta.date_facture '; - - return $this->_bdd->obtenirTous($requete); - } - - public function obtenir(string $id) - { - $requete = 'SELECT'; - $requete .= ' * '; - $requete .= 'FROM'; - $requete .= ' afup_compta_facture '; - $requete .= 'WHERE id=' . $id; - - return $this->_bdd->obtenirEnregistrement($requete); - } - - public function obtenirParNumeroFacture($numerofacture) - { - $requete = 'SELECT'; - $requete .= ' * '; - $requete .= 'FROM'; - $requete .= ' afup_compta_facture '; - $requete .= 'WHERE numero_facture = ' . $this->_bdd->echapper($numerofacture); - - return $this->_bdd->obtenirEnregistrement($requete); - } - - public function obtenir_details(string $id) - { - $requete = 'SELECT'; - $requete .= ' * '; - $requete .= 'FROM'; - $requete .= ' afup_compta_facture_details '; - $requete .= 'WHERE idafup_compta_facture=' . $id; - - return $this->_bdd->obtenirTous($requete); - } - - public function ajouter($date_devis, $societe, $service, $adresse, $code_postal, $ville, $id_pays, - $nom, $prenom, $tel, $email, $tva_intra, $observation, $ref_clt1, $ref_clt2, $ref_clt3, - $etat_paiement = 0, $date_paiement = null, $devise = 'EUR') - { - $requete = 'INSERT INTO '; - $requete .= 'afup_compta_facture ('; - $requete .= 'date_devis,societe,service,adresse,code_postal,ville,id_pays,'; - $requete .= 'nom,prenom,tel,'; - $requete .= 'email,tva_intra,observation,ref_clt1,ref_clt2,ref_clt3,etat_paiement,date_paiement,numero_devis,devise_facture) '; - $requete .= 'VALUES ('; - $requete .= $this->_bdd->echapper($date_devis) . ','; - $requete .= $this->_bdd->echapper($societe) . ','; - $requete .= $this->_bdd->echapper($service) . ','; - $requete .= $this->_bdd->echapper($adresse) . ','; - $requete .= $this->_bdd->echapper($code_postal) . ','; - $requete .= $this->_bdd->echapper($ville) . ','; - $requete .= $this->_bdd->echapper($id_pays) . ','; - $requete .= $this->_bdd->echapper($nom) . ','; - $requete .= $this->_bdd->echapper($prenom) . ','; - $requete .= $this->_bdd->echapper($tel) . ','; - $requete .= $this->_bdd->echapper($email) . ','; - $requete .= $this->_bdd->echapper($tva_intra) . ','; - $requete .= $this->_bdd->echapper($observation) . ','; - $requete .= $this->_bdd->echapper($ref_clt1) . ','; - $requete .= $this->_bdd->echapper($ref_clt2) . ','; - $requete .= $this->_bdd->echapper($ref_clt3) . ', '; - $requete .= $this->_bdd->echapper($etat_paiement) . ', '; - $requete .= $this->_bdd->echapper($date_paiement) . ', '; - $requete .= $this->_bdd->echapper($this->genererNumeroDevis()) . ', '; - $requete .= $this->_bdd->echapper($devise) . ' '; - $requete .= ');'; - - return $this->_bdd->executer($requete); - } - - public function ajouter_details($id, $ref, $designation, int $quantite, float $pu, int $tva = 0) - { - $requete = 'INSERT INTO '; - $requete .= 'afup_compta_facture_details ('; - $requete .= 'idafup_compta_facture,ref,designation,quantite,pu,tva) '; - $requete .= 'VALUES ('; - $requete .= $id . ','; - $requete .= $this->_bdd->echapper($ref) . ','; - $requete .= $this->_bdd->echapper($designation) . ','; - $requete .= $this->_bdd->echapper($quantite) . ','; - $requete .= $this->_bdd->echapper($pu) . ','; - $requete .= $this->_bdd->echapper($tva) . ' '; - - $requete .= ');'; - - return $this->_bdd->executer($requete); - } - - public function modifier($id, $date_devis, $societe, $service, $adresse, $code_postal, $ville, $id_pays, - $nom, $prenom, $tel, $email, $tva_intra, $observation, $ref_clt1, $ref_clt2, $ref_clt3, - $numero_devis, $numero_facture, $etat_paiement, $date_paiement, $devise) - { - $requete = 'UPDATE '; - $requete .= 'afup_compta_facture '; - $requete .= 'SET '; - $requete .= 'date_devis=' . $this->_bdd->echapper($date_devis) . ','; - $requete .= 'societe=' . $this->_bdd->echapper($societe) . ','; - $requete .= 'service=' . $this->_bdd->echapper($service) . ','; - $requete .= 'adresse=' . $this->_bdd->echapper($adresse) . ','; - $requete .= 'code_postal=' . $this->_bdd->echapper($code_postal) . ','; - $requete .= 'ville=' . $this->_bdd->echapper($ville) . ','; - $requete .= 'id_pays=' . $this->_bdd->echapper($id_pays) . ','; - $requete .= 'nom=' . $this->_bdd->echapper($nom) . ','; - $requete .= 'prenom=' . $this->_bdd->echapper($prenom) . ','; - $requete .= 'tel=' . $this->_bdd->echapper($tel) . ','; - $requete .= 'email=' . $this->_bdd->echapper($email) . ','; - $requete .= 'tva_intra=' . $this->_bdd->echapper($tva_intra) . ','; - $requete .= 'observation=' . $this->_bdd->echapper($observation) . ', '; - $requete .= 'ref_clt1=' . $this->_bdd->echapper($ref_clt1) . ','; - $requete .= 'ref_clt2=' . $this->_bdd->echapper($ref_clt2) . ','; - $requete .= 'ref_clt3=' . $this->_bdd->echapper($ref_clt3) . ', '; - $requete .= 'etat_paiement=' . $this->_bdd->echapper($etat_paiement) . ', '; - $requete .= 'date_paiement=' . $this->_bdd->echapper($date_paiement) . ', '; - $requete .= 'numero_devis=' . $this->_bdd->echapper($numero_devis) . ', '; - $requete .= 'devise_facture=' . $this->_bdd->echapper($devise) . ' '; - - if ($numero_facture) { - $requete .= ', '; - $requete .= 'numero_facture=' . $this->_bdd->echapper($numero_facture) . ' '; - } - $requete .= 'WHERE '; - $requete .= 'id=' . $this->_bdd->echapper($id) . ' '; - - return $this->_bdd->executer($requete); - } - - public function modifier_details(string $id, $ref, $designation, int $quantite, float $pu, int $tva = 0) - { - if (!is_numeric($id)) { - throw new \RuntimeException('STOP ! L\'id de la ligne `afup_compta_facture_details` est invalide (' . $id . ').'); - } - - $requete = 'UPDATE '; - $requete .= 'afup_compta_facture_details '; - $requete .= 'SET '; - $requete .= 'ref=' . $this->_bdd->echapper($ref) . ','; - $requete .= 'designation=' . $this->_bdd->echapper($designation) . ','; - $requete .= 'quantite=' . $this->_bdd->echapper($quantite) . ','; - $requete .= 'pu=' . $this->_bdd->echapper($pu) . ','; - $requete .= 'tva=' . $this->_bdd->echapper($tva) . ' '; - $requete .= 'WHERE '; - $requete .= 'id=' . $id . ' '; - - return $this->_bdd->executer($requete); - } - - public function obtenirDernier() - { - /** - * @TODO ne supporte pas les enregistrements concurrents ! - */ - $requete = 'SELECT MAX(id)'; - $requete .= 'FROM'; - $requete .= ' afup_compta_facture '; - - return $this->_bdd->obtenirUn($requete); - } - - - public function transfertDevis($numero_devis) - { - $numero_facture = $this->genererNumeroFacture(); - - $requete = 'UPDATE '; - $requete .= 'afup_compta_facture '; - $requete .= 'SET '; - $requete .= 'date_facture=' . $this->_bdd->echapper(date('Y-m-d')) . ','; - $requete .= 'numero_facture=' . $this->_bdd->echapper($numero_facture) . ' '; - $requete .= 'WHERE '; - $requete .= 'numero_devis=' . $this->_bdd->echapper($numero_devis) . ' '; - - return $this->_bdd->executer($requete); - } - - public function genererNumeroFacture(): string - { - $year = (int) date('Y'); - - $sql = "SELECT MAX(CAST(SUBSTRING_INDEX(numero_facture, '-', -1) AS UNSIGNED)) + 1 - FROM afup_compta_facture - WHERE LEFT(numero_facture, 4)="; - $index = $this->_bdd->obtenirUn($sql . $year); - - // index null = changement d'année - // on va chercher l'index de l'année dernière - if (null === $index) { - $index = $this->_bdd->obtenirUn($sql . ($year - 1)); - $index = (int) (is_null($index) ? 1 : $index); - } - - return "$year-$index"; - } - - public function genererNumeroDevis(): string - { - $requete = 'SELECT'; - $requete .= " MAX(CAST(SUBSTRING_INDEX(numero_devis, '-', -1) AS UNSIGNED)) + 1 "; - $requete .= 'FROM'; - $requete .= ' afup_compta_facture '; - $requete .= 'WHERE'; - $requete .= ' LEFT(numero_devis, 4)=' . $this->_bdd->echapper(date('Y')); - - $index = $this->_bdd->obtenirUn($requete); - return date('Y') . '-' . sprintf('%02d', (is_null($index) ? 1 : $index)); - } - - - public function genererDevis(string $reference, $chemin = null): void - { - $requete = 'SELECT * FROM afup_compta_facture WHERE numero_devis=' . $this->_bdd->echapper($reference); - $coordonnees = $this->_bdd->obtenirEnregistrement($requete); - - $requete = 'SELECT * FROM afup_compta_facture_details WHERE idafup_compta_facture=' . $this->_bdd->echapper($coordonnees['id']); - $details = $this->_bdd->obtenirTous($requete); - - $dateDevis = isset($coordonnees['date_devis']) && !empty($coordonnees['date_devis']) - ? \DateTimeImmutable::createFromFormat('Y-m-d', (string) $coordonnees['date_devis']) - : new \DateTimeImmutable(); - - $isSubjectedToVat = Vat::isSubjectedToVat($dateDevis); - - $bankAccountFactory = new BankAccountFactory(); - // Construction du PDF - $pdf = new PDF_Facture($bankAccountFactory->createApplyableAt($dateDevis), $isSubjectedToVat); - $pdf->AddPage(); - - $pdf->Cell(130, 5); - $pdf->Cell(60, 5, 'Le ' . $dateDevis->format('d/m/Y')); - - $pdf->Ln(); - $pdf->Ln(); - $pdf->Ln(); - - - // A l'attention du client [adresse] - $pdf->SetFont('Arial', '', 10); - $pdf->Ln(10); - $pdf->setx(120); - $pdf->MultiCell(130, 5, $coordonnees['societe'] . "\n" - . $coordonnees['service'] . "\n" - . $coordonnees['adresse'] . "\n" - . $coordonnees['code_postal'] . " " - . $coordonnees['ville'] . "\n" - . $this->pays->obtenirNom($coordonnees['id_pays']) - . ($coordonnees['tva_intra'] ? ("\n" . 'N° TVA Intracommunautaire : ' . $coordonnees['tva_intra']) : null), - ); - - $pdf->Ln(10); - $pdf->SetFont('Arial', 'BU', 10); - $pdf->Cell(0, 5, 'Devis n° ' . $reference, 0, 0, "C"); - $pdf->SetFont('Arial', '', 10); - if ($coordonnees['ref_clt1'] || $coordonnees['ref_clt2'] || $coordonnees['ref_clt3']) { - $pdf->Ln(15); - $pdf->Cell(40, 5, 'Repère(s) : '); - } - - if ($coordonnees['ref_clt1']) { - $pdf->setx(30); - $pdf->Cell(100, 5, $coordonnees['ref_clt1']); - $pdf->Ln(5); - } - if ($coordonnees['ref_clt2']) { - $pdf->setx(30); - $pdf->Cell(100, 5, $coordonnees['ref_clt2']); - $pdf->Ln(5); - } - if ($coordonnees['ref_clt3']) { - $pdf->setx(30); - $pdf->Cell(100, 5, $coordonnees['ref_clt3']); - $pdf->Ln(5); - } - $pdf->Ln(10); - - $pdf->MultiCell(180, 5, "Comme convenu, nous vous prions de trouver votre devis"); - - // Cadre - $pdf->Ln(5); - $pdf->SetFillColor(200, 200, 200); - $pdf->Cell(30, 5, 'Type', 1, 0, 'L', 1); - $pdf->Cell($isSubjectedToVat ? 60 : 80, 5, 'Description', 1, 0, 'L', 1); - $pdf->Cell(20, 5, 'Quantite', 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - if ($isSubjectedToVat) { - $pdf->Cell(20, 5, 'TVA', 1, 0, 'C', 1); - } - $pdf->Cell(30, 5, 'Prix' . ($isSubjectedToVat ? ' HT' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - $pdf->Cell(30, 5, 'Total' . ($isSubjectedToVat ? ' TTC' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - - $totalTtc = 0; - $totalHt = 0; - $devise = match ($coordonnees['devise_facture']) { - 'DOL' => ' $', - default => ' €', - }; - - $yInitial = $pdf->getY(); - - $vatAmounts = []; - - foreach ($details as $detail) { - if ($detail['quantite'] != 0) { - $montantHt = $detail['quantite'] * $detail['pu']; - $montantTtc = $montantHt; - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - - if ($isSubjectedToVat) { - $x = $pdf->GetX(); - $y = $pdf->GetY(); - - $pdf->MultiCell(30, 5, $detail['ref'], 'T'); - $x += 30; - $pdf->SetXY($x, $y); - - $pdf->MultiCell(60, 5, $detail['designation'], 'T'); - - $x += 60; - $pdf->SetXY($x, $y); - $pdf->MultiCell(20, 5, $detail['quantite'], 'T', 0, "C"); - - $x += 20; - $pdf->SetXY($x, $y); - $pdf->MultiCell(20, 5, $detail['tva'] . '%', 'T', 'C', "C"); - if (!isset($vatAmounts[$detail['tva']])) { - $vatAmounts[$detail['tva']] = 0; - } - $vatAmounts[$detail['tva']] += ($detail['tva'] / 100) * $montantTtc; - $montantTtc *= 1 + ($detail['tva'] / 100); - - $x += 20; - $pdf->SetXY($x, $y); - - $pdf->MultiCell(30, 5, $this->formatFactureValue($detail['pu'], $isSubjectedToVat) . $devise, 'T', 0, "R"); - - $x += 30; - $pdf->SetXY($x, $y); - $pdf->MultiCell(30, 5, $this->formatFactureValue($montantTtc, $isSubjectedToVat) . $devise, 'T', 0, "R"); - } else { - $pdf->Cell(30, 5, $detail['ref'], 1); - $pdf->Cell(80, 5, $detail['designation'], 1); - $pdf->Cell(20, 5, $detail['quantite'], 1, 0, "C"); - $pdf->Cell(30, 5, $detail['pu'] . $devise, 1, 0, "R"); - $pdf->Cell(30, 5, $montantHt . $devise, 1, 0, "R"); - } - - $totalHt += $montantHt; - $totalTtc += $montantTtc; - } - } - - if ($isSubjectedToVat) { - $columns = [0, 30, 90, 110, 130, 160, 190]; - - foreach ($columns as $column) { - $pdf->Line($pdf->GetX() + $column, $yInitial, $pdf->GetX() + $column, $pdf->GetY()); - } - - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(160, 5, 'TOTAL HT', 1, 0, 'R', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($totalHt, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); - $pdf->Ln(5); - - foreach ($vatAmounts as $vat => $amount) { - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(160, 5, 'Total TVA ' . $vat . '%', 1, 0, 'R', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($amount, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); - $pdf->Ln(5); - } - } else { - $pdf->ln(); - } - - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(160, 5, 'TOTAL' . ($isSubjectedToVat ? ' TTC' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($totalTtc, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); - - $pdf->Ln(15); - if (!$isSubjectedToVat) { - $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); - } - $pdf->Ln(10); - $pdf->Cell(10, 5, 'Observations : '); - $pdf->Ln(5); - $pdf->SetFont('Arial', '', 8); - $pdf->MultiCell(130, 5, $coordonnees['observation']); - - if (is_null($chemin)) { - $pdf->Output('Devis - ' . $coordonnees['societe'] . ' - ' . $coordonnees['date_devis'] . '.pdf', 'D', true); - exit(0); - } else { - $pdf->Output($chemin, 'F', true); - } - } - - - public function genererFacture(string $reference, $chemin = null): void - { - $requete = 'SELECT * FROM afup_compta_facture WHERE numero_facture=' . $this->_bdd->echapper($reference); - $coordonnees = $this->_bdd->obtenirEnregistrement($requete); - - $requete = 'SELECT * FROM afup_compta_facture_details WHERE idafup_compta_facture=' . $this->_bdd->echapper($coordonnees['id']); - $details = $this->_bdd->obtenirTous($requete); - - $dateFacture = isset($coordonnees['date_facture']) && !empty($coordonnees['date_facture']) - ? \DateTimeImmutable::createFromFormat('Y-m-d', (string) $coordonnees['date_facture']) - : new \DateTimeImmutable(); - - $bankAccountFactory = new BankAccountFactory(); - - $isSubjectedToVat = Vat::isSubjectedToVat($dateFacture); - - // 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(); - - - // À l'attention du client [adresse] - $pdf->SetFont('Arial', '', 10); - $pdf->Ln(10); - $pdf->setx(120); - $pdf->MultiCell(130, 5, $coordonnees['societe'] . "\n" - . $coordonnees['service'] . "\n" - . $coordonnees['adresse'] . "\n" - . $coordonnees['code_postal'] . "\n" - . $coordonnees['ville'] . "\n" - . $this->pays->obtenirNom($coordonnees['id_pays']) - . ($coordonnees['tva_intra'] ? ("\n" . 'N° TVA Intracommunautaire : ' . $coordonnees['tva_intra']) : null)); - - $pdf->Ln(10); - $pdf->SetFont('Arial', 'BU', 10); - $pdf->Cell(0, 5, 'Facture n° ' . $reference, 0, 0, "C"); - $pdf->SetFont('Arial', '', 10); - - if ($coordonnees['ref_clt1'] || $coordonnees['ref_clt2'] || $coordonnees['ref_clt3']) { - $pdf->Ln(15); - $pdf->Cell(40, 5, 'Repère(s) : '); - } - if ($coordonnees['ref_clt1']) { - $pdf->setx(30); - $pdf->Cell(100, 5, $coordonnees['ref_clt1']); - $pdf->Ln(5); - } - if ($coordonnees['ref_clt2']) { - $pdf->setx(30); - $pdf->Cell(100, 5, $coordonnees['ref_clt2']); - $pdf->Ln(5); - } - if ($coordonnees['ref_clt3']) { - $pdf->setx(30); - $pdf->Cell(100, 5, $coordonnees['ref_clt3']); - $pdf->Ln(5); - } - $pdf->Ln(10); - - $pdf->MultiCell(180, 5, "Comme convenu, nous vous prions de trouver votre facture"); - // Cadre - $pdf->Ln(5); - $pdf->SetFillColor(200, 200, 200); - $pdf->Cell(30, 5, 'Type', 1, 0, 'L', 1); - $pdf->Cell($isSubjectedToVat ? 60 : 80, 5, 'Description', 1, 0, 'L', 1); - $pdf->Cell(20, 5, 'Quantite', 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - if ($isSubjectedToVat) { - $pdf->Cell(20, 5, 'TVA', 1, 0, 'C', 1); - } - $pdf->Cell(30, 5, 'Prix' . ($isSubjectedToVat ? ' HT' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - $pdf->Cell(30, 5, 'Total' . ($isSubjectedToVat ? ' TTC' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - - $totalTtc = 0; - $totalHt = 0; - $devise = match ($coordonnees['devise_facture']) { - 'DOL' => ' $', - default => ' €', - }; - $yInitial = $pdf->getY(); - $columns = $isSubjectedToVat ? [0, 30, 90, 110, 130, 160, 190] : [0, 30, 110, 130, 160, 190]; - - $vatAmounts = []; - - foreach ($details as $detail) { - if ($detail['quantite'] != 0) { - $montantHt = $detail['quantite'] * $detail['pu']; - $montantTtc = $montantHt; - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - - $y = $pdf->GetY(); - $x = $pdf->GetX(); - - $pdf->MultiCell(30, 5, $detail['ref'], 'T'); - $x += 30; - $pdf->SetXY($x, $y); - $designationLength = $isSubjectedToVat ? 60 : 80; - $pdf->MultiCell($designationLength, 5, $detail['designation'], 'T'); - - $x += $designationLength; - $pdf->SetXY($x, $y); - $pdf->MultiCell(20, 5, $detail['quantite'], 'T', 0, "C"); - - if ($isSubjectedToVat) { - $x += 20; - $pdf->SetXY($x, $y); - $pdf->MultiCell(20, 5, $detail['tva'] . '%', 'T', 'C', "C"); - if (!isset($vatAmounts[$detail['tva']])) { - $vatAmounts[$detail['tva']] = 0; - } - $vatAmounts[$detail['tva']] += ($detail['tva'] / 100) * $montantTtc; - $montantTtc *= 1 + ($detail['tva'] / 100); - } - - $x += 20; - $pdf->SetXY($x, $y); - - $pdf->MultiCell(30, 5, $this->formatFactureValue($detail['pu'], $isSubjectedToVat) . $devise, 'T', 0, "R"); - - $x += 30; - $pdf->SetXY($x, $y); - $pdf->MultiCell(30, 5, $this->formatFactureValue($montantTtc, $isSubjectedToVat) . $devise, 'T', 0, "R"); - - $totalHt += $montantHt; - $totalTtc += $montantTtc; - } - } - - $pdf->Ln(); - - foreach ($columns as $column) { - $pdf->Line($pdf->GetX() + $column, $yInitial, $pdf->GetX() + $column, $pdf->GetY()); - } - - if ($isSubjectedToVat) { - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(160, 5, 'TOTAL HT', 1, 0, 'R', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($totalHt, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); - $pdf->Ln(5); - - foreach ($vatAmounts as $vat => $amount) { - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(160, 5, 'Total TVA ' . $vat . '%', 1, 0, 'R', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($amount, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); - $pdf->Ln(5); - } - } - - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(160, 5, 'TOTAL' . ($isSubjectedToVat ? ' TTC' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); - $pdf->Cell(30, 5, $this->formatFactureValue($totalTtc, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); - $pdf->Ln(15); - - if (!$isSubjectedToVat) { - $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); - } - - $pdf->Ln(); - $pdf->Cell(10, 5, 'Payable à réception.'); - if ($dateFacture >= new \DateTime('2025-01-01')) { - $pdf->Ln(); - $pdf->MultiCell(190, 5, "Pénalités pour retard de paiement, 3 fois le taux d'intérêt légal sur les sommes dues. -Indemnité forfaitaire pour frais de recouvrement de 40€. -Pas d'escompte en cas de paiement anticipé. -"); - } - - $pdf->Ln(10); - if ($coordonnees['observation']) { - $pdf->Cell(10, 5, 'Observations : '); - $pdf->Ln(5); - $pdf->SetFont('Arial', '', 8); - $pdf->MultiCell(130, 5, $coordonnees['observation']); - } - - if (is_null($chemin)) { - $pdf->Output('Facture - ' . $coordonnees['societe'] . ' - ' . $coordonnees['date_facture'] . '.pdf', 'D', true); - exit(0); - } else { - $pdf->Output($chemin, 'F', true); - } - } - - private function formatFactureValue($value, bool $isSubjectedToVat) - { - if (!$isSubjectedToVat) { - return $value; - } - - return number_format((float) $value, 2, ',', ' '); - } - - /** - * Envoi par mail d'une facture au format PDF - * - * @param string $reference Reference de la facturation - * @return bool Succès de l'envoi - */ - public function envoyerFacture(string $reference) - { - $personne = $this->obtenirParNumeroFacture($reference); - - $sujet = "Facture AFUP\n"; - - $corps = "Bonjour, \n\n"; - $corps .= "Veuillez trouver ci-joint la facture correspondant à la participation au forum organisé par l'AFUP.\n"; - $corps .= "Nous restons à votre disposition pour toute demande complémentaire.\n\n"; - $corps .= "Le bureau\n\n"; - $corps .= AFUP_RAISON_SOCIALE . "\n"; - $corps .= AFUP_ADRESSE . "\n"; - $corps .= AFUP_CODE_POSTAL . " " . AFUP_VILLE . "\n"; - - $chemin_facture = AFUP_CHEMIN_RACINE . 'cache' . DIRECTORY_SEPARATOR . 'fact' . $reference . '.pdf'; - $this->genererFacture($reference, $chemin_facture); - - $message = new Message($sujet, new MailUser(MailUser::DEFAULT_SENDER_EMAIL, MailUser::DEFAULT_SENDER_NAME), new MailUser($personne['email'], $personne['nom'])); - $message->addAttachment(new Attachment( - $chemin_facture, - 'facture-' . $reference . '.pdf', - 'base64', - 'application/pdf', - )); - $ok = Mailing::envoyerMail($message, $corps); - - @unlink($chemin_facture); - - return $ok; - } -} diff --git a/sources/AppBundle/Accounting/InvoicingMailer.php b/sources/AppBundle/Accounting/InvoicingMailer.php new file mode 100644 index 000000000..72be76405 --- /dev/null +++ b/sources/AppBundle/Accounting/InvoicingMailer.php @@ -0,0 +1,51 @@ +getInvoiceNumber(); + + $sujet = "Facture AFUP\n"; + + $corps = "Bonjour, \n\n"; + $corps .= "Veuillez trouver ci-joint la facture correspondant à la participation au forum organisé par l'AFUP.\n"; + $corps .= "Nous restons à votre disposition pour toute demande complémentaire.\n\n"; + $corps .= "Le bureau\n\n"; + $corps .= AFUP_RAISON_SOCIALE . "\n"; + $corps .= AFUP_ADRESSE . "\n"; + $corps .= AFUP_CODE_POSTAL . ' ' . AFUP_VILLE . "\n"; + + $cheminFacture = AFUP_CHEMIN_RACINE . 'cache' . DIRECTORY_SEPARATOR . 'fact' . $invoiceNumber . '.pdf'; + $this->pdfGenerator->generateInvoice($invoicing, $cheminFacture); + + $message = new Message( + $sujet, + new MailUser(MailUser::DEFAULT_SENDER_EMAIL, MailUser::DEFAULT_SENDER_NAME), + new MailUser($invoicing->getEmail(), $invoicing->getLastname()), + ); + $message->addAttachment(new Attachment( + $cheminFacture, + 'facture-' . $invoiceNumber . '.pdf', + 'base64', + 'application/pdf', + )); + $ok = Mailing::envoyerMail($message, $corps); + + @unlink($cheminFacture); + + return $ok; + } +} diff --git a/sources/AppBundle/Accounting/InvoicingNumberGenerator.php b/sources/AppBundle/Accounting/InvoicingNumberGenerator.php new file mode 100644 index 000000000..4de1ff32c --- /dev/null +++ b/sources/AppBundle/Accounting/InvoicingNumberGenerator.php @@ -0,0 +1,35 @@ +repository->getNextInvoiceIndex($year); + + if ($index === null) { + $index = $this->repository->getNextInvoiceIndex($year - 1); + $index = $index ?? 1; + } + + return "$year-$index"; + } + + public function generateQuotationNumber(): string + { + $year = (int) date('Y'); + + $index = $this->repository->getNextQuotationIndex($year); + + return date('Y') . '-' . sprintf('%02d', $index ?? 1); + } +} diff --git a/sources/AppBundle/Accounting/InvoicingPdfGenerator.php b/sources/AppBundle/Accounting/InvoicingPdfGenerator.php new file mode 100644 index 000000000..4acd328e3 --- /dev/null +++ b/sources/AppBundle/Accounting/InvoicingPdfGenerator.php @@ -0,0 +1,299 @@ +getInvoiceDate() !== null + ? \DateTimeImmutable::createFromMutable($invoicing->getInvoiceDate()) + : new \DateTimeImmutable(); + + $isSubjectedToVat = Vat::isSubjectedToVat($date); + $pdf = $this->buildPdf($date, $isSubjectedToVat); + $pdf->AddPage(); + + $this->renderHeader($pdf, $date->format('d/m/Y')); + $this->renderRecipient($pdf, $invoicing); + + $pdf->SetFont('Arial', 'BU', 10); + $pdf->Cell(0, 5, 'Facture n° ' . $invoicing->getInvoiceNumber(), 0, 0, 'C'); + $pdf->SetFont('Arial', '', 10); + + $this->renderClientReferences($pdf, $invoicing); + + $pdf->MultiCell(180, 5, 'Comme convenu, nous vous prions de trouver votre facture'); + + $devise = $this->currencySymbol($invoicing); + [$totalHt, $totalTtc, $vatAmounts] = $this->renderLineItems($pdf, $invoicing->getDetails(), $isSubjectedToVat, $devise, true); + + $this->renderTotals($pdf, $isSubjectedToVat, $totalHt, $totalTtc, $vatAmounts, $devise); + + $pdf->Ln(15); + if (!$isSubjectedToVat) { + $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); + } + + $pdf->Ln(); + $pdf->Cell(10, 5, 'Payable à réception.'); + if ($date >= new \DateTimeImmutable('2025-01-01')) { + $pdf->Ln(); + $pdf->MultiCell(190, 5, "Pénalités pour retard de paiement, 3 fois le taux d'intérêt légal sur les sommes dues.\nIndemnité forfaitaire pour frais de recouvrement de 40€.\nPas d'escompte en cas de paiement anticipé.\n"); + } + + $pdf->Ln(10); + if ($invoicing->getObservation() !== '') { + $pdf->Cell(10, 5, 'Observations : '); + $pdf->Ln(5); + $pdf->SetFont('Arial', '', 8); + $pdf->MultiCell(130, 5, $invoicing->getObservation()); + } + + $this->output($pdf, $path, 'Facture - ' . $invoicing->getCompany() . ' - ' . ($invoicing->getInvoiceDate() ? $invoicing->getInvoiceDate()->format('Y-m-d') : '') . '.pdf'); + } + + public function generateQuotation(Invoicing $invoicing, ?string $path = null): void + { + $date = $invoicing->getQuotationDate() !== null + ? \DateTimeImmutable::createFromMutable($invoicing->getQuotationDate()) + : new \DateTimeImmutable(); + + $isSubjectedToVat = Vat::isSubjectedToVat($date); + $pdf = $this->buildPdf($date, $isSubjectedToVat); + $pdf->AddPage(); + + $this->renderHeader($pdf, $date->format('d/m/Y')); + $this->renderRecipient($pdf, $invoicing); + + $pdf->SetFont('Arial', 'BU', 10); + $pdf->Cell(0, 5, 'Devis n° ' . $invoicing->getQuotationNumber(), 0, 0, 'C'); + $pdf->SetFont('Arial', '', 10); + + $this->renderClientReferences($pdf, $invoicing); + + $pdf->MultiCell(180, 5, 'Comme convenu, nous vous prions de trouver votre devis'); + + $devise = $this->currencySymbol($invoicing); + [$totalHt, $totalTtc, $vatAmounts] = $this->renderLineItems($pdf, $invoicing->getDetails(), $isSubjectedToVat, $devise, false); + + $this->renderTotals($pdf, $isSubjectedToVat, $totalHt, $totalTtc, $vatAmounts, $devise); + + $pdf->Ln(15); + if (!$isSubjectedToVat) { + $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); + } + $pdf->Ln(10); + $pdf->Cell(10, 5, 'Observations : '); + $pdf->Ln(5); + $pdf->SetFont('Arial', '', 8); + $pdf->MultiCell(130, 5, $invoicing->getObservation()); + + $this->output($pdf, $path, 'Devis - ' . $invoicing->getCompany() . ' - ' . ($invoicing->getQuotationDate() ? $invoicing->getQuotationDate()->format('Y-m-d') : '') . '.pdf'); + } + + private function buildPdf(\DateTimeImmutable $date, bool $isSubjectedToVat): PDF_Facture + { + $bankAccountFactory = new BankAccountFactory(); + return new PDF_Facture($bankAccountFactory->createApplyableAt($date), $isSubjectedToVat); + } + + private function renderHeader(PDF_Facture $pdf, string $formattedDate): void + { + $pdf->Cell(130, 5); + $pdf->Cell(60, 5, 'Le ' . $formattedDate); + $pdf->Ln(); + $pdf->Ln(); + $pdf->Ln(); + } + + private function renderRecipient(PDF_Facture $pdf, Invoicing $invoicing): void + { + $pdf->SetFont('Arial', '', 10); + $pdf->Ln(10); + $pdf->setx(120); + $pdf->MultiCell(130, 5, + $invoicing->getCompany() . "\n" + . $invoicing->getService() . "\n" + . $invoicing->getAddress() . "\n" + . $invoicing->getZipcode() . ' ' + . $invoicing->getCity() . "\n" + . $this->pays->obtenirNom($invoicing->getCountryId()) + . ($invoicing->getTvaIntra() ? ("\nN° TVA Intracommunautaire : " . $invoicing->getTvaIntra()) : ''), + ); + $pdf->Ln(10); + } + + private function renderClientReferences(PDF_Facture $pdf, Invoicing $invoicing): void + { + if ($invoicing->getRefClt1() !== '' || $invoicing->getRefClt2() !== '' || $invoicing->getRefClt3() !== '') { + $pdf->Ln(15); + $pdf->Cell(40, 5, 'Repère(s) : '); + } + foreach ([$invoicing->getRefClt1(), $invoicing->getRefClt2(), $invoicing->getRefClt3()] as $ref) { + if ($ref !== '') { + $pdf->setx(30); + $pdf->Cell(100, 5, $ref); + $pdf->Ln(5); + } + } + $pdf->Ln(10); + } + + /** + * Renders the line-items table and returns [totalHt, totalTtc, vatAmounts]. + * + * @param InvoicingDetail[] $details + * @return array{float, float, array} + */ + private function renderLineItems(PDF_Facture $pdf, array $details, bool $isSubjectedToVat, string $devise, bool $drawColumnLines): array + { + $pdf->Ln(5); + $pdf->SetFillColor(200, 200, 200); + $pdf->Cell(30, 5, 'Type', 1, 0, 'L', 1); + $pdf->Cell($isSubjectedToVat ? 60 : 80, 5, 'Description', 1, 0, 'L', 1); + $pdf->Cell(20, 5, 'Quantite', 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + if ($isSubjectedToVat) { + $pdf->Cell(20, 5, 'TVA', 1, 0, 'C', 1); + } + $pdf->Cell(30, 5, 'Prix' . ($isSubjectedToVat ? ' HT' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + $pdf->Cell(30, 5, 'Total' . ($isSubjectedToVat ? ' TTC' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + + $totalHt = 0.0; + $totalTtc = 0.0; + $vatAmounts = []; + $yInitial = $pdf->getY(); + $columns = $isSubjectedToVat ? [0, 30, 90, 110, 130, 160, 190] : [0, 30, 110, 130, 160, 190]; + + foreach ($details as $detail) { + if ((float) $detail->getQuantity() === 0.0) { + continue; + } + + $montantHt = $detail->getQuantity() * $detail->getUnitPrice(); + $montantTtc = $montantHt; + + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + + if (!$drawColumnLines && !$isSubjectedToVat) { + $pdf->Cell(30, 5, $detail->getReference(), 1); + $pdf->Cell(80, 5, $detail->getDesignation(), 1); + $pdf->Cell(20, 5, number_format((float) $detail->getQuantity(), 2, '.', ''), 1, 0, 'C'); + $pdf->Cell(30, 5, number_format($detail->getUnitPrice(), 2, '.', '') . $devise, 1, 0, 'R'); + $pdf->Cell(30, 5, $this->formatValue($montantHt, false) . $devise, 1, 0, 'R'); + } else { + $y = $pdf->GetY(); + $x = $pdf->GetX(); + + $pdf->MultiCell(30, 5, $detail->getReference(), 'T'); + $x += 30; + $pdf->SetXY($x, $y); + + $designationLength = $isSubjectedToVat ? 60 : 80; + $pdf->MultiCell($designationLength, 5, $detail->getDesignation(), 'T'); + $x += $designationLength; + $pdf->SetXY($x, $y); + + $pdf->MultiCell(20, 5, number_format((float) $detail->getQuantity(), 2, '.', ''), 'T', 0, 'C'); + $x += 20; + + if ($isSubjectedToVat) { + $pdf->SetXY($x, $y); + $tva = (float) $detail->getTva(); + $pdf->MultiCell(20, 5, number_format($tva, 2, '.', '') . '%', 'T', 'C', 'C'); + $tvaKey = (string) $tva; + $vatAmounts[$tvaKey] = ($vatAmounts[$tvaKey] ?? 0.0) + ($tva / 100) * $montantTtc; + $montantTtc *= 1 + ($tva / 100); + $x += 20; + } + + $pdf->SetXY($x, $y); + $unitPrice = $isSubjectedToVat + ? $this->formatValue($detail->getUnitPrice(), true) + : number_format($detail->getUnitPrice(), 2, '.', ''); + $pdf->MultiCell(30, 5, $unitPrice . $devise, 'T', 0, 'R'); + $x += 30; + $pdf->SetXY($x, $y); + $pdf->MultiCell(30, 5, $this->formatValue($montantTtc, $isSubjectedToVat) . $devise, 'T', 0, 'R'); + } + + $totalHt += $montantHt; + $totalTtc += $montantTtc; + } + + $pdf->Ln(); + + if ($drawColumnLines) { + foreach ($columns as $column) { + $pdf->Line($pdf->GetX() + $column, $yInitial, $pdf->GetX() + $column, $pdf->GetY()); + } + } elseif ($isSubjectedToVat) { + $columns = [0, 30, 90, 110, 130, 160, 190]; + foreach ($columns as $column) { + $pdf->Line($pdf->GetX() + $column, $yInitial, $pdf->GetX() + $column, $pdf->GetY()); + } + } + + return [$totalHt, $totalTtc, $vatAmounts]; + } + + /** + * @param array $vatAmounts + */ + private function renderTotals(PDF_Facture $pdf, bool $isSubjectedToVat, float $totalHt, float $totalTtc, array $vatAmounts, string $devise): void + { + if ($isSubjectedToVat) { + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(160, 5, 'TOTAL HT', 1, 0, 'R', 1); + $pdf->Cell(30, 5, $this->formatValue($totalHt, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); + $pdf->Ln(5); + + foreach ($vatAmounts as $vat => $amount) { + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(160, 5, 'Total TVA ' . number_format((float) $vat, 2, '.', '') . '%', 1, 0, 'R', 1); + $pdf->Cell(30, 5, $this->formatValue($amount, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); + $pdf->Ln(5); + } + } + + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(160, 5, 'TOTAL' . ($isSubjectedToVat ? ' TTC' : ''), 1, 0, $isSubjectedToVat ? 'R' : 'L', 1); + $pdf->Cell(30, 5, $this->formatValue($totalTtc, $isSubjectedToVat) . $devise, 1, 0, 'R', 1); + } + + private function formatValue(float $value, bool $isSubjectedToVat): string + { + if (!$isSubjectedToVat) { + return (string) $value; + } + + return number_format($value, 2, ',', ' '); + } + + private function currencySymbol(Invoicing $invoicing): string + { + return $invoicing->getCurrency() === InvoicingCurrency::Dollar ? ' $' : ' €'; + } + + private function output(PDF_Facture $pdf, ?string $path, string $filename): void + { + if ($path === null) { + $pdf->Output($filename, 'D', true); + exit(0); + } + + $pdf->Output($path, 'F', true); + } +} diff --git a/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php b/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php index 78d2855e3..8ffdcafc6 100644 --- a/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php +++ b/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php @@ -15,6 +15,7 @@ use Aura\SqlQuery\Mysql\Select; use CCMBenchmark\Ting\Repository\CollectionInterface; use CCMBenchmark\Ting\Repository\HydratorSingleObject; +use CCMBenchmark\Ting\Repository\HydratorArray; use CCMBenchmark\Ting\Repository\Metadata; use CCMBenchmark\Ting\Repository\MetadataInitializer; use CCMBenchmark\Ting\Repository\Repository; @@ -28,30 +29,7 @@ class InvoicingRepository extends Repository implements MetadataInitializer { public function getById(int $id): ?Invoicing { - /** @var Select $builder */ - $builder = $this->getQueryBuilder(self::QUERY_SELECT); - $builder->cols(['acf.*', 'acfd.*']) - ->from('afup_compta_facture acf') - ->leftJoin('afup_compta_facture_details acfd', 'acfd.idafup_compta_facture = acf.id') - ->where('acf.id = :id'); - - $hydrator = new HydratorRelational(); - $hydrator->addRelation(new RelationMany(new AggregateFrom('acfd'), new AggregateTo('acf'), 'setDetails')); - $hydrator->callableFinalizeAggregate(fn(array $row) => $row['acf']); - - $collection = $this->getQuery($builder->getStatement()) - ->setParams(['id' => $id]) - ->query($this->getCollection($hydrator)); - - if ($collection->count() === 0) { - return null; - } - - /** @var Invoicing $entity */ - $entity = $collection->first(); - $entity->setDetails(array_values($entity->getDetails())); - - return $entity; + return $this->findOneWithDetails('acf.id = :id', ['id' => $id]); } public function getQuotationsByPeriodId(?int $periodId = null, string $sort = 'date', string $direction = 'desc'): CollectionInterface @@ -116,6 +94,77 @@ public function getInvoicesByPeriodId(?int $periodId = null, string $sort = 'dat ->query($this->getCollection($hydrator)); } + public function getOneByInvoiceNumber(string $number): ?Invoicing + { + return $this->findOneWithDetails('acf.numero_facture = :number', ['number' => $number]); + } + + public function getOneByQuotationNumber(string $number): ?Invoicing + { + return $this->findOneWithDetails('acf.numero_devis = :number', ['number' => $number]); + } + + public function getNextInvoiceIndex(int $year): ?int + { + $sql = 'SELECT MAX(CAST(SUBSTRING_INDEX(numero_facture, \'-\', -1) AS UNSIGNED)) + 1 AS next_index + FROM afup_compta_facture + WHERE LEFT(numero_facture, 4) = :year'; + + $rows = iterator_to_array( + $this->getQuery($sql)->setParams(['year' => (string) $year])->query($this->getCollection(new HydratorArray())), + ); + + return isset($rows[0]['next_index']) ? (int) $rows[0]['next_index'] : null; + } + + public function getNextQuotationIndex(int $year): ?int + { + $sql = 'SELECT MAX(CAST(SUBSTRING_INDEX(numero_devis, \'-\', -1) AS UNSIGNED)) + 1 AS next_index + FROM afup_compta_facture + WHERE LEFT(numero_devis, 4) = :year'; + + $rows = iterator_to_array( + $this->getQuery($sql)->setParams(['year' => (string) $year])->query($this->getCollection(new HydratorArray())), + ); + + return isset($rows[0]['next_index']) ? (int) $rows[0]['next_index'] : null; + } + + public function convertQuotationToInvoice(Invoicing $quotation, string $invoiceNumber): void + { + $quotation->setInvoiceNumber($invoiceNumber); + $quotation->setInvoiceDate(new \DateTime()); + $this->save($quotation); + } + + private function findOneWithDetails(string $whereClause, array $params): ?Invoicing + { + /** @var Select $builder */ + $builder = $this->getQueryBuilder(self::QUERY_SELECT); + $builder->cols(['acf.*', 'acfd.*']) + ->from('afup_compta_facture acf') + ->leftJoin('afup_compta_facture_details acfd', 'acfd.idafup_compta_facture = acf.id') + ->where($whereClause); + + $hydrator = new HydratorRelational(); + $hydrator->addRelation(new RelationMany(new AggregateFrom('acfd'), new AggregateTo('acf'), 'setDetails')); + $hydrator->callableFinalizeAggregate(fn(array $row) => $row['acf']); + + $collection = $this->getQuery($builder->getStatement()) + ->setParams($params) + ->query($this->getCollection($hydrator)); + + if ($collection->count() === 0) { + return null; + } + + /** @var Invoicing $entity */ + $entity = $collection->first(); + $entity->setDetails(array_values($entity->getDetails())); + + return $entity; + } + public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) { $metadata = new Metadata($serializerFactory); diff --git a/sources/AppBundle/Controller/Admin/Accounting/Invoice/DownloadInvoiceAction.php b/sources/AppBundle/Controller/Admin/Accounting/Invoice/DownloadInvoiceAction.php index e88106f17..6eb80a82f 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Invoice/DownloadInvoiceAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Invoice/DownloadInvoiceAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\Invoice; -use Afup\Site\Comptabilite\Facture; +use AppBundle\Accounting\InvoicingPdfGenerator; use AppBundle\Accounting\Model\Invoicing; use AppBundle\Accounting\Model\Repository\InvoicingRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -15,20 +15,20 @@ class DownloadInvoiceAction extends AbstractController { public function __construct( - private readonly Facture $facture, + private readonly InvoicingPdfGenerator $pdfGenerator, private readonly InvoicingRepository $invoicingRepository, ) {} public function __invoke(Request $request): Response { $invoiceRef = $request->query->get('ref'); - $invoice = $this->invoicingRepository->getOneBy(['invoiceNumber' => $invoiceRef]); + $invoice = $this->invoicingRepository->getOneByInvoiceNumber($invoiceRef); if (!$invoice instanceof Invoicing) { throw new NotFoundHttpException("Cette facture n'existe pas"); } ob_start(); - $this->facture->genererFacture($invoiceRef); + $this->pdfGenerator->generateInvoice($invoice); $pdf = ob_get_clean(); $response = new Response($pdf); diff --git a/sources/AppBundle/Controller/Admin/Accounting/Invoice/SendInvoiceEmailAction.php b/sources/AppBundle/Controller/Admin/Accounting/Invoice/SendInvoiceEmailAction.php index fe9c59c7a..1690914b6 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Invoice/SendInvoiceEmailAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Invoice/SendInvoiceEmailAction.php @@ -4,8 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\Invoice; -use Afup\Site\Comptabilite\Facture; -use AppBundle\Accounting\Model\Invoicing; +use AppBundle\Accounting\InvoicingMailer; use AppBundle\Accounting\Model\Repository\InvoicingRepository; use AppBundle\AuditLog\Audit; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -16,7 +15,7 @@ class SendInvoiceEmailAction extends AbstractController { public function __construct( - private readonly Facture $facture, + private readonly InvoicingMailer $invoicingMailer, private readonly InvoicingRepository $invoicingRepository, private readonly Audit $audit, ) {} @@ -24,12 +23,12 @@ public function __construct( public function __invoke(Request $request): Response { $invoiceRef = $request->query->get('ref'); - $invoice = $this->invoicingRepository->getOneBy(['invoiceNumber' => $invoiceRef]); - if (!$invoice instanceof Invoicing) { + $invoice = $this->invoicingRepository->getOneByInvoiceNumber($invoiceRef); + if ($invoice === null) { throw new NotFoundHttpException("Cette facture n'existe pas"); } - if ($this->facture->envoyerfacture($invoiceRef)) { + if ($this->invoicingMailer->sendInvoice($invoice)) { $this->audit->log('Envoi par email de la facture n°' . $invoiceRef); $this->addFlash('notice', 'La facture a été envoyée'); } else { diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php index bb927191e..a488ad175 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\Quotation; -use Afup\Site\Comptabilite\Facture; +use AppBundle\Accounting\InvoicingNumberGenerator; use AppBundle\Accounting\Entity\Repository\ProduitRepository; use AppBundle\Accounting\Form\QuotationType; use AppBundle\Accounting\Model\Invoicing; @@ -19,7 +19,7 @@ class AddQuotationAction extends AbstractController { public function __construct( private readonly InvoicingRepository $invoicingRepository, - private readonly Facture $facture, + private readonly InvoicingNumberGenerator $numberGenerator, private readonly InvoicingDetailRepository $invoicingDetailRepository, private readonly ProduitRepository $produitRepository, ) {} @@ -32,7 +32,7 @@ public function __invoke(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { try { $this->invoicingRepository->startTransaction(); - $quotation->setQuotationNumber($this->facture->genererNumeroDevis()); + $quotation->setQuotationNumber($this->numberGenerator->generateQuotationNumber()); $this->invoicingRepository->save($quotation); foreach ($quotation->getDetails() as $detail) { $detail->setInvoicingId($quotation->getId()); diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/ConvertQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/ConvertQuotationAction.php index 31a68e16a..b1e7c7e0c 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Quotation/ConvertQuotationAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/ConvertQuotationAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\Quotation; -use Afup\Site\Comptabilite\Facture; +use AppBundle\Accounting\InvoicingNumberGenerator; use AppBundle\Accounting\Model\Repository\InvoicingRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -14,7 +14,7 @@ class ConvertQuotationAction extends AbstractController { public function __construct( - private readonly Facture $facture, + private readonly InvoicingNumberGenerator $numberGenerator, private readonly InvoicingRepository $invoicingRepository, ) {} @@ -26,7 +26,7 @@ public function __invoke(Request $request): Response throw new NotFoundHttpException("Ce devis n'existe pas"); } - $this->facture->transfertDevis($quotationRef); + $this->invoicingRepository->convertQuotationToInvoice($quotation, $this->numberGenerator->generateInvoiceNumber()); $this->addFlash('notice', 'Le devis a été transformé en facture'); return $this->redirectToRoute('admin_accounting_invoices_list'); diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/DownloadQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/DownloadQuotationAction.php index ee27a2539..004d37a8f 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/Quotation/DownloadQuotationAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/DownloadQuotationAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\Quotation; -use Afup\Site\Comptabilite\Facture; +use AppBundle\Accounting\InvoicingPdfGenerator; use AppBundle\Accounting\Model\Repository\InvoicingRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -14,20 +14,20 @@ class DownloadQuotationAction extends AbstractController { public function __construct( - private readonly Facture $facture, + private readonly InvoicingPdfGenerator $pdfGenerator, private readonly InvoicingRepository $invoicingRepository, ) {} public function __invoke(Request $request): Response { $quotationRef = $request->query->get('ref'); - $quotation = $this->invoicingRepository->getOneBy(['quotationNumber' => $quotationRef]); + $quotation = $this->invoicingRepository->getOneByQuotationNumber($quotationRef); if ($quotation === null) { throw new NotFoundHttpException("Ce devis n'existe pas"); } ob_start(); - $this->facture->genererDevis($quotationRef); + $this->pdfGenerator->generateQuotation($quotation); $pdf = ob_get_clean(); $response = new Response($pdf); diff --git a/sources/AppBundle/Controller/Website/Payment/InvoiceAction.php b/sources/AppBundle/Controller/Website/Payment/InvoiceAction.php index 73b8d2c5c..8b4a25230 100644 --- a/sources/AppBundle/Controller/Website/Payment/InvoiceAction.php +++ b/sources/AppBundle/Controller/Website/Payment/InvoiceAction.php @@ -4,8 +4,10 @@ namespace AppBundle\Controller\Website\Payment; -use Afup\Site\Comptabilite\Facture; use Afup\Site\Utils\Utils; +use AppBundle\Accounting\InvoicingPaymentStatus; +use AppBundle\Accounting\Model\Invoicing; +use AppBundle\Accounting\Model\Repository\InvoicingRepository; use AppBundle\Payment\PayboxBilling; use AppBundle\Payment\PayboxFactory; use AppBundle\Twig\ViewRenderer; @@ -18,7 +20,7 @@ class InvoiceAction extends AbstractController { public function __construct( private readonly ViewRenderer $view, - private readonly Facture $facture, + private readonly InvoicingRepository $invoicingRepository, private readonly PayboxFactory $payboxFactory, ) {} @@ -29,44 +31,42 @@ public function __invoke(Request $request): Response if (!$id) { throw $this->createNotFoundException('Facture inexistante'); } - $invoice = $this->facture->obtenir($id); + $invoice = $this->invoicingRepository->getById((int) $id); if (!$invoice) { throw $this->createNotFoundException('Facture inexistante'); } $paybox = null; - if ($invoice['etat_paiement'] === '0') { + if ($invoice->getPaymentStatus() === InvoicingPaymentStatus::Waiting) { $paybox = $this->buildPaybox($invoice); } return $this->view->render('site/payment/invoice.html.twig', [ - 'invoice_number' => $invoice['numero_facture'], + 'invoice_number' => $invoice->getInvoiceNumber(), 'ref' => $ref, 'paybox' => $paybox, ]); } - private function buildPaybox(array $invoice): string + private function buildPaybox(Invoicing $invoice): string { - $details = $this->facture->obtenir_details($invoice['numero_facture']); - - $amount = 0; - foreach ($details as $d) { - $amount += $d['quantite'] * $d['pu'] * (1 + ($d['tva'] / 100)); + $amount = 0.0; + foreach ($invoice->getDetails() as $detail) { + $amount += $detail->getQuantity() * $detail->getUnitPrice() * (1 + ($detail->getTva() / 100)); } $paybox = $this->payboxFactory->getPaybox(); $paybox ->setTotal($amount * 100) - ->setCmd($invoice['numero_facture']) - ->setPorteur($invoice['email']) + ->setCmd($invoice->getInvoiceNumber()) + ->setPorteur($invoice->getEmail()) ->setUrlRetourEffectue($this->generateUrl('payment_invoice_redirect', ['type' => 'success'], UrlGeneratorInterface::ABSOLUTE_URL)) ->setUrlRetourAnnule($this->generateUrl('payment_invoice_redirect', ['type' => 'canceled'], UrlGeneratorInterface::ABSOLUTE_URL)) ->setUrlRetourRefuse($this->generateUrl('payment_invoice_redirect', ['type' => 'refused'], UrlGeneratorInterface::ABSOLUTE_URL)) ->setUrlRetourErreur($this->generateUrl('payment_invoice_redirect', ['type' => 'error'], UrlGeneratorInterface::ABSOLUTE_URL)) ; - $payboxBilling = new PayboxBilling($invoice['prenom'], $invoice['nom'], $invoice['adresse'], $invoice['code_postal'], $invoice['ville'], $invoice['id_pays']); + $payboxBilling = new PayboxBilling($invoice->getFirstname(), $invoice->getLastname(), $invoice->getAddress(), $invoice->getZipcode(), $invoice->getCity(), $invoice->getCountryId()); return $paybox->generate(new \DateTime(), $payboxBilling); } diff --git a/sources/AppBundle/Controller/Website/Payment/InvoiceDownloadAction.php b/sources/AppBundle/Controller/Website/Payment/InvoiceDownloadAction.php index 48f8d178e..39b1771f7 100644 --- a/sources/AppBundle/Controller/Website/Payment/InvoiceDownloadAction.php +++ b/sources/AppBundle/Controller/Website/Payment/InvoiceDownloadAction.php @@ -4,29 +4,33 @@ namespace AppBundle\Controller\Website\Payment; -use Afup\Site\Comptabilite\Facture; use Afup\Site\Utils\Utils; +use AppBundle\Accounting\InvoicingPdfGenerator; +use AppBundle\Accounting\Model\Repository\InvoicingRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class InvoiceDownloadAction extends AbstractController { - public function __construct(private readonly Facture $facture) {} + public function __construct( + private readonly InvoicingPdfGenerator $pdfGenerator, + private readonly InvoicingRepository $invoicingRepository, + ) {} public function __invoke(Request $request): Response { - $invoiceRef = Utils::decryptFromText(urldecode($request->query->get('ref', ''))); - if (!$invoiceRef) { + $invoiceId = Utils::decryptFromText(urldecode($request->query->get('ref', ''))); + if (!$invoiceId) { throw $this->createNotFoundException('Facture inexistante, ref manquant'); } - $invoice = $this->facture->obtenir($invoiceRef); + $invoice = $this->invoicingRepository->getById((int) $invoiceId); if (!$invoice) { throw $this->createNotFoundException('Facture inexistante'); } ob_start(); - $this->facture->genererFacture($invoice['numero_facture']); + $this->pdfGenerator->generateInvoice($invoice); $pdf = ob_get_clean(); $response = new Response($pdf); diff --git a/sources/AppBundle/Controller/Website/Payment/InvoiceRedirectAction.php b/sources/AppBundle/Controller/Website/Payment/InvoiceRedirectAction.php index f01069f60..15f1b7bec 100644 --- a/sources/AppBundle/Controller/Website/Payment/InvoiceRedirectAction.php +++ b/sources/AppBundle/Controller/Website/Payment/InvoiceRedirectAction.php @@ -4,8 +4,8 @@ namespace AppBundle\Controller\Website\Payment; -use Afup\Site\Comptabilite\Facture; use Afup\Site\Utils\Utils; +use AppBundle\Accounting\Model\Repository\InvoicingRepository; use AppBundle\Payment\PayboxResponseFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -13,7 +13,7 @@ class InvoiceRedirectAction extends AbstractController { - public function __construct(private readonly Facture $facture) {} + public function __construct(private readonly InvoicingRepository $invoicingRepository) {} public function __invoke(Request $request, string $type = 'success'): RedirectResponse { @@ -21,8 +21,12 @@ public function __invoke(Request $request, string $type = 'success'): RedirectRe if (!$invoiceRef) { throw $this->createNotFoundException('Facture inexistante, ref manquant'); } - $invoice = $this->facture->obtenirParNumeroFacture($invoiceRef); - $cryptRef = urlencode(Utils::cryptFromText($invoice['id'])); + // getOneBy() intentionally used here: only getId() is needed, no details required + $invoice = $this->invoicingRepository->getOneBy(['invoiceNumber' => $invoiceRef]); + if ($invoice === null) { + throw $this->createNotFoundException('Facture inexistante'); + } + $cryptRef = urlencode(Utils::cryptFromText($invoice->getId())); $payboxResponse = PayboxResponseFactory::createFromRequest($request); if ($payboxResponse->isSuccessful()) {