Commit 2070d27c3994c03f5c9c82b7bcc432b336f884e9

Authored by Etienne Pallier
1 parent f4d2696b
Exists in master and in 1 other branch dev

GROS Nettoyage de la liste des fournisseurs

- GROSSE requete sql très compliquée de suppression des doublons (et des
espaces en trop)
- Suppression de la règle technique/inventoriable
- prix_ht désormais obligatoire

v4.105.24-3.7.9
CHANGES.txt
... ... @@ -119,8 +119,12 @@ Outre ces changements, voici d'autres changements importants :
119 119  
120 120 ======= CHANGES =======
121 121  
122   -TODO
123   - - (i) Nettoyage de la liste des fournisseurs : GROSSE requete sql compliquée de suppression des doublons (et des espaces en trop)
  122 +-------
  123 +16/10/2020 v4.105.24-3.7.9 (EP)
  124 + - (i) GROS Nettoyage de la liste des fournisseurs :
  125 + => GROSSE requete sql très compliquée de suppression des doublons (et des espaces en trop)
  126 + - (i) Suppression de la règle technique/inventoriable
  127 + - (e) prix_ht désormais obligatoire
124 128  
125 129 -------
126 130 15/10/2020 v4.105.24-3.7.9 (EP)
... ...
src/Controller/AppController.php
... ... @@ -3640,7 +3640,8 @@ class AppController extends Controller
3640 3640 if (!$configuration->envoi_mail && !$configuration->envoi_mail_guests) return null;
3641 3641  
3642 3642 // Si notification email pas demandée, on quitte
3643   - if (!$DEBUG) if (! $this->isNotifierActionSendingEmail($action) ) return null;
  3643 + if (!$DEBUG)
  3644 + if (! $this->isNotifierActionSendingEmail($action) ) return null;
3644 3645  
3645 3646 /*
3646 3647 * 4.A - CRÉATION DU MAIL (sujet et body)
... ... @@ -3757,6 +3758,9 @@ class AppController extends Controller
3757 3758 * - (TODO:) les responsables (scientifique et chef projet) du projet
3758 3759 */
3759 3760  
  3761 + // ssi Envoi général activé
  3762 + if ($configuration->envoi_mail) {
  3763 +
3760 3764 if (!$IS_ENTITY_OTHER) {
3761 3765  
3762 3766 // - (1) Ajout de l'utilisateur du matériel
... ... @@ -3824,6 +3828,8 @@ class AppController extends Controller
3824 3828  
3825 3829 } // uniquement pour entité liée à un matériel
3826 3830  
  3831 + } // ssi envoi général activé
  3832 +
3827 3833  
3828 3834 /*
3829 3835 * b) Envoi à la liste spécifique
... ... @@ -3835,7 +3841,8 @@ class AppController extends Controller
3835 3841 * systématiquement de toutes les actions
3836 3842 *
3837 3843 */
3838   - //$specificUsers = [];
  3844 +
  3845 + // ssi envoi liste spécifique activé
3839 3846 //if ($configuration->envoi_mail_guests && $action != 'printLabelRuban') {
3840 3847 if ($configuration->envoi_mail_guests) {
3841 3848 // mail aux adresses specifiees dans la config
... ... @@ -3849,7 +3856,8 @@ class AppController extends Controller
3849 3856 //$specificUsers[] = $specificUser;
3850 3857 // $mailList[sizeof($mailList)] = $configuration['emailGuest' . $i];
3851 3858 }
3852   - }
  3859 + } // ssi envoi liste spécifique activé
  3860 +
3853 3861  
3854 3862 /*
3855 3863 * c) NETTOYAGE
... ...
src/Controller/FournisseursController.php
... ... @@ -12,6 +12,21 @@ use Cake\ORM\TableRegistry;
12 12 class FournisseursController extends AppController
13 13 {
14 14  
  15 + /*
  16 + * @Override
  17 + *
  18 + * Initialisation des autorisations pour les actions spécifiques à ce controleur
  19 + *
  20 + */
  21 + protected function setAuthorizations() {
  22 +
  23 + // - Action 'cleanup'
  24 + $this->setAuthorizationsForAction('cleanup (Nettoyage)', -1, [
  25 + 'super' => 0,
  26 + ]);
  27 +
  28 + }
  29 +
