Compare commits

...

13 Commits

14 changed files with 1766 additions and 305 deletions

19
.gitignore vendored
View File

@ -1,17 +1,7 @@
# Ignore tout par défaut
*
# Exceptions : fichiers et dossiers à inclure
!.gitignore
!Jenkinsfile
# Dossiers à inclure
!calendar/
!gestion/
# Inclure tout le contenu de ces dossiers
!calendar/**
!gestion/**
vendor/
config/
data/
# Ignorer spécifiquement (même dans les dossiers inclus)
*.sql
@ -23,3 +13,6 @@ node_modules/
**/.env.*
**/dist/
**/build/
*.sh
docker-compose.yml

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

@ -1,4 +1,5 @@
<?php
namespace OCA\Gestion\Controller;
use OCA\Gestion\Constants\BddConstant;
@ -43,9 +44,7 @@ class InvoiceController extends Controller
IMailer $mailer,
MailerService $mailerService,
IUserSession $userSession
)
{
) {
$this->currentUserIdNextcloud = $UserId;
$this->invoicePdfService = $invoicePdfService;
$this->rootFolder = $rootFolder;
@ -55,9 +54,9 @@ class InvoiceController extends Controller
$this->config = $config;
$this->user = $userSession->getUser();
try{
try {
$this->storage = $rootFolder->getUserFolder($this->currentUserIdNextcloud);
}catch(\OC\User\NoUserException $e){
} catch(\OC\User\NoUserException $e) {
}
parent::__construct($AppName, request: $request);
@ -68,14 +67,14 @@ class InvoiceController extends Controller
* @NoCSRFRequired
*/
public function getInvoicePdfContent($factureId){
$facture = $this->gestionRepository->getFactureByFactureId($factureId);
if($facture == null)
public function getInvoicePdfContent($factureId)
{
$facture = $this->gestionRepository->getFactureByFactureId($factureId);
if($facture == null) {
return new DataResponse("La facture n'existe pas", 404, ['Content-Type' => 'application/json']);
}
$factureGeneratedResponse = $this->invoicePdfService->generateFacturePdfByFactureId($factureId,$this->currentUserIdNextcloud);
if($factureGeneratedResponse == null){
$factureGeneratedResponse = $this->invoicePdfService->generateFacturePdfByFactureId($factureId, $this->currentUserIdNextcloud);
if($factureGeneratedResponse == null) {
return new DataResponse("La facture n'a pas été générée correctement", 404, ['Content-Type' => 'application/json']);
}
$factureContent = base64_encode($factureGeneratedResponse['content']);
@ -91,12 +90,11 @@ class InvoiceController extends Controller
public function sendInvoicePdfViaMail($factureId, $email = '')
{
$facture = $this->gestionRepository->getFactureByFactureId($factureId);
if($facture == null)
{
if($facture == null) {
return new DataResponse("La facture n'existe pas", 404, ['Content-Type' => 'application/json']);
}
$factureGeneratedResponse = $this->invoicePdfService->generateFacturePdfByFactureId($factureId,$this->currentUserIdNextcloud);
if($factureGeneratedResponse == null){
$factureGeneratedResponse = $this->invoicePdfService->generateFacturePdfByFactureId($factureId, $this->currentUserIdNextcloud);
if($factureGeneratedResponse == null) {
return new DataResponse("La facture n'a pas été générée correctement", 404, ['Content-Type' => 'application/json']);
}
$factureContent = $factureGeneratedResponse["content"];
@ -109,7 +107,8 @@ class InvoiceController extends Controller
$message->attach($content);
$message->setSubject("Facture");
$signature = $this->mailerService->getFooterContent();
$signature = $this->mailerService->getFooterContent($this->getUserNameForEmailSignature());
$message->setHtmlBody(
"<p>Bonjour.</p>".
@ -128,4 +127,11 @@ class InvoiceController extends Controller
}
return new DataResponse("E-mail envoyé avec succès à ".$email.".", 200, ['Content-Type' => 'application/json']);
}
public function getUserNameForEmailSignature()
{
$configs = json_decode($this->gestionRepository->getConfiguration(BddConstant::DEFAULT_ADMIN_ID_NEXTCLOUD));
$currentConfig = $configs[0];
return $currentConfig->nom . " " . $currentConfig->prenom;
}
}

View File

@ -3352,6 +3352,7 @@ class Bdd
if($factureData == null) {
return null;
}
$products = $this->getDevisProduits($factureData["devis_id"]);
$isDevisNegative = $this->isDevisNegative($factureData['devis_id'], $factureData['client_id']);
$factureData = $this->setDevisStartAndEndTime($factureData);
@ -3359,6 +3360,11 @@ class Bdd
$factureData["products"] = $products;
$factureData["configuration"] = $configuration;
// Récupération des informations client et gestion TVA
$client = $this->getClientById($factureData['client_id']);
$hasTva = ($client && isset($client['tva'])) ? ($client['tva'] == 1) : true;
$groupClient = $this->getTvaItracomuIdClient($factureData['client_id']);
$isClientInsideGroup = $factureData["group_id"] != null;
if($isClientInsideGroup) {
$factureData["client_real_adress"] = $factureData["group_address"];
@ -3371,11 +3377,15 @@ class Bdd
$factureData["client_real_adress"] = $clientAdresses["address"];
$factureData["client_adress_city"] = $clientAdresses["city"];
}
$factureData['is_negative'] = $isDevisNegative;
$factureData["is_tva"] = $hasTva;
$factureData["client_tva_intracommu"] = $groupClient["tva_intracommu"];
$configurationAdresses = FileExportHelpers::GetAddressAndCityFromAddress($configuration->adresse);
$factureData["configuration_adresse"] = $configurationAdresses["address"];
$factureData["configuration_adresse_city"] = $configurationAdresses["city"];
return $factureData;
}
@ -3522,6 +3532,13 @@ class Bdd
$products = $this->getDevisProduits($invoice["devis_id"]);
$invoice["products"] = $products;
$invoice["configuration"] = $configuration;
// Déterminer la TVA pour cette facture
$hasTva = true; // Valeur par défaut
if(isset($invoice['client_id']) && $invoice['client_id']) {
$client = $this->getClientById($invoice['client_id']);
$hasTva = ($client && isset($client['tva'])) ? ($client['tva'] == 1) : true;
}
$invoice["is_tva"] = $hasTva;
$isClientInsideGroup = $invoice["group_id"] != null;
if($isClientInsideGroup) {
@ -5318,7 +5335,7 @@ COMMENTAIRES: ".$comment;
$configuration = $this->getConfiguration(BddConstant::DEFAULT_ADMIN_ID_NEXTCLOUD);
$configuration = json_decode($configuration);
$currentConfig = $configuration[0];
$tvaValue = $currentConfig->tva_default;
$isFactureSingleClient = $facture['fk_client_id'] != null
&& $facture['fk_client_id'] != 0;
@ -5329,6 +5346,26 @@ COMMENTAIRES: ".$comment;
$devis = $this->getDevisByFkFactureId($factureId);
$factureGroupIsRelatedToAnyDevis = $devis != null;
// LOGIQUE TVA SIMPLIFIÉE
$tvaValue = (float)$currentConfig->tva_default;
$isTvaApplicable = true;
// Récupérer le statut TVA du client de cette facture
$clientId = $facture['fk_client_id'];
if ($clientId != null && $clientId != 0) {
$client = $this->getClientById($clientId);
if (isset($client['tva']) && $client['tva'] == 0) {
$isTvaApplicable = false;
$tvaValue = 0;
}
} else {
$clientId = $devis['id_client'];
$client = $this->getClientById($clientId);
if (isset($client['tva']) && $client['tva'] == 0) {
$isTvaApplicable = false;
$tvaValue = 0;
}
}
if($isFactureSingleClient) {
$client = $this->getClientById($facture['fk_client_id']);
@ -5337,6 +5374,7 @@ COMMENTAIRES: ".$comment;
$facture['client_address'] = $client["client_address"];
$facture['siret'] = $client["client_legal_one"];
$facture['mail'] = $client["client_mail"];
if (!$factureGroupIsRelatedToAnyDevis) {
$devisList = $this->getDevisDataByClientIdAndMonthYear(
$facture['fk_client_id'],
@ -5344,7 +5382,6 @@ COMMENTAIRES: ".$comment;
$facture['year'],
$devisMentionFilters
);
} else {
$devisList = $this->getDevisDataGroupByFactureId($factureId, $devisMentionFilters);
}
@ -5355,6 +5392,7 @@ COMMENTAIRES: ".$comment;
$facture['client_address'] = $clientGroupFacturation["address"] . ' - ' .$clientGroupFacturation["postal_code"] . ' ' . $clientGroupFacturation['city'];
$facture['siret'] = $clientGroupFacturation["siret_number"];
$facture['mail'] = $clientGroupFacturation["email"];
if (!$factureGroupIsRelatedToAnyDevis) {
$devisList = $this->getDevisDataByClientGroupFacturationIdAndMonthYear(
$facture['fk_client_group_facturation_id'],
@ -5366,22 +5404,36 @@ COMMENTAIRES: ".$comment;
$devisList = $this->getDevisDataGroupByFactureId($factureId, $devisMentionFilters);
}
}
$factureTotalHt = 0;
$factureTotalTva = 0;
$factureTotalTtc = 0;
foreach($devisList as &$currentDevis) {
$totalHt = 0;
$totalTva = 0;
$totalTtc = 0;
$devisProducts = $this->getDevisProduits($currentDevis['devis_id']);
foreach($devisProducts as $currentProduct) {
$valueHt = $currentProduct['produit_price'] * $currentProduct['quantite'];
// CALCUL SIMPLE : TVA OU PAS TVA
if ($isTvaApplicable) {
// Client soumis à TVA : utiliser le taux par défaut
$valueTtc = PriceHelpers::calculPriceWithVatValue($valueHt, $tvaValue);
$tvaAmount = $valueTtc - $valueHt;
} else {
// Client exonéré : TTC = HT
$valueTtc = $valueHt;
$tvaAmount = 0;
}
$totalHt += $valueHt;
$totalTtc += $valueTtc;
$tvaAmount = $valueTtc - $valueHt;
$totalTva += $tvaAmount;
}
$currentDevis["totalHt"] = $totalHt;
$currentDevis["totalTtc"] = $totalTtc;
$currentDevis["totalTva"] = $totalTva;
@ -5390,10 +5442,19 @@ COMMENTAIRES: ".$comment;
$factureTotalTva += $totalTva;
$facture["devisList"][] = $currentDevis;
}
$facture["totalHt"] = $factureTotalHt;
$facture["totalTtc"] = $factureTotalTtc;
$facture["totalTva"] = $factureTotalTva;
$facture["isFactureClientGroup"] = !$isFactureSingleClient;
// INFOS TVA POUR LE TEMPLATE
$facture["tvaInfo"] = [
'is_applicable' => $isTvaApplicable,
'rate' => $tvaValue,
'is_exempt' => !$isTvaApplicable
];
return $facture;
}
@ -5402,6 +5463,7 @@ COMMENTAIRES: ".$comment;
$defaultTvaValue = $configuration->tva_default;
$factureData = $this->getFactureByFactureId($factureId);
$isFactureForSingleClient = $factureData['fk_client_id'] != null && $factureData['fk_client_id'] != 0;
$devisMentionFilters = [
DevisMentionConstant::FACTURED_FORMATTED,
DevisMentionConstant::FACTURED
@ -5409,6 +5471,16 @@ COMMENTAIRES: ".$comment;
$devis = $this->getDevisByFkFactureId($factureId);
$factureGroupIsRelatedToAnyDevis = $devis != null;
$hasTva = true; // Valeur par défaut
if($isFactureForSingleClient) {
$client = $this->getClientById($factureData['fk_client_id']);
$hasTva = ($client && isset($client['tva'])) ? ($client['tva'] == 1) : true;
$groupClient = $this->getTvaItracomuIdClient($factureData['fk_client_id']);
} else {
$client = $this->getClientById($devis['id_client']);
$hasTva = ($client && isset($client['tva'])) ? ($client['tva'] == 1) : true;
$groupClient = $this->getTvaItracomuIdClient($devis['id_client']);
}
// Déterminer le taux de TVA global pour l'affichage
$globalTvaValue = $defaultTvaValue;
@ -5437,7 +5509,6 @@ COMMENTAIRES: ".$comment;
} else {
$factureDevisList = $this->getDevisDataGroupByFactureId($factureId, $devisMentionFilters);
}
$factureIncrement = 0;
$productsCount = 0;
$totalHt = 0;
@ -5525,15 +5596,22 @@ COMMENTAIRES: ".$comment;
} elseif($globalTvaValue > 0) {
$tvaLabel = "TVA ".$globalTvaValue. "%";
} else {
$tvaLabel = "TVA (exonéré)";
$tvaLabel = "TVA 0 %";
}
if($hasTva) {
$totaPricesArray = [
"TOTAL HT" => $totalHt,
$tvaLabel => $totalTva,
"TOTAL TTC" => $totalTtc
];
} else {
$totaPricesArray = [
"TOTAL HT" => $totalHt
];
}
$factureData["is_tva"] = $hasTva;
$factureData["devis"] = $factureDevisList;
$factureData["configuration"] = $configuration;
$configurationAdresses = FileExportHelpers::GetAddressAndCityFromAddress($configuration->adresse);
@ -5542,6 +5620,7 @@ COMMENTAIRES: ".$comment;
$factureData["productsCount"] = $productsCount;
$factureData["totalPrices"] = $totaPricesArray;
$factureData["template_type_key"] = $templateType;
$factureData["client_tva_intracommu"] = $groupClient["tva_intracommu"];
return $factureData;
}
@ -5633,4 +5712,25 @@ COMMENTAIRES: ".$comment;
}
return null;
}
public function getTvaItracomuIdClient($clientId)
{
$sql = "SELECT
gf.tva_intracommu
FROM ".$this->tableprefix."client_group_facturation AS gf
LEFT JOIN ".$this->tableprefix."client AS gc
ON gc.fk_client_group_facturation_id = gf.id
WHERE gc.id = ?;";
$data = $this->execSQLNoJsonReturn(
$sql,
[$clientId]
);
if(!empty($data)) {
return $data[0];
}
return null;
}
}

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,7 +27,7 @@ declare(strict_types=1);
namespace OCA\Gestion\Service\InvoiceGroupPdfHandler;
use DateTime;
use \FPDF;
use FPDF;
use OCA\Gestion\Helpers\FileExportHelpers;
use OCA\Gestion\Helpers\PriceHelpers;
@ -50,15 +50,16 @@ class InvoiceGroupPdfHandler extends FPDF
public $thereIsOrderOrCaseNumber = false;
public $startingYOfArticlesTable = 100;
public int $maxArticlePerPage = 19;
public int $maxArticlePerPage = 7;
public $additionalArticlesLineBasedOnMultiline = 0;
public $interLigneHeader = 5;
public $hasTva = true;
function Header()
public function Header()
{
if ($this->logo != "nothing") {
$this->Image($this->logoPath . "logo.png", 4, 2, 50, 35);
$this->AddWatermark();
//$this->AddWatermark();
} else {
$this->Cell(55, 30, '');
}
@ -66,8 +67,19 @@ class InvoiceGroupPdfHandler extends FPDF
$this->DrawInvoiceCompanyAndClientInfo();
$this->DrawInvoiceInfoTable();
}
function AddWatermark()
public function SafeTextForPdf($text)
{
if (empty($text)) {
return '';
}
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML401, 'UTF-8');
return iconv('UTF-8', 'ISO-8859-1//IGNORE', $text);
}
public function AddWatermark()
{
try {
$this->SetAlpha(0.2);
$imagePath = $this->logoPath . "filigrane_pdf.png";
@ -89,9 +101,13 @@ class InvoiceGroupPdfHandler extends FPDF
// Ajouter l'image en filigrane
$this->Image($imagePath, $x, $y, $width, $height); // Chemin, position x, position y, largeur, hauteur
$this->SetAlpha(0.1); // Définir l'opacité
} catch (\Throwable $th) {
}
function SetAlpha($alpha)
}
public function SetAlpha($alpha)
{
// Appliquer la transparence au document
$this->SetFillColor(255, 255, 255, $alpha * 255);
@ -106,21 +122,21 @@ class InvoiceGroupPdfHandler extends FPDF
}
function Footer()
public function Footer()
{
$this->SetY(-34);
$this->SetFont('ComicSans', '', 7);
$this->SetFont('Arial', '', 7);
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('Tout retard de paiement entraînera de plein droit une pénalité de retard de 3 fois le taux légal ')));
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('(Loi 2008-776 du 4 août 2008) et une indemnité forfaitaire de 40 EUR pour frais de recouvrement sera appliquée.')));
$this->MultiCell(0, 4, $this->SafeTextForPdf('Tout retard de paiement entraînera de plein droit une pénalité de retard de 3 fois le taux légal '));
$this->MultiCell(0, 4, $this->SafeTextForPdf('(Loi 2008-776 du 4 août 2008) et une indemnité forfaitaire de 40 EUR pour frais de recouvrement sera appliquée.'));
$this->Ln(1);
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('Si les frais de recouvrement sont supérieurs à ce montant forfaitaire, une indemnisation complémentaire')));
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('sera due sur présentation de justificatifs (articles L.441-3 et L.441-6 du code de commerce).')));
$this->MultiCell(0, 4, $this->SafeTextForPdf('Si les frais de recouvrement sont supérieurs à ce montant forfaitaire, une indemnisation complémentaire'));
$this->MultiCell(0, 4, $this->SafeTextForPdf('sera due sur présentation de justificatifs (articles L.441-3 et L.441-6 du code de commerce).'));
$this->SetY(-10);
$this->SetFont('ComicSans', '', 7);
$this->Cell(0, 10, utf8_decode(html_entity_decode($this->factureData['configuration']->legal_one)), 0, 0, 'C');
$this->SetFont('Arial', '', 7);
$this->Cell(0, 10, $this->SafeTextForPdf($this->factureData['configuration']->legal_one), 0, 0, 'C');
}
public function InvoicePdfFactory(array $factureData, $logo = null)
@ -132,6 +148,8 @@ class InvoiceGroupPdfHandler extends FPDF
$this->devisCountToGet = ($this->devisCount <= $this->maxArticlePerPage) ? $this->devisCount : $this->maxArticlePerPage;
$this->devisList = $this->factureData['devis'];
$this->logo = $logo;
// Déterminer si la TVA est applicable
$this->hasTva = isset($factureData['is_tva']) ? $factureData['is_tva'] : true;
}
public function GetInvoiceFilename()
@ -167,7 +185,7 @@ class InvoiceGroupPdfHandler extends FPDF
public function DrawInvoiceCompanyInfo()
{
$this->SetY(40);
$this->SetFont('ComicSans', '', 10);
$this->SetFont('Arial', '', 10);
$this->Cell(0, $this->interLigneHeader, FileExportHelpers::FormatTextForExport($this->factureData['configuration']->entreprise), 0, 1);
$this->Cell(0, $this->interLigneHeader, FileExportHelpers::FormatTextForExport($this->factureData['configuration_adresse']), 0, 1);
$this->Cell(0, $this->interLigneHeader, FileExportHelpers::FormatTextForExport($this->factureData['configuration_adresse_city']), 0, 1);
@ -176,7 +194,7 @@ class InvoiceGroupPdfHandler extends FPDF
}
public function DrawInvoiceClientInfo()
{
$this->SetFont('ComicSans', '', 10);
$this->SetFont('Arial', '', 10);
$clientName = $this->factureData['group_name'];
$clientInfoXAxis = 135;
@ -245,11 +263,22 @@ class InvoiceGroupPdfHandler extends FPDF
}
$this->SetXY($clientInfoXAxis, $clientInfoYAxis);
$this->Cell(0, $this->interLigneHeader, trim(FileExportHelpers::FormatTextForExport($this->factureData['client_adress_city'])));
// Gestion Siret ou TVA intracommunautaire selon is_tva
if ($this->hasTva) {
// Avec TVA : afficher le Siret
if ($this->factureData['siret']) {
$clientInfoYAxis += $this->interLigneHeader;
$this->SetXY($clientInfoXAxis, $clientInfoYAxis);
$this->Cell(0, $this->interLigneHeader, 'Siret: ' . $this->factureData['siret']);
}
} else {
// Sans TVA : afficher N° TVA intracommunautaire
$clientInfoYAxis += $this->interLigneHeader;
$this->SetXY($clientInfoXAxis, $clientInfoYAxis);
$tvaIntracommu = isset($this->factureData['client_tva_intracommu']) ? $this->factureData['client_tva_intracommu'] : '';
$this->Cell(0, $this->interLigneHeader, 'TVA intracom : ' . $tvaIntracommu);
}
}
@ -266,15 +295,15 @@ class InvoiceGroupPdfHandler extends FPDF
$factureDateEcheance->modify('last day of next month');
$factureDateEcheance = $factureDateEcheance->format('d-m-Y');
$this->SetFont('ComicSans', '', 10);
$this->SetFont('Arial', '', 10);
$this->Cell(25, 7, 'DATE', 1, 0, 'C');
$this->Cell(104, 7, 'CLIENT', 1, 0, 'C');
$this->Cell(39, 7, 'FACTURE', 1, 0, 'C');
$this->Cell(36, 7, 'ECHEANCE', 1, 1, 'C');
$this->SetFont('ComicSans', '', 10);
$this->SetFont('Arial', '', 10);
$this->Cell(25, 7, $factureDatePaiement, 1, 0, 'C');
$this->Cell(104, 7, utf8_decode(html_entity_decode($this->factureData['group_name'])), 1, 0, 'C');
$this->Cell(104, 7, $this->SafeTextForPdf($this->factureData['group_name']), 1, 0, 'C');
$this->Cell(39, 7, $this->factureData['num'], 1, 0, 'C');
$this->Cell(36, 7, $factureDateEcheance, 1, 1, 'C');
@ -294,11 +323,16 @@ class InvoiceGroupPdfHandler extends FPDF
$this->Line(3, $this->startingYOfArticlesTable + $gapBetweenStartingOfArticlesTableAndColumnName, 207, $this->startingYOfArticlesTable + $gapBetweenStartingOfArticlesTableAndColumnName);
// les traits verticaux colonnes
$additionalMargRight = 1;
$endingLine = 233 + $additionnalheight;
$endingLine = $this->startingYOfArticlesTable + $tableHeight;
$this->Line(27 + $additionalMargRight, $this->startingYOfArticlesTable, 27 + $additionalMargRight, $endingLine);
$this->Line(142 + $additionalMargRight, $this->startingYOfArticlesTable, 142 + $additionalMargRight, $endingLine);
if ($this->hasTva) {
$this->Line(164 + $additionalMargRight, $this->startingYOfArticlesTable, 164 + $additionalMargRight, $endingLine);
$this->Line(182 + $additionalMargRight, $this->startingYOfArticlesTable, 182 + $additionalMargRight, $endingLine);
} else {
$this->Line(182 + $additionalMargRight, $this->startingYOfArticlesTable, 182 + $additionalMargRight, $endingLine);
}
}
public function DrawArticlesTableHeader()
@ -306,9 +340,9 @@ class InvoiceGroupPdfHandler extends FPDF
$additionalMargRight = 1;
$tvaValue = $this->factureData["configuration"]->tva_default;
$columnNameY = $this->startingYOfArticlesTable - 1.5;
$this->SetFont('ComicSans', '', 10);
$this->SetXY(12 + $additionalMargRight, $columnNameY);
$this->Cell(7, 10, "Date", 0, 0, 'C');
$this->SetFont('Arial', '', 10);
$this->SetXY(35 + $additionalMargRight, $columnNameY);
$this->Cell(7, 10, "", 0, 0, 'C');
$this->SetXY(30 + $additionalMargRight, $columnNameY);
$this->Cell(100, 10, "Description", 0, 0, 'C');
@ -316,18 +350,23 @@ class InvoiceGroupPdfHandler extends FPDF
$this->SetXY(143 + $additionalMargRight, $columnNameY);
$this->Cell(20, 10, "Prix Uni. HT", 0, 0, 'C');
if ($this->hasTva) {
$this->SetXY(163 + $additionalMargRight, $columnNameY);
$this->Cell(20, 10, 'TVA ' . $tvaValue . '%', 0, 0, 'C');
$this->SetXY(185, $columnNameY);
$this->Cell(20, 10, "Prix Uni. TTC", 0, 0, 'C');
} else {
$this->SetXY(185, $columnNameY);
$this->Cell(20, 10, "Montant HT", 0, 0, 'C');
}
}
public function DrawArticlesTableValue()
{
// Set espacement avant de continue
$this->SetFont('ComicSans', '', 10);
$this->SetFont('Arial', '', 10);
$devisData = $this->factureData['devis'];
$tvaValue = $this->factureData["configuration"]->tva_default;
$totalHt = 0;
@ -336,6 +375,7 @@ class InvoiceGroupPdfHandler extends FPDF
$yValue = $this->startingYOfArticlesTable + 13;
// $maxDescriptionWidth = 102;
$maxDescriptionWidth = 104.3;
// $maxDescriptionWidth = 51;
$currentIndexPosition = $this->currentIndexPosition;
for ($currentIndexPosition; $currentIndexPosition < ($this->initialIndexPosition + $this->devisCountToGet); $currentIndexPosition++) {
$currentDevis = $devisData[$currentIndexPosition];
@ -347,20 +387,32 @@ class InvoiceGroupPdfHandler extends FPDF
foreach ($products as $product) {
$valueHt = $product['produit_price'] * $product["quantite"];
if ($this->hasTva) {
$valueTtc = PriceHelpers::calculPriceWithVatValue($valueHt, $tvaValue);
$tvaAmount = $valueTtc - $valueHt;
} else {
$valueTtc = $valueHt; // Sans TVA, TTC = HT
$tvaAmount = 0;
}
$totalHt += $valueHt;
$totalTtc += $valueTtc;
$productDescription = $product["produit_description"] ?? "";
$currentDevisDefuntName = $currentDevis["defunt_nom"] ?? "";
$dateValue = "";
$dateAndDefuntName = "";
if ($productIncrement == 0) {
$dateValue = $devisDate;
$productDescription .= " de " . $currentDevis["defunt_nom"] ?? "";
}
$tvaAmount = $valueTtc - $valueHt;
$this->SetXY(4, $yValue);
// $productDescription .= " de " . $currentDevis["defunt_nom"] ?? "";
$dateAndDefuntName = $dateValue . " - " . $currentDevisDefuntName;
$this->SetXY(70, $yValue);
$this->Cell(5, 6, $currentDevisDefuntName, 0, 0);
$this->SetXY(4, $yValue + 5);
$this->Cell(5, 6, $dateValue, 0, 0);
}
$this->SetXY(30, $yValue);
$this->SetXY(30, $yValue + 5);
$productDescription = FileExportHelpers::FormatTextForExport($productDescription);
$productDescriptionWidth = $this->GetStringWidth($productDescription);
$productDescriptionWidthIsGreaterThanMaxWidth = $productDescriptionWidth > $maxDescriptionWidth;
@ -378,14 +430,20 @@ class InvoiceGroupPdfHandler extends FPDF
}
$this->SetXY(144, $yValue);
$this->SetXY(144, $yValue + 5);
$this->Cell(20, 6, number_format($valueHt, 2, '.', '') . chr(128), 0, 0, 'C');
$this->SetXY(165, $yValue);
if ($this->hasTva) {
$this->SetXY(165, $yValue + 5);
$this->Cell(20, 6, number_format($tvaAmount, 2, '.', '') . chr(128), 0, 0, 'C');
$this->SetXY(182, $yValue);
$this->SetXY(182, $yValue + 5);
$this->Cell(25, 6, number_format($valueTtc, 2, '.', '') . chr(128), 0, 1, 'C');
} else {
$this->SetXY(182, $yValue + 5);
$this->Cell(25, 6, number_format($valueHt, 2, '.', '') . chr(128), 0, 1, 'C');
}
$yValue += 6;
$totalTva += $tvaAmount;
if ($productDescriptionWidthIsGreaterThanMaxWidth) {
@ -393,6 +451,11 @@ class InvoiceGroupPdfHandler extends FPDF
}
$productIncrement++;
}
$yValue += 6;
$isLastLigneDevis = $currentIndexPosition == (($this->initialIndexPosition + $this->devisCountToGet) - 1);
if (!$isLastLigneDevis) {
$this->Line(3, $yValue, 207, $yValue);
}
}
$this->currentIndexPosition = $currentIndexPosition;
$this->initialIndexPosition = $this->currentIndexPosition;
@ -406,21 +469,21 @@ class InvoiceGroupPdfHandler extends FPDF
{
$startOfYAfterMainTable = 239;
$this->SetY($startOfYAfterMainTable);
$this->SetFont('ComicSans', '', 8);
$this->MultiCell(0, 4, utf8_decode(html_entity_decode("Paiement à votre convenance par chèque à l'ordre de " . $this->factureData['configuration']->entreprise)));
$this->MultiCell(0, 4, utf8_decode(html_entity_decode("en indiquant le numéro de facture, ou par virement :")));
$this->SetFont('Arial', '', 8);
$this->MultiCell(0, 4, $this->SafeTextForPdf("Paiement à votre convenance par chèque à l'ordre de " . $this->factureData['configuration']->entreprise));
$this->MultiCell(0, 4, $this->SafeTextForPdf("en indiquant le numéro de facture, ou par virement :"));
$this->Ln(1);
//Table IBAN
$startOftable = 3;
$this->SetX($startOftable);
$this->SetFont('ComicSans', '', 8);
$this->SetFont('Arial', '', 8);
$ibanWidth = 62;
$this->Cell($ibanWidth, 6.5, 'IBAN : FR76 1360 6000 1436 5418 1800 038', 1, 1, 'C');
$this->Cell($ibanWidth, 6.5, 'IBAN : FR76 1670 6052 4453 9757 9734 871', 1, 1, 'C');
$ibanCursorX = $this->GetX();
$this->SetX($startOftable);
$this->Cell($ibanWidth, 6.5, 'Code SWIFT : AGRI FR PP 836', 1, 1, 'C');
$this->Cell($ibanWidth, 6.5, 'Code SWIFT : AGRI FR PP867', 1, 1, 'C');
//TABLE HT
$tableWidth = 48; // Largeur totale de la 2e table (20+20)
@ -430,7 +493,7 @@ class InvoiceGroupPdfHandler extends FPDF
$startOfArrayX = $pageWidth - $tableWidth - $marginRight;
$startOfArrayY = $startOfYAfterMainTable + 0.5;
$this->SetFont('ComicSans', '', 10);
$this->SetFont('Arial', '', 10);
$totalPriceArray = $this->totalPrices;
foreach ($totalPriceArray as $label => $price) {
$this->SetXY($startOfArrayX, $startOfArrayY);
@ -449,7 +512,7 @@ class InvoiceGroupPdfHandler extends FPDF
$this->DrawBankAndTotalPriceInfo();
}
function MultiAlignCell($w, $h, $text, $border = 0, $ln = 0, $align = 'L', $fill = false)
public function MultiAlignCell($w, $h, $text, $border = 0, $ln = 0, $align = 'L', $fill = false)
{
// Store reset values for (x,y) positions
$x = $this->GetX() + $w;
@ -464,19 +527,22 @@ class InvoiceGroupPdfHandler extends FPDF
}
}
function NbLines($w, $txt)
public function NbLines($w, $txt)
{
// Compute the number of lines a MultiCell of width w will take
if (!isset($this->CurrentFont))
if (!isset($this->CurrentFont)) {
$this->Error('No font has been set');
}
$cw = $this->CurrentFont['cw'];
if ($w == 0)
if ($w == 0) {
$w = $this->w - $this->rMargin - $this->x;
}
$wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize;
$s = str_replace("\r", '', (string) $txt);
$nb = strlen($s);
if ($nb > 0 && $s[$nb - 1] == "\n")
if ($nb > 0 && $s[$nb - 1] == "\n") {
$nb--;
}
$sep = -1;
$i = 0;
$j = 0;
@ -492,22 +558,26 @@ class InvoiceGroupPdfHandler extends FPDF
$nl++;
continue;
}
if ($c == ' ')
if ($c == ' ') {
$sep = $i;
}
$l += $cw[$c];
if ($l > $wmax) {
if ($sep == -1) {
if ($i == $j)
if ($i == $j) {
$i++;
} else
}
} else {
$i = $sep + 1;
}
$sep = -1;
$j = $i;
$l = 0;
$nl++;
} else
} else {
$i++;
}
}
return $nl;
}
@ -516,38 +586,42 @@ class InvoiceGroupPdfHandler extends FPDF
{
$k = $this->k;
$hp = $this->h;
if ($style == 'F')
if ($style == 'F') {
$op = 'f';
elseif ($style == 'FD' || $style == 'DF')
} elseif ($style == 'FD' || $style == 'DF') {
$op = 'B';
else
} else {
$op = 'S';
}
$MyArc = 4 / 3 * (sqrt(2) - 1);
$this->_out(sprintf('%.2F %.2F m', ($x + $r) * $k, ($hp - $y) * $k));
$xc = $x + $w - $r;
$yc = $y + $r;
$this->_out(sprintf('%.2F %.2F l', $xc * $k, ($hp - $y) * $k));
if (strpos($corners, '2') === false)
if (strpos($corners, '2') === false) {
$this->_out(sprintf('%.2F %.2F l', ($x + $w) * $k, ($hp - $y) * $k));
else
} else {
$this->_Arc($xc + $r * $MyArc, $yc - $r, $xc + $r, $yc - $r * $MyArc, $xc + $r, $yc);
}
$xc = $x + $w - $r;
$yc = $y + $h - $r;
$this->_out(sprintf('%.2F %.2F l', ($x + $w) * $k, ($hp - $yc) * $k));
if (strpos($corners, '3') === false)
if (strpos($corners, '3') === false) {
$this->_out(sprintf('%.2F %.2F l', ($x + $w) * $k, ($hp - ($y + $h)) * $k));
else
} else {
$this->_Arc($xc + $r, $yc + $r * $MyArc, $xc + $r * $MyArc, $yc + $r, $xc, $yc + $r);
}
$xc = $x + $r;
$yc = $y + $h - $r;
$this->_out(sprintf('%.2F %.2F l', $xc * $k, ($hp - ($y + $h)) * $k));
if (strpos($corners, '4') === false)
if (strpos($corners, '4') === false) {
$this->_out(sprintf('%.2F %.2F l', ($x) * $k, ($hp - ($y + $h)) * $k));
else
} else {
$this->_Arc($xc - $r * $MyArc, $yc + $r, $xc - $r, $yc + $r * $MyArc, $xc - $r, $yc);
}
$xc = $x + $r;
$yc = $y + $r;
@ -555,8 +629,9 @@ class InvoiceGroupPdfHandler extends FPDF
if (strpos($corners, '1') === false) {
$this->_out(sprintf('%.2F %.2F l', ($x) * $k, ($hp - $y) * $k));
$this->_out(sprintf('%.2F %.2F l', ($x + $r) * $k, ($hp - $y) * $k));
} else
} else {
$this->_Arc($xc - $r, $yc - $r * $MyArc, $xc - $r * $MyArc, $yc - $r, $xc, $yc - $r);
}
$this->_out($op);
}

