From 3f18c55d5ec8373467c92622aa80262a74cfe366 Mon Sep 17 00:00:00 2001
From: Patrick Barroca <pbarroca@afi-sa.fr>
Date: Mon, 4 May 2020 15:08:35 +0200
Subject: [PATCH] dev #109790 Implements Drive checkout of holds.

---
 FEATURES/109790                               |  10 +
 VERSIONS_WIP/109790                           |   1 +
 .../controllers/DriveCheckoutController.php   | 172 ++++
 .../controllers/OuverturesController.php      |  23 +-
 .../views/scripts/drive-checkout/index.phtml  |  20 +
 .../drive-checkout/list-all-holds.phtml       |   8 +
 .../scripts/drive-checkout/list-holds.phtml   |  14 +
 .../views/scripts/drive-checkout/list.phtml   |  27 +
 .../views/scripts/drive-checkout/plan.phtml   |  14 +
 .../admin/views/scripts/ouvertures/list.phtml |  17 +-
 .../controllers/DriveCheckoutController.php   | 104 +++
 .../views/scripts/drive-checkout/plan.phtml   |   2 +
 cosmogramme/sql/patch/patch_388.php           |  16 +
 cosmogramme/sql/patch/patch_389.php           |  23 +
 library/Class/AdminVar.php                    |  17 +
 library/Class/Bib.php                         |  53 +-
 library/Class/Bib/DriveOpening.php            |  85 ++
 .../Bib/DriveOpening/AfternoonPeriod.php      |  29 +
 .../Class/Bib/DriveOpening/MorningPeriod.php  |  29 +
 library/Class/Bib/DriveOpening/Period.php     | 129 +++
 library/Class/CodifAnnexe.php                 |   7 +
 library/Class/DriveCheckout.php               | 217 +++++
 library/Class/DriveCheckout/Holds.php         |  98 ++
 library/Class/DriveCheckout/Notification.php  |  83 ++
 library/Class/DriveCheckout/Plan.php          | 305 +++++++
 library/Class/ICal/Abstract.php               |  38 +
 library/Class/ICal/Autoloader.php             |  50 ++
 library/Class/ICal/DriveCheckout.php          |  59 ++
 library/Class/Mail.php                        |  18 +
 library/Class/Ouverture.php                   |  68 +-
 library/Class/Ouverture/Visitor.php           |  11 +-
 library/Class/SearchCriteria/DateRange.php    |   2 +-
 library/Class/TableDescription.php            |  40 +-
 .../TableDescription/DriveCheckout/Holds.php  |  51 ++
 .../DriveCheckout/HoldsWithCheckouts.php      |  43 +
 .../TableDescription/DriveCheckout/List.php   |  35 +
 .../DriveCheckout/ListWithActions.php         |  81 ++
 library/Class/TableDescription/Openings.php   |  34 +-
 .../Class/TableDescription/Openings/Drive.php |  46 +
 library/Class/User/Cards.php                  |  15 +-
 library/Class/Users.php                       |   7 +-
 library/Class/WebService/SIGB/Exemplaire.php  |  10 +-
 .../WebService/SIGB/ExemplaireOperation.php   |  18 +-
 .../WebService/SIGB/Koha/PatronInfoReader.php |  22 +-
 .../SIGB/Nanook/PatronInfoReader.php          |  23 +
 library/Class/WebService/SIGB/Reservation.php |  34 +-
 library/ZendAfi/Acl/AdminControllerGroup.php  |   1 +
 library/ZendAfi/Acl/AdminControllerRoles.php  |   2 +
 .../ZendAfi/Controller/Action/Helper/Ical.php |  34 +
 .../Action/Helper/RenderIcalArticles.php      |  27 +-
 .../ZendAfi/Controller/Plugin/AdminAuth.php   |   2 +-
 .../Controller/Plugin/Manager/Library.php     |  10 +-
 .../Controller/Plugin/Manager/Opening.php     |  14 +-
 .../Controller/Plugin/Manager/User.php        |   7 +
 .../Plugin/ResourceDefinition/Opening.php     |  48 +-
 library/ZendAfi/Form.php                      |   2 +-
 library/ZendAfi/Form/Admin/Library.php        |   9 +-
 library/ZendAfi/Form/Admin/Ouverture.php      |  40 +-
 .../Form/Element/Date.php}                    |  15 +-
 library/ZendAfi/Validate/DateFormat.php       |  13 +-
 .../ZendAfi/View/Helper/Abonne/HoldsBoard.php |  12 +-
 library/ZendAfi/View/Helper/Accueil/Base.php  |   2 +-
 .../ZendAfi/View/Helper/Admin/ContentNav.php  |   3 +-
 .../ZendAfi/View/Helper/Admin/HelpLink.php    |   2 +
 .../View/Helper/Admin/SubscribeUsers.php      |  13 +-
 .../ZendAfi/View/Helper/DriveCheckoutPlan.php | 203 +++++
 library/ZendAfi/View/Helper/FormDate.php      |  31 +
 .../View/Helper/LibraryOpeningsAdmin.php      |  22 +-
 .../View/Helper/RenderLibraryOpening.php      |   2 +-
 library/storm                                 |   2 +-
 .../View/Wrapper/DriveCheckoutPlan.php        | 109 +++
 .../Wrapper/DriveCheckoutPlan/Library.php     | 163 ++++
 .../DriveCheckoutPlan/LibrarySelected.php     |  37 +
 .../Wrapper/DriveCheckoutPlan/RichContent.php |  51 ++
 .../DriveCheckoutPlan/RichContent/Date.php    | 121 +++
 .../DriveCheckoutPlan/RichContent/Library.php |  92 ++
 .../DriveCheckoutPlan/RichContent/Time.php    |  77 ++
 .../Library/View/Wrapper/Library.php          | 105 +--
 .../Library/View/Wrapper/SearchHistory.php    |   3 +-
 .../templates/Intonation/System/Abstract.php  |   4 +-
 .../Intonation/View/Abonne/Holds.php          |  12 +-
 .../Intonation/View/DriveCheckoutPlan.php     |  37 +
 public/admin/skins/bokeh72/config.json        |   4 +-
 public/admin/skins/bokeh74/config.json        |   4 +-
 .../bokeh74/icons/actions/shopping_16.png     | Bin 0 -> 316 bytes
 .../bokeh74/icons/actions/shopping_24.png     | Bin 0 -> 535 bytes
 .../skins/bokeh74/icons/menu/shopping_24.png  | Bin 0 -> 535 bytes
 .../skins/bokeh74/icons/menu/shopping_48.png  | Bin 0 -> 853 bytes
 public/admin/skins/noel/config.json           |   4 +-
 .../skins/noel/icons/actions/shopping_16.png  | Bin 0 -> 316 bytes
 .../skins/noel/icons/actions/shopping_24.png  | Bin 0 -> 535 bytes
 .../skins/noel/icons/menu/shopping_24.png     | Bin 0 -> 535 bytes
 .../skins/noel/icons/menu/shopping_48.png     | Bin 0 -> 853 bytes
 public/admin/skins/retro/config.json          |   5 +-
 .../skins/retro/icons/actions/shopping_16.png | Bin 0 -> 316 bytes
 .../skins/retro/icons/menu/shopping_24.png    | Bin 0 -> 535 bytes
 .../skins/retro/icons/menu/shopping_48.png    | Bin 0 -> 853 bytes
 .../admin/controllers/BibControllerTest.php   |   2 +-
 .../controllers/OuverturesControllerTest.php  |  33 +-
 .../controllers/UserGroupControllerTest.php   |  12 +
 .../AbonneControllerMultimediaTest.php        |   6 +-
 .../opac/controllers/BibControllerTest.php    |   4 +-
 .../controllers/MultimediaControllerTest.php  |   2 +-
 tests/bootstrap.php                           |   2 +-
 tests/db/UpgradeDBTest.php                    |  94 ++
 tests/fixtures/KohaFixtures.php               |   7 +-
 tests/fixtures/NanookFixtures.php             |  14 +
 tests/library/Class/Multimedia/DeviceTest.php |   8 +-
 .../library/Class/Multimedia/LocationTest.php |  14 +-
 .../Class/WebService/SIGB/KohaTest.php        |  41 +-
 .../Class/WebService/SIGB/NanookTest.php      |  39 +-
 .../View/Helper/RenderLibraryOpeningTest.php  |  16 +-
 .../DriveCheckOutBookingTest.php              | 842 ++++++++++++++++++
 .../DriveCheckoutAdminControllerTest.php      | 617 +++++++++++++
 .../DriveCheckoutOpeningsTest.php             | 287 ++++++
 115 files changed, 5324 insertions(+), 268 deletions(-)
 create mode 100644 FEATURES/109790
 create mode 100644 VERSIONS_WIP/109790
 create mode 100644 application/modules/admin/controllers/DriveCheckoutController.php
 create mode 100644 application/modules/admin/views/scripts/drive-checkout/index.phtml
 create mode 100644 application/modules/admin/views/scripts/drive-checkout/list-all-holds.phtml
 create mode 100644 application/modules/admin/views/scripts/drive-checkout/list-holds.phtml
 create mode 100644 application/modules/admin/views/scripts/drive-checkout/list.phtml
 create mode 100644 application/modules/admin/views/scripts/drive-checkout/plan.phtml
 create mode 100644 application/modules/opac/controllers/DriveCheckoutController.php
 create mode 100644 application/modules/opac/views/scripts/drive-checkout/plan.phtml
 create mode 100644 cosmogramme/sql/patch/patch_388.php
 create mode 100644 cosmogramme/sql/patch/patch_389.php
 create mode 100644 library/Class/Bib/DriveOpening.php
 create mode 100644 library/Class/Bib/DriveOpening/AfternoonPeriod.php
 create mode 100644 library/Class/Bib/DriveOpening/MorningPeriod.php
 create mode 100644 library/Class/Bib/DriveOpening/Period.php
 create mode 100644 library/Class/DriveCheckout.php
 create mode 100644 library/Class/DriveCheckout/Holds.php
 create mode 100644 library/Class/DriveCheckout/Notification.php
 create mode 100644 library/Class/DriveCheckout/Plan.php
 create mode 100644 library/Class/ICal/Abstract.php
 create mode 100644 library/Class/ICal/Autoloader.php
 create mode 100644 library/Class/ICal/DriveCheckout.php
 create mode 100644 library/Class/TableDescription/DriveCheckout/Holds.php
 create mode 100644 library/Class/TableDescription/DriveCheckout/HoldsWithCheckouts.php
 create mode 100644 library/Class/TableDescription/DriveCheckout/List.php
 create mode 100644 library/Class/TableDescription/DriveCheckout/ListWithActions.php
 create mode 100644 library/Class/TableDescription/Openings/Drive.php
 create mode 100644 library/ZendAfi/Controller/Action/Helper/Ical.php
 rename library/{Class/TableDescription/OpeningsLabelled.php => ZendAfi/Form/Element/Date.php} (75%)
 create mode 100644 library/ZendAfi/View/Helper/DriveCheckoutPlan.php
 create mode 100644 library/ZendAfi/View/Helper/FormDate.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/Library.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/LibrarySelected.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Date.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Library.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/DriveCheckoutPlan/RichContent/Time.php
 create mode 100644 library/templates/Intonation/View/DriveCheckoutPlan.php
 create mode 100644 public/admin/skins/bokeh74/icons/actions/shopping_16.png
 create mode 100644 public/admin/skins/bokeh74/icons/actions/shopping_24.png
 create mode 100644 public/admin/skins/bokeh74/icons/menu/shopping_24.png
 create mode 100644 public/admin/skins/bokeh74/icons/menu/shopping_48.png
 create mode 100644 public/admin/skins/noel/icons/actions/shopping_16.png
 create mode 100644 public/admin/skins/noel/icons/actions/shopping_24.png
 create mode 100644 public/admin/skins/noel/icons/menu/shopping_24.png
 create mode 100644 public/admin/skins/noel/icons/menu/shopping_48.png
 create mode 100644 public/admin/skins/retro/icons/actions/shopping_16.png
 create mode 100644 public/admin/skins/retro/icons/menu/shopping_24.png
 create mode 100644 public/admin/skins/retro/icons/menu/shopping_48.png
 create mode 100644 tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php
 create mode 100644 tests/scenarios/DriveCheckOut/DriveCheckoutAdminControllerTest.php
 create mode 100644 tests/scenarios/DriveCheckOut/DriveCheckoutOpeningsTest.php