15 30 /**
16 31 * Give authorization for unite
17 32 *
... ... @@ -70,6 +85,12 @@ class FournisseursController extends AppController
70 85 }
71 86 */
72 87  
  88 + // Page de nettoyage de la liste des fournisseurs
  89 + public function cleanup() {
  90 +
  91 + }
  92 +
  93 +
73 94 /**
74 95 * Index method
75 96 *
... ...
src/Controller/MaterielsController.php
... ... @@ -11,6 +11,7 @@ use App\Model\Entity\User;
11 11 use Cake\I18n\FrozenDate;
12 12 use Cake\Database\Expression\QueryExpression;
13 13 use Cake\Database\Query;
  14 +use phpDocumentor\Reflection\Types\True_;
14 15  
15 16 //use App\Controller\DocumentsController;
16 17 //App::import('Controller', 'Documents');
... ... @@ -343,10 +344,16 @@ class MaterielsController extends AppController {
343 344 ]);
344 345 */
345 346  
  347 +
346 348 /*
347 349 * c) Règles d'accès (ACLs)
348 350 *
349 351 */
  352 +
  353 + // - Action 'execSqlRequestForBugfix' (bugfix) => autorisé seulement à superadmin
  354 + $this->setAuthorizationsForAction('execSqlRequestForBugfix (bugfix)', -1, [
  355 + 'super' => 0,
  356 + ]);
350 357  
351 358 // - Action 'add' (ajout d'un nouveau matériel) => autorisé pour tous
352 359 //$this->setAuthorizationsForAction('add', 0);
... ... @@ -1691,7 +1698,177 @@ class MaterielsController extends AppController {
1691 1698  
1692 1699 } // view
1693 1700  
  1701 +
  1702 + public function execSqlRequestForBugfix() {
  1703 + debug("START...");
  1704 + $this->execSqlRequestForBugfix_fournisseurs();
  1705 + debug("...STOP");
  1706 + exit;
  1707 + /*
  1708 + return $this->redirect([
  1709 + 'controller' => 'pages',
  1710 + 'action' => 'tools',
  1711 + ]);
  1712 + */
  1713 + }
