diff --git a/FEATURES/128818 b/FEATURES/128818
new file mode 100644
index 0000000000000000000000000000000000000000..68fe517766fad7afe46658554b606c81c8742823
--- /dev/null
+++ b/FEATURES/128818
@@ -0,0 +1,10 @@
+        '128818' =>
+            ['Label' => $this->_('Administration PNB : Refonte des tableaux de bord'),
+             'Desc' => $this->_('Les deux tableaux de bord PNB (utilisation et historique de prêts)  permettent de filtrer et trier selon plusieurs critères. Les exports utilisent les critères en cours d'utilisation.'),
+             'Image' => '',
+             'Video' => 'https://www.youtube.com/watch?v=dGVzEPS3Gsw',
+             'Category' => $this->_('Ressources numériques'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'https://wiki.bokeh-library-portal.org/index.php?title=PNB_Dilicom_tableau_de_bord',
+             'Test' => '',
+             'Date' => '2021-06-09'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/128818 b/VERSIONS_WIP/128818
new file mode 100644
index 0000000000000000000000000000000000000000..9a70f6e8a9d14e8c491dab2f6b0033c6d9b464b2
--- /dev/null
+++ b/VERSIONS_WIP/128818
@@ -0,0 +1 @@
+ - ticket #128818 : Administration PNB Dilicom : Filtre et tri dans les tableaux de bord
\ No newline at end of file
diff --git a/application/modules/admin/controllers/AlbumController.php b/application/modules/admin/controllers/AlbumController.php
index 2832f1783c28ead31c45e8a92df98adb247dbfc5..fcc127e82052023e423c80f88cf20e625b403fab 100644
--- a/application/modules/admin/controllers/AlbumController.php
+++ b/application/modules/admin/controllers/AlbumController.php
@@ -72,48 +72,6 @@ class Admin_AlbumController extends ZendAfi_Controller_Action {
   }
 
 
-  public function dilicomAction() {
-    $this->view->titre = $this->_('PNB Dilicom');
-    $form = $this->_formImportDilicom();
-    $this->view->form_import_dilicom = $form;
-
-    if (!$this->_request->isPost())
-      return $this->view->dilicom_items = Class_Album_Item::findAll();
-
-    if ($form->isValid($this->_request->getPost()) && $form->offers->receive()) {
-      $xml = file_get_contents($form->offers->getFileName());
-      $report = function($import_count, $errors) {
-        $this->_helper->notify($this->view->_('%d livres numériques importés. %s',
-                                              $import_count,
-                                              implode(',', $errors)));
-      };
-
-      $this->_redirect('admin/album');
-      return;
-    }
-
-    $this->_helper->notify($this->_('Le fichier reçu n\'est pas valide'));
-    $this->_redirect('admin/album/import-onix');
-  }
-
-
-  public function dilicomExportCsvAction() {
-    $items = array_map(function($item) { return new Class_TableDescription_PNBItemsRenderer($item); },
-                      Class_Album_Item::findAll());
-
-    $this->_helper->csv('dilicom_csv.csv',
-                        $this->view->renderCsv(new Class_TableDescription_PNBItemsExport('pnb'),
-                                               $items));
-  }
-
-
-  public function dilicomExportLoansCsvAction() {
-    $this->_helper->csv('dilicom_loans_csv.csv',
-                        $this->view->renderCsv(new Class_TableDescription_PNBLoans(''),
-                                               Class_Loan_Pnb::findAllBy(['order' => 'loan_date'])));
-  }
-
-
   public function importeadAction() {
     $this->view->titre = $this->_('Import/Export EAD');
 
@@ -138,17 +96,6 @@ class Admin_AlbumController extends ZendAfi_Controller_Action {
   }
 
 
-  protected function _formImportDilicom() {
-    return $this->view
-      ->newForm(['id' => 'import_dilicom', 'class' => 'form'])
-      ->setMethod('post')
-      ->setAttrib('enctype', 'multipart/form-data')
-      ->setAction($this->view->url(['action' => 'dilicom']))
-      ->addElement($this->view->newFormElementFile('offers', 'xml'), 'offers')
-      ->addElement('submit', 'submit', ['label' => $this->_('Importer le fichier XML')]);
-  }
-
-
   protected function _formImportEAD() {
     return $this->view
       ->newForm(['id' => 'import_ead', 'class' => 'form'])
diff --git a/application/modules/admin/controllers/PnbController.php b/application/modules/admin/controllers/PnbController.php
new file mode 100644
index 0000000000000000000000000000000000000000..e245072f510522d7e96fca11a9752f38acafe751
--- /dev/null
+++ b/application/modules/admin/controllers/PnbController.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Copyright (c) 2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class Admin_PnbController extends ZendAfi_Controller_Action {
+  public function getPlugins() {
+    return [ ZendAfi_Controller_Plugin_MultiSelection_Album::class ];
+  }
+
+  public function indexAction() {
+    $this->view->titre = $this->_('PNB Dilicom');
+  }
+
+
+  public function usageAction() {
+    $search = $this->_helper->search([],
+                                     new Class_Album_UsageReport_SearchCriteria($this->_getParams()));
+
+    $this->view->subview = $this->view->partial('pnb/usage.phtml',
+                                                ['search' => $search,
+                                                 'subtitle' => $this->_('Utilisation des ressources PNB Dilicom')]);
+
+    $this->_forward('index');
+  }
+
+
+  public function loansAction() {
+    $search = $this->_helper->search([],
+                                     new Class_Loan_SearchCriteria($this->_getParams()));
+
+    $this->view->subview = $this->view->partial('pnb/loans.phtml',
+                                                ['search' => $search,
+                                                 'subtitle' => $this->_('Historique des prêts')]);
+    $this->_forward('index');
+  }
+
+
+  public function exportCsvAction() {
+    $params = $this->_request->getParams();
+    $items = (new Class_Album_UsageReport_SearchCriteria($params))
+      ->findPage(1, Class_SearchCriteria::PAGE_NO_LIMIT);
+
+    $this->_helper->csv('dilicom_csv.csv',
+                        $this->view->renderCsv(new Class_TableDescription_PNBItemsExport('pnb'),
+                                          $items
+                                               ));
+  }
+
+
+  public function exportLoansCsvAction() {
+    $search = new Class_Loan_SearchCriteria($this->_getParams());
+    $items = $search->findPage(1, Class_SearchCriteria::PAGE_NO_LIMIT);
+
+    $this->_helper->csv('dilicom_loans_csv.csv',
+                        $this->view->renderCsv(new Class_TableDescription_PNBLoans(''),
+                                               $items));
+  }
+}
diff --git a/application/modules/admin/controllers/WidgetController.php b/application/modules/admin/controllers/WidgetController.php
index c7decc5cb9717ee0ac8ba96e1524a878d9c21aa6..f2b18caec9937863bcb7073d354e4bcec980662c 100644
--- a/application/modules/admin/controllers/WidgetController.php
+++ b/application/modules/admin/controllers/WidgetController.php
@@ -271,7 +271,7 @@ class Admin_WidgetController extends ZendAfi_Controller_Action {
   }
 
 
-  protected function _getParams($widget) {
+  protected function _getParams($widget=null) {
     if('add' == $this->_request->getActionName())
       return $widget->forForm();
 
diff --git a/application/modules/admin/views/scripts/album/dilicom.phtml b/application/modules/admin/views/scripts/album/dilicom.phtml
deleted file mode 100644
index 5f036500ed8cc9b9ae5c6f3cfe0a654ef2d6d082..0000000000000000000000000000000000000000
--- a/application/modules/admin/views/scripts/album/dilicom.phtml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-echo $this->tag('h2', $this->_('Utilisation des ressources PNB Dilicom'));
-
-$skin = Class_Admin_Skin::current();
-
-echo $this->Button((new Class_Entity())
-                   ->setUrl($this->url(['module' => 'admin',
-                                        'controller' => 'album',
-                                        'action' => 'dilicom-export-csv'], null, true))
-                   ->setText($this->_('Exporter le tableau en CSV'))
-                   ->setImage($this->tagImg($skin->getIconUrl('actions', 'test'),
-                                            ['style' => 'filter: invert();'])));
-
-echo $this->Button((new Class_Entity())
-                   ->setUrl($this->url(['module' => 'admin',
-                                        'controller' => 'album',
-                                        'action' => 'dilicom-export-loans-csv'], null, true))
-                   ->setText($this->_('Exporter l\'historique des prêts en CSV'))
-                   ->setImage($this->tagImg($skin->getIconUrl('actions', 'test'),
-                                            ['style' => 'filter: invert();'])));
-
-$items = array_map(function($item) { return new Class_TableDescription_PNBItemsRenderer($item);},
-                   $this->dilicom_items);
-
-echo $this->renderTable((new Class_TableDescription_PNBItems('pnb_dilicom'))->setPager(true),
-                        $items);
-
-echo $this->tag('h2', $this->_('Import des offres Dilicom/PNB'));
-
-echo $this->form_import_dilicom;
diff --git a/application/modules/admin/views/scripts/pnb/index.phtml b/application/modules/admin/views/scripts/pnb/index.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..4e9594e0f53f4a9e2bab9a97dedf090f3aa641ac
--- /dev/null
+++ b/application/modules/admin/views/scripts/pnb/index.phtml
@@ -0,0 +1,26 @@
+<div class='menu'>
+<?php
+   $menus = [
+             ['icon' => 'books',
+              'label' => $this->_('Utilisation'),
+              'title' => $this->_('Utilisation des ressources PNB'),
+              'url' => $this->url(['module' => 'admin',
+                                   'controller' => 'pnb',
+                                   'action' => 'usage'], null, true)],
+             ['icon' => 'digital_resources',
+              'label' => $this->_('Prêts'),
+              'title' => $this->_('Historique des prêts PNB'),
+              'url' => $this->url(['module' => 'admin',
+                                   'controller' => 'pnb',
+                                   'action' => 'loans'], null, true)]
+   ];
+
+echo $this->getHelper('Admin_Nav')->generateMenu($menus);
+?>
+</div>
+
+<?php
+if ($this->subview)
+  echo $this->tag('div',
+                    $this->subview,
+                    ['class' => 'subview']);
diff --git a/application/modules/admin/views/scripts/pnb/loans.phtml b/application/modules/admin/views/scripts/pnb/loans.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..945721cf7e653f00a1cc37bb8ed24f607de4abd3
--- /dev/null
+++ b/application/modules/admin/views/scripts/pnb/loans.phtml
@@ -0,0 +1,3 @@
+<?php
+echo $this->tag('h2', $this->subtitle);
+echo $this->searchPnbLoans($this->search);
diff --git a/application/modules/admin/views/scripts/pnb/usage.phtml b/application/modules/admin/views/scripts/pnb/usage.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..c9fe927ff15fc6d711606feb1770158206181823
--- /dev/null
+++ b/application/modules/admin/views/scripts/pnb/usage.phtml
@@ -0,0 +1,3 @@
+<?php
+echo $this->tag('h2', $this->subtitle);
+echo $this->searchPnb($this->search);
diff --git a/cosmogramme/sql/patch/patch_411.php b/cosmogramme/sql/patch/patch_411.php
new file mode 100644
index 0000000000000000000000000000000000000000..4f4e0424dde461df84711093ee29d0b5d44ef62c
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_411.php
@@ -0,0 +1,21 @@
+<?php
+try {
+  $adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
+
+  $adapter->query('CREATE TABLE if not exists `album_usage_report` ( '
+                  . 'id int(11) unsigned not null auto_increment,'
+                  . 'item_id int(11) unsigned not null,'
+                  . 'title varchar(255) not null,'
+                  . 'loan_quantity int(11),'
+                  . 'total_quantity int(11),'
+                  . 'live_quantity int(11),'
+                  . 'hold_count int(11),'
+                  . 'duration int(11),'
+                  . 'license_expiration int(11),'
+                  . 'order_date datetime,'
+                  . 'genres varchar(255),'
+                  . 'sections varchar(255),'
+                  . 'primary key (id)'
+                  . ') engine=MyISAM default charset=utf8');
+} catch(Exception $e) {
+}
diff --git a/library/Class/Album.php b/library/Class/Album.php
index b31732f863db773237bdba3ceac82a4723aa08b6..3bbcb0c0426ee50c22141d7fcf049946a45b5d5a 100644
--- a/library/Class/Album.php
+++ b/library/Class/Album.php
@@ -81,6 +81,11 @@ class AlbumLoader extends Storm_Model_Loader {
                                    'order' => 'id',
                                    'limit' => $limit]);
   }
+
+
+  public function findAllDilicom() {
+    return Class_Album::findAllBy(['type_doc_id' => Class_TypeDoc::DILICOM]);
+  }
 }
 
 
diff --git a/library/Class/Album/Item.php b/library/Class/Album/Item.php
index e261a42ae9eeeb6f460a1e05d9e99afaba8b67bf..9eef9ef2a58ea4b472f20537438b4b5718ad85c4 100644
--- a/library/Class/Album/Item.php
+++ b/library/Class/Album/Item.php
@@ -164,16 +164,26 @@ class Class_Album_Item extends Storm_Model_Abstract {
   }
 
 
+  public function getAlbumSections() {
+    return ($album = $this->getAlbum())
+      ? $album->getSections()
+      : '' ;
+  }
+
+
   public function getAlbumSectionIds() {
-    if ($album = $this->getAlbum())
-      return explode(';',$album->getSections());
-    return [];
+    return explode(';' , $this->getAlbumSections());
+  }
+
+
+  public function getAlbumGenre() {
+    return ($album = $this->getAlbum())
+      ? $album->getGenre()
+      : '' ;
   }
 
 
   public function getAlbumGenreIds() {
-    if ($album = $this->getAlbum())
-      return explode(';',$album->getGenre());
-    return [];
+    return explode(';',$this->getAlbumGenre());
   }
 }
diff --git a/library/Class/Album/UsageReport.php b/library/Class/Album/UsageReport.php
new file mode 100644
index 0000000000000000000000000000000000000000..835da0c1d51e5fcdc3032bbb7ea1f49e54664ea0
--- /dev/null
+++ b/library/Class/Album/UsageReport.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Album_UsageReport extends Storm_Model_Abstract {
+
+  protected
+    $_table_name = 'album_usage_report',
+    $_belongs_to = ['item' => ['model' => 'Class_Album_Item',
+                               'referenced_in' => 'item_id']],
+    $_overrided_attributes = ['id',
+                              'item_id',
+                              'title',
+                              'loan_quantity',
+                              'total_quantity',
+                              'live_quantity',
+                              'hold_count',
+                              'duration',
+                              'license_expiration',
+                              'order_date',
+                              'genres',
+                              'sections'];
+
+
+  public function isInfiniteLoan() {
+    return $this->getItem()->isDilicomInfinite($this->getItem()->getLoanQuantity());
+  }
+
+
+  public function getLoanQuantityOrLocalLoanCount() {
+    return $this->isInfiniteLoan()
+      ? $this->getItem()->getLocalLoanCount()
+      : $this->getItem()->getQuantity();
+  }
+
+
+  public function getLoanCountOrLocalOngoing() {
+    return $this->isInfiniteLoan()
+      ? $this->getItem()->getLocalOngoingCount()
+      : $this->getItem()->getLoanCount();
+  }
+
+
+  public function quantityOrInfinite($value) {
+    return $this->getItem()->isDilicomInfinite($value)
+      ? '∞'
+      : $value;
+  }
+
+
+  public function getQuantityOnTotal() {
+    return $this->getLoanQuantityOrLocalLoanCount()
+      . ' / '
+      . $this->quantityOrInfinite($this->getItem()->getLoanQuantity());
+
+  }
+
+
+  public function getLiveQuantityOnTotal() {
+    return $this->getItem()->getLocalOngoingCount()
+      . ' / '
+      . $this->getItem()->getLoanAllowedNumberOfUsers();
+  }
+
+
+  public function getLocalOngoingCount() {
+    return $this->getItem()->getLocalOngoingCount();
+  }
+
+
+  public function getLicenseExpirationOrInfinite() {
+    return ('∞' == $this->quantityOrInfinite($this->getItem()->getAvailabilityDuration()))
+            ? '∞'
+            : $this->getItem()->getAvailabilityRemainingDaysBeforeEndDate();
+  }
+
+
+  public function getTitre() {
+    return $this->getTitle();
+  }
+
+
+  public function getAlbum() {
+    return ($item = $this->getItem())
+      ? $item->getAlbum()
+      : null ;
+  }
+
+
+  public function getAlbumId() {
+   return ($item = $this->getItem())
+      ? $item->getAlbumId()
+      : null ;
+  }
+
+
+  public function getGenreLabels() {
+    $item = $this->getItem();
+    if ($item && ($genre = $item->getAlbumGenreIds()))
+      return implode(', ',Class_CodifGenre::labelsOfIds($genre)->getArrayCopy());
+    return '';
+  }
+
+
+  public function getSectionLabels() {
+    $item = $this->getItem();
+    if ($item && ($sections = $item->getAlbumSectionIds()))
+      return implode(', ',Class_CodifSection::labelsOfIds($sections)->getArrayCopy());
+    return '';
+  }
+
+
+  public function getMainAuthor() {
+    return $this->getAlbum()->getMainAuthorName();
+  }
+
+
+  public function getFirstEditor() {
+    return $this->getAlbum()->getFirstEditor();
+  }
+
+
+  public function getFirstCollection() {
+    return $this->getAlbum()->getFirstCollection();
+  }
+
+
+  public function getYear() {
+    return $this->getAlbum()->getAnnee();
+  }
+
+
+  public function getCategory() {
+    return $this->getAlbum()->getCategoryLabel();
+  }
+}
diff --git a/library/Class/Album/UsageReport/SearchCriteria.php b/library/Class/Album/UsageReport/SearchCriteria.php
new file mode 100644
index 0000000000000000000000000000000000000000..78fc4cae77887f0ad2263b491071266b1ade0bd4
--- /dev/null
+++ b/library/Class/Album/UsageReport/SearchCriteria.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Album_UsageReport_SearchCriteria extends Class_SearchCriteria {
+  protected $_model_class = Class_Album_UsageReport::class;
+
+  public function __construct($params) {
+    $this->_criteria = [new Class_Album_UsageReport_SearchCriteria_Title($params),
+                        new Class_Album_UsageReport_SearchCriteria_OrderDate($params),
+                        new Class_Album_UsageReport_SearchCriteria_LoanCount($params),
+                        new Class_Album_UsageReport_SearchCriteria_HoldCount($params),
+                        new Class_Album_UsageReport_SearchCriteria_LicenseExpiration($params),
+                        new Class_SearchCriteria_Order($params)];
+
+    if (Class_AdminVar::get('DILICOM_PNB_BOARD_DISPLAY_SECTION')) {
+      $this->_criteria[] = new Class_Album_UsageReport_SearchCriteria_Genre($params);
+      $this->_criteria[] = new Class_Album_UsageReport_SearchCriteria_Section($params);
+    }
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_OrderDate extends Class_SearchCriteria_DateRange {
+  protected $_name = 'order_date';
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Date de commande'));
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_Title extends Class_SearchCriteria_TextLike {
+  protected $_name = 'title';
+
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Titre'));
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_LoanCount extends Class_SearchCriteria_NumRange {
+  protected $_name = 'loan_quantity';
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Nombre de prêts'));
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_HoldCount extends Class_SearchCriteria_NumRange {
+  protected $_name = 'hold_count';
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Nombre de réservations'));
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_LicenseExpiration extends Class_SearchCriteria_NumRange {
+  protected $_name = 'license_expiration';
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Nombre de jours restants sur la licence'));
+  }
+}
+
+
+
+
+abstract class Class_Album_UsageReport_SearchCriteria_SelectElement extends Class_SearchCriteria_MultiCheckbox {
+
+
+  abstract protected function _getElement($id);
+
+  abstract protected function _getElementsFromAlbum($album);
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_getLabel());
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_Genre extends Class_Album_UsageReport_SearchCriteria_SelectElement {
+  protected
+    $_checkbox_name = ZendAfi_View_Helper_TagListeCoches::SOURCE_GENRE_PNB,
+    $_name = 'genres';
+
+  protected function _getElement($id) {
+    return Class_CodifGenre::find($id);
+  }
+
+
+  protected function _getElementsFromAlbum($album) {
+    return $album->getGenre();
+  }
+
+
+  protected function _getLabel() {
+    return $this->_('Genre');
+  }
+}
+
+
+
+
+class Class_Album_UsageReport_SearchCriteria_Section extends Class_Album_UsageReport_SearchCriteria_SelectElement {
+  protected
+    $_checkbox_name = ZendAfi_View_Helper_TagListeCoches::SOURCE_SECTION_PNB,
+    $_name = 'sections';
+
+  protected function _getElement($id) {
+    return Class_CodifSection::find($id);
+  }
+
+
+  protected function _getElementsFromAlbum($album) {
+    return $album->getSections();
+  }
+
+
+  protected function _getLabel() {
+    return $this->_('Section');
+  }
+}
diff --git a/library/Class/Batch/Dilicom.php b/library/Class/Batch/Dilicom.php
index 87567239b2e635850c42d3a5c4921526bb888ccd..dbe6ed9ea53096d678bc67b027b0475c82f04616 100644
--- a/library/Class/Batch/Dilicom.php
+++ b/library/Class/Batch/Dilicom.php
@@ -66,7 +66,8 @@ class Class_Batch_Dilicom extends Class_Batch_Abstract {
     return [new Class_Batch_DilicomJobOnix($this),
             new Class_Batch_DilicomJobEndedLoans($this),
             new Class_Batch_DilicomJobUnindexExpiredOrders($this),
-            new Class_Batch_DilicomJobProcessHolds($this)
+            new Class_Batch_DilicomJobProcessHolds($this),
+            new Class_Batch_DilicomJobGenerateUsageReport($this)
     ];
   }
-}
\ No newline at end of file
+}
diff --git a/library/Class/Batch/DilicomJobGenerateUsageReport.php b/library/Class/Batch/DilicomJobGenerateUsageReport.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b45601fef05e25424dd0cea83921c96e417949a
--- /dev/null
+++ b/library/Class/Batch/DilicomJobGenerateUsageReport.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Batch_DilicomJobGenerateUsageReport extends Class_Batch_Job {
+  public function getLabel() {
+    return $this->_('Dilicom Job : Génération du rapport d\'utilisation des ressources PNB');
+  }
+
+
+  public function run() {
+    $logger = $this->getLogger();
+
+    $logger->log($this->_('Traitement des statistiques PNB'));
+    if (!$this->isEnabled()) {
+      $logger->log($this->_('Traitement des statistiques PNB désactivé'));
+      return;
+    }
+
+    if (!Class_Album_Item::count()) {
+      $logger->log($this->_('Aucun exemplaire PNB trouvés'));
+      return;
+    }
+
+    Class_Album_UsageReport::basicDeleteBy([]);
+
+    $page=0;
+    while ($items = Class_Album_Item::findAllBy(['order' => 'id',
+                                                 'limitPage' => [$page, 100]])) {
+      $this->_generateReportTableForItems($items);
+      $page++;
+    }
+  }
+
+
+  protected function _generateReportTableForItems($items) {
+    foreach ($items as $item) {
+      $usage_report = (new Class_Album_UsageReport);
+      $usage_report->setItemId($item->getId())
+                   ->setTitle($item->getAlbum()->getTitre())
+                   ->setLoanQuantity($usage_report->getLoanQuantityOrLocalLoanCount())
+                   ->setTotalQuantity($item->getLoanQuantity())
+                   ->setLiveQuantity($usage_report->getLocalOngoingCount())
+                   ->setHoldCount(Class_Hold_Pnb::countByAlbum($item->getAlbum()))
+                   ->setDuration($item->getDuration())
+                   ->setLicenseExpiration($item->getAvailabilityRemainingDaysBeforeEndDate())
+                   ->setOrderDate($item->getOrderDate())
+                   ->setGenres($item->getAlbumGenre())
+                   ->setSections($item->getAlbumSections())
+                   ->save();
+    }
+
+    $this->getLogger()
+         ->log($this->_('Statistiques générées pour %s exemplaires', count($items)));
+  }
+}
diff --git a/library/Class/CodifGenre.php b/library/Class/CodifGenre.php
index 18051ab70cea8731ba701f7b4c794432b9fead28..7103dd7f2aab10ba87f956e3a6bce6a97176e810 100644
--- a/library/Class/CodifGenre.php
+++ b/library/Class/CodifGenre.php
@@ -33,11 +33,22 @@ class Class_CodifGenreLoader extends Storm_Model_Loader {
 
 
   public function labelsOfIds($ids) {
+    if (empty($ids))
+      return new Storm_Model_Collection();
     return (new Storm_Model_Collection(Class_CodifGenre::findAllBy(['id_genre' => $ids])))
       ->collect('libelle');
   }
 
 
+  public function findAllByAlbums($albums) {
+    $ids = (new Storm_Model_Collection($albums))
+      ->injectInto([],
+                   function($ids, $album) { return array_merge($ids,
+                                                               explode(';', $album->getGenre())); });
+    return $this->findAllBy(['id_genre' => $ids]);
+  }
+
+
   public function getMultiOptions() {
     $models = Class_CodifGenre::findAllBy(['order' => 'libelle']);
     $options = ['' => $this->_('tous')];
diff --git a/library/Class/CodifSection.php b/library/Class/CodifSection.php
index eb3c7ac2e85674ce251fb5342f776a30066325da..1ffdfc74b4e34100c7ca849efe63b6c6ac4abb71 100644
--- a/library/Class/CodifSection.php
+++ b/library/Class/CodifSection.php
@@ -23,6 +23,8 @@ class Class_CodifSectionLoader extends Storm_Model_Loader {
   use Trait_Translator;
 
   public function labelsOfIds($ids) {
+    if (empty($ids))
+      return new Storm_Model_Collection();
     return (new Storm_Model_Collection(Class_CodifSection::findAllBy(['id_section' => $ids])))
       ->collect('libelle');
   }
@@ -35,6 +37,15 @@ class Class_CodifSectionLoader extends Storm_Model_Loader {
   }
 
 
+  public function findAllByAlbums($albums) {
+    $ids = (new Storm_Model_Collection($albums))
+      ->injectInto([],
+                   function($ids, $album) { return array_merge($ids,
+                                                              explode(';', $album->getSections())); });
+    return $this->findAllBy(['id_section' => $ids]);
+  }
+
+
   public function getMultiOptions() {
     $datas = Class_CodifSection::findAllVisibleOrderedByLabel();
     $items  = ['' => $this->_('toutes')];
@@ -78,4 +89,4 @@ class Class_CodifSection extends Storm_Model_Abstract {
   public function validate() {
     $this->checkAttribute('libelle', '' != $this->getLibelle(), $this->_('Vous devez définir le libellé'));
   }
-}
\ No newline at end of file
+}
diff --git a/library/Class/Hold/Pnb.php b/library/Class/Hold/Pnb.php
index 706e868428d78497e345fe59b0c765ab79f6168f..5c5202fad6020a8c23c6179c064e2ba4b752c565 100644
--- a/library/Class/Hold/Pnb.php
+++ b/library/Class/Hold/Pnb.php
@@ -76,6 +76,11 @@ class Class_Hold_PnbLoader extends Storm_Model_Loader {
   }
 
 
+  public function countByAlbum($album) {
+    return Class_Hold_Pnb::findAllByAlbum($album)->count();
+  }
+
+
   public function countPendingByAlbum($album){
     return Class_Hold_Pnb::findAllByAlbum($album)->select('isPending')->count();
   }
diff --git a/library/Class/Loan/Pnb.php b/library/Class/Loan/Pnb.php
index 71c0453581d839f3b26152c666cb6fba5622516e..055ae71f49ffcc8c664c985ae27d044b4c72e0d7 100644
--- a/library/Class/Loan/Pnb.php
+++ b/library/Class/Loan/Pnb.php
@@ -174,6 +174,16 @@ class Class_Loan_Pnb extends Storm_Model_Abstract {
   }
 
 
+  public function getGenres() {
+    return array_filter(array_map('trim', explode(';', $this->getAlbum()->getGenre())));
+  }
+
+
+  public function getSections() {
+    return array_filter(array_map('trim', explode(';', $this->getAlbum()->getSections())));
+  }
+
+
   public function getUser() {
     if ($user = parent::_get('user'))
       return $user;
diff --git a/library/Class/Loan/SearchCriteria.php b/library/Class/Loan/SearchCriteria.php
new file mode 100644
index 0000000000000000000000000000000000000000..83b52c3bf71bae4ad787dee4cdf2e806442ee1f0
--- /dev/null
+++ b/library/Class/Loan/SearchCriteria.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Loan_SearchCriteria extends Class_SearchCriteria {
+  protected $_model_class = Class_Loan_Pnb::class;
+
+  public function __construct($params) {
+    $this->_criteria = [new Class_Loan_SearchCriteria_LoanDate($params),
+                        new Class_Loan_SearchCriteria_ReturnDate($params),
+                        new Class_Loan_SearchCriteria_Order($params)];
+  }
+}
+
+
+
+
+
+class Class_Loan_SearchCriteria_Order extends Class_SearchCriteria_Order {
+  protected $_value = 'loan_date';
+}
+
+
+
+
+class Class_Loan_SearchCriteria_LoanDate extends Class_SearchCriteria_DateRange {
+  protected
+    $_name = 'loan_date';
+
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Date de prêt'));
+  }
+}
+
+
+
+
+class Class_Loan_SearchCriteria_ReturnDate extends Class_SearchCriteria_DateRange {
+  protected
+    $_name = 'expected_return_date';
+
+
+  public function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Date de retour'));
+  }
+}
diff --git a/library/Class/RendezVous/SearchCriteria/Date.php b/library/Class/RendezVous/SearchCriteria/Date.php
index 83480576ca6a3ee2e5f0d4ded0c5130a60d0e89b..13174aafdb04a0e3a4e07e4b36644cab5b55b713 100644
--- a/library/Class/RendezVous/SearchCriteria/Date.php
+++ b/library/Class/RendezVous/SearchCriteria/Date.php
@@ -21,7 +21,10 @@
 
 
 class Class_RendezVous_SearchCriteria_Date extends Class_SearchCriteria_DateRange {
-  protected $_name = 'date';
+  protected
+    $_name = 'date',
+    $_start_suffix = '_start',
+    $_end_suffix = '_end';
 
   protected function _defaultStart() {
     return $this->getTimeSource()->dateFormat(static::DATE_FORMAT);
diff --git a/library/Class/SearchCriteria/CustomField/DateRange.php b/library/Class/SearchCriteria/CustomField/DateRange.php
index d7eecd195d400f5b539df4f8b23c8e57b9613b60..85f6ff9282c9bcebf925c9bbf13ba1687ecfd906 100644
--- a/library/Class/SearchCriteria/CustomField/DateRange.php
+++ b/library/Class/SearchCriteria/CustomField/DateRange.php
@@ -66,6 +66,10 @@ class Class_SearchCriteria_CustomField_DateRange extends Class_SearchCriteria_Cu
 class Class_SearchCriteria_CustomField_CustomizedDateRange
   extends Class_SearchCriteria_DateRange {
 
+  protected
+    $_start_suffix = '_start',
+    $_end_suffix = '_end';
+
   public function __construct($params) {
     $this->_name = $params['name'];
     unset($params['name']);
diff --git a/library/Class/SearchCriteria/DateRange.php b/library/Class/SearchCriteria/DateRange.php
index 6364c7d50ce69e269140d9e1188304cda1a5519f..703e3f71139ada248ffba33be1383d0e8730389c 100644
--- a/library/Class/SearchCriteria/DateRange.php
+++ b/library/Class/SearchCriteria/DateRange.php
@@ -20,45 +20,10 @@
  */
 
 
-class Class_SearchCriteria_DateRange extends Class_SearchCriteria_Abstract {
-  use Trait_TimeSource;
+class Class_SearchCriteria_DateRange extends Class_SearchCriteria_Range {
 
   const DATE_FORMAT = 'd/m/Y';
 
-  protected
-    $_start_name,
-    $_end_name,
-    $_value_start = '',
-    $_value_end = '';
-
-
-  public function __construct($params) {
-    $this->_start_name = $this->getName() . '_start';
-    $this->_end_name = $this->getName() . '_end';
-
-    parent::__construct($params);
-
-    $this->_value_start = isset($params[$this->_start_name])
-      ? $this->_filterDate($params[$this->_start_name])
-      : $this->_defaultStart();
-    $this->_element->setStartValue($this->_value_start);
-
-    $this->_value_end = isset($params[$this->_end_name])
-      ? $this->_filterDate($params[$this->_end_name])
-      : $this->_defaultEnd();
-    $this->_element->setEndValue($this->_value_end);
-  }
-
-
-  protected function _defaultStart() {
-    return '';
-  }
-
-
-  protected function _defaultEnd() {
-    return '';
-  }
-
 
   public function buildElement() {
     return new ZendAfi_Form_Element_DateRangePicker($this->getName(),
@@ -67,15 +32,6 @@ class Class_SearchCriteria_DateRange extends Class_SearchCriteria_Abstract {
   }
 
 
-  public function acceptSearchVisitor($visitor) {
-    if ($this->_value_start)
-      $visitor->addWhereParam($this->_name . ' >= "' . $this->_sqlFormat($this->_value_start) . '"');
-
-    if ($this->_value_end)
-      $visitor->addWhereParam($this->_name . ' <= "' . $this->_sqlFormat($this->_value_end) . '"');
-  }
-
-
   public function isValidDate($value) {
     return (new ZendAfi_Validate_DateFormat())->isValid($value);
   }
@@ -91,28 +47,36 @@ class Class_SearchCriteria_DateRange extends Class_SearchCriteria_Abstract {
 
     $time = strtotime(substr($value, 0, 10));
     if ($this->_value_start
-        && strtotime($this->_sqlFormat($this->_value_start)) > $time)
+        && strtotime($this->_sqlFormat($this->_value_start)) >= $time)
       return false;
 
     if ($this->_value_end
-        && strtotime($this->_sqlFormat($this->_value_end)) < $time)
+        && strtotime($this->_sqlFormat($this->_value_end)) <= $time)
       return false;
 
     return true;
   }
 
 
-  protected function _isAllValues() {
-    return !($this->_value_start || $this->_value_end);
+
+  public function acceptSearchVisitor($visitor) {
+    if ($this->_value_start)
+      $visitor->addWhereParam(sprintf('left(%s, 10) >= "%s"',
+                                      $this->_name,
+                                      $this->_sqlFormat($this->_value_start)));
+
+    if ($this->_value_end)
+      $visitor->addWhereParam(sprintf('left(%s, 10) <= "%s"',
+                                      $this->_name,
+                                      $this->_sqlFormat($this->_value_end)));
   }
 
 
-  protected function _filterDate($value) {
-    if (null === $value)
-      return;
 
-    if ('' === $value)
-      return '';
+
+  protected function _filterValue($value) {
+    if ($value === null || $value === '')
+      return $value;
 
     if (!(new ZendAfi_Validate_DateFormat())->setFormat(static::DATE_FORMAT)->isValid($value)) {
       $this->_element->addError($this->_('Les dates doivent être au format JJ/MM/AAAA'));
@@ -128,25 +92,18 @@ class Class_SearchCriteria_DateRange extends Class_SearchCriteria_Abstract {
   }
 
 
-  public function getCompositeValues() {
-    return [$this->_start_name => $this->_value_start,
-            $this->_end_name => $this->_value_end];
+  protected function _getDescribeFromLabel($start) {
+    return $this->_('depuis le %s', $start);
   }
 
 
-  public function describeOn($view) {
-    if ('' == $this->_value_start && '' == $this->_value_end)
-      return;
-
-    $description = $this->_element->getLabel() . ' : ';
-    if ('' != $this->_value_start && '' != $this->_value_end)
-      return $description . $this->_('entre le %s et le %s',
-                                     $this->_value_start, $this->_value_end);
+  protected function _getDescribeBetweenLabel($start, $end) {
+    return $this->_('entre le %s et le %s',
+             $start, $end);
+  }
 
-    if ('' != $this->_value_start)
-      return $description . $this->_('depuis le %s', $this->_value_start);
 
-    if ('' != $this->_value_end)
-      return $description . $this->_('jusqu\'au %s', $this->_value_end);
+  protected function _getDescribeToLabel($end) {
+    return $this->_('jusqu\'au %s', $end);
   }
 }
diff --git a/library/Class/SearchCriteria/MultiCheckbox.php b/library/Class/SearchCriteria/MultiCheckbox.php
new file mode 100644
index 0000000000000000000000000000000000000000..2c2d28f4bb55f4c624de7f1f15f7d9e0665a0a9d
--- /dev/null
+++ b/library/Class/SearchCriteria/MultiCheckbox.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_SearchCriteria_MultiCheckbox extends Class_SearchCriteria_Abstract {
+  const end_or_semicolon = '(;|$)';
+  const start_or_semicolon = '(^|;)';
+
+  protected $_value = Class_SearchCriteria_Abstract::ALL_VALUES;
+
+  public function acceptSearchVisitor($visitor) {
+    if (!$this->_value || $this->_isAllValues())
+      return;
+
+    return $visitor->addWhereParam($this->_matchValuesInField( $this->_name, $this->_value));
+  }
+
+
+  protected function _matchValuesInField($field, $values) {
+    return implode(' or ',
+                   array_map(function ($value)
+                             use ($field) {
+                                             return sprintf('%s RLIKE "%s%s%s"',
+                                                            $field,
+                                                            self::start_or_semicolon,
+                                                            $value,
+                                                            self::end_or_semicolon);
+                                           },
+                             explode(';',$values)));
+
+
+    $values_str = implode(sprintf( '%s" or %s RLIKE "%s',
+                                  self::end_or_semicolon,
+                                  $field ,
+                                  self::start_or_semicolon),
+                          explode(';', $values));
+
+    return sprintf(' %s RLIKE "%s%s%s"',
+                   $field,
+                   self::start_or_semicolon,
+                   $values_str,
+                   self::end_or_semicolon);
+  }
+
+
+  protected function _getLabel() {
+    return $this->_name;
+  }
+
+
+  protected function _getCheckboxName() {
+    return $this->_checkbox_name
+      ? $this->_checkbox_name
+      : $this->_name ;
+  }
+
+
+  public function buildElement() {
+    return new ZendAfi_Form_Element_CochesSuggestion($this->getName(),
+                                                     ['label' => $this->_getLabel(),
+                                                      'value' => $this->_value,
+                                                      'name' =>  $this->getName(),
+                                                      'rubrique' => $this->_getCheckboxName()]);
+  }
+}
diff --git a/library/Class/SearchCriteria/NumRange.php b/library/Class/SearchCriteria/NumRange.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7973f58bf09951fc3ba6d34a16a418b323a442a
--- /dev/null
+++ b/library/Class/SearchCriteria/NumRange.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_SearchCriteria_NumRange extends Class_SearchCriteria_Range {
+
+  public function __construct($params) {
+    parent::__construct($params);
+
+    $this->_element->setCompositeInputValues($params);
+  }
+
+
+  public function buildElement() {
+    return new ZendAfi_Form_Element_Range($this->getName(),
+                                          ['label' => $this->getName() . $this->_(' de '),
+                                           'separator' => $this->_(' jusque ')]);
+  }
+
+
+  public function isValidNumber($value) {
+    return is_int($value);
+  }
+
+
+
+  protected function _filterValue($value) {
+
+    if ($value === null || $value === '')
+      return $value;
+
+
+    if (!(new Zend_Validate_Int())->isValid($value)) {
+      $this->_element->addError($this->_('%s n\'est pas un nombre valide', $value));
+      return;
+    }
+
+    return $value;
+  }
+
+
+  public function modelMatch($model) {
+    if ($this->_isAllValues())
+      return true;
+
+    if ((!$value = $model->callGetterByAttributeName($this->_name))
+        || !$this->isValidNumber($value))
+      return false;
+
+    if ($this->_value_start && $this->_value_start > $value)
+      return false;
+
+    if ($this->_value_end && $this->_value_end < $value)
+      return false;
+
+    return true;
+  }
+}
diff --git a/library/Class/SearchCriteria/Range.php b/library/Class/SearchCriteria/Range.php
new file mode 100644
index 0000000000000000000000000000000000000000..e256f7ab46215b5d4e13aa0339bd83f421214e0a
--- /dev/null
+++ b/library/Class/SearchCriteria/Range.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_SearchCriteria_Range extends Class_SearchCriteria_Abstract {
+  use Trait_TimeSource;
+
+  protected
+    $_start_name,
+    $_end_name,
+    $_start_suffix = '_debut',
+    $_end_suffix = '_fin',
+    $_value_start = '',
+    $_value_end = '';
+
+
+  public function __construct($params) {
+    $this->_start_name = $this->getName() . $this->_start_suffix;
+    $this->_end_name = $this->getName() . $this->_end_suffix;;
+
+    parent::__construct($params);
+    $this->_value_start = isset($params[$this->_start_name])
+      ? $this->_filterValue($params[$this->_start_name])
+      : $this->_defaultStart();
+    $this->_element->setStartValue($this->_value_start);
+
+    $this->_value_end = isset($params[$this->_end_name])
+      ? $this->_filterValue($params[$this->_end_name])
+      : $this->_defaultEnd();
+    $this->_element->setEndValue($this->_value_end);
+  }
+
+
+  protected function _filterValue($value) {
+    return $value;
+  }
+
+
+  protected function _sqlFormat($value) {
+    return $value;
+  }
+
+
+  protected function _defaultStart() {
+    return '';
+  }
+
+
+  protected function _defaultEnd() {
+    return '';
+  }
+
+
+  protected function _isAllValues() {
+    return !($this->_value_start || $this->_value_end);
+  }
+
+
+  public function getCompositeValues() {
+    return [$this->_start_name => $this->_value_start,
+            $this->_end_name => $this->_value_end];
+  }
+
+
+  public function acceptSearchVisitor($visitor) {
+    if ($this->_value_start)
+      $visitor->addWhereParam($this->_name . ' >= "' . $this->_sqlFormat($this->_value_start) . '"');
+
+    if ($this->_value_end)
+      $visitor->addWhereParam($this->_name . ' <= "' . $this->_sqlFormat($this->_value_end) . '"');
+  }
+
+
+  protected function _getDescribeFromLabel($start) {
+    return $this->_('depuis %s', $start);
+  }
+
+
+  protected function _getDescribeBetweenLabel($start, $end) {
+    return $this->_('entre %s et %s',
+             $start, $end);
+  }
+
+
+  protected function _getDescribeToLabel($end) {
+    return $this->_('jusqu\'à %s', $end);
+  }
+
+
+  public function describeOn($view) {
+    if ('' == $this->_value_start && '' == $this->_value_end)
+      return;
+
+    $description = $this->_element->getLabel() . ' : ';
+    if ('' != $this->_value_start && '' != $this->_value_end)
+      return $description
+        . $this->_getDescribeBetweenLabel($this->_value_start, $this->_value_end);
+
+    if ('' != $this->_value_start)
+      return $description
+        . $this->_getDescribeFromLabel($this->_value_start);
+
+    if ('' != $this->_value_end)
+      return $description
+        . $this->_getDescribeToLabel($this->_value_end);
+  }
+}
diff --git a/library/Class/TableDescription/PNBItemsExport.php b/library/Class/TableDescription/PNBItemsExport.php
index dd5647c45222361628286b2b9b7df9beef51a975..db0b083f09c3ddc0b874cb2f1f2b890d30b8b72f 100644
--- a/library/Class/TableDescription/PNBItemsExport.php
+++ b/library/Class/TableDescription/PNBItemsExport.php
@@ -20,26 +20,36 @@
  */
 
 
-class Class_TableDescription_PNBItemsExport extends Class_TableDescription_PNBItems {
+class Class_TableDescription_PNBItemsExport extends Class_TableDescription_PNBUsages {
   use Trait_Translator;
 
-  public function init() {
+
+  protected function _addLoansColumns() {
+    parent::_addLoansColumns();
+    return $this->addColumn($this->_('Nombre de prêts'),  'loan_quantity_or_local_loan_count');
+  }
+
+
+  protected function _addLiveLoansColumns() {
+    parent::_addLiveLoansColumns();
+    return $this->addColumn($this->_('Prêts simultanés'), 'loan_count_or_local_ongoing');
+  }
+
+
+  protected function _addExtraColumns() {
     $this
-      ->addColumn($this->_('Titre'), 'title')
-      ->addColumn($this->_('Prêts / Droits'), 'quantity_on_total')
-      ->addColumn($this->_('Nombre de prêts'),  'loan_quantity_or_local_loan_count')
-      ->addColumn($this->_('Prêts simultanés / Droits'), 'live_quantity')
-      ->addColumn($this->_('Prêts simultanés'), 'loan_count_or_local_ongoing')
-      ->addColumn($this->_('Durée de prêt en jours'), 'duration')
-      ->addColumn($this->_('Nombre de jours restant sur la licence'), 'license_expiration')
-      ->addColumn($this->_('Date de commande'),  'order_date')
       ->addColumn($this->_('Auteur'), 'main_author')
       ->addColumn($this->_('Éditeur'), 'first_editor')
       ->addColumn($this->_('Collection'), 'first_collection')
-      ->addColumn($this->_('Année'), 'year')
-      ->addColumn($this->_('Genre'), 'first_kind')
-      ->addColumn($this->_('Section'), 'first_section')
-      ->addColumn($this->_('Catégorie'), 'category')
-      ;
+      ->addColumn($this->_('Année'), 'year');
+
+    parent::_addExtraColumns();
+
+    return $this->addColumn($this->_('Catégorie'), 'category');
+  }
+
+
+  protected function _getRowActions() {
+    return $this;
   }
 }
diff --git a/library/Class/TableDescription/PNBItemsRenderer.php b/library/Class/TableDescription/PNBItemsRenderer.php
deleted file mode 100644
index 29a32a186742f480cf2b0e2140e44afc22163cd2..0000000000000000000000000000000000000000
--- a/library/Class/TableDescription/PNBItemsRenderer.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-/**
- * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved.
- *
- * BOKEH is free software; you can redistribute it and/or modify
- * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
- * the Free Software Foundation.
- *
- * There are special exceptions to the terms and conditions of the AGPL as it
- * is applied to this software (see README file).
- *
- * BOKEH is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
- * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
- */
-
-
-class Class_TableDescription_PNBItemsRenderer {
-  use Trait_GetterByAttributeName;
-
-  protected $_item;
-
-  public function __construct($item) {
-    $this->_item = $item;
-  }
-
-
-  public function __call($name, $params) {
-    return call_user_func_array([$this->_item, $name], $params);
-  }
-
-
-  public function getOrderDate() {
-    return ($date = $this->_item->getOrderDate())
-      ? (new DateTime($date))->format('d/m/Y')
-      : '';
-  }
-
-
-  public function getDuration() {
-    return $this->quantityOrInfinite($this->_item->getDuration());
-  }
-
-
-  public function getAlbumId() {
-    return $this->_item->getAlbumId();
-  }
-
-
-  public function getSection() {
-    if ($sections = $this->_item->getAlbumSectionIds())
-      return implode(', ',Class_CodifSection::labelsOfIds($sections)->getArrayCopy());
-    return '';
-  }
-
-
-  public function getGenre() {
-    if ($genre = $this->_item->getAlbumGenreIds())
-      return implode(', ',Class_CodifGenre::labelsOfIds($genre)->getArrayCopy());
-    return '';
-  }
-
-
-  public function quantityOrInfinite($value) {
-    return $this->_item->isDilicomInfinite($value)
-      ? '∞'
-      : $value;
-  }
-
-
-  public function getQuantityOnTotal() {
-    return $this->getLoanQuantityOrLocalLoanCount()
-      . ' / '
-      . $this->quantityOrInfinite($this->_item->getLoanQuantity());
-  }
-
-
-  public function getLiveQuantity() {
-    return $this->getLocalOngoingCount()
-      . ' / '
-      . $this->_item->getLoanAllowedNumberOfUsers();
-  }
-
-
-  public function getLicenseExpiration() {
-    return ('∞' == $this->quantityOrInfinite($this->_item->getAvailabilityDuration()))
-            ? '∞'
-            : $this->_item->getAvailabilityRemainingDaysBeforeEndDate();
-  }
-
-
-  public function getLoanQuantityOrLocalLoanCount() {
-    return $this->isInfiniteLoan()
-      ? $this->_item->getLocalLoanCount()
-      : $this->_item->getQuantity();
-  }
-
-
-  public function getLoanCountOrLocalOngoing() {
-    return $this->isInfiniteLoan()
-      ? $this->_item->getLocalOngoingCount()
-      : $this->_item->getLoanCount();
-  }
-
-
-  public function isInfiniteLoan() {
-    return $this->_item->isDilicomInfinite($this->_item->getLoanQuantity());
-  }
-}
diff --git a/library/Class/TableDescription/PNBLoansHistory.php b/library/Class/TableDescription/PNBLoansHistory.php
new file mode 100644
index 0000000000000000000000000000000000000000..04cc182269b455f8bb1641661e7480e90f1291cf
--- /dev/null
+++ b/library/Class/TableDescription/PNBLoansHistory.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_TableDescription_PNBLoansHistory extends Class_TableDescription {
+  use Trait_Translator;
+
+  public function init() {
+    $this
+      ->addColumn($this->_('Date d\'emprunt'),
+                  function($model) { return (new DateTime($model->getLoanDate()))
+                                        ->format('d/m/Y');})
+      ->addColumn($this->_('Date de retour'),
+                  function($model) { return (new DateTime($model->getExpectedReturnDate()))
+                                        ->format('d/m/Y');})
+      ->addColumn($this->_('Titre'), ['attribute' => 'title',
+                                      'sortable' => false]);
+
+    if (Class_AdminVar::get('DILICOM_PNB_BOARD_DISPLAY_SECTION'))
+      $this->addColumn($this->_('Genre'),
+                       ['callback' => function ($loan) {
+                                        $ids = $loan->getGenres();
+                                        return implode('; ',
+                                                       Class_CodifGenre::labelsOfIds($ids)
+                                                       ->getArrayCopy());
+                                      },
+                        'sortable' => false])
+
+           ->addColumn($this->_('Section'),
+                       ['callback' => function ($loan) {
+                                        $ids = $loan->getSections();
+                                        return implode('; ',
+                                                       Class_CodifSection::labelsOfIds($ids)
+                                                       ->getArrayCopy());
+                                       },
+                        'sortable' => false]);
+  }
+}
diff --git a/library/Class/TableDescription/PNBItems.php b/library/Class/TableDescription/PNBUsages.php
similarity index 64%
rename from library/Class/TableDescription/PNBItems.php
rename to library/Class/TableDescription/PNBUsages.php
index c630484111aa1ad0f16b4171930748793baf2481..f916ee438d50233a5e642800e4c1b714251a584f 100644
--- a/library/Class/TableDescription/PNBItems.php
+++ b/library/Class/TableDescription/PNBUsages.php
@@ -20,25 +20,49 @@
  */
 
 
-class Class_TableDescription_PNBItems extends Class_TableDescription {
+class Class_TableDescription_PNBUsages extends Class_TableDescription {
+
   public function init() {
     $this
       ->addColumn($this->_('Titre'), 'title')
-      ->addColumn($this->_('Nombre de prêts'), 'quantity_on_total')
-      ->addColumn($this->_('Nombre de prêts simultanés'), 'live_quantity')
-      ->addColumn($this->_('Nombre de réservations'),
-                  function($model)
-                  {
-                    return Class_Hold_Pnb::findAllByAlbum($model->getAlbum())->count();
-                  })
+      ->_addLoansColumns()
+      ->_addLiveLoansColumns()
+      ->addColumn($this->_('Nombre de réservations'), 'hold_count')
       ->addColumn($this->_('Durée de prêt en jours'), 'duration')
-      ->addColumn($this->_('Nombre de jours restant sur la licence'), 'license_expiration')
-      ->addColumn($this->_('Date de commande'), 'order_date');
+      ->addColumn($this->_('Nombre de jours restant sur la licence'), 'license_expiration_or_infinite')
+      ->addColumn($this->_('Date de commande'),
+                  function($usage) {
+                    return (new DateTime($usage->getOrderDate()))->format('d/m/Y');
+                  })
+      ->_addExtraColumns();
+    return $this->_getRowActions();
+  }
+
+
+  protected function _addLoansColumns() {
+    return $this->addColumn($this->_('Prêts / Droits'),
+                            ['attribute' => 'quantity_on_total',
+                             'sort_attribute' => 'loan_quantity']);
+  }
+
 
+  protected function _addLiveLoansColumns() {
+    return $this->addColumn($this->_('Prêts simultanés / Droits'),
+                  ['attribute' => 'live_quantity_on_total',
+                   'sort_attribute' => 'live_quantity']);
+  }
+
+
+  protected function _addExtraColumns() {
     if (Class_AdminVar::get('DILICOM_PNB_BOARD_DISPLAY_SECTION'))
-      $this->addColumn($this->_('Genre'), 'genre')
-           ->addColumn($this->_('Section'), 'section');
+      $this->addColumn($this->_('Genre'), 'genre_labels')
+           ->addColumn($this->_('Section'), 'section_labels');
+
+    return $this;
+  }
 
+
+  protected function _getRowActions() {
     $this->addRowAction(['url' => ['module' => 'admin',
                                    'controller' => 'album',
                                    'action' => 'edit_album',
@@ -53,7 +77,6 @@ class Class_TableDescription_PNBItems extends Class_TableDescription {
 
   protected function _addMultiSelectionAction() {
     $multi_selection = new Class_MultiSelection_AlbumItem();
-
     foreach( (new ZendAfi_Controller_Plugin_MultiSelection_LeafActions($multi_selection))
             ->getActions() as $action) {
       $action['id'] = function($model) { return $model->getAlbumId(); };
diff --git a/library/Class/User/SearchCriteria/Age.php b/library/Class/User/SearchCriteria/Age.php
index 557bc18f84e03f5547934ac49f81cc3d4db34c87..f03184e9ff804e6c95f0936e83f44c50c26b426e 100644
--- a/library/Class/User/SearchCriteria/Age.php
+++ b/library/Class/User/SearchCriteria/Age.php
@@ -41,7 +41,7 @@ class Class_User_SearchCriteria_Age extends Class_SearchCriteria_Abstract {
     $this->_to = isset($params[$this->_end_name])
       ? $params[$this->_end_name]
       : '';
-    $this->_element->loadDefault($params);
+    $this->_element->setCompositeInputValues($params);
   }
 
 
diff --git a/library/Class/User/SearchCriteria/DateFin.php b/library/Class/User/SearchCriteria/DateFin.php
index 12438d1e81cbcb4363d4f6ab510d16891dda9fce..a16366f6a7723249e36f1c9bf863ba9f8d78e73a 100644
--- a/library/Class/User/SearchCriteria/DateFin.php
+++ b/library/Class/User/SearchCriteria/DateFin.php
@@ -22,7 +22,9 @@
 
 class Class_User_SearchCriteria_DateFin extends Class_SearchCriteria_DateRange {
   protected
-    $_name = 'date_fin';
+    $_name = 'date_fin',
+    $_start_suffix = '_start',
+    $_end_suffix = '_end';
 
 
   public function buildElement() {
diff --git a/library/Class/User/SearchCriteria/DateMaj.php b/library/Class/User/SearchCriteria/DateMaj.php
index 31e68236eb4e3573f563d789edeb20985a61304d..b2a1987118730653391fc719dd14194e22a3c467 100644
--- a/library/Class/User/SearchCriteria/DateMaj.php
+++ b/library/Class/User/SearchCriteria/DateMaj.php
@@ -22,7 +22,10 @@
 
 class Class_User_SearchCriteria_DateMaj extends Class_SearchCriteria_DateRange {
   protected
-    $_name = 'date_maj';
+    $_name = 'date_maj',
+    $_start_suffix = '_start',
+    $_end_suffix = '_end';
+
 
 
   public function buildElement() {
diff --git a/library/Class/User/SearchCriteria/LastLogin.php b/library/Class/User/SearchCriteria/LastLogin.php
index df66ceefe5d4f7eed7a903b00e66a4eb880f1fb9..ed6a215481b657e53acdb88b2cebdf5e6379e1ee 100644
--- a/library/Class/User/SearchCriteria/LastLogin.php
+++ b/library/Class/User/SearchCriteria/LastLogin.php
@@ -22,7 +22,9 @@
 
 class Class_User_SearchCriteria_LastLogin extends Class_SearchCriteria_DateRange {
   protected
-    $_name = 'last_login';
+    $_name = 'last_login',
+    $_start_suffix = '_start',
+    $_end_suffix = '_end';
 
 
   public function buildElement() {
diff --git a/library/ZendAfi/Acl/AdminControllerGroup.php b/library/ZendAfi/Acl/AdminControllerGroup.php
index 331860369612beeb540861cb13ef2ef42fa298d0..05eff8f0132b4e4c7a67fdcf785a203c09af6976 100644
--- a/library/ZendAfi/Acl/AdminControllerGroup.php
+++ b/library/ZendAfi/Acl/AdminControllerGroup.php
@@ -58,7 +58,7 @@ class ZendAfi_Acl_AdminControllerGroup {
                  'modo' => Class_UserGroup::RIGHT_USER_MODO,
                  'registration' => Class_UserGroup::RIGHT_USER_INSCRIPTIONS,
                  'album' => Class_UserGroup::RIGHT_USER_BIB_NUM,
-                 'album/dilicom' => Class_UserGroup::RIGHT_ADMIN_PNB_DILICOM,
+                 'pnb' => Class_UserGroup::RIGHT_ADMIN_PNB_DILICOM,
                  'bibnum' => Class_UserGroup::RIGHT_USER_BIB_NUM,
                  'opds' => Class_UserGroup::RIGHT_USER_OPDS_READ,
                  'oai' => Class_UserGroup::RIGHT_USER_BIB_NUM,
@@ -95,7 +95,7 @@ class ZendAfi_Acl_AdminControllerGroup {
                           'harvest/jamendo-browse' => Class_AdminVar::isJamendoEnabled(),
                           'harvest/soundcloud' => Class_AdminVar::isSoundCloudEnabled(),
                           'sito/create' => Class_AdminVar::isSitoInAlbums(),
-                          'album/dilicom' => Class_AdminVar::isDilicomPNBEnabled(),
+                          'pnb' => Class_AdminVar::isDilicomPNBEnabled(),
                           'album/import_ead' => Class_AdminVar::isImportEadEnabled(),
                           'premier-chapitre' => Class_AdminVar::isPremierChapitreEnabled(),
                           'i18n' => Class_AdminVar::isTranslationEnabled(),
diff --git a/library/ZendAfi/Acl/AdminControllerRoles.php b/library/ZendAfi/Acl/AdminControllerRoles.php
index 2dc194e0415493f6de3256b5543ee24f992783c6..8f26a14f7df17dbb575b1053e45f27d99cd97332 100644
--- a/library/ZendAfi/Acl/AdminControllerRoles.php
+++ b/library/ZendAfi/Acl/AdminControllerRoles.php
@@ -96,6 +96,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl {
     $this->add(new Zend_Acl_Resource('bibnum'));
     $this->add(new Zend_Acl_Resource('harvest'));
     $this->add(new Zend_Acl_Resource('album'));
+    $this->add(new Zend_Acl_Resource('pnb'));
     $this->add(new Zend_Acl_Resource('oai'));
     $this->add(new Zend_Acl_Resource('frbr-link'));
     $this->add(new Zend_Acl_Resource('print'));
@@ -177,6 +178,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl {
     $this->allow('admin_bib','lieu');
     $this->allow('admin_bib','bibnum');
     $this->allow('admin_bib','album');
+    $this->allow('admin_bib','pnb');
     $this->allow('admin_bib','oai');
     $this->allow('admin_bib','frbr-link');
     $this->allow('admin_bib','registration');
@@ -203,6 +205,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl {
     $this->deny('modo_portail','bibnum');
     $this->deny('modo_portail','harvest');
     $this->deny('modo_portail','album');
+    $this->deny('modo_portail','pnb');
     $this->deny('modo_portail','oai');
     $this->deny('modo_portail','frbr-link');
     $this->deny('modo_portail','profil');
diff --git a/library/ZendAfi/Controller/Action.php b/library/ZendAfi/Controller/Action.php
index 8f7c78c2da415b63362907f9e15342c46a8c8dbc..d64c646a435d728034bacf8a462285e5a86616e4 100644
--- a/library/ZendAfi/Controller/Action.php
+++ b/library/ZendAfi/Controller/Action.php
@@ -409,6 +409,11 @@ class ZendAfi_Controller_Action extends Zend_Controller_Action {
       ? $ig
       : new ZendAfi_Controller_Action_NullInspector();
   }
+
+
+  protected function _getParams($widget = null) {
+    return $this->_request->getParams();
+  }
 }
 
 
diff --git a/library/ZendAfi/Form.php b/library/ZendAfi/Form.php
index 1585975f2e9e8350c784cf865ffb309b63df8d50..423bf75b05af2300a1e99d1d5c65297730f074b6 100644
--- a/library/ZendAfi/Form.php
+++ b/library/ZendAfi/Form.php
@@ -70,7 +70,7 @@ class ZendAfi_Form extends Zend_Form {
   public function setDefaults(array $defaults) {
     foreach ($this->getElements() as $name => $element)
       if(method_exists($element, 'setDefaults'))
-        $element->setDefaults($this, $defaults);
+        $element->setDefaults($defaults);
 
     return parent::setDefaults($defaults);
   }
diff --git a/library/ZendAfi/Form/AdvancedSearch.php b/library/ZendAfi/Form/AdvancedSearch.php
index bad58547113e97a5d1a9e26e954cd81a2153d264..5ae5cf13e510bed4a4400eb1ab2383f2ae57786a 100644
--- a/library/ZendAfi/Form/AdvancedSearch.php
+++ b/library/ZendAfi/Form/AdvancedSearch.php
@@ -117,13 +117,13 @@ class ZendAfi_Form_AdvancedSearch extends ZendAfi_Form {
     parent::populate($datas);
 
     foreach(array_keys($this->_axes) as $type)
-      $this->$type->loadDefault($datas);
+      $this->$type->setCompositeInputValues($datas);
 
     if ($this->annee)
-      $this->annee->loadDefault($datas);
+      $this->annee->setCompositeInputValues($datas);
 
     foreach($this->_elementsByClass(ZendAfi_Form_Element_SearchAxeMultiInput::class) as $element)
-      $element->loadDefault($datas);
+      $element->setCompositeInputValues($datas);
   }
 
 
diff --git a/library/ZendAfi/Form/Element/DateRangePicker.php b/library/ZendAfi/Form/Element/DateRangePicker.php
index 6ea216d80d7225c8d330e7dede7cff5a7e3c0952..dcdf653ac4ae761d06139dedcc6bddff2e2d620d 100644
--- a/library/ZendAfi/Form/Element/DateRangePicker.php
+++ b/library/ZendAfi/Form/Element/DateRangePicker.php
@@ -101,7 +101,7 @@ class ZendAfi_Form_Element_DateRangePicker extends Zend_Form_Element_Xhtml {
   }
 
 
-  public function setDefaults($form, $params) {
+  public function setDefaults($params) {
     foreach ([$this->_start, $this->_end] as $element)
       if (array_key_exists($element->getName(), $params))
         $element->setValue($params[$element->getName()]);
diff --git a/library/ZendAfi/Form/Element/Range.php b/library/ZendAfi/Form/Element/Range.php
index 72767a87f9c3e1c4fb9912eadf86661613961b90..1d84a1dd4b4a04cb312317da7b32c7b9d4a4dac5 100644
--- a/library/ZendAfi/Form/Element/Range.php
+++ b/library/ZendAfi/Form/Element/Range.php
@@ -31,11 +31,23 @@ class ZendAfi_Form_Element_Range extends Zend_Form_Element {
   }
 
 
-  public function loadDefault($datas) {
+  public function setCompositeInputValues($datas) {
     if (array_key_exists($this->getName() . $this->getAttrib('from_suffix'), $datas))
       $this->setAttrib('from_value', $datas[$this->getName() . $this->getAttrib('from_suffix')]);
 
     if (array_key_exists($this->getName() . $this->getAttrib('to_suffix'), $datas))
       $this->setAttrib('to_value', $datas[$this->getName() . $this->getAttrib('to_suffix')]);
   }
+
+
+  public function setStartValue($value) {
+    $this->setAttrib('from_value', $value);
+    return $this;
+  }
+
+
+  public function setEndValue($value) {
+    $this->setAttrib('to_value', $value);
+    return $this;
+  }
 }
diff --git a/library/ZendAfi/Form/Element/SearchAxe.php b/library/ZendAfi/Form/Element/SearchAxe.php
index a5fa1debf2b0d14ce1eecbee6c7dccd80642141b..e4d35a6c2e44fe454ab09097c3549562ce29c322 100644
--- a/library/ZendAfi/Form/Element/SearchAxe.php
+++ b/library/ZendAfi/Form/Element/SearchAxe.php
@@ -38,7 +38,7 @@ class ZendAfi_Form_Element_SearchAxe extends Zend_Form_Element {
   }
 
 
-  public function loadDefault($datas) {
+  public function setCompositeInputValues($datas) {
     if (array_key_exists($this->getAttrib('input_prefix') . $this->getName(), $datas))
       $this->setValue($datas[$this->getAttrib('input_prefix') . $this->getName()]);
 
diff --git a/library/ZendAfi/Form/Element/SearchAxeMultiInput.php b/library/ZendAfi/Form/Element/SearchAxeMultiInput.php
index 78d9ca5c083c89e88e0b46c073c1e7e04486b90c..5c4444c6e475e4d2a3886b7c2051a90d51cf7b7d 100644
--- a/library/ZendAfi/Form/Element/SearchAxeMultiInput.php
+++ b/library/ZendAfi/Form/Element/SearchAxeMultiInput.php
@@ -97,7 +97,7 @@ class ZendAfi_Form_Element_SearchAxeMultiInput extends ZendAfi_Form_Element_Mult
   }
 
 
-  public function loadDefault($datas) {
+  public function setCompositeInputValues($datas) {
     $this->setValues($this->_custom_multi_param->values($this, $datas));
   }
 
diff --git a/library/ZendAfi/Form/Element/SelectDynamicFacet.php b/library/ZendAfi/Form/Element/SelectDynamicFacet.php
index 6f9ec980e9c08403e084f1737b461c4230c1eb74..c0ebfb21ed39d37a26e9e2dc85bbadea26aaa883 100644
--- a/library/ZendAfi/Form/Element/SelectDynamicFacet.php
+++ b/library/ZendAfi/Form/Element/SelectDynamicFacet.php
@@ -49,6 +49,6 @@ class ZendAfi_Form_Element_SelectDynamicFacet extends Zend_Form_Element_Select {
   }
 
 
-  public function loadDefault($datas) {
+  public function setCompositeInputValues($datas) {
   }
 }
diff --git a/library/ZendAfi/View/Helper/Admin/ContentNav.php b/library/ZendAfi/View/Helper/Admin/ContentNav.php
index a5b92f69e957391762c27b3ba1d60e60a1a6e4aa..c10e51717b808a8bac80344ca0b70fbdb19398d7 100644
--- a/library/ZendAfi/View/Helper/Admin/ContentNav.php
+++ b/library/ZendAfi/View/Helper/Admin/ContentNav.php
@@ -84,7 +84,7 @@ class ZendAfi_View_Helper_Admin_ContentNav extends ZendAfi_View_Helper_BaseHelpe
                     ['websites',          $this->_('Sitothèque'),        '/admin/album/add-website'],
 
                     ['jamendo',           $this->_('Jamendo'),           '/admin/harvest/jamendo-browse'],
-                    ['pnb',               $this->_('PNB Dilicom'),       '/admin/album/dilicom'],
+                    ['pnb',               $this->_('PNB Dilicom'),       '/admin/pnb'],
                     ['premierchapitre',   $this->_('Premier-Chapitre'),  '/admin/premier-chapitre'],
                     ['soundcloud',        $this->_('SoundCloud'),        '/admin/harvest/soundcloud'],
                     ['opds',              $this->_('Catalogues OPDS'),   '/admin/opds'],
diff --git a/library/ZendAfi/View/Helper/Admin/HelpLink.php b/library/ZendAfi/View/Helper/Admin/HelpLink.php
index 24e755a247d807204ddf4bf79688c436a825d719..094a4449c5642b688fd8140cf359ddc3ffafa99d 100644
--- a/library/ZendAfi/View/Helper/Admin/HelpLink.php
+++ b/library/ZendAfi/View/Helper/Admin/HelpLink.php
@@ -132,6 +132,7 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki {
                                   'plan' => 'Prise_de_rendez-vous_par_les_professionnels'],
      'identity-providers'     => ['index' => 'Fournisseurs_d\'identités'],
      'variable'               => ['index' => 'Liste_des_variables'],
+     'pnb'                    => ['index' => 'PNB_Dilicom_tableau_de_bord']
     ];
 
 
@@ -149,4 +150,4 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki {
 
     return sprintf($this->_pattern, $controller_mapping[$action]);
   }
-}
\ No newline at end of file
+}
diff --git a/library/ZendAfi/View/Helper/Admin/Search.php b/library/ZendAfi/View/Helper/Admin/Search.php
index c68257ce1358ed84932a61464f4225fd8a0f3487..10f71c11974e6b38a7ab95df52b04b17d4ad857f 100644
--- a/library/ZendAfi/View/Helper/Admin/Search.php
+++ b/library/ZendAfi/View/Helper/Admin/Search.php
@@ -45,7 +45,7 @@ abstract class ZendAfi_View_Helper_Admin_Search extends ZendAfi_View_Helper_Base
 
   protected function _resultMessage($total) {
     return  $this->view->_plural($total,
-                                 'Auncun élément trouvé',
+                                 'Aucun élément trouvé',
                                  '%d élément trouvé',
                                  '%d éléments trouvés',
                                  $total);
diff --git a/library/ZendAfi/View/Helper/Admin/SearchPnb.php b/library/ZendAfi/View/Helper/Admin/SearchPnb.php
new file mode 100644
index 0000000000000000000000000000000000000000..c32e793000492da0e52ce5f557d3c3111800e565
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/SearchPnb.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_View_Helper_Admin_SearchPnb extends ZendAfi_View_Helper_Admin_Search {
+  protected
+    $_table_description_class = Class_TableDescription_PNBUsages::class,
+    $_table_id = 'album_usage_report';
+
+
+  public function searchPnb($context) {
+    return $this->_search($context);
+  }
+
+
+  protected function _headerLine() {
+    $export_button = $this->view->button((new Class_Entity())
+                        ->setText($this->_('Exporter le tableau en CSV'))
+
+                        ->setUrl($this->view->url(['module' => 'admin',
+                                                   'controller' => 'pnb',
+                                                   'action' => 'export-csv'],
+                                                  null, true)
+                                 . '?'
+                                 . http_build_query(array_filter($this->_context->getParams())))
+                        ->setImage($this->view->tagImg(Class_Admin_Skin::current()
+                                                           ->getIconUrl('actions',
+                                                                        'test'),
+                                                           ['style' => 'filter: invert();']))
+                        ->setAttribs(['style' => 'vertical-align: middle;']));
+
+    return $this->_tag('p',
+                       $this->_resultMessage($this->_context->getTotal()) . $export_button);
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Admin/SearchPnbLoans.php b/library/ZendAfi/View/Helper/Admin/SearchPnbLoans.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff91a1b87a9970763ac36b239f42c368c3f381a0
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/SearchPnbLoans.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_View_Helper_Admin_SearchPnbLoans extends ZendAfi_View_Helper_Admin_Search {
+  protected
+    $_table_description_class = Class_TableDescription_PNBLoansHistory::class,
+    $_table_id = 'pnb_loans_table';
+
+
+  public function searchPnbLoans($context) {
+    return $this->_search($context);
+  }
+
+
+
+  protected function _headerLine() {
+    $export_button = $this->view->button((new Class_Entity())
+                        ->setText($this->_('Exporter le tableau en CSV'))
+
+                        ->setUrl($this->view->url(['module' => 'admin',
+                                                   'controller' => 'pnb',
+                                                   'action' => 'export-loans-csv'],
+                                                  null, true)
+                                 . '?'
+                                 . http_build_query(array_filter($this->_context->getParams())))
+                        ->setImage($this->view->tagImg(Class_Admin_Skin::current()
+                                                           ->getIconUrl('actions',
+                                                                        'test'),
+                                                           ['style' => 'filter: invert();']))
+                        ->setAttribs(['style' => 'vertical-align: middle;']));
+
+    return $this->_tag('p',
+                       $this->_resultMessage($this->_context->getTotal()) . $export_button);
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php b/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php
index 0c29ced16bc17a6f033d9b32c55506ff3c35c82e..5e53665ca7d895108975d67f647334d5489a0c69 100644
--- a/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php
+++ b/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php
@@ -57,7 +57,7 @@ class ZendAfi_View_Helper_Admin_SearchRendezVous
 
   protected function _resultMessage($total) {
     return $this->view->_plural($total,
-                                'Auncun rendez-vous trouvé',
+                                'Aucun rendez-vous trouvé',
                                 '%d rendez-vous trouvé',
                                 '%d rendez-vous trouvés',
                                 $total);
diff --git a/library/ZendAfi/View/Helper/TagListeCoches.php b/library/ZendAfi/View/Helper/TagListeCoches.php
index a916aa0de2d110d635d2456fb34d0ec584818f4b..3d3e57edc0428985225dacd6e7fb8d4f20773384 100644
--- a/library/ZendAfi/View/Helper/TagListeCoches.php
+++ b/library/ZendAfi/View/Helper/TagListeCoches.php
@@ -20,6 +20,10 @@
  */
 
 class ZendAfi_View_Helper_TagListeCoches extends ZendAfi_View_Helper_BaseHelper {
+  const
+    SOURCE_TYPE_DOC = 'type_doc',
+    SOURCE_GENRE_PNB = 'genre_pnb',
+    SOURCE_SECTION_PNB = 'section_pnb';
   protected
     $selected_all_means_nothing = true,
     $_name,
@@ -62,9 +66,11 @@ class ZendAfi_View_Helper_TagListeCoches extends ZendAfi_View_Helper_BaseHelper
       return call_user_func($data);
 
     $datas =
-      ['type_doc' => new ZendAfi_View_Helper_TagListeCochesSourceDocType(),
+      [static::SOURCE_TYPE_DOC => new ZendAfi_View_Helper_TagListeCochesSourceDocType(),
        'section' => $this->_simpleSourceFor('Class_CodifSection'),
        'genre' => $this->_simpleSourceFor('Class_CodifGenre'),
+       static::SOURCE_GENRE_PNB => new ZendAfi_View_Helper_TagListeCochesSourcePnb('Class_CodifGenre'),
+       static::SOURCE_SECTION_PNB => new ZendAfi_View_Helper_TagListeCochesSourcePnb('Class_CodifSection'),
        'langue' => $this->_simpleSourceFor('Class_CodifLangue'),
        'emplacement' => $this->_simpleSourceFor('Class_CodifEmplacement'),
        'nature_doc' => $this->_simpleSourceFor('Class_NatureDoc'),
@@ -202,7 +208,7 @@ class ZendAfi_View_Helper_TagListeCoches extends ZendAfi_View_Helper_BaseHelper
 
 class ZendAfi_View_Helper_TagListeCochesSource extends Class_Entity {
   public function __construct($model) {
-    $this->setModel($model);
+    $this->setModelClass($model);
   }
 
 
@@ -229,7 +235,7 @@ class ZendAfi_View_Helper_TagListeCochesSource extends Class_Entity {
     $filter = ($where = $this->getWhere()) ? $where : [];
     $filter['order'] = 'libelle';
 
-    return call_user_func([$this->getModel('model'), 'findAllBy'], $filter);
+    return call_user_func([$this->getModelClass('model'), 'findAllBy'], $filter);
   }
 }
 
@@ -332,3 +338,20 @@ class ZendAfi_View_Helper_TagListeCochesSuggestion extends ZendAfi_View_Helper_T
   }
 
 }
+
+
+
+
+
+class ZendAfi_View_Helper_TagListeCochesSourcePnb extends ZendAfi_View_Helper_TagListeCochesSource {
+  protected function _getElement($id) {
+    $model = $this->getModelClass();
+    return call_user_func([$model, 'find'], $id);
+  }
+
+
+  protected function getInstances() {
+    return call_user_func([$this->getModelClass(), 'findAllByAlbums'],
+                          Class_Album::findAllDilicom());
+  }
+}
diff --git a/library/storm b/library/storm
index f079a07b1240a87906582fb1e328df1b3eb38074..24c1b0d7aaa7bb33dcc87edb852ecc671d03bbd1 160000
--- a/library/storm
+++ b/library/storm
@@ -1 +1 @@
-Subproject commit f079a07b1240a87906582fb1e328df1b3eb38074
+Subproject commit 24c1b0d7aaa7bb33dcc87edb852ecc671d03bbd1
diff --git a/public/admin/skins/bokeh74/global.css b/public/admin/skins/bokeh74/global.css
index 60da2ea378d4feb56e62ddfdf5be1c5c4361b700..b07a7f20c2e2ac1462dfa32ed7f4adce65b5730d 100755
--- a/public/admin/skins/bokeh74/global.css
+++ b/public/admin/skins/bokeh74/global.css
@@ -576,7 +576,6 @@ table {
 
 .modules .menu li {
     display: inline-block;
-    float: left;
     padding: 0 0.5em;
     margin: 1em 0.5em;
 }
@@ -639,11 +638,6 @@ table {
     line-height: 1.5em;
 }
 
-.modules .subview {
-    float: left;
-    width: 100%;
-}
-
 .critique {
     margin-bottom: 1em;
     margin-top: 1em;
@@ -1005,6 +999,12 @@ table#logs img {
     white-space: nowrap;
 }
 
+
+table#album_item th, table#album_usage_report th {
+    white-space: normal;
+}
+
+
 .modules th a img {
     width: 8px;
     height: 12px;
diff --git a/tests/application/modules/admin/controllers/UsersControllerTest.php b/tests/application/modules/admin/controllers/UsersControllerTest.php
index b0f9767dffa2f1ee8cb7f0e11b1ad2f65e21c192..9d1e2cc936a602325242a29bda4183838d1ebea3 100644
--- a/tests/application/modules/admin/controllers/UsersControllerTest.php
+++ b/tests/application/modules/admin/controllers/UsersControllerTest.php
@@ -2040,7 +2040,7 @@ abstract class UsersControllerMassDeleteTestCase extends Admin_AbstractControlle
          ->with(['role_level' => 2,
                  'statut' => 1,
                  'order' => 'nom asc',
-                 'where' => '(date_fin <= "2018-05-28") AND (date_maj <= "2018-05-28") AND (login LIKE "%andy%" OR nom LIKE "%andy%" OR prenom LIKE "%andy%" OR pseudo LIKE "%andy%" OR mail LIKE "%andy%" OR idabon LIKE "%andy%") AND (role_level <= 7)'])
+                 'where' => '(left(date_fin, 10) <= "2018-05-28") AND (left(date_maj, 10) <= "2018-05-28") AND (login LIKE "%andy%" OR nom LIKE "%andy%" OR prenom LIKE "%andy%" OR pseudo LIKE "%andy%" OR mail LIKE "%andy%" OR idabon LIKE "%andy%") AND (role_level <= 7)'])
          ->answers(2)
 
          ->whenCalled('countBy')
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index d3954de66c713d75524a5d7c455f0667da3a0584..45606866b9af215d1a29cb0143163eaccd9e78f9 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -3857,4 +3857,41 @@ class UpgradeDB_410_Test extends UpgradeDBTestCase {
   public function catalogueTypeDocShouldBeTypeText() {
     $this->assertFieldType('catalogue', 'type_doc', 'text');
   }
-}
\ No newline at end of file
+}
+
+
+
+
+class UpgradeDB_411_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    $this->dropTable('album_usage_report');
+  }
+
+
+  public function datas() {
+    return
+      [
+       ['id',  'int(11) unsigned'],
+       ['item_id', 'int(11) unsigned'],
+       ['title', 'varchar(255)'],
+       ['loan_quantity', 'int(11)'],
+       ['total_quantity', 'int(11)'],
+       ['live_quantity', 'int(11)'],
+       ['hold_count', 'int(11)'],
+       ['duration', 'int(11)'],
+       ['license_expiration', 'int(11)'],
+       ['order_date', 'datetime'],
+       ['genres', 'varchar(255)'],
+       ['sections', 'varchar(255)']
+      ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider datas
+   */
+  public function fieldsShouldExists($field, $type) {
+    $this->assertFieldType('album_usage_report', $field, $type);
+  }
+}
diff --git a/tests/library/ZendAfi/View/Helper/Admin/MenuGaucheAdminTest.php b/tests/library/ZendAfi/View/Helper/Admin/MenuGaucheAdminTest.php
index 37d408d930bf2be52770f7c53138e2a6a1a493ff..0b4be96fe7b447803c688f0ec5ff0d08efde407c 100644
--- a/tests/library/ZendAfi/View/Helper/Admin/MenuGaucheAdminTest.php
+++ b/tests/library/ZendAfi/View/Helper/Admin/MenuGaucheAdminTest.php
@@ -100,8 +100,8 @@ class ZendAfi_View_Helper_Admin_MenuGaucheAdminVariableAsAdminTest
 
 
   /** @test */
-  public function menuImportDilicomShouldNotBeVisibleWhenOptionDILICOM_PNB_isDisabled() {
-    $this->assertNotXPath($this->helper->Admin_ContentNav(), '//a[contains(@href, "/admin/album/dilicom")]');
+  public function menuPnbDilicomShouldNotBeVisibleWhenOptionDILICOM_PNB_isDisabled() {
+    $this->assertNotXPath($this->helper->Admin_ContentNav(), '//a[contains(@href, "/admin/pnb")]');
   }
 
 
@@ -151,7 +151,7 @@ class ZendAfi_View_Helper_Admin_MenuGaucheAdminPNBActivatedAsAdminTest
 
   /** @test */
   public function menuImportDilicomShouldBeVisibleWhenOptionDILICOM_PNB_isEnabled() {
-    $this->assertXPath($this->helper->Admin_ContentNav(), '//a[contains(@href, "/admin/album/dilicom")]');
+    $this->assertXPath($this->helper->Admin_ContentNav(), '//a[contains(@href, "/admin/pnb")]');
   }
 }
 
diff --git a/tests/scenarios/PnbDilicom/PnbDilicomAdminTest.php b/tests/scenarios/PnbDilicom/PnbDilicomAdminTest.php
index 9bba4fa5f65f99931080f51a8ce64d36bdb52634..1190aec59592ebbedf664ce3115fd8202d2518d7 100644
--- a/tests/scenarios/PnbDilicom/PnbDilicomAdminTest.php
+++ b/tests/scenarios/PnbDilicom/PnbDilicomAdminTest.php
@@ -21,9 +21,13 @@
 
 require_once 'tests/fixtures/DilicomFixtures.php';
 
-abstract class PnbDilicomAdminAlbumControllerTestCase extends Admin_AbstractControllerTestCase {
+abstract class PnbDilicomAdminTestCase extends Admin_AbstractControllerTestCase {
   const BASE_TEST_URL = "https://pnb-test.centprod.com/v3/pnb-numerique/json/";
-  protected $_storm_default_to_volatile = true;
+  protected
+    $_storm_default_to_volatile = true,
+    $_log = '',
+    $_debug_log = '';
+
 
   public function setUp() {
     parent::setUp();
@@ -114,10 +118,18 @@ abstract class PnbDilicomAdminAlbumControllerTestCase extends Admin_AbstractCont
                    ['id' => 23,
                     'libelle' => 'Heavy Metal']);
 
+    $this->fixture('Class_CodifGenre',
+                   ['id' => 24,
+                    'libelle' => 'Jazz']);
+
     $this->fixture('Class_CodifSection',
                    ['id' => 33,
                     'libelle' => 'Espace métal']);
 
+    $this->fixture('Class_CodifSection',
+                   ['id' => 34,
+                    'libelle' => 'Espace jazz']);
+
     $fondu = $this->fixture('Class_AlbumCategorie',
                             ['id' => 1301,
                              'libelle' => 'Fondu']);
@@ -127,8 +139,8 @@ abstract class PnbDilicomAdminAlbumControllerTestCase extends Admin_AbstractCont
                     'id_origine' => 'Dilicom-3663608260879',
                     'titre' => 'Hell is from here to eternity',
                     'type_doc_id' => Class_TypeDoc::DILICOM,
-                    'genre' => '23',
-                    'sections' => '33',
+                    'genre' => '23;24',
+                    'sections' => '33;34',
                     'categorie' => $fondu])
          ->addAuthor('Iron Maiden')
          ->addEditor('EMI')
@@ -200,6 +212,20 @@ abstract class PnbDilicomAdminAlbumControllerTestCase extends Admin_AbstractCont
   }
 
 
+  public function getLogger() {
+    return $this->logger = $this->mock()
+                                ->whenCalled('log')
+                                ->willDo(
+                                         function($message, $target = '') {
+                                           if ($target == 'debug') {
+                                             $this->_debug_log .= $message . "\n";
+                                             return;
+                                           }
+                                           $this->_log .= $message;
+                                         });
+  }
+
+
   public function tearDown() {
     RessourcesNumeriquesFixtures::deactivateDilicom();
     Class_Loan_Pnb::setTimeSource(null);
@@ -211,7 +237,7 @@ abstract class PnbDilicomAdminAlbumControllerTestCase extends Admin_AbstractCont
 
 
 
-class PnbDilicomExtendLoanImpossibleTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomExtendLoanImpossibleTest extends PnbDilicomAdminTestCase {
   protected $_http;
   public function setUp() {
     parent::setUp();
@@ -250,7 +276,7 @@ class PnbDilicomExtendLoanImpossibleTest extends PnbDilicomAdminAlbumControllerT
 
 
 
-class PnbDilicomExtendLoanSuccessTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomExtendLoanSuccessTest extends PnbDilicomAdminTestCase {
   protected $_http;
   public function setUp() {
     parent::setUp();
@@ -279,14 +305,12 @@ class PnbDilicomExtendLoanSuccessTest extends PnbDilicomAdminAlbumControllerTest
   public function loanDateShouldBeModified() {
     $this->assertEquals('2021-01-10 11:00:00',Class_Loan_Pnb::find(1)->getExpectedReturnDate());
   }
-
-
 }
 
 
 
 
-class PnbDilicomReturnLoanImpossibleTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomReturnLoanImpossibleTest extends PnbDilicomAdminTestCase {
   protected $_http;
   public function setUp() {
     parent::setUp();
@@ -328,7 +352,7 @@ class PnbDilicomReturnLoanImpossibleTest extends PnbDilicomAdminAlbumControllerT
 
 
 
-class PnbDilicomReturnLoanSuccessTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomReturnLoanSuccessTest extends PnbDilicomAdminTestCase {
   protected $_http;
   public function setUp() {
     parent::setUp();
@@ -366,35 +390,74 @@ class PnbDilicomReturnLoanSuccessTest extends PnbDilicomAdminAlbumControllerTest
 
 
 
-class PnbDilicomAdminAlbumControllerImportDilicomTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomAdminIndexTest extends PnbDilicomAdminTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/pnb');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToUsage() {
+    $this->assertXPathContentContains('//div[@class="menu"]/ul/li/a[@href="/admin/pnb/usage"]',
+                                      'Utilisation');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToLoans() {
+    $this->assertXPathContentContains('//div[@class="menu"]/ul/li/a[@href="/admin/pnb/loans"]',
+                                      'Prêts');
+  }
+}
+
+
+
 
+class PnbDilicomAdminUsageTest extends PnbDilicomAdminTestCase {
   public function setUp() {
     parent::setUp();
-    $this->dispatch('/admin/album/dilicom', true);
+
+    (new Class_Batch_DilicomJobGenerateUsageReport(new Class_Batch_Dilicom))
+      ->setLogger($this->getLogger())
+      ->run();
+
+    $this->dispatch('/admin/pnb/usage');
   }
 
 
   /** @test */
-  public function tableSorterPagerJsShouldBeLoaded() {
-    $this->assertXPath('//script[contains(@src, "tablesorter/addons/pager/jquery.tablesorter.pager.min")]');
+  public function inputSearchTitleShouldBePresent() {
+    $this->assertXPath( '//input[@name="search_title"]');
   }
 
 
   /** @test */
-  public function tableSorterPagerCSSShouldBeLoaded() {
-    $this->assertXPath('//link[contains(@href, "/public/opac/js/tablesorter/addons/pager/jquery.tablesorter.pager.css")]');
+  public function inputOrderDateShouldBeDisplay() {
+    $this->assertXPath( '//input[@name="search_order_date_debut"]');
   }
 
 
   /** @test */
-  public function tableSorterPagerShouldBeActivated() {
-    $this->assertXPathContentContains('//script',
-                                      'tablesorterPager({container:');
+  public function titleShouldBeImportPnbDilicom() {
+    $this->assertXPathContentContains('//h1', 'PNB Dilicom');
   }
 
 
   /** @test */
-  public function beingHumanTitleShouldBeInTalbe() {
+  public function usedRessourcesShouldBePresent() {
+    $this->assertXPathContentContains('//h2', 'Utilisation des ressources PNB Dilicom');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTopMenu() {
+    $this->assertXPath('//div[@class="menu"]');
+  }
+
+
+  /** @test */
+  public function beingHumanTitleShouldBeInTable() {
     $this->assertXPathContentContains('//table//tr/td', 'Being human being');
   }
 
@@ -437,13 +500,7 @@ class PnbDilicomAdminAlbumControllerImportDilicomTest extends PnbDilicomAdminAlb
 
   /** @test */
   public function exportCSVButtonShouldBePresent() {
-    $this->assertXPathContentContains('//div[@class="modules"]/button[contains(@data-url, "/admin/album/dilicom-export-csv")]', 'Exporter le tableau en CSV');
-  }
-
-
-  /** @test */
-  public function exportCSVLoansButtonShouldBePresent() {
-    $this->assertXPathContentContains('//div[@class="modules"]/button[contains(@data-url, "/admin/album/dilicom-export-loans-csv")]', 'Exporter l\'historique des prêts en CSV');
+    $this->assertXPathContentContains('//div[@class="modules"]//button[contains(@data-url, "/admin/pnb/export-csv")]', 'Exporter le tableau en CSV');
   }
 
 
@@ -474,138 +531,159 @@ class PnbDilicomAdminAlbumControllerImportDilicomTest extends PnbDilicomAdminAlb
 
 
 
-class PnbDilicomAdminAlbumControllerEditTest extends PnbDilicomAdminAlbumControllerTestCase {
+abstract class PnbDilicomAdminSearchTestCase extends PnbDilicomAdminTestCase {
+  protected $_search_params;
 
   public function setUp() {
     parent::setUp();
-    $this->dispatch('/admin/album/editalbum/id/23', true);
+
+    (new Class_Batch_DilicomJobGenerateUsageReport(new Class_Batch_Dilicom))
+      ->setLogger($this->getLogger())
+      ->run();
+
+    Class_AdminVar::set('DILICOM_PNB_BOARD_DISPLAY_SECTION',1);
+    $where = '(title like "%Being%") AND (left(order_date, 10) >= "2016-01-01") AND (left(order_date, 10) <= "2017-12-31") AND (genres RLIKE "(^|;)23(;|$)") AND (sections RLIKE "(^|;)33(;|$)")';
+    $this->onLoaderOfModel('Class_Album_UsageReport')
+         ->whenCalled('findAllBy')
+         ->with($this->_getSql($where))
+         ->answers([Class_Album_UsageReport::findFirstBy(['item_id' => 1]),
+                    Class_Album_UsageReport::findFirstBy(['item_id' => 2])])
+         ->whenCalled('countBy')
+         ->with($this->_getSql($where,null))
+         ->answers(2)
+         ->beStrict();
+
+    Class_Album::find(23)->setGenre('23');
+    $this->_search_params = ['search_title' =>'Being',
+                             'search_order' => 'loan_quantity',
+                             'search_genres' => 23,
+                             'search_sections' =>33,
+                             'search_order_date_debut' => '01/01/2016',
+                             'search_order_date_fin' => '31/12/2017'
+    ];
+  }
+
+
+  protected function _getSql($where, $limit=[1,20]) {
+    return array_filter (
+                         ['order' => 'loan_quantity',
+                          'where' => $where,
+                          'limitPage' => $limit]);
   }
+}
 
 
-  /** @test */
-  public function inputTitreShouldContainsBeingHumanBeing() {
-    $this->assertXPath('//input[@name="titre"][@value="Being human being"]');
-  }
 
 
-  /** @test */
-  public function fieldsetUsageShouldBeVisible() {
-    $this->assertXPathContentContains('//fieldset/legend','Utilisation');
+class PnbDilicomAdminSearchUsageTest extends PnbDilicomAdminSearchTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/pnb/usage?' . http_build_query($this->_search_params));
   }
 
 
   /** @test */
-  public function dtInUsageShouldContainsPret() {
-    $this->assertXPathContentContains('//fieldset//dl//dt', 'Prêt');
+  public function beingHumanTitleShouldBeInTable() {
+    $this->assertXPathContentContains('//table//tr/td', 'Being human being');
   }
 
 
   /** @test */
-  public function dddldtShouldContainsDuration() {
-    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dt', 'Durée (j)');
+  public function hellIsFromHereToEternityShoulNotBeInTable() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'Hell is from here to eternity');
   }
 
 
   /** @test */
-  public function dddlddShouldContains45() {
-    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dd', '45');
+  public function exportButtonShouldKeepParams() {
+    $this->assertXPath('//button[contains(@data-url,"/export-csv?' . http_build_query($this->_search_params) . '")]');
   }
+}
 
 
-  /** @test */
-  public function dddldtShouldContainsDateDeCommande() {
-    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dt', 'Date de commande');
+
+
+class PnbDilicomAdminSearchLoanHoldLicenseUsageTest extends PnbDilicomAdminSearchTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->_search_params = [];
+    $this->_search_params['search_loan_quantity_debut'] = '3';
+    $this->_search_params['search_loan_quantity_fin'] = '5';
+    $this->_search_params['search_hold_count_debut'] = '3';
+    $this->_search_params['search_license_expiration_fin'] = '10';
+
+    $this->dispatch('/admin/pnb/usage?' . http_build_query($this->_search_params));
   }
 
 
-  /** @test */
-  public function dddldtShouldContainsNumeroCommande() {
-    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dt', 'Numéro de commande');
+  protected function _getSql($where,$limit=[1,20]) {
+    $where = '(loan_quantity >= "3") AND (loan_quantity <= "5") AND (hold_count >= "3") AND (license_expiration <= "10")';
+    return array_filter(['where' => $where,
+                         'limitPage' => $limit]);
   }
 
 
+
   /** @test */
-  public function dtInUsageShouldContainsAuthorizedDevices() {
-    $this->assertXPathContentContains('//fieldset//dl//dt', 'Appareils autorisés');
+  public function hellIsFromHereToEternityShoulNotBeInTable() {
+    $this->assertXPathContentContains('//table//tr//td', 'Being human being');
   }
 }
 
 
 
 
-class PnbDilicomAdminAlbumControllerEditPostTest extends PnbDilicomAdminAlbumControllerTestCase {
+
+class PnbDilicomAdminSearchMultipleGenreAndSectionUsageTest extends PnbDilicomAdminSearchTestCase {
 
   public function setUp() {
     parent::setUp();
+    $this->_search_params['search_genres'] = '23;2';
+    $this->_search_params['search_sections'] = '33;23';
 
-    $this->postDispatch('/admin/album/edit_album/id/23',
-                        ['titre' => 'Tootem']);
-    Class_Album::clearCache();
-    Class_Album_Item::clearCache();
-    Class_Album_UsageConstraint::clearCache();
-  }
-
-
-  /** @test */
-  public function constraintsShouldNotHaveBeenDeleted() {
-    $item = Class_Album::find(23)->getItems()[0];
-    $this->assertNotNull($item->getUsageConstraints()->getLoanConstraint());
+    $this->dispatch('/admin/pnb/usage?' . http_build_query($this->_search_params));
   }
-}
 
 
+  protected function _getSql($where,$limit=[1,20]) {
+    $where = '(title like "%Being%") AND (left(order_date, 10) >= "2016-01-01") AND (left(order_date, 10) <= "2017-12-31") AND (genres RLIKE "(^|;)23(;|$)" or genres RLIKE "(^|;)2(;|$)") AND (sections RLIKE "(^|;)33(;|$)" or sections RLIKE "(^|;)23(;|$)")';
 
+    return array_filter(['where' => $where,
+                         'order' => 'loan_quantity',
+                         'limitPage' => $limit]);
+  }
 
-class PnbDilicomAdminAlbumControllerImportDilicomPaginatorTest extends PnbDilicomAdminAlbumControllerTestCase {
 
-  public function setUp() {
-    parent::setUp();
-    $this->generateDilicomAlbums();
-    $this->dispatch('admin/album/dilicom', true);
+  /** @test */
+  public function beingHumanTitleShouldBeInTable() {
+    $this->assertXPathContentContains('//table//tr/td', 'Being human being');
   }
 
 
   /** @test */
-  public function pageShouldContainsPager() {
-    $this->assertXPathContentContains('//div[contains(@class,"pager model_table_pager")]', '1');
+  public function hellIsFromHereToEternityShoulNotBeInTable() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'Hell is from here to eternity');
   }
 
 
-  protected function generateDilicomAlbums() {
-    for($id=50; $id<=75; $id++) {
-      $this->fixture('Class_Album',
-                     ['id' => $id,
-                      'titre' => 'Being human n°' . $id,
-                      'type_doc_id' => Class_TypeDoc::DILICOM,
-                      'usage_constraints' =>
-                      [
-                       $this->fixture('Class_Album_UsageConstraint',
-                                      ['id' => 1000 + $id,
-                                       'usage_type' => Class_Album_UsageConstraint::LOAN_CONSTRAINT,
-                                       'serialized_datas' => json_encode([Class_Album_UsageConstraint::DURATION => 45,
-                                                                          Class_Album_UsageConstraint::QUANTITY => 30,
-                                                                          Class_Album_UsageConstraint::MAX_NB_OF_USERS => 15])]),
-
-                       $this->fixture('Class_Album_UsageConstraint',
-                                      ['id' => 100 + $id,
-                                       'usage_type' => Class_Album_UsageConstraint::DEVICE_SHARE_CONSTRAINT,
-                                       'serialized_datas' => json_encode([Class_Album_UsageConstraint::QUANTITY => 6])])
-                      ]
-                     ]);
-    }
+  /** @test */
+  public function exportButtonShouldKeepParams() {
+    $this->assertXPath('//button[contains(@data-url,"'
+                       . http_build_query($this->_search_params)
+                       . '")]');
   }
 }
 
 
 
 
-class PnbDilicomAdminAlbumControllerExportCsvTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomAdminSearchExportCsvTest extends PnbDilicomAdminSearchTestCase {
 
   public function setUp() {
     parent::setUp();
-
     Class_Album_UsageConstraints::setTimeSource(new TimeSourceForTest('2015-12-04 14:14:14'));
-    $this->dispatch('admin/album/dilicom-export-csv', true);
+    $this->dispatch('/admin/pnb/export-csv?' . http_build_query($this->_search_params));
   }
 
 
@@ -615,6 +693,12 @@ class PnbDilicomAdminAlbumControllerExportCsvTest extends PnbDilicomAdminAlbumCo
   }
 
 
+  protected function _getSql($where,$limit=null) {
+    return ['where' => $where,
+            'order' => 'loan_quantity'];
+  }
+
+
   /** @test */
   public function filenameShouldBeDilicomCsv() {
     $this->assertContains(['name' => 'Content-Type',
@@ -625,10 +709,9 @@ class PnbDilicomAdminAlbumControllerExportCsvTest extends PnbDilicomAdminAlbumCo
 
   /** @test */
   public function csvShouldContainsAlbumsItems() {
-    $this->assertEquals('Titre;"Prêts / Droits";"Nombre de prêts";"Prêts simultanés / Droits";"Prêts simultanés";"Durée de prêt en jours";"Nombre de jours restant sur la licence";"Date de commande";Auteur;Éditeur;Collection;Année;Genre;Section;Catégorie
-"Being human being";"10 / 30";10;"2 / 15";2;45;2095;30/03/2015;;;;;;;"Albums non classés"
-"Being human being";"20 / 20";20;"0 / 15";5;20;19902;29/08/2015;;;;;;;"Albums non classés"
-"Hell is from here to eternity";"2 / ∞";2;"1 / 1";1;359;∞;30/03/2015;"Iron Maiden";EMI;"Temple Of Rock";1992;"Heavy Metal";"Espace métal";Fondu
+    $this->assertEquals('Titre;"Prêts / Droits";"Nombre de prêts";"Prêts simultanés / Droits";"Prêts simultanés";"Nombre de réservations";"Durée de prêt en jours";"Nombre de jours restant sur la licence";"Date de commande";Auteur;Éditeur;Collection;Année;Genre;Section;Catégorie
+"Being human being";"10 / 30";10;"2 / 15";2;0;45;2095;30/03/2015;;;;;"Heavy Metal";;"Albums non classés"
+"Being human being";"20 / 20";20;"0 / 15";5;0;20;19902;29/08/2015;;;;;"Heavy Metal";;"Albums non classés"
 ', $this->_response->getBody());
   }
 }
@@ -636,7 +719,90 @@ class PnbDilicomAdminAlbumControllerExportCsvTest extends PnbDilicomAdminAlbumCo
 
 
 
-class PnbDilicomAdminAlbumControllerExportLoansCsvTest extends PnbDilicomAdminAlbumControllerTestCase {
+class PnbDilicomAdminAlbumControllerEditTest extends PnbDilicomAdminTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/album/editalbum/id/23', true);
+  }
+
+
+  /** @test */
+  public function inputTitreShouldContainsBeingHumanBeing() {
+    $this->assertXPath('//input[@name="titre"][@value="Being human being"]');
+  }
+
+
+  /** @test */
+  public function fieldsetUsageShouldBeVisible() {
+    $this->assertXPathContentContains('//fieldset/legend','Utilisation');
+  }
+
+
+  /** @test */
+  public function dtInUsageShouldContainsPret() {
+    $this->assertXPathContentContains('//fieldset//dl//dt', 'Prêt');
+  }
+
+
+  /** @test */
+  public function dddldtShouldContainsDuration() {
+    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dt', 'Durée (j)');
+  }
+
+
+  /** @test */
+  public function dddlddShouldContains45() {
+    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dd', '45');
+  }
+
+
+  /** @test */
+  public function dddldtShouldContainsDateDeCommande() {
+    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dt', 'Date de commande');
+  }
+
+
+  /** @test */
+  public function dddldtShouldContainsNumeroCommande() {
+    $this->assertXPathContentContains('//fieldset//dl/dd/dl/dt', 'Numéro de commande');
+  }
+
+
+  /** @test */
+  public function dtInUsageShouldContainsAuthorizedDevices() {
+    $this->assertXPathContentContains('//fieldset//dl//dt', 'Appareils autorisés');
+  }
+}
+
+
+
+
+class PnbDilicomAdminAlbumControllerEditPostTest extends PnbDilicomAdminTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->postDispatch('/admin/album/edit_album/id/23',
+                        ['titre' => 'Tootem']);
+    Class_Album::clearCache();
+    Class_Album_Item::clearCache();
+    Class_Album_UsageConstraint::clearCache();
+  }
+
+
+  /** @test */
+  public function constraintsShouldNotHaveBeenDeleted() {
+    $item = Class_Album::find(23)->getItems()[0];
+    $this->assertNotNull($item->getUsageConstraints()->getLoanConstraint());
+  }
+}
+
+
+
+
+
+class PnbDilicomAdminAlbumControllerExportLoansCsvTest extends PnbDilicomAdminTestCase {
   public function setUp() {
     parent::setUp();
 
@@ -671,8 +837,6 @@ class PnbDilicomAdminAlbumControllerExportLoansCsvTest extends PnbDilicomAdminAl
                     'loan_date' => '2020-02-06 13:57:33',
                     'loan_link' => 'https://pnb-dilicom.centprod.com/v2//XXXXXXXX.do',
                     'order_line_id' => '82377a045ce56ef0a072a8b']);
-
-    $this->dispatch('admin/album/dilicom-export-loans-csv');
   }
 
 
@@ -684,6 +848,7 @@ class PnbDilicomAdminAlbumControllerExportLoansCsvTest extends PnbDilicomAdminAl
 
   /** @test */
   public function filenameShouldBeDilicomLoansCsv() {
+    $this->dispatch('admin/pnb/export-loans-csv');
     $this->assertContains(['name' => 'Content-Type',
                            'value' => 'text/csv; name="dilicom_loans_csv.csv"',
                            'replace' => true],
@@ -691,8 +856,61 @@ class PnbDilicomAdminAlbumControllerExportLoansCsvTest extends PnbDilicomAdminAl
   }
 
 
-  /** @test */
-  public function csvShouldContainsAlbumsItems() {
+  public function urlParamsAndWhereQuery() {
+    return [
+
+            [// fr format
+             'search_loan_date_debut/10%2F12%2F2020/search_loan_date_fin/31%2F12%2F2020',
+             ['order' => 'loan_date',
+              'where' => '(left(loan_date, 10) >= "2020-12-10") AND (left(loan_date, 10) <= "2020-12-31")']
+            ],
+
+
+            [
+             '',
+             ['order' => 'loan_date']
+            ],
+
+
+
+            [
+             'search_loan_date_debut/10%2F12%2F2020',
+             ['order' => 'loan_date',
+              'where' => '(left(loan_date, 10) >= "2020-12-10")']
+            ],
+
+
+            [
+             'search_loan_date_fin/31%2F12%2F2020',
+             ['order' => 'loan_date',
+              'where' => '(left(loan_date, 10) <= "2020-12-31")']
+            ],
+
+
+
+            [
+             'search_expected_return_date_debut/10%2F12%2F2020/search_expected_return_date_fin/31%2F12%2F2020',
+             ['order' => 'loan_date',
+              'where' => '(left(expected_return_date, 10) >= "2020-12-10") AND (left(expected_return_date, 10) <= "2020-12-31")']
+            ],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider urlParamsAndWhereQuery
+   **/
+  public function csvShouldContainsAlbumsItems($url, $findall_params) {
+    $all_loans = Class_Loan_Pnb::findAllBy(['order' => 'loan_date']);
+    $this->onLoaderOfModel(Class_Loan_Pnb::class)
+         ->whenCalled('findAllBy')
+         ->with($findall_params)
+         ->answers($all_loans)
+         ->beStrict();
+
+    $this->dispatch('admin/pnb/export-loans-csv/' . $url);
+
     $this->assertEquals('Date;Titre;"Date de commande";Auteur;Éditeur;Collection;Année;Genre;Section;Catégorie;Bibliothèque
 16/12/2016;"Being human being";30/03/2015;;;;;;;"Albums non classés";
 16/12/2016;"Hell is from here to eternity";30/03/2015;"Iron Maiden";EMI;"Temple Of Rock";1992;"Heavy Metal";"Espace métal";Fondu;
@@ -709,13 +927,13 @@ class PnbDilicomAdminAlbumControllerExportLoansCsvTest extends PnbDilicomAdminAl
 
 
 class PnbDilicomAdminAlbumControllerExportLoansCsvWithMissingAlbumTest
-  extends PnbDilicomAdminAlbumControllerTestCase {
+  extends PnbDilicomAdminTestCase {
 
   public function setUp() {
     parent::setUp();
 
     Class_Album::find(23)->delete();
-    $this->dispatch('admin/album/dilicom-export-loans-csv');
+    $this->dispatch('admin/pnb/export-loans-csv');
   }
 
 
@@ -776,46 +994,141 @@ class PnbDilicomAdminIndexControllerTest extends AbstractControllerTestCase {
 
 
 
+abstract class PnbDilicomAdminLoansTestCase extends PnbDilicomAdminTestCase {
+  public function setUp() {
+    parent::setUp();
 
-class PnbDilicomAdminAlbumControllerImportTest extends Admin_AbstractControllerTestCase {
+    foreach(range(5, 25) as $i)
+      $this->fixture(Class_Loan_Pnb::class,
+                     ['id' => $i,
+                      'record_origin_id' => 'Dilicom-3663608260879',
+                      'subscriber_id' => '000006',
+                      'user_id' => 4078,
+                      'expected_return_date' => sprintf('2018-01-%s 13:57:33',
+                                                        str_pad($i, 2, '0', STR_PAD_LEFT)),
+                      'loan_date' => sprintf('2017-11-%s 13:57:33',
+                                             str_pad($i, 2, '0', STR_PAD_LEFT)),
+                      'loan_link' => 'https://pnb-dilicom.centprod.com/v2//XXXXXXXX.do',
+                      'order_line_id' => '584837a045ce56ef0a072a8b']);
+  }
+}
 
-  protected $_storm_default_to_volatile = true;
 
 
-  public function setUp() {
-    parent::setUp();
 
-    RessourcesNumeriquesFixtures::activateDilicom();
-    $this->dispatch('/admin/album/dilicom', true);
+class PnbDilicomAdminLoansTest extends PnbDilicomAdminLoansTestCase {
+  protected $_search_params = ['search_order' => 'loan_date',
+                               'search_loan_date_debut' => '10/11/2017',
+                               'search_loan_date_fin' => '20/11/2017'];
+
+  public function setup() {
+    parent::setup();
+    $this->dispatch('/admin/pnb/loans?' . http_build_query($this->_search_params));
   }
 
 
   /** @test */
-  public function titleShouldBeImportPnbDilicom() {
-    $this->assertXPathContentContains('//h1', 'PNB Dilicom');
+  public function pageShouldContainsTopMenu() {
+    $this->assertXPath('//div[@class="menu"]');
   }
 
 
   /** @test */
-  public function importOffresPnbDilicomShouldBePresent() {
-    $this->assertXPathContentContains('//h2', 'Import des offres Dilicom/PNB');
+  public function h2ShouldBeLoansHistory() {
+    $this->assertXPathContentContains('//h2', 'Historique des prêts');
   }
 
 
   /** @test */
-  public function usedRessourcesShouldBePresent() {
-    $this->assertXPathContentContains('//h2', 'Utilisation des ressources PNB Dilicom');
+  public function exportCSVLoansButtonWithParamsShouldBePresent() {
+    $this->assertXPathContentContains('//button[contains(@data-url, "/admin/pnb/export-loans-csv?' . http_build_query($this->_search_params) . '")]',
+                                      'Exporter le tableau en CSV');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsPaginationWithParamsForPageTwo() {
+    $this->assertXPath('//div[@class="pager"]//a[contains(@href, "/admin/pnb/loans/search_order/loan_date/search_loan_date_debut/10%2F11%2F2017/search_loan_date_fin/20%2F11%2F2017/search_expected_return_date_debut//search_expected_return_date_fin//page/2")]');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsColumnForTitle() {
+    $this->assertXPathContentContains('//table//td', 'Hell is from here to eternity');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsColumnForGenre() {
+    $this->assertNotXPathContentContains('//table//td', 'Heavy Metal; Jazz');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsColumnForSections() {
+    $this->assertNotXPathContentContains('//table//td', 'Espace métal; Espace jazz');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsColumnForLoanDate() {
+    $this->assertXPathContentContains('//table//td', '10/11/2017');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsColumnForExpectedReturnDate() {
+    $this->assertXPathContentContains('//table//td', '10/01/2018');
+  }
+
+
+  /** @test */
+  public function searchFormShouldContainsInputForReturnDateStart() {
+    $this->assertXPath('//form//fieldset[@id="fieldset-search_group"]//input[@name="search_expected_return_date_debut"]');
+  }
+
+
+  /** @test */
+  public function searchFormShouldContainsInputForReturnDateEnd() {
+    $this->assertXPath('//form//fieldset[@id="fieldset-search_group"]//input[@name="search_expected_return_date_fin"]');
+  }
+
+
+  /** @test */
+  public function searchFormShouldContainsInputForLoanDateStart() {
+    $this->assertXPath('//form//fieldset[@id="fieldset-search_group"]//input[@name="search_loan_date_debut"]');
+  }
+
+
+  /** @test */
+  public function searchFormShouldContainsInputForLoanDateEnd() {
+    $this->assertXPath('//form//fieldset[@id="fieldset-search_group"]//input[@name="search_loan_date_fin"]');
+  }
+}
+
+
+
+
+class PnbDilicomAdminControllerWithDilicomGenreSectionAdminVar extends PnbDilicomAdminLoansTestCase {
+
+  public function setUp() {
+    parent::setup();
+    Class_AdminVar::set('DILICOM_PNB_BOARD_DISPLAY_SECTION',1);
+    $this->dispatch('/admin/pnb/loans?'
+                    . http_build_query(['search_order' => 'loan_date',
+                                        'search_loan_date_debut' => '10/11/2017',
+                                        'search_loan_date_fin' => '20/11/2017']));
   }
 
 
   /** @test */
-  public function formImportOffersShouldContainsFileInputForXML() {
-    $this->assertXPath('//form[contains(@action, "admin/album/dilicom")]//input[@type="file"][@name="offers"]');
+  public function tableShouldContainsColumnForGenre() {
+    $this->assertXPathContentContains('//table//td', 'Heavy Metal; Jazz');
   }
 
 
   /** @test */
-  public function formShouldHaveSubmitButtonImportXML() {
-    $this->assertXPath('//input[@type="submit"][@value="Importer le fichier XML"]');
+  public function tableShouldContainsColumnForSections() {
+    $this->assertXPathContentContains('//table//td', 'Espace métal; Espace jazz');
   }
 }
diff --git a/tests/scenarios/PnbDilicom/PnbDilicomBatchTest.php b/tests/scenarios/PnbDilicom/PnbDilicomBatchTest.php
index 54f726162ee18216f4387d52b5b73c341c8af2db..7feb4cc659289db398268ea17db5e8aedba6f698 100644
--- a/tests/scenarios/PnbDilicom/PnbDilicomBatchTest.php
+++ b/tests/scenarios/PnbDilicom/PnbDilicomBatchTest.php
@@ -797,3 +797,109 @@ class PnbDilicomBatchJobUnindexExpiredOrdersByQuantityLeftTest extends PnbDilico
   }
 
 }
+
+
+class PnbDilicomBatchJobGenerateUsageReportTest extends PnbDilicomBatchTestCase {
+  protected $_report;
+
+  public function setUp() {
+    parent::setUp();
+    $this->_enableDilicom();
+    $this->fixture('Class_CodifGenre',
+                   ['id' => 23,
+                    'libelle' => 'Heavy Métal']);
+
+    $this->fixture('Class_CodifGenre',
+                   ['id' => 24,
+                    'libelle' => 'Jazz']);
+
+    $this->fixture('Class_CodifSection',
+                   ['id' => 33,
+                    'libelle' => 'Espace métal']);
+
+    $this->fixture('Class_CodifSection',
+                   ['id' => 34,
+                    'libelle' => 'Espace jazz']);
+
+    $this->_prepareTotemThora();
+
+    Class_Album::find(3)->setGenre('23;24')->save();
+    Class_Album::find(3)->setSections('33;34')->save();
+    Class_Album::find(3)
+      ->getItems()[0]
+      ->getUsageConstraints()
+      ->getLoanConstraint()
+      ->setDuration(1);
+
+
+    $this->_time_source = new TimeSourceForTest('2015-04-02 14:14:14');
+    Class_Album_UsageConstraint::setTimeSource($this->_time_source);
+    Class_Album_UsageConstraints::setTimeSource($this->_time_source);
+
+    (new Class_Batch_DilicomJobGenerateUsageReport(new Class_Batch_Dilicom))
+      ->setLogger($this->getLogger())
+      ->run();
+
+    $this->_report = Class_Album_UsageReport::find(1);
+  }
+
+
+  /** @test */
+  public function usageReportItemIdShouldBeOne() {
+    $this->assertEquals('1', $this->_report->getItemId());
+  }
+
+
+  /** @test */
+  public function usageReportTitreShouldBeTotemEtThora() {
+    $this->assertEquals('Totem et Thora', $this->_report->getTitle());
+  }
+
+
+  /** @test */
+  public function usageReportQuantityOnTotalShouldBe4On50() {
+    $this->assertEquals('4 / 50', $this->_report->getQuantityOnTotal());
+  }
+
+
+  /** @test */
+  public function usageReportLicenceExpirationShouldBe9998() {
+    $this->assertEquals('9998', $this->_report->getLicenseExpiration());
+  }
+
+
+  /** @test */
+  public function usageReportOrderDateShouldBe2015_04_01() {
+    $this->assertEquals('2015-04-01 00:00:00', $this->_report->getOrderDate());
+  }
+
+
+  /** @test */
+  public function usageReportGenreShouldBe2324() {
+    $this->assertEquals('23;24', $this->_report->getGenres());
+  }
+
+
+  /** @test */
+  public function usageReportSectionShouldBe3334() {
+    $this->assertEquals('33;34', $this->_report->getSections());
+  }
+
+
+  /** @test */
+  public function usageReportLiveQuantityShouldBe0slash50() {
+    $this->assertEquals('0 / 50', $this->_report->getLiveQuantityOnTotal());
+  }
+
+
+  /** @test */
+  public function usageReportHoldCountShouldBeZero() {
+    $this->assertEquals('0', $this->_report->getHoldCount());
+  }
+
+
+  /** @test */
+  public function usageReportDurationShouldBeOne() {
+    $this->assertEquals('1', $this->_report->getDuration());
+  }
+}
diff --git a/tests/scenarios/PnbDilicom/PnbDilicomUsergroupTest.php b/tests/scenarios/PnbDilicom/PnbDilicomUsergroupTest.php
index ea48c4bce5eebca6a31c7c0d69d89f2bb53f7247..9598058c23ecab542502ee663843785b55fb0eda 100644
--- a/tests/scenarios/PnbDilicom/PnbDilicomUsergroupTest.php
+++ b/tests/scenarios/PnbDilicom/PnbDilicomUsergroupTest.php
@@ -131,6 +131,6 @@ class PnbDilicomUsergroupAdminMenuWithRightTest extends PnbDilicomUsergroupAdmin
 
   /** @test */
   public function pageShouldContainsDilicomLink() {
-    $this->assertXPathContentContains('//a[contains(@href, "album/dilicom")]', 'PNB Dilicom');
+    $this->assertXPathContentContains('//a[contains(@href, "/pnb")]', 'PNB Dilicom');
   }
-}
\ No newline at end of file
+}
diff --git a/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php b/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php
index 1d8be0b80dd4ef264c31af681c79a9a05bb6d81e..b5f137ec653d9bb9b46c6b9d14c783ce63ff35b2 100644
--- a/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php
+++ b/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php
@@ -407,8 +407,8 @@ class UsergroupAgendaAdminAllSearchTest extends UsergroupAgendaAdminModoPortailL
   public function datesFilterShouldDefaultToFromNowUntil3MonthsLater() {
     $this->dispatch('/admin/usergroup-agenda/all');
     $params = Class_RendezVous::getFirstAttributeForLastCallOn('findAllBy');
-    $this->assertContains('date >= "2019-03-27"', $params['where']);
-    $this->assertContains('date <= "2019-06-27"', $params['where']);
+    $this->assertContains('left(date, 10) >= "2019-03-27"', $params['where']);
+    $this->assertContains('left(date, 10) <= "2019-06-27"', $params['where']);
   }
 
 
@@ -424,7 +424,7 @@ class UsergroupAgendaAdminAllSearchTest extends UsergroupAgendaAdminModoPortailL
   public function validDateStartShouldBeApplied() {
     $this->dispatchWithQuery(['search_date_start' => '26/03/2019']);
     $params = Class_RendezVous::getFirstAttributeForLastCallOn('findAllBy');
-    $this->assertContains('date >= "2019-03-26"', $params['where']);
+    $this->assertContains('left(date, 10) >= "2019-03-26"', $params['where']);
   }
 
 
@@ -432,7 +432,7 @@ class UsergroupAgendaAdminAllSearchTest extends UsergroupAgendaAdminModoPortailL
   public function invalidDateEndShouldBeIgnored() {
     $this->dispatchWithQuery(['search_date_end' => 'anything']);
     $params = Class_RendezVous::getFirstAttributeForLastCallOn('findAllBy');
-    $this->assertNotContains('date <=', $params['where']);
+    $this->assertNotContains('left(date, 10) <=', $params['where']);
   }
 
 
@@ -440,7 +440,7 @@ class UsergroupAgendaAdminAllSearchTest extends UsergroupAgendaAdminModoPortailL
   public function validDateEndShouldBeApplied() {
     $this->dispatchWithQuery(['search_date_end' => '26/03/2019']);
     $params = Class_RendezVous::getFirstAttributeForLastCallOn('findAllBy');
-    $this->assertContains('date <= "2019-03-26"', $params['where']);
+    $this->assertContains('left(date, 10) <= "2019-03-26"', $params['where']);
   }
 
 
@@ -1228,7 +1228,7 @@ class UsergroupAgendaAdminSearchCustomFieldDateRangeTest
 
   /** @test */
   public function lastQueryWhereShouldContainsLimitationOnDates() {
-    $this->assertEquals('str_to_date(value, "%d/%m/%Y") is not null and str_to_date(value, "%d/%m/%Y") >= "2019-06-01" and str_to_date(value, "%d/%m/%Y") <= "2019-07-31"',
+    $this->assertEquals('str_to_date(value, "%d/%m/%Y") is not null and left(str_to_date(value, "%d/%m/%Y"), 10) >= "2019-06-01" and left(str_to_date(value, "%d/%m/%Y"), 10) <= "2019-07-31"',
                         $this->lastCustomFieldValueClause('where'));
   }
 }