feat generate recap devis

This commit is contained in:
Tolotsoa 2025-09-02 20:49:17 +03:00
parent 90d9b5a52c
commit a278022748
16 changed files with 5938 additions and 4247 deletions

View File

@ -1,4 +1,5 @@
<?php
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
@ -72,6 +73,7 @@ return [
['name' => 'page#exportDevisToFacture', 'url' => '/exportDevisToFacture', 'verb' => 'POST'],
['name' => 'page#exportFactureToPdf', 'url' => '/facture/exportFactureToPdf', 'verb' => 'POST'],
['name' => 'page#exportFactureByClientAndMonthYearToPdf', 'url' => '/facture/exportFactureByClientAndMonthYearToPdf', 'verb' => 'POST'],
['name' => 'page#exportDevisRecap', 'url' => '/devis/exportDevisRecap', 'verb' => 'POST'],
['name' => 'page#getProduits', 'url' => '/getProduits', 'verb' => 'PROPFIND'],
['name' => 'page#getProduitsById', 'url' => '/getProduitsById', 'verb' => 'POST'],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
<?php
namespace OCA\Gestion\Service\Devis;
use OCA\Gestion\Db\Bdd;
use OCA\Gestion\Constants\MultipleFactureTypeConstant;
/**
* Classe responsable du traitement et de la préparation des données de devis
*/
class DevisDataProcessor
{
/** @var Bdd */
private $gestionBdd;
public function __construct(Bdd $gestionBdd)
{
$this->gestionBdd = $gestionBdd;
}
public function prepareDevisData($devisData, $currentConfig, $filter, $idNextCloud)
{
$data_devis = [];
foreach ($devisData as $devis) {
$devis_temp = $this->createDevisStructure($devis, $currentConfig);
$devis_temp = $this->calculateDevisAmounts($devis_temp, $devis, $filter, $idNextCloud);
$data_devis[] = $devis_temp;
}
return $data_devis;
}
public function createDevisStructure($devis, $currentConfig)
{
return array(
'devis_id' => $devis['devis_id'],
'devis_full_number' => $devis['devis_full_number'],
'calendar_uuid' => $devis['calendar_uuid'],
'num_commande' => '',
'devis_date' => $devis['devis_date'],
'lieu_nom' => $devis['lieu_nom'],
'lieu_adresse' => $devis['lieu_adresse'],
'defunt_nom' => $devis['defunt_nom'],
'defunt_sexe' => $devis['defunt_sexe'],
'client_nom' => $devis['client_nom'],
'client_entreprise' => $devis['client_entreprise'],
'client_adresse' => $devis['client_adresse'],
'group_name' => $devis['group_name'],
'group_address' => $devis['group_address'],
'group_postal_code' => $devis['group_postal_code'],
'group_city' => $devis['group_city'],
'group_email' => $devis['group_email'],
'group_siret_number' => $devis['group_siret_number'],
'montant_htc' => 0,
'tva' => $currentConfig->tva_default,
'montant_tva' => 0,
'montant_ttc' => 0
);
}
public function calculateDevisAmounts($devis_temp, $devis, $filter, $idNextCloud)
{
$produits = json_decode($this->gestionBdd->getListProduit($devis['devis_id'], $idNextCloud));
foreach ($produits as $produit) {
$htPrice = $this->getProductPrice($produit, $devis, $filter);
$devis_temp['montant_htc'] += $htPrice * $produit->quantite;
}
$devis_temp['montant_tva'] = ($devis_temp['montant_htc'] * $devis_temp['tva']) / 100;
$devis_temp['montant_ttc'] = $devis_temp['montant_tva'] + $devis_temp['montant_htc'];
return $devis_temp;
}
public function getProductPrice($produit, $devis, $filter)
{
$htPrice = $produit->prix_unitaire;
if ($devis['group_name'] != null) {
$price = $this->gestionBdd->getProductPriceByClientGroupId($filter, $produit->id);
if ($price != null) {
$htPrice = $price;
}
}
return $htPrice;
}
public function getClientInfoForDevis($devis, $filterType)
{
if ($filterType === 'group' && !empty($devis['group_name'])) {
return [
'name' => 'Groupe ' . $devis['group_name'],
'address' => $devis['group_address'],
'city' => $devis['group_postal_code'] . ' ' . $devis['group_city'],
'siret' => $devis['group_siret_number'],
'email' => $devis['group_email']
];
} else {
$clientAddresses = \OCA\Gestion\Helpers\FileExportHelpers::GetAddressAndCityFromAddress($devis['client_adresse']);
return [
'name' => $devis['client_nom'],
'address' => $clientAddresses['address'],
'city' => $clientAddresses['city'],
'siret' => '',
'email' => ''
];
}
}
public function generateCsvContent($devisData)
{
$headers = [
'Numéro Devis',
'Date',
'Client Nom',
'Client Entreprise',
'Défunt',
'Lieu',
'Thanatopracteur',
'Commentaire',
'UUID Calendrier'
];
$csvContent = $this->arrayToCsv($headers);
foreach ($devisData as $devis) {
$row = [
$devis['devis_full_number'] ?? '',
$devis['devis_date'] ?? '',
$devis['client_nom'] ?? '',
$devis['client_entreprise'] ?? '',
$devis['defunt_nom'] ?? '',
$devis['lieu_nom'] ?? '',
trim(($devis['thanato_nom'] ?? '') . ' ' . ($devis['thanato_prenom'] ?? '')),
$devis['devis_comment'] ?? '',
$devis['calendar_uuid'] ?? ''
];
$csvContent .= $this->arrayToCsv($row);
}
return $csvContent;
}
public function getClientNameForFolder($firstDevis, $filterType)
{
if ($filterType === MultipleFactureTypeConstant::GROUP_FILTER_TYPE && !empty($firstDevis["group_name"])) {
return $firstDevis["group_name"];
}
return $firstDevis["client_nom"] ?? 'CLIENT_INCONNU';
}
public function getRecapFilename($month, $year)
{
$monthStr = str_pad($month, 2, '0', STR_PAD_LEFT);
return $year . $monthStr;
}
private function arrayToCsv($array)
{
$output = fopen('php://temp', 'r+');
fputcsv($output, $array, ';');
rewind($output);
$csvLine = fgets($output);
fclose($output);
return $csvLine;
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace OCA\Gestion\Service\Devis;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
class DevisPdfGenerator
{
/** @var IRootFolder */
private $rootFolder;
/** @var DevisPdfLayoutManager */
private $layoutManager;
/** @var DevisPdfTableRenderer */
private $tableRenderer;
public const DEFAULT_NEXTCLOUD_ADMIN = 'admin';
public function __construct(IRootFolder $rootFolder)
{
$this->rootFolder = $rootFolder;
$this->layoutManager = new DevisPdfLayoutManager($rootFolder);
$this->tableRenderer = new DevisPdfTableRenderer();
}
public function generatePdfDocument($storage, $currentConfig, $data_devis, $clientInfo, $month, $year, $montant)
{
$configurationAddresses = \OCA\Gestion\Helpers\FileExportHelpers::GetAddressAndCityFromAddress($currentConfig->adresse);
$configurationAddress = $configurationAddresses["address"];
$configurationAddressCity = $configurationAddresses["city"];
$pdf = $this->createPdfInstance();
$folderPath = $this->createDestinationFolder($storage, $currentConfig, $data_devis[0], $month, $year);
// Créer le contexte une seule fois
$context = new PageContext($pdf, $currentConfig, $configurationAddress, $configurationAddressCity, $data_devis, $clientInfo, $year, $montant);
$this->generatePdfPages($context);
return $this->savePdfFile($storage, $pdf, $folderPath, $clientInfo, $month, $year);
}
public function createPdfInstance()
{
$pdf = new \FPDF();
$pdf->AddFont('Arial', '', 'Comic Sans MS.php');
$pdf->AddFont('Arial', 'B', 'comic-sans-bold.php');
return $pdf;
}
public function createDestinationFolder($storage, $currentConfig, $firstDevis, $month, $year)
{
$date_temp = date("t-m-Y", strtotime($firstDevis['devis_date']));
$formatter = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::LONG, \IntlDateFormatter::NONE);
$date_formated = $formatter->format(\DateTime::createFromFormat('d-m-Y', $date_temp));
$folderPath = html_entity_decode($currentConfig->path) . '/DOCUMENTS RECAPITULATIFS/' . $year . '/' .
str_pad($month, 2, '0', STR_PAD_LEFT) . ' ' .
strtoupper(\OCA\Gestion\Helpers\FileExportHelpers::ConvertSpecialChar(explode(' ', $date_formated)[1])) . '/';
try {
$storage->newFolder($folderPath);
} catch(NotPermittedException $e) {
// Le dossier existe déjà
}
return $folderPath;
}
private function generatePdfPages(PageContext $context)
{
$pagination = $this->calculatePagination(count($context->dataDevis));
$totals = ['ht' => 0, 'tva' => 0, 'ttc' => 0];
for ($num_page = 1; $num_page <= $pagination['nb_pages']; $num_page++) {
$this->generateSinglePage($context, $num_page, $pagination, $totals);
}
}
private function calculatePagination($totalItems)
{
return [
'nb_pages' => ceil($totalItems / 26),
'items_per_page' => 26,
'current_index' => 0
];
}
// VOICI LA FONCTION CORRIGÉE - 4 paramètres au lieu de 10
private function generateSinglePage(PageContext $context, $num_page, &$pagination, &$totals)
{
$context->pdf->AddPage();
$context->pdf->SetAutoPagebreak(false);
$context->pdf->SetMargins(0, 0, 10);
$this->layoutManager->addPageHeader($context->pdf, $context->currentConfig, $context->configurationAddress, $context->configurationAddressCity);
$this->layoutManager->addPageNumber($context->pdf, $num_page, $pagination['nb_pages']);
$this->layoutManager->addClientInfoSection($context->pdf, $context->clientInfo, $context->dataDevis[0]);
$this->layoutManager->addDocumentTitle($context->pdf, $context->dataDevis[0], $context->year);
$itemsToProcess = min($pagination['items_per_page'], count($context->dataDevis) - $pagination['current_index']);
$config = [
'pagination' => $pagination,
'itemsToProcess' => $itemsToProcess,
'numPage' => $num_page,
'nbPage' => $pagination['nb_pages'],
'totals' => &$totals,
'sansMontant' => $context->montant
];
$this->tableRenderer->createDevisTable(
$context->pdf,
$context->dataDevis,
$config
);
$this->layoutManager->addLegalFooter($context->pdf, $context->currentConfig);
$pagination['current_index'] += $itemsToProcess;
}
private function savePdfFile($storage, $pdf, $folderPath, $clientInfo, $month, $year)
{
$filename = $this->generateDevisRecapFilename($clientInfo, $month, $year);
$fullPdfPath = $folderPath . $filename . '.pdf';
$storage->newFile($fullPdfPath);
$pdfContent = $pdf->Output('', 'S');
$file_pdf = $storage->get($fullPdfPath);
$file_pdf->putContent($pdfContent);
return $fullPdfPath;
}
private function generateDevisRecapFilename($clientInfo, $month, $year)
{
$monthName = strtoupper(\OCA\Gestion\Helpers\FileExportHelpers::ConvertSpecialChar(date('F', mktime(0, 0, 0, $month, 10))));
$clientName = strtoupper(\OCA\Gestion\Helpers\FileExportHelpers::ConvertSpecialChar($clientInfo['name']));
return $clientName . '_RECAP_DEVIS_' . $monthName . '_' . $year;
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace OCA\Gestion\Service\Devis;
use OCP\Files\IRootFolder;
class DevisPdfLayoutManager
{
/** @var IRootFolder */
private $rootFolder;
private $defaultImagePath = "/var/www/html/data/admin/files/.gestion/";
public const DEFAULT_NEXTCLOUD_ADMIN = 'admin';
public function __construct(IRootFolder $rootFolder)
{
$this->rootFolder = $rootFolder;
}
public function addPageHeader($pdf, $config, $address, $city)
{
if ($this->doesLogoExist()) {
$pdf->Image($this->defaultImagePath."logo.png", 10, 10, 75, 25);
}
$this->addCompanyInfo($pdf, $config, $address, $city);
}
public function addPageNumber($pdf, $currentPage, $totalPages)
{
if ($totalPages > 1) {
$pdf->SetXY(120, 5);
$pdf->SetFont("Arial", "B", 9);
$pdf->Cell(160, 8, $currentPage . '/' . $totalPages, 0, 0, 'C');
}
}
public function addClientInfoSection($pdf, $clientInfo, $firstDevis)
{
$date_temp = date("t-m-Y", strtotime($firstDevis['devis_date']));
$formatter = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::LONG, \IntlDateFormatter::NONE);
$date_formated = $formatter->format(\DateTime::createFromFormat('d-m-Y', $date_temp));
$this->addClientInfo($pdf, $clientInfo, $date_formated);
}
public function addDocumentTitle($pdf, $firstDevis, $year)
{
$date_temp = date("t-m-Y", strtotime($firstDevis['devis_date']));
$formatter = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::LONG, \IntlDateFormatter::NONE);
$date_formated = $formatter->format(\DateTime::createFromFormat('d-m-Y', $date_temp));
$pdf->SetFont("Arial", "B", 14);
$pdf->SetXY(10, 90);
$pdf->Cell(
0,
10,
"RECAPITULATIF DEVIS " .
strtoupper(\OCA\Gestion\Helpers\FileExportHelpers::ConvertSpecialChar(explode(' ', $date_formated)[1])) . " " . $year,
0,
0,
'C'
);
}
public function addLegalFooter($pdf, $config)
{
$y0 = 260;
$pageWidth = $pdf->GetPageWidth();
$pdf->SetFont('Arial', '', 6);
$pdf->SetXY(1, $y0 + 4);
$pdf->Cell($pageWidth, 5, mb_convert_encoding(html_entity_decode($config->legal_one), 'ISO-8859-1', 'UTF-8'), 0, 0, 'C');
$pdf->SetXY(1, $y0 + 8);
$pdf->Cell($pageWidth, 5, mb_convert_encoding(html_entity_decode($config->legal_two), 'ISO-8859-1', 'UTF-8'), 0, 0, 'C');
$pdf->SetXY(1, $y0 + 12);
$pdf->Cell($pageWidth, 5, mb_convert_encoding(html_entity_decode($config->telephone), 'ISO-8859-1', 'UTF-8'), 0, 0, 'C');
}
private function addCompanyInfo($pdf, $config, $address, $city)
{
$companyInfoXAxis = 10;
$companyInfoYAxis = 40;
$pdf->SetFont('Arial', '', 11);
$pdf->SetXY($companyInfoXAxis, $companyInfoYAxis);
$pdf->Cell(0, 7, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($config->entreprise));
$companyInfoYAxis += 7;
$pdf->SetXY($companyInfoXAxis, $companyInfoYAxis);
$pdf->Cell(0, 7, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($address));
$companyInfoYAxis += 7;
$pdf->SetXY($companyInfoXAxis, $companyInfoYAxis);
$pdf->Cell(0, 7, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($city));
$companyInfoYAxis += 7;
$pdf->SetXY($companyInfoXAxis, $companyInfoYAxis);
$pdf->Cell(0, 7, mb_convert_encoding(html_entity_decode('Tél : '), 'ISO-8859-1', 'UTF-8') . \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($config->telephone));
$companyInfoYAxis += 7;
$pdf->SetXY($companyInfoXAxis, $companyInfoYAxis);
$pdf->Cell(0, 7, 'Mail : ' . $config->mail);
}
private function addClientInfo($pdf, $clientInfo, $dateFormatted)
{
$clientInfoXAxis = 125;
$clientInfoYAxis = 40;
$pdf->SetFont('Arial', '', 11);
$pdf->SetXY($clientInfoXAxis, $clientInfoYAxis);
$pdf->Cell(0, 7, mb_convert_encoding($clientInfo['name'], 'ISO-8859-1', 'UTF-8'));
$clientInfoYAxis += 7;
$pdf->SetXY($clientInfoXAxis, $clientInfoYAxis);
$pdf->SetMargins(0, 0, 10);
$pdf->MultiCell(0, 7, trim(\OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($clientInfo['address'])));
$pdf->SetMargins(0, 0, 0);
$clientInfoYAxis += 14;
$pdf->SetXY($clientInfoXAxis, $clientInfoYAxis);
$pdf->Cell(0, 7, trim(mb_convert_encoding(html_entity_decode($clientInfo['city']), 'ISO-8859-1', 'UTF-8')));
if (!empty($clientInfo['siret'])) {
$clientInfoYAxis += 7;
$pdf->SetXY($clientInfoXAxis, $clientInfoYAxis);
$pdf->Cell(0, 7, 'Siret: ' . \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($clientInfo['siret']));
}
if (!empty($clientInfo['email'])) {
$clientInfoYAxis += 7;
$pdf->SetXY($clientInfoXAxis, $clientInfoYAxis);
$pdf->Cell(0, 7, 'Email: ' . mb_convert_encoding(html_entity_decode($clientInfo['email']), 'ISO-8859-1', 'UTF-8'));
}
$clientInfoYAxis += 7;
$pdf->SetXY($clientInfoXAxis, $clientInfoYAxis);
$pdf->Cell(0, 7, "le " . mb_convert_encoding($dateFormatted, 'ISO-8859-1', 'UTF-8'));
}
private function doesLogoExist()
{
$storage = $this->rootFolder->getUserFolder(self::DEFAULT_NEXTCLOUD_ADMIN);
try {
if (isset($storage)) {
$storage->get('/.gestion/logo.png');
return true;
}
return false;
} catch (\OCP\Files\NotFoundException $e) {
return false;
}
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace OCA\Gestion\Service\Devis;
class DevisPdfTableRenderer
{
public function createDevisTable($pdf, $dataDevis, $config)
{
// Extraction des paramètres du tableau de configuration
$pagination = $config['pagination'];
$itemsToProcess = $config['itemsToProcess'];
$numPage = $config['numPage'];
$nbPage = $config['nbPage'];
$totals = &$config['totals']; // Référence pour modification
$sansMontant = $config['sansMontant'] ?? false; // false par défaut = afficher les montants
$this->drawTableStructure($pdf, $numPage, $nbPage, $sansMontant);
$this->addTableHeaders($pdf, $sansMontant);
$this->populateTableData($pdf, $dataDevis, $pagination['current_index'], $itemsToProcess, $totals, $sansMontant);
// Afficher les totaux seulement sur la dernière page ET si les montants sont affichés
if ($numPage == $nbPage && !$sansMontant) {
$this->addTableTotals($pdf, $totals);
}
}
private function drawTableStructure($pdf, $numPage, $nbPage, $sansMontant)
{
$pdf->SetLineWidth(0.2);
$pdf->Rect(5, 105, 200, 130, "D");
$pdf->Line(5, 115, 205, 115);
// Si c'est la dernière page ET qu'on affiche les montants, ligne de total à 225
$endY = ($numPage == $nbPage && !$sansMontant) ? 225 : 235;
if (!$sansMontant) {
// AVEC montants : toutes les colonnes
$verticalLines = [25, 45, 70, 90, 125, 155, 175, 190];
} else {
// SANS montants : seulement jusqu'à Défunt
$verticalLines = [35, 70, 105, 125, 155];
}
foreach ($verticalLines as $x) {
$pdf->Line($x, 105, $x, $endY);
}
}
private function addTableHeaders($pdf, $sansMontant)
{
$pdf->SetFont('Arial', 'B', 8);
if (!$sansMontant) {
// AVEC montants
$headers = [
[5, 20, mb_convert_encoding(html_entity_decode("N° Devis"), 'ISO-8859-1', 'UTF-8')],
[25, 20, mb_convert_encoding(html_entity_decode("N° Dossier"), 'ISO-8859-1', 'UTF-8')],
[45, 25, mb_convert_encoding(html_entity_decode("N° Commande"), 'ISO-8859-1', 'UTF-8')],
[70, 20, "Date"],
[90, 35, "Lieu du soin"],
[125, 30, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport("Défunt")],
[155, 20, "H.T."],
[175, 15, "TVA 20%"],
[190, 15, "T.T.C"]
];
} else {
// SANS montants - colonnes plus larges
$headers = [
[5, 30, mb_convert_encoding(html_entity_decode("N° Devis"), 'ISO-8859-1', 'UTF-8')],
[35, 35, mb_convert_encoding(html_entity_decode("N° Dossier"), 'ISO-8859-1', 'UTF-8')],
[70, 35, mb_convert_encoding(html_entity_decode("N° Commande"), 'ISO-8859-1', 'UTF-8')],
[105, 20, "Date"],
[125, 30, "Lieu du soin"],
[155, 50, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport("Défunt")]
];
}
foreach ($headers as $header) {
$pdf->SetXY($header[0], 106);
$pdf->Cell($header[1], 8, $header[2], 0, 0, 'C');
}
}
private function populateTableData($pdf, $dataDevis, $startIndex, $itemsToProcess, &$totals, $sansMontant)
{
$formatterDate = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE);
$formatterDate->setPattern('dd-MMM');
$yDevis = 115;
for ($i = $startIndex; $i < $startIndex + $itemsToProcess && $i < count($dataDevis); $i++) {
$devis = $dataDevis[$i];
$dateSoin = new \DateTime($devis['devis_date']);
$this->addTableRow($pdf, $devis, $formatterDate, $dateSoin, $yDevis, $sansMontant);
// Calculer les totaux seulement si on affiche les montants
if (!$sansMontant) {
$totals['ht'] += $devis['montant_htc'];
$totals['tva'] += $devis['montant_tva'];
$totals['ttc'] += $devis['montant_ttc'];
}
$yDevis += 5;
}
}
private function addTableRow($pdf, $devis, $formatterDate, $dateSoin, $yDevis, $sansMontant)
{
$pdf->SetFont('Arial', '', 7);
if (!$sansMontant) {
// AVEC montants
$rowData = [
[6, 18, $devis['devis_full_number']],
[26, 18, $devis['calendar_uuid']],
[46, 23, $devis['num_commande']],
[71, 18, mb_convert_encoding($formatterDate->format($dateSoin), 'ISO-8859-1', 'UTF-8')],
[91, 33, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($devis['lieu_nom'])],
[126, 28, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($devis['defunt_nom'])],
[156, 18, number_format($devis['montant_htc'], 2, '.', '') . chr(128), 'C'],
[176, 13, number_format($devis['montant_tva'], 2, '.', '') . chr(128), 'C'],
[191, 13, number_format($devis['montant_ttc'], 2, '.', '') . chr(128), 'C']
];
} else {
// SANS montants - colonnes plus larges
$rowData = [
[6, 28, $devis['devis_full_number']],
[36, 33, $devis['calendar_uuid']],
[71, 33, $devis['num_commande']],
[106, 18, mb_convert_encoding($formatterDate->format($dateSoin), 'ISO-8859-1', 'UTF-8')],
[126, 28, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($devis['lieu_nom'])],
[156, 48, \OCA\Gestion\Helpers\FileExportHelpers::FormatTextForExport($devis['defunt_nom'])]
];
}
foreach ($rowData as $data) {
$pdf->SetXY($data[0], $yDevis);
$align = isset($data[3]) ? $data[3] : '';
$pdf->Cell($data[1], 5, $data[2], 0, 0, $align);
}
}
private function addTableTotals($pdf, $totals)
{
// Cette méthode n'est appelée que si on affiche les montants (!$sansMontant)
$pdf->Line(5, 225, 205, 225);
$pdf->SetFont('Arial', 'B', 8);
$pdf->SetXY(5, 225);
$pdf->Cell(150, 8, 'TOTAL', 0, 0, 'C');
$pdf->SetXY(156, 225);
$pdf->Cell(18, 8, number_format($totals['ht'], 2, '.', '') . chr(128), 0, 0, 'C');
$pdf->SetXY(176, 225);
$pdf->Cell(13, 8, number_format($totals['tva'], 2, '.', '') . chr(128), 0, 0, 'C');
$pdf->SetXY(191, 225);
$pdf->Cell(13, 8, number_format($totals['ttc'], 2, '.', '') . chr(128), 0, 0, 'C');
$pdf->SetXY(170, 240);
$pdf->Cell(20, 8, 'TOTAL TTC', 0, 0, 'C');
$pdf->SetXY(190, 240);
$pdf->Cell(15, 8, number_format($totals['ttc'], 2, '.', '') . chr(128), 0, 0, 'C');
$lines = [
[170, 240, 170, 248],
[190, 240, 190, 248],
[205, 240, 205, 248],
[170, 240, 205, 240],
[170, 248, 205, 248]
];
foreach ($lines as $line) {
$pdf->Line($line[0], $line[1], $line[2], $line[3]);
}
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace OCA\Gestion\Service\Devis;
use OCA\Gestion\Db\Bdd;
use OCP\Files\IRootFolder;
use OCA\Gestion\Constants\MultipleFactureTypeConstant;
/**
* Service principal pour la génération des récapitulatifs de devis
*/
class DevisRecapService
{
/** @var Bdd */
private $gestionBdd;
/** @var IRootFolder */
private $rootFolder;
/** @var DevisDataProcessor */
private $dataProcessor;
/** @var DevisPdfGenerator */
private $pdfGenerator;
public const DEFAULT_NEXTCLOUD_ADMIN = 'admin';
public function __construct(
Bdd $gestionBdd,
IRootFolder $rootFolder,
) {
$this->gestionBdd = $gestionBdd;
$this->rootFolder = $rootFolder;
$this->dataProcessor = new DevisDataProcessor($gestionBdd);
$this->pdfGenerator = new DevisPdfGenerator($rootFolder);
}
public function generateDevisRecap($filter, $month, $year, $idNextCloud, $filterType, $montant)
{
$storage = $this->rootFolder->getUserFolder($idNextCloud);
$configs = json_decode($this->gestionBdd->getConfiguration(self::DEFAULT_NEXTCLOUD_ADMIN));
$currentConfig = $configs[0];
$devisData = $this->getDevisData($filter, $month, $year, $currentConfig, $filterType);
if (empty($devisData)) {
return null;
}
$processedData = $this->dataProcessor->prepareDevisData($devisData, $currentConfig, $filter, $idNextCloud);
$clientInfo = $this->dataProcessor->getClientInfoForDevis($processedData[0], $filterType);
return $this->pdfGenerator->generatePdfDocument(
$storage,
$currentConfig,
$processedData,
$clientInfo,
$month,
$year,
$montant
);
}
private function getDevisData($filter, $month, $year, $currentConfig, $filterType)
{
$isFilterByClient = $filterType === MultipleFactureTypeConstant::CLIENT_FILTER_TYPE;
if ($isFilterByClient) {
return $this->gestionBdd->getDevisPdfDataByClientAndMonthYear($filter, $month, $year, $currentConfig);
} else {
return $this->gestionBdd->getDevisPdfDataByClientGroupFacturationAndMonthYear($filter, $month, $year, $currentConfig);
}
}
// Méthodes utilitaires conservées pour la compatibilité CSV
public function generateCsvContent($devisData)
{
return $this->dataProcessor->generateCsvContent($devisData);
}
public function getClientNameForFolder($firstDevis, $filterType)
{
return $this->dataProcessor->getClientNameForFolder($firstDevis, $filterType);
}
public function getRecapFilename($month, $year)
{
return $this->dataProcessor->getRecapFilename($month, $year);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace OCA\Gestion\Service\Devis;
use OCP\Files\IRootFolder;
/**
* Objet simple pour regrouper les paramètres de contexte
*/
class PageContext
{
public $pdf;
public $currentConfig;
public $configurationAddress;
public $configurationAddressCity;
public $dataDevis;
public $clientInfo;
public $year;
public $montant;
public function __construct($pdf, $currentConfig, $configurationAddress, $configurationAddressCity, $dataDevis, $clientInfo, $year, $montant)
{
$this->pdf = $pdf;
$this->currentConfig = $currentConfig;
$this->configurationAddress = $configurationAddress;
$this->configurationAddressCity = $configurationAddressCity;
$this->dataDevis = $dataDevis;
$this->clientInfo = $clientInfo;
$this->year = $year;
$this->montant = $montant;
}
}

View File

@ -41,7 +41,8 @@ use OCA\Gestion\Service\InvoiceRecap\InvoiceRecapService;
use OCP\DB\Exception;
use OCP\Files\IRootFolder;
class InvoicePdfService {
class InvoicePdfService
{
/** @var Bdd */
private $gestionBdd;
@ -55,13 +56,15 @@ class InvoicePdfService {
public function __construct(
Bdd $gestionBdd,
IRootFolder $rootFolder,
InvoiceRecapService $invoiceRecapService) {
InvoiceRecapService $invoiceRecapService
) {
$this->gestionBdd = $gestionBdd;
$this->rootFolder = $rootFolder;
$this->invoiceRecapService = $invoiceRecapService;
}
private function getLogo(){
private function getLogo()
{
$storage = $this->rootFolder->getUserFolder(self::DEFAULT_NEXTCLOUD_ADMIN);
try {
try {
@ -73,15 +76,15 @@ class InvoicePdfService {
} catch(\OCP\Files\NotFoundException $e) {
$file = $storage->get('/.gestion/logo.jpeg');
}
}
catch(\OCP\Files\NotFoundException $e) {
} catch(\OCP\Files\NotFoundException $e) {
return "nothing";
}
return base64_encode($file->getContent());
}
private function generateFactureSinglePdfByFactureId($factureId,$idNextCloud){
private function generateFactureSinglePdfByFactureId($factureId, $idNextCloud)
{
$storage = $this->rootFolder->getUserFolder($idNextCloud);
$configs = json_decode($this->gestionBdd->getConfiguration(self::DEFAULT_NEXTCLOUD_ADMIN));
$currentConfig = $configs[0];
@ -108,8 +111,7 @@ class InvoicePdfService {
foreach($factureFolders as $folder) {
try {
$storage->newFolder($folder);
}
catch(\OCP\Files\NotPermittedException $e) {
} catch(\OCP\Files\NotPermittedException $e) {
}
$ff_pdf = $folder.$pdfFilename.'.pdf';
$storage->newFile($ff_pdf);
@ -124,17 +126,18 @@ class InvoicePdfService {
];
}
public function generateFacturePdfByFactureId($factureId,$idNextCloud){
public function generateFacturePdfByFactureId($factureId, $idNextCloud)
{
$factureType = $this->gestionBdd->getFactureTypeByFactureId($factureId);
if($factureType == FactureTypeConstant::TYPE_SINGLE) {
return $this->generateFactureSinglePdfByFactureId($factureId, $idNextCloud);
}
else{
} else {
return $this->generateFactureGroupPdfByFactureId($factureId, $idNextCloud);
}
}
private function getGroupFactureFolder(array $factureData,$racinePath){
private function getGroupFactureFolder(array $factureData, $racinePath)
{
$clientRacineFolder = $racinePath.'CLIENTS/'.mb_strtoupper($factureData["group_name"], 'UTF-8').'/';
$factureDate = $factureData['date_paiement'];
$factureDatetime = new DateTime($factureDate);
@ -146,7 +149,8 @@ class InvoicePdfService {
];
}
private function getFacturesFolder(array $factureData,$racinePath){
private function getFacturesFolder(array $factureData, $racinePath)
{
$clientRacineFolder = $racinePath.'CLIENTS/'.mb_strtoupper($factureData["client_nom"], 'UTF-8').'/';
$defuntsFolder = $clientRacineFolder.'DEFUNTS/'.mb_strtoupper($factureData['defunt_nom'], 'UTF-8').'/'.'FACTURES'.'/';
$devisDate = $factureData['devis_date'];
@ -160,7 +164,8 @@ class InvoicePdfService {
];
}
private function generateFactureGroupPdfByFactureId($factureId,$idNextCloud){
private function generateFactureGroupPdfByFactureId($factureId, $idNextCloud)
{
$storage = $this->rootFolder->getUserFolder($idNextCloud);
$configs = json_decode($this->gestionBdd->getConfiguration(self::DEFAULT_NEXTCLOUD_ADMIN));
$currentConfig = $configs[0];
@ -198,8 +203,7 @@ class InvoicePdfService {
foreach($factureFolders as $folder) {
try {
$storage->newFolder($folder);
}
catch(\OCP\Files\NotPermittedException $e) {
} catch(\OCP\Files\NotPermittedException $e) {
}
$ff_pdf = $folder.$pdfFilename.'.pdf';
$storage->newFile($ff_pdf);
@ -214,13 +218,15 @@ class InvoicePdfService {
];
}
public function generateFacturePdfByFactureIds(array $factureIds,$idNextCloud){
public function generateFacturePdfByFactureIds(array $factureIds, $idNextCloud)
{
foreach($factureIds as $factureId) {
$this->generateFacturePdfByFactureId($factureId, $idNextCloud);
}
}
public function generateMultipleInvoicePdfByClientAndMonthYear($filter,$month,$year,$idNextCloud,$filterType){
public function generateMultipleInvoicePdfByClientAndMonthYear($filter, $month, $year, $idNextCloud, $filterType)
{
$storage = $this->rootFolder->getUserFolder($idNextCloud);
$configs = json_decode($this->gestionBdd->getConfiguration(self::DEFAULT_NEXTCLOUD_ADMIN));
$currentConfig = $configs[0];
@ -247,8 +253,7 @@ class InvoicePdfService {
$pdfContent = $pdf->Output('', 'S');
try {
$storage->newFolder($clientRacineFolder);
}
catch(\OCP\Files\NotPermittedException $e) {
} catch(\OCP\Files\NotPermittedException $e) {
}
$storage->newFile($filenamePath);
$file_pdf = $storage->get($filenamePath);
@ -256,11 +261,13 @@ class InvoicePdfService {
return $filenamePath;
}
public function generateInvoiceRecap($filter,$filterType,$date,$idNextCloud){
public function generateInvoiceRecap($filter, $filterType, $date, $idNextCloud)
{
$this->invoiceRecapService->generateInvoiceRecap($filter, $filterType, $date, $idNextCloud);
}
public function exportGroupOfDevisIntoFacture($clientId,$clientType,$month,$year,$facturationDate,$idNextcloud = BddConstant::DEFAULT_ADMIN_ID_NEXTCLOUD){
public function exportGroupOfDevisIntoFacture($clientId, $clientType, $month, $year, $facturationDate, $idNextcloud = BddConstant::DEFAULT_ADMIN_ID_NEXTCLOUD)
{
try {
$datetime = new Datetime();
$month = $month ?? $datetime->format('m');
@ -326,8 +333,7 @@ class InvoicePdfService {
return $factureGeneratedResponse["filenames"];
}
return null;
}
catch(Exception){
} catch(Exception) {
return null;
}

View File

@ -5,7 +5,7 @@ import "../css/mycss.css";
import { globalConfiguration } from "./modules/mainFunction.mjs";
import "./listener/main_listener";
import "./listener/devisListener";
import { exportClientDevisByMonthAndYearToPdf } from "./modules/ajaxRequest.mjs";
import { exportClientDevisByMonthAndYearToPdf, exportClientDevisRecap } from "./modules/ajaxRequest.mjs";
import 'select2/dist/css/select2.css';
import 'select2';
import '../css/mycss.css';

View File

@ -148,7 +148,6 @@ document.onchange = function(event) {
};
$('body').on('click', '#showGroupDevisFacturationModal', function () {
console.log("sdsfs");
$('#groupDevisFacturationModal').show();
});
@ -156,6 +155,14 @@ $('body').on('click', '#closeGroupDevisModal', function () {
$('#groupDevisFacturationModal').hide();
});
$('body').on('click', '#showDevisRecapModal', function () {
$('#devisRecapMontant').show();
});
$('body').on('click', '#closeDevisRecapModal', function () {
$('#devisRecapMontant').hide();
});
$('body').on('click', '#invoiceGroupQuote', function () {
var dateValue = document.getElementById("facturationDate").value;
@ -202,3 +209,95 @@ $('body').on('click', '#invoiceGroupQuote', function () {
hideLoader();
});
});
$('body').on('click', '#devisRecapAction', function () {
var valMontant = document.getElementById("sansMontant").checked;
const urlParams = new URLSearchParams(window.location.search);
const filter = urlParams.get('cli');
const year = urlParams.get('annee');
const month = urlParams.get('mois');
const filterType = urlParams.get('filterType');
var devisPayload = {
clientId: filter,
month: month,
year: year,
clientType: filterType,
montant: valMontant
};
showLoader();
$.ajax({
url: baseUrl + '/devis/exportDevisRecap',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(devisPayload)
}).done(function (response) {
if (response != null && response.trim() !== "") {
// Le retour est une string directe, pas un array JSON
var filename = response.replace(/\\/g, '/'); // Corriger les backslashes Windows
showSuccess('Sauvegardé dans ' + filename);
// Fermeture de la modal avec gestion d'erreur intégrée
var modalId = 'devisRecapMontant';
// Essayer Bootstrap 5 d'abord
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
var modalElement = document.getElementById(modalId);
if (modalElement) {
var modalInstance = bootstrap.Modal.getInstance(modalElement);
if (modalInstance) {
modalInstance.hide();
} else {
// Si pas d'instance, créer et fermer
modalInstance = new bootstrap.Modal(modalElement);
modalInstance.hide();
}
}
}
// Essayer Bootstrap 4/3 avec jQuery
else if (typeof $ !== 'undefined' && $.fn.modal) {
try {
$('#' + modalId).modal('hide');
} catch (e) {
console.warn('Erreur lors de la fermeture de la modal avec jQuery:', e);
// Fallback manuel
var modalElement = document.getElementById(modalId);
if (modalElement) {
modalElement.classList.remove('show');
modalElement.style.display = 'none';
}
}
}
// Fallback : fermeture manuelle pure JavaScript
else {
var modalElement = document.getElementById(modalId);
if (modalElement) {
// Retirer les classes Bootstrap
modalElement.classList.remove('show');
modalElement.style.display = 'none';
modalElement.setAttribute('aria-hidden', 'true');
// Retirer le backdrop si présent
var backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.remove();
}
// Retirer la classe modal-open du body
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
document.body.style.removeProperty('overflow');
}
}
} else {
showError(t('gestion', "Les données pour sauvegarde sont vides"));
}
}).fail(function (response, code) {
showError(t('gestion', "Erreur dans la génération du récapitulatif devis"));
}).always(function () {
hideLoader();
});
});

View File

@ -858,6 +858,7 @@ export function exportClientDevisByMonthAndYearToPdf(clientId,year,month,filterT
});
};
/**
* Set bijoux photo
* @param {*} bijouxId

View File

@ -3,6 +3,7 @@
<form method="get" class="d-flex flex-row align-items-center">
<select name="cli" id="clientselector">
<?php
$showRecapButton = false;
foreach ($_['clients'] as $key => $client) {
?>
<option <?php
@ -17,8 +18,9 @@
?>
</select>&nbsp;&nbsp;
<select name="annee" id="yearselector">
<option value="-1" <?php if ((int) $_GET['annee'] == -1)
echo 'selected' ?>>Toutes les années</option>
<option value="-1" <?php if ((int) $_GET['annee'] == -1) {
echo 'selected';
} ?>>Toutes les années</option>
<?php
$currentYear = date('Y');
for ($year = $currentYear; $year >= $currentYear - 10; $year--) {
@ -27,32 +29,45 @@
?>
</select>&nbsp;&nbsp;
<select name="mois" id="monthselector">
<option value="0" <?php if ((int) $_GET['mois'] == 0)
echo 'selected' ?>>Tous les mois</option>
<option value="1" <?php if ((int) $_GET['mois'] == 1)
echo 'selected' ?>>Janvier</option>
<option value="2" <?php if ((int) $_GET['mois'] == 2)
echo 'selected' ?>>Fevrier</option>
<option value="3" <?php if ((int) $_GET['mois'] == 3)
echo 'selected' ?>>Mars</option>
<option value="4" <?php if ((int) $_GET['mois'] == 4)
echo 'selected' ?>>Avril</option>
<option value="5" <?php if ((int) $_GET['mois'] == 5)
echo 'selected' ?>>Mai</option>
<option value="6" <?php if ((int) $_GET['mois'] == 6)
echo 'selected' ?>>Juin</option>
<option value="7" <?php if ((int) $_GET['mois'] == 7)
echo 'selected' ?>>Juillet</option>
<option value="8" <?php if ((int) $_GET['mois'] == 8)
echo 'selected' ?>>Août</option>
<option value="9" <?php if ((int) $_GET['mois'] == 9)
echo 'selected' ?>>Septembre</option>
<option value="10" <?php if ((int) $_GET['mois'] == 10)
echo 'selected' ?>>Octobre</option>
<option value="11" <?php if ((int) $_GET['mois'] == 11)
echo 'selected' ?>>Novembre</option>
<option value="12" <?php if ((int) $_GET['mois'] == 12)
echo 'selected' ?>>Decembre</option>
<option value="0" <?php if ((int) $_GET['mois'] == 0) {
echo 'selected';
} ?>>Tous les mois</option>
<option value="1" <?php if ((int) $_GET['mois'] == 1) {
echo 'selected';
} ?>>Janvier</option>
<option value="2" <?php if ((int) $_GET['mois'] == 2) {
echo 'selected';
} ?>>Fevrier</option>
<option value="3" <?php if ((int) $_GET['mois'] == 3) {
echo 'selected';
} ?>>Mars</option>
<option value="4" <?php if ((int) $_GET['mois'] == 4) {
echo 'selected';
} ?>>Avril</option>
<option value="5" <?php if ((int) $_GET['mois'] == 5) {
echo 'selected';
} ?>>Mai</option>
<option value="6" <?php if ((int) $_GET['mois'] == 6) {
echo 'selected';
} ?>>Juin</option>
<option value="7" <?php if ((int) $_GET['mois'] == 7) {
echo 'selected';
} ?>>Juillet</option>
<option value="8" <?php if ((int) $_GET['mois'] == 8) {
echo 'selected';
} ?>>Août</option>
<option value="9" <?php if ((int) $_GET['mois'] == 9) {
echo 'selected';
} ?>>Septembre</option>
<option value="10" <?php if ((int) $_GET['mois'] == 10) {
echo 'selected';
} ?>>Octobre</option>
<option value="11" <?php if ((int) $_GET['mois'] == 11) {
echo 'selected';
} ?>>Novembre</option>
<option value="12" <?php if ((int) $_GET['mois'] == 12) {
echo 'selected';
} ?>>Decembre</option>
</select>&nbsp;&nbsp;
<input type="hidden" name="filterType" id="filterType"
value="<?php echo($_GET['filterType'] ?? 'group'); ?>">
@ -62,6 +77,9 @@
<?php
$clients = $_['clients'];
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
if(intval($_GET['mois']) != 0 && intval($_GET['annee']) != 0) {
$showRecapButton = true;
}
$devis = array_filter($_['devis'], function ($currentDevis) {
if ($currentDevis->cid) {
$datesplit = explode("-", $currentDevis->date);
@ -101,6 +119,11 @@
Facturer
</button>
<?php
}
if (strcmp($_GET['cli'], '') != 0 && sizeof($devis) > 0) {
?>
<?php if($showRecapButton) {?><button class="btn btn-secondary" type="button" id="showDevisRecapModal" data-toggle="modal"
data-target="#devisRecapMontant">Generer le document recapitulatif</button><?php }
}
}
?>
@ -110,8 +133,9 @@
<div id="gestion-canvas" class="canvas_div_pdf">
<?php
if ($_SERVER['REQUEST_METHOD'] == 'GET' && strcmp($_GET['cli'], '') != 0) {
if (sizeof($devis) == 0)
if (sizeof($devis) == 0) {
echo "Aucun devis trouvé.";
}
}
;
@ -291,6 +315,27 @@
}
?>
</div>
<div class="modal" id="devisRecapMontant" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Souhaitez-vous le récapitulatif avec ou sans montant ?</h5>
</div>
<div class="modal-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sansMontant" value="">
<label class="form-check-label" for="sansMontant">
Sans Montant
</label>
</div>
</div>
<div class="modal-footer">
<button id="closeDevisRecapModal" type="button" class="btn btn-secondary">Annuler</button>
<button id="devisRecapAction" type="button" class="btn btn-primary">Générer</button>
</div>
</div>
</div>
</div>
<div class="modal" id="groupDevisFacturationModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">