1694 1714  
  1715 + // (EP20201016) Bugfix liste fournisseurs en BD
  1716 + private function execSqlRequestForBugfix_fournisseurs() {
  1717 +
  1718 + $DEBUG=false;
  1719 + //$DEBUG=true;
  1720 +
  1721 + $NL = '<br />';
  1722 + $CLEAN = false;
  1723 +
  1724 + $ftable= $this->Materiels->Fournisseurs;
  1725 + // OU ENCORE (même chose) :
  1726 + //$ftable= $this->getTableLocator()->get('Fournisseurs');
  1727 + // OU ENCORE (même chose) :
  1728 + //$ftable= TableRegistry::getTableLocator()->get('Fournisseurs');
  1729 +
  1730 + function show($entities, $AFEW=true) {
  1731 + //foreach ($entities as $e) echo("[$e] <br>");
  1732 + $nb = $entities->count();
  1733 + $i=0;
  1734 + foreach ($entities as $e) {
  1735 + $i++;
  1736 + if ($AFEW && $i==10) echo "......................<br>";
  1737 + if (
  1738 + !$AFEW
  1739 + ||
  1740 + ( $AFEW && ($i<10 || $i>($nb-10)) )
  1741 + )
  1742 + echo('['.$e->nom.'] <br/>');
  1743 + }
  1744 + }
  1745 +
  1746 + function getAllFournisseursLike($fname, $table) {
  1747 + $fournisseurs = $table
  1748 + ->find()
  1749 + //->where(['nom =' => $fname]);
  1750 + // On cherche le nom du fournisseur avec ou sans espace
  1751 + ->where( [ 'nom in' => [ $fname, trim($fname), " $fname", "$fname ", " $fname " ] ] )
  1752 + // avec leurs materiels associés
  1753 + ->contain(['Materiels'])
  1754 + ;
  1755 + return $fournisseurs;
  1756 + }
  1757 +
  1758 + function getFournisseurWithMaxMatos($entities) {
  1759 + $feurs_nbmatos = [];
  1760 + foreach ($entities as $e) $feurs_nbmatos[$e->nom] = count($e->materiels);
  1761 + //$fdoublons_nbmatos = sort($fdoublons_nbmatos);
  1762 + //$nbmatos_max = max($feurs_nbmatos);
  1763 + dump($feurs_nbmatos);
  1764 + $fname_max = array_search( max($feurs_nbmatos), $feurs_nbmatos );
  1765 + //debug($fname_max);
  1766 + foreach ($entities as $e)
  1767 + if ($e->nom == $fname_max)
  1768 + return $e;
  1769 + /*
  1770 + foreach ($entities as $e) {
  1771 + debug($e->nom);
  1772 + if ($e->nom == $fname_max) {
  1773 + debug($e->nom);
  1774 + return $e;
  1775 + }
  1776 + }
  1777 + */
  1778 + }
  1779 +
  1780 + function cleanup($f, $table, $DEBUG) {
  1781 + $fname = trim($f->nom);
  1782 + if ($f->nom != trim($f->nom)) {
  1783 + $f->nom = trim($f->nom);
  1784 + !$DEBUG && $table->save($f);
  1785 + }
  1786 + }
  1787 +
  1788 + while (! $CLEAN) {
  1789 + // On sortira (et terminera) au prochain tour, sauf si on a trouvé un doublon
  1790 + $CLEAN = true;
  1791 + $fournisseurs = $ftable->find();
  1792 + $nb = $fournisseurs->count();
  1793 + print "Liste fournisseurs AVANT cleanup ($nb) : $NL $NL";
  1794 + show($fournisseurs, false);
  1795 + print "$NL$NL";
  1796 + foreach ($fournisseurs as $f) {
  1797 + $fname = trim($f->nom);
  1798 + echo $fname;
  1799 + // On cherche TOUS les doublons imaginables de ce fournisseur
  1800 + // et on les récupère AVEC leurs materiels associés
  1801 + $fdoublons = getAllFournisseursLike($fname, $ftable);
  1802 + // Pas de doublons => on passe au suivant
  1803 + if ( $fdoublons->count() == 1 ) {
  1804 + echo '<br><br>';
  1805 + cleanup($f, $ftable, $DEBUG);
  1806 + continue;
  1807 + /* pour debug
  1808 + // VIRER
  1809 + $CLEAN = true;
  1810 + break;
  1811 + */
  1812 + }
  1813 + /* Il y a des doublons => on les supprime
  1814 + Stratégie :
  1815 + - on garde seulement celui qui a le plus de matériels liés (l’élu)
  1816 + - on supprime les autres après avoir relié leurs matériels au fournisseur élu
  1817 + */
  1818 + echo " (doublons) : <br>";
  1819 + //show($fdoublons);
  1820 + //debug($fdoublons->toArray());
  1821 + //dump($fdoublons->toArray());
  1822 + // - a) on trie les fournisseurs selon leur nb de matos associés, et on élit celui qui en a le plus
  1823 + $f_max = getFournisseurWithMaxMatos($fdoublons);
  1824 + //debug($f_max->nom);
  1825 + // - b) on supprime les doublons après avoir mis à jour leurs matos associés
  1826 + foreach ($fdoublons as $fdoublon) {
  1827 + //debug($fdoublon->nom);
  1828 + // tous sauf f_max
  1829 + if ($fdoublon->nom != $f_max->nom) {
  1830 + //debug("diff");
  1831 + // on associe les matos du doublon à f_max
  1832 + echo( '- Relie les matos de ['.$fdoublon->nom.']('.$fdoublon->id.') à ['.$f_max->nom.']('.$f_max->id.') :'.$NL );
  1833 + foreach ($fdoublon->materiels as $m) {
  1834 + //debug($m);
  1835 + echo('-- matos '.$m->id.' : f_id='.$m->fournisseur_id.', ['.$m->designation.']'.$NL);
  1836 + $m->fournisseur_id = $f_max->id;
  1837 + //echo($m->id.': f_id='.$m->fournisseur_id.', ['.$m->designation.']');
  1838 + //!$DEBUG && $this->Materiels->save($m);
  1839 + if ( !$this->Materiels->save($m) ) {
  1840 + debug("Le matériel n'a pas pu être sauvegardé !!");
  1841 + debug($m->getErrors());
  1842 + $host = $this->getCurrentURL(false);
  1843 + echo '<a href="'.$host.'/materiels/view/'.$m->id.'">Voir ce materiel pour le corriger</a>';
  1844 + //throw new \ErrorException("Le matériel n'a pas pu être sauvegardé !!");
  1845 + exit;
  1846 + }
  1847 + }
  1848 + // maintenant, on peut supprimer le doublon
  1849 + echo( '- Delete ['.$fdoublon->nom.']('.$fdoublon->id.')'.$NL );
  1850 + !$DEBUG && $ftable->delete($fdoublon);
  1851 + }
  1852 + }
  1853 + // On nettoie f si besoin (on peut car maintenant il est unique)
  1854 + cleanup($f_max, $ftable, $DEBUG);
  1855 + // On quitte la boucle en cours car on a supprimé des éléments de cette boucle => il faut donc recommencer depuis le début
  1856 + echo '<br><br>';
  1857 + if (!$DEBUG) {
  1858 + $CLEAN = false;
  1859 + break;
  1860 + }
  1861 + } // end foreach
  1862 + } // end while not CLEAN
  1863 +
  1864 + print "$NL$NL";
  1865 + $fournisseurs = $ftable->find();
  1866 + $nb = $fournisseurs->count();
  1867 + print "Liste fournisseurs APRÈS cleanup ($nb) : $NL $NL";
  1868 + show($fournisseurs);
  1869 +
  1870 + }
  1871 +
