feat change template facture not vat

This commit is contained in:
Tolotsoa 2025-08-22 01:33:56 +03:00
parent 572d7708ec
commit 351801d4ac
7 changed files with 1266 additions and 19 deletions

View File

@ -15,6 +15,7 @@
"symfony/framework-bundle": "^5.4"
},
"require": {
"setasign/fpdf": "^1.8"
"setasign/fpdf": "^1.8",
"dompdf/dompdf": "^3.1"
}
}

298
gestion/composer.lock generated
View File

@ -4,8 +4,296 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "79912f1c84e373fcd7778fb095176063",
"content-hash": "2cbec79cf4e2f6e464d23e0fd4df0b09",
"packages": [
{
"name": "dompdf/dompdf",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "a51bd7a063a65499446919286fb18b518177155a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a",
"reference": "a51bd7a063a65499446919286fb18b518177155a",
"shasum": ""
},
"require": {
"dompdf/php-font-lib": "^1.0.0",
"dompdf/php-svg-lib": "^1.0.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.0"
},
"time": "2025-01-15T14:09:04+00:00"
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "The FontLib Community",
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
},
"time": "2024-12-02T14:37:59+00:00"
},
{
"name": "dompdf/php-svg-lib",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "The SvgLib Community",
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/dompdf/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0"
},
"time": "2024-04-29T13:26:35+00:00"
},
{
"name": "masterminds/html5",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
"time": "2025-07-25T09:04:22+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "v8.9.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"require-dev": {
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
"rawr/cross-data-providers": "^2.0.0"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
},
"time": "2025-07-11T13:20:48+00:00"
},
{
"name": "setasign/fpdf",
"version": "1.8.5",
@ -4777,10 +5065,10 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@ -0,0 +1,7 @@
<?php
namespace OCA\Gestion\Exception;
class TemplateException extends \Exception
{
}

View File

@ -0,0 +1,126 @@
<?php
namespace OCA\Gestion\Service;
use Throwable;
use Dompdf\Dompdf;
use Dompdf\Options;
use OCA\Gestion\Exception\TemplateException;
class HtmlToPdfService
{
private $templatesPath;
private $assetsPath;
public function __construct()
{
$this->templatesPath = __DIR__ . '/../../templates/pdf/';
$this->assetsPath = '/var/www/html/data/admin/files/.gestion/';
}
public function generatePdf($templateName, $data, $options = [])
{
try {
$htmlContent = $this->renderPhpTemplate($templateName, $data);
return $this->convertToPdf($htmlContent, $options);
} catch (\Exception $e) {
error_log('HtmlToPdfService Error: ' . $e->getMessage());
throw new TemplateException('Erreur génération PDF: ' . $e->getMessage());
}
}
/**
* Rend un template PHP avec les données
*/
private function renderPhpTemplate($templateName, $data)
{
$templateFile = $this->templatesPath . $templateName . '.php';
if (!file_exists($templateFile)) {
throw new TemplateException("Template introuvable: " . $templateFile);
}
try {
$templateData = array_merge($data, [
'logo_base64' => $this->getLogoBase64()
]);
extract($templateData);
ob_start();
include $templateFile;
return ob_get_clean();
} catch (Throwable $e) {
ob_end_clean();
throw new TemplateException("Erreur lors du rendu du template: " . $e->getMessage(), 0, $e);
}
}
/**
* Récupère le logo en base64
*/
private function getLogoBase64()
{
$logoPath = $this->assetsPath . 'logo.png';
if (file_exists($logoPath)) {
return base64_encode(file_get_contents($logoPath));
}
// Retour d'un pixel transparent si pas de logo
return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
}
/**
* Convertit HTML en PDF
*/
private function convertToPdf($htmlContent, $options = [])
{
$dompdfOptions = new Options();
$dompdfOptions->set('defaultFont', 'DejaVu Sans');
$dompdfOptions->set('isRemoteEnabled', true);
$dompdfOptions->set('isHtml5ParserEnabled', true);
$dompdfOptions->set('enable_css_float', true);
$dompdfOptions->set('enable_font_subsetting', true);
$paper = $options['paper'] ?? 'A4';
$orientation = $options['orientation'] ?? 'portrait';
$dompdf = new Dompdf($dompdfOptions);
$dompdf->loadHtml($htmlContent);
$dompdf->setPaper($paper, $orientation);
$dompdf->render();
return $dompdf->output();
}
public function generateInvoiceFilename($factureData, $prefix = 'FACTURE')
{
$numero = str_pad($factureData['num'] ?? '', 6, '0', STR_PAD_LEFT);
$month = str_pad($factureData['month'] ?? date('m'), 2, '0', STR_PAD_LEFT);
$year = $factureData['year'] ?? date('Y');
return "{$prefix}_{$numero}_{$month}_{$year}.pdf";
}
/**
* Debug template PHP
*/
public function debugTemplate($templateName, $data)
{
echo "=== DONNÉES REÇUES ===\n";
var_dump($data);
echo "\n=== TEMPLATE PHP UTILISÉ ===\n";
$templateFile = $this->templatesPath . $templateName . '.php';
echo "Fichier: $templateFile\n";
echo "Existe: " . (file_exists($templateFile) ? "OUI" : "NON") . "\n";
echo "\n=== HTML GÉNÉRÉ ===\n";
$html = $this->renderPhpTemplate($templateName, $data);
echo substr($html, 0, 2000) . "...\n";
return $html;
}
}

View File

@ -27,19 +27,21 @@ declare(strict_types=1);
namespace OCA\Gestion\Service;
use DateTime;
use OCA\Gestion\Constants\BddConstant;
use OCA\Gestion\Constants\ClientTemplateTypeConstant;
use OCA\Gestion\Constants\DevisMentionConstant;
use OCA\Gestion\Constants\FactureTypeConstant;
use OCA\Gestion\Constants\MultipleFactureTypeConstant;
use Exception;
use OCA\Gestion\Db\Bdd;
use OCA\Gestion\Helpers\DateHelpers;
use OCA\Gestion\Service\InvoiceGroupPdfHandler\InvoiceFunecapPdfHandler;
use OCA\Gestion\Service\InvoiceGroupPdfHandler\InvoiceGroupPdfHandler;
use OCA\Gestion\Service\InvoiceGroupPdfHandler\InvoiceOgfPdfHandler;
use OCA\Gestion\Service\InvoiceRecap\InvoiceRecapService;
use OCP\DB\Exception;
use OCP\Files\IRootFolder;
use OCA\Gestion\Helpers\DateHelpers;
use OCA\Gestion\Constants\BddConstant;
use OCA\Gestion\Service\HtmlToPdfService;
use OCA\Gestion\Exception\TemplateException;
use OCA\Gestion\Constants\FactureTypeConstant;
use OCA\Gestion\Constants\DevisMentionConstant;
use OCA\Gestion\Constants\ClientTemplateTypeConstant;
use OCA\Gestion\Constants\MultipleFactureTypeConstant;
use OCA\Gestion\Service\InvoiceRecap\InvoiceRecapService;
use OCA\Gestion\Service\InvoiceGroupPdfHandler\InvoiceOgfPdfHandler;
use OCA\Gestion\Service\InvoiceGroupPdfHandler\InvoiceGroupPdfHandler;
use OCA\Gestion\Service\InvoiceGroupPdfHandler\InvoiceFunecapPdfHandler;
class InvoicePdfService
{
@ -52,6 +54,8 @@ class InvoicePdfService
/** @var InvoiceRecapService */
private $invoiceRecapService;
private $htmlToPdfService;
private const DEFAULT_NEXTCLOUD_ADMIN = "admin";
public function __construct(
Bdd $gestionBdd,
@ -61,6 +65,7 @@ class InvoicePdfService
$this->gestionBdd = $gestionBdd;
$this->rootFolder = $rootFolder;
$this->invoiceRecapService = $invoiceRecapService;
$this->htmlToPdfService = new HtmlToPdfService();
}
private function getLogo()
@ -174,6 +179,12 @@ class InvoicePdfService
if($invoicePdfData == null) {
return "";
}
// NOUVELLE LOGIQUE : Vérifier si TVA exonérée
if ($this->isTvaExempt($invoicePdfData)) {
return $this->generateWithHtmlTemplate($invoicePdfData, $storage);
}
$templateType = $invoicePdfData['template_type_key'];
$clean_folder = html_entity_decode(string: $currentConfig->path).'/';
$factureFolders = $this->getGroupFactureFolder($invoicePdfData, $clean_folder);
@ -200,17 +211,20 @@ class InvoicePdfService
$pdfContent = $pdf->Output('', 'S');
$pdfFilename = $pdf->GetInvoiceFilename();
$filenames = [];
foreach($factureFolders as $folder) {
try {
$storage->newFolder($folder);
} catch(\OCP\Files\NotPermittedException $e) {
} catch(TemplateException $e) {
// Ignore si le dossier existe déjà
}
$ff_pdf = $folder.$pdfFilename.'.pdf';
$ff_pdf = $folder.$pdfFilename;
$storage->newFile($ff_pdf);
$file_pdf = $storage->get($ff_pdf);
$file_pdf->putContent($pdfContent);
$filenames[] = $ff_pdf;
}
$this->gestionBdd->setFactureGeneratedDate($factureId);
return [
"content" => $pdfContent,
@ -218,6 +232,19 @@ class InvoicePdfService
];
}
public function sanitizePathDev(string $path): string
{
$path = ltrim($path, '/');
// Remplacer accents UTF-8 par ASCII
$path = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $path);
// Supprimer caractères interdits par Nextcloud/Docker
$path = preg_replace('#[<>:"/\\|?*]#', '_', $path);
// Supprimer doublons d'espaces
$path = preg_replace('#\s+#', ' ', $path);
return trim($path);
}
public function generateFacturePdfByFactureIds(array $factureIds, $idNextCloud)
{
foreach($factureIds as $factureId) {
@ -338,4 +365,305 @@ class InvoicePdfService
}
}
/**
* NOUVELLE MÉTHODE : Génération avec template HTML
*/
private function generateWithHtmlTemplate($invoicePdfData, $storage)
{
try {
// Préparer les données pour le template HTML
$templateData = $this->prepareHtmlTemplateData($invoicePdfData);
// Générer le PDF avec le nouveau système
$pdfContent = $this->htmlToPdfService->generatePdf('facture_dv_thanato', $templateData);
// Nom du fichier
$pdfFilename = $this->htmlToPdfService->generateInvoiceFilename($invoicePdfData);
// Sauvegarder dans Nextcloud
$clean_folder = html_entity_decode(string: $invoicePdfData['configuration']->path).'/';
$factureFolders = $this->getGroupFactureFolder($invoicePdfData, $clean_folder);
$filenames = [];
foreach ($factureFolders as $folder) {
// --- GESTION DU CHEMIN ---
if (($_ENV['APP_ENV'] ?? 'prod') === 'dev') {
// En dev : remplacer les accents et caractères spéciaux pour Docker
$relativeFolder = $this->sanitizePathDev($folder);
$pdfFileNameSafe = $this->sanitizePathDev($pdfFilename);
} else {
// En prod : garder UTF-8, juste retirer slash initial
$relativeFolder = ltrim($folder, '/');
$pdfFileNameSafe = $pdfFilename;
}
// --- CREATION DU DOSSIER ---
try {
if (!$storage->nodeExists($relativeFolder)) {
$storage->newFolder($relativeFolder);
}
} catch (\Exception $e) {
echo "ERROR creating folder '$relativeFolder': " . $e->getMessage();
exit;
}
// --- CHEMIN COMPLET DU FICHIER ---
$ff_pdf = rtrim($relativeFolder, '/') . '/' . $pdfFileNameSafe . '.pdf';
// --- CREATION OU MISE A JOUR DU FICHIER ---
try {
if ($storage->nodeExists($ff_pdf)) {
$file_pdf = $storage->get($ff_pdf);
} else {
$file_pdf = $storage->newFile($ff_pdf);
}
$file_pdf->putContent($pdfContent);
$filenames[] = $ff_pdf;
} catch (\Throwable $e) {
echo "ERROR on file '$ff_pdf': " . $e->getMessage();
exit;
}
}
$this->gestionBdd->setFactureGeneratedDate($invoicePdfData['id']);
return [
"content" => $pdfContent,
"filenames" => $filenames
];
} catch (TemplateException $e) {
error_log('HTML Template PDF Error: ' . $e->getMessage());
throw $e;
}
}
/**
* NOUVELLE MÉTHODE : Détecte si TVA exonérée
*/
private function isTvaExempt($invoicePdfData)
{
// Vérifier si c'est un client unique avec TVA = 0
if (isset($invoicePdfData['fk_client_id']) && $invoicePdfData['fk_client_id'] != null && $invoicePdfData['fk_client_id'] != 0) {
$client = $this->gestionBdd->getClientById($invoicePdfData['fk_client_id']);
if (isset($client['tva']) && $client['tva'] == 0) {
return true;
}
}
// Vérification alternative dans les totaux si la première méthode ne fonctionne pas
if (isset($invoicePdfData['totalPrices'])) {
foreach ($invoicePdfData['totalPrices'] as $label => $amount) {
if (strpos($label, 'TVA') !== false && (strpos($label, 'exonéré') !== false || $amount == 0)) {
return true;
}
}
}
return false;
}
/**
* Prépare les données pour le template HTML - Version refactorisée
*/
private function prepareHtmlTemplateData($invoicePdfData)
{
return [
'company' => $this->prepareCompanyData($invoicePdfData),
'client' => $this->prepareClientData($invoicePdfData),
'facture' => $this->prepareInvoiceData($invoicePdfData),
'groupedArticles' => $this->prepareGroupedArticles($invoicePdfData),
'totals' => $this->prepareTotals($invoicePdfData),
'bank' => $this->prepareBankData($invoicePdfData),
'legal_text' => $this->getLegalText()
];
}
/**
* Prépare les données de l'entreprise
*/
private function prepareCompanyData($invoicePdfData)
{
$config = $invoicePdfData['configuration'];
return [
'name' => $config->entreprise ?? 'DV Thanato',
'address' => $invoicePdfData['configuration_adresse'] ?? '47 rue Boldoduc',
'city' => $invoicePdfData['configuration_adresse_city'] ?? '59800 Lille',
'phone' => $config->telephone ?? '06.13.57.29.84',
'email' => $config->mail ?? 'soins@dvthanato.fr',
'logo' => 'logo_dv_thanato.png'
];
}
/**
* Prépare les données du client
*/
private function prepareClientData($invoicePdfData)
{
return [
'name' => $invoicePdfData['group_name'] ?? '',
'address' => $invoicePdfData['client_real_adress'] ?? '',
'city' => $invoicePdfData['client_adress_city'] ?? '',
'siret' => $invoicePdfData['siret'] ?? ''
];
}
/**
* Prépare les données de la facture
*/
private function prepareInvoiceData($invoicePdfData)
{
$numero = $invoicePdfData['num'] ?? '';
return [
'date' => date('d-m-Y'),
'number' => 'FAC' . str_pad((string)$numero, 6, '0', STR_PAD_LEFT),
'echeance' => date('d-m-Y', strtotime('+30 days')),
'period_start' => date('d/m/Y', strtotime($invoicePdfData['date'] ?? 'now')),
'period_end' => date('d/m/Y', strtotime($invoicePdfData['date_paiement'] ?? 'now'))
];
}
/**
* Prépare les articles groupés par date et défunt
*/
private function prepareGroupedArticles($invoicePdfData)
{
if (!isset($invoicePdfData['devis']) || !is_array($invoicePdfData['devis'])) {
return [];
}
$groupedByDate = [];
foreach ($invoicePdfData['devis'] as $devis) {
$this->processDevis($devis, $groupedByDate);
}
return $this->convertToIndexedArray($groupedByDate);
}
/**
* Traite un devis et l'ajoute aux données groupées
*/
private function processDevis($devis, &$groupedByDate)
{
$dateDevis = date('d/m/Y', strtotime($devis['devis_date'] ?? 'now'));
$defuntNom = $devis['defunt_nom'] ?? 'Non défini';
$this->initializeDateGroup($groupedByDate, $dateDevis);
$this->initializeDefuntGroup($groupedByDate, $dateDevis, $defuntNom);
$this->addServicesToDefunt($groupedByDate, $dateDevis, $defuntNom, $devis['products'] ?? []);
}
/**
* Initialise un groupe de date
*/
private function initializeDateGroup(&$groupedByDate, $dateDevis)
{
if (!isset($groupedByDate[$dateDevis])) {
$groupedByDate[$dateDevis] = [
'date' => $dateDevis,
'defunts' => []
];
}
}
/**
* Initialise un groupe de défunt
*/
private function initializeDefuntGroup(&$groupedByDate, $dateDevis, $defuntNom)
{
if (!isset($groupedByDate[$dateDevis]['defunts'][$defuntNom])) {
$groupedByDate[$dateDevis]['defunts'][$defuntNom] = [
'nom' => $defuntNom,
'services' => []
];
}
}
/**
* Ajoute les services à un défunt
*/
private function addServicesToDefunt(&$groupedByDate, $dateDevis, $defuntNom, $products)
{
foreach ($products as $product) {
$service = $this->createServiceFromProduct($product);
$groupedByDate[$dateDevis]['defunts'][$defuntNom]['services'][] = $service;
}
}
/**
* Crée un service à partir d'un produit
*/
private function createServiceFromProduct($product)
{
$prixHt = ($product['produit_price'] ?? 0) * ($product['quantite'] ?? 1);
return [
'reference' => $product['produit_reference'] ?? '',
'description' => $product['produit_description'] ?? 'Produit inconnu',
'prix_ht' => number_format($prixHt, 2, ',', ' '),
'quantite' => number_format($product['quantite'] ?? 1, 2, ',', ' '),
'prix_ttc' => number_format($prixHt, 2, ',', ' ')
];
}
/**
* Convertit les données groupées en array indexé
*/
private function convertToIndexedArray($groupedByDate)
{
$groupedArticles = [];
foreach ($groupedByDate as $dateGroup) {
$defuntsArray = array_values($dateGroup['defunts']);
$dateGroup['defunts'] = $defuntsArray;
$groupedArticles[] = $dateGroup;
}
return $groupedArticles;
}
/**
* Prépare les totaux
*/
private function prepareTotals($invoicePdfData)
{
$totalPrices = $invoicePdfData['totalPrices'] ?? [];
return [
'total_ht' => number_format($totalPrices['TOTAL HT'] ?? 0, 2, ',', ' '),
'total_tva' => '0,00',
'total_ttc' => number_format($totalPrices['TOTAL TTC'] ?? 0, 2, ',', ' '),
'tva_label' => 'TVA (exonéré)'
];
}
/**
* Prépare les données bancaires
*/
private function prepareBankData($invoicePdfData)
{
$config = $invoicePdfData['configuration'];
return [
'iban' => 'FR76 1670 6052 4453 9757 9734 871',
'swift' => 'AGRI FR PP867',
'rcs' => $config->legal_one ?? '901 115 931 R.C.S Lille Métropole'
];
}
/**
* Texte légal
*/
private function getLegalText()
{
return "Tout retard de paiement entraînera de plein droit une pénalité de retard de 3 fois le taux légal " .
"(Loi 2008-776 du 4 août 2008) et une indemnité forfaitaire de 40 EUR pour frais de recouvrement sera appliquée.\n" .
"Si les frais de recouvrement sont supérieurs à ce montant forfaitaire, une indemnisation complémentaire " .
"sera due sur présentation de justificatifs (articles L 441-3 et L 441-6 du code de commerce).";
}
}