diff --git a/FEATURES/109790 b/FEATURES/109790
new file mode 100644
index 00000000000..3f967268726
--- /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 00000000000..4c0ae20eb2c
--- /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 00000000000..161a15f2df4
--- /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 6bc5361db7e..cb3acc02480 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 00000000000..35b77715169
--- /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 00000000000..536a3431707
--- /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 00000000000..661ce491d61
--- /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 00000000000..89ac4e7de43
--- /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 00000000000..34d26a02c58
--- /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 7b65e0cc396..33001fad8bc 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 00000000000..334fe48d4f3
--- /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 00000000000..aafd6c5ca8d
--- /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 00000000000..7550f20493b
--- /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 00000000000..0692ddc5212
--- /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 14e33346b9b..c8d2f8c8011 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 57a69a360f1..f76e32fd965 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 00000000000..fd4953e4ec7
--- /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 00000000000..40a10c90fff
--- /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 00000000000..31d0b095e99
--- /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 00000000000..1f8a6775fb6
--- /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 61aed280e3c..8a4241e40f3 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 00000000000..927abc4414d
--- /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 00000000000..3566117b996
--- /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 00000000000..dfa2a0eadda
--- /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 00000000000..c4bedf95a6a
--- /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 00000000000..ee1251b2ccf
--- /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 00000000000..48940296cc0
--- /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 00000000000..c859a861b1e
--- /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 eea9b6839c2..9c0315f465b 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 a464754d0f9..cda49f4e608 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 c0b08314d04..4e6ff16555e 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 8142c035414..b829d5b0648 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 9412d0466ca..a523d8e647f 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 00000000000..7fcfd881fd6
--- /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 00000000000..62257a5fb78
--- /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 00000000000..5caf56025d8
--- /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 00000000000..a1c2971cad3
--- /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 9dd982a22ec..1a9749fe3d3 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 00000000000..2c4c786eff7
--- /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 d0c304ba224..bc7207a3799 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 4e9cacfda16..4627a843425 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 6a71c43dcdb..bebd4ff9d16 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 9cfdb3e8817..867194cb73c 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 d7c2f84da82..0f75c707c5a 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 93f09e9e715..8d702184622 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 5a36cc4ce3c..f3640c20c11 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 ea1d5397bc0..857dadb4176 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 b3f6c534d60..01cb776ffe3 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 00000000000..23ca6ffc485
--- /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 a43dfe844d5..2718ba2cc30 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 dd63656e84a..abe917b3d60 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 3837c4d03c1..f3ce96675d5 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 60e7a96b1a2..d46e3d339b0 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 c584377b659..e03c89998ad 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 e7e24dcfc71..74a6501af68 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 8d589b0d6e0..277b0a00a57 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 efe44da5367..6af1bc8a88b 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 88ccaad739d..2dc2a8b0188 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 2cb18e9c55d..c9500756809 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 d5ddd780990..7f97a0efd98 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 62363f20e94..49d6f2a7c9d 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 517378ee6fd..b3a9d032f57 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 a94a7acdb05..55d1333992a 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 0076068b6ec..fb1dd64c225 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 752cd3ee36b..161c079f9ef 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 00000000000..923129fba9d
--- /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 00000000000..e1951dc0cb8
--- /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 342c62553d1..d746165213c 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 01622651cbf..37d5d97f65e 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 7aaada2df1e..67e32fa4f21 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 00000000000..1340b09a17a
--- /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 00000000000..7363e127c04
--- /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 00000000000..f1dbc107e83
--- /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 00000000000..02d3aab0668
--- /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 00000000000..13116fca5be
--- /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 00000000000..950046b7296
--- /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 00000000000..a142bed07c1
--- /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 80e62075b22..21800425ef8 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 c44c9640d88..549bb162e74 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 a493477b9b7..2e9ffbfc191 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 c412502edb3..6b2b388f4cb 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 00000000000..1b55a876773
--- /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 41a3cc44d57..fc165ddf047 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 7a2989a908c..15f4733bbca 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
GIT binary patch
literal 316
zcmV-C0mJ@@P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60
z0RN!9r;`8x0P#sgK~y-6rP9wXLs1Y0@L!2oHw_CSvC>B%yoIIr@DyIeO84Ce>7t1R
zNxQNTwo-&`E7#)A(c@~l>Pt@Mn=>=toIk@q(Wv;_IK&E$(Te>ioM4PI+^2%2thk9j
z+QF-6Myx+4?g*V4r|v}Vw-X%TqK3dF_Gblduv<f557)_A4%@&MPVxSWz#}$so;G?Y
z7F@mj4HiL>$W=IPOoH!Zof4l!P7yVqzc36t!b|YmtW)C8%Atb6EP+;t%)`iZI)}Ny
zAnZC`gRf?ViN9g3(iChc@AaUZ_X2mOZ;<#na*C+=g0A}C*Ztv|f5Qie&rtJ9j%H5)
O0000<MNUMnLSTZ56p-5h

literal 0
HcmV?d00001

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
GIT binary patch
literal 535
zcmV+y0_gpTP)<h;3K|Lk000e1NJLTq000{R000;W1^@s63qXeZ00006VoOIv0RI60
z0RN!9r;`8x0nAB6K~zYIwbo5*Q$Z94;3vj~phYo)8&Rl&x)f=Iy6Rr5zs5!PBJOqL
zZ?K@<_zyH2#e(7oD6Qh6)u^>X#qY(Po6NnP<o4d^3x`bJdCxg>W+rFCcyurwY!QDF
zwiszT74ecdiQb6dRn`ms+t5mId+;1ba0mUsU&7&pehqiSxP!}hi_`cP3wI}+FX9*$
za4q6*;cP<xFj$TST*U?4PUs7`Dcg?YRl*TC5&UoLMEnTvrMGw|XklHk+9dYkhv6AC
z#S(sEUrg>QiO$K+u{U9|9@>~0muxnCKA13B?mruo?UJaC`?x3D&Zbz$8!TfbCtK*^
z3ieMByb2q*E@W=fDeBrv`ON-1W@J}2Ud5`cO~u?hS!;^9wZO+*KlsPolIUi(u@y~D
zW4`2Y26H8DF7PooAN=$0TOt17>i?28-yFa3y0inIf>m*^LXsTMLj1z->`62}WZ3QN
zq96RX#f_6ajm90rGpmb_ijU|Z?zqan<7>@CUxHs{WpT26WgluL%k^<(gSheIxr$9p
z;k@jXJ{Zk+aiA2ui_fJ+9yRQVo3gf*MAQErqpZ--%N^wLs8uw?^0>)Uc<J9%^&-R@
Z;4dm=$mLJDH8%hN002ovPDHLkV1geY2FL&a