1695 1872 /**
1696 1873 * Add or Edit method (do either add() or edit())
1697 1874 * => Factorisation de add() et edit()
... ...
src/Model/Entity/Fournisseur.php
... ... @@ -13,6 +13,9 @@ use Cake\ORM\Entity;
13 13 class Fournisseur extends Entity
14 14 {
15 15  
  16 + // Ce qui s'affiche quand on fait echo $entity
  17 + public function __toString() { return $this->nom; }
  18 +
16 19 /**
17 20 * Fields that can be mass assigned using newEntity() or patchEntity().
18 21 *
... ...
src/Model/Table/FournisseursTable.php
... ... @@ -28,7 +28,8 @@ class FournisseursTable extends AppTable
28 28 parent::initialize($config);
29 29  
30 30 $this->setTable('fournisseurs');
31   - $this->setDisplayField('id');
  31 + //$this->setDisplayField('id');
  32 + $this->setDisplayField('nom');
32 33 $this->setPrimaryKey('id');
33 34  
34 35 $this->hasMany('Materiels', [
... ...
src/Model/Table/MaterielsTable.php
... ... @@ -10,6 +10,7 @@ use Cake\I18n\Time;
10 10 use Cake\I18n\Date;
11 11 use Cake\ORM\Association\BelongsTo;
12 12 use Cake\ORM\Entity;
  13 +use Cake\I18n\FrozenDate;
13 14  
14 15 /**
15 16 * Materiels Model
... ... @@ -47,6 +48,12 @@ use Cake\ORM\Entity;
47 48 class MaterielsTable extends AppTable
48 49 {
49 50  
  51 + private $PREV_SEUIL_INVENTORIABLE = 800;
  52 +
  53 + private $LAST_SEUIL_INVENTORIABLE_YEAR = 2020;
  54 + private $LAST_SEUIL_INVENTORIABLE_DATE = '03/06/2020';
  55 + private $LAST_SEUIL_INVENTORIABLE; // 1000€ pour IRAP (depuis 3/6/2020)
  56 +
50 57 public $ALL_STATUS = array(
51 58 'CREATED',
52 59 'VALIDATED',
... ... @@ -409,11 +416,12 @@ class MaterielsTable extends AppTable
409 416  
410 417  
411 418 $validator->numeric('prix_ht')
412   - ->allowEmpty('prix_ht')
  419 + // (EP202010 prix obligatoire)
  420 + //->allowEmpty('prix_ht')
413 421 ->add('prix_ht', 'valid', [
414   - 'rule' => 'check_string',
415   - 'message' => 'Ce champ contient des caractères interdits',
416   - 'provider' => 'table'
  422 + 'rule' => 'check_string',
  423 + 'message' => 'Ce champ contient des caractères interdits',
  424 + 'provider' => 'table'
417 425 ]);
418 426 $validator->allowEmpty('eotp')->add('eotp', 'valid', [
419 427 'rule' => 'check_string',
... ... @@ -537,6 +545,7 @@ class MaterielsTable extends AppTable
537 545 */
538 546 //$this->config = TableRegistry::getTableLocator()->get('Configurations')->get(1);
539 547 $this->config = TableRegistry::getTableLocator()->get('Configurations')->find()->first();
  548 + $this->LAST_SEUIL_INVENTORIABLE = $this->config->prix_inventaire_administratif;