View File

@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Facture DV Thanato</title>
<style>
@page { margin: 20mm 15mm; size: A4; }
body { font-family: Arial, sans-serif; font-size: 11px; line-height: 1.4; color: #000; margin: 0; padding: 0; }
.container { width: 100%; }
/* LOGO EN HAUT */
.logo-section { margin-bottom: 20px; }
.logo { width: 180px; height: auto; }
/* HEADER AVEC TABLE POUR ALIGNEMENT */
.header-table { width: 100%; margin-bottom: 30px; }
.header-table td { vertical-align: top; padding: 0; }
.header-left { width: 45%; }
.header-right { width: 55%; text-align: right; }
/* INFORMATIONS GAUCHE */
.invoice-info { line-height: 1.6; }
.info-line { margin: 5px 0; }
.info-line strong { display: inline-block; width: 140px; font-weight: bold; }
/* INFORMATIONS CLIENT DROITE */
.client-info { text-align: right; line-height: 1.5; font-size: 12px; }
.client-info strong { display: block; margin-bottom: 3px; font-size: 13px; }
.invoice-header { margin: 25px 0; }
.invoice-header h2 { font-size: 14px; margin: 5px 0; font-weight: bold; }
.invoice-period { font-size: 11px; margin: 5px 0; }
/* TABLEAU SERVICES */
.services-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.services-table th { background-color: #f0f0f0; border: 1px solid #ccc; padding: 8px; text-align: left; font-weight: bold; font-size: 11px; }
.services-table td { border: 1px solid #ccc; padding: 6px 8px; vertical-align: top; font-size: 10px; }
.ref-col { width: 12%; }
.label-col { width: 48%; }
.price-col { width: 13%; text-align: right; }
.qty-col { width: 10%; text-align: center; }
.amount-col { width: 17%; text-align: right; }
/* LIGNES SPÉCIALES */
.date-row { background-color: #f8f8f8; font-weight: bold; }
.date-row td { padding: 8px; border: none; border-bottom: 1px solid #ddd; text-align: center; }
.deceased-row { font-style: italic; background-color: #fafafa; }
.deceased-row td { padding: 8px; border: none; font-weight: bold; }
/* TOTAUX */
.totals-section { margin-top: 30px; }
.totals-table { width: 300px; float: right; border-collapse: collapse; }
.totals-table td { padding: 8px 12px; font-size: 12px; }
.total-label { text-align: right; font-weight: bold; width: 60%; }
.total-amount { text-align: right; font-weight: bold; width: 40%; }
.total-row-ht { border-top: 1px solid #ccc; }
.total-row-final { border-top: 2px solid #000; font-size: 13px; }
/* CLEAR FLOAT */
.clear { clear: both; }
/* MENTIONS LÉGALES */
.legal-section { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ccc; }
.legal-text { font-size: 9px; line-height: 1.4; margin-bottom: 20px; text-align: justify; }
.bank-info { margin: 20px 0; font-size: 10px; line-height: 1.5; }
.bank-info strong { display: inline-block; width: 150px; }
.company-footer { margin-top: 30px; padding: 15px; background-color: #f5f5f5; font-size: 10px; line-height: 1.5; }
.company-footer strong { display: block; margin-bottom: 5px; }
</style>
</head>
<body>
<div class="container">
<!-- Logo en haut à gauche -->
<div class="logo-section">
<img src="data:image/png;base64,<?= $logo_base64 ?>" alt="<?= htmlspecialchars($company['name']) ?>" class="logo">
</div>
<!-- Header avec table pour compatibilité dompdf -->
<table class="header-table">
<tr>
<td class="header-left">
<div class="invoice-info">
<div class="info-line">
<strong>Date :</strong><?= htmlspecialchars($facture['date']) ?>
</div>
<div class="info-line">
<strong>Échéance :</strong><?= htmlspecialchars($facture['echeance']) ?>
</div>
<div class="info-line">
<strong>Mode de paiement :</strong>Virement
</div>
</div>
</td>
<td class="header-right">
<div class="client-info">
<strong><?= htmlspecialchars($client['name']) ?></strong>
<?= htmlspecialchars($client['address']) ?><br>
<?= htmlspecialchars($client['city']) ?><br>
<?php if (!empty($client['siret'])): ?>
SIRET: <?= htmlspecialchars($client['siret']) ?>
<?php endif; ?>
</div>
</td>
</tr>
</table>
<!-- Numéro de facture -->
<div class="invoice-header">
<h2>Facture <?= htmlspecialchars($facture['number']) ?></h2>
<div class="invoice-period">Période : du <?= htmlspecialchars($facture['period_start']) ?> au <?= htmlspecialchars($facture['period_end']) ?></div>
</div>
<!-- Tableau des services -->
<table class="services-table">
<thead>
<tr>
<th class="ref-col">Référence</th>
<th class="label-col">Libellé</th>
<th class="price-col">P.U HT</th>
<th class="qty-col">Quantité</th>
<th class="amount-col">Montant HT</th>
</tr>
</thead>
<tbody>
<?php foreach ($groupedArticles as $dateGroup): ?>
<!-- Ligne de date centrée -->
<tr class="date-row">
<td colspan="5"><strong><?= htmlspecialchars($dateGroup['date']) ?></strong></td>
</tr>
<?php foreach ($dateGroup['defunts'] as $defunt): ?>
<!-- Ligne du défunt -->
<tr class="deceased-row">
<td colspan="5"><?= htmlspecialchars($defunt['nom']) ?></td>
</tr>
<!-- Services pour ce défunt -->
<?php foreach ($defunt['services'] as $service): ?>
<tr>
<td class="ref-col"><?= htmlspecialchars($service['reference']) ?></td>
<td class="label-col"><?= htmlspecialchars($service['description']) ?></td>
<td class="price-col"><?= htmlspecialchars($service['prix_ht']) ?> €</td>
<td class="qty-col"><?= htmlspecialchars($service['quantite']) ?></td>
<td class="amount-col"><?= htmlspecialchars($service['prix_ttc']) ?> €</td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
<!-- Section totaux -->
<div class="totals-section">
<table class="totals-table">
<tr class="total-row-ht">
<td class="total-label">Total HT</td>
<td class="total-amount"><?= htmlspecialchars($totals['total_ht']) ?> €</td>
</tr>
<tr>
<td class="total-label"><?= htmlspecialchars($totals['tva_label']) ?></td>
<td class="total-amount"><?= htmlspecialchars($totals['total_tva']) ?> €</td>
</tr>
<tr class="total-row-final">
<td class="total-label">Montant total</td>
<td class="total-amount"><?= htmlspecialchars($totals['total_ttc']) ?> €</td>
</tr>
</table>
</div>
<div class="clear"></div>
<!-- Mentions légales -->
<div class="legal-section">
<div class="legal-text">
<?= nl2br(htmlspecialchars($legal_text)) ?>
</div>
<div class="bank-info">
<div><strong>Coordonnées bancaires</strong></div>
<div><strong>IBAN:</strong> <?= htmlspecialchars($bank['iban']) ?></div>
<div><strong>BIC:</strong> <?= htmlspecialchars($bank['swift']) ?></div>
</div>
<div class="company-footer">
<strong><?= htmlspecialchars($company['name']) ?></strong>
<?= htmlspecialchars($company['address']) ?>, <?= htmlspecialchars($company['city']) ?><br>
<?= htmlspecialchars($bank['rcs']) ?> - Code NAF 9603Z
</div>
</div>
</div>
</body>
</html>

View File

@ -125,6 +125,240 @@
],
"install-path": "../doctrine/instantiator"
},
{
"name": "dompdf/dompdf",
"version": "v3.1.0",
"version_normalized": "3.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "a51bd7a063a65499446919286fb18b518177155a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a",
"reference": "a51bd7a063a65499446919286fb18b518177155a",
"shasum": ""
},
"require": {
"dompdf/php-font-lib": "^1.0.0",
"dompdf/php-svg-lib": "^1.0.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"time": "2025-01-15T14:09:04+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.0"
},
"install-path": "../dompdf/dompdf"
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
},
"time": "2024-12-02T14:37:59+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "The FontLib Community",
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
},
"install-path": "../dompdf/php-font-lib"
},
{
"name": "dompdf/php-svg-lib",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
},
"time": "2024-04-29T13:26:35+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "The SvgLib Community",
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/dompdf/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0"
},
"install-path": "../dompdf/php-svg-lib"
},
{
"name": "masterminds/html5",
"version": "2.10.0",
"version_normalized": "2.10.0.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"time": "2025-07-25T09:04:22+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
"install-path": "../masterminds/html5"
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
@ -1318,6 +1552,75 @@
},
"install-path": "../psr/log"
},
{
"name": "sabberworm/php-css-parser",
"version": "v8.9.0",
"version_normalized": "8.9.0.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"require-dev": {
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
"rawr/cross-data-providers": "^2.0.0"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"time": "2025-07-11T13:20:48+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
},
"install-path": "../sabberworm/php-css-parser"
},
{
"name": "sebastian/cli-parser",
"version": "1.0.1",