View File

@ -42,22 +42,30 @@ class InvoicePdfHandler extends FPDF
protected $extgstates = [];
private $thereIsOrderOrCaseNumber = false;
private $startingYOfArticlesTable = 100;
private $endingYOfArticlesTable = 230;
private $articleTablesHeight = 130;
public $interLigneHeader = 5;
public $hasTva = true;
public function Header()
{
if ($this->logo != "nothing") {
$this->Image($this->logoPath . "logo.png", 4, 2, 50, 35);
$this->AddWatermark();
$this->Image($this->logoPath . "logo.png", 2, 10, 75, 25);
//$this->AddWatermark();
} else {
$this->Cell(55, 30, '');
}
}
public function SafeTextForPdf($text)
{
if (empty($text)) {
return '';
}
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML401, 'UTF-8');
return iconv('UTF-8', 'ISO-8859-1//IGNORE', $text);
}
public function AddWatermark()
{
$this->SetAlpha(0.2);
@ -97,21 +105,23 @@ class InvoicePdfHandler extends FPDF
$this->SetY(-34);
$this->SetFont('ComicSans', '', 7);
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('Tout retard de paiement entraînera de plein droit une pénalité de retard de 3 fois le taux légal ')));
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('(Loi 2008-776 du 4 août 2008) et une indemnité forfaitaire de 40 EUR pour frais de recouvrement sera appliquée.')));
$this->MultiCell(0, 4, $this->SafeTextForPdf('Tout retard de paiement entraînera de plein droit une pénalité de retard de 3 fois le taux légal '));
$this->MultiCell(0, 4, $this->SafeTextForPdf('(Loi 2008-776 du 4 août 2008) et une indemnité forfaitaire de 40 EUR pour frais de recouvrement sera appliquée.'));
$this->Ln(1);
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('Si les frais de recouvrement sont supérieurs à ce montant forfaitaire, une indemnisation complémentaire')));
$this->MultiCell(0, 4, utf8_decode(html_entity_decode('sera due sur présentation de justificatifs (articles L.441-3 et L.441-6 du code de commerce).')));
$this->MultiCell(0, 4, $this->SafeTextForPdf('Si les frais de recouvrement sont supérieurs à ce montant forfaitaire, une indemnisation complémentaire'));
$this->MultiCell(0, 4, $this->SafeTextForPdf('sera due sur présentation de justificatifs (articles L.441-3 et L.441-6 du code de commerce).'));
$this->SetY(-10);
$this->SetFont('ComicSans', '', 7);
$this->Cell(0, 10, utf8_decode(html_entity_decode($this->factureData['configuration']->legal_one)), 0, 0, 'C');
$this->Cell(0, 10, $this->SafeTextForPdf($this->factureData['configuration']->legal_one), 0, 0, 'C');
}
public function InvoicePdfFactory(array $factureData, $logo = null)
{
$this->factureData = $factureData;
$this->logo = $logo;
// Déterminer si la TVA est applicable
$this->hasTva = isset($factureData['is_tva']) ? $factureData['is_tva'] : true;
}
public function MutlipleInvoicePdfFactory(array $multipleInvoiceData, $logo = null)
@ -155,6 +165,7 @@ class InvoicePdfHandler extends FPDF
$this->Cell(0, $this->interLigneHeader, FileExportHelpers::FormatTextForExport('Tél : ') . FileExportHelpers::FormatTextForExport($this->factureData['configuration']->telephone), 0, 1);
$this->Cell(0, $this->interLigneHeader, 'Mail : ' . $this->factureData['configuration']->mail, 0, 1);
}
private function DrawInvoiceClientInfo()
{
$this->SetFont('ComicSans', '', 10);
@ -203,7 +214,17 @@ class InvoicePdfHandler extends FPDF
$this->Cell(0, $this->interLigneHeader, trim(FileExportHelpers::FormatTextForExport($this->factureData['client_adress_city'])));
$clientInfoYAxis += $this->interLigneHeader;
$this->SetXY($clientInfoXAxis, $clientInfoYAxis);
// Gestion Siret ou TVA intracommunautaire selon is_tva
if ($this->hasTva) {
// Avec TVA : afficher le Siret
$this->Cell(0, $this->interLigneHeader, 'Siret: ' . $this->factureData['siret']);
} else {
// Sans TVA : afficher N° TVA intracommunautaire
$tvaIntracommu = isset($this->factureData['client_tva_intracommu']) ? $this->factureData['client_tva_intracommu'] : '';
$this->Cell(0, $this->interLigneHeader, 'TVA intracom : ' . $tvaIntracommu);
}
$clientInfoYAxis += $this->interLigneHeader;
$this->SetXY($clientInfoXAxis, $clientInfoYAxis);
$this->Cell(0, $this->interLigneHeader, FileExportHelpers::FormatTextForExport('Mail : ') . $this->factureData['client_mail']);
@ -236,7 +257,7 @@ class InvoicePdfHandler extends FPDF
$this->SetFont('ComicSans', '', 10);
$this->Cell(25, 7, $factureDatePaiement, 1, 0, 'C');
$this->Cell(104, 7, utf8_decode(html_entity_decode($this->factureData['client_nom'])), 1, 0, 'C');
$this->Cell(104, 7, $this->SafeTextForPdf($this->factureData['client_nom']), 1, 0, 'C');
$this->Cell(39, 7, $this->factureData['num'], 1, 0, 'C');
$this->Cell(36, 7, $factureDateEcheance, 1, 1, 'C');
@ -275,31 +296,40 @@ class InvoicePdfHandler extends FPDF
$endingLine = $this->thereIsOrderOrCaseNumber ? 231 : 230; // mois +1 pour le groupe
$this->Line(27 + $additionalMargRight, $this->startingYOfArticlesTable, 27 + $additionalMargRight, $endingLine);
$this->Line(142 + $additionalMargRight, $this->startingYOfArticlesTable, 142 + $additionalMargRight, $endingLine);
if ($this->hasTva) {
$this->Line(164 + $additionalMargRight, $this->startingYOfArticlesTable, 164 + $additionalMargRight, $endingLine);
$this->Line(182 + $additionalMargRight, $this->startingYOfArticlesTable, 182 + $additionalMargRight, $endingLine);
} else {
$this->Line(182 + $additionalMargRight, $this->startingYOfArticlesTable, 182 + $additionalMargRight, $endingLine);
}
}
private function DrawArticlesTableHeader()
{
$additionalMargRight = 1;
$tvaDefault = $this->factureData["configuration"]->tva_default;
$clientTvaStatus = isset($this->factureData["tva"]) ? (int)$this->factureData["tva"] : 1;
// Déterminer le libellé TVA selon le statut du client
$tvaLabel = ($clientTvaStatus === 0) ? "TVA 0%" : "TVA " . $tvaDefault . "%";
$tvaValue = $this->factureData["configuration"]->tva_default;
$columnNameY = $this->startingYOfArticlesTable - 1;
$this->SetFont('ComicSans', '', 10);
$this->SetXY(12 + $additionalMargRight, $columnNameY);
$this->Cell(7, 10, "Date", 0, 0, 'C');
$this->Cell(7, 10, "", 0, 0, 'C');
$this->SetXY(30 + $additionalMargRight, $columnNameY);
$this->Cell(100, 10, "Description", 0, 0, 'C');
$this->SetXY(143 + $additionalMargRight, $columnNameY);
$this->Cell(20, 10, "Prix Uni. HT", 0, 0, 'C');
if ($this->hasTva) {
$this->SetXY(163 + $additionalMargRight, $columnNameY);
$this->Cell(20, 10, $tvaLabel, 0, 0, 'C');
$this->Cell(20, 10, 'TVA ' . $tvaValue . '%', 0, 0, 'C');
$this->SetXY(185, $columnNameY);
$this->Cell(20, 10, "Prix Uni. TTC", 0, 0, 'C');
} else {
$this->SetXY(185, $columnNameY);
$this->Cell(20, 10, "Montant HT", 0, 0, 'C');
}
}
public function DrawArticlesTableValueAndReturnTotalPrice()
@ -308,14 +338,7 @@ class InvoicePdfHandler extends FPDF
$devisDate = $this->factureData['devis_date'];
$devisDate = DateTime::createFromFormat('Y-m-d', $devisDate);
$devisDate = $devisDate->format('d-m-Y');
$tvaDefault = $this->factureData["configuration"]->tva_default;
$clientTvaStatus = isset($this->factureData["tva"]) ? (int)$this->factureData["tva"] : 1;
// Déterminer le taux de TVA à appliquer selon le statut du client
$tvaValue = ($clientTvaStatus === 0) ? 0 : $tvaDefault;
$tvaLabel = ($clientTvaStatus === 0) ? "0%" : $tvaDefault . "%";
$tvaValue = $this->factureData["configuration"]->tva_default;
$totalHt = 0;
$totalTtc = 0;
$totalTva = 0;
@ -326,23 +349,19 @@ class InvoicePdfHandler extends FPDF
foreach ($products as $product) {
$valueHt = $product['produit_price'] * $product['quantite'];
// Calculer la TVA selon le statut du client
if ($clientTvaStatus === 0) {
// Client exonéré - pas de TVA
$valueTtc = $valueHt;
$tvaAmount = 0;
} else {
// Client soumis à la TVA - utiliser le calcul normal
if ($this->hasTva) {
$valueTtc = PriceHelpers::calculPriceWithVatValue($valueHt, $tvaValue);
$tvaAmount = $valueTtc - $valueHt;
} else {
$valueTtc = $valueHt; // Sans TVA, TTC = HT
$tvaAmount = 0;
}
$totalHt += $valueHt;
$totalTtc += $valueTtc;
$totalTva += $tvaAmount;
$productDescription = $product["produit_description"];
$dateValue = "";
if ($product === end($products)) {
$dateValue = $devisDate;
$productDescription .= " de " . FileExportHelpers::GetSexeLabel($this->factureData['defunt_sexe']) . ' ' . $this->factureData["defunt_nom"];
@ -350,22 +369,40 @@ class InvoicePdfHandler extends FPDF
$this->SetXY(5, $yValue);
$this->Cell(5, 6, $dateValue, 0, 0);
$this->SetXY(30, $yValue);
$this->MultiAlignCell($maxDescriptionWidth, 6, utf8_decode(html_entity_decode($productDescription)), 0, '0', );
$this->MultiAlignCell($maxDescriptionWidth, 6, $this->SafeTextForPdf($productDescription), 0, '0', );
$this->SetXY(144, $yValue);
$this->Cell(20, 6, number_format($valueHt, 2, '.', '') . chr(128), 0, 0, 'C');
if ($this->hasTva) {
$this->SetXY(165, $yValue);
$this->Cell(20, 6, number_format($tvaAmount, 2, '.', '') . chr(128), 0, 0, 'C');
$this->SetXY(182, $yValue);
$this->Cell(25, 6, number_format($valueTtc, 2, '.', '') . chr(128), 0, 1, 'C');
$yValue += 12;
} else {
$this->SetXY(182, $yValue);
$this->Cell(25, 6, number_format($valueHt, 2, '.', '') . chr(128), 0, 1, 'C');
}
$yValue += 12;
$totalTva += $tvaAmount;
}
// Construction du tableau de retour selon hasTva
if ($this->hasTva) {
return [
"TOTAL HT" => $totalHt,
"TVA " . $tvaLabel => $totalTva,
"TVA " . $tvaValue . "%" => $totalTva,
"TOTAL TTC" => $totalTtc
];
} else {
return [
"TOTAL HT" => $totalHt
];
}
}
private function DrawBankAndTotalPriceInfo($totalPriceArray)
@ -373,8 +410,8 @@ class InvoicePdfHandler extends FPDF
$startOfYAfterMainTable = 236;
$this->SetY($startOfYAfterMainTable);
$this->SetFont('ComicSans', '', 8);
$this->MultiCell(0, 4, utf8_decode(html_entity_decode("Paiement à votre convenance par chèque à l'ordre de " . $this->factureData['configuration']->entreprise)));
$this->MultiCell(0, 4, utf8_decode(html_entity_decode("en indiquant le numéro de facture, ou par virement :")));
$this->MultiCell(0, 4, $this->SafeTextForPdf("Paiement à votre convenance par chèque à l'ordre de " . $this->factureData['configuration']->entreprise));
$this->MultiCell(0, 4, $this->SafeTextForPdf("en indiquant le numéro de facture, ou par virement :"));
$this->Ln(1);
$startOftable = 3;

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()
@ -108,17 +113,7 @@ class InvoicePdfService
}
$pdfFilename = $prefixPdf."_".$pdfFilename;
$filenames = [];
foreach($factureFolders as $folder) {
try {
$storage->newFolder($folder);
} catch(\OCP\Files\NotPermittedException $e) {
}
$ff_pdf = $folder.$pdfFilename.'.pdf';
$storage->newFile($ff_pdf);
$file_pdf = $storage->get($ff_pdf);
$file_pdf->putContent($pdfContent);
$filenames[] = $ff_pdf;
}
$filenames = $this->savePdfToFolders($factureFolders, $pdfFilename, $pdfContent, $storage);
$this->gestionBdd->setFactureGeneratedDate($factureId);
return [
"content" => $pdfContent,
@ -174,6 +169,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 +201,7 @@ class InvoicePdfService
$pdfContent = $pdf->Output('', 'S');
$pdfFilename = $pdf->GetInvoiceFilename();
$filenames = [];
foreach($factureFolders as $folder) {
try {
$storage->newFolder($folder);
} catch(\OCP\Files\NotPermittedException $e) {
}
$ff_pdf = $folder.$pdfFilename.'.pdf';
$storage->newFile($ff_pdf);
$file_pdf = $storage->get($ff_pdf);
$file_pdf->putContent($pdfContent);
$filenames[] = $ff_pdf;
}
$filenames = $this->savePdfToFolders($factureFolders, $pdfFilename, $pdfContent, $storage);
$this->gestionBdd->setFactureGeneratedDate($factureId);
return [
"content" => $pdfContent,
@ -218,6 +209,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) {
@ -249,16 +253,10 @@ class InvoicePdfService
}
$clientRacineFolder = $racinePath.'CLIENTS/'.mb_strtoupper($clientNameInFolder, 'UTF-8').'/';
$filename = "FACTURE".'_'.$pdf->GetMultipleInvoiceFilename($month, $year);
$filenamePath = $clientRacineFolder.$filename.'.pdf';
$pdfContent = $pdf->Output('', 'S');
try {
$storage->newFolder($clientRacineFolder);
} catch(\OCP\Files\NotPermittedException $e) {
}
$storage->newFile($filenamePath);
$file_pdf = $storage->get($filenamePath);
$file_pdf->putContent($pdfContent);
return $filenamePath;
$singleFolderArray = [$clientRacineFolder];
$filenames = $this->savePdfToFolders($singleFolderArray, $filename, $pdfContent, $storage);
return $filenames[0];
}
public function generateInvoiceRecap($filter, $filterType, $date, $idNextCloud)
@ -338,4 +336,315 @@ 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 = [];
$filenames = $this->savePdfToFolders($factureFolders, $pdfFilename, $pdfContent, $storage);
$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;
}
}
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).";
}
/**
* Fonction générique pour sauvegarder un fichier PDF dans plusieurs dossiers
*
* @param array $factureFolders Liste des dossiers sauvegarder
* @param string $pdfFilename Nom du fichier PDF (sans extension)
* @param string $pdfContent Contenu binaire du PDF
* @param mixed $storage Instance de stockage Nextcloud
* @return array Liste des chemins des fichiers créés
*/
private function savePdfToFolders(array $factureFolders, string $pdfFilename, string $pdfContent, $storage): array
{
$filenames = [];
foreach ($factureFolders as $folder) {
// Créer le chemin complet étape par étape
$this->ensurePathExists($storage, $folder);
$ff_pdf = $folder . $pdfFilename . '.pdf';
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) {
// Supprimez ce var_dump pour éviter l'erreur de headers
// var_dump("yyyyyyyyyyyyyyyyyyyy".$e->getMessage());
error_log("ERROR on file '$ff_pdf': " . $e->getMessage());
continue;
}
}
return $filenames;
}
private function ensurePathExists($storage, $path)
{
$parts = explode('/', trim($path, '/'));
$currentPath = '';
foreach ($parts as $part) {
if (empty($part)) {
continue;
}
$currentPath .= '/' . $part;
try {
if (!$storage->nodeExists($currentPath)) {
$storage->newFolder($currentPath);
}
} catch (\Throwable $e) {
error_log("Cannot create folder '$currentPath': " . $e->getMessage());
}
}
}
}