540 549  
541 550 // 1) Définition de nos propres règles de gestion
542 551  
... ... @@ -568,9 +577,14 @@ class MaterielsTable extends AppTable
568 577 // return if checked as "administratif" price MUST be set
569 578 $checkPriceIfIsAdministratif = function ($entity) {
570 579 //if ($entity->materiel_administratif)
571   - if (! $entity->materiel_technique)
572   - return ($entity->prix_ht >= $this->config->prix_inventaire_administratif); // 1000€ pour l'IRAP
  580 + if (! $entity->materiel_technique) {
  581 + //debug($entity->date_acquisition);
  582 + //debug($entity->date_acquisition->format('Y'));
  583 + $year = $entity->date_acquisition->format('Y');
  584 + $this->SEUIL_INVENTORIABLE = ($year >= $this->LAST_SEUIL_INVENTORIABLE_YEAR) ? $this->LAST_SEUIL_INVENTORIABLE : $this->PREV_SEUIL_INVENTORIABLE;
  585 + return ($entity->prix_ht >= $this->SEUIL_INVENTORIABLE); // 1000€ pour l'IRAP (depuis 2017 ?)
573 586 //return ($entity->prix_ht !== null);
  587 + }
574 588 return true;
575 589 };
576 590  
... ... @@ -655,16 +669,20 @@ class MaterielsTable extends AppTable
655 669 'message' => 'Le matériel vaut moins de ' . $configuration->prix_inventaire_administratif . '€ HT, il n\'est donc pas inventoriable.'
656 670 ]);
657 671 */
  672 +
  673 + /* (EP 202010 n'est plus nécessaire)
658 674 $rules->add($checkPriceIfIsAdministratif, [
659 675 'errorField' => 'prix_ht',
660 676 //'message' => 'Le matériel ne peut pas être inventoriable et ne pas avoir de prix'
661   - 'message' => "Si le matériel n'est pas technique, son prix doit obligatoirement être supérieur à 1000€ (inventoriable)"
  677 + 'message' => "Si le matériel n'est pas technique, son prix HT doit obligatoirement être supérieur à ".
  678 + $this->LAST_SEUIL_INVENTORIABLE."€ (inventoriable)"
662 679 ]);
663 680 $rules->add($checkPriceIfIsAdministratif, [
664 681 'errorField' => 'materiel_technique',
665 682 //'message' => 'Le matériel ne peut pas être inventoriable et ne pas avoir de prix'
666   - 'message' => "Si le matériel n'est pas technique, son prix doit obligatoirement être supérieur à 1000€ (inventoriable)"
  683 + 'message' => "Si le matériel n'est pas technique, son prix HT doit obligatoirement être supérieur à ".$this->LAST_SEUIL_INVENTORIABLE."€ (inventoriable)"
667 684 ]);
  685 + */