literal 0
HcmV?d00001

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
GIT binary patch
literal 535
zcmV+y0_gpTP)<h;3K|Lk000e1NJLTq000{R000;W1^@s63qXeZ00006VoOIv0RI60
z0RN!9r;`8x0nAB6K~zYIwbo5*Q$Z94;3vj~phYo)8&Rl&x)f=Iy6Rr5zs5!PBJOqL
zZ?K@<_zyH2#e(7oD6Qh6)u^>X#qY(Po6NnP<o4d^3x`bJdCxg>W+rFCcyurwY!QDF
zwiszT74ecdiQb6dRn`ms+t5mId+;1ba0mUsU&7&pehqiSxP!}hi_`cP3wI}+FX9*$
za4q6*;cP<xFj$TST*U?4PUs7`Dcg?YRl*TC5&UoLMEnTvrMGw|XklHk+9dYkhv6AC
z#S(sEUrg>QiO$K+u{U9|9@>~0muxnCKA13B?mruo?UJaC`?x3D&Zbz$8!TfbCtK*^
z3ieMByb2q*E@W=fDeBrv`ON-1W@J}2Ud5`cO~u?hS!;^9wZO+*KlsPolIUi(u@y~D
zW4`2Y26H8DF7PooAN=$0TOt17>i?28-yFa3y0inIf>m*^LXsTMLj1z->`62}WZ3QN
zq96RX#f_6ajm90rGpmb_ijU|Z?zqan<7>@CUxHs{WpT26WgluL%k^<(gSheIxr$9p
z;k@jXJ{Zk+aiA2ui_fJ+9yRQVo3gf*MAQErqpZ--%N^wLs8uw?^0>)Uc<J9%^&-R@
Z;4dm=$mLJDH8%hN002ovPDHLkV1geY2FL&a