View File

@ -29,24 +29,27 @@ namespace OCA\Gestion\Service;
use OCA\Gestion\Constants\BddConstant;
use OCP\Files\IRootFolder;
class MailerService {
class MailerService
{
private $adminStorage;
public function __construct(
IRootFolder $rootFolder,
){
) {
$this->adminStorage = $rootFolder->getUserFolder(BddConstant::DEFAULT_ADMIN_ID_NEXTCLOUD);
}
public function getFooterContent ($userName = "Johann"){
public function getFooterContent($userName = "DEKINDT Vanessa")
{
$wish = "<p>Vous en souhaitant bonne réception. </p>";
$cordialement = "<p> Cordialement,</p>";
$userName = "<p> {$userName} </p>" ;
$signatureImage = $this->getSignatureHtmlEmailContent();
return $wish . $cordialement .$userName . $signatureImage ;
// $signatureImage = $this->getSignatureHtmlEmailContent();
// return $wish . $cordialement .$userName . $signatureImage ;
return $wish . $cordialement .$userName ;
}
private function getSignatureHtmlEmailContent (){
private function getSignatureHtmlEmailContent()
{
$signatureImage = $this->getSignatureContent();
if (!$signatureImage) {
return "";
@ -54,14 +57,15 @@ class MailerService {
return "<img width='170' height='80' style= 'width: 170;height: 80px;display:block;' src='data:image/jpeg;base64,".base64_encode($signatureImage)."'>" ;
}
private function getSignatureContent(){
try{
if(isset($this->adminStorage)){
private function getSignatureContent()
{
try {
if(isset($this->adminStorage)) {
$file = $this->adminStorage->get('/.gestion/sign.jpg');
return $file->getContent();
}
} catch(\OCP\Files\NotFoundException $e) {
}
catch(\OCP\Files\NotFoundException $e) {}
return false;
}

View File

@ -2,6 +2,7 @@
use OC\URLGenerator;
use OCA\Gestion\Helpers\PriceHelpers;
$facture = $_['facture'];
$factureOrderNumber = $facture->facture_order_number == '' ? '-' : $facture->facture_order_number;
$factureCaseNumber = $facture->facture_case_number == '' ? '-' : $facture->facture_case_number;
@ -11,7 +12,7 @@ $currentConfig = json_decode($_['configuration'])[0];
<div class="bootstrap-iso">
<div id="factureId" data-id="<?php echo $facture->id; ?>"></div>
<div id="factureIdentifier" data-id="<?php echo $facture->id; ?>"></div>
<h2 class="mt-3 mb-3 text-center"> <?php echo ('Facture n° '.$facture->num); ?>
<h2 class="mt-3 mb-3 text-center"> <?php echo('Facture n° '.$facture->num); ?>
</h2>
<hr />
<div class="row">
@ -34,7 +35,7 @@ $currentConfig = json_decode($_['configuration'])[0];
} else {
echo "<span style='font-size:12px' id='Company-logo' data-html2canvas-ignore><b><center>" . $l->t('You can add your company logo here.') . "</center></b><br/><i>" . $l->t('To add a logo, drop the logo.png file in ".gestion" folder at the root of your Nextcloud Files app. Remember to set "Show hidden files".') . "</i><br/><br/><center>" . $l->t('This message will not appear on generated PDF.') . "</center></span>";
}
?>
?>
</div>
<div class="col-5 h-100 m-0" style="min-height:250px;">
<h5 class="p-3 m-0 text-dark text-center border border-2 border-dark"><?php p($l->t('TO')); ?>
@ -42,7 +43,7 @@ $currentConfig = json_decode($_['configuration'])[0];
<p style="min-height:180px;"
class="p-3 m-0 h-100 text-center text-dark text-center border border-top-0 border-2 border-dark">
<span><?php echo $facture->group_name; ?></span><br />
<?php if($isFactureClientGroup == false){
<?php if($isFactureClientGroup == false) {
?>
<span><?php echo $facture->client_name; ?></span><br />
<?php }?>
@ -88,7 +89,7 @@ $currentConfig = json_decode($_['configuration'])[0];
<tbody>
<?php
$devisList = $facture->devisList;
foreach ($devisList as $currentDevis) {
foreach ($devisList as $currentDevis) {
?>
<tr>
<td><?php echo $currentDevis->devis_full_number; ?></td>
@ -113,7 +114,7 @@ $currentConfig = json_decode($_['configuration'])[0];
class="mb-2 btn btn-outline-success sendmail"
><?php p($l->t('Send by email'));?></button>
</div>
<div class="mt-0 table-responsive">
<div class="mt-0 table-responsive">
<table id="totalFactureGroupPrice" class="table table-striped table-xl">
<thead class="bg-dark text-white">
<tr>
@ -125,14 +126,31 @@ $currentConfig = json_decode($_['configuration'])[0];
</thead>
<tbody>
<tr>
<td class="text-center"><?php echo (PriceHelpers::formatDecimalPrice($facture->totalHt).'€'); ?></td>
<td class="text-center"><?php echo (PriceHelpers::formatDecimalPrice($currentConfig->tva_default).'€'); ?></td>
<td class="text-center"><?php echo (PriceHelpers::formatDecimalPrice($facture->totalTva).'€'); ?></td>
<td class="text-center"><?php echo (PriceHelpers::formatDecimalPrice($facture->totalTtc).'€'); ?></td>
<td class="text-center"><?php echo PriceHelpers::formatDecimalPrice($facture->totalHt).'€'; ?></td>
<td class="text-center">
<?php
// Affichage simple : Exonéré ou taux par défaut
if (isset($facture->tvaInfo) && $facture->tvaInfo->is_exempt) {
echo 'Exonéré';
} else {
echo PriceHelpers::formatDecimalPrice($facture->tvaInfo->rate).'%';
}
?>
</td>
<td class="text-center">
<?php
if (isset($facture->tvaInfo) && $facture->tvaInfo->is_exempt) {
echo '0,00€';
} else {
echo PriceHelpers::formatDecimalPrice($facture->totalTva).'€';
}
?>
</td>
<td class="text-center"><?php echo PriceHelpers::formatDecimalPrice($facture->totalTtc).'€'; ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col m-0 pb-0 alert alert-info text-center">
<p><span id="mentions_default"><?php p($l->t('Please set in global configuration')); ?></span></p>
</div>

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",