668 686  
669 687 $rules->add($rules->isUnique([
670 688 'numero_laboratoire'
... ...
src/Template/Fournisseurs/cleanup.ctp 0 → 100644
... ... @@ -0,0 +1,33 @@
  1 +
  2 +<div class="fournisseurs index">
  3 +
  4 + <?php echo '<h2><i class="icon-list"></i> Nettoyage de la liste des Fournisseurs</h2>'; ?>
  5 +
  6 + <?= $this->Html->link(
  7 + "Nettoyage de toute la liste (suppression des doublons et des espaces en trop)",
  8 + ['controller'=>'Materiels', 'action' => 'exec_sql_request_for_bugfix'],
  9 + ['title' => 'Nettoyage', 'style' => 'margin: 0 2px', 'escape' => false ])
  10 + ?>
  11 +
  12 + <br><br>
  13 + (TODO) Remplacer un fournisseur par un autre :
  14 + <br>
  15 + Remplacer le fournisseur FOURNISSEUR1 par FOURNISSEUR2
  16 + <br><br>
  17 +
  18 + [SUBMIT]
  19 +
  20 +
  21 +</div>
  22 +
  23 +<!--
  24 +<div class="actions">
  25 + <php echo $this->element('menu') ?>
  26 + <php
  27 +
  28 +echo $this->element('menu_index', [
  29 + 'pluralHumanName' => 'Fournisseurs',
  30 + 'singularHumanName' => 'Fournisseur'
  31 +])?>
  32 + </div>
  33 +-->
0 34 \ No newline at end of file
... ...
src/Template/Materiels/view.ctp
... ... @@ -311,7 +311,7 @@ $CAN_PRINT_LABEL = $IS_VALIDATED &amp;&amp; $configuration-&gt;hasPrinter &amp;&amp; $USER_IS_ADMIN
311 311  
312 312 // Bouton Modifier
313 313 if ($CAN_EDIT)
314   - $echoActionButton($this->Html, 'icon-pencil', $bStyle, ' Editer', 'materiels', 'edit', $entity->id);
  314 + $echoActionButton($this->Html, 'icon-pencil', $bStyle, ' Éditer', 'materiels', 'edit', $entity->id);
315 315  
316 316 // Bouton Supprimer
317 317 if ($CAN_DELETE)
... ... @@ -666,10 +666,29 @@ $CAN_PRINT_LABEL = $IS_VALIDATED &amp;&amp; $configuration-&gt;hasPrinter &amp;&amp; $USER_IS_ADMIN
666 666 }
667 667  
668 668 $displayElement(__('Prix (HT)'), h($entity->prix_ht) . ' €');
669   - $displayElement(__('Fournisseur'), $entity->has('fournisseur') ? $entity->fournisseur->nom : '');
  669 +
  670 + // Fournisseur (lien)
  671 + //$displayElement(__('Fournisseur'), $entity->has('fournisseur') ? $entity->fournisseur->nom : '');
  672 + $displayElement(__('Fournisseur'),
  673 + $this->Html->link(
  674 + h($entity->fournisseur->nom),
  675 + ['controller'=>'Fournisseurs', 'action'=>'view', $entity->fournisseur->id]
  676 + )
  677 + );
  678 +
670 679 $displayElement(__('Lieu de stockage'), $entity->has('site') ? h($entity->site->nom) : '');
671 680 $displayElement(__('Détail lieu de stockage'), h($entity->lieu_detail));
672   - $displayElement(__('Nom de l\'utilisateur'), $this->Html->link(h($entity->nom_responsable), 'mailto:' . h($entity->email_responsable)));
  681 +
  682 + // Acheteur (lien mailto)
  683 + $displayElement(__("Nom de l'acheteur"),
  684 + $this->Html->link(
  685 + h($entity->nom_responsable),
  686 + // Mailto
  687 + 'mailto:' . h($entity->email_responsable)
  688 + //['controller'=>'Users', 'action'=>'view', 'id'=>$entity->id]
  689 + )
  690 + );
  691 +
673 692 $displayElement(__('N. interne (labo)'), h($entity->numero_laboratoire));
674 693 if ($entity->gestionnaire_id) {
675 694 $gestionnaire = TableRegistry::getTableLocator()->get('Users')->get($entity->gestionnaire_id);
... ...
src/Template/Pages/tools.ctp
... ... @@ -107,6 +107,22 @@ echo &#39;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&#39;;
107 107  
108 108 if ($role == 'Super Administrateur') :
109 109  
  110 + // Exécution d'un script correctif (bugfix) temporaire (superadmin only)
  111 + echo '<tr><td>';
  112 + echo $this->Html->link('Exécuter le dernier script correctif', [
  113 + 'controller' => 'Materiels',
  114 + 'action' => 'exec_sql_request_for_bugfix'
  115 + ]);
  116 + echo '</td></tr>';
  117 +
  118 + // Nettoyage de la liste des fournisseurs (superadmin only)
  119 + echo '<tr><td>';
  120 + echo $this->Html->link('Nettoyer la liste des Fournisseurs', [
  121 + 'controller' => 'Fournisseurs',
  122 + 'action' => 'cleanup'
  123 + ]);
  124 + echo '</td></tr>';
  125 +
110 126 // Page des messages de log (level info, debug, et notice)
111 127 echo '<tr><td>';
112 128 echo $this->Html->link('Voir les messages de LOG', [
... ...