literal 0
HcmV?d00001

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
GIT binary patch
literal 853
zcmV-b1FHOqP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x0}4q*K~!jg?U_Gs6EPUZe+f-W+krw`Ar-;Ef>a?ffP~bJ%rGG_F+e{7
z0|PJyBnn9V1f41*2EGCl5^PkZ6@Lec3N4Wc41j1FC1J3yaUJ{4ch2PwDUW2GlY9KW
z&uhQFvwbkY0J*_kpboSnN?me$f}xoF5^w=H4II$!-2}=GHp-IwOZ&eGtOL)0Moe8!
zxCm?#-NU|RQ~K&Dt|NLVZSg^A0@qxG6&yGxfj7Xg{QU!X3jEY~H*iZKfL&lw`|mJt
z88{+;?*KEvCr6uBJfhPxZ-FTbuY&s|u2^hLNgo0Cp|5y=HN{}hbJKuJioZ2qW8-vk
ze+M||YtRA1(pMX3(Mc&IiV?<U70xSZIBnE{6<IF;=flV@0R(vIAu29ZU$a&g(E-d^
zjHV6M*PN9Vfg!}(GMG@uJVnL9wBq+S@mCB0oKuW=KZ_G!L-BvsMhZ(Ib?9F7MpG=P
zKCNjK7C=@L*yJ;GBHN%Y+^j>GBGeTAVk6lm#|ZFP*4x1Ao=S7WDsZ39dTj*wn}Lq>
zQ3f6W+w^a_>|3^_<}$L9wV<SBp#XdXjz$erPEjo)BKcp@ECS0}tMI@wh>5TOY>|CV
zyOu4fxoZo%XlZa87?oF>(L2DHy!vo%MflS9()%!PX%uYeONcWY6DR16a)`JnPDA=Y
z`ZRIoRl)jNXx$RnsJu21rxGUYYS$#38l6L}F$-tkKG_sN%g|_2seca<=nK{Mq~U%G
zam2F+J%B&JXG25PSS^t>H>kEv0c;>PID0THr%gv06H2{bBQRmO-=)nIfN37&C55`Q
zB?%zQ6mkk6DuwzLK;jg7PxoUk4|AXaeAFA50%!mqwEyxj2i{?Lf7mwl`cR!v>Ltt=
z*RDx8s_o<2J>yGO|4bC%6yj4-fN5#`5O5Fk5&I0{JyMbFO^D*00lorPfh*d*C%|)y
z-7cs{+<7U+qkY#D$^&9)Dj*(dS@(KSYFIvp*+!rZEX9d9Wh@~y#X<>jVo@c#p07#`
fwkO{J1Nh<}ujd1_tftv(00000NkvXXu0mjfMV)YY

literal 0
HcmV?d00001

diff --git a/public/admin/skins/noel/config.json b/public/admin/skins/noel/config.json
index 01e9f770ac8..ce45549444f 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
GIT binary patch
literal 316
zcmV-C0mJ@@P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60
z0RN!9r;`8x0P#sgK~y-6rP9wXLs1Y0@L!2oHw_CSvC>B%yoIIr@DyIeO84Ce>7t1R
zNxQNTwo-&`E7#)A(c@~l>Pt@Mn=>=toIk@q(Wv;_IK&E$(Te>ioM4PI+^2%2thk9j
z+QF-6Myx+4?g*V4r|v}Vw-X%TqK3dF_Gblduv<f557)_A4%@&MPVxSWz#}$so;G?Y
z7F@mj4HiL>$W=IPOoH!Zof4l!P7yVqzc36t!b|YmtW)C8%Atb6EP+;t%)`iZI)}Ny
zAnZC`gRf?ViN9g3(iChc@AaUZ_X2mOZ;<#na*C+=g0A}C*Ztv|f5Qie&rtJ9j%H5)
O0000<MNUMnLSTZ56p-5h

literal 0
HcmV?d00001

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
GIT binary patch
literal 535
zcmV+y0_gpTP)<h;3K|Lk000e1NJLTq000{R000;W1^@s63qXeZ00006VoOIv0RI60
z0RN!9r;`8x0nAB6K~zYIwbo5*Q$Z94;3vj~phYo)8&Rl&x)f=Iy6Rr5zs5!PBJOqL
zZ?K@<_zyH2#e(7oD6Qh6)u^>X#qY(Po6NnP<o4d^3x`bJdCxg>W+rFCcyurwY!QDF
zwiszT74ecdiQb6dRn`ms+t5mId+;1ba0mUsU&7&pehqiSxP!}hi_`cP3wI}+FX9*$
za4q6*;cP<xFj$TST*U?4PUs7`Dcg?YRl*TC5&UoLMEnTvrMGw|XklHk+9dYkhv6AC
z#S(sEUrg>QiO$K+u{U9|9@>~0muxnCKA13B?mruo?UJaC`?x3D&Zbz$8!TfbCtK*^
z3ieMByb2q*E@W=fDeBrv`ON-1W@J}2Ud5`cO~u?hS!;^9wZO+*KlsPolIUi(u@y~D
zW4`2Y26H8DF7PooAN=$0TOt17>i?28-yFa3y0inIf>m*^LXsTMLj1z->`62}WZ3QN
zq96RX#f_6ajm90rGpmb_ijU|Z?zqan<7>@CUxHs{WpT26WgluL%k^<(gSheIxr$9p
z;k@jXJ{Zk+aiA2ui_fJ+9yRQVo3gf*MAQErqpZ--%N^wLs8uw?^0>)Uc<J9%^&-R@
Z;4dm=$mLJDH8%hN002ovPDHLkV1geY2FL&a

literal 0
HcmV?d00001

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
GIT binary patch
literal 535
zcmV+y0_gpTP)<h;3K|Lk000e1NJLTq000{R000;W1^@s63qXeZ00006VoOIv0RI60
z0RN!9r;`8x0nAB6K~zYIwbo5*Q$Z94;3vj~phYo)8&Rl&x)f=Iy6Rr5zs5!PBJOqL
zZ?K@<_zyH2#e(7oD6Qh6)u^>X#qY(Po6NnP<o4d^3x`bJdCxg>W+rFCcyurwY!QDF
zwiszT74ecdiQb6dRn`ms+t5mId+;1ba0mUsU&7&pehqiSxP!}hi_`cP3wI}+FX9*$
za4q6*;cP<xFj$TST*U?4PUs7`Dcg?YRl*TC5&UoLMEnTvrMGw|XklHk+9dYkhv6AC
z#S(sEUrg>QiO$K+u{U9|9@>~0muxnCKA13B?mruo?UJaC`?x3D&Zbz$8!TfbCtK*^
z3ieMByb2q*E@W=fDeBrv`ON-1W@J}2Ud5`cO~u?hS!;^9wZO+*KlsPolIUi(u@y~D
zW4`2Y26H8DF7PooAN=$0TOt17>i?28-yFa3y0inIf>m*^LXsTMLj1z->`62}WZ3QN
zq96RX#f_6ajm90rGpmb_ijU|Z?zqan<7>@CUxHs{WpT26WgluL%k^<(gSheIxr$9p
z;k@jXJ{Zk+aiA2ui_fJ+9yRQVo3gf*MAQErqpZ--%N^wLs8uw?^0>)Uc<J9%^&-R@
Z;4dm=$mLJDH8%hN002ovPDHLkV1geY2FL&a

literal 0
HcmV?d00001

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
GIT binary patch
literal 853
zcmV-b1FHOqP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x0}4q*K~!jg?U_Gs6EPUZe+f-W+krw`Ar-;Ef>a?ffP~bJ%rGG_F+e{7
z0|PJyBnn9V1f41*2EGCl5^PkZ6@Lec3N4Wc41j1FC1J3yaUJ{4ch2PwDUW2GlY9KW
z&uhQFvwbkY0J*_kpboSnN?me$f}xoF5^w=H4II$!-2}=GHp-IwOZ&eGtOL)0Moe8!
zxCm?#-NU|RQ~K&Dt|NLVZSg^A0@qxG6&yGxfj7Xg{QU!X3jEY~H*iZKfL&lw`|mJt
z88{+;?*KEvCr6uBJfhPxZ-FTbuY&s|u2^hLNgo0Cp|5y=HN{}hbJKuJioZ2qW8-vk
ze+M||YtRA1(pMX3(Mc&IiV?<U70xSZIBnE{6<IF;=flV@0R(vIAu29ZU$a&g(E-d^
zjHV6M*PN9Vfg!}(GMG@uJVnL9wBq+S@mCB0oKuW=KZ_G!L-BvsMhZ(Ib?9F7MpG=P
zKCNjK7C=@L*yJ;GBHN%Y+^j>GBGeTAVk6lm#|ZFP*4x1Ao=S7WDsZ39dTj*wn}Lq>
zQ3f6W+w^a_>|3^_<}$L9wV<SBp#XdXjz$erPEjo)BKcp@ECS0}tMI@wh>5TOY>|CV
zyOu4fxoZo%XlZa87?oF>(L2DHy!vo%MflS9()%!PX%uYeONcWY6DR16a)`JnPDA=Y
z`ZRIoRl)jNXx$RnsJu21rxGUYYS$#38l6L}F$-tkKG_sN%g|_2seca<=nK{Mq~U%G
zam2F+J%B&JXG25PSS^t>H>kEv0c;>PID0THr%gv06H2{bBQRmO-=)nIfN37&C55`Q
zB?%zQ6mkk6DuwzLK;jg7PxoUk4|AXaeAFA50%!mqwEyxj2i{?Lf7mwl`cR!v>Ltt=
z*RDx8s_o<2J>yGO|4bC%6yj4-fN5#`5O5Fk5&I0{JyMbFO^D*00lorPfh*d*C%|)y
z-7cs{+<7U+qkY#D$^&9)Dj*(dS@(KSYFIvp*+!rZEX9d9Wh@~y#X<>jVo@c#p07#`
fwkO{J1Nh<}ujd1_tftv(00000NkvXXu0mjfMV)YY

literal 0
HcmV?d00001

diff --git a/public/admin/skins/retro/config.json b/public/admin/skins/retro/config.json
index 140ff78b5c2..91889c2c808 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
GIT binary patch
literal 316
zcmV-C0mJ@@P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60
z0RN!9r;`8x0P#sgK~y-6rP9wXLs1Y0@L!2oHw_CSvC>B%yoIIr@DyIeO84Ce>7t1R
zNxQNTwo-&`E7#)A(c@~l>Pt@Mn=>=toIk@q(Wv;_IK&E$(Te>ioM4PI+^2%2thk9j
z+QF-6Myx+4?g*V4r|v}Vw-X%TqK3dF_Gblduv<f557)_A4%@&MPVxSWz#}$so;G?Y
z7F@mj4HiL>$W=IPOoH!Zof4l!P7yVqzc36t!b|YmtW)C8%Atb6EP+;t%)`iZI)}Ny
zAnZC`gRf?ViN9g3(iChc@AaUZ_X2mOZ;<#na*C+=g0A}C*Ztv|f5Qie&rtJ9j%H5)
O0000<MNUMnLSTZ56p-5h

literal 0
HcmV?d00001

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
GIT binary patch
literal 535
zcmV+y0_gpTP)<h;3K|Lk000e1NJLTq000{R000;W1^@s63qXeZ00006VoOIv0RI60
z0RN!9r;`8x0nAB6K~zYIwbo5*Q$Z94;3vj~phYo)8&Rl&x)f=Iy6Rr5zs5!PBJOqL
zZ?K@<_zyH2#e(7oD6Qh6)u^>X#qY(Po6NnP<o4d^3x`bJdCxg>W+rFCcyurwY!QDF
zwiszT74ecdiQb6dRn`ms+t5mId+;1ba0mUsU&7&pehqiSxP!}hi_`cP3wI}+FX9*$
za4q6*;cP<xFj$TST*U?4PUs7`Dcg?YRl*TC5&UoLMEnTvrMGw|XklHk+9dYkhv6AC
z#S(sEUrg>QiO$K+u{U9|9@>~0muxnCKA13B?mruo?UJaC`?x3D&Zbz$8!TfbCtK*^
z3ieMByb2q*E@W=fDeBrv`ON-1W@J}2Ud5`cO~u?hS!;^9wZO+*KlsPolIUi(u@y~D
zW4`2Y26H8DF7PooAN=$0TOt17>i?28-yFa3y0inIf>m*^LXsTMLj1z->`62}WZ3QN
zq96RX#f_6ajm90rGpmb_ijU|Z?zqan<7>@CUxHs{WpT26WgluL%k^<(gSheIxr$9p
z;k@jXJ{Zk+aiA2ui_fJ+9yRQVo3gf*MAQErqpZ--%N^wLs8uw?^0>)Uc<J9%^&-R@
Z;4dm=$mLJDH8%hN002ovPDHLkV1geY2FL&a

literal 0
HcmV?d00001

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
GIT binary patch
literal 853
zcmV-b1FHOqP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x0}4q*K~!jg?U_Gs6EPUZe+f-W+krw`Ar-;Ef>a?ffP~bJ%rGG_F+e{7
z0|PJyBnn9V1f41*2EGCl5^PkZ6@Lec3N4Wc41j1FC1J3yaUJ{4ch2PwDUW2GlY9KW
z&uhQFvwbkY0J*_kpboSnN?me$f}xoF5^w=H4II$!-2}=GHp-IwOZ&eGtOL)0Moe8!
zxCm?#-NU|RQ~K&Dt|NLVZSg^A0@qxG6&yGxfj7Xg{QU!X3jEY~H*iZKfL&lw`|mJt
z88{+;?*KEvCr6uBJfhPxZ-FTbuY&s|u2^hLNgo0Cp|5y=HN{}hbJKuJioZ2qW8-vk
ze+M||YtRA1(pMX3(Mc&IiV?<U70xSZIBnE{6<IF;=flV@0R(vIAu29ZU$a&g(E-d^
zjHV6M*PN9Vfg!}(GMG@uJVnL9wBq+S@mCB0oKuW=KZ_G!L-BvsMhZ(Ib?9F7MpG=P
zKCNjK7C=@L*yJ;GBHN%Y+^j>GBGeTAVk6lm#|ZFP*4x1Ao=S7WDsZ39dTj*wn}Lq>
zQ3f6W+w^a_>|3^_<}$L9wV<SBp#XdXjz$erPEjo)BKcp@ECS0}tMI@wh>5TOY>|CV
zyOu4fxoZo%XlZa87?oF>(L2DHy!vo%MflS9()%!PX%uYeONcWY6DR16a)`JnPDA=Y
z`ZRIoRl)jNXx$RnsJu21rxGUYYS$#38l6L}F$-tkKG_sN%g|_2seca<=nK{Mq~U%G
zam2F+J%B&JXG25PSS^t>H>kEv0c;>PID0THr%gv06H2{bBQRmO-=)nIfN37&C55`Q
zB?%zQ6mkk6DuwzLK;jg7PxoUk4|AXaeAFA50%!mqwEyxj2i{?Lf7mwl`cR!v>Ltt=
z*RDx8s_o<2J>yGO|4bC%6yj4-fN5#`5O5Fk5&I0{JyMbFO^D*00lorPfh*d*C%|)y
z-7cs{+<7U+qkY#D$^&9)Dj*(dS@(KSYFIvp*+!rZEX9d9Wh@~y#X<>jVo@c#p07#`
fwkO{J1Nh<}ujd1_tftv(00000NkvXXu0mjfMV)YY

literal 0
HcmV?d00001

diff --git a/tests/application/modules/admin/controllers/BibControllerTest.php b/tests/application/modules/admin/controllers/BibControllerTest.php
index 5dfb73001d9..bfe6f4e34da 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 a9cfa65dc5c..9e88dbe3691 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 9a1e33b9e51..348e871343d 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 a13440d7070..c63a62975a1 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 2e2cb69dfd4..8353e822ae7 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 22fa897a70e..7371ad0c893 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 c2923096893..0dc28b4a65b 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 2ce84cb7d49..c5338f69a40 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 6fac4ea0fd4..ddc8a16b8d1 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 124e5e3f51e..722b59f86fd 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 fad36552c37..6cbeeb7e850 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 d5d0e625326..88e7de94c7c 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 20f8fc948d4..e6c78064481 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 5b7901831f9..678e6789eed 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 9a192100f49..746e051bebb 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 00000000000..ef9f3018744
--- /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 00000000000..a19005ead07
--- /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 00000000000..e6f30ff9901
--- /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());
+  }
+}
-- 
GitLab