diff --git a/FEATURES/109790 b/FEATURES/109790
new file mode 100644
index 0000000000000000000000000000000000000000..3f967268726de15958d791eb825604432a961aa1
--- /dev/null
+++ b/FEATURES/109790
@@ -0,0 +1,10 @@
+        '109790' =>
+            ['Label' => $this->_('Retrait des documents sur rendez-vous (Drive)'),
+             'Desc' => $this->_('Les abonnés peuvent prendre rendez-vous pour retirer leurs documents à des horaires pré-définies'),
+             'Image' => '',
+             'Video' => '',
+             'Category' => $this->_('Circulation'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Cat%C3%A9gorie:Drive',
+             'Test' => '',
+             'Date' => '2020-05-04'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/109790 b/VERSIONS_WIP/109790
new file mode 100644
index 0000000000000000000000000000000000000000..4c0ae20eb2cca2e9bd2d30338e564d35e6d17f84
--- /dev/null
+++ b/VERSIONS_WIP/109790
@@ -0,0 +1 @@
+ - ticket #109790 : Retrait des documents sur rendez-vous (Drive)
\ No newline at end of file
diff --git a/application/modules/admin/controllers/DriveCheckoutController.php b/application/modules/admin/controllers/DriveCheckoutController.php
new file mode 100644
index 0000000000000000000000000000000000000000..161a15f2df4033b1ee105d30ebcdbff401cc1156
--- /dev/null
+++ b/application/modules/admin/controllers/DriveCheckoutController.php
@@ -0,0 +1,172 @@
+<?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 Admin_DriveCheckoutController extends ZendAfi_Controller_Action {
+  use Trait_TimeSource;
+
+  public function indexAction() {
+    $this->view->titre = $this->view->_('Drive : rendez-vous');
+    $this->view->libraries = Class_Bib::findAllBy(['enable_drive' => true,
+                                                   'order' => 'libelle']);
+    $this->view->disabled_libraries = Class_Bib::findAllBy(['enable_drive' => false,
+                                                            'order' => 'libelle']);
+  }
+
+
+  public function listAction() {
+    $this->view->library = Class_Bib::find($this->_getParam('id_bib'));
+    $this->view->date = $this->_getParam('date', $this->getCurrentDate());
+    $this->view->titre = $this->view->_('Drive : rendez-vous : %s, %s',
+                                        $this->view->library->getLibelle(),
+                                        strftime('%d %B %Y', strtotime($this->view->date)));
+    $this->view->checkouts = Class_DriveCheckout::findAllBy(['role' => 'library',
+                                                             'model' => $this->view->library,
+                                                             'left(start_at,10)' => $this->view->date,
+                                                             'order' => 'start_at']);
+    $this->view->table_description = new Class_TableDescription_DriveCheckout_ListWithActions('checkouts');
+  }
+
+
+  public function deleteAction() {
+    $checkout = Class_DriveCheckout::find($this->_getParam('id'));
+    $checkout->delete();
+
+    $this->_helper->notify($this->_('Rendez-vous pour %s supprimé',
+                                    $checkout->getNomComplet()));
+    $this->_redirect(sprintf('/admin/drive-checkout/list/id_bib/%s/date/%s',
+                             $checkout->getLibraryId(),
+                             substr($checkout->getStartAt(), 0, 10)));
+  }
+
+
+  public function icalAction() {
+    if (!$checkout = Class_DriveCheckout::find($this->_getParam('id'))) {
+      $this->getHelper('ViewRenderer')->setNoRender();
+      $this->_helper->notify($this->_('Impossible d\'importer un retrait inconnu.'));
+      $this->_redirectToIndex();
+      return;
+    }
+
+    $content = (new Class_ICal_DriveCheckout($checkout))
+      ->renderCalendar(Class_Profil::getCurrentProfil());
+
+    $this->_helper->ical('calendar.ics', $content);
+  }
+
+
+  public function listHoldsAction() {
+    $this->view->checkout = Class_DriveCheckout::find($this->_getParam('id'));
+    $this->view->titre = sprintf('%s, %s, %s, %s',
+                                 $this->view->checkout->getNomComplet(),
+                                 $this->view->checkout->getIdAbon(),
+                                 strftime('%d %B %H:%M',
+                                          strtotime($this->view->checkout->getStartAt())),
+                                 $this->view->checkout->getLibraryLabel());
+  }
+
+
+  public function listAllHoldsAction() {
+    $this->view->user = Class_Users::find($this->_getParam('id_user'));
+    foreach($this->view->user->getReservations() as $hold)
+      $hold->getExemplaireOPAC($this->view->user);//cache initialization
+
+    $this->view->titre = $this->_('Réservations pour %s, %s',
+                                  $this->view->user->getNomComplet(),
+                                  $this->view->user->getIdabon());
+  }
+
+
+  public function listCsvAction() {
+    $this->listAction();
+
+    $filename = implode(' ', [$this->view->date,
+                              $this->view->library->getLibelle(),
+                              $this->view->_('rendez-vous')]) . '.csv';
+    $this
+      ->_helper
+      ->csv($filename,
+            $this->view->renderCsv(new Class_TableDescription_DriveCheckout_List('checkouts'),
+                                   $this->view->checkouts));
+  }
+
+
+  public function itemsCsvAction() {
+    $library = Class_Bib::find($this->_getParam('id_bib'));
+    $date = $this->_getParam('date', $this->getCurrentDate());
+
+    $checkouts = Class_DriveCheckout::findAllBy(['role' => 'library',
+                                                 'model' => $library,
+                                                 'left(start_at,10)' => $date,
+                                                 'order' => 'start_at']);
+
+    $holds = (new Storm_Collection($checkouts))
+      ->injectInto(new Storm_Collection(),
+                   function($holds, $checkout)
+                   {
+                     $holds->addAll($checkout->getLibraryHolds());
+                     return $holds;
+                   });
+
+
+    $filename = implode(' ', [$date,
+                              $library->getLibelle(),
+                              $this->view->_('documents')]) . '.csv';
+    $this
+      ->_helper
+      ->csv($filename,
+            $this->view->renderCsv(new Class_TableDescription_DriveCheckout_HoldsWithCheckouts('holds'),
+                                   $holds->getArrayCopy()));
+  }
+
+
+  public function planAction() {
+    $user = Class_Users::find($this->_getParam('id_user'));
+
+    $plan = new Class_DriveCheckout_Plan($this->_request->getParams(), $user);
+    $plan->doNotFilterLibraries();
+
+    if (!$plan->isValid()) {
+      $this->getHelper('ViewRenderer')->setNoRender();
+      $this->_helper->notify($plan->getLastMessage());
+      $this->_redirect($this->view->url($plan->fallbackUrl()));
+      return;
+    }
+
+    if ($this->_request->isPost()
+        && ($checkout = $plan->persist())) {
+      $this->getHelper('ViewRenderer')->setNoRender();
+      $this->_helper->notify($this->view->_('Retrait planifié pour %s, le %s, %s',
+                                            $checkout->getNomComplet(),
+                                            strftime('%d %B %H:%M', strtotime($checkout->getStartAt())),
+                                            $checkout->getLibraryLabel()));
+      $this->_redirect(sprintf('/admin/drive-checkout/list/id_bib/%s/date/%s',
+                               $checkout->getLibraryId(),
+                               substr($checkout->getStartAt(), 0, 10)));
+      return;
+    }
+
+    $this->view->titre = $this->view->_('Planifier un retrait pour %s',
+                                        $user->getNomComplet());
+    $this->view->plan = $plan;
+    $this->view->user = $user;
+  }
+}
diff --git a/application/modules/admin/controllers/OuverturesController.php b/application/modules/admin/controllers/OuverturesController.php
index 6bc5361db7e1e5048570502c36ef3b878ff3f643..cb3acc02480e74ffc156e5861de70aad4fd40b76 100644
--- a/application/modules/admin/controllers/OuverturesController.php
+++ b/application/modules/admin/controllers/OuverturesController.php
@@ -20,7 +20,7 @@
  */
 
 class Admin_OuverturesController extends ZendAfi_Controller_Action {
-  protected $_library, $_is_multimedia = false;
+  protected $_library, $_is_used_for = null;
 
   public function getPlugins() {
     return ['ZendAfi_Controller_Plugin_ResourceDefinition_Opening',
@@ -29,13 +29,15 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
 
 
   public function init() {
+    $this->_used_for = (int)$this->_getParam('used_for', null);
+
     if ((!$this->_library = $this->_getLibrary())
-        || ($this->_getParam('multimedia') && !Class_AdminVar::isMultimediaEnabled())) {
+        || (($this->_used_for ===  Class_Ouverture::USED_FOR_MULTIMEDIA)
+            && !Class_AdminVar::isMultimediaEnabled())) {
       $this->_redirect('/admin/bib');
       return;
     }
 
-    $this->_is_multimedia = $this->_isMultimedia();
     parent::init();
   }
 
@@ -45,11 +47,6 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
   }
 
 
-  protected function _isMultimedia() {
-    return null !== $this->_getParam('multimedia');
-  }
-
-
   public function acceptVisitor($visitor) {
     parent::acceptVisitor($visitor);
     $visitor
@@ -57,10 +54,10 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
                      {
                        return $this->_getLibrary();
                      })
-      ->visitIsMultimedia(function()
-                          {
-                            return $this->_isMultimedia();
-                          });
+      ->visitUsedFor(function()
+                     {
+                       return (int)$this->_getParam('used_for');
+                     });
     return $this;
   }
 
@@ -76,7 +73,7 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
     if ($this->_response->isRedirect())
       return;
 
-    $this->view->multimedia = $this->_is_multimedia;
+    $this->view->used_for = $this->_used_for;
     $this->view->model_name = 'library';
     $this->view->library = $this->_library;
 
diff --git a/application/modules/admin/views/scripts/drive-checkout/index.phtml b/application/modules/admin/views/scripts/drive-checkout/index.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..35b777151695baf1f6548bad96e8be4dff2e5f3c
--- /dev/null
+++ b/application/modules/admin/views/scripts/drive-checkout/index.phtml
@@ -0,0 +1,20 @@
+<?php
+$render_libraries = function($libraries)
+{
+  return $this->tagUlLi(
+    array_map(
+      function($library)
+      {
+        return $this->tagAnchor(['action' => 'list',
+                                 'id_bib' => $library->getId()],
+                                $library->getLibelle());
+      },
+      $libraries));
+};
+
+
+echo $render_libraries($this->libraries);
+echo $this->tag('h3', $this->_('Drive désactivé'));
+echo $render_libraries($this->disabled_libraries);
+
+?>
diff --git a/application/modules/admin/views/scripts/drive-checkout/list-all-holds.phtml b/application/modules/admin/views/scripts/drive-checkout/list-all-holds.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..536a343170793a1c14bfa933651bee9ca6bae597
--- /dev/null
+++ b/application/modules/admin/views/scripts/drive-checkout/list-all-holds.phtml
@@ -0,0 +1,8 @@
+<?php
+echo $this->renderTable((new Class_TableDescription('holds'))
+                        ->addColumn($this->_('Bibliothèque'), function($hold) { return $hold->getBibliotheque(); })
+                        ->addColumn($this->_('Code-barres'), function($hold) { return $hold->getCodeBarre(); })
+                        ->addColumn($this->_('Etat'), function($hold) { return $hold->getEtat(); })
+                        ->addColumn($this->_('Titre'), function($hold) { return $hold->getTitre(); }),
+
+                        $this->user->getReservations());
diff --git a/application/modules/admin/views/scripts/drive-checkout/list-holds.phtml b/application/modules/admin/views/scripts/drive-checkout/list-holds.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..661ce491d617982e1c29b3c5ce79263fa8789415
--- /dev/null
+++ b/application/modules/admin/views/scripts/drive-checkout/list-holds.phtml
@@ -0,0 +1,14 @@
+<?php
+echo $this->renderTable(new Class_TableDescription_DriveCheckout_Holds('holds'),
+                        $this->checkout->getLibraryHolds());
+
+echo $this->tagAnchor($this->url(['module' => 'admin',
+                                  'controller' => 'drive-checkout',
+                                  'action' => 'list-all-holds',
+                                  'id_user' => $this->checkout->getUser()->getId()],
+                                 null,
+                                 true),
+                      $this->_('Voir toutes les réservations de %s',
+                               $this->checkout->getNomComplet()),
+
+                      ['data-popup' => 'true']);
diff --git a/application/modules/admin/views/scripts/drive-checkout/list.phtml b/application/modules/admin/views/scripts/drive-checkout/list.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..89ac4e7de436abe227d087420b9cbb61c0ab965a
--- /dev/null
+++ b/application/modules/admin/views/scripts/drive-checkout/list.phtml
@@ -0,0 +1,27 @@
+<?php
+$skin = Class_Admin_Skin::current();
+echo $this->Button((new Class_Entity())
+                   ->setUrl($this->url(['action' => 'items-csv']))
+                   ->setText($this->_('Exporter les documents (.csv)'))
+                   ->setImage($this->tagImg($skin->getIconUrl('actions', 'test'),
+                                            ['style' => 'filter: invert();']))
+                   ->setAttribs(['style' => 'float:right']));
+
+
+echo $this->Button((new Class_Entity())
+                   ->setUrl($this->url(['action' => 'list-csv']))
+                   ->setText($this->_('Exporter les rendez-vous (.csv)'))
+                   ->setImage($this->tagImg($skin->getIconUrl('actions', 'test'),
+                                            ['style' => 'filter: invert();']))
+                   ->setAttribs(['style' => 'float:right']));
+
+
+echo $this->tag('label', $this->_('Date'), ['for' => 'date', 'style' => 'margin-right: 5px']);
+
+$date_url = $this->url(['date' => null]) . '/date/';
+echo $this->formDate('date',
+                     $this->date,
+                     ['onchange' => 'window.location=\'' . $date_url . '\' + this.value']);
+
+
+echo $this->renderTable($this->table_description, $this->checkouts);
diff --git a/application/modules/admin/views/scripts/drive-checkout/plan.phtml b/application/modules/admin/views/scripts/drive-checkout/plan.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..34d26a02c581375f1fd4869dd1ea63aba7f8674d
--- /dev/null
+++ b/application/modules/admin/views/scripts/drive-checkout/plan.phtml
@@ -0,0 +1,14 @@
+<?php
+
+echo $this->tagAnchor($this->url(['module' => 'admin',
+                                  'controller' => 'drive-checkout',
+                                  'action' => 'list-all-holds',
+                                  'id_user' => $this->user->getId()],
+                                 null,
+                                 true),
+                      $this->_('Voir toutes les réservations de %s',
+                               $this->user->getNomComplet()),
+
+                      ['data-popup' => 'true']);
+
+echo $this->driveCheckoutPlan($this->plan);
diff --git a/application/modules/admin/views/scripts/ouvertures/list.phtml b/application/modules/admin/views/scripts/ouvertures/list.phtml
index 7b65e0cc396e760943b7ff10600cfc23cbff655b..33001fad8bca608fdfd613fba73bb219dec184b2 100644
--- a/application/modules/admin/views/scripts/ouvertures/list.phtml
+++ b/application/modules/admin/views/scripts/ouvertures/list.phtml
@@ -1,10 +1,17 @@
 <?php
-echo $this->renderForm($this->form);
+if ($this->used_for === Class_Ouverture::USED_FOR_LIBRARY)
+  echo $this->renderForm($this->form);
+
+$button_text = $this->_('Ajouter une plage d\'ouverture');
+if ($this->used_for === Class_Ouverture::USED_FOR_MULTIMEDIA)
+  $button_text =  $this->_('Ajouter une plage horaire de réservation multimédia');
+
+if ($this->used_for === Class_Ouverture::USED_FOR_DRIVE)
+  $button_text =  $this->_('Ajouter une plage d\'ouverture du drive');
+
 
 echo $this->button_New(
-  (new Class_Entity())->setText($this->multimedia
-                                 ? $this->_('Ajouter une plage horaire de réservation multimedia')
-                                 : $this->_('Ajouter une plage d\'ouverture'))
+  (new Class_Entity())->setText( $button_text)
                       ->setUrl($this->url(['action' => 'add',
                                            'id' => null])));
 echo $this->button_Back(
@@ -13,4 +20,4 @@ echo $this->button_Back(
                                         'controller' => 'bib',
                                            'action' => 'index'], null, true)));
 
-echo $this->libraryOpeningsAdmin($this->library, $this->multimedia);
+echo $this->libraryOpeningsAdmin($this->library, $this->used_for);
diff --git a/application/modules/opac/controllers/DriveCheckoutController.php b/application/modules/opac/controllers/DriveCheckoutController.php
new file mode 100644
index 0000000000000000000000000000000000000000..334fe48d4f3b4e67a43d5b0eb4aacd4bfea8b41a
--- /dev/null
+++ b/application/modules/opac/controllers/DriveCheckoutController.php
@@ -0,0 +1,104 @@
+<?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 DriveCheckoutController extends ZendAfi_Controller_Action {
+  protected $_user = null;
+
+  public function init()  {
+    parent::init();
+    $this->_user = $this->view->user = Class_Users::getIdentity();
+  }
+
+
+  public function planAction() {
+    if (!$this->_isActive())
+      return;
+
+    $plan = new Class_DriveCheckout_Plan($this->_request->getParams(), $this->_user);
+
+    if (!$plan->isValid()) {
+      $this->getHelper('ViewRenderer')->setNoRender();
+      $this->_helper->notify($plan->getLastMessage());
+      $this->_redirect($this->view->url($plan->fallbackUrl()));
+      return;
+    }
+
+    if ($this->_request->isPost()
+        && ($checkout = $plan->persist())) {
+      $this->getHelper('ViewRenderer')->setNoRender();
+      $this->_helper->notify($this->_('Le retrait de vos documents de %s est planifié pour %s.',
+                                      $checkout->getLibraryLabel(),
+                                      $checkout->getDateTimeLabel()));
+      $this->_redirect($this->view->url($plan->resetUrl()));
+      return;
+    }
+
+    $this->view->plan = $plan;
+  }
+
+
+  public function deleteAction() {
+    if (!$this->_isActive())
+      return;
+
+    $this->getHelper('ViewRenderer')->setNoRender();
+    $message = $this->_('Impossible de supprimer un retrait inconnu.');
+    if ($checkout = Class_DriveCheckout::findFor($this->_getParam('id'), $this->_user)) {
+      $checkout->delete();
+      $message = $this->_('Le retrait de vos documents de %s planifié pour %s a été supprimé.',
+                          $checkout->getLibraryLabel(),
+                          $checkout->getDateTimeLabel());
+    }
+
+    $this->_helper->notify($message);
+    $this->_redirect('/opac/drive-checkout/plan');
+  }
+
+
+  public function icalAction() {
+    if (!$this->_isActive())
+      return;
+
+    if (!$checkout = Class_DriveCheckout::findFor($this->_getParam('id'), $this->_user)) {
+      $this->getHelper('ViewRenderer')->setNoRender();
+      $this->_helper->notify($this->_('Impossible d\'importer un retrait inconnu.'));
+      $this->_redirect('/opac/drive-checkout/plan');
+      return;
+    }
+
+    $content = (new Class_ICal_DriveCheckout($checkout))
+      ->renderCalendar(Class_Profil::getCurrentProfil());
+
+    $this->_helper->ical('calendar.ics', $content);
+  }
+
+
+  protected function _isActive() {
+    if (Class_AdminVar::isDriveCheckoutEnabled())
+      return true;
+
+    $this->getHelper('ViewRenderer')->setNoRender();
+    $this->_helper->notify($this->_('La planification du retrait des réservations est désactivée.'));
+    $this->_redirect($this->view->url([], null, true));
+    return false;
+  }
+}
diff --git a/application/modules/opac/views/scripts/drive-checkout/plan.phtml b/application/modules/opac/views/scripts/drive-checkout/plan.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..aafd6c5ca8d346f2ae4222896d110c997985e3f5
--- /dev/null
+++ b/application/modules/opac/views/scripts/drive-checkout/plan.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->driveCheckoutPlan($this->plan);
diff --git a/cosmogramme/sql/patch/patch_388.php b/cosmogramme/sql/patch/patch_388.php
new file mode 100644
index 0000000000000000000000000000000000000000..7550f20493be3e8e1610640d18f56c9170c3d488
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_388.php
@@ -0,0 +1,16 @@
+<?php
+$adapter = Zend_Db_Table::getDefaultAdapter();
+
+try {
+  $adapter->query('create table if not exists `drive_checkout` ('
+                  . 'id int(11) unsigned not null auto_increment,'
+                  . 'user_id int(11) not null,'
+                  . 'library_id int(11) not null,'
+                  . 'start_at datetime not null,'
+                  . 'primary key (id),'
+                  . 'key user_id (user_id),'
+                  . 'key library_id (library_id),'
+                  . 'key start_at (start_at)'
+                  . ') engine=MyISAM default charset=utf8');
+} catch (Exception $e) {
+}
\ No newline at end of file
diff --git a/cosmogramme/sql/patch/patch_389.php b/cosmogramme/sql/patch/patch_389.php
new file mode 100644
index 0000000000000000000000000000000000000000..0692ddc52122fa2ddca96abbbe42b8cdf09e9b4f
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_389.php
@@ -0,0 +1,23 @@
+<?php
+$adapter = Zend_Db_Table::getDefaultAdapter();
+
+try {
+  $adapter->query('alter table ouvertures change multimedia used_for int(11) default 0');
+}
+catch (Exception $e){}
+
+try {
+  $adapter->query('alter table ouvertures add index used_for (used_for)');
+}
+catch (Exception $e){}
+
+try {
+  $adapter->query('alter table bib_c_site add enable_drive tinyint(1) default 0');
+}
+catch (Exception $e){}
+
+try {
+  $adapter->query('alter table ouvertures add column max_per_period_matin int(11) default null,
+add column max_per_period_apres_midi int(11) default null;');
+}
+catch (Exception $e){}
\ No newline at end of file
diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php
index 14e33346b9bc2d2a3088ee3d808d62e3fbe063b3..c8d2f8c801157bf0abfa9cd19bcbd3f33a7b130a 100644
--- a/library/Class/AdminVar.php
+++ b/library/Class/AdminVar.php
@@ -137,6 +137,7 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
        'usergroup-agenda' => $this->_getRendezVousVars(),
        'templating' => $this->_getTemplatingVars(),
        'identity-providers' => $this->_getIdentityProvidersVars(),
+       'drive-checkout' => $this->_getDriveCheckoutVars()
        ];
   }
 
@@ -550,6 +551,17 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
   }
 
 
+  protected function _getDriveCheckoutVars() {
+    return
+      ['ENABLE_DRIVE_CHECKOUT' => Class_AdminVar_Meta::newOnOff($this->_('Activer la prise de rendez-vous pour récupérer des réservations (Drive)')),
+       'DRIVE_TEMPLATE_NEW_RDV_SUBJECT' => Class_AdminVar_Meta::newDefault($this->_('Sujet des courriels de confirmation de création de rendez-vous drive.'),
+                                                                           ['value'=> 'Confirmation de rendez-vous à {library.libelle}']),
+       'DRIVE_TEMPLATE_NEW_RDV_CONTENT' => Class_AdminVar_Meta::newEditor($this->_('Modèle utilisé pour les courriels de confirmation de création de rendez-vous drive.'),
+                                                                          ['value'=> '<p>Bonjour {user.nom_complet},</p> <p>nous vous confirmons votre rendez-vous <strong> &agrave; {library.libelle} {rendez_vous.date_time_label}</strong>.</p>']),
+      ];
+  }
+
+
   public function allVarsValues() {
     if (null !== static::$_all_vars_values)
       return static::$_all_vars_values;
@@ -1131,6 +1143,11 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
   public function isIdentityProvidersEnabled() {
     return Class_AdminVar::isModuleEnabled('ENABLE_IDENTITY_PROVIDERS');
   }
+
+
+  public function isDriveCheckoutEnabled() {
+    return Class_AdminVar::isModuleEnabled('ENABLE_DRIVE_CHECKOUT');
+  }
 }
 
 
diff --git a/library/Class/Bib.php b/library/Class/Bib.php
index 57a69a360f17fccbd841fdfd444510741b8230bd..f76e32fd965347a78e0bde0e592d7029c8b18911 100644
--- a/library/Class/Bib.php
+++ b/library/Class/Bib.php
@@ -273,7 +273,7 @@ class Class_Bib extends Storm_Model_Abstract {
 
                   'ouvertures_multimedia' => ['model' => 'Class_Ouverture',
                                               'role' => 'bib',
-                                              'scope' => ['multimedia' => 1],
+                                              'scope' => ['used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA],
                                               'order' => ['jour',
                                                           'validity_end desc',
                                                           'validity_start desc',
@@ -917,19 +917,62 @@ class Class_Bib extends Storm_Model_Abstract {
   }
 
 
-  public function getOuvertureOnDate($time) {
-    if($this->hasHoraire())
+  public function isDriveEnabled() {
+    return !$this->isAttributeEmpty('enable_drive') && 1 == (int)$this->getEnableDrive();
+  }
+
+
+  public function hasDriveOuvertures() {
+    return $this->isDriveEnabled() && Class_Ouverture::hasDriveFor($this);
+  }
+
+
+  /**
+   * @param $date DateTime
+   * @return Class_Ouverture or null
+   */
+  public function getDriveOuvertureOnDate($date) {
+    return $this->isDriveEnabled() && $date
+      ? $this->getOuvertureOnDate($date->getTimeStamp(), Class_Ouverture::USED_FOR_DRIVE)
+      : null;
+  }
+
+
+  public function hasOuverturesFor($used_for) {
+    return null !== Class_Ouverture::findFirstBy(['id_site' => $this->getId(),
+                                                  'used_for' => $used_for]);
+  }
+
+
+  public function getOuvertureOnDate($time, $used_for = Class_Ouverture::USED_FOR_LIBRARY) {
+    if ($this->hasHoraire())
       return null;
 
     if ($ouverture = Class_Ouverture::findFirstBy(['jour' => date('Y-m-d', $time),
-                                                   'id_site' => $this->getId()]))
+                                                   'id_site' => $this->getId(),
+                                                   'used_for' => $used_for]))
       return $ouverture;
 
     if ($this->isClosedOnHolidays() && Class_Date_Holiday::isHoliday($time))
       return null;
 
     $day_of_week = Class_Date::dayOfWeek($time);
-    $openings = new Storm_Model_Collection($this->getOuvertures());
+    $openings = new Storm_Model_Collection(
+                                           Class_Ouverture::findAllBy
+                                           (
+                                            [
+                                             'id_site' => $this->getId(),
+                                             'used_for'=> $used_for,
+                                             'order' => [
+                                                         'jour',
+                                                         'validity_end desc',
+                                                         'validity_start desc',
+                                                         'jour_semaine',
+                                                         'debut_matin'
+                                             ],
+                                            ]
+                                           )
+    );
 
     $blessed = $openings->select('hasValidityRange')
                         ->detect(
diff --git a/library/Class/Bib/DriveOpening.php b/library/Class/Bib/DriveOpening.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd4953e4ec77b04b793099cfc27b45c9ba61be81
--- /dev/null
+++ b/library/Class/Bib/DriveOpening.php
@@ -0,0 +1,85 @@
+<?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_Bib_DriveOpening {
+  protected $_morning, $_afternoon;
+
+  /**
+   * @param $start DateTime
+   * @param $end DateTime
+   * @param $library Class_Bib
+   *
+   * @return Storm_Collection of DateTime on which library has an available drive opening
+   */
+  public static function allBetweenFor($start, $end, $library) {
+    $dates = new Storm_Collection();
+    while ($start <= $end) {
+      (new static($library, $start))->appendIfOpenTo($dates);
+      $start->modify('+1 day');
+    }
+
+    return $dates;
+  }
+
+
+  /**
+   * @param $library Class_Bib
+   * @param $date DateTime
+   *
+   * @return Storm_Collection of DateTime
+   */
+  public static function timesFor($library, $date) {
+    return (new static($library, $date))->times();
+  }
+
+
+  /**
+   * @param $library Class_Bib
+   * @param $date DateTime
+   */
+  public function __construct($library, $date) {
+    $opening = $library->getDriveOuvertureOnDate($date);
+    $this->_morning = new Class_Bib_DriveOpening_MorningPeriod($library, $date, $opening);
+    $this->_afternoon = new Class_Bib_DriveOpening_AfternoonPeriod($library, $date, $opening);
+  }
+
+
+  public function appendIfOpenTo($collection) {
+    $period = (new Storm_Collection([$this->_morning, $this->_afternoon]))
+      ->detect(function($period)
+               {
+                 return $period->isAvailable();
+               });
+
+    if ($period)
+      $collection->append(new DateTime($period->format('c')));
+  }
+
+
+  public function times() {
+    $times = new Storm_Collection;
+    foreach([$this->_morning, $this->_afternoon] as $period)
+      $period->injectTimesInto($times);
+
+    return $times;
+  }
+}
diff --git a/library/Class/Bib/DriveOpening/AfternoonPeriod.php b/library/Class/Bib/DriveOpening/AfternoonPeriod.php
new file mode 100644
index 0000000000000000000000000000000000000000..40a10c90fff4a2df8059ecb88da30ea100f574d8
--- /dev/null
+++ b/library/Class/Bib/DriveOpening/AfternoonPeriod.php
@@ -0,0 +1,29 @@
+<?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_Bib_DriveOpening_AfternoonPeriod extends Class_Bib_DriveOpening_Period {
+  protected
+    $_closed_method = 'isClosedPm',
+    $_start_method = 'getDebutApresMidi',
+    $_end_method = 'getFinApresMidi',
+    $_quota_method = 'getMaxPerPeriodApresMidi';
+}
\ No newline at end of file
diff --git a/library/Class/Bib/DriveOpening/MorningPeriod.php b/library/Class/Bib/DriveOpening/MorningPeriod.php
new file mode 100644
index 0000000000000000000000000000000000000000..31d0b095e990994786311f93a7671d87406d2e60
--- /dev/null
+++ b/library/Class/Bib/DriveOpening/MorningPeriod.php
@@ -0,0 +1,29 @@
+<?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_Bib_DriveOpening_MorningPeriod extends Class_Bib_DriveOpening_Period {
+  protected
+    $_closed_method = 'isClosedAm',
+    $_start_method = 'getDebutMatin',
+    $_end_method = 'getFinMatin',
+    $_quota_method = 'getMaxPerPeriodMatin';
+}
diff --git a/library/Class/Bib/DriveOpening/Period.php b/library/Class/Bib/DriveOpening/Period.php
new file mode 100644
index 0000000000000000000000000000000000000000..1f8a6775fb6d76bfe919aba5a12d213a9e50b647
--- /dev/null
+++ b/library/Class/Bib/DriveOpening/Period.php
@@ -0,0 +1,129 @@
+<?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_Bib_DriveOpening_Period {
+  const CHECKOUT_TIME_SLOT = '30';
+
+
+  protected
+    $_closed_method, $_start_method, $_end_method, $_quota_method,
+    $_opening, $_date, $_library,
+    $_times;
+
+  public function __construct($library, $date, $opening) {
+    $this->_opening = $opening;
+    $this->_library = $library;
+    $this->_date = $date;
+  }
+
+
+  public function isAvailable() {
+    return $this->_isClosed()
+      ? false
+      : $this->_quotaByPeriod() > Class_DriveCheckout::countBetweenForLibrary($this->startDateTime(),
+                                                                              $this->endDateTime(),
+                                                                              $this->_library);
+  }
+
+
+  public function injectTimesInto($collection) {
+    $collection->addAll($this->_times()->select([$this, 'isTimeAvailable']));
+  }
+
+
+  public function isTimeAvailable($time) {
+    return ($quota = $this->quota())
+      ? $quota > Class_DriveCheckout::countAtTimeForLibrary($time, $this->_library)
+      : false;
+  }
+
+
+  public function format($format) {
+    return $this->_date->format($format);
+  }
+
+
+  public function isClosed() {
+    return call_user_func([$this->_opening, $this->_closed_method]);
+  }
+
+
+  public function start() {
+    return call_user_func([$this->_opening, $this->_start_method]);
+  }
+
+
+  public function startDateTime() {
+    return $this->_asDateTime($this->start());
+  }
+
+
+  public function end() {
+    return call_user_func([$this->_opening, $this->_end_method]);
+  }
+
+
+  public function endDateTime() {
+    return $this->_asDateTime($this->end());
+  }
+
+
+  public function quota() {
+    return call_user_func([$this->_opening, $this->_quota_method]);
+  }
+
+
+  protected function _isClosed() {
+    return !$this->_opening || $this->_opening->isClosed() || $this->isClosed();
+  }
+
+
+  protected function _asDateTime($time) {
+    return new DateTime($this->_date->format('Y-m-d') . ' ' . $time . ':00');
+  }
+
+
+  protected function _times() {
+    if ($this->_isClosed())
+      return new Storm_Collection();
+
+    if (null !== $this->_times)
+      return $this->_times;
+
+    $day = $this->format('Y-m-d');
+    $period = new DatePeriod(new DateTime($day . ' ' . $this->start()),
+                             new DateInterval('PT' . static::CHECKOUT_TIME_SLOT . 'M'),
+                             new DateTime($day . ' ' . $this->end()));
+
+    $times = new Storm_Collection();
+    foreach($period as $step)
+      $times->append($step);
+
+    return $this->_times = $times;
+  }
+
+
+  protected function _quotaByPeriod() {
+    return $this->_times()->isEmpty() || (!$quota = $this->quota())
+      ? 0
+      : $quota * $this->_times()->count();
+  }
+}
\ No newline at end of file
diff --git a/library/Class/CodifAnnexe.php b/library/Class/CodifAnnexe.php
index 61aed280e3ccc0323d54d14deeacfc6c1c103e07..8a4241e40f37e59598b489a8c50c66e4315896b9 100644
--- a/library/Class/CodifAnnexe.php
+++ b/library/Class/CodifAnnexe.php
@@ -99,6 +99,13 @@ class Class_CodifAnnexe extends Storm_Model_Abstract {
   }
 
 
+  public function getLibraryId() {
+    return $this->hasBib()
+      ? $this->getBib()->getId()
+      : null;
+  }
+
+
   public function isVisible() {
     return (1 != $this->getInvisible());
   }
diff --git a/library/Class/DriveCheckout.php b/library/Class/DriveCheckout.php
new file mode 100644
index 0000000000000000000000000000000000000000..927abc4414de305d7f3116ea8fcd02a3fff57eba
--- /dev/null
+++ b/library/Class/DriveCheckout.php
@@ -0,0 +1,217 @@
+<?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_DriveCheckoutLoader extends Storm_Model_Loader {
+  public function findFutureFor($library, $user) {
+    if (!$library || !$user)
+      return;
+
+    return Class_DriveCheckout::findFirstBy(['library_id' => $library->getId(),
+                                             'user_id' => $user->getId(),
+                                             'where' => 'date(start_at) >= curdate()',
+                                             'order' => 'start_at']);
+  }
+
+
+  public function findFor($id, $user) {
+    return $user
+      ? Class_DriveCheckout::findFirstBy(['id' => (int)$id,
+                                          'user_id' => $user->getId()])
+      : null;
+  }
+
+
+  public function countBetweenForLibrary($start, $end, $library) {
+    return $library && $start && $end
+      ? Class_DriveCheckout::countBy(['library_id' => $library->getId(),
+                                      'where' => sprintf('start_at >= "%s" and start_at < "%s"',
+                                                         $this->_dateAsSql($start),
+                                                         $this->_dateAsSql($end))])
+      : 0;
+  }
+
+
+  public function countAtTimeForLibrary($time, $library) {
+    return $library && $time
+      ? Class_DriveCheckout::countBy(['library_id' => $library->getId(),
+                                      'start_at' => $this->_dateAsSql($time)])
+      : 0;
+  }
+
+
+  protected function _dateAsSql($date) {
+    return $date->format('Y-m-d H:i:s');
+  }
+}
+
+
+
+
+class Class_DriveCheckout extends Storm_Model_Abstract {
+  use Trait_Translator;
+
+  protected
+    $_loader_class = 'Class_DriveCheckoutLoader',
+    $_table_name = 'drive_checkout',
+    $_belongs_to = ['user' => ['model' => 'Class_Users'],
+                    'library' => ['model' => 'Class_Bib']],
+
+    $_default_attribute_values = ['start_at' => null,
+                                  'user_id' => null,
+                                  'library_id' => null];
+
+
+  public function getLibraryLabel() {
+    return ($library = $this->getLibrary())
+      ? $library->getLibelle()
+      : '';
+  }
+
+
+  public function getDateTimeLabel() {
+    return strftime($this->_('le %A %d %B à %Hh%M'),
+                    strtotime($this->getStartAt()));
+  }
+
+
+  public function getDateTimeStartAt() {
+    return DateTime::createFromFormat('Y-m-d H:i:s', $this->getStartAt());
+  }
+
+
+  public function getIdAbon() {
+    return ($user = $this->getUser())
+      ? $user->getIdabon()
+      : '';
+  }
+
+
+  public function getNomComplet() {
+    return ($user = $this->getUser())
+      ? $user->getNomComplet()
+      : '';
+  }
+
+
+  public function getHolds() {
+    if (!$user = $this->getUser())
+      return [];
+
+
+    return Class_DriveCheckout_Holds::newFor($user)
+      ->selectWaitingToBePulledIn($this->getLibrary())
+      ->collect(function($hold)
+                {
+                  return new Class_DriveCheckout_Hold($this, $hold);
+                });
+  }
+
+
+  public function getLibraryHolds() {
+    if (!$user = $this->getUser())
+      return [];
+
+
+    return Class_DriveCheckout_Holds::newFor($user)
+      ->selectWithLibraryInfo()
+      ->selectLibrary($this->getLibrary())
+      ->collect(function($hold)
+                {
+                  return new Class_DriveCheckout_Hold($this, $hold);
+                });
+  }
+
+
+  public function getMailRecipient() {
+    return ($user = $this->getUser())
+      ? $user->getMail()
+      : '';
+  }
+
+
+  public function notify() {
+    return (new Class_DriveCheckout_Notification($this))->send();
+  }
+}
+
+
+
+
+class Class_DriveCheckout_Hold {
+  protected
+    $_checkout,
+    $_hold,
+    $_item;
+
+  public function __construct($checkout, $hold) {
+    $this->_checkout = $checkout;
+    $this->_hold = $hold;
+    $this->_item = $hold->getExemplaireOPAC($checkout->getUser());
+  }
+
+
+  public function getStartAt() {
+    return $this->_checkout->getStartAt();
+  }
+
+
+  public function getIdAbon() {
+    return $this->_checkout->getIdAbon();
+  }
+
+
+  public function getNomComplet() {
+    return $this->_checkout->getNomComplet();
+  }
+
+
+  public function getCote() {
+    return $this->_item
+      ? $this->_item->getCote()
+      : $this->_hold->getCote();
+  }
+
+
+  public function getStatus() {
+    return $this->_hold->getEtat();
+  }
+
+
+  public function getCodeBarres() {
+    return $this->_item
+      ? $this->_item->getCodeBarres()
+      : $this->_hold->getCodeBarre();
+  }
+
+
+  public function getTitle() {
+    return $this->_item
+      ? $this->_item->getTitrePrincipal()
+      : $this->_hold->getTitre();
+  }
+
+
+  public function getRecord() {
+    return $this->_item
+      ? $this->_item->getNotice()
+      : null;
+  }
+}
\ No newline at end of file
diff --git a/library/Class/DriveCheckout/Holds.php b/library/Class/DriveCheckout/Holds.php
new file mode 100644
index 0000000000000000000000000000000000000000..3566117b996e2c0fa86546875c04cec7fb02db11
--- /dev/null
+++ b/library/Class/DriveCheckout/Holds.php
@@ -0,0 +1,98 @@
+<?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_DriveCheckout_Holds extends Storm_Collection {
+  public static function newFor($user) {
+    return new static($user ? $user->getReservations() : []);
+  }
+
+
+  public function selectWaitingToBePulledIn($library) {
+    return $this
+      ->selectWaitingToBePulledInAnyLibrary()
+      ->selectLibrary($library);
+  }
+
+
+  public function selectWaitingToBePulledInAnyLibrary() {
+    return $this
+      ->selectWaitingToBePulled()
+      ->selectWithLibraryInfo();
+  }
+
+
+  public function selectWaitingToBePulled() {
+    return $this->select(function($each)
+                         {
+                           return $each->isWaitingToBePulled();
+                         });
+  }
+
+
+  public function countWaitingToBePulled() {
+    return $this->selectWaitingToBePulled()
+                ->count();
+  }
+
+
+  public function selectLibrary($library) {
+    return $library
+      ? $this->select(function($each) use($library)
+                      {
+                        return ($each->getLocationId() == $library->getId())
+                          || ($each->getBibliotheque() == $library->getLibelle());
+                      })
+      : $this->newInstance([]);
+  }
+
+
+  public function selectWithLibraryInfo() {
+    return $this->select(function($each)
+                         {
+                           return $each->getLocationId() || $each->getBibliotheque();
+                         });
+  }
+
+
+  public function maxAvailabilityEndDateIn($library) {
+    return $this
+      ->selectWaitingToBePulledIn($library)
+      ->collect(function($each)
+                {
+                  return $each->getAvailabilityEndDate();
+                })
+      ->injectInto(null,
+                   function($current, $date)
+                   {
+                     if (!$date)
+                       return $current;
+
+                     if (false === ($datetime = DateTime::createFromFormat('!Y-m-d', $date)))
+                       return $current;
+
+                     if (null == $current)
+                       return $datetime;
+
+                     return $current < $datetime ? $current : $datetime;
+                   });
+  }
+}
diff --git a/library/Class/DriveCheckout/Notification.php b/library/Class/DriveCheckout/Notification.php
new file mode 100644
index 0000000000000000000000000000000000000000..dfa2a0eadda60c17f3a39dca31305246b4876a33
--- /dev/null
+++ b/library/Class/DriveCheckout/Notification.php
@@ -0,0 +1,83 @@
+<?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_DriveCheckout_Notification {
+  protected $_checkout;
+
+  public function __construct($checkout) {
+    $this->_checkout = $checkout;
+  }
+
+
+  public function getMailRecipient() {
+    return $this->_checkout
+      ? $this->_checkout->getMailRecipient()
+      : '';
+  }
+
+
+  public function getLibrary() {
+    return $this->_checkout
+      ? $this->_checkout->getLibrary()
+      : null;
+  }
+
+
+  public function getUser() {
+    return $this->_checkout
+      ? $this->_checkout->getUser()
+      : null;
+  }
+
+
+  public function send() {
+    if ((!$library = $this->getLibrary())
+        || (!$user = $this->getUser())
+        || (!$recipient = $this->getMailRecipient()))
+      return;
+
+    $mailer = new Class_MailHtml();
+    if (!$mailer->isMailValid($recipient))
+      return;
+
+    $data_source = ['rendez_vous' => $this->_checkout,
+                    'user' => $user,
+                    'library' => $library];
+
+    $body = (new Class_ModeleFusion())
+      ->setContenu(Class_AdminVar::getValueOrDefault('DRIVE_TEMPLATE_NEW_RDV_CONTENT'))
+      ->setDataSource($data_source)
+      ->getContenuFusionne();
+
+    $subject = (new Class_ModeleFusion())
+      ->setContenu(Class_AdminVar::getValueOrDefault('DRIVE_TEMPLATE_NEW_RDV_SUBJECT'))
+      ->setDataSource($data_source)
+      ->getContenuFusionne();
+
+    $mail = $mailer->prepare($recipient, $subject, $body, true);
+    $attachment = $mail
+      ->createAttachment((new Class_ICal_DriveCheckout($this->_checkout))->renderCalendar(Class_Profil::getCurrentProfil()),
+                         Class_ICal_DriveCheckout::MIME_TYPE);
+    $attachment->filename = 'calendar.ics';
+
+    $mailer->send($mail);
+  }
+}
diff --git a/library/Class/DriveCheckout/Plan.php b/library/Class/DriveCheckout/Plan.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4bedf95a6a5a71e677b30ee3ca35d4a94bf02df
--- /dev/null
+++ b/library/Class/DriveCheckout/Plan.php
@@ -0,0 +1,305 @@
+<?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_DriveCheckout_Plan {
+  use Trait_TimeSource, Trait_Translator, Trait_LastMessage;
+
+  const
+    LIBRARY_PARAM = 'id_bib',
+    DATE_PARAM = 'checkout_date',
+    TIME_PARAM = 'checkout_time';
+
+  protected
+    $_params = [],
+    $_libraries,
+    $_openings,
+    $_times,
+    $_holds,
+    $_user,
+    $_library,
+    $_date,
+    $_time,
+    $_should_filter_libraries = true;
+
+
+  public function __construct($params, $user) {
+    $this->_params = $params;
+    $this->_user = $user;
+  }
+
+
+  public function doNotFilterLibraries() {
+    $this->_should_filter_libraries = false;
+    return $this;
+  }
+
+
+  public function getId() {
+    return $this->_user->getId();
+  }
+
+
+  public function hasLibraries() {
+    return !$this->_getLibraries()->isEmpty();
+  }
+
+
+  public function injectIntoLibraries($value, $closure) {
+    return $this->_getLibraries()->injectInto($value, $closure);
+  }
+
+
+  public function selectedLibrary() {
+    if ($this->_library)
+      return $this->_library;
+
+    return ($library = Class_Bib::find((int)$this->_getParam(static::LIBRARY_PARAM)))
+      && $this->_getLibraries()->includes($library)
+      ? $this->_library = $library
+      : null;
+  }
+
+
+  public function injectIntoOpenings($value, $closure) {
+    return $this->_getOpenings()->injectInto($value, $closure);
+  }
+
+
+  public function selectedDate() {
+    if (!$this->_hasParam(static::DATE_PARAM))
+      return;
+
+    if ($this->_date)
+      return $this->_date;
+
+    return (false !== $date = DateTime::createFromFormat('Y-m-d',
+                                                         $this->_getParam(static::DATE_PARAM)))
+      && $this->_getOpenings()->includes($date->modify('midnight'))
+      ? $this->_date = $date
+      : null;
+  }
+
+
+  public function injectIntoTimes($value, $closure) {
+    return $this->_getTimes()->injectInto($value, $closure);
+  }
+
+
+  public function selectedTime() {
+    if (!$this->_hasParam(static::TIME_PARAM)
+        || (!$date = $this->selectedDate()))
+      return;
+
+    if ($this->_time)
+      return $this->_time;
+
+    $time = DateTime::createFromFormat('Y-m-d H:i:s',
+                                       sprintf('%s %s:00',
+                                               $date->format('Y-m-d'),
+                                               $this->_getParam(static::TIME_PARAM)));
+
+    return (false !== $time) && $this->_getTimes()->includes($time)
+      ? $this->_time = $time
+      : null;
+  }
+
+
+  public function isValid() {
+    if (!$this->_user)
+      return $this->_error($this->_('Vous devez vous connecter pour planifier le retrait de vos documents'));
+
+    if ($this->_hasParam(static::LIBRARY_PARAM) && (!$library = $this->selectedLibrary()))
+      return $this->_error($this->_('Le site de retrait choisi est invalide'));
+
+    if ($this->_hasParam(static::DATE_PARAM) && !$this->selectedDate())
+      return $this->_error($this->_('La date choisie n\'est pas disponible'));
+
+    if ($this->_hasParam(static::TIME_PARAM) && !$this->selectedTime())
+      return $this->_error($this->_('L\'horaire choisi n\'est pas disponible'));
+
+    return true;
+  }
+
+
+  /**
+   * @return Class_DriveCheckout created for this plan or null if something has gone wrong
+   */
+  public function persist() {
+    if (!$this->_user
+        || (!$library = $this->selectedLibrary())
+        || (!$time = $this->selectedTime()))
+      return;
+
+    $params = ['start_at' => $time->format('Y-m-d H:i:s'),
+               'user' => $this->_user,
+               'library' => $library];
+
+    $checkout = Class_DriveCheckout::newInstance($params);
+    if ($checkout->save())
+      $checkout->notify();
+
+    return $checkout;
+  }
+
+
+  public function findFutureFor($library) {
+    return Class_DriveCheckout::findFutureFor($library, $this->_user);
+  }
+
+
+  public function holds() {
+    return Class_DriveCheckout_Holds::newFor($this->_user);
+  }
+
+
+  protected function _getParam($name) {
+    return isset($this->_params[$name]) ? $this->_params[$name] : null;
+  }
+
+
+  protected function _hasParam($name) {
+    return null !== $this->_getParam($name);
+  }
+
+
+  protected function _getHolds() {
+    if ($this->_holds)
+      return $this->_holds;
+
+    return $this->_holds = Class_DriveCheckout_Holds::newFor($this->_user)
+      ->select(function($hold)
+               {
+                 return $hold->isWaitingToBePulled()
+                   && ($hold->getLocationId() || $hold->getBibliotheque());
+               })
+      ;
+  }
+
+
+  protected function _getLibraries() {
+    if ($this->_libraries)
+      return $this->_libraries;
+
+    if (!$this->_should_filter_libraries)
+      return $this->_libraries = new Storm_Model_Collection(Class_Bib::findAllBy(['enable_drive' => 1]));
+
+    $holds = $this->_getHolds();
+    if ($holds->isEmpty())
+      return $this->_libraries = new Storm_Model_Collection();
+
+    $ids = $holds->collect(function($hold) { return $hold->getLocationId(); })
+                 ->reject(function($id) { return null === $id; });
+
+    if (!$ids->isEmpty())
+      return $this->_libraries = $this->_getLibrariesByParams(['id_site' => $ids->getArrayCopy()]);
+
+    $labels = $holds->collect(function($hold)
+                              {
+                                return $hold->getBibliotheque();
+                              })
+                    ->reject(function($label)
+                             {
+                               return !$label;
+                             });
+
+    if (!$labels->isEmpty())
+      return $this->_libraries = $this->_getLibrariesByParams(['libelle' => $labels->getArrayCopy()]);
+
+    return $this->_libraries = new Storm_Model_Collection();
+  }
+
+
+  protected function _getLibrariesByParams($params) {
+    $params = array_merge(['enable_drive' => 1,
+                           'order' => 'libelle'],
+                          $params);
+
+    return new Storm_Model_Collection(Class_Bib::findAllBy($params));
+  }
+
+
+  protected function _getOpenings() {
+    if ($this->_openings)
+      return $this->_openings;
+
+    if ((!$library = $this->selectedLibrary())
+        || !$library->hasDriveOuvertures())
+      return $this->_openings = new Storm_Collection();
+
+    $start = $this->getTimeSource()->asDateTime()->modify('tomorrow');
+    $end = $this->_maxOpeningDateFor($library);
+
+    return $this->_openings = Class_Bib_DriveOpening::allBetweenFor($start, $end, $library);
+  }
+
+
+  protected function _maxOpeningDateFor($library) {
+    return null !== ($max = $this->_getHolds()->maxAvailabilityEndDateIn($library))
+      ? $max
+      : $this->getTimeSource()->asDateTime()->modify('+2 weeks midnight');
+  }
+
+
+  protected function _getTimes() {
+    if ($this->_times)
+      return $this->_times;
+
+    return $this->_times = ($library = $this->selectedLibrary()) && ($date = $this->selectedDate())
+      ? Class_Bib_DriveOpening::timesFor($library, $date)
+      : new Storm_Collection();
+  }
+
+
+  public function libraryUrlFor($library) {
+    return [static::LIBRARY_PARAM => $library->getId(),
+            static::DATE_PARAM => null,
+            static::TIME_PARAM => null];
+  }
+
+
+  public function dateUrlFor($date) {
+    if (!$library = $this->selectedLibrary())
+      return $this->resetUrl();
+
+    return [static::LIBRARY_PARAM => $library->getId(),
+            static::DATE_PARAM => $date->format('Y-m-d'),
+            static::TIME_PARAM => null];
+  }
+
+
+  public function resetUrl() {
+    return [static::LIBRARY_PARAM => null,
+            static::DATE_PARAM => null,
+            static::TIME_PARAM => null];
+  }
+
+
+  public function fallbackUrl() {
+    if ($date = $this->selectedDate())
+      return $this->dateUrlFor($date);
+
+    if ($library = $this->selectedLibrary())
+      return $this->libraryUrlFor($library);
+
+    return $this->resetUrl();
+  }
+}
diff --git a/library/Class/ICal/Abstract.php b/library/Class/ICal/Abstract.php
new file mode 100644
index 0000000000000000000000000000000000000000..ee1251b2ccf9d8bb522e3f4a40adbbee71cc6227
--- /dev/null
+++ b/library/Class/ICal/Abstract.php
@@ -0,0 +1,38 @@
+<?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
+ */
+
+
+abstract class Class_ICal_Abstract {
+  use Trait_Translator;
+
+  const
+    MIME_TYPE = 'text/calendar;charset=utf-8',
+    PRODUCT_ID = 'http://bokeh-library-portal.org';
+
+  protected $_model;
+
+  public function __construct($model) {
+    $this->_model = $model;
+  }
+
+
+  abstract public function renderCalendar($profil);
+}
diff --git a/library/Class/ICal/Autoloader.php b/library/Class/ICal/Autoloader.php
new file mode 100644
index 0000000000000000000000000000000000000000..48940296cc05245e204efd534e6adb7586ab9199
--- /dev/null
+++ b/library/Class/ICal/Autoloader.php
@@ -0,0 +1,50 @@
+<?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_ICal_Autoloader {
+  use Trait_Singleton;
+
+  protected $_autoloaded = false;
+
+  public function ensureAutoload() {
+    if ($this->_autoloaded)
+      return;
+
+    $loader = function ($class) {
+      if (false === strpos($class, 'Eluceo\\iCal'))
+        return;
+
+      if ('\\' == substr($class, 0, 1))
+        $class = substr($class, 1);
+
+      $class = str_replace('Eluceo\\iCal\\', '', $class);
+      $class = str_replace('\\', '/', $class);
+      $path = __DIR__ . '/../../iCal/src/' . $class . '.php';
+
+      if (file_exists($path))
+        require_once $path;
+    };
+
+    spl_autoload_register($loader);
+    $this->_autoloaded = true;
+  }
+}
diff --git a/library/Class/ICal/DriveCheckout.php b/library/Class/ICal/DriveCheckout.php
new file mode 100644
index 0000000000000000000000000000000000000000..c859a861b1ea568c2de9d612cd4b1a324efc44be
--- /dev/null
+++ b/library/Class/ICal/DriveCheckout.php
@@ -0,0 +1,59 @@
+<?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_ICal_DriveCheckout extends Class_ICal_Abstract {
+  const PREFIX_ID = '//drive-checkout:';
+
+  public function renderCalendar($profil) {
+    if (!$this->_model || !$profil)
+      return '';
+
+    Class_ICal_Autoloader::getInstance()->ensureAutoload();
+
+    $event = $this->_newEvent();
+    $event->setUseTimezone(true);
+    $event->setDtStart($this->_model->getDateTimeStartAt());
+    $event->setSummary($this->_('Retrait des réservations à %s', $this->_model->getLibraryLabel()));
+    $event->setLocation($this->_model->getLibraryLabel());
+    if (($library = $this->_model->getLibrary())
+        && ($latitude = $library->getLatitude())
+        && ($longitude = $library->getLongitude()))
+      $event->setGeoLocation(new \Eluceo\iCal\Property\Event\Geo($latitude, $longitude));
+
+    $event->setUrl(Class_Url::absolute(['controller' => 'drive-checkout',
+                                         'action' => 'plan',
+                                         'id_profil' => $profil->getId()],
+                                        null, true));
+
+    $calendar = new \Eluceo\iCal\Component\Calendar(static::PRODUCT_ID);
+    $calendar->addComponent($event);
+
+    return $calendar->render();
+  }
+
+
+  protected function _newEvent() {
+    return new \Eluceo\iCal\Component\Event(Class_Url::absolute([], null, true)
+                                            . static::PREFIX_ID
+                                            . $this->_model->getId());
+  }
+}
diff --git a/library/Class/Mail.php b/library/Class/Mail.php
index eea9b6839c236c41f21c096349ace578a23e38c7..9c0315f465b4ab813b272326707f28c92c636258 100644
--- a/library/Class/Mail.php
+++ b/library/Class/Mail.php
@@ -40,6 +40,14 @@ class Class_Mail {
 
 
   public function mail($destinataire, $sujet, $body, $send_bcc = false) {
+    return $this->send($this->prepare($destinataire, $sujet, $body, $send_bcc));
+  }
+
+
+  /**
+   * @return ZendAfi_Mail
+   */
+  public function prepare($destinataire, $sujet, $body, $send_bcc=false) {
     $mail = new ZendAfi_Mail('utf8');
     $mail
       ->setSubject($sujet)
@@ -50,6 +58,16 @@ class Class_Mail {
 
     $this->_setBodyIn($body, $mail);
 
+    return $mail;
+  }
+
+
+  /**
+   * @param $mail ZendAfi_Mail from this prepare()
+   *
+   * @return bool true or error string
+   */
+  public function send($mail) {
     try {
       $mail->send();
       return true;
diff --git a/library/Class/Ouverture.php b/library/Class/Ouverture.php
index a464754d0f962c601cc08350682abc3806ab513d..cda49f4e6089013a32cdf08edcf0bbeb2ed06431 100644
--- a/library/Class/Ouverture.php
+++ b/library/Class/Ouverture.php
@@ -21,7 +21,9 @@
 
 class OuvertureLoader extends Storm_Model_Loader {
   use Trait_Translator;
-  const CLOSED_VALUE = '00:00';
+  const
+    CLOSED_VALUE = '00:00',
+    CLOSED_VALUE_SQL = '00:00:00';
 
   public function compare($a, $b) {
     if ($a->getJourSemaine() && $b->getJourSemaine() && $a->getJourSemaine() > $b->getJourSemaine())
@@ -82,6 +84,14 @@ class OuvertureLoader extends Storm_Model_Loader {
     return Class_Ouverture::newInstance(['horaires' => array_fill(0, 4, static::CLOSED_VALUE),
                                          'jour' => $day]);
   }
+
+
+  public function hasDriveFor($library) {
+    return $library
+      ? 0 < Class_Ouverture::countBy(['id_site' => $library->getId(),
+                                      'used_for' => Class_Ouverture::USED_FOR_DRIVE])
+      : false;
+  }
 }
 
 
@@ -97,7 +107,10 @@ class Class_Ouverture extends Storm_Model_Abstract {
     JEUDI = 4,
     VENDREDI = 5,
     SAMEDI = 6,
-    DIMANCHE = 7;
+    DIMANCHE = 7,
+    USED_FOR_LIBRARY = 0,
+    USED_FOR_MULTIMEDIA = 1,
+    USED_FOR_DRIVE = 2;
 
   protected $_table_name = 'ouvertures';
   protected $_loader_class = 'OuvertureLoader';
@@ -106,11 +119,13 @@ class Class_Ouverture extends Storm_Model_Abstract {
                                           'jour' => null,
                                           'debut_matin' => '10:00',
                                           'fin_matin' => '12:00',
+                                          'max_per_period_matin' => null,
                                           'debut_apres_midi' => '12:00',
                                           'fin_apres_midi' => '18:00',
+                                          'max_per_period_apres_midi' => null,
                                           'validity_start' => null,
                                           'validity_end' => null,
-                                          'multimedia' => 0];
+                                          'used_for' => 0];
 
   protected $_belongs_to = ['bib' => ['model' => 'Class_Bib',
                                       'referenced_in' => 'id_site']];
@@ -125,6 +140,26 @@ class Class_Ouverture extends Storm_Model_Abstract {
   }
 
 
+  public function getMaxPerPeriodMatin() {
+    if (!$this->isDrive())
+      return null;
+
+    return is_null($value = parent::_get('max_per_period_matin'))
+      ? 1
+      : (int)$value;
+  }
+
+
+  public function getMaxPerPeriodApresMidi() {
+    if (!$this->isDrive())
+      return null;
+
+    return is_null($value = parent::_get('max_per_period_apres_midi'))
+      ? 1
+      : (int)$value;
+  }
+
+
   public function beforeSave() {
     if ($this->getJour())
       $this->setJour(Class_Ouverture::getLoader()->humanDateToDate($this->getJour()));
@@ -140,6 +175,27 @@ class Class_Ouverture extends Storm_Model_Abstract {
   }
 
 
+
+  public function beMultimedia() {
+    return $this->setUsedFor(static::USED_FOR_MULTIMEDIA);
+  }
+
+
+  public function beDrive() {
+    return $this->setUsedFor(static::USED_FOR_DRIVE);
+  }
+
+
+  public function isDrive() {
+    return (int)$this->getUsedFor() === static::USED_FOR_DRIVE;
+  }
+
+
+  public function isMultimedia() {
+    return (int)$this->getUsedFor() === static::USED_FOR_MULTIMEDIA;
+  }
+
+
   public function getLibelle() {
     return $this->getLabel();
   }
@@ -225,13 +281,11 @@ class Class_Ouverture extends Storm_Model_Abstract {
   }
 
 
-
   public function validate() {
     $this->checkAttribute('validity_range',
                           Class_Date::isEndDateAfterStartDate($this->getValidityStart(),
                                                               $this->getValidityEnd()),
-                          "La date de début doit être plus récente que la date de fin");
-
+                          $this->_('La date de début doit être plus récente que la date de fin'));
   }
 
 
@@ -252,7 +306,6 @@ class Class_Ouverture extends Storm_Model_Abstract {
   }
 
 
-
   public function isValidDuring($from, $to) {
     if ($this->isValidOnDate($from) || $this->isValidOnDate($to))
       return true;
@@ -307,7 +360,6 @@ class Class_Ouverture extends Storm_Model_Abstract {
   }
 
 
-
   public function hasValidityRange() {
     return $this->hasValidityStart() || $this->hasValidityEnd();
   }
diff --git a/library/Class/Ouverture/Visitor.php b/library/Class/Ouverture/Visitor.php
index c0b08314d04e2f204f639413b436b3b402f352ac..4e6ff16555ec18774095d87056cc2f4dcd0a384a 100644
--- a/library/Class/Ouverture/Visitor.php
+++ b/library/Class/Ouverture/Visitor.php
@@ -33,7 +33,7 @@ class Class_Ouverture_Visitor {
     $_content,
     $_openings,
     $_should_keep_all = false,
-    $_multimedia = false;
+    $_used_for = null;
 
   public function __construct() {
     $this->_openings = [static::DEFAULT_KEY => [],
@@ -50,10 +50,7 @@ class Class_Ouverture_Visitor {
 
 
   public function visitOpening($opening) {
-    if ($this->_multimedia && !$opening->getMultimedia())
-      return $this;
-
-    if (!$this->_multimedia && $opening->getMultimedia())
+    if ((int)$opening->getUsedFor() !== (int)$this->_used_for)
       return $this;
 
     if (!$opening->hasValidityRange() && $opening->isRecurrent())
@@ -189,8 +186,8 @@ class Class_Ouverture_Visitor {
   }
 
 
-  public function setMultimedia($flag=true) {
-    $this->_multimedia = $flag;
+  public function setUsedFor($used_for) {
+    $this->_used_for = $used_for;
     return $this;
   }
 }
\ No newline at end of file
diff --git a/library/Class/SearchCriteria/DateRange.php b/library/Class/SearchCriteria/DateRange.php
index 8142c03541493c9db44f49d34a6b4786262b1e43..b829d5b0648b05a5dfd1492fed38332b668efdd9 100644
--- a/library/Class/SearchCriteria/DateRange.php
+++ b/library/Class/SearchCriteria/DateRange.php
@@ -109,7 +109,7 @@ class Class_SearchCriteria_DateRange extends Class_SearchCriteria_Abstract {
     if ('' === $value)
       return '';
 
-    if (!(new ZendAfi_Validate_DateFormat())->isValid($value, static::DATE_FORMAT)) {
+    if (!(new ZendAfi_Validate_DateFormat())->setFormat(static::DATE_FORMAT)->isValid($value)) {
       $this->_element->addError($this->_('Les dates doivent être au format JJ/MM/AAAA'));
       return;
     }
diff --git a/library/Class/TableDescription.php b/library/Class/TableDescription.php
index 9412d0466ca8565cd0c13790a9ed8e43b6633ab3..a523d8e647fb5ed4889249203dc69e6b53378055 100644
--- a/library/Class/TableDescription.php
+++ b/library/Class/TableDescription.php
@@ -123,7 +123,25 @@ class Class_TableDescription {
   }
 
 
+  public function prependColumn($label, $description) {
+    $this
+      ->_columns
+      ->prepend($this->newColumn($label, $description));
+
+    return $this;
+  }
+
+
   public function addColumn($label, $description) {
+    $this
+      ->_columns
+      ->add($this->newColumn($label, $description));
+
+    return $this;
+  }
+
+
+  public function newColumn($label, $description) {
     if ($description instanceof Closure)
       $description = ['callback' => $description];
 
@@ -136,19 +154,12 @@ class Class_TableDescription {
                                 'sortable' => true,
                                 'sort_attribute' => ''],
                                $description);
-    $this
-      ->_columns
-      ->add($this->newColumn($label, $description)
-            ->setOptions($description['options']));
-
-    return $this;
-  }
-
 
-  public function newColumn($label, $description) {
-    return $description['callback']
+    $column = $description['callback']
       ? new Class_TableDescription_ColumnForCallback($label, $description, $this)
       : new Class_TableDescription_ColumnForAttribute($label, $description, $this);
+
+    return $column->setOptions($description['options']);
   }
 
 
@@ -267,6 +278,15 @@ class Class_TableDescription_Columns {
   }
 
 
+  /** @return Class_TableDescription_ColumnAbstract subclass */
+  public function prepend($column) {
+    $new_array = $this->_columns->getArrayCopy();
+    array_unshift($new_array, $column);
+    $this->_columns->exchangeArray($new_array);
+    return $column;
+  }
+
+
   public function acceptVisitor($visitor) {
     $this->_columns->eachDo(function($column) use($visitor)
                             {
diff --git a/library/Class/TableDescription/DriveCheckout/Holds.php b/library/Class/TableDescription/DriveCheckout/Holds.php
new file mode 100644
index 0000000000000000000000000000000000000000..7fcfd881fd621ab9b108aa44f8a3dc3edabaec59
--- /dev/null
+++ b/library/Class/TableDescription/DriveCheckout/Holds.php
@@ -0,0 +1,51 @@
+<?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_TableDescription_DriveCheckout_Holds extends Class_TableDescription {
+  public function init() {
+    $this
+      ->addColumn($this->_('Cote'),
+                  function($hold) { return $hold->getCote(); })
+
+      ->addColumn($this->_('Code-barres'),
+                  function($hold) { return $hold->getCodeBarres(); })
+
+      ->addColumn($this->_('État'),
+                  function($hold){ return $hold->getStatus(); })
+
+      ->addColumn($this->_('Titre'),
+                  function($hold){ return $hold->getTitle(); })
+
+      ->addRowAction(['canvas_callback' => function($hold, $canvas)
+                    {
+                      if (!$record = $hold->getRecord())
+                        return '';
+                      $view = $canvas->getView();
+                      return $view->tagAnchor($view->urlNotice($record, [], null, true),
+                                              Class_Admin_Skin::current()
+                                              ->renderActionIconOn('view', $view),
+                                              ['target' => '_blank',
+                                               'title' => $this->_('Voir la notice %s',
+                                                                   $record->getTitrePrincipal())]);
+                    }]);
+  }
+}
diff --git a/library/Class/TableDescription/DriveCheckout/HoldsWithCheckouts.php b/library/Class/TableDescription/DriveCheckout/HoldsWithCheckouts.php
new file mode 100644
index 0000000000000000000000000000000000000000..62257a5fb78f223fb5f558db8552a5362260eb3e
--- /dev/null
+++ b/library/Class/TableDescription/DriveCheckout/HoldsWithCheckouts.php
@@ -0,0 +1,43 @@
+<?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_TableDescription_DriveCheckout_HoldsWithCheckouts extends Class_TableDescription {
+  public function init() {
+    $this
+      ->addColumn($this->_('Jour'),
+                  function($hold) { return strftime('%d %B',strtotime($hold->getStartAt())); })
+      ->addColumn($this->_('Heure'),
+                  function($hold) { return strftime('%H:%M',strtotime($hold->getStartAt())); })
+      ->addColumn($this->_('Carte'),
+                  function($hold) { return $hold->getIdabon(); })
+      ->addColumn($this->_('Abonné'),
+                  function($hold) { return $hold->getNomComplet(); })
+      ->addColumn($this->_('Cote'),
+                  function($hold) { return $hold->getCote(); })
+
+      ->addColumn($this->_('Code-barres'),
+                  function($hold) { return $hold->getCodeBarres(); })
+
+      ->addColumn($this->_('Titre'),
+                  function($hold){ return strip_tags($hold->getTitle()); });
+  }
+}
diff --git a/library/Class/TableDescription/DriveCheckout/List.php b/library/Class/TableDescription/DriveCheckout/List.php
new file mode 100644
index 0000000000000000000000000000000000000000..5caf56025d8585c724e02241bd0adcbbdfb48802
--- /dev/null
+++ b/library/Class/TableDescription/DriveCheckout/List.php
@@ -0,0 +1,35 @@
+<?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_TableDescription_DriveCheckout_List extends Class_TableDescription {
+  public function init() {
+    $this
+      ->addColumn($this->_('Heure'),
+                  function($checkout)
+                  {
+                    return strftime('%H:%M',
+                                    strtotime($checkout->getStartAt()));
+                  })
+      ->addColumn($this->_('Carte'), 'id_abon')
+      ->addColumn($this->_('Abonné'), 'nom_complet');
+  }
+}
\ No newline at end of file
diff --git a/library/Class/TableDescription/DriveCheckout/ListWithActions.php b/library/Class/TableDescription/DriveCheckout/ListWithActions.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1c2971cad301b23750a623f98d85a2a4a1e2226
--- /dev/null
+++ b/library/Class/TableDescription/DriveCheckout/ListWithActions.php
@@ -0,0 +1,81 @@
+<?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_TableDescription_DriveCheckout_ListWithActions extends Class_TableDescription_DriveCheckout_List {
+  public function init() {
+    parent::init();
+    $this
+      ->addRowAction(['canvas_callback' => [$this, 'listHolds']])
+      ->addRowAction(['canvas_callback' => [$this, 'viewUser']])
+      ->addRowAction(['canvas_callback' => [$this, 'deleteCheckout']]);
+  }
+
+
+  public function deleteCheckout($checkout, $canvas) {
+    $view = $canvas->getView();
+    return $view->tagAnchor(['action' => 'delete',
+                             'id' => $checkout->getId()],
+
+                            Class_Admin_Skin::current()->renderActionIconOn('delete',
+                                                                            $view),
+
+                            ['title' => $this->_('Supprimer le rendez-vous pour %s',
+                                                 $checkout->getNomComplet()),
+                             'onclick' => sprintf('return confirm(\'%s\')',
+                                                  $this->_('Etes-vous sûr de vouloir supprimer le rendez-vous pour %s ?',
+                                                           $checkout->getNomComplet()))]);
+  }
+
+
+  public function listHolds($checkout, $canvas) {
+    $view = $canvas->getView();
+    return $view->tagAnchor(['action' => 'list-holds',
+                             'id' => $checkout->getId()],
+
+                            Class_Admin_Skin::current()->renderActionIconOn('view',
+                                                                            $view),
+
+                            ['data-popup' => 'true',
+                             'title' => $this->_('Voir les réservations pour %s',
+                                                 $checkout->getNomComplet())]);
+  }
+
+
+  public function viewUser($checkout, $canvas) {
+    if (!$user = $checkout->getUser())
+      return '';
+
+    $view = $canvas->getView();
+    return $view->tagAnchor($view->url(['module' => 'admin',
+                                        'controller' => 'users',
+                                        'action' => 'edit',
+                                        'id' => $user->getId()],
+                                       null,
+                                       true),
+
+                            Class_Admin_Skin::current()->renderActionIconOn('user',
+                                                                            $view),
+
+                            ['title' => $this->_('Voir la fiche de %s',
+                                                 $checkout->getNomComplet())]);
+  }
+}
\ No newline at end of file
diff --git a/library/Class/TableDescription/Openings.php b/library/Class/TableDescription/Openings.php
index 9dd982a22ecf94e0221ae45a07af0f0563d68a09..1a9749fe3d39058d46a9c2d05da3502a0d8c9e5d 100644
--- a/library/Class/TableDescription/Openings.php
+++ b/library/Class/TableDescription/Openings.php
@@ -31,18 +31,8 @@ class Class_TableDescription_Openings extends Class_TableDescription {
       ->addColumn($this->_('Jour'),
                   function($model) { return $this->_humanDateRange($model); })
 
-      ->addColumn($this->_('Matinée'),
-                  function($model)
-                  {
-                    return $this->_timeSegment($model->getDebutMatin(), $model->getFinMatin());
-                  })
-
-      ->addColumn($this->_('Après-midi'),
-                  function($model)
-                  {
-                    return $this->_timeSegment($model->getDebutApresMidi(), $model->getFinApresMidi());
-                  })
-
+      ->_addMorningColumns()
+      ->_addAfternoonColumns()
       ->addRowAction(['url' => ['module' => 'admin',
                                 'controller' => 'ouvertures',
                                 'action' => 'edit',
@@ -69,6 +59,26 @@ class Class_TableDescription_Openings extends Class_TableDescription {
   }
 
 
+  protected function _addMorningColumns() {
+    return $this->addColumn($this->_('Matinée'),
+                            function($model)
+                            {
+                              return $this->_timeSegment($model->getDebutMatin(),
+                                                         $model->getFinMatin());
+                            });
+  }
+
+
+  protected function _addAfternoonColumns() {
+    return $this->addColumn($this->_('Après-midi'),
+                            function($model)
+                            {
+                              return $this->_timeSegment($model->getDebutApresMidi(),
+                                                         $model->getFinApresMidi());
+                            });
+  }
+
+
   public function setView($view) {
     $this->_view = $view;
     return $this;
diff --git a/library/Class/TableDescription/Openings/Drive.php b/library/Class/TableDescription/Openings/Drive.php
new file mode 100644
index 0000000000000000000000000000000000000000..2c4c786eff7dba4a27dbea0c7ee02b512fc0268c
--- /dev/null
+++ b/library/Class/TableDescription/Openings/Drive.php
@@ -0,0 +1,46 @@
+<?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_Openings_Drive extends Class_TableDescription_Openings {
+  protected function _addMorningColumns() {
+    return
+      parent::_addMorningColumns()
+      ->addColumn($this->_('Quota matin'),
+                  function($model)
+                  {
+                    return $this->_('%s pers.',
+                                    $model->getMaxPerPeriodMatin());
+                  });
+  }
+
+
+  protected function _addAfternoonColumns() {
+    return
+      parent::_addAfternoonColumns()
+      ->addColumn($this->_('Quota après-midi'),
+                  function($model)
+                  {
+                    return $this->_('%s pers.',
+                                    $model->getMaxPerPeriodApresMidi());
+                  });
+  }
+}
\ No newline at end of file
diff --git a/library/Class/User/Cards.php b/library/Class/User/Cards.php
index d0c304ba22490d6186340b752cf748b1e5117504..bc7207a3799146d9a5e4c3a8ade0da6d60e23779 100644
--- a/library/Class/User/Cards.php
+++ b/library/Class/User/Cards.php
@@ -208,12 +208,23 @@ class Class_User_Cards extends Storm_Model_Collection {
                                            'Vous avez %d documents en retard.',
                                            $late_loans_count));
 
-      if ($pullable_items_count = $this->countWaitingToBePulled())
+      $actions = Class_AdminVar::isModuleEnabled('ENABLE_DRIVE_CHECKOUT')
+        ? ['actions' =>
+           [
+            Class_Url::assemble(['module' => 'opac',
+                                 'controller' => 'drive-checkout',
+                                 'action' => 'plan']) => $this->_('Planifier le retrait de mes documents')]]
+        : [];
+
+
+      if ($pullable_items_count = $this->countWaitingToBePulled()) {
         $notifiable->notify($this->_plural($pullable_items_count,
                                            '',
                                            'Vous avez %d document en attente de retrait.',
                                            'Vous avez %d documents en attente de retrait.',
-                                           $pullable_items_count));
+                                           $pullable_items_count),
+                            $actions);
+      }
     } catch(Exception $e) {
       $notifiable->notify($this->_('Une erreur est survenue nous empêchant de lister vos prêts.'));
       $notifiable->notify($this->_('Erreur : "%s"', $e->getMessage()));
diff --git a/library/Class/Users.php b/library/Class/Users.php
index 4e9cacfda163fce6986b30b8a9d6999418b91652..4627a8434259cc33862e5d61b7b9353b94272278 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -452,11 +452,14 @@ class Class_Users extends Storm_Model_Abstract {
                   'bookmarked_searches' => ['model' => 'Class_User_BookmarkedSearch',
                                             'role' => 'user',
                                             'dependents' => 'delete'],
+
                   'user_identities' => ['model' => 'Class_User_Identity',
                                         'role' => 'user',
-                                        'dependents' => 'delete']
-
+                                        'dependents' => 'delete'],
 
+                  'drive_checkouts' => ['model' => 'Class_DriveCheckout',
+                                        'role' => 'user',
+                                        'dependents' => 'delete']
 
     ],
 
diff --git a/library/Class/WebService/SIGB/Exemplaire.php b/library/Class/WebService/SIGB/Exemplaire.php
index 6a71c43dcdbe12c552c4171875b3d266783985ca..bebd4ff9d1607af8f7951af97155c73bc740851e 100644
--- a/library/Class/WebService/SIGB/Exemplaire.php
+++ b/library/Class/WebService/SIGB/Exemplaire.php
@@ -112,13 +112,15 @@ class Class_WebService_SIGB_Exemplaire {
   /**
    * @return Class_Exemplaire
    */
-  public function getExemplaireOPAC() {
+  public function getExemplaireOPAC($user=null) {
     if($this->_exemplaire_opac)
       return $this->_exemplaire_opac;
 
     $operation = (new Class_Entity())->updateAttributes(['CodeBarre' => $this->code_barre,
                                                          'NoNotice' => $this->no_notice]);
-    return $this->_exemplaire_opac = Class_Exemplaire::findFirstBySIGBOperation(Class_Users::getIdentity(), $operation);
+
+    if (!$user) $user = Class_Users::getIdentity();
+    return $this->_exemplaire_opac = Class_Exemplaire::findFirstBySIGBOperation($user, $operation);
   }
 
 
@@ -251,6 +253,7 @@ class Class_WebService_SIGB_Exemplaire {
   public function getCodeBarre(){
     if (!$this->code_barre && ($ex_opac = $this->getExemplaireOPAC()))
       $this->code_barre = $ex_opac->getCodeBarres();
+
     return $this->code_barre;
   }
 
@@ -268,6 +271,9 @@ class Class_WebService_SIGB_Exemplaire {
 
 
   public function getCote() {
+    if (!$this->_cote && ($item = $this->getExemplaireOPAC()))
+      $this->_cote = $item->getCote();
+
     return $this->_cote;
   }
 
diff --git a/library/Class/WebService/SIGB/ExemplaireOperation.php b/library/Class/WebService/SIGB/ExemplaireOperation.php
index 9cfdb3e881783747cc52a6bfb30e58cf2186a611..867194cb73cf4a95a130699df859a2e0742bd385 100644
--- a/library/Class/WebService/SIGB/ExemplaireOperation.php
+++ b/library/Class/WebService/SIGB/ExemplaireOperation.php
@@ -112,6 +112,20 @@ abstract class Class_WebService_SIGB_ExemplaireOperation {
   }
 
 
+  public function setCote($cote) {
+    $this->_exemplaire->setCote($cote);
+    return $this;
+  }
+
+
+  /**
+   * @return string
+   */
+  public function getCote() {
+    return $this->_exemplaire->getCote();
+  }
+
+
   /**
    * @return Class_WebService_SIGB_Exemplaire
    */
@@ -190,8 +204,8 @@ abstract class Class_WebService_SIGB_ExemplaireOperation {
   /**
    * @return Class_Exemplaire
    */
-  public function getExemplaireOPAC() {
-    return $this->_exemplaire->getExemplaireOPAC();
+  public function getExemplaireOPAC($user = null) {
+    return $this->_exemplaire->getExemplaireOPAC($user);
   }
 
 
diff --git a/library/Class/WebService/SIGB/Koha/PatronInfoReader.php b/library/Class/WebService/SIGB/Koha/PatronInfoReader.php
index d7c2f84da8288d4f9b8ac62fc9a5d6714c6e6991..0f75c707c5a7f9105f388afe27980925b1b71e30 100644
--- a/library/Class/WebService/SIGB/Koha/PatronInfoReader.php
+++ b/library/Class/WebService/SIGB/Koha/PatronInfoReader.php
@@ -19,13 +19,15 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class Class_WebService_SIGB_Koha_PatronInfoReader extends Class_WebService_SIGB_AbstractILSDIPatronInfoReader {
+class Class_WebService_SIGB_Koha_PatronInfoReader
+  extends Class_WebService_SIGB_AbstractILSDIPatronInfoReader {
   use Class_WebService_SIGB_Koha_TraitFormat, Trait_Translator;
 
   protected
     $_current_exemplaire_operation,
     $_grouped_holds_itypes = [];
 
+
   public static function newInstance() {
     return new self();
   }
@@ -61,13 +63,20 @@ class Class_WebService_SIGB_Koha_PatronInfoReader extends Class_WebService_SIGB_
 
 
   public function endBarcode($code) {
-    $this->_current_operation->getExemplaire()->setCodeBarre($code);
+    $this->_current_operation->setCodeBarre($code);
+  }
+
+
+  public function endItemcallnumber($data) {
+    if ($data)
+      $this->_current_operation->setCote($data);
   }
 
 
   public function endItemNumber($id) {
     if ($this->_xml_parser->inParents('loan'))
       $this->_current_operation->setId($id);
+
     $this->_current_operation->getExemplaire()->setId($id);
   }
 
@@ -128,7 +137,8 @@ class Class_WebService_SIGB_Koha_PatronInfoReader extends Class_WebService_SIGB_
     $this->_current_operation->getExemplaire()->setBibliotheque($site->getLibelle());
 
     if ($this->_currentHold)
-      $this->_currentHold->setPickupLocationLabel($site->getLibelle());
+      $this->_currentHold->setPickupLocationLabel($site->getLibelle())
+                         ->setLocationId($site->getLibraryId());
   }
 
 
@@ -166,7 +176,9 @@ class Class_WebService_SIGB_Koha_PatronInfoReader extends Class_WebService_SIGB_
 
 
   public function endExpirationDate($data) {
-    $this->_currentHold->setExpirationDate($data);
+    $this->_currentHold
+      ->setAvailabilityEndDate($data)
+      ->setExpirationDate($data);
   }
 
 
@@ -180,5 +192,3 @@ class Class_WebService_SIGB_Koha_PatronInfoReader extends Class_WebService_SIGB_
     $this->getEmprunteur()->setNbEmprunts((int)$data);
   }
 }
-
-?>
\ No newline at end of file
diff --git a/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php b/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php
index 93f09e9e715ea69bc984a6c56b43e22032b1d7bc..8d7021846226cfd5df05a6be8d165016662142ff 100644
--- a/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php
+++ b/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php
@@ -83,6 +83,11 @@ class Class_WebService_SIGB_Nanook_PatronInfoReader extends Class_WebService_SIG
    * @param string $data
    */
   public function endBarcode($data) {
+    if ($this->_xml_parser->inParents('hold')) {
+      $this->_currentHold->setCodeBarre($data);
+      return;
+    }
+
     $this->_emprunteur->setCodeBarres($data);
   }
 
@@ -201,6 +206,24 @@ class Class_WebService_SIGB_Nanook_PatronInfoReader extends Class_WebService_SIG
   }
 
 
+  public function endAvailabilityEndDate($data) {
+    if ($data && $this->_xml_parser->inParents('hold'))
+      $this->_currentHold->setAvailabilityEndDate($data);
+  }
+
+
+  public function endLocationId($data) {
+    if ($data && $this->_xml_parser->inParents('hold'))
+      $this->_currentHold->setLocationId($data);
+  }
+
+
+  public function endCote($data) {
+    if ($data && $this->_xml_parser->inParents('hold'))
+      $this->_currentHold->setCote($data);
+  }
+
+
   public function startSuggest() {
     $this->_current_suggest = (new Class_WebService_SIGB_Nanook_Suggestion)
       ->setUser($this->_user);
diff --git a/library/Class/WebService/SIGB/Reservation.php b/library/Class/WebService/SIGB/Reservation.php
index 5a36cc4ce3c3cd0982a9ef3d0227bd6e3d60dacf..f3640c20c11f4bd95e767343479bf8d5d17e210a 100644
--- a/library/Class/WebService/SIGB/Reservation.php
+++ b/library/Class/WebService/SIGB/Reservation.php
@@ -20,10 +20,13 @@
  */
 
 class Class_WebService_SIGB_Reservation extends Class_WebService_SIGB_ExemplaireOperation {
-  protected $rang;
-  protected $etat;
-  protected $pickup_location_label;
-  protected $waiting_to_be_pulled = false;
+  protected
+    $rang,
+    $etat,
+    $pickup_location_label,
+    $waiting_to_be_pulled = false,
+    $_availability_end_date,
+    $_location_id;
 
 
   public function getRang() {
@@ -84,9 +87,30 @@ class Class_WebService_SIGB_Reservation extends Class_WebService_SIGB_Exemplaire
   }
 
 
+  public function setAvailabilityEndDate($value) {
+    $this->_availability_end_date = $value;
+    return $this;
+  }
+
+
+  public function getAvailabilityEndDate() {
+    return $this->_availability_end_date;
+  }
+
+
+  public function setLocationId($id) {
+    $this->_location_id = $id;
+    return $this;
+  }
+
+
+  public function getLocationId() {
+    return $this->_location_id;
+  }
+
+
   /** @codeCoverageIgnore */
   public function __toString(){
     return parent::__toString().", Rang:".$this->getRang();
   }
 }
-?>
diff --git a/library/ZendAfi/Acl/AdminControllerGroup.php b/library/ZendAfi/Acl/AdminControllerGroup.php
index ea1d5397bc0cf51ca14450f1de199b5ec3a8b6c7..857dadb41761bce6b0220226868d9b66d363779a 100644
--- a/library/ZendAfi/Acl/AdminControllerGroup.php
+++ b/library/ZendAfi/Acl/AdminControllerGroup.php
@@ -87,6 +87,7 @@ class ZendAfi_Acl_AdminControllerGroup {
                           'rendez-vous' => Class_AdminVar::isRendezVousEnabled(),
                           'federation-reviews' => Class_AdminVar::isFederationEnabled(),
                           'identity-providers' => Class_AdminVar::isIdentityProvidersEnabled(),
+                          'drive-checkout' => Class_AdminVar::isDriveCheckoutEnabled(),
                           ];
 
     $this->_activated = array_merge($this->_activated,
diff --git a/library/ZendAfi/Acl/AdminControllerRoles.php b/library/ZendAfi/Acl/AdminControllerRoles.php
index b3f6c534d6077826f19a2baa9e38cb89f049a0c3..01cb776ffe39acb6ca23175744229c0e5c90c934 100644
--- a/library/ZendAfi/Acl/AdminControllerRoles.php
+++ b/library/ZendAfi/Acl/AdminControllerRoles.php
@@ -102,6 +102,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl {
     $this->add(new Zend_Acl_Resource('rendez-vous'));
     $this->add(new Zend_Acl_Resource('journal'));
     $this->add(new Zend_Acl_Resource('identity-providers'));
+    $this->add(new Zend_Acl_Resource('drive-checkout'));
 
     $codifications = ['codification-browser',
                       'thesauri',
@@ -156,6 +157,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl {
     $this->allow('modo_bib','users/change-admin-skin');
     $this->allow('modo_bib','users/settings');
     $this->allow('modo_bib','file-manager');
+    $this->allow('modo_bib','drive-checkout');
 
     $this->allow('admin_bib','rss');
     $this->allow('admin_bib','catalogue');
diff --git a/library/ZendAfi/Controller/Action/Helper/Ical.php b/library/ZendAfi/Controller/Action/Helper/Ical.php
new file mode 100644
index 0000000000000000000000000000000000000000..23ca6ffc485541a533580c51f917183a11cf3688
--- /dev/null
+++ b/library/ZendAfi/Controller/Action/Helper/Ical.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 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_Controller_Action_Helper_Ical extends Zend_Controller_Action_Helper_Abstract {
+  public function direct($filename, $content) {
+    $this->_actionController->getHelper('ViewRenderer')->setNoRender();
+
+    $response = $this->getResponse();
+    $response->clearAllHeaders();
+
+    $response->setHeader('Content-Type', Class_ICal_Abstract::MIME_TYPE, true);
+    $response->setHeader('Content-Disposition', 'attachment;filename="' . $filename . '"', true);
+    $response->setBody($content);
+  }
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php b/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php
index a43dfe844d59c12420a32b9a18c6c72d5dfd934f..2718ba2cc30189401e376b0c4bd2b8075bda4c28 100644
--- a/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php
+++ b/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php
@@ -23,10 +23,9 @@ class ZendAfi_Controller_Action_Helper_RenderIcalArticles
   extends Zend_Controller_Action_Helper_Abstract {
 
   const PRODID = 'http://bokeh-library-portal.org';
-  static protected $_autoloaded = false;
 
   public function direct($profil, $articles=[]) {
-    static::ensureAutoload();
+    Class_ICal_Autoloader::getInstance()->ensureAutoload();
 
     $vCalendar = new \Eluceo\iCal\Component\Calendar(static::PRODID);
 
@@ -46,28 +45,4 @@ class ZendAfi_Controller_Action_Helper_RenderIcalArticles
 
     echo $vCalendar->render();
   }
-
-
-  public static function ensureAutoload() {
-    if (static::$_autoloaded)
-      return;
-
-    $loader = function ($class) {
-      if (false === strpos($class, 'Eluceo\\iCal'))
-        return;
-
-      if ('\\' == substr($class, 0, 1))
-        $class = substr($class, 1);
-
-      $class = str_replace('Eluceo\\iCal\\', '', $class);
-      $class = str_replace('\\', '/', $class);
-      $path = __DIR__ . '/../../../../iCal/src/' . $class . '.php';
-
-      if (file_exists($path))
-        require_once $path;
-    };
-
-    spl_autoload_register($loader);
-    static::$_autoloaded = true;
-  }
 }
diff --git a/library/ZendAfi/Controller/Plugin/AdminAuth.php b/library/ZendAfi/Controller/Plugin/AdminAuth.php
index dd63656e84ae20d4e2335d35d89c09c68d1576a8..abe917b3d60a0121041df5b8df570c7e642e594b 100644
--- a/library/ZendAfi/Controller/Plugin/AdminAuth.php
+++ b/library/ZendAfi/Controller/Plugin/AdminAuth.php
@@ -63,7 +63,7 @@ class ZendAfi_Controller_Plugin_AdminAuth extends Zend_Controller_Plugin_Abstrac
 
     if ((!$user = Class_Users::getIdentity())
         && $action !== "authenticate"
-        && in_array($controller, ["abonne", 'bookmarked-searches'])) {
+        && in_array($controller, ["abonne", 'bookmarked-searches', 'drive-checkout'])) {
       $request->setParam('redirect', $this->_getRedirect($request, $action));
       $controller = 'auth';
       $action = ($request->getParam('render') == 'popup') ? 'popup-login' : 'login';
diff --git a/library/ZendAfi/Controller/Plugin/Manager/Library.php b/library/ZendAfi/Controller/Plugin/Manager/Library.php
index 3837c4d03c1334adb5c15cb1286864bf2b4e7016..f3ce96675d52ba281b99606985f8b08313cd2fbe 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/Library.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/Library.php
@@ -109,12 +109,20 @@ class ZendAfi_Controller_Plugin_Manager_Library extends ZendAfi_Controller_Plugi
              'icon' => 'calendar',
              'label' => $this->_('Planification des ouvertures')],
 
-            ['url' => '/admin/ouvertures/index/id_site/%s/multimedia/1',
+            ['url' => '/admin/ouvertures/index/id_site/%s/used_for/'.Class_Ouverture::USED_FOR_MULTIMEDIA,
              'icon' => 'computers',
              'label' => $this->_('Planification des ouvertures multimédia'),
              'condition' => function($model)
               {
                 return Class_AdminVar::isMultimediaEnabled();
+              }],
+
+            ['url' => '/admin/ouvertures/index/id_site/%s/used_for/'.Class_Ouverture::USED_FOR_DRIVE,
+             'icon' => 'shopping',
+             'label' => $this->_('Planification des ouvertures du drive'),
+             'condition' => function($library)
+              {
+                return Class_AdminVar::isDriveCheckoutEnabled() && $library->isDriveEnabled();
               }]];
   }
 }
diff --git a/library/ZendAfi/Controller/Plugin/Manager/Opening.php b/library/ZendAfi/Controller/Plugin/Manager/Opening.php
index 60e7a96b1a2bec92236301740a0d7a9d2ff5bf89..d46e3d339b07592081bdb8f2ee46cd30915b2899 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/Opening.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/Opening.php
@@ -53,6 +53,16 @@ class ZendAfi_Controller_Plugin_Manager_Opening extends ZendAfi_Controller_Plugi
   }
 
 
+  protected function _getForm($model){
+    $form = parent::_getForm($model);
+    if ((int)$this->_getParam('used_for') !== Class_Ouverture::USED_FOR_DRIVE){
+      $form->removeElement('max_per_period_matin');
+      $form->removeElement('max_per_period_apres_midi');
+    }
+    return $form;
+  }
+
+
   public function duplicateAction() {
     if (!$this->_canAdd()) {
       $this->_helper->notify($this->_view->_('Vous n\'avez pas la permission "%s"',
@@ -88,8 +98,8 @@ class ZendAfi_Controller_Plugin_Manager_Opening extends ZendAfi_Controller_Plugi
 
 
   protected function _updateNewModel($model) {
-    if(null !== $this->_getParam('multimedia'))
-      $model->setMultimedia(1);
+    if ($module = $this->_getParam('used_for'))
+      $model->setUsedFor($module);
 
     return $this;
   }
diff --git a/library/ZendAfi/Controller/Plugin/Manager/User.php b/library/ZendAfi/Controller/Plugin/Manager/User.php
index c584377b6590fd04e6149e43ed13863cc18f7d4e..e03c89998ad16e05bf4e9664cd2bcaf3c1a27e47 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/User.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/User.php
@@ -65,6 +65,13 @@ class ZendAfi_Controller_Plugin_Manager_User extends ZendAfi_Controller_Plugin_M
               }
             ],
 
+            ['url' => Class_Url::assemble(['module' => 'admin',
+                                           'controller' => 'drive-checkout',
+                                           'action' => 'plan']) . '/id_user/%s',
+             'icon' => 'shopping',
+             'condition' => function() { return Class_AdminVar::isDriveCheckoutEnabled(); },
+             'label' => $this->_('Planifier un retrait de réservations')],
+
             ['url' => ['module' => 'opac',
                        'controller' => 'auth',
                        'action' => 'become',
diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Opening.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Opening.php
index e7e24dcfc710a16370e6ffe008e32dffa8681e7c..74a6501af686e013df6a794abf18aab46077b332 100644
--- a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Opening.php
+++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Opening.php
@@ -24,7 +24,7 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Opening extends ZendAfi_Contr
   public function getDefinitions() {
     return ['model' => ['class' => 'Class_Ouverture',
                         'name' => 'ouverture',
-                        'scope' => ['id_site', 'multimedia'],
+                        'scope' => ['id_site', 'used_for'],
                         'order' => 'jour desc, jour_semaine, validity_start'],
 
             'sort' => ['Class_Ouverture', 'compare'],
@@ -47,14 +47,19 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Opening extends ZendAfi_Contr
 
     $lib_label = $this->_library->getLibelle();
 
-    return $this->_isMultimedia()
-      ? ['successful_add' => $this->_('Plage horaire de réservation multimedia %s ajoutée', $lib_label),
-         'successful_save' => $this->_('Plage horaire de réservation multimedia %s sauvegardée', $lib_label),
-         'successful_delete' => $this->_('Plage horaire de réservation multimedia %s supprimée', $lib_label)]
+    if ($this->_getUsedFor() === Class_Ouverture::USED_FOR_MULTIMEDIA)
+      return ['successful_add' => $this->_('Plage horaire de réservation multimedia %s ajoutée', $lib_label),
+              'successful_save' => $this->_('Plage horaire de réservation multimedia %s sauvegardée', $lib_label),
+              'successful_delete' => $this->_('Plage horaire de réservation multimedia %s supprimée', $lib_label)];
 
-      : ['successful_add' => $this->_('Plage d\'ouverture %s ajoutée', $lib_label),
-         'successful_save' => $this->_('Plage d\'ouverture %s sauvegardée', $lib_label),
-         'successful_delete' => $this->_('Plage d\'ouverture %s supprimée', $lib_label)];
+    if ($this->_getUsedFor() === Class_Ouverture::USED_FOR_DRIVE)
+      return ['successful_add' => $this->_('Plage d\'ouverture du drive %s ajoutée', $lib_label),
+              'successful_save' => $this->_('Plage d\'ouverture du drive %s sauvegardée', $lib_label),
+              'successful_delete' => $this->_('Plage d\'ouverture du drive %s supprimée', $lib_label)];
+
+    return ['successful_add' => $this->_('Plage d\'ouverture %s ajoutée', $lib_label),
+            'successful_save' => $this->_('Plage d\'ouverture %s sauvegardée', $lib_label),
+            'successful_delete' => $this->_('Plage d\'ouverture %s supprimée', $lib_label)];
   }
 
 
@@ -64,14 +69,19 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Opening extends ZendAfi_Contr
 
     $lib_label = $this->_library->getLibelle();
 
-    return $this->_isMultimedia()
-      ? ['edit' => ['title' => $this->_('%s : modifier une plage horaire de réservation multimedia', $lib_label)],
-         'add' => ['title' => $this->_('%s : ajouter une plage horaire de réservation multimedia', $lib_label)],
-         'index' => ['title' => $this->_('%s : plages horaire de réservation multimedia', $lib_label)]]
+    if ($this->_getUsedFor() === Class_Ouverture::USED_FOR_MULTIMEDIA)
+      return ['edit' => ['title' => $this->_('%s : modifier une plage horaire de réservation multimedia', $lib_label)],
+              'add' => ['title' => $this->_('%s : ajouter une plage horaire de réservation multimedia', $lib_label)],
+              'index' => ['title' => $this->_('%s : plages horaire de réservation multimedia', $lib_label)]];
+
+    if ($this->_getUsedFor() === Class_Ouverture::USED_FOR_DRIVE)
+      return ['edit' => ['title' => $this->_('%s : modifier une plage d\'ouverture du drive', $lib_label)],
+              'add' => ['title' => $this->_('%s : ajouter une plage d\'ouverture du drive', $lib_label)],
+              'index' => ['title' => $this->_('%s : plages d\'ouverture du drive', $lib_label)]];
 
-      : ['edit' => ['title' => $this->_('%s : modifier une plage d\'ouverture', $lib_label)],
-         'add' => ['title' => $this->_('%s : ajouter une plage d\'ouverture', $lib_label)],
-         'index' => ['title' => $this->_('%s : plages d\'ouverture', $lib_label)]];
+    return ['edit' => ['title' => $this->_('%s : modifier une plage d\'ouverture', $lib_label)],
+            'add' => ['title' => $this->_('%s : ajouter une plage d\'ouverture', $lib_label)],
+            'index' => ['title' => $this->_('%s : plages d\'ouverture', $lib_label)]];
   }
 
 
@@ -86,14 +96,14 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Opening extends ZendAfi_Contr
   }
 
 
-  public function visitIsMultimedia($callback) {
-    $this->_is_multimedia_callback = $callback;
+  public function visitUsedFor($callback) {
+    $this->_used_for_callback = $callback;
     return $this;
   }
 
 
-  protected function _isMultimedia() {
-    return $this->_is_multimedia = call_user_func($this->_is_multimedia_callback);
+  protected function _getUsedFor() {
+    return  call_user_func($this->_used_for_callback);
   }
 }
 ?>
\ No newline at end of file
diff --git a/library/ZendAfi/Form.php b/library/ZendAfi/Form.php
index 8d589b0d6e025ee21e4e470753b7618c879dc36e..277b0a00a57935adc39a390983b4805f0e3a7d78 100644
--- a/library/ZendAfi/Form.php
+++ b/library/ZendAfi/Form.php
@@ -138,7 +138,7 @@ class ZendAfi_Form extends Zend_Form {
   }
 
   /**
-   * @param  Storm_Model_Abstrict $model
+   * @param  Storm_Model_Abstract $model
    */
   public function addModelErrors($model) {
     $model->validate();
diff --git a/library/ZendAfi/Form/Admin/Library.php b/library/ZendAfi/Form/Admin/Library.php
index efe44da536715c49dd9c885cce85dbf69f674169..6af1bc8a88bef8a2335f9733f9acd0d8725ccebe 100644
--- a/library/ZendAfi/Form/Admin/Library.php
+++ b/library/ZendAfi/Form/Admin/Library.php
@@ -155,7 +155,12 @@ class ZendAfi_Form_Admin_Library extends ZendAfi_Form {
                                                                    "1" => "oui"],
                                                 ])
       ->addElement('checkbox', 'notify_on_new_resa', ['label' => $this->_('Notifier la bibliothèque au dépôt d\'une nouvelle réservation')])
-      ->addElement('checkbox', 'notify_on_new_user', ['label' => $this->_('Notifier la bibliothèque lors de l\'inscription d\'un nouvel utilisateur')])
+      ->addElement('checkbox', 'notify_on_new_user', ['label' => $this->_('Notifier la bibliothèque lors de l\'inscription d\'un nouvel utilisateur')]);
+
+    if (Class_AdminVar::isDriveCheckoutEnabled())
+      $this->addElement('checkbox', 'enable_drive', ['label' => $this->_('Activer le drive')]);
+
+    $this
       ->addDisplayGroup(['libelle',
                          'responsable',
                          'rewrite_url',
@@ -164,7 +169,6 @@ class ZendAfi_Form_Admin_Library extends ZendAfi_Form {
                         'library',
                         ['legend' => $this->_('Bibliothèque')])
 
-
       ->addDisplayGroup(['id_lieu'],
                         'location',
                         ['legend' => $this->_('Lieu')])
@@ -195,6 +199,7 @@ class ZendAfi_Form_Admin_Library extends ZendAfi_Form {
 
       ->addDisplayGroup(['gln',
                          'visibilite',
+                         'enable_drive',
                          'url_web',
                          'interdire_resa',
                          'notify_on_new_resa',
diff --git a/library/ZendAfi/Form/Admin/Ouverture.php b/library/ZendAfi/Form/Admin/Ouverture.php
index 88ccaad739dd67e267128cfa505d512c38476037..2dc2a8b0188e61207f7b7481ba51fe3026cf7adc 100644
--- a/library/ZendAfi/Form/Admin/Ouverture.php
+++ b/library/ZendAfi/Form/Admin/Ouverture.php
@@ -27,8 +27,8 @@ class ZendAfi_Form_Admin_Ouverture extends ZendAfi_Form {
     Class_ScriptLoader::getInstance()
       ->addJqueryReady('formSelectToggleVisibilityForElement("#jour_semaine", "#fieldset-plage_ouverture tr:nth-child(3)", ["0"]);'
                        . 'formSelectToggleVisibilityForElement("#jour_semaine", "#fieldset-plage_ouverture tr:nth-child(4)", ["1", "2", "3", "4", "5", "6", "7"]);'
-                       . 'checkBoxToggleVisibilityForElement("#closed_am", $("#fieldset-plage_ouverture select[name$=\'_matin\']").parents("tr"), false);'
-                       . 'checkBoxToggleVisibilityForElement("#closed_pm", $("#fieldset-plage_ouverture select[name$=\'_apres_midi\']").parents("tr"), false);');
+                       . 'checkBoxToggleVisibilityForElement("#closed_am", $("#fieldset-plage_ouverture_matin select[name$=\'_matin\']").parents("tr"), false);'
+                       . 'checkBoxToggleVisibilityForElement("#closed_pm", $("#fieldset-plage_ouverture_apres_midi select[name$=\'_apres_midi\']").parents("tr"), false);');
 
     $possible_hours = Class_Multimedia_Location::getPossibleHours(5);
 
@@ -56,33 +56,51 @@ class ZendAfi_Form_Admin_Ouverture extends ZendAfi_Form {
 
       ->addElement('select',
                    'debut_matin',
-                   ['label' => $this->_('Début matinée'),
+                   ['label' => $this->_('Début'),
                     'multiOptions' => $possible_hours])
 
       ->addElement('select',
                    'fin_matin',
-                   ['label' => $this->_('Fin matinée'),
+                   ['label' => $this->_('Fin'),
                     'multiOptions' => $possible_hours])
 
-      ->addElement('checkbox', 'closed_am', ['label' => $this->_('Fermé matinée')])
+      ->addElement('number',
+                   'max_per_period_matin',
+                   ['label' => $this->_('Nombre de personnes pour %s mn',
+                                        Class_Bib_DriveOpening_Period::CHECKOUT_TIME_SLOT),
+                   'value' =>1])
+
+      ->addElement('checkbox', 'closed_am', ['label' => $this->_('Fermé')])
 
       ->addElement('select',
                    'debut_apres_midi',
-                   ['label' => $this->_('Début après-midi'),
+                   ['label' => $this->_('Début'),
                     'multiOptions' => $possible_hours])
 
       ->addElement('select',
                    'fin_apres_midi',
-                   ['label' => $this->_('Fin après-midi'),
+                   ['label' => $this->_('Fin'),
                     'multiOptions' => $possible_hours])
 
-      ->addElement('checkbox', 'closed_pm', ['label' => $this->_('Fermé après-midi')])
+      ->addElement('number',
+                   'max_per_period_apres_midi',
+                   ['label' => $this->_('Nombre de personnes pour %s mn',
+                                        Class_Bib_DriveOpening_Period::CHECKOUT_TIME_SLOT),
+                   'value' => 1])
 
-      ;
+      ->addElement('checkbox', 'closed_pm', ['label' => $this->_('Fermé')]);
 
-    $this->addDisplayGroup($this->getElementsNames(),
+    $this->addDisplayGroup(['label', 'jour_semaine', 'jour', 'validity_range'],
                            'plage_ouverture',
-                           ['legend' => $this->_('Plage d\'ouverture')]);
+                           ['legend' => $this->_('Général')]);
+
+    $this->addDisplayGroup(['debut_matin', 'fin_matin','closed_am','max_per_period_matin'],
+                           'plage_ouverture_matin',
+                           ['legend' => $this->_('Matinée')]);
+
+    $this->addDisplayGroup(['debut_apres_midi', 'fin_apres_midi','closed_pm','max_per_period_apres_midi'],
+                           'plage_ouverture_apres_midi',
+                           ['legend' => $this->_('Après-Midi')]);
 
     $isValid = function($value) {
       $validator = (new Class_Entity())->updateAttributes(['Valid' => true,
diff --git a/library/Class/TableDescription/OpeningsLabelled.php b/library/ZendAfi/Form/Element/Date.php
similarity index 75%
rename from library/Class/TableDescription/OpeningsLabelled.php
rename to library/ZendAfi/Form/Element/Date.php
index 2cb18e9c55d5e126ddb0abdd92e1dffb600dca2f..c9500756809b5bf995e7e3f7d4e98cd0e23cbb93 100644
--- a/library/Class/TableDescription/OpeningsLabelled.php
+++ b/library/ZendAfi/Form/Element/Date.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Copyright (c) 2012-2019, Agence Française Informatique (AFI). All rights reserved.
+ * 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
@@ -19,10 +19,15 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
+class ZendAfi_Form_Element_Date extends Zend_Form_Element_Text {
+  /**
+   * HTML5 input type = Date
+   * @var string
+   */
+  public $helper = 'formDate';
+
 
-class Class_TableDescription_OpeningsLabelled extends Class_TableDescription_Openings {
   public function init() {
-    $this->addColumn($this->_('Libellé'), 'label');
-    parent::init();
+    $this->addValidator(new ZendAfi_Validate_DateFormat());
   }
-}
+}
\ No newline at end of file
diff --git a/library/ZendAfi/Validate/DateFormat.php b/library/ZendAfi/Validate/DateFormat.php
index d5ddd78099090838f2787a465ce89d8a435c4e2f..7f97a0efd98c6aaa4b62666a19bfb77f57d73603 100644
--- a/library/ZendAfi/Validate/DateFormat.php
+++ b/library/ZendAfi/Validate/DateFormat.php
@@ -21,12 +21,19 @@
 
 
 class ZendAfi_Validate_DateFormat extends Zend_Validate_Abstract {
+  protected $_format = 'Y-m-d';
 
-  public function isValid($string, $format = 'Y-m-d') {
+  public function setFormat($format) {
+    $this->_format = $format;
+    return $this;
+  }
+
+
+  public function isValid($string) {
     if (!$string)
       return false;
 
-    $date = DateTime::createFromFormat($format, $string);
-    return $date && ($date->format($format) === $string);
+    $date = DateTime::createFromFormat($this->_format, $string);
+    return $date && ($date->format($this->_format) === $string);
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Abonne/HoldsBoard.php b/library/ZendAfi/View/Helper/Abonne/HoldsBoard.php
index 62363f20e942c037ad46db748164eb2be2d893a9..49d6f2a7c9d6f99ac54a86f927fce2abd726e2d1 100644
--- a/library/ZendAfi/View/Helper/Abonne/HoldsBoard.php
+++ b/library/ZendAfi/View/Helper/Abonne/HoldsBoard.php
@@ -31,11 +31,17 @@ class ZendAfi_View_Helper_Abonne_HoldsBoard extends ZendAfi_View_Helper_BaseHelp
     $html = [$this->view->openBoiteContent($this->_('Réservations en cours')),
 
              $this->view->div(['class' => 'abonneTitre'],
-                              $user->getNomAff()),
+                              $user->getNomAff())];
 
-             $this->view->abonne_ReservationsTable($cards->getHolds(), $fiche),
+    if (Class_AdminVar::isModuleEnabled('ENABLE_DRIVE_CHECKOUT'))
+      $html []= $this->view->tagAnchor(['controller' => 'drive-checkout',
+                                        'action' => 'plan'],
+                                       $this->_('Planifier le retrait de mes documents'),
+                                       ['class' => 'checkout-plan-link']);
 
-             $this->view->closeBoiteContent()];
+    $html []= $this->view->abonne_ReservationsTable($cards->getHolds(), $fiche);
+
+    $html []= $this->view->closeBoiteContent();
 
     if (!empty($consultations_reservations)) {
       $html [] = $this->view->openBoiteContent($this->_('Réservations pour la consultation sur place'));
diff --git a/library/ZendAfi/View/Helper/Accueil/Base.php b/library/ZendAfi/View/Helper/Accueil/Base.php
index 517378ee6fd78d52766d00c6365a83649abc7a0b..b3a9d032f572d3927a94298e9c4fa92e1bb678f2 100644
--- a/library/ZendAfi/View/Helper/Accueil/Base.php
+++ b/library/ZendAfi/View/Helper/Accueil/Base.php
@@ -209,7 +209,7 @@ class ZendAfi_View_Helper_Accueil_Base extends ZendAfi_View_Helper_ModuleAbstrac
 
     $profil = Class_Profil::getCurrentProfil();
 
-    if(!array_key_exists('boite', $this->preferences) || !$this->preferences['boite'])
+    if(!array_key_exists('boite', $this->preferences) || !$this->preferences['boite'] || !is_string($this->preferences['boite']))
       $this->preferences['boite'] = $profil->isTelephone() ? 'boite_telephone' : $style_boite[$this->getDivision()];
 
     return $profil->getPathBoites() . $this->preferences['boite'] . '.html';
diff --git a/library/ZendAfi/View/Helper/Admin/ContentNav.php b/library/ZendAfi/View/Helper/Admin/ContentNav.php
index a94a7acdb05ed16df178101a22c4b2cf8151e699..55d1333992ae3c042f9c8e5bafa2222cc8047cae 100644
--- a/library/ZendAfi/View/Helper/Admin/ContentNav.php
+++ b/library/ZendAfi/View/Helper/Admin/ContentNav.php
@@ -69,7 +69,8 @@ class ZendAfi_View_Helper_Admin_ContentNav extends ZendAfi_View_Helper_BaseHelpe
                     ['newsletters',           $this->_("Lettres d'information"),   '/admin/newsletter'],
                     ['trainings',             $this->_('Activités'),               '/admin/activity'],
                     ['places',                $this->_('Lieux'),                   '/admin/lieu'],
-                    ['meeting',              $this->_('Rendez-vous'),             '/admin/usergroup-agenda'],
+                    ['meeting',               $this->_('Rendez-vous'),             '/admin/usergroup-agenda'],
+                    ['drive-checkout',        $this->_('Drive : rendez-vous'),     '/admin/drive-checkout'],
                     ['filebrowser',           $this->_('Explorateur de fichiers'), '/admin/file-manager'],
                    ]);
   }
diff --git a/library/ZendAfi/View/Helper/Admin/HelpLink.php b/library/ZendAfi/View/Helper/Admin/HelpLink.php
index 0076068b6ece804c6b0dcd94686029d0f68bc3e6..fb1dd64c22576dc0da1b1e71eef19f45b863099f 100644
--- a/library/ZendAfi/View/Helper/Admin/HelpLink.php
+++ b/library/ZendAfi/View/Helper/Admin/HelpLink.php
@@ -128,6 +128,8 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki {
      'genre'                  => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'],
      'emplacement'            => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'],
      'section'                => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'],
+     'drive-checkout'         => ['index' => 'Gérer_les_listes_de_rendez-vous_en_mode_drive',
+                                  'plan' => 'Prise_de_rendez-vous_par_les_professionnels']
     ];
 
 
diff --git a/library/ZendAfi/View/Helper/Admin/SubscribeUsers.php b/library/ZendAfi/View/Helper/Admin/SubscribeUsers.php
index 752cd3ee36bbb12e68c15c0388e81103299e9273..161c079f9efc12d0a6adf7c4553db3c8fe89fd62 100644
--- a/library/ZendAfi/View/Helper/Admin/SubscribeUsers.php
+++ b/library/ZendAfi/View/Helper/Admin/SubscribeUsers.php
@@ -108,7 +108,7 @@ class ZendAfi_View_Helper_Admin_SubscribeUsers extends ZendAfi_View_Helper_BaseH
                    $user->getPrenom(),
                    $user->getLogin(),
                    $user->getMail(),
-                   $this->_deleteLinkOf($user));
+                   $this->_editUserLink($user) . $this->_deleteLinkOf($user));
   }
 
 
@@ -121,6 +121,17 @@ class ZendAfi_View_Helper_Admin_SubscribeUsers extends ZendAfi_View_Helper_BaseH
   }
 
 
+  protected function _editUserLink($user) {
+    return $this->view->tagAnchor($this->view->url(['module' => 'admin',
+                                                    'controller' => 'users',
+                                                    'action' => 'edit',
+                                                    'id' => $user->getId()],
+                                                   null,
+                                                   true),
+                                  $this->view->boutonIco('type=edit'));
+  }
+
+
   protected function _subscribeUsersForm($search) {
     $users_found = Class_Users::getLoader()->findAllLike($search, $this->_by_right);
 
diff --git a/library/ZendAfi/View/Helper/DriveCheckoutPlan.php b/library/ZendAfi/View/Helper/DriveCheckoutPlan.php
new file mode 100644
index 0000000000000000000000000000000000000000..923129fba9d9c9c138580f2293046ba9e4fd60cb
--- /dev/null
+++ b/library/ZendAfi/View/Helper/DriveCheckoutPlan.php
@@ -0,0 +1,203 @@
+<?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_DriveCheckoutPlan extends ZendAfi_View_Helper_BaseHelper {
+  protected
+    $_plan,
+    $_user;
+
+  /**
+   * @param $plan Class_DriveCheckout_Plan
+   * @return string
+   */
+  public function driveCheckoutPlan($plan) {
+    if (!$plan)
+      return '';
+
+    $this->_plan = $plan;
+
+    return
+      $this->view->openBoiteContent($this->_('Planifier le retrait de mes documents'))
+      . $this->_tag('h2', $this->_('Site de retrait'))
+      . $this->_libraries()
+      . $this->_dates()
+      . $this->_times()
+      . $this->view->closeBoiteContent();
+  }
+
+
+  protected function _libraries() {
+    if ($library = $this->_plan->selectedLibrary())
+      return $this->_selectedLibrary($library);
+
+    if (!$this->_plan->hasLibraries())
+      return $this->_('Aucun document à retirer pour l\'instant');
+
+    $list = $this->_plan
+      ->injectIntoLibraries([],
+                            function($value, $library)
+                            {
+                              $value[] = $this->_library($library);
+                              return $value;
+                            });
+
+    return $this->_tag('ul', implode($list));
+  }
+
+
+  protected function _selectedLibrary($library) {
+    return $this
+      ->_tag('h3',
+             $library->getLibelle()
+             . ' ' . $this->_tagAnchor($this->_plan->resetUrl(),
+                                       $this->_('modifier'),
+                                       ['title' => $this->_('retour à la liste des sites de retrait')])
+             . BR . $this->_holdsInfoFor($library));
+  }
+
+
+  protected function _library($library) {
+    if ($existing = $this->_plan->findFutureFor($library)) {
+      return $this
+        ->_tag('li',
+               $library->getLibelle()
+               . BR . $this->_('Retrait planifié %s', $existing->getDateTimeLabel())
+               . ' '
+               . $this->_tagAnchor(['action' => 'ical',
+                                    'id' => $existing->getId()],
+                                   $this->_('Ajouter à mon agenda'),
+                                   ['title' => $this->_('Importer le retrait de %s pour %s au format ICal',
+                                                        $library->getLibelle(),
+                                                        $existing->getDateTimeLabel()),
+                                    ])
+               . ' / '
+               . $this->_tagAnchor(['action' => 'delete',
+                                    'id' => $existing->getId()],
+                                   $this->_('Supprimer'),
+                                   ['title' => $this->_('Supprimer le retrait de %s pour %s',
+                                                        $library->getLibelle(),
+                                                        $existing->getDateTimeLabel()),
+                                    'onclick' => 'return confirm(\''.  $this->_escapeJsAttrib($this->_('Etes vous sûr de vouloir supprimer ce retrait ?')) . '\')'])
+               . BR . $this->_holdsInfoFor($library));
+    }
+
+    return $this
+      ->_tag('li',
+             $this->_tagAnchor($this->_plan->libraryUrlFor($library),
+                               $library->getLibelle(),
+                               ['title' => $this->_('Choisir de retirer les documents de %s',
+                                                    $library->getLibelle())])
+             . BR . $this->_holdsInfoFor($library));
+  }
+
+
+  protected function _holdsInfoFor($library) {
+    $holds = $this->_plan->holds()->selectLibrary($library);
+    $total = $holds->count();
+    $ready = $holds->countWaitingToBePulled();
+    $not_ready = $total - $ready;
+
+    return sprintf('%s, %s, %s',
+                   $this->_plural($total,
+                                  'Aucune réservation',
+                                  '%s réservation',
+                                  '%s réservations',
+                                  $total),
+                   $this->_('%s à retirer', $ready),
+                   $this->_('%s en attente de traitement', $not_ready));
+  }
+
+
+  protected function _dates() {
+    if (!$this->_plan->selectedLibrary())
+      return '';
+
+    if ($date = $this->_plan->selectedDate())
+      return $this->_selectedDate($date);
+
+    $list = $this->_plan
+      ->injectIntoOpenings([],
+                           function($value, $opening)
+                           {
+                             $value[] = $this->_opening($opening);
+                             return $value;
+                           });
+
+    return $this->_tag('h2', $this->_('Date de retrait'))
+      . $this->_tag('ul', implode($list));
+  }
+
+
+  protected function _opening($opening) {
+    $label = $this->_dateLabel($opening);
+
+    return $this
+      ->_tag('li',
+             $this->_tagAnchor($this->_plan->dateUrlFor($opening),
+                               $label,
+                               ['title' => $this->_('Choisir de retirer les documets %s',
+                                                    $label)]));
+  }
+
+
+  protected function _selectedDate($date) {
+    $library = $this->_plan->selectedLibrary();
+
+    return
+      $this->_tag('h2', $this->_('Date de retrait'))
+      . $this->_tag('h3',
+                    $this->_dateLabel($date)
+                    . ' ' . $this->_tagAnchor($this->_plan->libraryUrlFor($library),
+                                              $this->_('modifier'),
+                                              ['title' => $this->_('retour à la liste des dates de retrait pour %s', $library->getLibelle())]));
+  }
+
+
+  protected function _dateLabel($date) {
+    return strftime($this->_('le %A %d %b'), $date->getTimeStamp());
+  }
+
+
+  protected function _times() {
+    if (!$this->_plan->selectedLibrary()
+        || (!$selected_date = $this->_plan->selectedDate()))
+      return '';
+
+    $multiOptions = $this->_plan
+      ->injectIntoTimes(['' => ''],
+                        function($value, $time)
+                        {
+                          $value[$time->format('H:i')] = $time->format('H\hi');
+                          return $value;
+                        });
+
+    $form = (new ZendAfi_Form(['data-backurl' => $this->view->url($this->_plan->dateUrlFor($selected_date))]))
+      ->addElement('select', 'checkout_time',
+                   ['label' => $this->_('Retirer les documents à :'),
+                    'multiOptions' => $multiOptions])
+      ->addUniqDisplayGroup('default')
+      ;
+
+    return $this->_tag('h2', $this->_('Heure de retrait'))
+      . $this->view->renderForm($form);
+  }
+}
diff --git a/library/ZendAfi/View/Helper/FormDate.php b/library/ZendAfi/View/Helper/FormDate.php
new file mode 100644
index 0000000000000000000000000000000000000000..e1951dc0cb8685eb6f9b9137688c965ed77d0c21
--- /dev/null
+++ b/library/ZendAfi/View/Helper/FormDate.php
@@ -0,0 +1,31 @@
+<?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_FormDate extends ZendAfi_View_Helper_FormHTML5 {
+  public function formDate($name, $value = null, $attribs = null)   {
+    return $this->renderElement($name, $value, $attribs);
+  }
+
+
+  public function inputType() {
+    return 'date';
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php b/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php
index 342c62553d173ed42d5abc974302c586b541729e..d746165213cba75fc3d8d573189eb63b0501f56b 100644
--- a/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php
+++ b/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php
@@ -22,10 +22,10 @@
 class ZendAfi_View_Helper_LibraryOpeningsAdmin
   extends ZendAfi_View_Helper_LibraryOpenings {
 
-  protected $_multimedia;
+  protected $_used_for;
 
-  public function libraryOpeningsAdmin($library, $multimedia=false) {
-    $this->_multimedia = $multimedia;
+  public function libraryOpeningsAdmin($library, $used_for = null) {
+    $this->_used_for = $used_for;
 
     return $this->libraryOpenings($library);
   }
@@ -34,7 +34,7 @@ class ZendAfi_View_Helper_LibraryOpeningsAdmin
   public function _getVisitor() {
     return (new Class_Ouverture_Visitor())
       ->beExhaustive()
-      ->setMultimedia($this->_multimedia);
+      ->setUsedFor($this->_used_for);
   }
 
 
@@ -72,14 +72,24 @@ class ZendAfi_View_Helper_LibraryOpeningsAdmin
     foreach($openings as $opening)
       $label = (!$label) ? $opening->getLabel() : $label;
 
-    $description = new Class_TableDescription_Openings('ouvertures');
+    $description = $this->_newTableDescription();
     return $this->_renderAdminSection($closure($label),
                                       $this->_renderTagModelTable($openings, $description));
   }
 
 
+  protected function _newTableDescription() {
+    $table_description_class = ($this->_used_for === Class_Ouverture::USED_FOR_DRIVE)
+      ? Class_TableDescription_Openings_Drive::class
+      : Class_TableDescription_Openings::class;
+
+    return new $table_description_class('ouvertures');
+  }
+
+
   protected function _renderSectionLabelled($label, $openings) {
-    $description = (new Class_TableDescription_OpeningsLabelled('ouvertures'));
+    $description = $this->_newTableDescription()
+                        ->prependColumn($this->_('Libellé'), 'label');
 
     return $this->_renderAdminSection($label,
                                       $this->_renderTagModelTable($openings, $description));
diff --git a/library/ZendAfi/View/Helper/RenderLibraryOpening.php b/library/ZendAfi/View/Helper/RenderLibraryOpening.php
index 01622651cbfe80a5c5bfe15b339125d1d055300f..37d5d97f65e48b339efda67e4bfc8f34fb1f8dc1 100644
--- a/library/ZendAfi/View/Helper/RenderLibraryOpening.php
+++ b/library/ZendAfi/View/Helper/RenderLibraryOpening.php
@@ -25,7 +25,7 @@ class ZendAfi_View_Helper_RenderLibraryOpening extends ZendAfi_View_Helper_BaseH
 
 
   public function renderLibraryOpening($library) {
-    if (!$library->hasOuvertures() || $library->hasHoraire())
+    if (!$library->hasOuverturesFor(Class_Ouverture::USED_FOR_LIBRARY) || $library->hasHoraire())
       return '';
 
     return $this->renderOuverturesForLibrary($library);
diff --git a/library/storm b/library/storm
index 7aaada2df1e203d1fb50685dafe371de1ac034a5..67e32fa4f2114f69c7a3d9b96d9419186cb2e7bf 160000
--- a/library/storm
+++ b/library/storm
@@ -1 +1 @@
-Subproject commit 7aaada2df1e203d1fb50685dafe371de1ac034a5
+Subproject commit 67e32fa4f2114f69c7a3d9b96d9419186cb2e7bf
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan.php
new file mode 100644
index 0000000000000000000000000000000000000000..1340b09a17ae29d5003235d0736bcb1ef415c81f
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Copyright (c) 2012-2018, 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 Intonation_Library_View_Wrapper_DriveCheckoutPlan
+  extends Intonation_Library_View_Wrapper_Abstract {
+
+  public function getMainTitle() {
+    return $this->_('Planifier le retrait de mes documents');
+  }
+
+
+  public function getSecondaryTitle() {
+    return '';
+  }
+
+
+  public function getDocType() {
+    return '';
+  }
+
+
+  public function getDocTypeLabel() {
+    return '';
+  }
+
+
+  public function getMainLink() {
+    return;
+  }
+
+
+  public function getPicture() {
+    return '';
+  }
+
+
+  public function getPictureAction() {
+    return '';
+  }
+
+
+  public function getFullDescription() {
+    return '';
+  }
+
+
+  public function getDescription() {
+    return '';
+  }
+
+
+  public function getDescriptionTitle() {
+    return '';
+  }
+
+
+  public function getSecondaryIco() {
+    return;
+  }
+
+
+  public function getSecondaryLink() {
+    return;
+  }
+
+
+  public function getBadges() {
+    return '';;
+  }
+
+
+  public function getActions() {
+    return [];
+  }
+
+
+  public function getEmbedMedia() {
+    return '';
+  }
+
+
+  public function getHtmlPicture() {
+    return '';
+  }
+
+
+  public function getOsmData() {
+    return null;
+  }
+}
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/Library.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/Library.php
new file mode 100644
index 0000000000000000000000000000000000000000..7363e127c044fc983e62843f607c8921bd3c5ef8
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/Library.php
@@ -0,0 +1,163 @@
+<?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 Intonation_Library_View_Wrapper_DriveCheckoutPlan_Library
+  extends Intonation_Library_View_Wrapper_Library {
+
+  protected
+    $_plan,
+    $_existing;
+
+  public function setPlan($plan) {
+    $this->_plan = $plan;
+    return $this;
+  }
+
+
+  public function getMainLink() {
+    return;
+  }
+
+
+  public function getSecondaryLink() {
+    return $this->_getExisting()
+      ? null
+      : new Intonation_Library_Link(['Url' => $this->_view->url($this->_plan->libraryUrlFor($this->_model)),
+                                     'Image' => Class_Template::current()->getIco($this->_view,
+                                                                                  'available',
+                                                                                  'library'),
+
+                                     'Text' => $this->_('Choisir ce site'),
+                                     'Title' => $this->_('Choisir de retirer les documents de %s',
+                                                         $this->_model->getLibelle())]);
+  }
+
+
+  public function getActions() {
+    return ($existing = $this->_getExisting())
+      ? [new Intonation_Library_Link(['Url' => $this->_view->url(['action' => 'ical',
+                                                                  'id' => $existing->getId()]),
+                                      'Image' => Class_Template::current()->getIco($this->_view,
+                                                                                   'extend-loan',
+                                                                                   'library'),
+
+                                      'Text' => $this->_('Ajouter à mon agenda'),
+                                      'Title' => $this->_('Importer le retrait de %s pour %s au format ICal',
+                                                          $this->_model->getLibelle(),
+                                                          $existing->getDateTimeLabel()),
+                                      ]),
+         new Intonation_Library_Link(['Url' => $this->_view->url(['action' => 'delete',
+                                                                  'id' => $existing->getId()]),
+                                      'Image' => Class_Template::current()->getIco($this->_view,
+                                                                                   'clean',
+                                                                                   'utils'),
+
+                                      'Text' => $this->_('Supprimer'),
+                                      'Title' => $this->_('Supprimer le retrait de %s pour %s',
+                                                          $this->_model->getLibelle(),
+                                                          $existing->getDateTimeLabel()),
+                                      'Class' => 'text-danger',
+                                      'Attribs' => ['onclick' => $this->_view->confirm($this->_('Etes vous sûr de vouloir supprimer ce retrait ?'))]])]
+      : [];
+  }
+
+
+  public function getDescription() {
+    $existing = ($existing = $this->_getExisting())
+      ? $this->_view->tag('p',
+                          Class_Template::current()->getIco($this->_view, 'available', 'library')
+                          . ' '
+                          . $this->_('Retrait planifié %s', $existing->getDateTimeLabel()),
+                          ['class' => 'text-success'])
+      : '';
+
+    return $existing . $this->getBadges();
+  }
+
+
+  public function getBadges() {
+    return
+      $this->_view->renderBadges($this->_holdsBadges(), $this)
+      . $this->_view->renderBadges($this->_localFieldsBadges(), $this);
+  }
+
+
+  protected function _holdsBadges() {
+    $holds = $this->_plan->holds()->selectLibrary($this->_model);
+    $total = $holds->count();
+    $ready = $holds->countWaitingToBePulled();
+    $not_ready = $total - $ready;
+
+    return [
+            ((new Intonation_Library_Badge)
+             ->setTag('span')
+             ->setClass('primary')
+             ->setText($this->_plural($total,
+                                      'Aucune réservation',
+                                      '%s réservation',
+                                      '%s réservations',
+                                      $total))
+             ->setImage(Class_Template::current()->getIco($this->_view,
+                                                          'loan',
+                                                          'library'))
+             ->setTitle($this->_plural($total,
+                                       'Aucun document réservé',
+                                       '%s document réservé',
+                                       '%s documents réservés',
+                                       $total))),
+
+            ((new Intonation_Library_Badge)
+             ->setTag('span')
+             ->setClass('success')
+             ->setText($this->_('%s à retirer', $ready))
+             ->setImage(Class_Template::current()->getIco($this->_view,
+                                                          'loan',
+                                                          'library'))
+             ->setTitle($this->_plural($ready,
+                                       'Aucun document ne peut être retiré',
+                                       '%s document peut être retiré',
+                                       '%s documents peuvent être retirés',
+                                       $ready))),
+
+            ((new Intonation_Library_Badge)
+             ->setTag('span')
+             ->setClass('secondary')
+             ->setText($this->_('%s en attente', $not_ready))
+             ->setImage(Class_Template::current()->getIco($this->_view,
+                                                          'loan',
+                                                          'library'))
+             ->setTitle($this->_plural($not_ready,
+                                       'Aucun document en attente de traitement',
+                                       '%s document est en attente de traitement',
+                                       '%s documents sont en attente de traitement',
+                                       $not_ready))),
+    ];
+  }
+
+
+  protected function _getExisting() {
+    if ($this->_existing)
+      return $this->_existing;
+
+    return $this->_existing = $this->_plan->findFutureFor($this->_model);
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/LibrarySelected.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/LibrarySelected.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1dbc107e8381a5386833688e2d3b72b0b374abf
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/LibrarySelected.php
@@ -0,0 +1,37 @@
+<?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 Intonation_Library_View_Wrapper_DriveCheckoutPlan_LibrarySelected
+  extends Intonation_Library_View_Wrapper_DriveCheckoutPlan_Library {
+
+
+  public function getSecondaryLink() {
+    return new Intonation_Library_Link(['Url' => $this->_view->url($this->_plan->resetUrl()),
+                                        'Image' => Class_Template::current()->getIco($this->_view,
+                                                                                     'refresh',
+                                                                                     'utils'),
+
+                                        'Text' => $this->_('Choisir un autre site'),
+                                        'Title' => $this->_('Retourner à la liste des sites de retrait'),
+                                        'Class' => 'text-danger']);
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent.php
new file mode 100644
index 0000000000000000000000000000000000000000..02d3aab0668746ce619f80db7cb771298ac1ef9b
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent.php
@@ -0,0 +1,51 @@
+<?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 Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent
+  extends Intonation_Library_View_Wrapper_RichContent_Abstract {
+
+  public function getNavigation() {
+    return;
+  }
+
+
+  public function getActions() {
+    return '';
+  }
+
+
+  public function getRowActions() {
+    return '';
+  }
+
+
+  protected function _getSectionsInstances() {
+    return [new Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent_Library,
+            new Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent_Date,
+            new Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent_Time];
+  }
+
+
+  protected function _getWrapper() {
+    return 'Intonation_Library_View_Wrapper_DriveCheckoutPlan';
+  }
+}
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Date.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Date.php
new file mode 100644
index 0000000000000000000000000000000000000000..13116fca5be5c171312d612c591b925d8032d912
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Date.php
@@ -0,0 +1,121 @@
+<?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 Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent_Date
+  extends Intonation_Library_View_Wrapper_RichContent_Section{
+
+  public function getTitle() {
+    return $this->_('Date de retrait');
+  }
+
+
+  public function getContent() {
+    if ($this->_content)
+      return $this->_content;
+
+    if (!$this->_model->selectedLibrary())
+      return $this->_content = '';
+
+    $html = $this->_getDates()->injectInto('', [$this, 'renderDate']);
+
+    return $this->_content = $this->_view->tag('ul', $html, ['class' => 'list-group']);
+  }
+
+
+  public function _getDates() {
+    return ($selected = $this->_model->selectedDate())
+      ? new Storm_Model_Collection([$selected])
+      : $this->_model->injectIntoOpenings(new Storm_Model_Collection(),
+                                          function($collection, $library)
+                                          {
+                                            $collection->append($library);
+                                            return $collection;
+                                          });
+  }
+
+
+  public function renderDate($html, $date) {
+    return $html . $this->_view->tag('li',
+                                     ($this->_model->selectedDate() == $date)
+                                     ? $this->_renderSelectedDate($date)
+                                     : $this->_renderDate($date),
+                                     ['class' => 'list-group-item']);
+  }
+
+
+  protected function _renderDate($date) {
+    $label = $this->_dateLabel($date);
+    $action = new Intonation_Library_Link(['Url' => $this->_view->url($this->_model->dateUrlFor($date)),
+                                           'Image' => Class_Template::current()->getIco($this->_view,
+                                                                                        'available',
+                                                                                        'library'),
+
+                                           'Text' => $label,
+                                           'Title' => $this->_('Choisir de retirer les documents %s',
+                                                               $label)]);
+
+    return $this->_view->tagAction($action);
+  }
+
+
+  protected function _renderSelectedDate($date) {
+    $label = $this->_dateLabel($date);
+    $action = new Intonation_Library_Link(['Url' => $this->_view->url($this->getNavUrl()),
+                                           'Image' => Class_Template::current()->getIco($this->_view,
+                                                                                        'refresh',
+                                                                                        'utils'),
+
+                                           'Text' => $this->_('Choisir une autre date'),
+                                           'Title' => $this->_('Retourner à la liste des dates de retrait pour %s',
+                                                               $this->_model->selectedLibrary()->getLibelle()),
+                                           'Class' => 'text-danger']);
+
+    return $label . BR . $this->_view->tagAction($action);
+  }
+
+
+  protected function _dateLabel($date) {
+    return strftime($this->_('le %A %d %b'), $date->getTimeStamp());
+  }
+
+
+  public function getClass() {
+    return 'drivecheckout_plan_date';
+  }
+
+
+  public function getNavUrl() {
+    return ($library = $this->_model->selectedLibrary())
+      ? $this->_model->libraryUrlFor($library)
+      : [];
+  }
+
+
+  public function getNavIco() {
+    return 'class fas fa-calendar-alt';
+  }
+
+
+  public function getNavTitle() {
+    return $this->_('Choisir une date de retrait');
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Library.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Library.php
new file mode 100644
index 0000000000000000000000000000000000000000..950046b7296e3d3ac35099005e5159f33f6b5542
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Library.php
@@ -0,0 +1,92 @@
+<?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 Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent_Library
+  extends Intonation_Library_View_Wrapper_RichContent_Section{
+
+  public function getTitle() {
+    return $this->_('Site de retrait');
+  }
+
+
+  public function getContent() {
+    if ($this->_content)
+      return $this->_content;
+
+    $librairies = $this->_getLibraries();
+
+    if ($librairies->isEmpty())
+      return $this->_content = $this->_view
+        ->tag('p', $this->_('Aucun document à retirer pour l\'instant'));
+
+    $wrapper_class = $this->_wrapperClass();
+
+    return $this->_content = $this->_view
+      ->renderList($librairies,
+                   function($library) use($wrapper_class)
+                   {
+                     return $this->_view->cardifyHorizontal((new $wrapper_class())
+                                                            ->setPlan($this->_model)
+                                                            ->setModel($library)
+                                                            ->setView($this->_view));
+                   });
+  }
+
+
+  protected function _wrapperClass() {
+    return $this->_model->selectedLibrary()
+      ? 'Intonation_Library_View_Wrapper_DriveCheckoutPlan_LibrarySelected'
+      : 'Intonation_Library_View_Wrapper_DriveCheckoutPlan_Library';
+  }
+
+
+  public function _getLibraries() {
+    return ($selected = $this->_model->selectedLibrary())
+      ? new Storm_Model_Collection([$selected])
+      : $this->_model->injectIntoLibraries(new Storm_Model_Collection(),
+                                           function($collection, $library)
+                                           {
+                                             $collection->append($library);
+                                             return $collection;
+                                           });
+  }
+
+
+  public function getClass() {
+    return 'drivecheckout_plan_library';
+  }
+
+
+  public function getNavUrl() {
+    return $this->_model->resetUrl();
+  }
+
+
+  public function getNavIco() {
+    return 'class fas fa-map-marker-alt';
+  }
+
+
+  public function getNavTitle() {
+    return $this->_('Choisir un site de retrait');
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Time.php b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Time.php
new file mode 100644
index 0000000000000000000000000000000000000000..a142bed07c130a6e0fc33ec305454362370ef1b6
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Time.php
@@ -0,0 +1,77 @@
+<?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 Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent_Time
+  extends Intonation_Library_View_Wrapper_RichContent_Section{
+
+  public function getTitle() {
+    return $this->_('Heure de retrait');
+  }
+
+
+  public function getContent() {
+    if ($this->_content)
+      return $this->_content;
+
+    if (!$this->_model->selectedDate())
+      return $this->_content = '';
+
+    $multiOptions = $this->_model
+      ->injectIntoTimes(['' => ''],
+                        function($value, $time)
+                        {
+                          $value[$time->format('H:i')] = $time->format('H\hi');
+                          return $value;
+                        });
+
+    $form = (new ZendAfi_Form(['data-backurl' => $this->_view->url($this->getNavUrl())]))
+      ->addElement('select', 'checkout_time',
+                   ['label' => $this->_('Retirer les documents à :'),
+                    'multiOptions' => $multiOptions])
+      ->addUniqDisplayGroup('default')
+      ;
+
+    return $this->_content = $this->_view->renderForm($form);
+  }
+
+
+  public function getClass() {
+    return 'drivecheckout_plan_time';
+  }
+
+
+  public function getNavUrl() {
+    return ($date= $this->_model->selectedDate())
+      ? $this->_model->dateUrlFor($date)
+      : [];
+  }
+
+
+  public function getNavIco() {
+    return 'class fas fa-clock';
+  }
+
+
+  public function getNavTitle() {
+    return $this->_('Choisir une heure de retrait');
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/Library.php b/library/templates/Intonation/Library/View/Wrapper/Library.php
index 80e62075b22bede90dbaca909fc993bebd1e7f7e..21800425ef8328e8679cec16764315477c0f7207 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Library.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Library.php
@@ -106,56 +106,7 @@ class Intonation_Library_View_Wrapper_Library extends Intonation_Library_View_Wr
 
 
   public function getBadges() {
-    $badges = [
-               ((new Intonation_Library_Badge)
-                ->setTag('span')
-                ->setClass('secondary')
-                ->setText($this->_model->getAdresse())
-                ->setTitle($this->_('%s adresse de la bibliothèque %s',
-                                    $this->_model->getAdresse(),
-                                    $this->_model->getLibelle()))),
-
-               ((new Intonation_Library_Badge)
-                ->setTag('span')
-                ->setClass('secondary')
-                ->setText($this->_model->getCp())
-                ->setTitle($this->_('%s code postale de la bibliothèque %s',
-                                    $this->_model->getCp(),
-                                    $this->_model->getLibelle()))),
-
-                ((new Intonation_Library_Badge)
-                 ->setTag('span')
-                 ->setClass('secondary')
-                 ->setText($this->_model->getVille())
-                 ->setTitle($this->_('%s ville de la bibliothèque %s',
-                                    $this->_model->getVille(),
-                                     $this->_model->getLibelle()))),
-
-                ((new Intonation_Library_Badge)
-                 ->setTag('a')
-                 ->setClass('primary text-light')
-                 ->setText(str_replace([' ', '.', ','], ' ', $this->_model->getTelephone()))
-                 ->setUrl(sprintf('tel:%s',
-                                  str_replace([' ', '.', ',',], '', $this->_model->getTelephone())))
-                 ->setImage(Class_Template::current()->getIco($this->_view,
-                                                             'phone',
-                                                              'utils'))
-                 ->setTitle($this->_('%s numéro de téléphone de la bibliothèque %s',
-                                    $this->_model->getTelephone(),
-                                     $this->_model->getLibelle()))),
-
-               ((new Intonation_Library_Badge)
-                  ->setTag('a')
-                  ->setClass('primary text-light')
-                  ->setUrl(sprintf('mailto:%s', $this->_model->getMail()))
-                  ->setImage(Class_Template::current()->getIco($this->_view,
-                                                             'email',
-                                                               'utils'))
-                  ->setText($this->_model->getMail())
-                  ->setTitle($this->_('%s adresse e-mail de la bibliothèque %s',
-                                    $this->_model->getMail(),
-                                      $this->_model->getLibelle())))
-    ];
+    $badges = $this->_localFieldsBadges();
 
     foreach($this->_model->getAllCustomFields()->getBadgeableFieldValues() as $field) {
       $label = $field->getLabel();
@@ -179,6 +130,60 @@ class Intonation_Library_View_Wrapper_Library extends Intonation_Library_View_Wr
   }
 
 
+  protected function _localFieldsBadges() {
+    return [
+            ((new Intonation_Library_Badge)
+             ->setTag('span')
+             ->setClass('secondary')
+             ->setText($this->_model->getAdresse())
+             ->setTitle($this->_('%s adresse de la bibliothèque %s',
+                                 $this->_model->getAdresse(),
+                                 $this->_model->getLibelle()))),
+
+            ((new Intonation_Library_Badge)
+             ->setTag('span')
+             ->setClass('secondary')
+             ->setText($this->_model->getCp())
+             ->setTitle($this->_('%s code postale de la bibliothèque %s',
+                                 $this->_model->getCp(),
+                                 $this->_model->getLibelle()))),
+
+            ((new Intonation_Library_Badge)
+             ->setTag('span')
+             ->setClass('secondary')
+             ->setText($this->_model->getVille())
+             ->setTitle($this->_('%s ville de la bibliothèque %s',
+                                 $this->_model->getVille(),
+                                 $this->_model->getLibelle()))),
+
+            ((new Intonation_Library_Badge)
+             ->setTag('a')
+             ->setClass('primary text-light')
+             ->setText(str_replace([' ', '.', ','], ' ', $this->_model->getTelephone()))
+             ->setUrl(sprintf('tel:%s',
+                              str_replace([' ', '.', ',',], '', $this->_model->getTelephone())))
+             ->setImage(Class_Template::current()->getIco($this->_view,
+                                                          'phone',
+                                                          'utils'))
+             ->setTitle($this->_('%s numéro de téléphone de la bibliothèque %s',
+                                 $this->_model->getTelephone(),
+                                 $this->_model->getLibelle()))),
+
+            ((new Intonation_Library_Badge)
+             ->setTag('a')
+             ->setClass('primary text-light')
+             ->setUrl(sprintf('mailto:%s', $this->_model->getMail()))
+             ->setImage(Class_Template::current()->getIco($this->_view,
+                                                          'email',
+                                                          'utils'))
+             ->setText($this->_model->getMail())
+             ->setTitle($this->_('%s adresse e-mail de la bibliothèque %s',
+                                 $this->_model->getMail(),
+                                 $this->_model->getLibelle())))
+    ];
+  }
+
+
   public function getActions() {
     return [];
   }
diff --git a/library/templates/Intonation/Library/View/Wrapper/SearchHistory.php b/library/templates/Intonation/Library/View/Wrapper/SearchHistory.php
index c44c9640d88eee17a13c49c3f81c071c32ac63b8..549bb162e74339e5c61c10f26412fcbe73eb4b2c 100644
--- a/library/templates/Intonation/Library/View/Wrapper/SearchHistory.php
+++ b/library/templates/Intonation/Library/View/Wrapper/SearchHistory.php
@@ -23,7 +23,8 @@
 class Intonation_Library_View_Wrapper_SearchHistory extends Intonation_Library_View_Wrapper_Search {
 
   public function getMainLink() {
-    return reset(parent::getActions());
+    $actions = parent::getActions();
+    return reset($actions);
   }
 
 
diff --git a/library/templates/Intonation/System/Abstract.php b/library/templates/Intonation/System/Abstract.php
index a493477b9b77cf6f0f4400421226ba4250cb3b6f..2e9ffbfc1918c4a15e367ab883366d3187ce3fe6 100644
--- a/library/templates/Intonation/System/Abstract.php
+++ b/library/templates/Intonation/System/Abstract.php
@@ -191,8 +191,10 @@ abstract class Intonation_System_Abstract {
       $controllers [] = 'widget';
     }
 
-    if($this->isVisibleForUserController())
+    if($this->isVisibleForUserController()) {
       $controllers [] = 'abonne';
+      $controllers [] = 'drive-checkout';
+    }
 
     if($this->isVisibleForCmsController()) {
       $controllers [] = 'cms';
diff --git a/library/templates/Intonation/View/Abonne/Holds.php b/library/templates/Intonation/View/Abonne/Holds.php
index c412502edb3007c1e9da2035fd74dde760e4caf8..6b2b388f4cb767c2b88efa1a0ab8d7bad60948aa 100644
--- a/library/templates/Intonation/View/Abonne/Holds.php
+++ b/library/templates/Intonation/View/Abonne/Holds.php
@@ -36,6 +36,16 @@ class Intonation_View_Abonne_Holds extends ZendAfi_View_Helper_BaseHelper {
       return $this->view->cardifyHorizontal($wrapped);
     };
 
-    return $this->view->renderTruncateList(new Storm_Collection($holds), $callback);
+    $actions = Class_AdminVar::isModuleEnabled('ENABLE_DRIVE_CHECKOUT')
+      ? [new Intonation_Library_Link(['Url' => $this->view->url(['controller' => 'drive-checkout',
+                                                                 'action' => 'plan']),
+                                      'Text' => $this->_('Planifier le retrait de mes documents'),
+                                      'Title' => $this->_('Prendre ou lister mes rendez-vous pour le retrait de mes documents'),
+                                      'Image' => Class_Template::current()->getIco($this->view,
+                                                                                   'agenda',
+                                                                                   'library')])]
+      : [];
+
+    return $this->view->renderCollection(new Storm_Collection($holds), $actions);
   }
 }
diff --git a/library/templates/Intonation/View/DriveCheckoutPlan.php b/library/templates/Intonation/View/DriveCheckoutPlan.php
new file mode 100644
index 0000000000000000000000000000000000000000..1b55a8767737a021b9d41fa76f638b9b3e120536
--- /dev/null
+++ b/library/templates/Intonation/View/DriveCheckoutPlan.php
@@ -0,0 +1,37 @@
+<?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 Intonation_View_DriveCheckoutPlan extends Intonation_View_Jumbotron_Abstract {
+  public function driveCheckoutPlan($plan) {
+    return $this->_core($plan);
+  }
+
+
+  protected function _getWrappedInstance() {
+    return new Intonation_Library_View_Wrapper_DriveCheckoutPlan;
+  }
+
+
+  protected function _getRichContentInstance() {
+    return new Intonation_Library_View_Wrapper_DriveCheckoutPlan_RichContent;
+  }
+}
diff --git a/public/admin/skins/bokeh72/config.json b/public/admin/skins/bokeh72/config.json
index 41a3cc44d5738ea38f6d436ba8d5e62e1442b2f4..fc165ddf0473a110f1b36d8289f27ea115731494 100644
--- a/public/admin/skins/bokeh72/config.json
+++ b/public/admin/skins/bokeh72/config.json
@@ -67,7 +67,8 @@
     "books": "../../images/picto/books.png",
     "tag": "../../images/picto/tag_blue.png",
     "suggestion": "../../images/picto/traductions_16.png",
-    "meeting": "../../images/picto/meeting_24.png"
+    "meeting": "../../images/picto/meeting_24.png",
+    "drive-checkout": "../../images/picto/paniers_16.png"
   },
 
   "actions":
@@ -116,6 +117,7 @@
     "images": "../../images/ico/album_images.png",
     "test": "../../images/ico/tester.gif",
     "basket": "../../images/picto/paniers_16.png",
+    "shopping": "../../images/picto/paniers_16.png",
     "permalink": "../../images/reseaux/permalink.gif",
     "mail": "../../images/ico/mail.png",
     "validate": "../../images/ico/coche_verte.gif",
diff --git a/public/admin/skins/bokeh74/config.json b/public/admin/skins/bokeh74/config.json
index 7a2989a908c7f233d9585865803ce11fe4ec0656..15f4733bbca3648f3de91558bbd5ec31057ba23b 100644
--- a/public/admin/skins/bokeh74/config.json
+++ b/public/admin/skins/bokeh74/config.json
@@ -72,7 +72,8 @@
     "tag": "icons/menu/tag_24.png",
     "suggestion": "icons/menu/suggestion_achat_24.png",
     "meeting": "icons/menu/meeting_24.png",
-    "identity_providers": "icons/menu/demande_inscri_24.png"
+    "identity_providers": "icons/menu/demande_inscri_24.png",
+    "drive-checkout": "icons/menu/shopping_24.png"
   },
 
   "actions":
@@ -121,6 +122,7 @@
     "images": "icons/actions/album_images_16.png",
     "test": "icons/actions/tester_16.png",
     "basket": "icons/actions/panier_24.png",
+    "shopping": "icons/actions/shopping_16.png",
     "permalink": "icons/actions/permalink_16.png",
     "mail": "icons/actions/mail_16.png",
     "validate": "icons/actions/coche_16.png",
diff --git a/public/admin/skins/bokeh74/icons/actions/shopping_16.png b/public/admin/skins/bokeh74/icons/actions/shopping_16.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f758bb4d49dd52f6478701e6b1ba3139cf288aa
Binary files /dev/null and b/public/admin/skins/bokeh74/icons/actions/shopping_16.png differ
diff --git a/public/admin/skins/bokeh74/icons/actions/shopping_24.png b/public/admin/skins/bokeh74/icons/actions/shopping_24.png
new file mode 100644
index 0000000000000000000000000000000000000000..c40bbe2426f9d5a91eeba0064bafdee7661fa410
Binary files /dev/null and b/public/admin/skins/bokeh74/icons/actions/shopping_24.png differ
diff --git a/public/admin/skins/bokeh74/icons/menu/shopping_24.png b/public/admin/skins/bokeh74/icons/menu/shopping_24.png
new file mode 100644
index 0000000000000000000000000000000000000000..c40bbe2426f9d5a91eeba0064bafdee7661fa410
Binary files /dev/null and b/public/admin/skins/bokeh74/icons/menu/shopping_24.png differ
diff --git a/public/admin/skins/bokeh74/icons/menu/shopping_48.png b/public/admin/skins/bokeh74/icons/menu/shopping_48.png
new file mode 100644
index 0000000000000000000000000000000000000000..90fcb8974d22112d483f92fd8ef6ea33f1afa60c
Binary files /dev/null and b/public/admin/skins/bokeh74/icons/menu/shopping_48.png differ
diff --git a/public/admin/skins/noel/config.json b/public/admin/skins/noel/config.json
index 01e9f770ac8c205d90b48117344f5e64d9242ca3..ce45549444fd15e70dab9ac680d0f6e5844ea0c6 100644
--- a/public/admin/skins/noel/config.json
+++ b/public/admin/skins/noel/config.json
@@ -70,7 +70,8 @@
     "books": "icons/menu/books_24.png",
     "tag": "icons/menu/tag_24.png",
     "suggestion": "icons/menu/suggestion_achat_24.png",
-    "meeting": "icons/menu/meeting_24.png"
+    "meeting": "icons/menu/meeting_24.png",
+    "drive-checkout": "icons/menu/shopping_24.png"
   },
 
   "actions":
@@ -119,6 +120,7 @@
     "images": "icons/actions/album_images_16.png",
     "test": "icons/actions/tester_16.png",
     "basket": "icons/actions/panier_24.png",
+    "shopping": "icons/actions/shopping_16.png",
     "permalink": "icons/actions/permalink_16.png",
     "mail": "icons/actions/mail_16.png",
     "validate": "icons/actions/coche_16.png",
diff --git a/public/admin/skins/noel/icons/actions/shopping_16.png b/public/admin/skins/noel/icons/actions/shopping_16.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f758bb4d49dd52f6478701e6b1ba3139cf288aa
Binary files /dev/null and b/public/admin/skins/noel/icons/actions/shopping_16.png differ
diff --git a/public/admin/skins/noel/icons/actions/shopping_24.png b/public/admin/skins/noel/icons/actions/shopping_24.png
new file mode 100644
index 0000000000000000000000000000000000000000..c40bbe2426f9d5a91eeba0064bafdee7661fa410
Binary files /dev/null and b/public/admin/skins/noel/icons/actions/shopping_24.png differ
diff --git a/public/admin/skins/noel/icons/menu/shopping_24.png b/public/admin/skins/noel/icons/menu/shopping_24.png
new file mode 100644
index 0000000000000000000000000000000000000000..c40bbe2426f9d5a91eeba0064bafdee7661fa410
Binary files /dev/null and b/public/admin/skins/noel/icons/menu/shopping_24.png differ
diff --git a/public/admin/skins/noel/icons/menu/shopping_48.png b/public/admin/skins/noel/icons/menu/shopping_48.png
new file mode 100644
index 0000000000000000000000000000000000000000..90fcb8974d22112d483f92fd8ef6ea33f1afa60c
Binary files /dev/null and b/public/admin/skins/noel/icons/menu/shopping_48.png differ
diff --git a/public/admin/skins/retro/config.json b/public/admin/skins/retro/config.json
index 140ff78b5c2b7b1fadd6e488560b2b465a2ccc48..91889c2c808c2a7552bd5a72664bcd2fb4d7da21 100644
--- a/public/admin/skins/retro/config.json
+++ b/public/admin/skins/retro/config.json
@@ -70,7 +70,8 @@
     "books": "icons/menu/books_24.png",
     "tag": "icons/menu/tag_24.png",
     "suggestion": "icons/menu/suggestion_achat_24.png",
-    "meeting": "icons/menu/meeting_24.png"
+    "meeting": "icons/menu/meeting_24.png",
+    "drive-checkout": "icons/menu/shopping_24.png"
   },
 
   "actions":
@@ -120,6 +121,8 @@
     "images": "icons/actions/album_images_16.png",
     "test": "icons/actions/tester_16.png",
     "basket": "icons/actions/panier_16.png",
+    "shopping": "icons/actions/shopping_16.png",
+    "shopping": "icons/actions/shopping_16.png",
     "permalink": "icons/actions/permalink_16.png",
     "mail": "icons/actions/mail_16.png",
     "validate": "icons/actions/coche_16.png",
diff --git a/public/admin/skins/retro/icons/actions/shopping_16.png b/public/admin/skins/retro/icons/actions/shopping_16.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f758bb4d49dd52f6478701e6b1ba3139cf288aa
Binary files /dev/null and b/public/admin/skins/retro/icons/actions/shopping_16.png differ
diff --git a/public/admin/skins/retro/icons/menu/shopping_24.png b/public/admin/skins/retro/icons/menu/shopping_24.png
new file mode 100644
index 0000000000000000000000000000000000000000..c40bbe2426f9d5a91eeba0064bafdee7661fa410
Binary files /dev/null and b/public/admin/skins/retro/icons/menu/shopping_24.png differ
diff --git a/public/admin/skins/retro/icons/menu/shopping_48.png b/public/admin/skins/retro/icons/menu/shopping_48.png
new file mode 100644
index 0000000000000000000000000000000000000000..90fcb8974d22112d483f92fd8ef6ea33f1afa60c
Binary files /dev/null and b/public/admin/skins/retro/icons/menu/shopping_48.png differ
diff --git a/tests/application/modules/admin/controllers/BibControllerTest.php b/tests/application/modules/admin/controllers/BibControllerTest.php
index 5dfb73001d9d649ea190b4ea3fbd092755fa59b7..bfe6f4e34da1ac5ec804189cbedd77f7ab7ee8b9 100644
--- a/tests/application/modules/admin/controllers/BibControllerTest.php
+++ b/tests/application/modules/admin/controllers/BibControllerTest.php
@@ -195,7 +195,7 @@ class BibControllerIndexWidthAdminPortailWithMultimediaTest extends BibControlle
 
   /** @test */
   public function shouldHaveActionToOuverturesForMultimedia() {
-    $this->assertXPath('//tr[1]//a[contains(@href, "ouvertures/index/id_site/2/multimedia/1")]');
+    $this->assertXPath('//tr[1]//a[contains(@href, "ouvertures/index/id_site/2/used_for/1")]');
   }
 }
 
diff --git a/tests/application/modules/admin/controllers/OuverturesControllerTest.php b/tests/application/modules/admin/controllers/OuverturesControllerTest.php
index a9cfa65dc5cea84066b2a8d982011a9594ae9983..9e88dbe36917a03d20aec7131590c97908b8c51e 100644
--- a/tests/application/modules/admin/controllers/OuverturesControllerTest.php
+++ b/tests/application/modules/admin/controllers/OuverturesControllerTest.php
@@ -257,7 +257,7 @@ class OuverturesControllerIndexActionSiteCranMultimediaEnabledTest extends Ouver
     parent::setUp();
 
     Class_AdminVar::set('MULTIMEDIA_KEY', 'SECRET_KEY');
-    $this->dispatch('/admin/ouvertures/index/id_site/1/multimedia/1', true);
+    $this->dispatch('/admin/ouvertures/index/id_site/1/used_for/1', true);
   }
 
 
@@ -269,7 +269,7 @@ class OuverturesControllerIndexActionSiteCranMultimediaEnabledTest extends Ouver
 
   /** @test */
   public function addButtonLabelShouldBeAjouterUnePlageHoraire(){
-    $this->assertXPathContentContains('//button','Ajouter une plage horaire de réservation multimedia');
+    $this->assertXPathContentContains('//button','Ajouter une plage horaire de réservation multimédia');
   }
 
   /** @test */
@@ -283,7 +283,7 @@ class OuverturesControllerIndexActionSiteCranMultimediaEnabledTest extends Ouver
 class OuverturesControllerIndexActionSiteCranMultimediaDisabledTest extends OuverturesControllerTestCase {
   public function setUp() {
     parent::setUp();
-    $this->dispatch('/admin/ouvertures/index/id_site/1/multimedia/1', true);
+    $this->dispatch('/admin/ouvertures/index/id_site/1/used_for/1', true);
   }
 
 
@@ -328,9 +328,9 @@ class OuverturesControllerPostIndexActionSiteCranTest extends OuverturesControll
 class OuverturesControllerPostIndexActionSiteCranWithMultimediaParamTest extends OuverturesControllerTestCase {
   public function setUp() {
     parent::setUp();
-    $_SERVER['HTTP_REFERER'] = '/admin/ouvertures/index/id_site/1/multimedia/1';
+    $_SERVER['HTTP_REFERER'] = '/admin/ouvertures/index/id_site/1/used_for/1';
     Class_AdminVar::set('MULTIMEDIA_KEY', 'SECRET_KEY');
-    $this->postDispatch('/admin/ouvertures/index/id_site/1/multimedia/1',
+    $this->postDispatch('/admin/ouvertures/index/id_site/1/used_for/1',
                         ['closed_on_holidays' => '1']);
     Class_Bib::clearCache();
   }
@@ -338,7 +338,7 @@ class OuverturesControllerPostIndexActionSiteCranWithMultimediaParamTest extends
 
   /** @test */
   public function responseShouldRedirectToIndexIdSiteOne() {
-    $this->assertRedirectTo('/admin/ouvertures/index/id_site/1/multimedia/1');
+    $this->assertRedirectTo('/admin/ouvertures/index/id_site/1/used_for/1');
   }
 }
 
@@ -588,7 +588,7 @@ class OuverturesControllerAddOuvertureCranTest extends OuverturesControllerTestC
 class OuverturesControllerAddOuvertureCranMultimediaDisabledTest extends OuverturesControllerTestCase {
   public function setUp() {
     parent::setUp();
-    $this->dispatch('/admin/ouvertures/add/id_site/1/multimedia/1', true);
+    $this->dispatch('/admin/ouvertures/add/id_site/1/used_for/1', true);
   }
 
   /** @test */
@@ -604,7 +604,7 @@ class OuverturesControllerAddOuvertureCranMultimediaEnabledTest extends Ouvertur
     parent::setUp();
     Class_AdminVar::set('MULTIMEDIA_KEY', 'SECRET_KEY');
 
-    $this->dispatch('/admin/ouvertures/add/id_site/1/multimedia/1', true);
+    $this->dispatch('/admin/ouvertures/add/id_site/1/used_for/1', true);
   }
 
 
@@ -705,7 +705,7 @@ class OuverturesControllerPostAddOuvertureCranMultimediaEnabledTest extends Ouve
   public function setUp() {
     parent::setUp();
     Class_AdminVar::set('MULTIMEDIA_KEY', 'SECRET_KEY');
-    $this->postDispatch('/admin/ouvertures/add/multimedia/1',
+    $this->postDispatch('/admin/ouvertures/add/used_for/1',
                         ['debut_matin' => '10:30',
                          'fin_matin' => '11:30',
                          'debut_apres_midi' => '14:00',
@@ -721,13 +721,13 @@ class OuverturesControllerPostAddOuvertureCranMultimediaEnabledTest extends Ouve
 
   /** @test */
   public function responseShouldRedirectToOuverturesIndexSiteThree() {
-    $this->assertRedirectTo('/admin/ouvertures/index/id_site/3/multimedia/1');
+    $this->assertRedirectTo('/admin/ouvertures/index/id_site/3/used_for/1');
   }
 
 
   /** @test */
   public function newOuvertureShouldBeMultimedia(){
-    $this->assertEquals(1, $this->_new_ouverture->getMultimedia());
+    $this->assertTrue($this->_new_ouverture->isMultimedia());
   }
 }
 
@@ -839,6 +839,17 @@ class OuverturesControllerEditWinterTuesdayOpeningsInCranTest extends Ouvertures
   public function inputValidityEndValueShouldBe2018_01_01() {
     $this->assertXPath('//form//input[@name="validity_end"][@value="01/01/2018"]');
   }
+
+  /** @test */
+  public function pageShouldNotContainsInputTextForMaxPerPeriodMatin() {
+    $this->assertNotXPath('//input[@type="text"][@name="max_per_period_matin"]');
+  }
+
+
+/** @test */
+  public function pageShouldNotContainsInputTextForMaxPerPeriodApresMidi() {
+    $this->assertNotXPath('//input[@type="text"][@name="max_per_period_apres_midi"]');
+  }
 }
 
 
diff --git a/tests/application/modules/admin/controllers/UserGroupControllerTest.php b/tests/application/modules/admin/controllers/UserGroupControllerTest.php
index 9a1e33b9e51d9f4249558359137d069c34a3745c..348e871343d6596cb99cc2b74eeb31a154b60517 100644
--- a/tests/application/modules/admin/controllers/UserGroupControllerTest.php
+++ b/tests/application/modules/admin/controllers/UserGroupControllerTest.php
@@ -1412,6 +1412,18 @@ class Admin_UserGroupControllerEditMembersWithPaginationTest
   public function nb29UsersShouldBeDisplay() {
     $this->assertXPathContentContains('//div', '29 utilisateurs', $this->_response->getBody());
   }
+
+
+  /** @test */
+  public function tableShouldContainsLinkToDeleteUserTwo() {
+    $this->assertXPath('//table//tr//td//a[contains(@href, "admin/usergroup/editmembers/id/1/delete/2")]');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsLinkToEditUserTwo() {
+    $this->assertXPath('//table//tr//td//a[contains(@href, "admin/users/edit/id/2")]');
+  }
 }
 
 
diff --git a/tests/application/modules/opac/controllers/AbonneControllerMultimediaTest.php b/tests/application/modules/opac/controllers/AbonneControllerMultimediaTest.php
index a13440d7070195fb831b50029416c363db623167..c63a62975a177b07234ea89aab5be7bbf2db9184 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerMultimediaTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerMultimediaTest.php
@@ -209,7 +209,7 @@ abstract class AbonneControllerMultimediaHoldTestCase extends AbstractController
 
 
   protected function _beMultimediaOpening($opening) {
-    $opening->setMultimedia(1)->assertSave();
+    $opening->beMultimedia()->assertSave();
     return $opening;
   }
 
@@ -331,7 +331,7 @@ class AbonneControllerMultimediaHoldLocationTest extends AbonneControllerMultime
     parent::setUp();
 
     $lundi = Class_Ouverture::chaqueLundi('8:00', '12:00', '13:00', '18:00')
-      ->setMultimedia(1);
+      ->beMultimedia();
     $lundi->assertSave();
 
     $this->fixture('Class_Bib',
@@ -340,7 +340,7 @@ class AbonneControllerMultimediaHoldLocationTest extends AbonneControllerMultime
                     'ouvertures' => [$lundi]]);
 
     $mercredi = Class_Ouverture::chaqueMercredi('8:00', '12:00', '13:00', '18:00')
-      ->setMultimedia(1);
+      ->beMultimedia();
     $mercredi->assertSave();
 
     $this->fixture('Class_Bib',
diff --git a/tests/application/modules/opac/controllers/BibControllerTest.php b/tests/application/modules/opac/controllers/BibControllerTest.php
index 2e2cb69dfd4cf236e1c359f47b1971ef737aa003..8353e822ae7d3d1e5ff4a39b02f4ff3f4c865182 100644
--- a/tests/application/modules/opac/controllers/BibControllerTest.php
+++ b/tests/application/modules/opac/controllers/BibControllerTest.php
@@ -520,7 +520,7 @@ class BibControllerBibViewAnnecyTest extends BibControllerBibViewTestCase {
                    ['id' => 8,
                     'id_site' => 4,
                     'jour' => '2016-07-07',
-                    'multimedia' => 1]);
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA]);
 
     $this->dispatch('bib/bibview/id/4', true);
   }
@@ -631,7 +631,7 @@ class BibControllerBibViewAnnecyTest extends BibControllerBibViewTestCase {
   /** @test */
   public function openingsShouldNotContainsMultimediaDate() {
     $this->assertNotXPathContentContains('//div[contains(@class, "library_schedule")]//li',
-                                      'le jeudi 07 juillet : 10h - 18h');
+                                         'le jeudi 07 juillet : 10h - 18h');
   }
 
 
diff --git a/tests/application/modules/opac/controllers/MultimediaControllerTest.php b/tests/application/modules/opac/controllers/MultimediaControllerTest.php
index 22fa897a70ec6efea747767a3e705b9d6428a83a..7371ad0c89306c8902298192d24767d393139472 100644
--- a/tests/application/modules/opac/controllers/MultimediaControllerTest.php
+++ b/tests/application/modules/opac/controllers/MultimediaControllerTest.php
@@ -27,7 +27,7 @@ trait TMultimediaControllerAbonneFixtureHoldSuccessOnSept12 {
                                                     ['id' => 1,
                                                      'Jour' => '2012-09-12',
                                                      'horaires' => ['08:00', '12:00', '12:00', '18:00'],
-                                                     'multimedia' => 1])]]);
+                                                     'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA])]]);
 
     $this->fixture('Class_Multimedia_Device',
                    ['id' => 1,
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index c292309689335f359d03f7f04998de1d8dbc147e..0dc28b4a65bd7066df080657076a8c35cd5f0d22 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -63,7 +63,7 @@ $_SERVER['HTTP_HOST'] = 'localhost';
 
 setupOpac();
 
-ZendAfi_Controller_Action_Helper_RenderIcalArticles::ensureAutoload();
+Class_ICal_Autoloader::getInstance()->ensureAutoload();
 require_once __DIR__ . '/../library/PhpParser/lib/bootstrap.php';
 
 (new Storm_Cache())->getCache()->setOption('caching', true);
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index 2ce84cb7d498409e59145eb6dca5e7dfa243e94c..c5338f69a40f5a750c11c3d8efbc5154d162a0cf 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -3229,3 +3229,97 @@ class UpgradeDB_387_Test extends UpgradeDBTestCase {
     $this->assertIndex('notices_avis', 'source_key');
   }
 }
+
+
+
+
+class UpgradeDB_388_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    $this->dropTable('drive_checkout');
+  }
+
+
+  /** @test */
+  public function tableDriveCheckoutShouldExist() {
+    $this->assertTable('drive_checkout');
+  }
+
+
+  public function fields() {
+    return [['id', 'int(11) unsigned'],
+            ['user_id', 'int(11)'],
+            ['library_id', 'int(11)'],
+            ['start_at', 'datetime'],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider fields
+   */
+  public function fieldShouldBeOfType($field, $type) {
+    $this->assertFieldType('drive_checkout', $field, $type);
+  }
+
+
+  public function indices() {
+    return [['PRIMARY'],
+            ['user_id'],
+            ['library_id'],
+            ['start_at'],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider indices
+   */
+  public function fieldShouldBeIndexed($index) {
+    $this->assertIndex('drive_checkout', $index);
+  }
+}
+
+
+
+
+class UpgradeDB_389_Test extends UpgradeDBTestCase {
+  public function prepare(){
+    $this->silentQuery('alter table ouvertures change used_for multimedia boolean');
+    $this->silentQuery('alter table ouvertures drop index used_for');
+    $this->silentQuery('alter table bib_c_site drop column enable_drive');
+    $this->silentQuery('alter table ouvertures drop column max_per_period_matin');
+    $this->silentQuery('alter table ouvertures drop column max_per_period_apres_midi');
+  }
+
+
+  /** @test */
+  public function tableouverturesShouldContainsColumnUsedForAsInt(){
+    $this->assertFieldType('ouvertures', 'used_for', 'int(11)');
+  }
+
+
+  /** @test */
+  public function tableouverturesShouldContainsIndexUsedFor(){
+    $this->assertIndex('ouvertures', 'used_for');
+  }
+
+
+  /** @test */
+  public function tableBibCSiteShouldContainsColumnEnableDriveAsTinyInt(){
+    $this->assertFieldType('bib_c_site', 'enable_drive', 'tinyint(1)');
+  }
+
+
+  /** @test */
+  public function tableOuverturesShouldContainsColumnMaxPerPeriodMatinAsInt(){
+    $this->assertFieldType('ouvertures', 'max_per_period_matin', 'int(11)');
+  }
+
+
+  /** @test */
+  public function tableOuverturesShouldContainsColumnMaxPerPeriodApresMidiAsInt(){
+    $this->assertFieldType('ouvertures', 'max_per_period_apres_midi', 'int(11)');
+  }
+}
\ No newline at end of file
diff --git a/tests/fixtures/KohaFixtures.php b/tests/fixtures/KohaFixtures.php
index 6fac4ea0fd43d75436ef75498f1168199da9f0c7..ddc8a16b8d19ba98c748810b24f23f95d34ac21b 100644
--- a/tests/fixtures/KohaFixtures.php
+++ b/tests/fixtures/KohaFixtures.php
@@ -920,7 +920,6 @@ class KohaFixtures {
                   <lowestPriority>0</lowestPriority>
                   <title>Harry Potter et la chambre des secrets</title>
               </hold>
-
               <hold>
                   <priority>3</priority>
                   <reservenotes />
@@ -935,8 +934,12 @@ class KohaFixtures {
               </hold>
               <hold>
                 <priority>0</priority>
-                <item></item>
+                <item>
+                  <barcode>3512099938</barcode>
+                  <itemcallnumber>HH TU 3</itemcallnumber>
+                </item>
                 <reservedate>2014-05-02</reservedate>
+                <expirationdate>2014-05-17</expirationdate>
                 <timestamp>2014-05-02 15:40:35</timestamp>
                 <biblionumber>235572</biblionumber>
                 <borrowernumber>17448</borrowernumber>
diff --git a/tests/fixtures/NanookFixtures.php b/tests/fixtures/NanookFixtures.php
index 124e5e3f51e734c68eec3134402996c06085caf4..722b59f86fdebe7343d1c2c2741fb52be6a48a13 100644
--- a/tests/fixtures/NanookFixtures.php
+++ b/tests/fixtures/NanookFixtures.php
@@ -292,6 +292,20 @@ class NanookFixtures {
       <author>Charles Le Blanc</author>
       <locationLabel>Site Principal</locationLabel>
     </hold>
+    <hold>
+      <bibId>88329</bibId>
+      <itemId>88928</itemId>
+      <barcode>80072946</barcode>
+      <cote>BD BUC</cote>
+      <title>Sillage. 18, psycholocauste</title>
+      <author>Philippe Buchet</author>
+      <locationLabel>Site Principal</locationLabel>
+      <locationId>12</locationId>
+      <priority>1</priority>
+      <available>1</available>
+      <availabilityDate>15/06/2020</availabilityDate>
+      <availabilityEndDate>2020-06-30</availabilityEndDate>
+    </hold>
   </holds>
   <suggests>
     <suggest>
diff --git a/tests/library/Class/Multimedia/DeviceTest.php b/tests/library/Class/Multimedia/DeviceTest.php
index fad36552c37e2199091c50dd46f60b215f369adf..6cbeeb7e8505ccecf12a01547d36c4f7180237cc 100644
--- a/tests/library/Class/Multimedia/DeviceTest.php
+++ b/tests/library/Class/Multimedia/DeviceTest.php
@@ -210,11 +210,11 @@ class Multimedia_DeviceCurrentHoldForUserWithoutHoldAndMaxSlotsAfterCloseHoursTe
       ->save();
 
     $lundi = Class_Ouverture::chaqueLundi('08:00', '12:00', '14:00', '18:00')
-      ->setMultimedia(1);
+      ->beMultimedia();
     $lundi->assertSave();
 
     $mardi = Class_Ouverture::chaqueMardi('08:00', '12:00', '12:00', '16:00')
-      ->setMultimedia(1);
+      ->beMultimedia();
     $mardi->assertSave();
 
 
@@ -285,7 +285,7 @@ abstract class Multimedia_DeviceCurrentHoldForUserWithoutHoldAndMaxSlotsAfterNex
       ->setOuvertures([$this->fixture('Class_Ouverture',
                                       ['id' => 5,
                                        'jour_semaine' => date('w'),
-                                       'multimedia' => 1,
+                                       'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                                        'horaires' => ['08:00', '12:00', '14:00', '23:00']])])
       ->assertSave();
 
@@ -384,7 +384,7 @@ class Multimedia_DeviceCreateHoldTest extends ModelTestCase {
                             'password' => 'jcupwqjms']);
 
     $jeudi = Class_Ouverture::chaqueJeudi('08:00', '12:00', '14:00', '18:00')
-      ->setMultimedia(1);
+      ->beMultimedia();
     $jeudi->assertSave();
 
     $this->fixture('Class_Bib',
diff --git a/tests/library/Class/Multimedia/LocationTest.php b/tests/library/Class/Multimedia/LocationTest.php
index d5d0e625326714c6cbc9e777b52813d239298dc6..88e7de94c7c8fa55ff12a1e402f0bf00d1022ee7 100644
--- a/tests/library/Class/Multimedia/LocationTest.php
+++ b/tests/library/Class/Multimedia/LocationTest.php
@@ -35,14 +35,14 @@ abstract class Multimedia_LocationTestCase extends ModelTestCase {
                    ['id' => 3,
                     'bib' => $antibe,
                     'jour_semaine' => Class_Ouverture::MERCREDI,
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['08:30', '12:00', '12:00', '17:45']]);
 
     $this->fixture('Class_Ouverture',
                    ['id' => 4,
                     'bib' => $antibe,
                     'jour_semaine' => Class_Ouverture::JEUDI,
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['10:00', '12:00', '14:00', '19:00']]);
 
     $this->fixture('Class_Ouverture',
@@ -52,7 +52,7 @@ abstract class Multimedia_LocationTestCase extends ModelTestCase {
                     'validity_start' => '2016-10-01',
                     'validity_end' => '2017-03-01',
                     'jour_semaine' => Class_Ouverture::JEUDI,
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['00:00', '00:00', '14:00', '15:00']]);
 
     $this->fixture('Class_Ouverture',
@@ -62,21 +62,21 @@ abstract class Multimedia_LocationTestCase extends ModelTestCase {
                     'validity_start' => '2016-10-01',
                     'validity_end' => '2017-03-01',
                     'jour_semaine' => Class_Ouverture::MARDI,
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['00:00', '00:00', '14:00', '15:00']]);
 
     $this->fixture('Class_Ouverture',
                    ['id' => 5,
                     'bib' => $antibe,
                     'jour' => '2012-09-19',
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['09:00', '12:00', '12:00', '18:00']]);
 
     $this->fixture('Class_Ouverture',
                    ['id' => 15,
                     'bib' => $antibe,
                     'jour' => '2012-09-09',
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['09:00', '12:00', '12:00', '18:00']]);
 
     // closed on september 20
@@ -84,7 +84,7 @@ abstract class Multimedia_LocationTestCase extends ModelTestCase {
                    ['id' => 16,
                     'bib' => $antibe,
                     'jour' => '2012-09-20',
-                    'multimedia' => 1,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
                     'horaires' => ['00:00', '00:00', '00:00', '00:00']]);
 
 
diff --git a/tests/library/Class/WebService/SIGB/KohaTest.php b/tests/library/Class/WebService/SIGB/KohaTest.php
index 20f8fc948d4b3fe4310ff41e53e6f5f73b68999d..e6c78064481ef49c012d9c3f564d47ab8f804652 100644
--- a/tests/library/Class/WebService/SIGB/KohaTest.php
+++ b/tests/library/Class/WebService/SIGB/KohaTest.php
@@ -461,9 +461,12 @@ class KohaGetEmprunteurLaureAfondTest extends KohaTestCase {
                                          'libelle' => 'Montmedy',
                                          'id_origine' => 'MON']);
 
+    $this->fixture('Class_Bib', ['id' => 88]);
+
     $this->fixture('Class_CodifAnnexe', ['id' => 35,
                                          'libelle' => 'Médiathèque publique',
-                                         'id_origine' => 'MPU']);
+                                         'id_origine' => 'MPU',
+                                         'id_bib' => 88]);
 
     $this->mock_web_client
       ->whenCalled('postData')
@@ -536,28 +539,56 @@ class KohaGetEmprunteurLaureAfondTest extends KohaTestCase {
 
 
   /** @test */
-  public function firstHoldWaitingToBePulledPickUpLocationShouldBeMontmedy() {
+  public function firstHoldWaitingToBePulledPickUpLocationShouldBeMediathequePublique() {
     $waiting_holds = $this->laurent->getHoldsWaitingToBePulled();
     $this->assertEquals('Médiathèque publique', $waiting_holds[0]->getPickupLocationLabel());
   }
 
 
   /** @test */
-  public function secondHoldWaitingToBePulledTitleShouldBeHarryPotterEtLePrisonnierDAzkaban() {
+  public function firstHoldWaitingToBePulledLocationIdShouldBe88() {
+    $waiting_holds = $this->laurent->getHoldsWaitingToBePulled();
+    $this->assertEquals(88, $waiting_holds[0]->getLocationId());
+  }
+
+
+  /** @test */
+  public function firstHoldWaitingToBePulledBarcodeShouldBe3512099938() {
+    $this->assertEquals('3512099938',
+                        $this->laurent->getHoldsWaitingToBePulled()[0]->getCodeBarre());
+  }
+
+
+  /** @test */
+  public function firstHoldWaitingToBePulledCoteShouldBeHH_TU_3() {
+    $this->assertEquals('HH TU 3',
+                        $this->laurent->getHoldsWaitingToBePulled()[0]->getCote());
+  }
+
+
+  /** @test */
+  public function firstHoldWaitingToBePulledAvailabilityEndDateShouldBe2014_05_17() {
+    $this->assertEquals('2014-05-17',
+                        $this->laurent->getHoldsWaitingToBePulled()[0]->getAvailabilityEndDate());
+  }
+
+
+  /** @test */
+  public function secondHoldWaitingToBePulledTitleShouldBeHarryPotterEtLOrdreDuPhenix() {
     $waiting_holds = $this->laurent->getHoldsWaitingToBePulled();
     $this->assertEquals('Harry Potter et l\'ordre du Phénix', $waiting_holds[1]->getTitre());
   }
 
 
   /** @test */
-  public function secondHoldWaitingToBePulledPickUpLocationShouldBeMediathequePublique() {
+  public function secondHoldWaitingToBePulledPickUpLocationShouldBeMontmedy() {
     $waiting_holds = $this->laurent->getHoldsWaitingToBePulled();
     $this->assertEquals('Montmedy', $waiting_holds[1]->getPickupLocationLabel());
   }
 
 
   /** @test */
-  public function nbReservationsShouldReturnThree() {
+  public function nbReservationsShouldReturnFour() {
     $this->assertEquals(4, $this->laurent->getNbReservations());
   }
 
diff --git a/tests/library/Class/WebService/SIGB/NanookTest.php b/tests/library/Class/WebService/SIGB/NanookTest.php
index 5b7901831f9f5cdf651b3c6f47d6b4ad36d70b76..678e6789eed405d08e742f325dda1e1113e69bd7 100644
--- a/tests/library/Class/WebService/SIGB/NanookTest.php
+++ b/tests/library/Class/WebService/SIGB/NanookTest.php
@@ -678,14 +678,14 @@ class NanookGetEmprunteurChristelDelpeyrouxTest extends NanookTestCase {
 
 
   /** @test */
-  public function nbReservationShouldBeThree() {
-    $this->assertEquals(3, $this->_emprunteur->getNbReservations());
+  public function nbReservationShouldBeFour() {
+    $this->assertEquals(4, $this->_emprunteur->getNbReservations());
   }
 
 
   /** @test */
-  public function nbWaitingToBePulledShouldBeOne() {
-    $this->assertCount(1, $this->_emprunteur->getHoldsWaitingToBePulled());
+  public function nbWaitingToBePulledShouldBeTwo() {
+    $this->assertCount(2, $this->_emprunteur->getHoldsWaitingToBePulled());
   }
 
 
@@ -765,6 +765,37 @@ class NanookGetEmprunteurChristelDelpeyrouxTest extends NanookTestCase {
   public function secondReservationEtatShouldBeDisponible() {
     $this->assertEquals('Disponible', $this->_emprunteur->getReservationAt(1)->getEtat());
   }
+
+
+  /** @test */
+  public function fourthHoldShouldBeWaitingToBePulled() {
+    $this->assertTrue($this->_emprunteur->getReservationAt(3)->isWaitingToBePulled());
+  }
+
+
+  /** @test */
+  public function fourthHoldAvailabilityEndDateShouldBe2020_06_30() {
+    $this->assertEquals('2020-06-30',
+                        $this->_emprunteur->getReservationAt(3)->getAvailabilityEndDate());
+  }
+
+
+  /** @test */
+  public function fourthHoldLocationIdShouldBe12() {
+    $this->assertEquals('12', $this->_emprunteur->getReservationAt(3)->getLocationId());
+  }
+
+
+  /** @test */
+  public function fourthHoldBarcodeShouldBe80072946() {
+    $this->assertEquals('80072946', $this->_emprunteur->getReservationAt(3)->getCodeBarre());
+  }
+
+
+  /** @test */
+  public function fourthHoldCodeShouldBD_BUC() {
+    $this->assertEquals('BD BUC', $this->_emprunteur->getReservationAt(3)->getCote());
+  }
 }
 
 
diff --git a/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php b/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php
index 9a192100f492899b52656ca24c292df063b6e2e5..746e051bebbcb2055e4d45db21917c4436d95e6b 100644
--- a/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php
+++ b/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php
@@ -30,8 +30,10 @@ abstract class ZendAfi_View_Helper_RenderLibraryOpeningTestCase extends ViewHelp
     $this->annecy = $this->fixture('Class_Bib',
                                    ['id' => 1,
                                     'libelle' => 'Annecy',
-                                    'ouvertures' => [
-                                                     Class_Ouverture::chaqueLundi('10:00', '12:00', '14:00', '18:00')]]);
+                                    'enable_drive' => true,
+                                    'ouvertures' => [Class_Ouverture::chaqueLundi('08:00','13:00','14:00','17:00')->beDrive(),
+                                                     Class_Ouverture::chaqueLundi('10:00', '12:00', '14:00', '18:00')
+                                                     ]]);
 
     $this->cran = $this->fixture('Class_Bib',
                                  ['id' => 2,
@@ -43,7 +45,11 @@ abstract class ZendAfi_View_Helper_RenderLibraryOpeningTestCase extends ViewHelp
 
     $this->seynod = $this->fixture('Class_Bib',
                                    ['id' => 3,
-                                    'libelle' => 'Seynod']);
+                                    'libelle' => 'Seynod',
+                                    'ouvertures' => [Class_Ouverture::chaqueLundi('08:00','13:00','14:00','17:00')->beDrive(),
+                                                     Class_Ouverture::chaqueLundi('08:00','15:00','18:00','20:00')->beMultimedia()
+                                    ]
+                                   ]);
 
     $this->meythet = $this->fixture('Class_Bib',
                                     ['id' => 4,
@@ -400,8 +406,8 @@ class ZendAfi_View_Helper_RenderLibraryOpeningsOnValidityRangeTest
 
 
   /** @test */
-  public function ouverturesOrderedIdShouldBe348_349_350_351_2() {
-    $this->assertEquals([348, 349 , 350, 351, 2],
+  public function ouverturesOrderedIdShouldBe348_349_350_351_3() {
+    $this->assertEquals([348, 349 , 350, 351, 3],
                         (new Storm_Model_Collection($this->cran->getOuvertures()))
                         ->collect('id')
                         ->getArrayCopy());
diff --git a/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php b/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef9f3018744a98070a76e507a9ed23dd49e2dde6
--- /dev/null
+++ b/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php
@@ -0,0 +1,842 @@
+<?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 DriveCheckOutBookingNotActiveTest extends AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+
+
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->dispatch('/opac/drive-checkout/plan');
+  }
+
+
+  /** @test */
+  public function shouldRedirectToHome() {
+    $this->assertRedirectTo('/');
+  }
+
+
+  /** @test */
+  public function notificationShouldContainsNotActiveMessage() {
+    $this->assertFlashMessengerContentContains('La planification du retrait des réservations est désactivée.');
+  }
+}
+
+
+
+
+abstract class DriveCheckOutBookingTestCase extends AbstractControllerTestCase {
+  protected
+    $_storm_default_to_volatile = true,
+    $_marcus,
+    $_mail_transport;
+
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 1);
+
+    $timesource = new TimeSourceForTest('2020-05-05 11:30');
+    Class_DriveCheckout_Plan::setTimeSource($timesource);
+
+    $this->_mail_transport = new MockMailTransport();
+    Zend_Mail::setDefaultTransport($this->_mail_transport);
+
+    $this->_setupProfile()
+         ->_setupLibraries()
+         ->_setupUser();
+  }
+
+
+  public function tearDown() {
+    Class_DriveCheckout_Plan::setTimeSource(null);
+    Class_Systeme_ModulesAccueil::reset();
+    parent::tearDown();
+  }
+
+
+  protected function _setupProfile() {
+    Class_Systeme_ModulesAccueil::reset();
+    Class_AdminVar::set('TEMPLATING', 1);
+
+    $profile = $this->fixture('Class_Profil',
+                              ['id' => 23,
+                               'mail_site' => 'zemail@mabib.st']);
+
+    $template = new Intonation_Template;
+    $template->applyOn($profile);
+    (new Class_Profil_Promoter())->promote($profile);
+
+    return $this;
+  }
+
+
+  protected function _setupLibraries() {
+    $lib_hotel_dieu = $this
+      ->fixture('Class_Bib',
+                ['id' => 1,
+                 'libelle' => 'Hotel-Dieu',
+                 'closed_on_holidays' => false,
+                 'enable_drive' => 1,
+                 'ouvertures' => [
+                                  Class_Ouverture::chaqueLundi('00:00', '00:00', '12:00', '18:00')
+                                  ->beDrive(),
+                                  Class_Ouverture::chaqueMardi('09:00', '12:00', '14:00', '18:00')
+                                  ->beDrive(),
+                                  Class_Ouverture::chaqueMercredi('00:00', '00:00', '12:00', '17:00')
+                                  ->beDrive(),
+                 ]]);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 2,
+                    'enable_drive' => 1,
+                    'libelle' => 'Albert Camus']);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 3,
+                    'enable_drive' => 1,
+                    'libelle' => 'Mauricette-Rafin']);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 4,
+                    'enable_drive' => 0,
+                    'libelle' => 'Le Turbomoteur']);
+
+    return $this;
+  }
+
+
+  protected function _setupUser() {
+    $this->_marcus = $this->fixture('Class_Users',
+                                    ['id' => 10,
+                                     'login' => 'MC',
+                                     'password' => 'US',
+                                     'prenom' => 'Marcus',
+                                     'nom' => 'Miller',
+                                     'pseudo' => '',
+                                     'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                                     'idabon' => 'mc123',
+                                     'id_site' => 1,
+                                     'mail' => 'mm@any-serveur.eu']);
+
+    $this
+      ->_marcus
+      ->setFicheSigb(['type_comm' => Class_IntBib::COM_NANOOK,
+                      'fiche' => (new Class_WebService_SIGB_Emprunteur('10', 'Marcus'))
+                      ->reservationsAddAll(
+                                           [$this->_holdIn(1)->setAvailabilityEndDate('2020-05-12'),
+                                            $this->_holdIn(1)->setAvailabilityEndDate('2020-05-13'),
+                                            $this->_holdIn(2),
+                                            $this->_holdIn(4)])]);
+
+
+    ZendAfi_Auth::getInstance()->logUser($this->_marcus);
+    return $this;
+  }
+
+
+  protected function _holdIn($library) {
+    return Class_WebService_SIGB_Reservation::newInstanceWithEmptyExemplaire()
+      ->setLocationId($library)
+      ->setWaitingToBePulled();
+  }
+}
+
+
+
+
+class DriveCheckOutPatronHoldsTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->fixture('Class_Profil',
+                   ['id' => 23,
+                    'libelle' => 'vintage']);
+  }
+
+
+  /** @test */
+  public function pageWithBootstrapShouldContainsLinkToDriveCheckoutPlan() {
+    $this->dispatch('/abonne/reservations/id_profil/1');
+    $this->assertXPathContentContains('//a[contains(@href, "/drive-checkout/plan")]',
+                                      'Planifier le retrait de mes documents');
+  }
+
+
+  /** @test */
+  public function pageWithHistoricProfileShouldContainsLinkToDriveCheckoutPlan() {
+    $this->dispatch('/abonne/reservations/id_profil/23');
+    $this->assertXPathContentContains('//a[contains(@href, "/drive-checkout/plan")]',
+                                      'Planifier le retrait de mes documents');
+  }
+
+
+  /** @test */
+  public function disabledDrivePageWithBootstrapShouldNotContainsLinkToDriveCheckoutPlan() {
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->dispatch('/abonne/reservations/id_profil/1');
+    $this->assertNotXPath('//a[contains(@href, "/drive-checkout/plan")]');
+  }
+
+
+  /** @test */
+  public function disabledDrivePageWithHistoricProfileShouldNotContainsLinkToDriveCheckoutPlan() {
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->dispatch('/abonne/reservations/id_profil/23');
+    $this->assertNotXPath('//a[contains(@href, "/drive-checkout/plan")]');
+  }
+}
+
+
+
+
+class DriveCheckOutUserNotificationsTest extends DriveCheckOutBookingTestCase {
+  protected $_actions;
+
+  public function notify($message, $actions = []) {
+    if (!isset($actions['actions']))
+      return $this;
+    $this->_actions []= $actions['actions'];
+  }
+
+
+  /** @test */
+  public function marcusNotificationActionsShouldContainsLinkToDriveCheckoutPlan() {
+    $this->_marcus->registerNotificationsOn($this);
+    $url = Class_Url::assemble(['module' => 'opac',
+                                'controller' => 'drive-checkout',
+                                'action' => 'plan']);
+    $this->assertEquals([$url => 'Planifier le retrait de mes documents'],
+                          $this->_actions[0]);
+  }
+
+
+  /** @test */
+  public function withDisabledDriveMarcusNotificationActionsShouldNotContainsLinkToDriveCheckoutPlan() {
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->_marcus->registerNotificationsOn($this);
+    $this->assertEmpty($this->_actions);
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/opac/drive-checkout/plan');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBePlanifierLeRetraitDeMesDocuments() {
+    $this->assertXPathContentContains('//title', 'Planifier le retrait de mes documents');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToChooseAlbertCamus() {
+    $this->assertXPath('//a[contains(@href, "/plan/id_bib/2")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsCardForAlbertCamus() {
+    $this->assertXPathContentContains('//div[@class="card-title"]', 'Albert Camus');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsBadge2Holds() {
+    $this->assertXPathContentContains('//span[contains(@class, "badge-primary")]',
+                                      '2 réservations');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsBadge2WaitingToBePulled() {
+    $this->assertXPathContentContains('//span[contains(@class, "badge-success")]',
+                                      '2 à retirer');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsBadgeNoneNotReady() {
+    $this->assertXPathContentContains('//span[contains(@class, "badge-secondary")]',
+                                      '0 en attente');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToChooseHotelDieu() {
+    $this->assertXPath('//a[contains(@href, "/plan/id_bib/1")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsCardForHotelDieu() {
+    $this->assertXPathContentContains('//div[@class="card-title"]', 'Hotel-Dieu');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToChooseMauricette() {
+    $this->assertNotXPath('//a[contains(@href, "/plan/id_bib/3")]');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToChooseLeTurbomoteur() {
+    $this->assertNotXPath('//a[contains(@href, "/plan/id_bib/4")]');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanWithFuturExistingTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->onLoaderOfModel('Class_DriveCheckout')
+         ->whenCalled('findFuturefor')->with(Class_Bib::find(2), $this->_marcus)
+         ->answers($this->fixture('Class_DriveCheckout',
+                                  ['id' => 2,
+                                   'library_id' => 2,
+                                   'user_id' => $this->_marcus->getId(),
+                                   'start_at' => '2020-05-12 09:00:00']));
+
+    $this->dispatch('/opac/drive-checkout/plan');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToChooseAlbertCamus() {
+    $this->assertNotXPath('//a[contains(@href, "/plan/id_bib/2")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsAlreadyPlannedCheckout() {
+    $this->assertXPathContentContains('//p',
+                                      'Retrait planifié le mardi 12 mai à 09h00');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToDeletePlannedCheckout() {
+    $this->assertXPathContentContains('//a[contains(@href, "/drive-checkout/delete/id/2")]',
+                                      'Supprimer');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToDownloadPlannedCheckout() {
+    $this->assertXPathContentContains('//a[contains(@href, "/drive-checkout/ical/id/2")]',
+                                      'Ajouter à mon agenda');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanDeleteExistingTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 2,
+                    'library_id' => 2,
+                    'user_id' => $this->_marcus->getId(),
+                    'start_at' => '2020-05-12 09:00:00']);
+
+    $this->dispatch('/opac/drive-checkout/delete/id/2');
+  }
+
+
+  /** @test */
+  public function existingShouldBeDeleted() {
+    $this->assertNull(Class_DriveCheckout::find(2));
+  }
+
+
+  /** @test */
+  public function notificationShouldContainsDeletionDetails() {
+    $this->assertFlashMessengerContentContains('Le retrait de vos documents de Albert Camus planifié pour le mardi 12 mai à 09h00 a été supprimé.');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanDownloadExistingTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 2,
+                    'library_id' => 2,
+                    'user_id' => $this->_marcus->getId(),
+                    'start_at' => '2020-05-12 09:00:00']);
+
+    Class_Bib::find(2)->setLieu($this->fixture('Class_Lieu',
+                                               ['id' => 233,
+                                                'libelle' => 'Albert Camus',
+                                                'latitude' => '-38.812239',
+                                                'longitude' => '177.137570']));
+
+    $this->dispatch('/opac/drive-checkout/ical/id/2');
+  }
+
+
+  /** @test */
+  public function contentTypeShouldBeTextCalendar() {
+    $this->assertHeaderContains('Content-Type', 'text/calendar;charset=utf-8');
+  }
+
+
+  /** @test */
+  public function contentDispositionShouldBeAttachment() {
+    $this->assertHeaderContains('Content-Disposition', 'attachment;filename="calendar.ics');
+  }
+
+
+  /** @test */
+  public function uidShouldBeDriveCheckout2() {
+    $this->assertContains('drive-checkout:2', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function dateTimeShouldBe20200512T090000WithTimezone() {
+    $this->assertContains('DTSTART;TZID=Europe/Paris:20200512T090000', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function urlToPlanInCurrentProfilShouldBePresent() {
+    $this->assertContains('/drive-checkout/plan/id_profil/1', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function summaryShouldBeRetraitDesReservations() {
+    $this->assertContains('SUMMARY:Retrait des réservations à Albert Camus',
+                          $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function locationShouldBeAlbertCamus() {
+    $this->assertContains('LOCATION:Albert Camus', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function geoShouldBeLat38Long177() {
+    $this->assertContains('GEO:-38.812239;177.137570', $this->_response->getBody());
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/opac/drive-checkout/plan/id_bib/1');
+  }
+
+
+  /** @test */
+  public function selectedLibrayShouldBeHotelDieu() {
+    $this->assertXPathContentContains('//div[@class="card-title"]', 'Hotel-Dieu');
+  }
+
+
+  /** @test */
+  public function linkToModifySelectedLibraryShouldBePresent() {
+    $this->assertXPathContentContains('//a[not(contains(@href, "/id_bib"))][contains(@href, "/plan")]',
+                                      'Choisir un autre site');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToChoose2020_05_12() {
+    $this->assertXPathContentContains('//a[contains(@href, "/checkout_date/2020-05-12")]',
+                                      'le mardi 12 mai');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToChoosePossibleButAfterMaxHold() {
+    $this->assertNotXPath('//a[contains(@href, "/checkout_date/2020-05-13")]');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToChooseNotOpened() {
+    $this->assertNotXPath('//a[contains(@href, "/checkout_date/2020-05-16")]');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuQuotaFullOn2020_05_12Test
+  extends DriveCheckOutBookingTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->onLoaderOfModel('Class_DriveCheckout')
+         ->whenCalled('countBetweenForLibrary')
+         ->willDo(function($start, $end, $library)
+                  {
+                    return '2020-05-12' == $start->format('Y-m-d') ? 600 : 0;
+                  });
+
+    $this->dispatch('/opac/drive-checkout/plan/id_bib/1');
+  }
+
+
+  /** @test */
+  public function selectedLibrayShouldBeHotelDieu() {
+    $this->assertXPathContentContains('//div[@class="card-title"]', 'Hotel-Dieu');
+  }
+
+
+  /** @test */
+  public function pageShouldContainslinkToModifySelectedLibrary() {
+    $this->assertXPathContentContains('//a[not(contains(@href, "/id_bib"))][contains(@href, "/plan")]',
+                                      'Choisir un autre site');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToChooseFull2020_05_12() {
+    $this->assertNotXPath('//a[contains(@href, "/checkout_date/2020-05-12")]');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuAt2020_05_16Test extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/opac/drive-checkout/plan/id_bib/1/checkout_date/2020-05-16');
+  }
+
+
+  /** @test */
+  public function shouldRedirectToHotelDieuSelected() {
+    $this->assertRedirectTo('/drive-checkout/plan/id_bib/1');
+  }
+
+
+  /** @test */
+  public function notificationShouldContainsUnavailableDate() {
+    $this->assertFlashMessengerContentContains('La date choisie n\'est pas disponible');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuAt2020_05_12Test extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/opac/drive-checkout/plan/id_bib/1/checkout_date/2020-05-12');
+  }
+
+
+  /** @test */
+  public function selectedShouldBeHotelDieu() {
+    $this->assertXPathContentContains('//div[@class="card-title"]', 'Hotel-Dieu');
+  }
+
+
+  /** @test */
+  public function selectedDateShouldBeMardi12Mai() {
+    $this->assertXPathContentContains('//li', 'le mardi 12 mai');
+  }
+
+
+  /** @test */
+  public function linkToModifySelectedDateShouldBePresent() {
+    $this->assertXPathContentContains('//a[not(contains(@href, "/checkout_date"))][contains(@href, "/plan/id_bib/1")]',
+                                      'Choisir une autre date');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsFormSelectToChooseTimes() {
+    $this->assertXPathContentContains('//form//select[@name="checkout_time"]//option[@value="09:00"]',
+                                      '09h00');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuAt2020_05_12QuotaFullAt09_00Test
+  extends DriveCheckOutBookingTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 1,
+                    'user_id' => 8987383,
+                    'library' => Class_Bib::find(1),
+                    'start_at' => '2020-05-12 09:00:00']);
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 2,
+                    'user_id' => 9987374,
+                    'library' => Class_Bib::find(1),
+                    'start_at' => '2020-05-12 14:00:00']);
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 3,
+                    'user_id' => 897890,
+                    'library' => Class_Bib::find(1),
+                    'start_at' => '2020-05-12 09:00:00']);
+
+    $this->dispatch('/opac/drive-checkout/plan/id_bib/1/checkout_date/2020-05-12');
+  }
+
+
+  /** @test */
+  public function selectedShouldBeHotelDieu() {
+    $this->assertXPathContentContains('//div[@class="card-title"]', 'Hotel-Dieu');
+  }
+
+
+  /** @test */
+  public function selectedDateShouldBeMardi12Mai() {
+    $this->assertXPathContentContains('//li', 'le mardi 12 mai');
+  }
+
+
+  /** @test */
+  public function linkToModifySelectedDateShouldBePresent() {
+    $this->assertXPathContentContains('//a[not(contains(@href, "/checkout_date"))][contains(@href, "/plan/id_bib/1")]',
+                                      'Choisir une autre date');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsFormSelectToChooseTimes() {
+    $this->assertXPath('//form//select[@name="checkout_time"]');
+  }
+
+
+  /** @test */
+  public function selectToChooseTimeShouldNotContains09_00() {
+    $this->assertNotXPath('//form//select[@name="checkout_time"]//option[@value="09:00"]');
+  }
+
+
+  /** @test */
+  public function selectToChooseTimeShouldContains09_30() {
+    $this->assertXPath('//form//select[@name="checkout_time"]//option[@value="09:30"]');
+  }
+
+
+  /** @test */
+  public function selectToChooseTimeShouldNotContains14_00() {
+    $this->assertNotXPath('//form//select[@name="checkout_time"]//option[@value="14:00"]');
+  }
+
+
+  /** @test */
+  public function selectToChooseTimeShouldContains14_30() {
+    $this->assertXPath('//form//select[@name="checkout_time"]//option[@value="14:30"]');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuAt2020_05_12_09_00Test
+  extends DriveCheckOutBookingTestCase {
+  protected $_mail;
+
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/opac/drive-checkout/plan/id_bib/1/checkout_date/2020-05-12',
+                        ['checkout_time' => '09:00']);
+    $this->_mail = $this->_mail_transport->sent_mail;
+  }
+
+
+  /** @test */
+  public function checkoutShouldBePlanned() {
+    $this->assertNotNull(Class_DriveCheckout::findFirstBy(['user_id' => $this->_marcus->getId(),
+                                                           'library_id' => 1,
+                                                           'start_at' => '2020-05-12 09:00:00']));
+  }
+
+
+  /** @test */
+  public function shouldRedirectToConfirm() {
+    $this->assertRedirectContains('/drive-checkout/plan');
+  }
+
+
+  /** @test */
+  public function notificationShouldContainsRendezVousDetails() {
+    $this->assertFlashMessengerContentContains('Le retrait de vos documents de Hotel-Dieu est planifié pour le mardi 12 mai à 09h00.');
+  }
+
+
+  /** @test */
+  public function mailShouldBeSent() {
+    $this->assertNotNull($this->_mail);
+  }
+
+
+  /** @test */
+  public function mailSubjectShouldBeConfirmationDeRdv() {
+    $this->assertContains('Confirmation de rendez-vous à Hotel-Dieu',
+                          quoted_printable_decode($this->_mail->getSubject()));
+  }
+
+
+  /** @test */
+  public function mailBodyShouldContainsCheckoutDetails() {
+    $this->assertContains('le mardi 12 mai à 09h00',
+                          quoted_printable_decode($this->_mail->getBodyHtml(true)));
+  }
+
+
+  /** @test */
+  public function mailFirstRecipientShouldBeMarus() {
+    $this->assertEquals('mm@any-serveur.eu', $this->_mail->getRecipients()[0]);
+  }
+
+
+  /** @test */
+  public function mailSiteShouldBeInBcc() {
+    $this->assertContains('zemail@mabib.st', $this->_mail->getHeaders()['Bcc']);
+  }
+
+
+  /** @test */
+  public function mailFromShouldBeMailSite() {
+    $this->assertEquals('zemail@mabib.st', $this->_mail->getFrom());
+  }
+
+
+  /** @test */
+  public function mailShouldHaveAttachment() {
+    $this->assertTrue($this->_mail->hasAttachments);
+  }
+
+
+  /** @test */
+  public function mailShouldHaveCalendarIcsPart() {
+    $part = (new Storm_Collection($this->_mail->getParts()))
+      ->detect(function($each)
+               {
+                 return Class_ICal_DriveCheckout::MIME_TYPE === $each->type
+                   && 'calendar.ics' === $each->filename;
+               });
+
+    $this->assertNotNull($part);
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanBibHotelDieuAt2020_05_12_05_00Test
+  extends DriveCheckOutBookingTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/opac/drive-checkout/plan/id_bib/1/checkout_date/2020-05-12',
+                        ['checkout_time' => '05:00']);
+  }
+
+
+  /** @test */
+  public function checkoutShouldNotBePlanned() {
+    $this->assertNull(Class_DriveCheckout::findFirstBy(['user_id' => $this->_marcus->getId(),
+                                                        'library_id' => 1,
+                                                        'start_at' => '2020-05-12 05:00:00']));
+  }
+
+
+  /** @test */
+  public function shouldRedirectToTimeSelection() {
+    $this->assertRedirectTo('/drive-checkout/plan/id_bib/1/checkout_date/2020-05-12');
+  }
+
+
+  /** @test */
+  public function notificationShouldContainsUnavailableTime() {
+    $this->assertFlashMessengerContentContains('L\'horaire choisi n\'est pas disponible');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanNotLoggedTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+    ZendAfi_Auth::getInstance()->clearIdentity();
+    $this->dispatch('/opac/drive-checkout/plan');
+  }
+
+
+  /** @test */
+  public function pageShouldContainLoginForm() {
+    $this->assertXPath('//div[@class="contenu"]//form[contains(@action, "/auth/login")]');
+  }
+}
+
+
+
+
+class DriveCheckOutBookingPlanWithoutHoldsTest extends DriveCheckOutBookingTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this
+      ->_marcus
+      ->setFicheSigb(['type_comm' => Class_IntBib::COM_NANOOK,
+                      'fiche' => (new Class_WebService_SIGB_Emprunteur('10', 'Marcus'))]);
+
+    $this->dispatch('/opac/drive-checkout/plan');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsNoHoldMessage() {
+    $this->assertXPathContentContains('//div//p', 'Aucun document à retirer pour l\'instant');
+  }
+}
\ No newline at end of file
diff --git a/tests/scenarios/DriveCheckOut/DriveCheckoutAdminControllerTest.php b/tests/scenarios/DriveCheckOut/DriveCheckoutAdminControllerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a19005ead070fff82c2afee64f2c795334e21cc1
--- /dev/null
+++ b/tests/scenarios/DriveCheckOut/DriveCheckoutAdminControllerTest.php
@@ -0,0 +1,617 @@
+<?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
+ */
+
+
+require_once('application/modules/admin/controllers/DriveCheckoutController.php');
+
+abstract class DriveCheckOutAdminControllerTestCase extends Admin_AbstractControllerTestCase {
+  protected
+    $_storm_default_to_volatile = true,
+    $_mail_transport;
+
+  public function setUp() {
+    parent::setUp();
+
+    $timesource = new TimeSourceForTest('2020-05-12 10:00:00');
+    Admin_DriveCheckoutController::setTimeSource($timesource);
+    Class_DriveCheckout_Plan::setTimeSource($timesource);
+
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 1);
+
+    $this->_mail_transport = new MockMailTransport();
+    Zend_Mail::setDefaultTransport($this->_mail_transport);
+
+    $lib_hotel_dieu = $this->fixture('Class_Bib',
+                                     ['id' => 1,
+                                      'libelle' => 'Hotel-Dieu',
+                                      'enable_drive' => true,
+                                      'ouvertures' => [
+                                                       Class_Ouverture::chaqueLundi('00:00', '00:00', '12:00', '18:00')
+                                                       ->beDrive(),
+                                                       Class_Ouverture::chaqueMardi('09:00', '12:00', '14:00', '18:00')
+                                                       ->beDrive()]]);
+
+    $lib_camus = $this->fixture('Class_Bib',
+                                ['id' => 2,
+                                 'libelle' => 'Albert Camus',
+                                 'enable_drive' => true]);
+
+    $lib_maurissette = $this->fixture('Class_Bib',
+                                      ['id' => 3,
+                                       'libelle' => 'Maurissette',
+                                       'enable_drive' => false]);
+
+
+    $emilie = $this->fixture('Class_Users',
+                             ['id' => 2,
+                              'login' => 'emilie',
+                              'password' => 'secret',
+                              'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                              'idabon' => 'A121',
+                              'bib' => $lib_hotel_dieu]);
+
+    $bernard = $this->fixture('Class_Users',
+                              ['id' => 3,
+                               'login' => 'bernard',
+                               'password' => 'secret',
+                               'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                               'idabon' => 'A123',
+                               'bib' => $lib_hotel_dieu]);
+
+    $maurice = $this->fixture('Class_Users',
+                              ['id' => 4,
+                               'login' => 'maurice',
+                               'password' => 'secret',
+                               'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                               'idabon' => 'A124',
+                               'bib' => $lib_hotel_dieu])
+                    ->setFicheSigb(['type_comm' => Class_IntBib::COM_NANOOK,
+                                    'fiche' => (new Class_WebService_SIGB_Emprunteur(4, 'Maurice'))
+                                    ->reservationsAddAll(
+                                                         [Class_WebService_SIGB_Reservation::newInstanceWithEmptyExemplaire()
+                                                          ->setExemplaireOPAC($this->fixture('Class_Exemplaire',
+                                                                                             ['id' => 2,
+                                                                                              'code_barres' => 'tintin123',
+                                                                                              'notice' => $this->fixture('Class_Notice',
+                                                                                                                         ['id' => 123,
+                                                                                                                          'titre_principal' => 'Tintin à Dole']),
+                                                                                              'cote' => 'BD2']))
+                                                          ->setBibliotheque($lib_camus->getLibelle())
+                                                          ->setCodeBarre('tintin123')
+                                                          ->setTitre('Tintin à Dole')
+                                                          ->setEtat('On le cherche')
+                                                          ->setWaitingToBePulled(),
+
+                                                          Class_WebService_SIGB_Reservation::newInstanceWithEmptyExemplaire()
+                                                          ->setBibliotheque($lib_camus->getLibelle())
+                                                          ->setCodeBarre('milou123')
+                                                          ->setTitre('Milou à Dole')
+                                                          ->setWaitingToBePulled(),
+
+                                                          Class_WebService_SIGB_Reservation::newInstanceWithEmptyExemplaire()
+                                                          ->setBibliotheque($lib_maurissette->getLibelle())
+                                                          ->setCodeBarre('tournesol123')
+                                                          ->setEtat('Disponible')
+                                                          ->setTitre('Tournesol à Maurissette')
+                                                          ->setWaitingToBePulled(),
+
+                                                          Class_WebService_SIGB_Reservation::newInstanceWithEmptyExemplaire()
+                                                          ->setBibliotheque($lib_camus->getLibelle())
+                                                          ->setCodeBarre('dupont123')
+                                                          ->setTitre('Dupont à Dole')
+                                                         ])]);
+
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 1,
+                    'user' => $emilie,
+                    'library' => $lib_hotel_dieu,
+                    'start_at' => '2020-05-12 09:00:00']);
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 2,
+                    'user' => $bernard,
+                    'library' => $lib_hotel_dieu,
+                    'start_at' => '2020-05-12 14:00:00']);
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 3,
+                    'user' => $maurice,
+                    'library' => $lib_camus,
+                    'start_at' => '2020-05-14 09:00:00']);
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 4,
+                    'user' => $emilie,
+                    'library' => $lib_camus,
+                    'start_at' => '2020-05-14 09:15:00']);
+
+    $this->fixture('Class_DriveCheckout',
+                   ['id' => 5,
+                    'user' => $bernard,
+                    'library' => $lib_camus,
+                    'start_at' => '2020-05-15 10:00:00']);
+
+  }
+}
+
+
+
+class DriveCheckoutAdminControllerAdminVarTest extends DriveCheckOutAdminControllerTestCase {
+  /** @test */
+  public function withAdminVarEnableDriveFalseShouldNotDisplayMenuEntry() {
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->dispatch('/admin/index');
+    $this->assertNotXPath('//a[contains(@href, "/admin/drive-checkout")]');
+  }
+
+
+  /** @test */
+  public function withAdminVarEnableDriveFalseShouldRedirectToIndexOnAdminDriveCheckout() {
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->dispatch('/admin/drive-checkout');
+    $this->assertRedirectTo('/opac/index/index/id_profil/1');
+  }
+
+
+  /** @test */
+  public function withAdminVarEnabledUsersEditMauriceShouldContainsLinkToPlanCheckoutForMaurice() {
+    $this->dispatch('/admin/users/edit/id/4');
+    $this->assertXPath('//a[contains(@href, "/admin/drive-checkout/plan/id_user/4")]');
+  }
+
+
+  /** @test */
+  public function withAdminVarEnabledFalseUsersEditMauriceShouldNotContainsLinkToPlanCheckoutForMaurice() {
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+    $this->dispatch('/admin/users/edit/id/4');
+    $this->assertNotXPath('//a[contains(@href, "/admin/drive-checkout/plan/id_user/4")]');
+  }
+
+
+  /** @test */
+  public function withAdminVarEnabledShouldDisplayMenuEntry() {
+    $this->dispatch('/admin/index');
+    $this->assertXPath('//a[contains(@href, "/admin/drive-checkout")]');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerIndexTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBeDrive() {
+    $this->assertXPathContentContains('//h1', 'Drive');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToLibraryHotelDieu() {
+    $this->assertXPathContentContains('//a[contains(@href, "/admin/drive-checkout/list/id_bib/1")]',
+                                      'Hotel-Dieu');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToLibraryMaurissetteWithDriveDisabled() {
+    $this->assertXPathContentContains('//h3[text()="Drive désactivé"]/following-sibling::ul/li/a[contains(@href, "/admin/drive-checkout/list/id_bib/3")]',
+                                      'Maurissette');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerListHotelDieuTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/list/id_bib/1');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBeRendezVousHotelDieu() {
+    $this->assertXPathContentContains('//h1', 'Drive : rendez-vous : Hotel-Dieu, 12 mai 2020');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsInputForDateValue2020_05_12() {
+    $this->assertXPath('//input[@name="date"][@type="date"][@value="2020-05-12"][@onchange]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithCheckout2020_05_12_at_9_00_For_Emilie() {
+    $this->assertXPath('//table//td[text()="09:00"]/following-sibling::td[text()="A121"]/following-sibling::td[text()="emilie"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithCheckout2020_05_12_at_14_00_For_Bernard() {
+    $this->assertXPath('//table//td[text()="14:00"]/following-sibling::td[text()="A123"]/following-sibling::td[text()="bernard"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTwoCheckouts() {
+    $this->assertXPathCount('//table[@id="checkouts"]/tbody/tr', 2);
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerListCamusOnMayFourteenthTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/list/id_bib/2/date/2020-05-14');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBeRendezVousHotelDieu() {
+    $this->assertXPathContentContains('//h1', 'Drive : rendez-vous : Albert Camus, 14 mai 2020');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithCheckout2020_05_14_at_9_00_For_Maurice() {
+    $this->assertXPath('//table//td[text()="09:00"]/following-sibling::td[text()="A124"]/following-sibling::td[text()="maurice"]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function tableShouldContainsLinkToListHoldsAssociatedToCheckoutThree() {
+    $this->assertXPath('//table[@id="checkouts"]//td/a[contains(@href, "/admin/drive-checkout/list-holds/id_bib/2/date/2020-05-14/id/3")][@data-popup="true"]');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsLinkToDeleteCheckoutThree() {
+    $this->assertXPath('//table[@id="checkouts"]//td/a[contains(@href, "/admin/drive-checkout/delete/id_bib/2/date/2020-05-14/id/3")]');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsLinkToEditUserMauriceIdFour() {
+    $this->assertXPath('//table[@id="checkouts"]//td/a[contains(@href, "/admin/users/edit/id/4")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTwoCheckouts() {
+    $this->assertXPathCount('//table[@id="checkouts"]/tbody/tr', 2);
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToListCSVIdBib2Date2020_05_14() {
+    $this->assertXPathContentContains('//button[contains(@data-url, "/admin/drive-checkout/list-csv/id_bib/2/date/2020-05-14")]', 'Exporter les rendez-vous (.csv)');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToItemsCSVIdBib2Date2020_05_14() {
+    $this->assertXPathContentContains('//button[contains(@data-url, "/admin/drive-checkout/items-csv/id_bib/2/date/2020-05-14")]', 'Exporter les documents (.csv)');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerExportCSVCamusOnMayFourteenthTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/list-csv/id_bib/2/date/2020-05-14');
+  }
+
+
+  /** @test */
+  public function filenameShouldBe2020_05_14_Albert_Camus_Rendez_Vous() {
+    $this->assertContains(['name' => 'Content-Type',
+                           'value' => 'text/csv; name="2020-05-14 Albert Camus rendez-vous.csv"',
+                           'replace' => true], $this->_response->getHeaders());
+  }
+
+
+  /** @test */
+  public function csvShouldContainsCheckouts() {
+    $this->assertEquals("Heure;Carte;Abonné\n"
+                        ."09:00;A124;maurice\n"
+                        ."09:15;A121;emilie\n",
+                        $this->_response->getBody());
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerListHoldsForCheckoutThreeTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/list-holds/id/3');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBeDocumentsForMaurice() {
+    $this->assertXPathContentContains('//title', 'maurice, A124, 14 mai 09:00, Albert Camus');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsHoldOnTintin() {
+    $this->assertXPath('//table//td[text()="BD2"]/following-sibling::td[text()="tintin123"]/following-sibling::td[text()="On le cherche"]/following-sibling::td[text()="Tintin à Dole"]/following-sibling::td/a[contains(@href, "/recherche/viewnotice/id/123")]');
+  }
+
+
+  /** @test */
+  public function tableShouldNotContainsHoldOnTournesol() {
+    $this->assertNotXPath('//table//td[contains(text(),"Tournesol")]');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsHoldOnDupont() {
+    $this->assertXPath('//table//td[contains(text(), "Dupont")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToShowAllHolds() {
+    $this->assertXPathContentContains('//a[contains(@href, "/admin/drive-checkout/list-all-holds/id_user/4")]',
+                                      'Voir toutes les réservations de maurice');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerListAllHoldsForMauriceIdFourTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/list-all-holds/id_user/4');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsHoldOnTournesol() {
+    $this->assertXPath('//table//td[text()="Maurissette"]/following-sibling::td[text()="tournesol123"]/following-sibling::td[text()="Disponible"]/following-sibling::td[text()="Tournesol à Maurissette"]');
+  }
+
+
+  /** @test */
+  public function tableShouldContainsHoldOnTintin() {
+    $this->assertXPath('//table//td[text()="Tintin à Dole"]');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerExportItemsCSVCamusOnMayFourteenthTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/items-csv/id_bib/2/date/2020-05-14');
+  }
+
+
+  /** @test */
+  public function filenameShouldBe2020_05_14_Albert_Camus_Rendez_Vous() {
+    $this->assertContains(['name' => 'Content-Type',
+                           'value' => 'text/csv; name="2020-05-14 Albert Camus documents.csv"',
+                           'replace' => true], $this->_response->getHeaders());
+  }
+
+
+  /** @test */
+  public function csvShouldContainsTintinMilouAndDupontItems() {
+    $this->assertEquals("Jour;Heure;Carte;Abonné;Cote;Code-barres;Titre\n"
+                        . "\"14 mai\";09:00;A124;maurice;BD2;tintin123;\"Tintin à Dole\"\n"
+                        . "\"14 mai\";09:00;A124;maurice;;milou123;\"Milou à Dole\"\n"
+                        . "\"14 mai\";09:00;A124;maurice;;dupont123;\"Dupont à Dole\"\n",
+                        $this->_response->getBody());
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerDeleteCheckoutThreeTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/drive-checkout/delete/id/3');
+  }
+
+
+  /** @test */
+  public function responseShouldRedirectToDriveCheckoutIdBib2Date2020_05_14() {
+    $this->assertRedirectTo('/admin/drive-checkout/list/id_bib/2/date/2020-05-14');
+  }
+
+
+  /** @test */
+  public function responsShouldNotifyCheckoutDeleted() {
+    $this->assertFlashMessengerContentContains('Rendez-vous pour maurice supprimé');
+  }
+
+
+  /** @test */
+  public function checkoutShouldHaveBeenDeleted() {
+    Class_DriveCheckout::clearCache();
+    $this->assertNull(Class_DriveCheckout::find(3));
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerPlanNewCheckoutForMauriceTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_DriveCheckout::find(3)->delete();
+    $this->dispatch('/admin/drive-checkout/plan/id_user/4');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBePlanCheckout() {
+    $this->assertXPathContentContains('//title', 'Planifier un retrait pour maurice');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToChooseHotelDieu() {
+    $this->assertXPath('//a[contains(@href, "/plan/id_user/4/id_bib/1")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToShowAllHolds() {
+    $this->assertXPathContentContains('//a[contains(@href, "/admin/drive-checkout/list-all-holds/id_user/4")]',
+                                      'Voir toutes les réservations de maurice');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerDownloadCheckoutThreeTest
+  extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    Class_DriveCheckout::find(3)
+      ->getLibrary()
+      ->setLieu($this->fixture('Class_Lieu',
+                               ['id' => 233,
+                                'libelle' => 'Quelque part',
+                                'latitude' => '-38.812239',
+                                'longitude' => '177.137570']));
+
+    $this->dispatch('/admin/drive-checkout/ical/id/3');
+  }
+
+
+  /** @test */
+  public function contentTypeShouldBeTextCalendar() {
+    $this->assertHeaderContains('Content-Type', 'text/calendar;charset=utf-8');
+  }
+
+
+  /** @test */
+  public function contentDispositionShouldBeAttachment() {
+    $this->assertHeaderContains('Content-Disposition', 'attachment;filename="calendar.ics');
+  }
+
+
+  /** @test */
+  public function uidShouldBeDriveCheckout3() {
+    $this->assertContains('drive-checkout:3', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function dateTimeShouldBe20200514T090000WithTimezone() {
+    $this->assertContains('DTSTART;TZID=Europe/Paris:20200514T090000', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function urlToPlanInCurrentProfilShouldBePresent() {
+    $this->assertContains('/drive-checkout/plan/id_profil/2', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function summaryShouldBeRetraitDesReservations() {
+    $this->assertContains('SUMMARY:Retrait des réservations à Albert Camus',
+                          $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function locationShouldBeAlbertCamus() {
+    $this->assertContains('LOCATION:Albert Camus', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function geoShouldBeLat38Long177() {
+    $this->assertContains('GEO:-38.812239;177.137570', $this->_response->getBody());
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerPlanInvalidCheckoutForMauriceHotelDieuOnMayFourteenthTest extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_DriveCheckout::find(3)->delete();
+    $this->dispatch('/admin/drive-checkout/plan/id_user/4/id_bib/1/checkout_date/2020-05-14');
+  }
+
+
+  /** @test */
+  public function pageShouldRedirectToDriveCheckoutPlanIdUserFourIdBibOne() {
+    $this->assertRedirectTo('/admin/drive-checkout/plan/id_user/4/id_bib/1');
+  }
+
+
+  /** @test */
+  public function pageShouldNotifyInvalidDate() {
+    $this->assertFlashMessengerContentContains('La date choisie n\'est pas disponible');
+  }
+}
+
+
+
+
+class DriveCheckoutAdminControllerPlanPostNewCheckoutForMauriceHotelDieuOnMayNineteenthTest
+  extends DriveCheckOutAdminControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_DriveCheckout::find(3)->delete();
+    $this->postDispatch('/admin/drive-checkout/plan/id_user/4/id_bib/1/checkout_date/2020-05-19',
+                        ['checkout_time' => '10:00']);
+  }
+
+
+  /** @test */
+  public function pageShouldRedirectToDriveCheckoutListIdBibOneDate2020_05_19() {
+    $this->assertRedirectTo('/admin/drive-checkout/list/id_bib/1/date/2020-05-19');
+  }
+
+
+  /** @test */
+  public function pageShouldNotifyCheckoutSaved() {
+    $this->assertFlashMessengerContentContains('Retrait planifié pour maurice, le 19 mai 10:00, Hotel-Dieu');
+  }
+}
\ No newline at end of file
diff --git a/tests/scenarios/DriveCheckOut/DriveCheckoutOpeningsTest.php b/tests/scenarios/DriveCheckOut/DriveCheckoutOpeningsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6f30ff9901d1f70b795147da2c8dee3ea3f4a4b
--- /dev/null
+++ b/tests/scenarios/DriveCheckOut/DriveCheckoutOpeningsTest.php
@@ -0,0 +1,287 @@
+<?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
+ */
+
+abstract class DriveCheckoutOpeningsTestCase extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+
+  public function setUp(){
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 1);
+    $this->fixture('Class_Bib',
+                   ['id' => 3,
+                    'libelle' => 'Carthage',
+                    'enable_drive' => true
+                   ]);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 5,
+                    'libelle' => 'Alexandrie',
+                    'enable_drive' => false
+                   ]);
+
+    $this->fixture('Class_Ouverture',
+                   ['id' => 1,
+                    'debut_matin' => '10:30',
+                    'fin_matin' => '11:30',
+                    'debut_apres_midi' => '14:00',
+                    'fin_apres_midi' => '15:00',
+                    'id_site' => 3,
+                    'used_for' => Class_Ouverture::USED_FOR_LIBRARY,
+                    'jour_semaine' => Class_Ouverture::VENDREDI,
+                    'jour' => '01/01/2020',
+                    'validity_start' => '00/00/0000',
+                    'validity_end' => ''
+                   ]
+    );
+
+    $this->fixture('Class_Ouverture',
+                   ['id'=> 10,
+                    'debut_matin' => '09:00',
+                    'fin_matin' => '11:00',
+                    'debut_apres_midi' => '16:00',
+                    'fin_apres_midi' => '17:00',
+                    'id_site' => 3,
+                    'used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA,
+                    'jour_semaine' => Class_Ouverture::MARDI,
+                    'jour' => '23/10/2020',
+                    'validity_start' => '00/00/0000',
+                    'validity_end' => ''
+                   ]
+    );
+
+
+    $this->fixture('Class_Ouverture',
+                   ['id'=> 12,
+                    'debut_matin' => '09:00',
+                    'fin_matin' => '11:00',
+                    'max_per_period_matin' => 3,
+                    'debut_apres_midi' => '16:00',
+                    'fin_apres_midi' => '17:00',
+                    'max_per_period_apres_midi' => 10,
+                    'id_site' => 3,
+                    'used_for' => Class_Ouverture::USED_FOR_DRIVE,
+                    'jour_semaine' => Class_Ouverture::MARDI,
+                    'jour' => '23/08/2020',
+                    'validity_start' => '00/00/0000',
+                    'validity_end' => ''
+                   ]
+    );
+
+    $this->fixture('Class_Ouverture',
+                   ['id'=> 13,
+                    'debut_matin' => '09:00',
+                    'fin_matin' => '11:00',
+                    'debut_apres_midi' => '16:00',
+                    'fin_apres_midi' => '17:00',
+                    'id_site' => 5,
+                    'used_for' => Class_Ouverture::USED_FOR_DRIVE,
+                    'jour_semaine' => Class_Ouverture::LUNDI,
+                    'jour' => '23/08/2020',
+                    'validity_start' => '00/00/0000',
+                    'validity_end' => ''
+                   ]
+    );
+
+  }
+}
+
+
+
+
+class DriveCheckoutOpeningsDeactivatedTest extends DriveCheckoutOpeningsTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_DRIVE_CHECKOUT', 0);
+  }
+
+
+  /** @test */
+  public function openingsDriveLinkShouldNotBePresent() {
+    $this->dispatch('/admin/ouvertures/index/id_site/3');
+    $this->assertNotXPath('//a[contains(@href, "/admin/ouvertures/index/id_site/3/used_for/2")]');
+  }
+
+
+  /** @test */
+  public function carthageLibraryFormShouldNotContainsCheckBoxEnableDrive() {
+    $this->dispatch('/admin/bib/edit/id/3');
+    $this->assertNotXPath('//form//input[@name="enable_drive"]');
+  }
+}
+
+
+
+
+class DriveCheckoutOpeningsAddNewTest extends DriveCheckoutOpeningsTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this->postDispatch('/admin/ouvertures/add/used_for/2',
+                        ['debut_matin' => '10:30',
+                         'fin_matin' => '11:30',
+                         'debut_apres_midi' => '14:00',
+                         'fin_apres_midi' => '15:00',
+                         'id_site' => 3,
+                         'jour_semaine' => Class_Ouverture::MARDI,
+                         'jour' => '23/10/2012',
+                         'validity_start' => '00/00/0000',
+                         'validity_end' => ''
+                        ]
+    );
+  }
+
+
+  /** @test */
+  public function responseShouldRedirectToOuverturesIndexSiteThreeUsed_ForTwo() {
+    $this->assertRedirectTo('/admin/ouvertures/index/id_site/3/used_for/2');
+  }
+
+
+  /** @test */
+  public function ouvertureUsed_ForShouldBeDRIVE() {
+    $this->assertEquals(Class_Ouverture::USED_FOR_DRIVE,
+                        Class_Ouverture::find(13)->getUsedFor());
+  }
+}
+
+
+
+
+class DriveCheckoutOpeningsIndexTest extends DriveCheckoutOpeningsTestCase {
+  public function setUp(){
+    parent::setUp();
+    $this->dispatch('/admin/ouvertures/index/id_site/3/used_for/2');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToEditOpening1() {
+    $this->assertNotXPath('//a[@href="/admin/ouvertures/edit/id_site/3/used_for/2/id/1"]');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToEditOpening10() {
+    $this->assertNotXPath('//a[contains(@href, "/ouvertures/edit/id_site/3/used_for/2/id/10")]');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToEditOpening13() {
+    $this->assertNotXPath('//a[contains(@href, "/ouvertures/edit/id_site/3/used_for/2/id/13")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToEditOpening12() {
+    $this->assertXPath('//a[@href="/admin/ouvertures/edit/id_site/3/used_for/2/id/12"]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function tdDataOpening12ShouldContains9h00dash11h00And3PersonsPerSlot() {
+    $this->assertXPath('//td[text()="09h00 - 11h00"]/following-sibling::td[text()="3 pers."]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function pageShouldContainsActionButtonToAddDriveOpening() {
+    $this->assertXPathContentContains('//button[@data-url="/admin/ouvertures/add/id_site/3/used_for/2"]', 'Ajouter une plage d\'ouverture du drive');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToOuverturesIndexIdSite3() {
+    $this->assertXPathContentContains('//a[contains(@href, "/admin/ouvertures/index/id_site/3/used_for/2")]',
+                                      'Planification des ouvertures du drive');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsCheckboxClosedOnHolidays() {
+    $this->assertNotXPath('//form//input[@name="closed_on_holidays"]');
+  }
+
+}
+
+
+
+
+class DriveCheckoutOpeningsEditTest extends DriveCheckoutOpeningsTestCase {
+  protected $_storm_default_to_volatile = true;
+
+  public function setUp(){
+    parent::setUp();
+    $this->dispatch('/admin/ouvertures/edit/id_site/3/used_for/2/id/12');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsInputTextForMaxPerPeriodMatin() {
+    $this->assertXPath('//input[@type="number"][@name="max_per_period_matin"][@value="3"]', $this->_response->getBody());
+  }
+
+
+/** @test */
+  public function pageShouldContainsInputTextForMaxPerPeriodApresMidi() {
+    $this->assertXPath('//input[@type="number"][@name="max_per_period_apres_midi"][@value="10"]');
+  }
+}
+
+
+
+
+class DriveCheckoutOpeningsAddGetTest extends DriveCheckoutOpeningsTestCase {
+  public function setUp(){
+    parent::setUp();
+    $this->dispatch('/admin/ouvertures/add/id_site/3/used_for/2/id/12');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsInputTextForMaxPerPeriodMatin() {
+    $this->assertXPath('//input[@type="number"][@name="max_per_period_matin"][@value="1"]', $this->_response->getBody());
+  }
+
+
+/** @test */
+  public function pageShouldContainsInputTextForMaxPerPeriodApresMidi() {
+    $this->assertXPath('//input[@type="number"][@name="max_per_period_apres_midi"][@value="1"]');
+  }
+}
+
+
+
+
+class DriveCheckoutOpeningsEditLibraryCarthageTest extends DriveCheckoutOpeningsTestCase {
+  /** @test */
+  public function carthageLibraryFormShouldContainsCheckBoxEnableDriveChecked() {
+    $this->dispatch('/admin/bib/edit/id/3');
+    $this->assertXPath('//form//input[@type="checkbox"][@name="enable_drive"][@checked]',$this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function alexandrieLibraryFormShouldContainsCheckBoxEnableDriveNotChecked() {
+    $this->dispatch('/admin/bib/edit/id/5');
+    $this->assertXPath('//form//input[@type="checkbox"][@name="enable_drive"][not(@checked)]',$this->_response->getBody());
+  }
+}