From 6d625d9ee184f5050a7acf9610871b37e2e83e28 Mon Sep 17 00:00:00 2001
From: efalcy <efalcy@afi-sa.fr>
Date: Tue, 28 Jan 2025 18:48:16 +0100
Subject: [PATCH 1/7] Fix error on Phase Batch Post Processing

---
 .../PhaseBatchesPostProcessingTest.php        | 40 +++++++++++--------
 1 file changed, 23 insertions(+), 17 deletions(-)

diff --git a/tests/library/Class/Cosmogramme/Integration/PhaseBatchesPostProcessingTest.php b/tests/library/Class/Cosmogramme/Integration/PhaseBatchesPostProcessingTest.php
index ba8502646cb..cb19f6a8524 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhaseBatchesPostProcessingTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhaseBatchesPostProcessingTest.php
@@ -1,6 +1,10 @@
 <?php
 /**
+<<<<<<< HEAD
  * Copyright (c) 2012-2025, Agence Française Informatique (AFI). All rights reserved.
+=======
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+>>>>>>> ed977a38e4 (Fix error on Phase Batch Post Processing)
  *
  * 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
@@ -20,9 +24,14 @@
  */
 
 
-abstract class PhaseBatchesPostProcessingTestCase
-  extends Class_Cosmogramme_Integration_PhaseTestCase
+
+
+abstract class PhaseBatchesPostProcessingTestCase extends Class_Cosmogramme_Integration_PhaseTestCase
 {
+  protected $_not_runnable;
+  protected Class_Batch_Abstract $_batch;
+  protected Class_Batch_Abstract $_not_runnable_batch;
+  protected Storm_Test_ObjectWrapper $_mock_http_client;
 
   public function setUp(): void
   {
@@ -48,32 +57,29 @@ abstract class PhaseBatchesPostProcessingTestCase
     Class_Batch::setTimeSource(new TimeSourceForTest('2025-08-29')); // tuesday for pick_day
 
     $this->fixture(Class_Batch::class,
-                   ['id'            => 34,
-                    'pick_day'      => '2',
-                    'processing_phase' => Class_Batch::AFTER_PROCESSING_RECORDS,
-                    'type'          => 'TestingTest']);
+      ['id'            => 34,
+       'pick_day'      => '2',
+       'processing_phase' => Class_Batch::AFTER_PROCESSING_RECORDS,
+       'type'          => 'TestingTest']);
 
-    $batch = $this->_batchMock('Testing Batch Prepocessing');
+    $this->_batch = $this->_batchMock('Testing Batch Prepocessing');
 
-    $this->fixture(Class_Batch::class,
-                   ['id'       => 99,
-                    'pick_day' => '3',
-                    'type'     => 'TestingNotRunnableTest']);
+    $this->_not_runnable = $this->fixture(Class_Batch::class,
+      ['id'       => 99,
+       'pick_day' => '3',
+       'type'     => 'TestingNotRunnableTest']);
 
-    $not_runnable_batch = $this->_batchMock('Testing Not Runnable Batch');
+    $this->_not_runnable_batch = $this->_batchMock('Testing Not Runnable Batch');
 
     $this
       ->onLoaderOfModel(Class_Batch::class)
-      ->whenCalled('getKnownType')->with('TestingTest')->answers($batch)
-      ->whenCalled('getKnownType')->with('TestingNotRunnableTest')->answers($not_runnable_batch);
+      ->whenCalled('getKnownType')->with('TestingTest')->answers($this->_batch)
+      ->whenCalled('getKnownType')->with('TestingNotRunnableTest')->answers($this->_not_runnable_batch);
   }
 
-
   protected function _batchMock(string $label): Class_Batch_Abstract
   {
-
     return new class($label) extends Class_Batch_Abstract {
-
       protected string $_label;
       protected bool $_has_run = false;
 
-- 
GitLab


From 15ecac77d0bfb8643817696c07ad619c6c37927e Mon Sep 17 00:00:00 2001
From: Henri-Damien LAURENT <hdlaurent@afi-sa.net>
Date: Wed, 4 Sep 2024 12:11:49 +0000
Subject: [PATCH 2/7] =?UTF-8?q?ARIEGE=20ALPHA=20V2=20squash=C3=A9e?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

task#207555 : fix reservation for Koha network

ariege_alpha_v2 faire le nettoyage à la fin du split

ariege_alpha_v2 : Fixing Permalink

dev_203587 : task_207500 : fixing Permalink

fix authentication

Migrate profile codes to ids

fixing facets for Emplacement

followup fixing error on auth

dev_203587 : task_208628 : error not managed and displayed when failing to reserve

dev_203587 : task_208361 : fix holds for Orphee network with use_card_number

dev#203587 : task#208000 : wrong facets displayed for Annexes

dev#203587: task#209507 : Fix Decalog with Centralized Holds when use_card_number set

dev#203587 : better translatability for Orphee Services

fix tests after master rebase

Fix branch test (adding one SQL fixture for article in PseudoRecordCmsTest)

dev#211651 : trying to test the updateFacetsFromExemplaires

Hotline : #214605 : fix getting annexes by id

tmp
---
 FEATURES/187054                               |   10 +
 FEATURES/204687                               |   10 +
 FEATURES/204690                               |   10 +
 VERSIONS_WIP/187054                           |    1 +
 VERSIONS_WIP/193099                           |    1 +
 VERSIONS_WIP/196022                           |    1 +
 VERSIONS_WIP/204687                           |    1 +
 VERSIONS_WIP/204690                           |    1 +
 .../admin/controllers/CmsController.php       |    5 +
 .../opac/controllers/AuthController.php       |    2 +-
 .../opac/controllers/RechercheController.php  |  716 +++++-----
 .../controllers/DataProfileController.php     |   24 +
 .../cosmo/views/scripts/annexe/index.phtml    |    6 +-
 .../scripts/data-profile/copy-codif.phtml     |    2 +
 .../views/scripts/data-profile/index.phtml    |   10 +-
 .../controllers/AnnexeControllerTest.php      |   12 +
 ...DataProfileControllerCodificationsTest.php |  295 +++++
 ...ataProfileControllerCodifsImporterTest.php |  765 +++++++++++
 .../controllers/DataProfileControllerTest.php |    6 +
 .../controllers/EmplacementControllerTest.php |   14 +-
 .../cosmo/controllers/GenreControllerTest.php |   20 +-
 .../controllers/SectionControllerTest.php     |   16 +-
 cosmogramme/css/form.css                      |   70 +-
 .../php/classes/classe_notice_integration.php |    7 +-
 cosmogramme/php/classes/classe_unimarc.php    |   33 +-
 cosmogramme/sql/patch/476.php                 |   13 +
 cosmogramme/sql/patch/patch_476.php           |   13 +
 .../php/classes/AbonneIntegrationTest.php     |    7 +-
 .../tests/php/classes/KohaPeriodiquesTest.php |   12 +-
 .../php/classes/KohaRecordIntegrationTest.php |   19 +-
 .../classes/NoticeIntegrationAloesTest.php    |    8 +-
 ...NoticeIntegrationCodificationRulesTest.php |  412 ++++++
 .../php/classes/NoticeIntegrationTest.php     |    7 +-
 library/Class/Catalogue.php                   |   13 +-
 library/Class/CodifAnnexe.php                 |  180 ++-
 library/Class/CodifEmplacement.php            |   15 +-
 library/Class/CodifSection.php                |    9 +
 .../Class/Codification/DataProfileRule.php    |   51 +
 library/Class/Codification/Rule.php           |   31 +-
 library/Class/Codification/RulesHelper.php    |  124 +-
 library/Class/CommSigb.php                    |   64 +-
 library/Class/Cosmogramme/CodifImporter.php   |  153 +++
 .../Cosmogramme/Integration/PhaseAbstract.php |    6 +
 .../Cosmogramme/Integration/PhaseNotice.php   |   14 +
 .../Cosmogramme/Integration/Record/Patron.php |    4 +-
 .../Cosmogramme/Integration/SplitBySite.php   |   15 +-
 library/Class/Exemplaire.php                  |  134 +-
 library/Class/Hold/Notify/User.php            |    2 +-
 library/Class/Hold/NotifyUser.php             |    3 +-
 library/Class/IntBib.php                      |   14 +-
 library/Class/IntBib/ForUrl.php               |   58 +
 library/Class/IntBib/SingleSigb.php           |   15 +-
 library/Class/IntProfilDonnees.php            |   16 +
 .../Migration/BranchFromCodeToIdNotice.php    |  177 +++
 .../Class/MoteurRecherche/RecordRequest.php   |   48 +-
 library/Class/Notice/Permalink.php            |   18 +-
 library/Class/ProfilePrefs.php                |   18 +
 library/Class/ProfilePrefs/Codification.php   |   71 +
 .../ProfilePrefs/Codification/Location.php    |   32 +
 .../ProfilePrefs/Codification/Section.php     |   33 +
 .../Class/ProfilePrefs/Codification/Sort.php  |   32 +
 .../Class/ProfileSerializer/UnimarcRecord.php |   21 +
 library/Class/Users.php                       |   11 +-
 .../WebService/SIGB/AbstractRESTService.php   |   22 +-
 .../Class/WebService/SIGB/AbstractService.php |   14 +-
 library/Class/WebService/SIGB/Decalog.php     |    3 +-
 .../SIGB/Decalog/BookableResponseReader.php   |    2 +-
 library/Class/WebService/SIGB/Emprunteur.php  |   61 +-
 .../WebService/SIGB/Koha/CommunityService.php |    3 +-
 .../WebService/SIGB/Koha/PatronInfoReader.php |   26 +-
 .../Class/WebService/SIGB/Koha/Service.php    |    5 +-
 .../WebService/SIGB/KohaLegacy/Service.php    |   12 +-
 .../SIGB/Nanook/PatronInfoReader.php          |    6 +-
 .../SIGB/Nanook/PreRegistration.php           |    8 +-
 .../SIGB/Nanook/PreRegistration/Data.php      |    5 +
 .../Class/WebService/SIGB/Nanook/Service.php  |   10 +-
 .../Class/WebService/SIGB/Opsys/Service.php   |    4 +-
 .../WebService/SIGB/Orphee/Emprunteur.php     |    7 +-
 .../Orphee/GetInfoUserCarteResponseReader.php |   14 +-
 .../Class/WebService/SIGB/Orphee/Service.php  |   34 +-
 .../SIGB/PreRegistration/AbstractData.php     |    4 +-
 library/ZendAfi/Auth/Adapter/CommSigb.php     |    1 +
 .../Controller/Action/Helper/NotifyError.php  |   34 +
 .../Plugin/ResourceDefinition/Emplacement.php |    3 +-
 .../Plugin/ResourceDefinition/Genre.php       |    3 +-
 .../Plugin/ResourceDefinition/Section.php     |    3 +-
 library/ZendAfi/Form/Cosmo/CodifsImporter.php |   78 ++
 library/ZendAfi/Form/Cosmo/DataProfile.php    |   78 ++
 library/ZendAfi/View/Helper/BoutonIco.php     |    1 +
 .../View/Helper/Cosmo/CodifsImporter.php      |   38 +
 .../ZendAfi/View/Helper/TagListeCoches.php    |    5 -
 .../Numel/tests/NumelTest.php                 |    8 +-
 tests/TearDown.php                            |    1 +
 .../modules/AbstractControllerTestCase.php    |    6 +
 .../controllers/CatalogueControllerTest.php   |    4 +-
 .../controllers/TypeDocsControllerTest.php    |    8 +-
 ...nneControllerSuggestionAchatNanookTest.php |   11 +-
 ...ollerPreRegistrationNanookDispatchTest.php |    9 +-
 .../AuthControllerPreRegistrationTest.php     |  112 +-
 .../opac/controllers/AuthControllerTest.php   |  184 ++-
 .../AuthControllerWithNanookTest.php          |  202 ++-
 .../controllers/NoticeAjaxControllerTest.php  |   13 +-
 .../RechercheControllerIdentifiersTest.php    |   40 +-
 ...hercheControllerReservationPergameTest.php |   87 +-
 .../RechercheControllerReservationTest.php    |  793 ++++++-----
 .../controllers/RechercheControllerTest.php   |   38 +-
 .../RechercheControllerHarryPotterTest.php    |    2 +-
 .../controllers/RechercheControllerTest.php   |    6 +-
 tests/fixtures/KohaFixtures.php               |   30 +-
 tests/fixtures/NanookFixtures.php             |   43 +-
 tests/library/Class/CommSigbTest.php          |  293 +++--
 .../Integration/PhaseItemFacetsTest.php       |   94 +-
 .../Integration/PhaseNoticeTest.php           |   67 +
 .../Integration/PhasePatronsTest.php          |   10 +-
 .../PhasePrepareIntegrationsTest.php          |   11 +-
 .../Integration/PhasePseudoRecordCmsTest.php  |   12 +
 .../Integration/bizarre_notice.txt            |  Bin 0 -> 28874 bytes
 .../Class/Cosmogramme/Integration/cabret.txt  |    1 +
 .../MoteurRechercheFacettesTest.php           |   13 +-
 tests/library/Class/MoteurRechercheTest.php   |   17 +-
 tests/library/Class/NoticeTest.php            |    7 +-
 .../Testing/WebService/SIGB/Opsys/Service.php |    9 +
 .../Class/WebService/SIGB/KohaLegacyTest.php  |   20 +-
 .../Class/WebService/SIGB/NanookTest.php      |   15 +-
 .../WebService/SIGB/OpsysServiceTest.php      |    1 +
 .../Class/WebService/SIGB/OrpheeFixtures.php  |   10 +-
 .../WebService/SIGB/OrpheeServiceTest.php     |   59 +-
 .../ZendAfi/View/Helper/Abonne/ResumeTest.php |    2 +-
 .../ZendAfi/View/Helper/FacettesTest.php      |   30 +-
 .../HandleBranchcode/HandleBranchcodeTest.php |  190 ++-
 .../SearchResult/SearchResultTest.php         |    4 +-
 .../TemplatesAbonneCentralizedHoldsTest.php   | 1154 ++++++++++++++++-
 .../TemplatesAbonneOrpheeBadgePretsTest.php   |  200 +++
 .../TemplatesAuthPreRegisterTest.php          |    7 +-
 .../Templates/TemplatesSearchTest.php         |   39 +-
 .../Templates/TemplatesWidgetTest.php         |    2 +-
 tests_db/UpgradeDBTest.php                    |  596 +++++++--
 137 files changed, 7300 insertions(+), 1555 deletions(-)
 create mode 100644 FEATURES/187054
 create mode 100644 FEATURES/204687
 create mode 100644 FEATURES/204690
 create mode 100644 VERSIONS_WIP/187054
 create mode 100644 VERSIONS_WIP/193099
 create mode 100644 VERSIONS_WIP/196022
 create mode 100644 VERSIONS_WIP/204687
 create mode 100644 VERSIONS_WIP/204690
 create mode 100644 cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/copy-codif.phtml
 create mode 100644 cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodificationsTest.php
 create mode 100644 cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodifsImporterTest.php
 create mode 100644 cosmogramme/sql/patch/476.php
 create mode 100644 cosmogramme/sql/patch/patch_476.php
 create mode 100644 cosmogramme/tests/php/classes/NoticeIntegrationCodificationRulesTest.php
 create mode 100644 library/Class/Codification/DataProfileRule.php
 create mode 100644 library/Class/Cosmogramme/CodifImporter.php
 create mode 100644 library/Class/IntBib/ForUrl.php
 create mode 100644 library/Class/Migration/BranchFromCodeToIdNotice.php
 create mode 100644 library/Class/ProfilePrefs/Codification.php
 create mode 100644 library/Class/ProfilePrefs/Codification/Location.php
 create mode 100644 library/Class/ProfilePrefs/Codification/Section.php
 create mode 100644 library/Class/ProfilePrefs/Codification/Sort.php
 create mode 100644 library/ZendAfi/Controller/Action/Helper/NotifyError.php
 create mode 100644 library/ZendAfi/Form/Cosmo/CodifsImporter.php
 create mode 100644 library/ZendAfi/View/Helper/Cosmo/CodifsImporter.php
 create mode 100644 tests/library/Class/Cosmogramme/Integration/bizarre_notice.txt
 create mode 100644 tests/library/Class/Cosmogramme/Integration/cabret.txt
 create mode 100644 tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php

diff --git a/FEATURES/187054 b/FEATURES/187054
new file mode 100644
index 00000000000..9e5a25dcd9e
--- /dev/null
+++ b/FEATURES/187054
@@ -0,0 +1,10 @@
+        '187054' =>
+            ['Label' => $this->_('Gestion par bibliothèque des codifications genre section et annexe'),
+             'Desc' => $this->_('Dans cosmogramme, il est maintenant possible de spécifier par bibliothèque les codifications : section, genre et emplacement.'),
+             'Image' => '',
+             'Video' => '',
+             'Category' => $this->('Cosmogramme'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => '',
+             'Test' => '',
+             'Date' => '2023-11-02'],
\ No newline at end of file
diff --git a/FEATURES/204687 b/FEATURES/204687
new file mode 100644
index 00000000000..a6698c5d283
--- /dev/null
+++ b/FEATURES/204687
@@ -0,0 +1,10 @@
+        '204687' =>
+            ['Label' => $this->_('Réservation d\'un doc BDP par un compte dépôt de la BDP : la fenêtre des sites de retrait est proposée'),
+             'Desc' => '',
+             'Image' => '',
+             'Video' => '',
+             'Category' => '',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => '',
+             'Test' => '',
+             'Date' => '2024-07-10'],
\ No newline at end of file
diff --git a/FEATURES/204690 b/FEATURES/204690
new file mode 100644
index 00000000000..cda282ea0b2
--- /dev/null
+++ b/FEATURES/204690
@@ -0,0 +1,10 @@
+        '204690' =>
+            ['Label' => $this->_('Compte BDP : Badge de prêts en retard'),
+             'Desc' => '',
+             'Image' => '',
+             'Video' => '',
+             'Category' => '',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => '',
+             'Test' => '',
+             'Date' => '2024-07-11'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/187054 b/VERSIONS_WIP/187054
new file mode 100644
index 00000000000..f758c79001a
--- /dev/null
+++ b/VERSIONS_WIP/187054
@@ -0,0 +1 @@
+ - fonctionnalité #187054 : Cosmogramme : Dans cosmogramme, il est maintenant possible de spécifier par bibliothèque les codifications : section, genre et emplacement.
\ No newline at end of file
diff --git a/VERSIONS_WIP/193099 b/VERSIONS_WIP/193099
new file mode 100644
index 00000000000..136546b0884
--- /dev/null
+++ b/VERSIONS_WIP/193099
@@ -0,0 +1 @@
+ - fonctionnalité #193099 : Compte lecteur : Il est maintenant possible de réserver un exemplaire via la bibliothèque centrale (BDP).
\ No newline at end of file
diff --git a/VERSIONS_WIP/196022 b/VERSIONS_WIP/196022
new file mode 100644
index 00000000000..05df97f1e7c
--- /dev/null
+++ b/VERSIONS_WIP/196022
@@ -0,0 +1 @@
+ - fonctionnalité #196022 : Compte lecteur : la création de compte à la première connexion rattache correctement celui-ci à sa bibliohtèque.
\ No newline at end of file
diff --git a/VERSIONS_WIP/204687 b/VERSIONS_WIP/204687
new file mode 100644
index 00000000000..ee41fe9209e
--- /dev/null
+++ b/VERSIONS_WIP/204687
@@ -0,0 +1 @@
+ - fonctionnalité #204687 : Réservation d'un doc BDP par un compte dépôt de la BDP : la fenêtre des sites de retrait est proposée
\ No newline at end of file
diff --git a/VERSIONS_WIP/204690 b/VERSIONS_WIP/204690
new file mode 100644
index 00000000000..39786e3343d
--- /dev/null
+++ b/VERSIONS_WIP/204690
@@ -0,0 +1 @@
+ - fonctionnalité #204690 : Compte BDP : Badge de prêts en retard
\ No newline at end of file
diff --git a/application/modules/admin/controllers/CmsController.php b/application/modules/admin/controllers/CmsController.php
index 4d0011c237a..a423b82412a 100644
--- a/application/modules/admin/controllers/CmsController.php
+++ b/application/modules/admin/controllers/CmsController.php
@@ -42,6 +42,11 @@ class Admin_CmsController extends ZendAfi_Controller_Action {
 
   protected function _renderList() {
     $bibs = Class_Bib::findAllForCurrentUserAndPortal();
+
+    $current_library = (Class_Users::isCurrentUserCanAccessAllBibs())
+      ? null
+      : $this->identity->getBib();
+
     $ids = array_map(fn($model) => $model->getId(), $bibs);
 
     $search = $this->_getParam('title_search', '');
diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php
index 80123f390cc..9cdc439fe2e 100644
--- a/application/modules/opac/controllers/AuthController.php
+++ b/application/modules/opac/controllers/AuthController.php
@@ -332,7 +332,7 @@ class AuthController extends ZendAfi_Controller_Action {
     $strategy->setDefaultUrl($url);
 
     $on_fail = function() use($strategy, $referer): void {
-      $strategy->setRedirectUrl($referer);
+      $strategy->setRedirectUrl($referer ?? '');
     };
 
     if ($target)
diff --git a/application/modules/opac/controllers/RechercheController.php b/application/modules/opac/controllers/RechercheController.php
index 4ed5978b90c..dd140a66302 100644
--- a/application/modules/opac/controllers/RechercheController.php
+++ b/application/modules/opac/controllers/RechercheController.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ * Copyright (c) 2025, 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
@@ -18,21 +18,26 @@
  * along with BOKEH; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
+
+
 class RechercheController extends ZendAfi_Controller_Action
 {
   protected $moteur;
   protected $preferences;
 
+  protected string $_code_annexe = '';
+
+
   public function __call($method_name, $args)
   {
     try {
       parent::__call($method_name, $args);
     } catch (Exception $e) {
       $this->_forward('simple',
-        'recherche',
-        'opac',
-        array_merge($this->_request->getParams(),
-          ['q' => urldecode($this->_request->getActionName())]));
+                      'recherche',
+                      'opac',
+                      array_merge($this->_request->getParams(),
+                                  ['q' => urldecode($this->_request->getActionName())]));
     }
   }
 
@@ -64,7 +69,7 @@ class RechercheController extends ZendAfi_Controller_Action
     parent::preDispatch();
     if ('json' === $this->_getParam('format')
         && 'viewnotice' === $this->_request->getActionName())
-          $this->_forward('json-record');
+      $this->_forward('json-record');
   }
 
   public function guideeAction()
@@ -72,7 +77,7 @@ class RechercheController extends ZendAfi_Controller_Action
     $this->_request->setActionName('simple');
     $this->_helper->catalogSearch(Class_Template::current()
                                   ->getSearchCriteria()
-                                   ->updateRubrique('guidee'));
+                                  ->updateRubrique('guidee'));
   }
 
   public function indexAction()
@@ -98,11 +103,20 @@ class RechercheController extends ZendAfi_Controller_Action
     $this->_helper->catalogSearch(Class_Template::current()->getSearchCriteria());
   }
 
+
+  protected function _getTitreRechercheViewNotice($criteres_recherche): void
+  {
+    if($title = $this->view->search_Title($criteres_recherche, $this->_request, $this))
+      $this->view->titreAdd($title);
+  }
+
+
   public function appelmenuAction()
   {
     $this->_forward('simple');
   }
 
+
   public function avanceeAction()
   {
     $this->view->annexes = Class_CodifAnnexe::getMultiOptions();
@@ -152,8 +166,8 @@ class RechercheController extends ZendAfi_Controller_Action
       new Class_Notice_NavigationRecherche(Class_Template::current()
                                            ->getSearchCriteria()
                                            ->setParams($this->_request->getParams()),
-        $this->moteur,
-        $notice);
+                                           $this->moteur,
+                                           $notice);
 
     if ($navigation = $this->_getParam('navigation')) {
       $notice_to_show = ('suivant' === $navigation
@@ -167,12 +181,14 @@ class RechercheController extends ZendAfi_Controller_Action
 
     if ($this->_getParam('code_rebond', false)
         || !$this->_getParam('retour_abonne', false) && !$this->_getParam('retour_avis', false))
-          $this->view->notice_navigation = $notice_navigation;
+      $this->view->notice_navigation = $notice_navigation;
 
     $current_module = $this->_getParam('current_module');
 
-    $current_module['preferences'] = $this->preferences = $this->extractProfilFromUrl()
-                                                               ->getCfgModulesPreferences('recherche', 'viewnotice', $notice->getTypeDoc());
+    $current_module['preferences'] =
+      $this->preferences =
+      $this->extractProfilFromUrl()
+           ->getCfgModulesPreferences('recherche', 'viewnotice', $notice->getTypeDoc());
 
     $current_module['action2'] = $notice->getTypeDoc();
     $this->view->_current_module = $current_module;
@@ -184,7 +200,7 @@ class RechercheController extends ZendAfi_Controller_Action
     $criteres_recherche = Class_Template::current()
       ->getSearchCriteria()
       ->setParams($this->_request->getParams(),
-        $this->preferences);
+                  $this->preferences);
     $this->view->criteres_recherche = $criteres_recherche;
 
     $this->view->display_modifier_vignette_link = Class_Users::isCurrentUserCanAccesBackend() && $notice->isVignetteUpdatableToCacheServer();
@@ -206,46 +222,72 @@ class RechercheController extends ZendAfi_Controller_Action
     $this->view->url_panier = $this->view->url(['controller' => 'panier',
                                                 'action'     => 'add-record-ajax',
                                                 'id_notice'  => $id_notice],
-      null,
-      true);
+                                               null,
+                                               true);
 
     $this->_helper->trackEvent('recherche',
-      'notice',
-      ['titre'   => $notice->getTitrePrincipal(),
-       'auteur'  => $notice->getAuteurPrincipal(),
-       'editeur' => $notice->getFirstEditeur(),
-       'tome'    => $notice->getTomeAlpha(),
-       'support' => $notice->getTypeDocLabel(),
-      ],
-      $notice->getId());
+                               'notice',
+                               ['titre'   => $notice->getTitrePrincipal(),
+                                'auteur'  => $notice->getAuteurPrincipal(),
+                                'editeur' => $notice->getFirstEditeur(),
+                                'tome'    => $notice->getTomeAlpha(),
+                                'support' => $notice->getTypeDocLabel(),
+                               ],
+                               $notice->getId());
 
     $stat = new Class_StatsNotices;
     $stat->addStatVisu($id_notice);
 
     Class_ScriptLoader::getInstance()->loadBabeltheque();
+    return null;
+  }
+
+
+  protected function _authorityFromParams()
+  {
+    if ((!$authority_id = $this->_getParam('authority_id'))
+        || (!$thesaurus_id = $this->_getParam('thesaurus_id')))
+      return null;
+
+    $items = Class_Exemplaire::findAllBy(['type'       => Class_Notice::TYPE_AUTHORITY,
+                                          'id_origine' => $authority_id]);
+    foreach ($items as $item)
+      if ($record = $this->_recordWithThesaurusFacet($thesaurus_id, $item))
+        return $record;
 
     return null;
   }
 
-  public function customizedInputAction()
+
+  protected function _recordWithThesaurusFacet($thesaurus_id, $item)
   {
+    if (!$record = $item->getNotice())
+      return null;
+
+    return in_array(Class_CodifThesaurus::CODE_FACETTE . $thesaurus_id, $record->getFacetCodes())
+      ? $record
+      : null;
+  }
+
+
+  public function customizedInputAction() {
     session_write_close();
     $this->_helper->getHelper('viewRenderer')->setNoRender();
 
     if ((!$form = Class_SearchForm::find($this->_getParam('form')))
         || (!$form = $form->getForm()))
-        return;
+      return;
     if ((!$element = $form->getElement($this->_getParam('element')))
         || (!$axe = $element->getAxe($this->_getParam('axe')))
         || (!$type = (new Class_CriteresRecherche_AxesParamTypes)->typeFor($axe)))
-        return;
+      return;
     $name = $element->getName() . '_' . $axe['name'];
     $id = $name . '_' . bin2hex(random_bytes(8));
 
     $element = $form->createElement($type->input(),
-      $name,
-      array_merge(['id' => $id],
-        $type->inputOptions()));
+                                    $name,
+                                    array_merge(['id' => $id],
+                                                $type->inputOptions()));
 
     if ($value = $this->_getParam('value'))
       $element->setValue($value);
@@ -265,7 +307,7 @@ class RechercheController extends ZendAfi_Controller_Action
     $this->_helper->getHelper('viewRenderer')->setNoRender();
 
     $result = $this->_findRecordByKeyOrId((string) $this->_getParam('clef'),
-      (int) $this->_getParam('id'));
+                                          (int) $this->_getParam('id'));
     ($record = $result->getRecord())
       ? $this->_helper->json($record
                              ->acceptVisitor(new Class_Notice_JsonVisitor)
@@ -314,10 +356,10 @@ class RechercheController extends ZendAfi_Controller_Action
     if (!($record = Class_Notice::find((int) $this->_getParam('id_notice'))))
       return $this->_redirectToIndex();
     $this->_redirect($this->view->absoluteUrl($record->fetchUrlLocalVignette()));
-
     return null;
   }
 
+
   public function rawThumbnailAction()
   {
     $viewRenderer = $this->getHelper('ViewRenderer');
@@ -327,24 +369,24 @@ class RechercheController extends ZendAfi_Controller_Action
     $this->_response->setHeader('Content-Type', 'image/png');
     if (!($record = Class_Notice::find((int) $this->_getParam('id'))))
       return $this->_redirectToIndex();
+
     (new Class_WebService_Vignette)
       ->renderThumbnail($record->getId(),
-        $record->getTitrePrincipal(),
-        $record->getTypeDoc());
+                        $record->getTitrePrincipal(),
+                        $record->getTypeDoc());
 
     return null;
   }
 
+
   public function reservationAction()
   {
     if (!$this->userConnected())
-    return;
-    if ((!$library = Class_Bib::find((int) $this->_getParam('id_bib')))
-        || !$record = Class_Notice::find((int) $this->_getParam('id_notice'))) {
-      $this->_redirectToIndex();
-
       return;
-    }
+
+    if ((!$library = Class_Bib::find((int)$this->_getParam('id_bib')))
+        || !$record = Class_Notice::find((int)$this->_getParam('id_notice')))
+      return $this->_redirectToIndex();
 
     $form = new ZendAfi_Form_Hold(['action'       => $this->view->url(),
                                    'data-backurl' => $this->view->url(['action' => 'viewnotice']),
@@ -380,7 +422,7 @@ class RechercheController extends ZendAfi_Controller_Action
       if ($message = (new Class_Mail)
           ->setFrom(Class_Mail::getNoReplyMail())
           ->sendMail($subject, $message_user . $message, $form->getValue('user_mail')))
-            $this->_helper->notify($this->_("Erreur d'envoi du mail : %s", $message), ['class' => 'error']);
+        $this->_helper->notify($this->_("Erreur d'envoi du mail : %s", $message), ['class' => 'error']);
 
       $this->_redirect('opac/recherche/viewnotice/id/' . $record->getId() . '?type_doc=' . $record->getTypeDoc());
 
@@ -409,14 +451,15 @@ class RechercheController extends ZendAfi_Controller_Action
 
     $data = Zend_Controller_Action_HelperBroker::getStaticHelper('Json')->encodeJson($json);
     $this->_response->setBody($data);
-
     return null;
   }
 
+
   public function consultationajaxAction()
   {
     if (!$this->userConnected())
       return null;
+
     $title = $this->view->_('Consultation sur place');
     if (!$exemplaire = Class_Exemplaire::find($this->_getParam('copy_id')))
       return $this->renderPopupResult($title, $this->view->_('L\'exemplaire n\'existe pas'));
@@ -426,10 +469,9 @@ class RechercheController extends ZendAfi_Controller_Action
       ->onPlaceConsultationBooking(Class_Users::getIdentity(), $exemplaire, $service_point);
 
     $this->renderPopupResult($title, $this->_getConsultationBookingMessage($response));
-
-    return null;
   }
 
+
   public function reservationajaxAction()
   {
     $viewRenderer = $this->getHelper('ViewRenderer');
@@ -439,31 +481,26 @@ class RechercheController extends ZendAfi_Controller_Action
       return null;
     $id_bib = (int) $this->_getParam('id_bib');
     $id_exemplaire = $this->_getParam('copy_id');
-    $code_annexe = $this->_getParam('code_annexe', '');
-
-    if (Class_CosmoVar::isSiteRetraitResaPatronLibrary())
-      $code_annexe = $user->getUserIdSite();
 
     if (!$item = Class_Exemplaire::find($id_exemplaire))
       return $this->renderPopupResult($this->view->_('Réservation'),
-        $this->_('Document introuvable'));
+                                      $this->_('Document introuvable'));
 
-    if (Class_CosmoVar::isSiteRetraitResaItemLibrary())
-      $code_annexe = $item->getAnnexe();
+    $code_annexe = $this->_getCodeAnnexe($user, $item);
 
     if ($item->requiresCalendarHold())
-      return $this->_redirect(sprintf('/recherche/reservation-calendar-ajax/id_bib/%s/copy_id/%s/code_annexe/%s',
-        $id_bib,
-        $id_exemplaire,
-        $code_annexe));
-
-    $ret = Class_CommSigb::getInstance()->reserveItem($item,
-      $code_annexe ?? '');
-
-    $this->renderPopupResult($this->view->_('Réservation'),
-      $this->_getHoldMessage($user, $id_exemplaire, $ret));
-
-    return null;
+      return $this->_redirect($this
+                              ->view
+                              ->url(['module' => 'opac',
+                                     'controller' => 'recherche',
+                                     'action' => 'reservation-calendar-ajax',
+                                     'id_bib' => $id_bib,
+                                     'copy_id' => $id_exemplaire,
+                                     'code_annexe' => $code_annexe ?? '']));
+
+    $ret = Class_CommSigb::getInstance()->reserveItem($item, $code_annexe ?? '');
+    return $this->renderPopupResult($this->view->_('Réservation'),
+                                    $this->_getHoldMessage($user, $item, $ret));
   }
 
   public function reservationCalendarAjaxAction()
@@ -477,20 +514,20 @@ class RechercheController extends ZendAfi_Controller_Action
       ->newForm(['id'  => 'reservation-calendar'])
       ->setAction($this->view->url())
       ->addElement('dateRangePicker',
-        'reservation-date',
-        ['label' => $this->_('Période de réservation'),
-         'start' => ['name'       => 'reservedate',
-                     'allowEmpty' => false,
-                     'DateFormat' => 'DD/MM/YYYY',
-                     'validators' => [new Zend_Validate_Date('DD/MM/YYYY')]],
-         'end' => ['name'       => 'expirationdate',
-                   'allowEmpty' => false,
-                   'DateFormat' => 'DD/MM/YYYY',
-                   'validators' => [new Zend_Validate_Date('DD/MM/YYYY')]],
-        ])
+                   'reservation-date',
+                   ['label' => $this->_('Période de réservation'),
+                    'start' => ['name'       => 'reservedate',
+                                'allowEmpty' => false,
+                                'DateFormat' => 'DD/MM/YYYY',
+                                'validators' => [new Zend_Validate_Date('DD/MM/YYYY')]],
+                    'end' => ['name'       => 'expirationdate',
+                              'allowEmpty' => false,
+                              'DateFormat' => 'DD/MM/YYYY',
+                              'validators' => [new Zend_Validate_Date('DD/MM/YYYY')]],
+                   ])
       ->addElement('Submit', $this->_('Valider'))
       ->addElement('Button', $this->_('Annuler'),
-        ['onclick' => 'opacDialogClose();return false;'])
+                   ['onclick' => 'opacDialogClose();return false;'])
       ->addUniqDisplayGroup('hold');
 
     if ($this->_request->isPost() && $form->isValid($this->_request->getPost())) {
@@ -499,16 +536,16 @@ class RechercheController extends ZendAfi_Controller_Action
 
       $comm_sigb = Class_CommSigb::getInstance();
       $comm_sigb->holdCalendar(
-        $id_exemplaire,
-        $code_annexe,
-        $reservedate,
-        $expirationdate);
+                               $id_exemplaire,
+                               $code_annexe,
+                               $reservedate,
+                               $expirationdate);
 
-      if (!$comm_sigb->hasErrors())
+      if ( !$comm_sigb->hasErrors())
         return $this->renderPopupResult($this->view->_('Réservation'),
-          $this->_getHoldMessage($this->userConnected(),
-            $id_exemplaire,
-            ['erreur' => '']));
+                                        $this->_getHoldMessage($this->_userConnected(),
+                                                               Class_Exemplaire::find($id_exemplaire),
+                                                               ['erreur' =>'']));
 
       $form
         ->addErrors($comm_sigb->getErrors())
@@ -519,15 +556,140 @@ class RechercheController extends ZendAfi_Controller_Action
     $form = $this->view->renderForm($form);
 
     return $this->renderPopupResult($this->view->_('Réservation'),
-      Class_ScriptLoader::getInstance()->html()
-      . $holds
-      . $form);
+                                    Class_ScriptLoader::getInstance()->html()
+                                    . $holds
+                                    . $form);
   }
 
+
+  protected function _renderHolds($id_exemplaire) {
+    $item = Class_Exemplaire::find($id_exemplaire);
+    $holds = Class_CommSigb::getInstance()->holdsForItem($item)['holds'] ?? [];
+    usort($holds,
+          fn($a, $b) => strcmp($a->getReserveDate(), $b->getReserveDate()));
+
+    $html = implode('',
+                    array_map(fn($hold) => $this->view->tag('li',
+                                                            $hold->renderRange()),
+                              $holds));
+
+    $js_holds = json_encode(array_map(
+                                      fn($hold) => [$hold->getReserveDate(), $hold->getExpirationDate()],
+                                      $holds));
+
+    Class_ScriptLoader::getInstance()
+      ->addAdminScript('jquery_ui_datepicker_i18n/datepicker-fr.js')
+      ->addJQueryReady('
+                    Date.prototype.withoutTime = function () {
+                        var d = new Date(this);
+                        d.setHours(0, 0, 0, 0);
+                        return d;
+                    }
+                    $.datepicker.setDefaults($.datepicker.regional["' . ((Zend_Registry::get('locale') == 'en_US') ? '' : 'fr' ). '"]);
+                       $("input").blur();
+                       $("#holds_view").datepicker(
+                        {
+                          numberOfMonths: 3,
+                          beforeShowDay: function(date) {
+                                            var holds = '. $js_holds . ';
+                                            var day = date.withoutTime();
+                                            var hold = holds.find( (hold) => {
+                                               return (new Date(hold[0]).withoutTime() <= day) && (day <= new Date(hold[1]).withoutTime());
+                                            });
+
+                                           if (undefined == hold)
+                                              return [false, "day-without-hold"];
+
+                                            return [false, "day-with-hold", "' . $this->_("Reservation déjà présente ce jour") . '"];
+                                          }}
+                      );');
+
+    return '<div id="holds_view"></div><div>'
+      . $this->view->tag('span', $this->_('Réservations non disponibles : '))
+      . $this->view->tag('ul',$html) . '</div>';
+  }
+
+
+  protected function _getHoldMessage(Class_Users $user,
+                                     ?Class_Exemplaire $item,
+                                     array $response): string
+  {
+    if ($response['erreur'] ?? false)
+      return $response['erreur'];
+
+    if (isset($response['popup']) && $response['popup'])
+      return '<iframe src="' . $response['popup'] . '"></iframe>';
+
+    $content = $this->_('Votre réservation est enregistrée.<br>Nous vous informerons quand le document%s sera disponible',
+                        $this->_getMessageNoticeLabel($item->getId()));
+
+    return ($location_label = $this->_getPickupLocationLabelForUserAndItem($user, $item))
+      ? $content . $this->_(' pour être retiré à : %s',$location_label)
+      : $content;
+  }
+
+
+  protected function _getPickupLocationLabelForUserAndItem(Class_Users $user, Class_Exemplaire $item) : string {
+    $current_hold = null;
+    $item_id = $item->getId();
+    $current_hold = (new Storm_Collection($user->getReservations()))
+      ->detect(fn($hold) => ($opac_item = $hold->getExemplaireOPAC()) && ($item_id == $opac_item->getId()));
+
+    return ($current_hold && $location_label = $current_hold->getPickupLocationLabel())
+      ? $location_label
+      : '';
+  }
+
+
+  protected function _getMessageNoticeLabel($item_id)
+  {
+    if (!$item_id
+        || !($item = Class_Exemplaire::find($item_id))
+        || !($notice = $item->getNotice()))
+      return '';
+
+    $title = strip_tags($notice->getTitrePrincipal());
+
+    if (!$notice->getAuteurPrincipal())
+      return sprintf(" '%s'", $title);
+
+    return sprintf(" '%s / %s'",
+                   $title,
+                   $notice->getAuteurPrincipal());
+  }
+
+
+
+  protected function _userConnected():?Class_Users {
+    if (! $user = Class_Users::getIdentity())
+      $this->_forwardToLogin($this->view->url(), '');
+
+    return $user;
+  }
+
+
+  protected function getReservationForm($action = 'reservation-pickup-ajax')
+  {
+    return $this->view
+      ->newForm(['id'     => 'pickup',
+                 'class'  => 'zend_form reservation_pickup',
+                 'action' => $this->view->url(['controller' => 'recherche',
+                                               'action'     => $action])])
+      ->addElement('Radio',
+                   'code_annexe',
+                   ['required'   => true,
+                    'allowEmpty' => false,
+                    'escape'     => false])
+
+      ->addDisplayGroup(['code_annexe'], 'group', ['legend' => 'Site de retrait']);
+  }
+
+
   public function consultationPickupAjaxAction()
   {
     if (!$this->userConnected())
       return null;
+
     if ($this->_request->isPost()) {
       return $this->_forward('consultationajax');
     }
@@ -538,141 +700,106 @@ class RechercheController extends ZendAfi_Controller_Action
     if (!$exemplaire = Class_Exemplaire::find($this->_getParam('copy_id')))
       return $this->renderPopupResult($this->view->_('Lieu de mise à disposition demandé'), $this->view->_('L\'exemplaire n\'existe pas'));
     $locations = Class_CommSigb::getInstance()->pickupLocation(Class_Users::getIdentity(), $exemplaire);
+
     foreach ($locations as $location) {
       foreach ($location->getServicePoints() as $service_point)
         $radio->addMultiOption($service_point->getCode(), $service_point->getLabel());
     }
 
-    $this->renderPopupResult($this->view->_('Lieu de mise à disposition demandé'),
-      $this->view->renderForm($form));
-
-    return null;
+    return $this->renderPopupResult($this->view->_('Lieu de mise à disposition demandé'),
+                                    $this->view->renderForm($form));
   }
 
-  public function reservationPickupAjaxAction()
-  {
-    if (!$this->userConnected())
-      return null;
-    if (($central_library_id = Class_CosmoVar::get('centralized_hold_mode'))
-        && in_array($central_library_id, [ $this->_getParam('code_annexe'),
-                                           $this->_getParam('int_bib'),
-                                           ])) {
-      $this->_forward('reservationajax');
-
-      return null;
-    }
-
-    if (!Class_CosmoVar::isSiteRetraitResaChoiceEnabled())
-      return $this->_forward('reservationajax');
-    $form = $this->getReservationForm();
-    $radio = $form->getElement('code_annexe');
-    $locations = $this->arePickupLocationsProvidedByComm()
-      ? $this->_addLocationsFromCommTo($radio)
-      : $this->_addLocationsFromCodifTo($radio);
-
-    $this->_setLocationTo($radio, $locations);
-    if ($this->_request->isPost()
-        && $form->isValid($this->_request->getPost())) {
-      return $this->_redirect($this->view->url(['module'      => 'opac',
-                                                'controller'  => 'recherche',
-                                                'action'      => 'reservationajax',
-                                                'render'      => null,
-                                                'code_annexe' => $this->_request->getPost('code_annexe')]));
-    }
 
-    $html = [];
-    if ($message = trim(Class_AdminVar::getValueOrDefault('HOLD_SITE_SELECTION_CUSTOM_MESSAGE')))
-      $html[] = $this->view->div(['class' => 'hold_site_selection_message'], $message);
+  public function reservationPickupAjaxAction() {
+    if ( ! $user = $this->_userConnected())
+      return ;
 
-    $html[] = (empty($radio->getMultiOptions())
-                ? $this->view->tag('p',
-                  $this->_('Aucun site de retrait disponible'),
-                  ['class' => 'error'])
-                : $this->view->renderForm($form));
+    if ( ! $item = Class_Exemplaire::find($this->_getParam('copy_id')))
+      return $this->renderPopupResult($this->view->_('Réservation'),
+                                      $this->_('Document introuvable'));
 
-    $this->renderPopupResult($this->view->_('Lieu de mise à disposition demandé'),
-      implode('', $html));
+    if ( ! $this->_shouldDisplayPickupLocations($user))
+      return $this->_holdItem($user, $item);
 
-    return null;
+    return ($item->canBeHeldFromCentralizedLibrary())
+      ?  $this->_holdItem($user, $item)
+      : $this->_pickupLocationForm($user);
   }
 
-  public function setMoteurRecherche($moteur)
-  {
-    $this->moteur = $moteur;
+
+  protected function _holdItemUnavailable(): void {
+    $this->renderPopupResult($this->_('Réservation impossible'),
+                             $this->_('Vous ne pouvez pas réserver cet exemplaire !'));
   }
 
-  public function downloadRecordAction()
-  {
-    if (!$record = Class_Notice::find($this->_getParam('id', 0)))
-      return $this->_redirect('/');
-    $this->_helper->binaryDownload($record->getUnimarc(), $record->getId() . '.txt');
 
-    return null;
-  }
+  protected function _holdItem(Class_Users $user, Class_Exemplaire $item): void {
+    $code_annexe = $this->_getCodeAnnexe($user, $item);
 
-  public function downloadItemAction()
-  {
-    if (!$item = Class_Exemplaire::find($this->_getParam('id', 0)))
-      return $this->_redirect('/');
-    if (!$raw_record = Class_Cosmogramme_Integration_RawRecord::findFor($item))
-      return $this->_redirect('/');
-    $this->_helper->binaryDownload($raw_record->getData(), $raw_record->getName());
+    $response = Class_CommSigb::getInstance()->reserveItem($item,
+                                                           $code_annexe);
 
-    return null;
+    $this->renderPopupResult($this->_('Réservation'),
+                             $this->_getHoldMessage($user, $item, $response));
   }
 
-  public function clearLastSearchSessionAction()
-  {
-    $this->_helper->getHelper('viewRenderer')->setNoRender();
-    Zend_Registry::get('session')->last_search = null;
-  }
 
-  public function reserverAction()
+  protected function _getCodeAnnexe( Class_Users $user,
+                                     Class_Exemplaire $item): string
   {
-    $this->view->titre = $this->_('Réserver un document');
+    if ($this->_code_annexe)
+      return $this->_code_annexe;
 
-    if (!$record = Class_Notice::find($this->_getParam('record_id', 0)))
-      return $this->view->_error_message = $this->_('Document introuvable');
-    if (!$record->hasExemplaires())
-      return $this->view->_error_message = $this->_('Le document n\'a pas d\'exemplaire.');
-    $this->view->items = (new Intonation_Library_Record($record))
-      ->getItemsFromSIGB();
+    $code_annexe = $this->_getParam('code_annexe', '');
 
-    return null;
-  }
+    if (Class_CosmoVar::isSiteRetraitResaPatronLibrary())
+      $code_annexe = $user->getUserIdSite();
 
-  protected function _getTitreRechercheViewNotice($criteres_recherche)
-  {
-    if ($title = $this->view->search_Title($criteres_recherche, $this->_request, $this))
-      return $this->view->titreAdd($title);
+    if (Class_CosmoVar::isSiteRetraitResaItemLibrary())
+      $code_annexe = $item->annexeCode();
 
-    return null;
+    return $this->_code_annexe = $code_annexe;
   }
 
-  protected function _authorityFromParams()
+
+  protected function _pickupLocationForm(Class_Users $user): void
   {
-    if ((!$authority_id = $this->_getParam('authority_id'))
-        || (!$thesaurus_id = $this->_getParam('thesaurus_id')))
-          return null;
-    $items = Class_Exemplaire::findAllBy(['type'       => Class_Notice::TYPE_AUTHORITY,
-                                          'id_origine' => $authority_id]);
-    foreach ($items as $item)
-      if ($record = $this->_recordWithThesaurusFacet($thesaurus_id, $item))
-        return $record;
+    $form = $this->getReservationForm();
 
-    return null;
-  }
+    $radio = $form->getElement('code_annexe');
 
-  protected function _recordWithThesaurusFacet($thesaurus_id, $item)
-  {
-    if (!$record = $item->getNotice())
-      return null;
+    if (($user = Class_Users::getIdentity())
+        && ($comm = $user->getSIGBComm()))
+      $this->_setPickupLocations($radio, $comm);
 
-    return in_array(Class_CodifThesaurus::CODE_FACETTE . $thesaurus_id, $record->getFacetCodes())
-      ? $record
-      : null;
+    if ($this->_request->isPost()
+        && $form->isValid($this->_request->getPost()))
+    {
+
+      $this->_redirect($this->view->url(['module'      => 'opac',
+                                         'controller'  => 'recherche',
+                                         'action'      => 'reservationajax',
+                                         'render'      => null,
+                                         'code_annexe' => $this->_request->getPost('code_annexe')]));
+      return;
+    }
+
+    $html = [];
+    if ($message = trim(Class_AdminVar::getValueOrDefault('HOLD_SITE_SELECTION_CUSTOM_MESSAGE')))
+      $html[] = $this->view->div(['class' => 'hold_site_selection_message'], $message);
+
+    $html[] = (empty($radio->getMultiOptions())
+               ? $this->view->tag('p',
+                                  $this->_('Aucun site de retrait disponible'),
+                                  ['class' => 'error'])
+               : $this->view->renderForm($form));
+
+    $this->renderPopupResult($this->view->_('Lieu de mise à disposition demandé'),
+                             implode('', $html));
   }
 
+
   protected function _findRecordByKeyOrId($key, $id)
   {
     $result = new RechercheController_FindRecordByKeyOrIdResult;
@@ -696,171 +823,142 @@ class RechercheController extends ZendAfi_Controller_Action
     return $this->view->_('Votre réservation est enregistrée.');
   }
 
-  protected function _renderHolds($id_exemplaire)
+
+  protected function userConnected()
   {
-    $item = Class_Exemplaire::find($id_exemplaire);
-    $holds = Class_CommSigb::getInstance()->holdsForItem($item)['holds'] ?? [];
-    usort($holds,
-      fn($a, $b) => strcmp($a->getReserveDate(), $b->getReserveDate()));
+    if (!$user = Class_Users::getIdentity()) {
+      $this->_forwardToLogin($this->view->url(), '');
 
-    $html = implode('',
-      array_map(fn($hold) => $this->view->tag('li',
-        $hold->renderRange()),
-        $holds));
+      return false;
+    }
 
-    $js_holds = json_encode(array_map(
-      fn($hold) => [$hold->getReserveDate(), $hold->getExpirationDate()],
-      $holds));
+    return $user;
+  }
 
-    Class_ScriptLoader::getInstance()
-      ->addAdminScript('jquery_ui_datepicker_i18n/datepicker-fr.js')
-      ->addJQueryReady('
-                    Date.prototype.withoutTime = function () {
-                        var d = new Date(this);
-                        d.setHours(0, 0, 0, 0);
-                        return d;
-                    }
-                    $.datepicker.setDefaults($.datepicker.regional["' . (('en_US' == Zend_Registry::get('locale')) ? '' : 'fr') . '"]);
-                       $("input").blur();
-                       $("#holds_view").datepicker(
-                        {
-                          numberOfMonths: 3,
-                          beforeShowDay: function(date) {
-                                            var holds = ' . $js_holds . ';
-                                            var day = date.withoutTime();
-                                            var hold = holds.find( (hold) => {
-                                               return (new Date(hold[0]).withoutTime() <= day) && (day <= new Date(hold[1]).withoutTime());
-                                            });
 
-                                           if (undefined == hold)
-                                              return [false, "day-without-hold"];
+  protected function _addLocationsFromCodifTo(Class_WebService_SIGB_LibraryWrapper $comm): array
+  {
+    $locations = [];
 
-                                            return [false, "day-with-hold", "' . $this->_('Reservation déjà présente ce jour') . '"];
-                                          }}
-                      );');
+    foreach ($comm->codifAnnexesForUrlServer() as $annexe)
+      $locations[$annexe->getCode()] = $annexe->getLibelle();
 
-    return '<div id="holds_view"></div><div>'
-      . $this->view->tag('span', $this->_('Réservations non disponibles : '))
-      . $this->view->tag('ul', $html) . '</div>';
+
+    return $locations;
   }
 
-  protected function _getHoldMessage($user, $item_id, $response)
+
+  protected function _setPickupLocations(Zend_Form_Element $element,
+                                         Class_WebService_SIGB_LibraryWrapper $comm): self
   {
-    if ($response['erreur'])
-      return $response['erreur'];
-    if (isset($response['popup']) && $response['popup'])
-      return '<iframe src="' . $response['popup'] . '"></iframe>';
-    $current_hold = null;
-    foreach ($user->getReservations() as $hold) {
-      if (!$opac_item = $hold->getExemplaireOPAC())
-      continue;
-      if ($item_id == $opac_item->getId()) {
-        $current_hold = $hold;
-
-        break;
-      }
-    }
+    $locations = $this->_arePickupLocationsProvidedByUserComm($comm)
+      ? $this->_addLocationsFromCommTo($comm)
+      : $this->_addLocationsFromCodifTo($comm);
 
-    return ($current_hold && ($location_label = $current_hold->getPickupLocationLabel()))
-      ? $this->view->_('Votre réservation est enregistrée.<br>Nous vous informerons quand le document%s sera disponible pour être retiré à : %s',
-        $this->_getMessageNoticeLabel($item_id), $location_label)
-      : $this->view->_('Votre réservation est enregistrée.<br>Nous vous informerons quand le document%s sera disponible',
-        $this->_getMessageNoticeLabel($item_id));
+    return $this->_setLocationTo($element, $locations);
   }
 
-  protected function _getMessageNoticeLabel($item_id)
+
+  protected function _setLocationTo(Zend_Form_Element $element, array $locations): self
   {
-    if (!$item_id
-        || !($item = Class_Exemplaire::find($item_id))
-        || !($notice = $item->getNotice()))
-          return '';
-    $title = strip_tags($notice->getTitrePrincipal());
+    $requested_location = $this->_getParam('code_annexe');
+    if (($user = Class_Users::getIdentity())
+        && ($user_location = $user->getLibraryCode())
+        && ($locations[$user_location] ?? ''))
+      $requested_location = $user_location;
 
-    if (!$notice->getAuteurPrincipal())
-      return sprintf(" '%s'", $title);
+    foreach ($locations as $k => $v)
+      $element->addMultiOption($k, $v);
 
-    return sprintf(" '%s / %s'",
-      $title,
-      $notice->getAuteurPrincipal());
+    if ($locations[$requested_location] ?? '')
+      $element->setValue($requested_location);
+
+    return $this;
   }
 
-  protected function userConnected()
+
+  protected function _addLocationsFromCommTo(Class_WebService_SIGB_LibraryWrapper $comm): array
   {
-    if (!$user = Class_Users::getIdentity()) {
-      $this->_forwardToLogin($this->view->url(), '');
+    if ( ! $item = Class_Exemplaire::find($this->_getParam('copy_id')))
+      return [];
+
+    $first = '';
+    $locations = $comm->pickupLocationsFor(Class_Users::getIdentity(), $item);
+    return ((isset($locations['statut']) && ! $locations['statut'])
+            ? []
+            : $locations);
+  }
 
+
+  protected function _shouldDisplayPickupLocations(Class_Users $user): bool
+  {
+    if ( ! Class_CosmoVar::isSiteRetraitResaChoiceEnabled())
       return false;
-    }
 
-    return $user;
+    if ( ! $comm = Class_Users::getIdentity()->getSIGBComm())
+      return false;
+
+    return $comm->providesPickupLocations()
+      || (count($comm->codifAnnexesForUrlServer()) > 1);
   }
 
-  protected function getReservationForm($action = 'reservation-pickup-ajax')
+
+  protected function _arePickupLocationsProvidedByUserComm(Class_WebService_SIGB_LibraryWrapper $comm): bool
   {
-    return $this->view
-      ->newForm(['id'     => 'pickup',
-                 'class'  => 'zend_form reservation_pickup',
-                 'action' => $this->view->url(['controller' => 'recherche',
-                                               'action'     => $action])])
-      ->addElement('Radio',
-        'code_annexe',
-        ['required'   => true,
-         'allowEmpty' => false,
-         'escape'     => false])
+    if ( !Class_CosmoVar::isSiteRetraitResaChoiceEnabled())
+      return false;
 
-      ->addDisplayGroup(['code_annexe'], 'group', ['legend' => 'Site de retrait']);
+    return $comm->providesPickupLocations();
   }
 
-  protected function _addLocationsFromCodifTo(Zend_Form_Element $element): array
-  {
-    $locations = [];
 
-    foreach (Class_CodifAnnexe::findAllByPickup() as $annexe) {
-      $element->addMultiOption($annexe->getCode(), $annexe->getLibelle());
-      $locations[$annexe->getCode()] = $annexe->getLibelle();
-    }
+  public function setMoteurRecherche($moteur) {
+    $this->moteur = $moteur;
+  }
 
-    return $locations;
+
+  public function downloadRecordAction() {
+    if (!$record = Class_Notice::find($this->_getParam('id', 0)))
+      return $this->_redirect('/');
+
+    $this->_helper->binaryDownload($record->getUnimarc(), $record->getId() . '.txt');
   }
 
-  protected function _setLocationTo(Zend_Form_Element $element, array $locations)
-  {
-    $requested_location = $this->_getParam('code_annexe');
-    if (($user = Class_Users::getIdentity())
-        && ($user_location = $user->getUserIdSite())
-        && ($locations[$user_location] ?? ''))
-          $requested_location = $user_location;
 
-    if ($locations[$requested_location] ?? '')
-      $element->setValue($requested_location);
+  public function downloadItemAction() {
+    if (!$item = Class_Exemplaire::find($this->_getParam('id', 0)))
+      return $this->_redirect('/');
+
+    if (!$raw_record = Class_Cosmogramme_Integration_RawRecord::findFor($item))
+      return $this->_redirect('/');
+
+    $this->_helper->binaryDownload($raw_record->getData(), $raw_record->getName());
   }
 
-  protected function _addLocationsFromCommTo(Zend_Form_Element $element): array
-  {
-    if ((!$user = Class_Users::getIdentity())
-        || (!$comm = $user->getSIGBComm())
-        || (!$item = Class_Exemplaire::find($this->_getParam('copy_id'))))
-          return [];
-    $locations = $comm->pickupLocationsFor($user, $item);
-    if (isset($locations['statut']) && !$locations['statut'])
-      return [];
-    foreach ($locations as $k => $v)
-      $element->addMultiOption($k, $v);
 
-    return $locations;
+  public function clearLastSearchSessionAction() {
+    $this->_helper->getHelper('viewRenderer')->setNoRender();
+    Zend_Registry::get('session')->last_search = null;
   }
 
-  protected function arePickupLocationsProvidedByComm()
-  {
-    if ((!$user = Class_Users::getIdentity())
-        || (!Class_Exemplaire::find($this->_getParam('copy_id'))))
-          return false;
 
-    return ($comm = $user->getSIGBComm()) ?
-      $comm->providesPickupLocations() : false;
+  public function reserverAction() {
+    $this->view->titre = $this->_('Réserver un document');
+
+    if ( ! $record = Class_Notice::find($this->_getParam('record_id', 0)))
+      return $this->view->_error_message = $this->_('Document introuvable');
+
+    if ( ! $record->hasExemplaires() )
+      return $this->view->_error_message = $this->_('Le document n\'a pas d\'exemplaire.');
+
+    $this->view->items = (new Intonation_Library_Record($record))
+      ->getItemsFromSIGB();
   }
 }
 
+
+
+
 class RechercheController_FindRecordByKeyOrIdResult
 {
   protected $_record;
diff --git a/cosmogramme/cosmozend/application/modules/cosmo/controllers/DataProfileController.php b/cosmogramme/cosmozend/application/modules/cosmo/controllers/DataProfileController.php
index 415e2414e6d..02031bf3fd6 100644
--- a/cosmogramme/cosmozend/application/modules/cosmo/controllers/DataProfileController.php
+++ b/cosmogramme/cosmozend/application/modules/cosmo/controllers/DataProfileController.php
@@ -19,6 +19,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
+require_once(__DIR__ . '/../../../../../php/classes/classe_profil_donnees.php');
 
 class Cosmo_DataProfileController extends ZendAfi_Controller_Action {
 
@@ -35,4 +36,27 @@ class Cosmo_DataProfileController extends ZendAfi_Controller_Action {
       $url .= '/'.$scope_field.'/'.$scope_value;
     $this->_redirect($url);
   }
+
+
+  public function copyCodifAction() {
+    $this->view->target_profile = $this->_getParam('id');
+    $this->view->titre = $this->_('Copier des règles de codification dans le profil: %s',
+                                  Class_IntProfilDonnees::getLabel($this->view->target_profile));
+  }
+
+
+  public function copyCodifConfirmAction() {
+    $importer =
+      (new Class_Cosmogramme_CodifImporter((int) $this->_getParam('id'),
+                                           (int) $this->_getParam('profile'),
+                                           (array) $this->_getParam('codif_types'),
+                                           (int) $this->_getParam('from')))
+      ->import();
+
+    ($errors = $importer->getErrors())
+      ? $this->_helper->notifyError(implode(BR, $errors), ['status' => 'error'])
+      : $this->_helper->notify($this->_('Les règles de codificatin ont été importées'));
+
+    return $this->_redirectToIndex();
+  }
 }
diff --git a/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/annexe/index.phtml b/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/annexe/index.phtml
index b93efb0e76c..d9c431d365b 100644
--- a/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/annexe/index.phtml
+++ b/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/annexe/index.phtml
@@ -4,13 +4,15 @@ echo $this->tagAnchor(['action' => 'add',
                       $this->_('Ajouter une annexe'));
 
 echo $this->tagModelTable($this->annexes,
-                          [$this->_('Bibliothèque'),
+                          [$this->_('id'),
+                           $this->_('Bibliothèque'),
                            $this->_('Code dans le SIGB'),
                            $this->_('Code facette calculé'),
                            $this->_('libellé'),
                            $this->_('Rejeter les exemplaires'),
                            $this->_('Exclu du PEB')],
-                          ['library_label',
+                          ['id_annexe',
+                           'library_label',
                            'id_origine',
                            'code',
                            'libelle',
diff --git a/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/copy-codif.phtml b/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/copy-codif.phtml
new file mode 100644
index 00000000000..e04def3c1f3
--- /dev/null
+++ b/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/copy-codif.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->Cosmo_CodifsImporter();
diff --git a/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/index.phtml b/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/index.phtml
index 16501c5a2e4..e0998d7564d 100644
--- a/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/index.phtml
+++ b/cosmogramme/cosmozend/application/modules/cosmo/views/scripts/data-profile/index.phtml
@@ -15,6 +15,13 @@ $isUsed = function($model, $attrib) {
   return $this->tag('div', $this->_('Oui (?)') . $html, ['class' => 'table_tooltip_wrapper']);
 };
 
+$hasCodifRules = function($profile) {
+  return $profile->hasCodifRules()
+    ? $this->tagAnchor(['action' => 'copy-codif', 'id' => $profile->getId()],
+                            $this->boutonIco('type=copy_codifs'))
+    : '';
+};
+
 echo $this->tagModelTable($this->data_profiles,
                           [$this->_('Libellé'),
                            $this->_('Type de fichier'),
@@ -25,7 +32,8 @@ echo $this->tagModelTable($this->data_profiles,
                            'format-label',
                            'isUsed'],
                           [['action' => 'edit', 'content' => $this->boutonIco('type=edit')],
-                           ['action' => 'delete', 'content' => $this->boutonIco('type=del')] ],
+                           ['action' => 'delete', 'content' => $this->boutonIco('type=del')],
+                           $hasCodifRules],
                           '',
                           null,
                           ['isUsed' => $isUsed]);
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/AnnexeControllerTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/AnnexeControllerTest.php
index bda0264b70c..b2fb2175713 100644
--- a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/AnnexeControllerTest.php
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/AnnexeControllerTest.php
@@ -71,12 +71,24 @@ class AnnexeControllerIndexTest extends AnnexeControllerTestCase {
   }
 
 
+  /** @test */
+  public function annexeAnnecyIdShouldBeInTable() {
+    $this->assertXPathContentContains('//table//td', 98);
+  }
+
+
   /** @test */
   public function annexeAnnecyShouldBeInTable() {
     $this->assertXPathContentContains('//table//td', 'Annexe Annecy');
   }
 
 
+  /** @test */
+  public function lostAnnexIdShouldBeInTable() {
+    $this->assertXPathContentContains('//table//td', 120);
+  }
+
+
   /** @test */
   public function lostAnnexShouldBeInTable() {
     $this->assertXPathContentContains('//table//td', 'Annex lost');
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodificationsTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodificationsTest.php
new file mode 100644
index 00000000000..73cd4ea8a1c
--- /dev/null
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodificationsTest.php
@@ -0,0 +1,295 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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 DataProfileControllerCodificationsSectionAddTest
+  extends CosmoControllerTestCase
+{
+
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this->dispatch('/cosmo/data-profile/add');
+  }
+
+
+  /** @test */
+  public function sectionTabShouldExist() {
+    $this->assertXPathContentContains('//fieldset[@id="fieldset-section_group"]/legend',
+                                      'Sections');
+  }
+
+
+  /** @test */
+  public function sectionLabelShouldExist() {
+    $this->assertXPathContentContains('//tr[@class="sections_field"]/td',
+                                      'Rechercher les sections dans');
+  }
+}
+
+
+
+
+class DataProfileControllerCodificationsSectionPostAddTest
+  extends CosmoControllerTestCase
+{
+
+  protected $_prefs;
+
+  public function setUp() :void
+  {
+    parent::setUp();
+
+    $this->postDispatch('cosmo/data-profile/add',
+                        ['libelle' => 'Section test profile',
+                         'accents' => Class_IntProfilDonnees::ENCODING_ISO2709,
+                         'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                         'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                         'sections_label' => ['LabelOne', 'LabelTwo'],
+                         'sections_zone' => ['919$a', '929$b'],
+                         'sections_mode' => ['equals', 'starts'],
+                         'sections_value' => ['L1', 'L2'],
+                         'sections_whitespace' => [0, 1],
+                         'sections_skipitems' => [1, 0]]);
+
+    $this->_prefs = Class_IntProfilDonnees::findFirstBy(['libelle' => 'Section test profile'])
+      ->getProfilePrefs()
+      ->getSections();
+
+  }
+
+
+  public function sectionsPrefs() : array {
+    return [['sections_label', ['LabelOne', 'LabelTwo']],
+            ['sections_zone', ['919$a', '929$b']],
+            ['sections_mode', ['equals', 'starts']],
+            ['sections_value', ['L1', 'L2']],
+            ['sections_whitespace', [0, 1]]];
+  }
+
+
+  /**
+   * @dataProvider sectionsPrefs
+   * @test */
+  public function profileDataShouldMatchSectionsPrefs($pref_key, $values) {
+    $this->assertEquals($this->_prefs[$pref_key], $values);
+  }
+}
+
+
+
+
+class DataProfileControllerCodificationsSortAddTest
+  extends CosmoControllerTestCase
+{
+
+  public function setUp() :void
+  {
+    parent::setUp();
+    $this->dispatch('/cosmo/data-profile/add');
+  }
+
+
+  /** @test */
+  public function sortTabShouldExist() {
+    $this->assertXPathContentContains('//fieldset[@id="fieldset-sort_group"]/legend',
+                                      'Genres');
+  }
+
+
+  /** @test */
+  public function sortLabelShouldExist() {
+    $this->assertXPathContentContains('//tr[@class="sorts_field"]/td',
+                                      'Rechercher les genres dans');
+  }
+}
+
+
+
+
+class DataProfileControllerCodificationsSortPostAddTest
+  extends CosmoControllerTestCase
+{
+
+  protected $_prefs;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->postDispatch('cosmo/data-profile/add',
+                        ['libelle' => 'Sort test profile',
+                         'accents' => Class_IntProfilDonnees::ENCODING_ISO2709,
+                         'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                         'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                         'sorts_label' => ['SortOne', 'SortTwo'],
+                         'sorts_zone' => ['995$2', '995$3'],
+                         'sorts_mode' => ['starts', 'contains'],
+                         'sorts_value' => ['S1', 'S2'],
+                         'sorts_whitespace' => [1, 0]]);
+
+    $this->_prefs = Class_IntProfilDonnees::findFirstBy(['libelle' => 'Sort test profile'])
+      ->getProfilePrefs()
+      ->getSorts();
+
+  }
+
+
+  public function sortsPrefs() : array {
+    return [['sorts_label', ['SortOne', 'SortTwo']],
+            ['sorts_zone', ['995$2', '995$3']],
+            ['sorts_mode', ['starts', 'contains']],
+            ['sorts_value', ['S1', 'S2']],
+            ['sorts_whitespace', [1, 0]]];
+  }
+
+
+  /**
+   * @dataProvider sortsPrefs
+   * @test */
+  public function profileDataShouldMatchSortsPrefs($pref_key, $values) {
+    $this->assertEquals($this->_prefs[$pref_key], $values);
+  }
+}
+
+
+
+
+class DataProfileControllerCodificationsLocationAddTest
+  extends CosmoControllerTestCase
+{
+
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this->dispatch('/cosmo/data-profile/add');
+  }
+
+
+  /** @test */
+  public function locationTabShouldExist() {
+    $this->assertXPathContentContains('//fieldset[@id="fieldset-location_group"]/legend',
+                                      'Emplacements');
+  }
+
+
+  /** @test */
+  public function locationLabelShouldExist() {
+    $this->assertXPathContentContains('//tr[@class="locations_field"]/td',
+                                      'Rechercher les emplacements dans');
+  }
+}
+
+
+
+
+class DataProfileControllerCodificationsLocationPostAddTest
+  extends CosmoControllerTestCase
+{
+
+  protected $_prefs;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->postDispatch('cosmo/data-profile/add',
+                        ['libelle' => 'Location test profile',
+                         'accents' => Class_IntProfilDonnees::ENCODING_ISO2709,
+                         'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                         'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                         'locations_label' => ['LocOne', 'LocTwo'],
+                         'locations_zone' => ['995$4', '995$5'],
+                         'locations_mode' => ['starts', 'equals'],
+                         'locations_value' => ['L1', 'L2'],
+                         'locations_whitespace' => [1, 1]]);
+
+    $this->_prefs = Class_IntProfilDonnees::findFirstBy(['libelle' => 'Location test profile'])
+      ->getProfilePrefs()
+      ->getLocations();
+
+  }
+
+
+  public function locationsPrefs() : array {
+    return [['locations_label', ['LocOne', 'LocTwo']],
+            ['locations_zone', ['995$4', '995$5']],
+            ['locations_mode', ['starts', 'equals']],
+            ['locations_value', ['L1', 'L2']],
+            ['locations_whitespace', [1, 1]]];
+  }
+
+
+  /**
+   * @dataProvider locationsPrefs
+   * @test */
+  public function profileDataShouldMatchLocationsPrefs($pref_key, $values) {
+    $this->assertEquals($this->_prefs[$pref_key], $values);
+  }
+}
+
+
+
+
+class DataProfileControllerCodificationsEmptyPostAddTest
+  extends CosmoControllerTestCase
+{
+
+  protected $_prefs;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->postDispatch('cosmo/data-profile/add',
+                        ['libelle' => 'Location test profile',
+                         'accents' => Class_IntProfilDonnees::ENCODING_ISO2709,
+                         'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                         'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                         'locations_label' => [''],
+                         'locations_zone' => [''],
+                         'locations_mode' => ['='],
+                         'locations_value' => [''],
+                         'locations_whitespace' => [0]]);
+
+    $this->_prefs = Class_IntProfilDonnees::findFirstBy(['libelle' => 'Location test profile'])
+      ->getProfilePrefs()
+      ->getLocations();
+
+  }
+
+
+  public function locationsPrefs() : array {
+    return [['locations_label', []],
+            ['locations_zone', []],
+            ['locations_mode', []],
+            ['locations_value', []],
+            ['locations_whitespace', []]];
+  }
+
+
+  /**
+   * @dataProvider locationsPrefs
+   * @test */
+  public function profileDataShouldMatchLocationsPrefs($pref_key, $values) {
+    $this->assertEquals($this->_prefs[$pref_key], $values);
+  }
+}
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodifsImporterTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodifsImporterTest.php
new file mode 100644
index 00000000000..5f1157a90b4
--- /dev/null
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerCodifsImporterTest.php
@@ -0,0 +1,765 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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 DataProfileControllerCodifsImporterTestCase extends CosmoControllerTestCase {
+
+  protected
+    $_target,
+    $_incoming;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->fixture(Class_IntProfilDonnees::class,
+                   ['id' => 17,
+                    'libelle' => 'Notices unimarc',
+                    'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                    'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                    'attributs' => []]);
+
+    $this->fixture(Class_IntProfilDonnees::class,
+                   ['id' => 18,
+                    'libelle' => 'Astrolabe',
+                    'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                    'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                    'attributs' => ['sorts' => ['sorts_label' => ['SortOne', 'SortTwo'],
+                                                 'sorts_zone' => ['995$b', '995$b'],
+                                                 'sorts_mode' => ['=', '*'],
+                                                 'sorts_value' => ['A', 'S'],
+                                                 'sorts_whitespace' => [1, 0]],
+                                     'sections' => ['sections_label' => ['SecOne', 'SecTwo'],
+                                                    'sections_zone' => ['902$a', '995$k'],
+                                                    'sections_mode' => ['/', '='],
+                                                    'sections_value' => ['Disque', 'R WIC'],
+                                                    'sections_whitespace' => [0, 1]],
+                                     'locations' => ['locations_label' => ['LocOne', 'LocTwo'],
+                                                     'locations_zone' => ['995$q', '995$q'],
+                                                     'locations_mode' => ['=', '='],
+                                                     'locations_value' => ['Disque', 'R;A'],
+                                                     'locations_whitespace' => [0, 0]]]]);
+
+    $this->fixture(Class_IntProfilDonnees::class,
+                   ['id' => 19,
+                    'libelle' => 'Unimarc Koha',
+                    'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                    'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                    'attributs' => []]);
+
+    $this->fixture(Class_IntProfilDonnees::class,
+                   ['id' => 20,
+                    'libelle' => 'Unimarc standard',
+                    'type_fichier' => Class_IntProfilDonnees::FT_AUTHORITY,
+                    'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                    'attributs' => []]);
+
+    $this->fixture(Class_IntProfilDonnees::class,
+                   ['id' => 21,
+                    'libelle' => 'Non Unimarc',
+                    'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                    'format' => Class_IntProfilDonnees::FORMAT_CSV,
+                    'attributs' => []]);
+
+    $this->fixture(Class_CodifGenre::class,
+                   ['id' => 1,
+                    'libelle' => 'Jeux',
+                    'regles' => '995$q=0@@J']);
+
+    $this->fixture(Class_CodifGenre::class,
+                   ['id' => 2,
+                    'libelle' => 'Politique',
+                    'regles' => '995$q=1@@POL;P']);
+
+    $this->fixture(Class_CodifGenre::class,
+                   ['id' => 3,
+                    'libelle' => 'Codif without rule',
+                    'regles' => '']);
+
+    $this->fixture(Class_CodifGenre::class,
+                   ['id' => 4,
+                    'libelle' => 'Null codif rule']);
+
+    $this->fixture(Class_CodifSection::class,
+                   ['id' => 5,
+                    'libelle' => 'Jeunesse',
+                    'regles' => '995$a=0@@J']);
+
+    $this->fixture(Class_CodifEmplacement::class,
+                   ['id' => 6,
+                    'libelle' => 'Étage',
+                    'regles' => '995$b=1@@1']);
+
+    $this->fixture(Class_CodifEmplacement::class,
+                   ['id' => 7,
+                    'libelle' => 'RDC',
+                    'regles' => '995$b=0@@0']);
+  }
+
+
+  public function getTarget(string $key) : array {
+    $attributes = Class_IntProfilDonnees::find($this->_target)
+      ->getAttributsAsArray();
+
+    return isset($attributes[$key]) ? $attributes[$key] : [];
+  }
+
+
+  public function getIncoming(string $key) : array {
+    $attributes = Class_IntProfilDonnees::find($this->_incoming)
+      ->getAttributsAsArray();
+
+    return isset($attributes[$key]) ? $attributes[$key] : [];
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterFormTest
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() :void
+  {
+    parent::setUp();
+
+    $this->dispatch('/cosmo/data-profile/copy-codif/id/17');
+  }
+
+
+  /** @test */
+  public function pageShouldContainTitle() {
+    $this
+      ->assertXPathContentContains('//h1',
+                                   'Copier des règles de codification dans le profil: Notices unimarc');
+  }
+
+
+  /** @test */
+  public function formActionShouldBeDataProfileCodifImportConfirm()
+  {
+    $this->assertXPath('//form[@action="/cosmo/data-profile/copy-codif-confirm/id/17"]');
+  }
+
+
+  /** @test */
+  public function fromGlobalsRulesOptionShouldBeInFromSelect() {
+    $this->assertXPathContentContains('//select[@id="from"]/option[@value="1"]',
+                                      'Les règles globales');
+  }
+
+
+  /** @test */
+  public function fromDataProfileRulesOptionShouldBeInFromSelect() {
+    $this->assertXPathContentContains('//select[@id="from"]/option[@value="2"]',
+                                      'Un profil de données');
+  }
+
+
+  /** @test */
+  public function SortsOptionShouldBeInCodifTypesSelect() {
+    $this->assertXPath('//input[@type="checkbox"][@name="codif_types[]"][@value="sorts"]');
+  }
+
+
+  /** @test */
+  public function SectionsOptionShouldBeInCodifTypesSelect() {
+    $this->assertXPath('//input[@type="checkbox"][@name="codif_types[]"][@value="sections"]');
+  }
+
+
+  /** @test */
+  public function LocationsOptionShouldBeInCodifTypesSelect() {
+    $this->assertXPath('//input[@type="checkbox"][@name="codif_types[]"][@value="locations"]');
+  }
+
+
+  /** @test */
+  public function astrolabeShouldBeInProfileSelect() {
+    $this->assertXPathContentContains('//select[@id="profile"]/option[@value="18"]',
+                                      'Astrolabe');
+  }
+
+
+  /** @test */
+  public function unimarcKohaShouldBeInProfileSelect() {
+    $this->assertXPathContentContains('//select[@id="profile"]/option[@value="19"]',
+                                      'Unimarc Koha');
+  }
+
+
+  /** @test */
+  public function authorityProfileShouldNotBeInProfileSelect() {
+    $this->assertNotXPath('//select[@id="profile"]/option[@value="20"]');
+  }
+
+
+  /** @test */
+  public function targetProfileShouldNotBeInProfileSelect() {
+    $this->assertNotXPath('//select[@id="profile"]/option[@value="17"]');
+  }
+
+
+  /** @test */
+  public function nonUnimarcProfileShouldNotBeInProfileSelect() {
+    $this->assertNotXPath('//select[@id="profile"]/option[@value="21"]');
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterNoTargetProfileTestCase
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $flash;
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 90;
+    $this->_incoming = 18;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sorts'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function flashMessengerContentShouldBeProfilDeDonneesWithError() {
+    $flash = new Zend_Session_Namespace('FlashMessenger');
+    $this->assertEquals(['message' => 'Profil de données cible inconnu',
+                         'status' => 'error'],
+                        reset($flash->__get('default'))['notification']);
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterNoIncomingProfileTestCase
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 90;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sorts'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function contextShouldExpectation() {
+    $this->assertFlashMessengerContentContains('Profil de données inconnu');
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterSortsFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 18;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sorts'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('sorts'), $this->getIncoming('sorts'));
+  }
+
+
+  /** @test */
+  public function profile17SectionsShouldNotBeErased() {
+    $this->assertEmpty($this->getTarget('sections'));
+  }
+
+
+  /** @test */
+  public function profile17LocationsShouldNotBeErased() {
+    $this->assertEmpty($this->getTarget('locations'));
+  }
+
+
+  /** @test */
+  public function contextShouldExpectation() {
+    $this->assertFlashMessengerContentContains('Les règles de codificatin ont été importées');
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterSectionsFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $_sorts;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 18;
+    $this->_sorts = ['sorts_label' => ['MySort'],
+                     'sorts_zone' => ['995$a'],
+                     'sorts_mode' => ['='],
+                     'sorts_value' => ['A'],
+                     'sorts_whitespace' => [0]];
+
+    Class_IntProfilDonnees::find($this->_target)
+      ->setAttributs(['sorts' => $this->_sorts])
+      ->save();
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sections'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('sections'), $this->getIncoming('sections'));
+  }
+
+
+  /** @test */
+  public function targetProfileSortsShouldNotBeErased() {
+    $this->assertEquals($this->_sorts, $this->getTarget('sorts'));
+  }
+
+
+  /** @test */
+  public function targetProfileLocationsShouldNotBeErased() {
+    $this->assertEmpty($this->getTarget('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterLocationsFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 18;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['locations'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('locations'), $this->getIncoming('locations'));
+  }
+
+
+  /** @test */
+  public function IncomingProfileSortsShouldNotBeErased() {
+    $this->assertNotEmpty($this->getIncoming('sorts'));
+  }
+
+
+  /** @test */
+  public function IncomingProfileSectionsShouldNotBeErased() {
+    $this->assertNotEmpty($this->getIncoming('sections'));
+  }
+
+
+  /** @test */
+  public function IncomingProfileLocationsShouldNotBeErased() {
+    $this->assertNotEmpty($this->getIncoming('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterSortsAndLocationsFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 18;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sorts', 'locations'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('sorts'), $this->getIncoming('sorts'));
+  }
+
+
+  /** @test */
+  public function profile17LocationsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('locations'), $this->getIncoming('locations'));
+  }
+
+
+  /** @test */
+  public function profile17SectionsShouldNotBeErased() {
+    $this->assertEmpty($this->getTarget('sections'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterAllFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 18;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sorts', 'sections', 'locations'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('sorts'), $this->getIncoming('sorts'));
+  }
+
+
+  /** @test */
+  public function profile17SectionsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('sections'), $this->getIncoming('sections'));
+  }
+
+
+  /** @test */
+  public function profile17LocationsShouldEqualsProfile18() {
+    $this->assertEquals($this->getTarget('locations'), $this->getIncoming('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterSortsFromGlobals
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $_expected = ['sorts_label' => ['Jeux', 'Politique'],
+                          'sorts_zone' => ['995$q', '995$q'],
+                          'sorts_mode' => ['=', '='],
+                          'sorts_value' => ['J', 'POL;P'],
+                          'sorts_whitespace' => ['0', '1']];
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_GLOBALS,
+                         'codif_types' => ['sorts']]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->_expected, $this->getTarget('sorts'));
+  }
+
+
+  /** @test */
+  public function profile17SectionsShouldBeEmpty() {
+    $this->assertEquals([], $this->getTarget('sections'));
+  }
+
+
+  /** @test */
+  public function profile17LocationsShouldBeEmpty() {
+    $this->assertEquals([], $this->getTarget('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterSectionsFromGlobals
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $_expected = ['sections_label' => ['Jeunesse'],
+                          'sections_zone' => ['995$a'],
+                          'sections_mode' => ['='],
+                          'sections_value' => ['J'],
+                          'sections_whitespace' => ['0']];
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_GLOBALS,
+                         'codif_types' => ['sections']]);
+  }
+
+
+  /** @test */
+  public function profile17SectionsShouldEqualsProfile18() {
+    $this->assertEquals($this->_expected, $this->getTarget('sections'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterLocationsFromGlobals
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $_expected = ['locations_label' => ['Étage', 'RDC'],
+                          'locations_zone' => ['995$b', '995$b'],
+                          'locations_mode' => ['=', '='],
+                          'locations_value' => ['1', '0'],
+                          'locations_whitespace' => ['1', '0']];
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_GLOBALS,
+                         'codif_types' => ['locations']]);
+  }
+
+
+  /** @test */
+  public function profile17LocationsShouldEqualsProfile18() {
+    $this->assertEquals($this->_expected, $this->getTarget('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterAllFromGlobals
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $_expected_sort = ['sorts_label' => ['Jeux', 'Politique'],
+                               'sorts_zone' => ['995$q', '995$q'],
+                               'sorts_mode' => ['=', '='],
+                               'sorts_value' => ['J', 'POL;P'],
+                               'sorts_whitespace' => ['0', '1']];
+
+  protected $_expected_sec = ['sections_label' => ['Jeunesse'],
+                              'sections_zone' => ['995$a'],
+                              'sections_mode' => ['='],
+                              'sections_value' => ['J'],
+                              'sections_whitespace' => ['0']];
+
+  protected $_expected_loc = ['locations_label' => ['Étage', 'RDC'],
+                              'locations_zone' => ['995$b', '995$b'],
+                              'locations_mode' => ['=', '='],
+                              'locations_value' => ['1', '0'],
+                              'locations_whitespace' => ['1', '0']];
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_GLOBALS,
+                         'codif_types' => ['sorts', 'sections', 'locations']]);
+  }
+
+
+  /** @test */
+  public function profile17SortsShouldEqualsProfile18() {
+    $this->assertEquals($this->_expected_sort, $this->getTarget('sorts'));
+  }
+
+
+  /** @test */
+  public function profile17SectionsShouldEqualsProfile18() {
+    $this->assertEquals($this->_expected_sec, $this->getTarget('sections'));
+  }
+
+
+  /** @test */
+  public function profile17LocationsShouldEqualsProfile18() {
+    $this->assertEquals($this->_expected_loc, $this->getTarget('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterEmptySortsFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  protected $_expected_sort = ['sorts_label' => ['SortOne', 'SortTwo'],
+                               'sorts_zone' => ['995$b', '995$b'],
+                               'sorts_mode' => ['=', '*'],
+                               'sorts_value' => ['A', 'S'],
+                               'sorts_whitespace' => [1, 0]];
+
+  protected $_expected_sec = ['sections_label' => ['SecOne', 'SecTwo'],
+                              'sections_zone' => ['902$a', '995$k'],
+                              'sections_mode' => ['/', '='],
+                              'sections_value' => ['Disque', 'R WIC'],
+                              'sections_whitespace' => [0, 1]];
+
+  protected $_expected_loc = ['locations_label' => ['LocOne', 'LocTwo'],
+                              'locations_zone' => ['995$q', '995$q'],
+                              'locations_mode' => ['=', '='],
+                              'locations_value' => ['Disque', 'R;A'],
+                              'locations_whitespace' => [0, 0]];
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 18;
+    $this->_incoming = 17;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => ['sorts', 'sections', 'locations'],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function emptyIncomingSortsShouldNotEraseExistingTarget() {
+    $this->assertEquals($this->_expected_sort, $this->getTarget('sorts'));
+  }
+
+
+  /** @test */
+  public function emptyIncomingSectionsShouldNotEraseExistingTarget() {
+    $this->assertEquals($this->_expected_sec, $this->getTarget('sections'));
+  }
+
+
+  /** @test */
+  public function emptyIncomingLocationsShouldNotEraseExistingTarget() {
+    $this->assertEquals($this->_expected_loc, $this->getTarget('locations'));
+  }
+}
+
+
+
+
+class DataProfileControllerCodifsImporterNothingFromExistingProfile
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_target = 17;
+    $this->_incoming = 18;
+
+    $this->postDispatch('/cosmo/data-profile/copy-codif-confirm/id/' . $this->_target,
+                        ['from' => Class_Cosmogramme_CodifImporter::FROM_PROFILE,
+                         'codif_types' => [],
+                         'profile' => $this->_incoming]);
+  }
+
+
+  /** @test */
+  public function wihtoutCodifTypeProvidedMessengerShouldContainNoTypeMessage() {
+    $this
+      ->assertFlashMessengerContentContains('Pas de type de règle fournie. Rien n\'a été importé');
+  }
+}
+
+
+
+
+
+class DataProfileControllerCodyCodifConfirmErrorsTest
+  extends DataProfileControllerCodifsImporterTestCase {
+
+  public function setUp() : void {
+    parent::setUp();
+    $this->dispatch('/cosmo/data-profile/copy-codif-confirm');
+  }
+
+
+  /** @test */
+  public function shouldRedirectToIndex()
+  {
+    $this->assertRedirectTo('/cosmo/data-profile/index');
+  }
+
+
+  /** @test */
+  public function flashMessengerContentShouldBeProfilDeDonneesWithError() {
+    $_flashnotifications = $this->_getFlashMessengerNotifications();
+    $this->assertEquals('error',
+                        reset($_flashnotifications)['notification']['status']);
+  }
+}
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerTest.php
index 092502fe63d..a8c2c3f8076 100644
--- a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerTest.php
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/DataProfileControllerTest.php
@@ -123,6 +123,12 @@ class Cosmo_DataProfileControllerIndexTest
   }
 
 
+  /** @test */
+  public function secondTrShouldContainsLinkImportCodificationInProfile() {
+    $this->assertXPath('//table//tr[2]/td/a[contains(@href, "/cosmo/data-profile/copy-codif/id/56")]');
+  }
+
+
   /** @test */
   public function thirdTrShouldContainsNanook() {
     $this->assertXPathContentContains('//table//tr[3]/td', 'Nanook');
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/EmplacementControllerTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/EmplacementControllerTest.php
index 43390fe19be..e0d657a86bb 100644
--- a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/EmplacementControllerTest.php
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/EmplacementControllerTest.php
@@ -32,6 +32,12 @@ abstract class Cosmo_EmplacementControllerTestCase extends CosmoControllerTestCa
                     'regles' =>
                     '995$x=' . Class_Codification_Rule::REMOVE_SPACE_PREFIX . '55',
                     'ne_pas_afficher' => 0]);
+
+    $this->fixture(Class_CodifEmplacement::class,
+                   ['id' => 19,
+                    'libelle' => 'Location without rule',
+                    'regles' => '',
+                    'ne_pas_afficher' => 0]);
   }
 }
 
@@ -61,6 +67,12 @@ class Cosmo_EmplacementControllerIndexTest
   }
 
 
+  /** @test */
+  public function codifWithoutRuleShouldNotBeDisplayed() {
+    $this->assertNotXPathContentContains('//td', 'Location without rule');
+  }
+
+
   /** @test */
   public function albumRuleShouldBePresent() {
     $this->assertXPathContentContains('//table//tr//td/ul//li',
@@ -186,7 +198,7 @@ class Cosmo_EmplacementControllerAddTest extends Cosmo_EmplacementControllerTest
 
   /** @test */
   public function defaultRuleShouldBePresent() {
-    $this->assertXpathContentContains('//script', '"values":{"rule_zone":["995"],"rule_field":["k"],"rule_sign":["\/"],"rule_values":["R"],"rule_space":["0"]}');
+    $this->assertXpathContentContains('//script', '"values":{"rule_zone":["995"],"rule_field":["k"],"rule_sign":["\/"],"rule_values":["R"],"rule_space":[0]}');
   }
 
 
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/GenreControllerTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/GenreControllerTest.php
index 74b6fec0e72..c5b36664e61 100644
--- a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/GenreControllerTest.php
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/GenreControllerTest.php
@@ -25,23 +25,27 @@ abstract class Cosmo_GenreControllerTestCase extends CosmoControllerTestCase {
   public function setUp(): void
   {
     parent::setUp();
-
-    $this->fixture('Class_CodifGenre',
+    $this->fixture(Class_CodifGenre::class,
                    ['id' => 5,
                     'libelle' => '01 Energie Enjeux',
                     'regles' => '995$q=0@@98']);
 
-    $this->fixture('Class_CodifGenre',
+    $this->fixture(Class_CodifGenre::class,
                    ['id' => 7,
                     'libelle' => '01 Energie Politique',
                     'regles' => '995$q=0@@99']);
 
-    $this->fixture('Class_CodifGenre',
+    $this->fixture(Class_CodifGenre::class,
                    ['id' => 9,
                     'libelle' => '01 Energie Vertes',
                     'regles' => '997$a=0@@11
 998$b/1@@22;33
 999$c*1@@44']);
+
+    $this->fixture(Class_CodifGenre::class,
+                   ['id' => 10,
+                    'libelle' => 'Sort without rules',
+                    'regles' => '']);
   }
 }
 
@@ -77,6 +81,12 @@ class Cosmo_GenreControllerIndexTest extends Cosmo_GenreControllerTestCase {
   }
 
 
+  /** @test */
+  public function codifWithoutRuleShouldNotBeDisplayed() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'Sort without rules');
+  }
+
+
   /** @test */
   public function energieVertesReglesShouldBeInTable() {
     $this->assertXPathContentContains('//table//tr//td/ul//li',
@@ -121,7 +131,7 @@ class Cosmo_GenreControllerEditTest extends Cosmo_GenreControllerTestCase {
 
   /** @test */
   public function reglesValueShouldBe995DollarQEquals99WithoutSpace() {
-    $this->assertXpathContentContains('//script', '"rule_zone":["995"],"rule_field":["q"],"rule_sign":["="],"rule_values":["99"],"rule_space":["0"]');
+    $this->assertXpathContentContains('//script', '"rule_zone":["995"],"rule_field":["q"],"rule_sign":["="],"rule_values":["99"],"rule_space":[0]');
   }
 }
 
diff --git a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/SectionControllerTest.php b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/SectionControllerTest.php
index f7224f75897..e6e1729183b 100644
--- a/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/SectionControllerTest.php
+++ b/cosmogramme/cosmozend/tests/application/modules/cosmo/controllers/SectionControllerTest.php
@@ -25,16 +25,20 @@ abstract class Cosmo_SectionControllerTestCase extends CosmoControllerTestCase {
   public function setUp(): void
   {
     parent::setUp();
-
-    $this->fixture('Class_CodifSection',
+    $this->fixture(Class_CodifSection::class,
                    ['id' => 5,
                     'libelle' => '01 Energie Enjeux',
                     'regles' => '995$q=98']);
 
-    $this->fixture('Class_CodifSection',
+    $this->fixture(Class_CodifSection::class,
                    ['id' => 7,
                     'libelle' => '01 Energie Politique',
                     'regles' => '995$q=99']);
+
+    $this->fixture(Class_CodifSection::class,
+                   ['id' => 8,
+                    'libelle' => 'Section without rules',
+                    'regles' => '']);
   }
 }
 
@@ -70,6 +74,12 @@ class Cosmo_SectionControllerIndexTest extends Cosmo_SectionControllerTestCase {
   }
 
 
+  /** @test */
+  public function codifWithoutRuleShouldNotBeDisplayed() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'Section without rules');
+  }
+
+
   /** @test */
   public function editEnergiePolitiqueShouldBePresent() {
     $this->assertXPath('//table//tr//td//a[contains(@href, "/cosmo/section/edit/id/7")]');
diff --git a/cosmogramme/css/form.css b/cosmogramme/css/form.css
index 8cad5ee477a..3210f023e01 100644
--- a/cosmogramme/css/form.css
+++ b/cosmogramme/css/form.css
@@ -1,59 +1,59 @@
 div.form
 {
-	margin-bottom:10px;
+    margin-bottom:10px;
 }
 span.form
 {
-	background-color:#FFFFCF;
-	color:#B73901;
-	margin-top:15px;
-	font-weight:bold;
+    background-color:#FFFFCF;
+    color:#B73901;
+    margin-top:15px;
+    font-weight:bold;
 }
 table.form
 {
-	width:100%;
-	border:1px solid;
-	border-color:#E0E0E0;
-	background-color:#FFFFCF;
+    width:100%;
+    border:1px solid;
+    border-color:#E0E0E0;
+    background-color:#FFFFCF;
 }
 th.form
 {
-	background-color:#E9F6A7;
-	color:#003687;
-	font-weight:bold;
-	border: none;
-	padding-top:7px;
-	padding-bottom:7px;
+    background-color:#E9F6A7;
+    color:#003687;
+    font-weight:bold;
+    border: none;
+    padding-top:7px;
+    padding-bottom:7px;
 }
 td.form
 {
-	border: none;
-	background-color:#FFFFCF;
-	padding-top:0px;
-	padding-bottom:5px;
-	height:10px;
+    border: none;
+    background-color:#FFFFCF;
+    padding-top:0px;
+    padding-bottom:5px;
+    height:10px;
 }
 td.form_milieu
 {
-	border: none;
-	background-color:#FFFFCF;
-	padding-top:0px;
-	padding-bottom:5px;
-	height:30px;
-	vertical-align:middle;
+    border: none;
+    background-color:#FFFFCF;
+    padding-top:0px;
+    padding-bottom:5px;
+    height:30px;
+    vertical-align:middle;
 }
 
 td.form_first
 {
-	border: none;
-	background-color:#FFFFCF;
-	padding-top:5px;
-	padding-bottom:5px;
-	height:10px;
+    border: none;
+    background-color:#FFFFCF;
+    padding-top:5px;
+    padding-bottom:5px;
+    height:10px;
 }
 form
 {
-	margin:0px;
+    margin:0px;
 }
 
 form.form {
@@ -69,4 +69,8 @@ form label {
     text-align: right;
     width: 100%;
     display: inline-block;
-}
\ No newline at end of file
+}
+
+.dropdown {
+    min-height: 30px;
+}
diff --git a/cosmogramme/php/classes/classe_notice_integration.php b/cosmogramme/php/classes/classe_notice_integration.php
index 6a98fb6f655..c20b85a5253 100644
--- a/cosmogramme/php/classes/classe_notice_integration.php
+++ b/cosmogramme/php/classes/classe_notice_integration.php
@@ -815,9 +815,10 @@ class notice_integration {
        * we can have one file with items for different libraries. We use "annexe" codification to find
        * to put each item in the library it belongs to.
        **/
-      if ($exemplaire->hasAnnexe() && ($annexe =  CodifAnnexeCache::getInstance()->find($ex['annexe']))) {
-        $exemplaire->setIdBib($annexe->getIdBib());
-      }
+      if ($exemplaire->hasAnnexe()
+          && ($annexe =  Class_CodifAnnexe::find($exemplaire->getAnnexe())))
+        $exemplaire
+          ->setIdBib($annexe->getIdBib());
 
       $exemplaires []= $exemplaire;
     }
diff --git a/cosmogramme/php/classes/classe_unimarc.php b/cosmogramme/php/classes/classe_unimarc.php
index e3ec07847a5..d1a3b3d0694 100644
--- a/cosmogramme/php/classes/classe_unimarc.php
+++ b/cosmogramme/php/classes/classe_unimarc.php
@@ -100,6 +100,7 @@ class notice_unimarc extends iso2709_record
     if ( ! $decode_string)
       $this->profil['accents'] = 0;
 
+    $this->_codif_rules->setDataProfile($this->profil);
     $this->setNotice($data, $this->profil['accents']);
     $this->_fluent = null;
     return true;
@@ -261,6 +262,7 @@ class notice_unimarc extends iso2709_record
       $counter = count($notice["exemplaires"]);
       for($i=0; $i <$counter; $i++) {
         $exemplaire = $notice["exemplaires"][$i];
+
         if (isset($exemplaire["section"]) && $exemplaire["section"])
           $notice["sections"][]=$exemplaire["section"];
 
@@ -269,8 +271,8 @@ class notice_unimarc extends iso2709_record
 
         if(isset($exemplaire["genre"]) && $exemplaire["genre"])
           $notice["genres"][]=$exemplaire["genre"];
-        elseif (isset($ret['genre']) && $ret['genre'] !== [])
-          $notice["exemplaires"][$i]["genre"] = $ret["genre"][0];
+        elseif (isset($ret['genre']) && !empty($ret['genre']))
+          $notice["exemplaires"][$i]["genre"] = reset($ret["genre"]);
 
         if (!$notice['cote'] && isset($exemplaire['cote']) && $exemplaire['cote'])
           $notice['cote']=$exemplaire['cote'];
@@ -617,11 +619,13 @@ class notice_unimarc extends iso2709_record
               $ex['ignore_exemplaire'] = true;
           }
 
-          if ($champ_annexe == $champ['code'] ) {
-            $annexe = CodifAnnexeCache::getInstance()->find($champ['valeur']);
+          if ( $champ_annexe == $champ['code'] ) {
+            $annexe = Class_CodifAnnexe::findByCodeAndBib($champ['valeur'] ?? '',
+                                                          $this->_id_bib ?? 0);
+
             if ($annexe && !$annexe->isVisible())
               $ex['ignore_exemplaire'] = true;
-            $ex['annexe'] = ($annexe) ? $annexe->getCode() : $champ['valeur'];
+            $ex['annexe'] = ($annexe) ? $annexe->getId() : $champ['valeur'];
           }
 
           if ($champ_availability == $champ['code'] )
@@ -791,7 +795,9 @@ class notice_unimarc extends iso2709_record
               return;
             }
 
-            $ex["annexe"] = $champ["valeur"];
+            $ex["annexe"] = ($annexe = Class_CodifAnnexe::findByCodeAndBib($champ["valeur"], $this->id_bib ?? 0))
+              ? $annexe->getId()
+              : $champ['valeur'];
             return;
           }
 
@@ -920,9 +926,13 @@ class notice_unimarc extends iso2709_record
 
     if (($champ["code"] == "9")
         && (0 === strpos($champ['valeur'], 'locdoc_codage_import:'))){
-      $ex['annexe'] = trim(str_replace('locdoc_codage_import:',
-                                       '',
-                                       $champ['valeur']));
+      $annexe_code = trim(str_replace('locdoc_codage_import:',
+                                      '',
+                                      $champ['valeur']));
+      $ex['annexe'] = ($annexe = Class_CodifAnnexe::findByCodeAndBib($annexe_code,
+                                                                     $this->id_bib ?? 0))
+        ? $annexe->getId()
+        : $annexe_code;
       return $ex;
     }
 
@@ -1549,6 +1559,11 @@ class notice_unimarc extends iso2709_record
   }
 
 
+  public function getProfile() : array {
+    return $this->profil;
+  }
+
+
   private function getIdCodeExemplaire($type, $champ, $sous_champ, $valeur) {
     $champ = $champ . '$' . $sous_champ;
     return $this->_codif_rules->getCodifId($type, $champ, $valeur);
diff --git a/cosmogramme/sql/patch/476.php b/cosmogramme/sql/patch/476.php
new file mode 100644
index 00000000000..a453b43a8e6
--- /dev/null
+++ b/cosmogramme/sql/patch/476.php
@@ -0,0 +1,13 @@
+<?php
+try {
+  (new Class_Migration_BranchFromCodeToIdNotice)->run();
+} catch (Exception $e) {}
+
+$adapter = Zend_Db_Table::getDefaultAdapter();
+
+try
+{
+  $adapter->query('ALTER TABLE `codif_annexe` ADD INDEX IF NOT EXISTS (`id_bib`);');
+}
+catch (Exception $e)
+{}
diff --git a/cosmogramme/sql/patch/patch_476.php b/cosmogramme/sql/patch/patch_476.php
new file mode 100644
index 00000000000..a453b43a8e6
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_476.php
@@ -0,0 +1,13 @@
+<?php
+try {
+  (new Class_Migration_BranchFromCodeToIdNotice)->run();
+} catch (Exception $e) {}
+
+$adapter = Zend_Db_Table::getDefaultAdapter();
+
+try
+{
+  $adapter->query('ALTER TABLE `codif_annexe` ADD INDEX IF NOT EXISTS (`id_bib`);');
+}
+catch (Exception $e)
+{}
diff --git a/cosmogramme/tests/php/classes/AbonneIntegrationTest.php b/cosmogramme/tests/php/classes/AbonneIntegrationTest.php
index 2eb977e7f7b..9a1e0939dbe 100644
--- a/cosmogramme/tests/php/classes/AbonneIntegrationTest.php
+++ b/cosmogramme/tests/php/classes/AbonneIntegrationTest.php
@@ -88,7 +88,6 @@ abstract class AbonneIntegrationXMLTestCase extends AbonneIntegrationTestCase {
 
 
 class AbonneIntegrationPipedASCIITest extends AbonneIntegrationASCIITestCase {
-
   public $user;
 
   public function setUp(): void {
@@ -98,7 +97,7 @@ class AbonneIntegrationPipedASCIITest extends AbonneIntegrationASCIITestCase {
                    ['id' => 28,
                     'id_origine' => 'CHAMOIS',
                     'code' => 'CHAMWOIS',
-                    'id_bib' => 42]);
+                    'id_bib' => 2]);
 
 
 
@@ -131,8 +130,8 @@ class AbonneIntegrationPipedASCIITest extends AbonneIntegrationASCIITestCase {
 
 
   /** @test */
-  public function idSiteShouldBe42() {
-    $this->assertEquals(42, $this->user->getIdSite());
+  public function idSiteShouldBe2() {
+    $this->assertEquals(2, $this->user->getIdSite());
   }
 
 
diff --git a/cosmogramme/tests/php/classes/KohaPeriodiquesTest.php b/cosmogramme/tests/php/classes/KohaPeriodiquesTest.php
index 84e4def3ae8..641dbc82ed7 100644
--- a/cosmogramme/tests/php/classes/KohaPeriodiquesTest.php
+++ b/cosmogramme/tests/php/classes/KohaPeriodiquesTest.php
@@ -699,35 +699,35 @@ class KohaPeriodiquesMatriculeAngesTest extends KohaPeriodiquesWithArticlesTestC
 
 
   /** @test */
-  public function allMatriculeAngesSerialFacettesShouldContainsYCHYJR() {
+  public function allMatriculeAngesSerialFacettesShouldContainsY3() {
     $records = Class_Notice::findAllBy(['type_doc' => Class_TypeDoc::PERIODIQUE,
                                         'clef_chapeau' =>  'MATRICULE DES ANGES LE']);
     foreach($records as $index => $record)
-      $this->assertContains('YCHYJR', $record->getFacettes(),
+      $this->assertContains('Y3', $record->getFacettes(),
                             'at position: ' . $index . ', title: ' . $record->getTitrePrincipal());
   }
 
 
   /** @test */
-  public function allMatriculeAngesArticlesFacettesShouldContainsYCHYJR() {
+  public function allMatriculeAngesArticlesFacettesShouldContainsY3() {
     $records = Class_Notice::findAllBy(['type_doc' => Class_TypeDoc::PERIODIQUE_ARTICLE,
                                         'clef_chapeau' =>  'MATRICULE DES ANGES LE 202']);
     foreach($records as $index => $record) {
       $record->updateFacetsFromExemplaires();
-      $this->assertContains('YCHYJR', $record->getFacettes(),
+      $this->assertContains('Y3', $record->getFacettes(),
                             'at position: ' . $index . ', title: ' . $record->getTitrePrincipal());
     }
   }
 
 
   /** @test */
-  public function titleRecordMatriculeFacettesShouldContainsYCHYJR() {
+  public function titleRecordMatriculeFacettesShouldContainsY3() {
     $title = Class_Notice::findFirstBy(['order' => 'id desc',
                                         'type_doc' => Class_TypeDoc::PERIODIQUE_TITLE]);
 
     $title->updateFacetsFromExemplaires();
 
-    $this->assertEquals('D800 Lfre Tper_title B1 YCHYJR HNRNR0001',
+    $this->assertEquals('D800 Lfre Tper_title B1 Y3 HNRNR0001',
                         $title->getFacettes());
   }
 }
diff --git a/cosmogramme/tests/php/classes/KohaRecordIntegrationTest.php b/cosmogramme/tests/php/classes/KohaRecordIntegrationTest.php
index 3e706d3c3e2..17a83749dba 100644
--- a/cosmogramme/tests/php/classes/KohaRecordIntegrationTest.php
+++ b/cosmogramme/tests/php/classes/KohaRecordIntegrationTest.php
@@ -289,6 +289,19 @@ class KohaRecordIntegrationFacetPresseTest extends KohaRecordIntegrationTestCase
                     'rule_list_label_length' => [2]
                    ]);
 
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 122,
+                    'id_bib' => 2,
+                    'id_origine' => 'MEDSTMAR',
+                    'code' => 'MEDSTMAR']);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 123,
+                    'id_bib' => 1,
+                    'id_origine' => 'MEDSTMAR',
+                    'code' => 'MEDSTMAR']);
+
     Class_CosmoVar::setValueOf('mode_doublon', Class_CosmoVar::DOUBLE_SEARCH_ALPHA_KEY);
     $this->loadNotice('unimarc_koha_okapi');
   }
@@ -330,7 +343,7 @@ class KohaRecordIntegrationFacetPresseTest extends KohaRecordIntegrationTestCase
 
   /** @test */
   public function facettesShouldContainsT2AndHANNE0001AndHMOIS0001AndJOUR0001() {
-    $this->assertEquals('HANNE0001 HMOIS0001 HJOUR0001 Lfre T2 B1 YMEDSTMAR HNRNR0001',
+    $this->assertEquals('HANNE0001 HMOIS0001 HJOUR0001 Lfre T2 B1 Y123 HNRNR0001',
                         Class_Notice::find(1)
                         ->updateFacetsFromExemplaires()
                         ->getFacettes());
@@ -339,7 +352,7 @@ class KohaRecordIntegrationFacetPresseTest extends KohaRecordIntegrationTestCase
 
   /** @test */
   public function facettesForRecord25ShouldContainsT2AndHANNE0001AndHMOIS0001() {
-    $this->assertEquals('HANNE0001 HMOIS0007 HJOUR0006 Lfre T2 B1 YMEDSTMAR HNRNR0001',
+    $this->assertEquals('HANNE0001 HMOIS0007 HJOUR0006 Lfre T2 B1 Y123 HNRNR0001',
                         Class_Notice::find(25)
                         ->updateFacetsFromExemplaires()
                         ->getFacettes());
@@ -674,7 +687,7 @@ class KohaRecordIntegrationEscapableAnnexeCodesTest
 
   /** @test */
   public function firstItemAnnexeShouldBeCHYJR() {
-    $this->assertEquals('CHYJR', $this->notice_data['exemplaires'][0]['annexe']);
+    $this->assertEquals(43, $this->notice_data['exemplaires'][0]['annexe']);
   }
 }
 
diff --git a/cosmogramme/tests/php/classes/NoticeIntegrationAloesTest.php b/cosmogramme/tests/php/classes/NoticeIntegrationAloesTest.php
index 827a279bc39..ae441992ee3 100644
--- a/cosmogramme/tests/php/classes/NoticeIntegrationAloesTest.php
+++ b/cosmogramme/tests/php/classes/NoticeIntegrationAloesTest.php
@@ -346,11 +346,13 @@ class NoticeIntegrationAloesDimancheALaPiscineTest extends NoticeIntegrationAloe
                     'regles' => '902$a=Roman']);
 
     $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 1,
+                   ['id' => 27,
+                    'code' => 'A',
                     'id_origine' => 'A',
                     'libelle' => 'Antibes',
                     'id_bib' => 4,
-                    'invisible' => 0]);
+                    'invisible' => 0])
+         ->assertSave();
 
     $this->onLoaderOfModel(Class_Exemplaire::class);
     $this->loadNotice("unimarc_dimanche_a_la_piscine");
@@ -426,7 +428,7 @@ class NoticeIntegrationAloesDimancheALaPiscineTest extends NoticeIntegrationAloe
    * @depends exemplaireShouldHaveBeenSavedForNotice1
    */
   public function exemplaireAnnexeShouldBeAntibes($exemplaire) {
-    $this->assertEquals('A', $exemplaire->getAnnexe());
+    $this->assertEquals(27, $exemplaire->getAnnexe());
   }
 
 
diff --git a/cosmogramme/tests/php/classes/NoticeIntegrationCodificationRulesTest.php b/cosmogramme/tests/php/classes/NoticeIntegrationCodificationRulesTest.php
new file mode 100644
index 00000000000..a6ef890fefd
--- /dev/null
+++ b/cosmogramme/tests/php/classes/NoticeIntegrationCodificationRulesTest.php
@@ -0,0 +1,412 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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 'NoticeIntegrationTest.php';
+
+abstract class NoticeIntegrationCodificationRulesTestCase extends NoticeIntegrationTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->_loadFixtures();
+
+    $this->loadNotice("unimarc_dimanche_a_la_piscine");
+  }
+
+  public function getProfilDonnees() {
+    return $this->fixture(Class_IntProfilDonnees::class,
+                          ['id' => 102,
+                           'libelle' => 'Unimarc avec codifications',
+                           'accents' => Class_IntProfilDonnees::ENCODING_UTF8,
+                           'type_fichier' => Class_IntProfilDonnees::FT_RECORDS,
+                           'format' => Class_IntProfilDonnees::FORMAT_UNIMARC,
+                           'attributs' => []]);
+
+  }
+
+
+  protected function _loadFixtures() {
+  }
+}
+
+
+
+
+class NoticeIntegrationStandarCodificationRulesTest
+  extends NoticeIntegrationCodificationRulesTestCase {
+
+  public function getProfilDonnees() {
+    return parent::getProfilDonnees()
+      ->setAttributs([['champ_code_barres' => 'f',
+                       'champ_cote' => 'k',
+                       'champ_type_doc' => 'r',
+                       'champ_annexe' => 'b',
+                       'champ_section' => 'v',
+                       'champ_emplacement' => 'r']])
+      ->getRawAttributes();
+  }
+
+
+  protected function _loadFixtures() {
+    $this->fixture(Class_CodifGenre::class, ['id' => 8,
+                                             'libelle' => 'Roman',
+                                             'regles' => '902$a/1@@Roman']);
+
+    $this->fixture(Class_CodifSection::class, ['id' => 9,
+                                               'libelle' => 'BD Adultes',
+                                               'regles' => '995$v=0@@n']);
+
+    $this->fixture(Class_CodifEmplacement::class, ['id' => 10,
+                                                   'libelle' => 'Exposition',
+                                                   'regles' => '995$r=1@@LIV']);
+
+    $this->fixture(Class_CodifGenre::class, ['id' => 11,
+                                             'libelle' => 'Unmatched sort',
+                                             'regles' => '902$a/1@@empire']);
+
+    $this->fixture(Class_CodifSection::class, ['id' => 12,
+                                               'libelle' => 'Unmatched section',
+                                               'regles' => '902$a/1@@empire']);
+
+    $this->fixture(Class_CodifEmplacement::class, ['id' => 13,
+                                                   'libelle' => 'Unmatched location',
+                                                   'regles' => '902$a/1@@empire']);
+  }
+
+
+  /** @test */
+  public function sortShouldBe8() {
+    $this->assertEquals(8, $this->notice_data['exemplaires'][0]['genre']);
+  }
+
+
+  /** @test */
+  public function SectionShould9() {
+    $this->assertEquals(9, $this->notice_data['exemplaires'][0]['section']);
+  }
+
+
+  /** @test */
+  public function locationShould10() {
+    $this->assertEquals(10, $this->notice_data['exemplaires'][0]['emplacement']);
+  }
+}
+
+
+
+
+class NoticeIntegrationDataProfileCodificationRulesTest
+  extends NoticeIntegrationCodificationRulesTestCase {
+
+  protected
+    $_sortone,
+    $_sectiontwo,
+    $_loctwo;
+
+  public function getProfilDonnees() {
+    return parent::getProfilDonnees()
+      ->setAttributs([['champ_code_barres' => 'f',
+                       'champ_cote' => 'k',
+                       'champ_type_doc' => 'r',
+                       'champ_annexe' => 'a',
+                       'champ_genre' => 'b',
+                       'champ_section' => 'k',
+                       'champ_emplacement' => 'q'],
+                      'sorts' => ['sorts_label' => ['SortOne', 'SortTwo', ''],
+                                  'sorts_zone' => ['995$b', '995$b', ''],
+                                  'sorts_mode' => ['=', '*', '='],
+                                  'sorts_value' => ['A', 'S', ''],
+                                  'sorts_whitespace' => [1, 0]],
+                      'sections' => ['sections_label' => ['SectionOne', 'SectionTwo'],
+                                     'sections_zone' => ['902$a', '995$k'],
+                                     'sections_mode' => ['/', '='],
+                                     'sections_value' => ['Disque', 'R WIC'],
+                                     'sections_whitespace' => [0, 1]],
+                      'locations' => ['locations_label' => ['LocOne', 'LocTwo'],
+                                      'locations_zone' => ['995$q', '995$q'],
+                                      'locations_mode' => ['=', '='],
+                                      'locations_value' => ['Disque', 'R;A'],
+                                      'locations_whitespace' => [0, 0]]])
+      ->getRawAttributes();
+  }
+
+
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this->_sortone = Class_CodifGenre::findFirstBy(['libelle' => 'SortOne']);
+    $this->_sectiontwo = Class_CodifSection::findFirstBy(['libelle' => 'SectionTwo']);
+    $this->_loctwo = Class_CodifEmplacement::findFirstBy(['libelle' => 'LocTwo']);
+  }
+
+
+  /** @test */
+  public function sortOneCodoficationShouldBeCreated() {
+    $this->assertNotNull($this->_sortone);
+  }
+
+
+  /** @test */
+  public function sectionTwoCodoficationShouldBeCreated() {
+    $this->assertNotNull($this->_sectiontwo);
+  }
+
+
+  /** @test */
+  public function locTwoCodoficationShouldBeCreated() {
+    $this->assertNotNull($this->_loctwo);
+  }
+
+
+  /** @test */
+  public function sortShouldBeSortOneId() {
+    $this->assertEquals($this->_sortone->getId(), $this->notice_data['exemplaires'][0]['genre']);
+  }
+
+
+  /** @test */
+  public function sectionShouldBeSectionTwoId() {
+    $this->assertEquals($this->_sectiontwo->getId(), $this->notice_data['exemplaires'][0]['section']);
+  }
+
+
+  /** @test */
+  public function locationShouldBeLocTwoId() {
+    $this->assertEquals($this->_loctwo->getId(), $this->notice_data['exemplaires'][0]['emplacement']);
+  }
+}
+
+
+
+
+class NoticeIntegrationDataProfileCodificationRulesWithExistingRulesTest
+  extends NoticeIntegrationCodificationRulesTestCase {
+
+  public function getProfilDonnees() {
+    return parent::getProfilDonnees()
+      ->setAttributs([['champ_code_barres' => 'f',
+                       'champ_cote' => 'k',
+                       'champ_type_doc' => 'r',
+                       'champ_annexe' => 'b',
+                       'champ_section' => 'm',
+                       'champ_emplacement' => 'b'],
+                      'sorts' => ['sorts_label' => ['SortThree'],
+                                  'sorts_zone' => ['902$a'],
+                                  'sorts_mode' => ['=',],
+                                  'sorts_value' => ['Roman'],
+                                  'sorts_whitespace' => [1]],
+                      'sections' => ['sections_label' => ['SectionThree'],
+                                     'sections_zone' => ['676$a'],
+                                     'sections_mode' => ['='],
+                                     'sections_value' => ['R'],
+                                     'sections_whitespace' => [1]],
+                      'locations' => ['locations_label' => ['LocThree'],
+                                      'locations_zone' => ['995$b'],
+                                      'locations_mode' => ['='],
+                                      'locations_value' => ['A'],
+                                      'locations_whitespace' => [0]]])
+      ->getRawAttributes();
+  }
+
+
+  protected function _loadFixtures() {
+    $this->fixture(Class_CodifGenre::class, ['id' => 40,
+                                             'libelle' => 'SortThree',
+                                             'regles' => '901$b/1@@Doc']);
+
+    $this->fixture(Class_CodifSection::class, ['id' => 41,
+                                               'libelle' => 'SectionThree',
+                                               'regles' => '995$m=1@@20140322']);
+
+    $this->fixture(Class_CodifEmplacement::class, ['id' => 42,
+                                                   'libelle' => 'LocThree',
+                                                   'regles' => '995$b=1@@A']);
+  }
+
+
+  /** @test */
+  public function sortOneCodificationShouldNotBeDuplicated() {
+    $this->assertCount(1, Class_CodifGenre::findAllBy(['libelle' => 'SortThree']));
+  }
+
+
+  /** @test */
+  public function sortIdShouldBe40() {
+    $this->assertEquals(40, $this->notice_data['exemplaires'][0]['genre']);
+  }
+
+
+  /** @test */
+  public function sectionTwoCodificationShouldNotBeDuplicated() {
+    $this->assertCount(1, Class_CodifSection::findAllBy(['libelle' => 'SectionThree']));
+  }
+
+
+  /** @test */
+  public function sectionIdShouldBe41() {
+    $this->assertEquals(41, $this->notice_data['exemplaires'][0]['section']);
+  }
+
+
+  /** @test */
+  public function locTwoCodificationShouldNotBeDuplicated() {
+    $this->assertCount(1, Class_CodifEmplacement::findAllBy(['libelle' => 'LocThree']));
+  }
+
+
+  /** @test */
+  public function locationIdShouldBe42() {
+    $this->assertEquals(42, $this->notice_data['exemplaires'][0]['emplacement']);
+  }
+}
+
+
+
+
+class NoticeIntegrationDataProfileCodificationRulesWithFallBackOnDefaultRulesTest
+  extends NoticeIntegrationCodificationRulesTestCase {
+
+  public function getProfilDonnees() {
+    return parent::getProfilDonnees()
+      ->setAttributs([['champ_code_barres' => 'f',
+                       'champ_cote' => 'k',
+                       'champ_type_doc' => 'r',
+                       'champ_annexe' => 'b',
+                       'champ_section' => 'v',
+                       'champ_emplacement' => 'r'],
+                      'sorts' => ['sorts_label' => ['Unmatched Sort'],
+                                  'sorts_zone' => ['902$a'],
+                                  'sorts_mode' => ['=',],
+                                  'sorts_value' => ['Foo'],
+                                  'sorts_whitespace' => [1]],
+                      'sections' => ['sections_label' => ['Unmatched Section'],
+                                     'sections_zone' => ['676$a'],
+                                     'sections_mode' => ['='],
+                                     'sections_value' => ['U'],
+                                     'sections_whitespace' => [1]],
+                      'locations' => ['locations_label' => ['Unmatched Loc'],
+                                      'locations_zone' => ['995$b'],
+                                      'locations_mode' => ['='],
+                                      'locations_value' => ['H'],
+                                      'locations_whitespace' => [0]]])
+      ->getRawAttributes();
+  }
+
+
+  protected function _loadFixtures() {
+    $this->fixture(Class_CodifGenre::class, ['id' => 43,
+                                             'libelle' => 'SortThree',
+                                             'regles' => '902$a=1@@Roman']);
+
+    $this->fixture(Class_CodifSection::class, ['id' => 44,
+                                               'libelle' => 'SectionThree',
+                                               'regles' => '995$v=1@@n']);
+
+    $this->fixture(Class_CodifEmplacement::class, ['id' => 45,
+                                                   'libelle' => 'LocThree',
+                                                   'regles' => '995$r=1@@LIV']);
+  }
+
+
+  /** @test */
+  public function sortShouldBeSortOneId() {
+    $this->assertEquals(43, $this->notice_data['exemplaires'][0]['genre']);
+  }
+
+
+  /** @test */
+  public function sectionShouldBeSectionTwoId() {
+    $this->assertEquals(44, $this->notice_data['exemplaires'][0]['section']);
+  }
+
+
+  /** @test */
+  public function locationShouldBeLocTwoId() {
+    $this->assertEquals(45, $this->notice_data['exemplaires'][0]['emplacement']);
+  }
+}
+
+
+
+
+class NoticeIntegrationDataProfileCodificationRulesWithNoRulesTest
+  extends NoticeIntegrationCodificationRulesTestCase {
+
+  public function getProfilDonnees() {
+    return parent::getProfilDonnees()
+      ->setAttributs([['champ_code_barres' => 'f',
+                       'champ_cote' => 'k',
+                       'champ_type_doc' => 'r',
+                       'champ_annexe' => 'b',
+                       'champ_section' => 'v',
+                       'champ_emplacement' => 'r'],
+                      'sorts' => ['sorts_label' => [],
+                                  'sorts_zone' => [],
+                                  'sorts_mode' => [],
+                                  'sorts_value' => [],
+                                  'sorts_whitespace' => []],
+                      'sections' => ['sections_label' => [],
+                                     'sections_zone' => [],
+                                     'sections_mode' => [],
+                                     'sections_value' => [],
+                                     'sections_whitespace' => []],
+                      'locations' => ['locations_label' => [],
+                                      'locations_zone' => [],
+                                      'locations_mode' => [],
+                                      'locations_value' => [],
+                                      'locations_whitespace' => []]])
+      ->getRawAttributes();
+  }
+
+
+  protected function _loadFixtures() {
+    $this->fixture(Class_CodifGenre::class, ['id' => 43,
+                                             'libelle' => 'SortThree',
+                                             'regles' => '902$a=1@@Roman']);
+
+    $this->fixture(Class_CodifSection::class, ['id' => 44,
+                                               'libelle' => 'SectionThree',
+                                               'regles' => '995$v=1@@n']);
+
+    $this->fixture(Class_CodifEmplacement::class, ['id' => 45,
+                                                   'libelle' => 'LocThree',
+                                                   'regles' => '995$r=1@@LIV']);
+  }
+
+
+  /** @test */
+  public function sortShouldBeSortOneId() {
+    $this->assertEquals(43, $this->notice_data['exemplaires'][0]['genre']);
+  }
+
+
+  /** @test */
+  public function sectionShouldBeSectionTwoId() {
+    $this->assertEquals(44, $this->notice_data['exemplaires'][0]['section']);
+  }
+
+
+  /** @test */
+  public function locationShouldBeLocTwoId() {
+    $this->assertEquals(45, $this->notice_data['exemplaires'][0]['emplacement']);
+  }
+}
diff --git a/cosmogramme/tests/php/classes/NoticeIntegrationTest.php b/cosmogramme/tests/php/classes/NoticeIntegrationTest.php
index d6e939ca84e..1790ce614a8 100644
--- a/cosmogramme/tests/php/classes/NoticeIntegrationTest.php
+++ b/cosmogramme/tests/php/classes/NoticeIntegrationTest.php
@@ -1476,6 +1476,11 @@ class NoticeIntegrationArchivesAlsaceTest extends NoticeIntegrationTestCase
     Class_CosmoVar::newInstanceWithId('unimarc_zone_matiere',
                                       ['valeur' => '600abcjxyz;601abcx;602ajxyz;605a;606ajxyz;607ajxyz;608ajxyz;610aejxyz;615amnx;616acfjxy;620abcdefghi']);
 
+    $this->fixture(Class_CodifAnnexe::class, ['id' => 99,
+                                              'id_bib' => 1,
+                                              'id_origine' => 'Bibliothèque des Dominicains',
+                                              'code' => 'Bibliothèque des Dominicains']);
+
     $this->loadNotice('unimarc_archives_alsace');
   }
 
@@ -1523,7 +1528,7 @@ class NoticeIntegrationArchivesAlsaceTest extends NoticeIntegrationTestCase
   /** @test */
   public function facettesWithExemplairesShouldContainsM1AndM2AndD1()
   {
-    $this->assertEquals('D94438 A1 A2 M1 M2 Lfre T1 B1 YBibliothèque des Dominicains HNRNR0001',
+    $this->assertEquals('D94438 A1 A2 M1 M2 Lfre T1 B1 Y99 HNRNR0001',
                         Class_Notice::find(1)->updateFacetsFromExemplaires()->getFacettes());
   }
 }
diff --git a/library/Class/Catalogue.php b/library/Class/Catalogue.php
index 5e8970111f0..973e70ab5c5 100644
--- a/library/Class/Catalogue.php
+++ b/library/Class/Catalogue.php
@@ -296,7 +296,7 @@ class Class_Catalogue extends Storm_Model_Abstract
 
 
   protected function _addThesaurusNovelty(array $multifacets) : array {
-    foreach(array_filter(explode(';', ($this->getThesaurusNovelty() ?? ''))) as $id)
+    foreach(array_filter(explode(';', ($this->_getThesaurusNovelty() ?? ''))) as $id)
       $multifacets[] = Class_CodifThesaurus::CODE_FACETTE . $id;
 
     return $multifacets;
@@ -625,7 +625,7 @@ class Class_Catalogue extends Storm_Model_Abstract
   }
 
 
-  public function getThesaurusNovelty() {
+  protected function _getThesaurusNovelty() {
     if (!$this->getNouveaute())
       return null;
 
@@ -644,10 +644,11 @@ class Class_Catalogue extends Storm_Model_Abstract
 
     Class_CodifThesaurus::collectForModelsUnderFixed('LibraryNovelty', $models, $collector);
 
-    $parts = array_filter(explode(';', $this->getAnnexe()));
-    $models = array_filter(array_map(
-                                     fn($code) => Class_CodifAnnexe::findFirstBy(['code' => (string)$code]),
-                                     $parts));
+    $models = [];
+    if ($parts = array_filter(explode(';', $this->getAnnexe())))
+      $models = Class_CodifAnnexe::query()
+        ->in('id_annexe', $parts)
+        ->fetchAll();
 
     Class_CodifThesaurus::collectForModelsUnderFixed('AnnexeNovelty', $models, $collector);
 
diff --git a/library/Class/CodifAnnexe.php b/library/Class/CodifAnnexe.php
index f6570a67627..b5cdbfdae2e 100644
--- a/library/Class/CodifAnnexe.php
+++ b/library/Class/CodifAnnexe.php
@@ -24,61 +24,170 @@ class CodifAnnexeLoader extends Storm_Model_Loader
 {
   use Trait_Translator, Trait_FacetableLoader;
 
-  public function findAllByPickup() {
-    return Class_CodifAnnexe::findAllBy(['no_pickup' => '0',
-                                         'order' => 'libelle']);
+  protected array $_annexe_by_code_id_bib_service_cache;
+  protected array $_annexe_by_code_cache;
+  protected array $_annexe_by_id_bib_cache;
+  protected array $_annexes_by_id_bibs_cache;
+
+
+  public function reset(): self
+  {
+    unset($this->_annexe_by_code_id_bib_service_cache);
+    unset($this->_annexes_by_id_bibs_cache);
+    unset($this->_annexe_by_code_cache);
+    unset($this->_annexe_by_id_bib_cache);
+    return $this;
+  }
+
+
+  public function findAllByPickup(): array
+  {
+    return Class_CodifAnnexe::query()
+      ->select(['code', 'libelle'])
+      ->eq('no_pickup', '0')
+      ->order('libelle')
+      ->fetchAll();
   }
 
 
-  public function findLibelleByCode($code) {
+  public function findLibelleByCode($code)
+  {
     return ($annexe = $this->findByCode($code))
       ? $annexe->getLibelle()
       : '';
   }
 
 
-  public function findByCode($code) {
-    if ($annexe = Class_CodifAnnexe::findFirstBy(['code' => $code]))
-      return $annexe;
+  public function findIdBibByCodeAndBib(string $code, int $id_bib) : int
+  {
+    return ($annexe = $this->findByCodeAndBib($code, $id_bib))
+      ? $annexe->getIdBib()
+      : $id_bib;
+  }
+
+
+  public function findByCodeAndBib(?string $code,
+                                   ?int $id_bib = 0,
+                                   ?string $service ='') : ?Class_CodifAnnexe
+  {
+    $key = $code ?? ''
+      . $id_bib ?? 0
+      . $service ?? '';
+
+    if ( isset($this->_annexe_by_code_id_bib_service_cache[$key]))
+      return $this->_annexe_by_code_id_bib_service_cache[$key];
+
+    if ($id_bib && $annexe = Class_CodifAnnexe::findFirstBy(['code' => $code, 'id_bib' => $id_bib]))
+      return $this->_annexe_by_code_id_bib_service_cache[$key] = $annexe;
 
-    return Class_CodifAnnexe::findFirstBy(['id_origine' => $code]);
+    if ($service && $annexe = $this->_findByCodeAndService($code ?? '', $service))
+      return $this->_annexe_by_code_id_bib_service_cache[$key] = $annexe;
+
+    return $this->_annexe_by_code_id_bib_service_cache[$key] = Class_CodifAnnexe::findByCode($code);
+  }
+
+
+  public function findByCodeAndCurrentUser(?string $code) : ?Class_CodifAnnexe
+  {
+    if (!($user = Class_Users::getIdentity()))
+      return $this->findByCode($code);
+
+    return $this->findByCodeAndBib($code, $user->getIdIntBib());
+  }
+
+
+  protected function _findByCodeAndService(string $code, string $service) : ?Class_CodifAnnexe
+  {
+    $codifs_selected = Class_CodifAnnexe::findAllBy(['code' =>  $code]);
+    foreach  ($codifs_selected as $codif)
+      if (($int_bib = Class_IntBib::find($codif->getIntBibId()))
+          && ($int_bib->getUrlServer() == $service))
+        return $codif;
+
+    return null;
+  }
+
+
+  public function findByCode($code) {
+    $key = is_array($code) ? implode($code) : (string) $code;
+    return $this->_annexe_by_code_cache[$key] ??=
+      (Class_CodifAnnexe::findFirstBy(['code' => $code])
+       ?? Class_CodifAnnexe::findFirstBy(['id_origine' => $code]));
   }
 
 
-  public function getMultiOptions() {
+  public function getMultiOptions(?Closure $callback = null): array
+  {
+    if ( ! $callback)
+      $callback = fn($annexe) => $annexe->getId();
+
     $annexes = Class_CodifAnnexe::findAllBy(['invisible' => 0,
                                              'order' => 'libelle']);
     $options = ['' => $this->_('tous')];
+
     foreach($annexes as $annexe)
-      $options[$annexe->getCode()] = $annexe->getLibelle();
+      $options[$callback($annexe)] = $annexe->getLibelle();
 
     return $options;
   }
 
 
-  public function getMultiOptionsFacets() {
-    $multi_options_facets = [];
-    $annexes =  ($annex_ids = array_filter(array_keys($this->getMultiOptions())))
-      ? Class_CodifAnnexe::findAllBy(['id_annexe' => $annex_ids])
-      : [];
+  public function getMultiOptionsFacets(): array
+  {
+    return $this->getMultiOptions(fn($annexe) => $annexe->getFacetCode());
+  }
 
-    foreach ($annexes as $annexe)
-      $multi_options_facets[$annexe->getFacetCode()] = $annexe->getLabel();
 
-    asort($multi_options_facets, SORT_NATURAL | SORT_FLAG_CASE);
-    array_unshift($multi_options_facets, $this->_('tous'));
-    return $multi_options_facets;
+  public function findFirstByLibrary(Class_Bib $library) : ?Class_CodifAnnexe
+  {
+    return Class_CodifAnnexe::findFirstBy(['id_bib' => $library->getId()]);
   }
 
 
-  public function findFirstByLibrary(Class_Bib $library) : ?Class_CodifAnnexe {
-    return Class_CodifAnnexe::findFirstBy(['id_bib' => $library->getId()]);
+  public function getLibrary(string $branch_id): ?Class_Bib
+  {
+    return ($annexe = Class_CodifAnnexe::find($branch_id))
+      ? $annexe->getBib()
+      : null;
+  }
+
+
+  public function getLibraryLabelFromItem(Class_Exemplaire $item): string
+  {
+    if ( ! $annexe = $item->getAnnexe())
+      return '';
+
+    return ($site = $this->find($annexe))
+      ? $site->getLibelle()
+      : '';
+  }
+
+
+  public function findByIdBib(int $id_bib): array
+  {
+    if (isset($this->_annexe_by_id_bib_cache[$id_bib]))
+      return $this->_annexe_by_id_bib_cache[$id_bib];
+
+    return $this->_annexe_by_id_bib_cache[$id_bib] =
+      Class_CodifAnnexe::findAllBy(['id_bib' => $id_bib]);
   }
 
 
-  protected function _criteriaOnQuery(Storm_Query $query, string $id): Storm_Query
+  public function findAllByIdBibs(array $id_bibs): array
   {
-    return $query->eq('code', addslashes($id));
+    $key = implode('-', $id_bibs);
+
+    if (isset($this->_annexes_by_id_bibs_cache[$key]))
+      return $this->_annexes_by_id_bibs_cache[$key];
+
+    if ($id_bibs && $annexes = Class_CodifAnnexe::query()
+        ->select(['code', 'libelle'])
+        ->not_eq('no_pickup', 1)
+        ->in('id_bib', $id_bibs)
+        ->fetchAll())
+      return $this->_annexes_by_id_bibs_cache[$key] = $annexes;
+
+    return $this->_annexes_by_id_bibs_cache[$key] = Class_CodifAnnexe::findAllByPickup();
   }
 }
 
@@ -96,6 +205,7 @@ class Class_CodifAnnexe extends Storm_Model_Abstract {
     $_loader_class = 'CodifAnnexeLoader',
     $_default_attribute_values = ['libelle' => '',
                                   'id_bib' => 0,
+                                  'code' => '',
                                   'id_origine' => '',
                                   'invisible' => 0,
                                   'no_pickup' => 0,
@@ -105,21 +215,33 @@ class Class_CodifAnnexe extends Storm_Model_Abstract {
                                       'referenced_in' => 'id_bib'],
                             'int_bib' => ['through' => 'bib']];
 
-  public function getMailIntBib() {
+
+  public function getMailIntBib()
+  {
     return $this->hasIntBib()
       ? $this->getIntBib()->getMail()
       : '';
   }
 
 
-  public function getLibraryLabel() {
+  public function getLibraryLabel()
+  {
     return $this->hasIntBib()
       ? $this->getIntBib()->getLabel()
       : '';
   }
 
 
-  public function getLibraryId() {
+  public function getIntBibId() : int
+  {
+    return $this->hasIntBib()
+      ? $this->getIntBib()->getId()
+      : 0;
+  }
+
+
+  public function getBibId() : ?int
+  {
     return $this->hasBib()
       ? $this->getBib()->getId()
       : null;
@@ -132,7 +254,7 @@ class Class_CodifAnnexe extends Storm_Model_Abstract {
 
 
   public function getFacetteIndex() {
-    return self::CODE_FACETTE . $this->getCode();
+    return self::CODE_FACETTE . $this->getId();
   }
 
 
@@ -162,7 +284,7 @@ class Class_CodifAnnexe extends Storm_Model_Abstract {
 
 
   public function asFacet() {
-    return static::CODE_FACETTE . $this->getCode();
+    return static::CODE_FACETTE . $this->getId();
   }
 
 
diff --git a/library/Class/CodifEmplacement.php b/library/Class/CodifEmplacement.php
index fc1253c1d20..bc4303a0f43 100644
--- a/library/Class/CodifEmplacement.php
+++ b/library/Class/CodifEmplacement.php
@@ -20,16 +20,28 @@
  */
 
 
+
 class Class_CodifEmplacementLoader extends Storm_Model_Loader
 {
   use Trait_FacetableLoader;
 
-  public function getLabel($id)
+  public function getLabel($id): string
   {
     return ($emplacement = Class_CodifEmplacement::find($id))
       ? $emplacement->getLibelle()
       : '';
   }
+
+
+  public function findOrCreateFromLabel(string $label): Class_CodifEmplacement
+  {
+    if ($emplacement = $this->findFirstBy(['libelle' => $label]))
+      return $emplacement;
+
+    $emplacement = (new Class_CodifEmplacement())->setLibelle($label);
+    $emplacement->save();
+    return $emplacement;
+  }
 }
 
 
@@ -56,7 +68,6 @@ class Class_CodifEmplacement extends Storm_Model_Abstract
 
   public function validate() {
     $this->checkAttribute('libelle', '' != $this->getLibelle(), $this->_('Vous devez définir le libellé'));
-    $this->checkAttribute('regles', '' != $this->getRegles(), $this->_('Vous devez définir au moins une règle'));
   }
 
 
diff --git a/library/Class/CodifSection.php b/library/Class/CodifSection.php
index a187e4c6d06..87b22d2d5a1 100644
--- a/library/Class/CodifSection.php
+++ b/library/Class/CodifSection.php
@@ -24,6 +24,15 @@ class Class_CodifSectionLoader extends Storm_Model_Loader
 {
   use Trait_Translator, Trait_FacetableLoader;
 
+  public function findOrCreateFromLabel(string $label) : Class_CodifSection {
+    if ($section = $this->findFirstBy(['libelle' => $label]))
+      return $section;
+    $section = (new Class_CodifSection())->setLibelle($label);
+    $section->save();
+    return $section;
+  }
+
+
   public function labelsOfIds($ids) {
     if (empty($ids))
       return new Storm_Model_Collection();
diff --git a/library/Class/Codification/DataProfileRule.php b/library/Class/Codification/DataProfileRule.php
new file mode 100644
index 00000000000..c9918768edf
--- /dev/null
+++ b/library/Class/Codification/DataProfileRule.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_Codification_DataProfileRule extends Class_Codification_Rule {
+
+  public static function newInstance(string $zone = '',
+                                     string $operator = '',
+                                     int $with_space = 0,
+                                     string $values = '',
+                                     string $label = '') : Class_Codification_DataProfileRule {
+
+    $values = array_unique(array_filter(explode(static::VALUE_SEPARATOR,
+                                                static::_formatValue($with_space,
+                                                                     $values)),
+                                        fn($value) => '' !== trim($value)));
+
+    return $values
+      ? new static($zone, $operator, $values, $with_space, $label)
+      : static::nullinstance();
+  }
+
+
+  public static function nullinstance() : Class_Codification_DataProfileRuleNull {
+    return new Class_Codification_DataProfileRuleNull;
+  }
+}
+
+
+
+
+class Class_Codification_DataProfileRuleNull extends Class_Codification_DataProfileRule {
+}
diff --git a/library/Class/Codification/Rule.php b/library/Class/Codification/Rule.php
index 9f01c2c9851..21b3cd608c7 100644
--- a/library/Class/Codification/Rule.php
+++ b/library/Class/Codification/Rule.php
@@ -33,6 +33,7 @@ class Class_Codification_Rule {
     $_operator,
     $_with_space,
     $_values = [],
+    $_label,
     $_type,
     $_codif;
 
@@ -57,11 +58,17 @@ class Class_Codification_Rule {
   }
 
 
-  public function __construct($zone, $operator, $values, $with_space) {
+  public function __construct(string $zone = '',
+                              string $operator = '',
+                              array $values = [],
+                              int $with_space = 0,
+                              string $label = '')
+  {
     $this->_zone = $zone;
     $this->_operator = $operator;
     $this->_values = $values;
     $this->_with_space = $with_space;
+    $this->_label = $label;
   }
 
 
@@ -110,7 +117,13 @@ class Class_Codification_Rule {
 
 
   public function getCodif() {
-    return $this->_codif;
+    return $this->_codif ??= $this->createCodif();
+  }
+
+
+  public function createCodif() : Storm_Model_Abstract {
+    $classname = 'Class_Codif' . ucfirst($this->_type);
+    return $classname::findOrCreateFromLabel($this->_label);
   }
 
 
@@ -135,11 +148,21 @@ class Class_Codification_Rule {
   }
 
 
-  public function isGenre() {
+  public function isGenre() : bool {
     return Class_Codification_RulesHelper::TYPE_GENRE === $this->getType();
   }
 
 
+  public function isSection() : bool {
+    return Class_Codification_RulesHelper::TYPE_SECTION === $this->getType();
+  }
+
+
+  public function isLocation() : bool {
+    return Class_Codification_RulesHelper::TYPE_EMPLACEMENT === $this->getType();
+  }
+
+
   public function getValuesAsString() {
     return implode(static::VALUE_SEPARATOR, array_unique($this->_values));
   }
@@ -192,7 +215,7 @@ class Class_Codification_Rule {
     return $this->_zone
       . $this->_getOperatorAsHumanRead()
       . $this->_getValuesAsHumanRead()
-      . ('1' === $this->getWithSpace()
+      . (1 === $this->getWithSpace()
          ? $this->_(' avec espace(s)')
          : $this->_(' sans espace(s)'));
   }
diff --git a/library/Class/Codification/RulesHelper.php b/library/Class/Codification/RulesHelper.php
index e16ca14b2d0..75444f0ce4d 100644
--- a/library/Class/Codification/RulesHelper.php
+++ b/library/Class/Codification/RulesHelper.php
@@ -25,16 +25,33 @@ class Class_Codification_RulesHelper {
   const
     TYPE_GENRE = 'genre',
     TYPE_SECTION = 'section',
-    TYPE_EMPLACEMENT = 'emplacement';
-
-  protected $_list_rules;
+    TYPE_EMPLACEMENT = 'emplacement',
+    ZONE_KEY = '_zone',
+    MODE_KEY = '_mode',
+    WS_KEY = '_whitespace',
+    VALUE_KEY = '_value',
+    LABEL_KEY = '_label',
+    DEFAULT_ITEM_FIELD = '995$0';
+
+  protected
+    $_list_rules,
+    $_profile_list_rules_cache,
+    $_data_profile;
 
   public function getCodifId($type, $zone_field, $value) {
+    $detect_closure = fn($codif_rule) => $codif_rule->isZoneContainsValue($zone_field, $value)
+      || $codif_rule->isZoneContainsValue(self::DEFAULT_ITEM_FIELD, $value);
+
+    $detect = $this
+      ->_getProfileListRules()
+      ->detect($detect_closure);
+
+    if ($detect && $id = $detect->getCodif()->getId())
+      return $id;
+
     $detect = $this
       ->_getListRules()
-      ->detect(
-               fn($codif_rule) => $codif_rule->isZoneContainsValue($zone_field, $value)
-                 || $codif_rule->isZoneContainsValue('995$0', $value));
+      ->detect($detect_closure);
 
     return $detect
       ? $detect->getCodif()->getId()
@@ -43,18 +60,92 @@ class Class_Codification_RulesHelper {
 
 
   public function getCodifIdByGenre($notice_unimarc) {
+    $select_closure = fn($rule) => $rule->isGenre();
+
+    $inject_closure = fn($next_result, $rule) => $this->_extractFieldsValues($next_result,
+                                                                             $rule,
+                                                                             $notice_unimarc);
+
+    $profile_list_rules = $this
+      ->_getProfileListRules()
+      ->select($select_closure)
+      ->injectInto([], $inject_closure);
+
+    if (!empty(array_filter($profile_list_rules)))
+      return array_filter($profile_list_rules);
+
+
     $list_result = $this
       ->_getListRules()
-      ->select(fn($rule) => $rule->isGenre())
-      ->injectInto([],
-                   fn($next_result, $rule) => $this->_extractFieldsValues($next_result,
-                                                      $rule,
-                                                      $notice_unimarc));
+      ->select($select_closure)
+      ->injectInto([], $inject_closure);
 
     return array_filter($list_result);
   }
 
 
+  public function _getProfileListRules() : Storm_Collection {
+    if (isset($this->_profile_list_rules_cache))
+      return $this->_profile_list_rules_cache;
+
+    $this->_profile_list_rules_cache = new Storm_Collection;
+
+    foreach ([Class_Cosmogramme_CodifImporter::SORT_KEY,
+              Class_Cosmogramme_CodifImporter::SECTION_KEY,
+              Class_Cosmogramme_CodifImporter::LOCATION_KEY] as $type)
+      $this->_getProfileListRulesForType($type);
+
+    return $this->_profile_list_rules_cache;
+  }
+
+
+  public function resetListRules() : self {
+    unset($this->_profile_list_rules_cache,
+          $this->_data_profile);
+
+    return $this;
+  }
+
+
+  protected function _getProfileListRulesForType(string $type) : self {
+    if (!$profile_rules = $this->getDataProfileAttribute($type))
+      return $this;
+
+    $key = $type . '_label';
+
+    if ( ! isset($profile_rules[$key]))
+      return $this;
+
+    foreach ($profile_rules[$key] as $i => $rule)
+      $this->_profile_list_rules_cache
+        ->add(Class_Codification_DataProfileRule::newInstance(...$this->_profileRuleFromIndex($i, $type, $profile_rules))
+              ->setType($this->_ruleType($type)));
+
+    return $this;
+  }
+
+
+  protected function _ruleType(string $type) : string {
+    if ($type == 'sorts')
+      return static::TYPE_GENRE;
+
+    if ($type == 'sections')
+      return static::TYPE_SECTION;
+
+    if ($type == 'locations')
+      return static::TYPE_EMPLACEMENT;
+  }
+
+
+  protected function _profileRuleFromIndex(int $index, string $type, array $rules) : array {
+    return [$rules[$type.static::ZONE_KEY][$index] ?? '',
+            $rules[$type.static::MODE_KEY][$index] ?? '',
+            $rules[$type.static::WS_KEY][$index] ?? 0,
+            $rules[$type.static::VALUE_KEY][$index] ?? '',
+            $rules[$type.static::LABEL_KEY][$index] ?? ''];
+  }
+
+
   public function _getListRules() {
     if ($this->_list_rules === null)
       $this->_init();
@@ -63,6 +154,17 @@ class Class_Codification_RulesHelper {
   }
 
 
+  public function setDataProfile(array $profile) : self {
+    $this->_data_profile = $profile;
+    return $this;
+  }
+
+
+  public function getDataProfileAttribute(string $key) : array {
+    return $this->_data_profile['attributs'][$key] ?? [];
+  }
+
+
   protected function _init() {
     $this->_list_rules = new Storm_Collection;
 
diff --git a/library/Class/CommSigb.php b/library/Class/CommSigb.php
index 0c8e4efe0db..79b88da326d 100644
--- a/library/Class/CommSigb.php
+++ b/library/Class/CommSigb.php
@@ -81,12 +81,6 @@ class Class_CommSigb {
     if ($emplacement = $sigb_exemplaire->getEmplacement())
       $exemplaire->setEmplacement($emplacement);
 
-    if (!$code_annexe = $sigb_exemplaire->getCodeAnnexe())
-      return $exemplaire;
-
-    if ($annexe = Class_CodifAnnexe::findFirstBy(['id_origine' => $code_annexe]))
-      $exemplaire->setIdBib($annexe->getIdBib());
-
     return $exemplaire;
   }
 
@@ -151,6 +145,13 @@ class Class_CommSigb {
     if (($central_library_id = $this->_isHoldCentralizedAndGetCentralLibrary($user, $exemplaire)) !== 0)
       return $this->_centralizedHold($user, $exemplaire, $central_library_id);
 
+    if ( ! $this->SIGBConnect($user) )
+      return $this->_error('Impossible de contacter le serveur de votre bibliothèque.');
+
+    if ( ! $exemplaire->canBeHeldBy($user) )
+      return $this->_error('Vous ne pouvez pas réserver cet exemplaire !');
+
+
     $reserver = function ($user, $sigb) use ($exemplaire, $code_annexe) {
 
       $result = $sigb->reserverExemplaire($user, $exemplaire, $code_annexe);
@@ -171,10 +172,12 @@ class Class_CommSigb {
   }
 
 
-  protected function _isHoldCentralizedAndGetCentralLibrary(Class_Users $user, Class_Exemplaire $exemplaire ) :int {
-    return ($central_library = Class_CosmoVar::get('centralized_hold_mode'))
-      && ($user->getIdIntBib() != $central_library)
-      && ($exemplaire->getIdIntBib() != $user->getIdIntBib())
+  protected function _isHoldCentralizedAndGetCentralLibrary(Class_Users $user, Class_Exemplaire $item ): int
+  {
+    if ( ! $central_library = Class_CosmoVar::get('centralized_hold_mode'))
+      return 0;
+
+    return $item->shouldUseCentralizedHold($user)
       ? $central_library
       : 0;
   }
@@ -191,14 +194,16 @@ class Class_CommSigb {
         && !($use_cardnumber = $user->getUseCardNumber()))
       return $this->_error($this->_('Réservation impossible : votre bibliothèque n\'autorise pas la réservation des documents de la bibliotheque centrale'));
 
-    $sigb_central = $this->SIGBCentralLibraryConnect();
+    if (!$sigb_central = $this->_SIGBCentralLibraryConnect())
+      return $this->_error( implode(BR, $this->getErrors()) );
+
     $result = $sigb_central->reserverExemplaire($user, $exemplaire, $central_library_id);
 
     if (true === ($result['statut'] ?? false))
       return ($this->notifyCentralizedHold($user, $exemplaire, $central_library_id)->hasErrors())
         ? $this->_error(implode("\n",$sigb_central->getErrors()))
         : $result;
-    return null;
+    return $this->_error($result['erreur']);
   }
 
 
@@ -299,21 +304,32 @@ class Class_CommSigb {
     return $this->withUserAndSIGBDo($std_user, $prolonger);
   }
 
-  public function SIGBCentralLibraryConnect() {
-    if (!$int_bib = Class_IntBib::find(Class_CosmoVar::get('centralized_hold_mode')))
-      return  $this->_error($this->_('Pas de paramètres pour la bibliothèque centralisée'));
-    if (($sigb = $int_bib->getSIGBComm())  && $sigb->isConnected())
+
+  protected function _SIGBCentralLibraryConnect() : ?Class_WebService_SIGB_LibraryWrapper{
+    if (!$int_bib = Class_IntBib::find(Class_CosmoVar::get('centralized_hold_mode'))){
+      $this->addError($this->_('Pas de paramètres pour la bibliothèque centralisée'));
+      return null;
+    }
+
+    if (($sigb = $int_bib->getSIGBComm())
+        && $sigb->isConnected())
       return $sigb;
-    return $this->_error($this->_("Une erreur de communication avec le serveur a fait échouer la requête. Merci de signaler ce problème à la bibliothèque."));
+
+    $this->addError($this->_("Une erreur de communication avec le serveur a fait échouer la requête. Merci de signaler ce problème à la bibliothèque."));
+    return null;
   }
 
 
   public function SIGBConnect($user) {
-    if (null == $sigb = $user->getSIGBComm())
-      return $this->_error($this->_('Communication SIGB indisponible'));
+    if (null == $sigb = $user->getSIGBComm()){
+      $this->addError($this->_('Communication SIGB indisponible'));
+      return null;
+    }
 
-    if (!$sigb->isConnected())
-      return $this->_error($this->_("Une erreur de communication avec le serveur a fait échouer la requête. Merci de signaler ce problème à la bibliothèque."));
+    if (!$sigb->isConnected()){
+      $this->addError($this->_("Une erreur de communication avec le serveur a fait échouer la requête. Merci de signaler ce problème à la bibliothèque."));
+      return null;
+    }
 
     return $sigb;
   }
@@ -328,8 +344,10 @@ class Class_CommSigb {
                                                  function() use ($user): void {
                                                    Class_WebService_SIGB_EmprunteurCache::newInstance()->remove($user);
                                                  });
-    $sigb = $this->SIGBConnect($user);
-    return (is_array($sigb)) ? $sigb : $closure($user, $sigb);
+    if ($sigb = $this->SIGBConnect($user))
+      return $closure($user, $sigb);
+
+    return $this->_error(implode(BR, $this->getErrors()));
   }
 
 
diff --git a/library/Class/Cosmogramme/CodifImporter.php b/library/Class/Cosmogramme/CodifImporter.php
new file mode 100644
index 00000000000..cf00a51db50
--- /dev/null
+++ b/library/Class/Cosmogramme/CodifImporter.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_Cosmogramme_CodifImporter {
+  use Trait_Translator, Trait_Errors;
+
+  const SORT_KEY = 'sorts';
+  const SECTION_KEY = 'sections';
+  const LOCATION_KEY = 'locations';
+
+  const FROM_GLOBALS = 1;
+  const FROM_PROFILE = 2;
+
+  protected
+    $_target_id,
+    $_incoming_id,
+    $_codif_types,
+    $_from,
+    $_target_attributes,
+    $_incoming_attributes;
+
+  public function __construct(int $target_id = 0,
+                              int $incoming_id = 0,
+                              array $codif_types = [],
+                              int $from = 0) {
+    $this->_target_id = $target_id;
+    $this->_incoming_id = $incoming_id;
+    $this->_codif_types = $codif_types;
+    $this->_from = $from;
+
+    if (empty($this->_codif_types))
+      return $this->addError($this->_('Pas de type de règle fournie. Rien n\'a été importé'));
+
+    $this->_getTargetAttributes()
+         ->_getIncomingAttributes();
+  }
+
+
+  public function import() : self {
+    if ($this->hasErrors())
+      return $this;
+
+    foreach ([static::SORT_KEY, static::SECTION_KEY, static::LOCATION_KEY] as $key)
+      $this->_incomingKeyToTarget($key);
+
+    if (!$data_profile = Class_IntProfilDonnees::find($this->_target_id))
+      return $this->addError($this->_('Profil de données inexistant'));
+
+    $data_profile->setAttributs($this->_target_attributes)
+                 ->save();
+
+    return $this;
+  }
+
+
+  protected function _incomingKeyToTarget(string $key) : self {
+    if (!in_array($key, $this->_codif_types))
+      return $this;
+
+    if ($rules = $this->_incoming_attributes[$key] ?? [])
+      $this->_target_attributes[$key] = $rules;
+
+    return $this;
+  }
+
+
+  protected function _getTargetAttributes() : self {
+    if (null == $profile = Class_IntProfilDonnees::find($this->_target_id))
+      return $this->addError($this->_('Profil de données cible inconnu'));
+
+    $this->_target_attributes = $profile->getAttributsAsArray() ?? [];
+
+    return $this;
+  }
+
+
+  protected function _getIncomingAttributes() : self {
+    if ($this->_from == static::FROM_GLOBALS)
+      return $this->_getGlobalCodifications();
+
+    if (null == $profile = Class_IntProfilDonnees::find($this->_incoming_id))
+      return $this->addError($this->_('Profil de données inconnu'));
+
+    $this->_incoming_attributes = $profile->getAttributsAsArray() ?? [];
+
+    return $this;
+  }
+
+
+  protected function _getGlobalCodifications() : self {
+    $this->_incoming_attributes =
+      array_merge($this->_globalCodifsFor(Class_CodifGenre::findAllBy(['regles not' => '']),
+                                          'sorts'),
+                  $this->_globalCodifsFor(Class_CodifSection::findAllBy(['regles not' => '']),
+                                          'sections'),
+                  $this->_globalCodifsFor(Class_CodifEmplacement::findAllBy(['regles not' => ''])
+                                          ,'locations'));
+    return $this;
+  }
+
+
+  protected function _globalCodifsFor($codifs, string $prefix) : array {
+    $rules = [];
+    foreach ($codifs as $codif)
+      $rules = $this->_mergeRules($rules, $this->_extractRule($codif), $prefix);
+
+    return $rules;
+  }
+
+
+  protected function _mergeRules(array $rules, array $rule, string $prefix) : array {
+    foreach (['_label', '_zone', '_mode', '_value', '_whitespace'] as $index => $key)
+      $rules[$prefix][$prefix . $key][] = $rule[$index];
+
+    return $rules;
+  }
+
+
+  protected function _extractRule($rule) : array {
+    $codif_rule = Class_Codification_DataProfileRule::newFrom($rule->getRegles());
+    return [$rule->getLibelle(),
+            $this->_extractZone($codif_rule),
+            $codif_rule->getOperator(),
+            $codif_rule->getValuesAsString(),
+            $codif_rule->getWithSpace()];
+  }
+
+
+  protected function _extractZone(Class_Codification_Rule $rule) : string {
+    return $rule->getField()
+      ? $rule->getZone() . '$' . $rule->getField()
+      : $rule->getZone();
+  }
+}
diff --git a/library/Class/Cosmogramme/Integration/PhaseAbstract.php b/library/Class/Cosmogramme/Integration/PhaseAbstract.php
index ec536306acc..2e560eab5ea 100644
--- a/library/Class/Cosmogramme/Integration/PhaseAbstract.php
+++ b/library/Class/Cosmogramme/Integration/PhaseAbstract.php
@@ -51,6 +51,12 @@ abstract class Class_Cosmogramme_Integration_PhaseAbstract {
   }
 
 
+  protected function _resetCodificationRules() : self
+  {
+    return $this;
+  }
+
+
   protected function getSqlAdapter() {
     if(!$this->_sql_adapter)
       $this->_sql_adapter = Zend_Db_Table::getDefaultAdapter();
diff --git a/library/Class/Cosmogramme/Integration/PhaseNotice.php b/library/Class/Cosmogramme/Integration/PhaseNotice.php
index cdfb68152b4..7fb1ae9960d 100644
--- a/library/Class/Cosmogramme/Integration/PhaseNotice.php
+++ b/library/Class/Cosmogramme/Integration/PhaseNotice.php
@@ -33,6 +33,20 @@ class Class_Cosmogramme_Integration_PhaseNotice
   }
 
 
+  protected function _resetCodificationRules() : self {
+    if (null !== $this->_codification_rules)
+      $this->_codification_rules->resetListRules();
+
+    return $this;
+  }
+
+
+  protected function _initOne(){
+    $this->_resetCodificationRules();
+    parent::_initOne();
+  }
+
+
   public function importRecord($data, $integration) {
     $integrator = new notice_integration();
     $integrator->setCodificationRules($this->_codification_rules);
diff --git a/library/Class/Cosmogramme/Integration/Record/Patron.php b/library/Class/Cosmogramme/Integration/Record/Patron.php
index a53a45141e9..39fbb323b27 100644
--- a/library/Class/Cosmogramme/Integration/Record/Patron.php
+++ b/library/Class/Cosmogramme/Integration/Record/Patron.php
@@ -48,7 +48,9 @@ class Class_Cosmogramme_Integration_Record_Patron {
     $enreg = $this->prepareData($enreg);
 
     if ($library_code = $this->_known_fields->libraryCodeFrom($enreg)) {
-      $annexe = Class_CodifAnnexe::findFirstBy(['id_origine' => $library_code]);
+      $annexe = Class_CodifAnnexe::findByCodeAndBib($library_code, $this->_integration
+                                                    ? $this->_integration->getIdBib()
+                                                    : '');
       $enreg['ID_SITE'] = $annexe ? $annexe->getIdBib() : $this->_integration->getIdBib();
     }
 
diff --git a/library/Class/Cosmogramme/Integration/SplitBySite.php b/library/Class/Cosmogramme/Integration/SplitBySite.php
index 7cbb654599c..9f236201f4a 100644
--- a/library/Class/Cosmogramme/Integration/SplitBySite.php
+++ b/library/Class/Cosmogramme/Integration/SplitBySite.php
@@ -64,25 +64,30 @@ class Class_Cosmogramme_Integration_SplitBySite  {
     $this->_logHash($this->_items_by_site, 'exemplaires');
 
     $this->getLogger()->log(sprintf(' %d notices traitées au total', $count_processed));
+
+    $this->getFileSystem()->delete($file_path_to_split);
+
+    $this->getLogger()->log(sprintf('Fichier %s supprimé', $file_path_to_split));
+
     return $this;
   }
 
 
   protected function _logTitle(string $sep_char, string $title, string $level) : self {
-    $this->getLogger()->log('\n');
+    $this->getLogger()->log("\n");
     $this->getLogger()->log(str_repeat($sep_char,50));
     $this->getLogger()->log($title);
     $this->getLogger()->log(str_repeat($sep_char,50));
-    $this->getLogger()->log(str_repeat('\n', $level));
+    $this->getLogger()->log(str_repeat("\n", $level));
     return $this;
   }
 
 
   protected function _logHash(array $hash, string $value_label) :self {
-    $this->_logTitle('-', sprintf('\t\t Statistiques %s', $value_label), 1);
+    $this->_logTitle('-', sprintf("\t\t Statistiques %s", $value_label), 1);
 
     foreach ($hash as $id_site => $count)
-      $this->getLogger()->log(sprintf(' site %s : %4d %s', $id_site,$count, $value_label));
+      $this->getLogger()->log(sprintf(" site %s : %4d %s", $id_site,$count, $value_label));
     return $this;
   }
 
@@ -134,7 +139,7 @@ class Class_Cosmogramme_Integration_SplitBySite  {
     $this->_items_by_site [$id_site] += count($zone995s);
 
     $new_record->update();
-    $this->_siteFileAppend($id_site, $new_record->getFullRecord());
+    $this->_siteFileAppend(trim($id_site), $new_record->getFullRecord());
     return $this;
   }
 
diff --git a/library/Class/Exemplaire.php b/library/Class/Exemplaire.php
index c1533a34b67..6c944520966 100644
--- a/library/Class/Exemplaire.php
+++ b/library/Class/Exemplaire.php
@@ -20,17 +20,6 @@
  */
 
 class Class_ExemplaireLoader extends Storm_Model_Loader {
-  protected $_annexes_by_code = [];
-
-
-  public function getCachedCodifAnnexeByCode($code) {
-    // For cosmogramme integration performance.
-    if (!isset($this->_annexes_by_code[$code]))
-      $this->_annexes_by_code[$code] = Class_CodifAnnexe::findByCode($code);
-    return $this->_annexes_by_code[$code];
-  }
-
-
 
 
   public function findFirstBySIGBOperation($user, $operation) {
@@ -39,7 +28,7 @@ class Class_ExemplaireLoader extends Storm_Model_Loader {
 
     $params = $operation->getCodeBarre()
       ? ['code_barres' => $operation->getCodeBarre()]
-      : ['id_origine' => $operation->getNoNotice()];
+    : ['id_origine' => $operation->getNoNotice()];
 
     if (!array_filter($params))
       return null;
@@ -121,7 +110,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
                                 'referenced_in' => 'id_origine'],
 
                     'int_bib' => [ 'model' => 'Class_IntBib',
-                                  'referenced_in' => 'id_int_bib'],
+                                   'referenced_in' => 'id_int_bib'],
 
                     'data_profile' => ['model' => 'Class_IntProfilDonnees',
                                        'referenced_in' => 'id_data_profile']
@@ -148,8 +137,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
                                           'shelf_key' => '',
                                           'shelf_key_update_date' => '0000-00-00 00:00:00'];
 
-  protected
-    $_sigb_exemplaire;
+  protected $_sigb_exemplaire;
 
 
   public function incrementGroupCount() {
@@ -213,7 +201,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
     $cote = $this->getCote();
 
     if ( ! $ils_ws_item_cote =
-        $this->_withILSWsItemDo(fn($ils_ws_item) => $ils_ws_item->getCote()))
+         $this->_withILSWsItemDo(fn($ils_ws_item) => $ils_ws_item->getCote()))
       return $cote;
 
     return strlen($cote) > strlen($ils_ws_item_cote)
@@ -222,10 +210,8 @@ class Class_Exemplaire extends Storm_Model_Abstract {
   }
 
 
-  public function getLibelleSite() {
-    if (null === ($site = Class_CodifAnnexe::findFirstBy(['code' => (string)$this->getAnnexe()])))
-      return '';
-    return $site->getLibelle();
+  public function getLibelleSite() : string {
+    return Class_CodifAnnexe::getLibraryLabelFromItem($this);
   }
 
 
@@ -395,7 +381,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
 
   public function updateAvailabilityAndLocationFromSIGBAndSaveIfNeeded() : self {
     if ( (! $sigb_exemplaire = $this->_withILSWsItemDo(fn($ils_ws_item) => $ils_ws_item))
-        && $this->getIsAvailable()) {
+         && $this->getIsAvailable()) {
       $this->setIsAvailable(false)->save();
       return $this;
     }
@@ -406,14 +392,20 @@ class Class_Exemplaire extends Storm_Model_Abstract {
     $this->setUrlForCurrentUser($sigb_exemplaire->getUrlForCurrentUser());
 
     $new_annexe = $this->getCodeAnnexe();
+
     $new_availability = $sigb_exemplaire->isDisponible();
 
     if (($new_annexe == $this->getAnnexe())
         && ($new_availability  == $this->getIsAvailable()))
       return $this;
 
+    $annexe_id = ($annexe = Class_CodifAnnexe::findByCodeAndBib($new_annexe, $this->getIdBib()))
+      ? $annexe->getId()
+      : $new_annexe;
+
+
     $this
-      ->setAnnexe($new_annexe)
+      ->setAnnexe($annexe_id)
       ->setIsAvailable($new_availability)
       ->save();
 
@@ -448,16 +440,28 @@ class Class_Exemplaire extends Storm_Model_Abstract {
   }
 
 
-  public function getBibNumberFromIdOrigine() {
-    if ($this->getDataProfile()
-        && $this->getDataProfile()->isSerialFormatKoha()
-        && $this->getNotice()->isPeriodique() )
-      return ($data = explode('-',$this->getIdOrigine())[0])
-        ? $data
-        : null;
+  public function getBibNumberFromIdOrigine(): string
+  {
+    $id_origine = $this->getIdOrigine() ?? '';
+    if ( ! $profile = $this->getDataProfile())
+      return $id_origine;
+
+    if ( ! $profile->isSerialFormatKoha())
+      return $id_origine;
 
-    return $this->getIdOrigine();
+    if ( ! $record = $this->getNotice())
+      return $id_origine;
 
+    if ( ! $record->isPeriodique())
+      return $id_origine;
+
+    return $this->_getIdOrigineFirstPart();
+  }
+
+
+  protected function _getIdOrigineFirstPart(): string
+  {
+    return explode('-', $this->getIdOrigine())[0] ?? '';
   }
 
 
@@ -488,8 +492,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
 
 
   public function getCodifAnnexe() {
-    return $this->getLoader()
-                ->getCachedCodifAnnexeByCode($this->getAnnexe());
+    return Class_CodifAnnexe::find($this->getAnnexe());
   }
 
 
@@ -538,7 +541,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
       return null;
 
     if ( (null == ($ils_ws_item = $this->_getILSWsItemFromIntBibOrCache()))
-        || (! $ils_ws_item->isValid()))
+         || (! $ils_ws_item->isValid()))
       return null;
 
     return $callback($ils_ws_item);
@@ -709,7 +712,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
     if ($emplacement = trim((string)$this->getEmplacement()))
       $facets []= Class_CodifEmplacement::CODE_FACETTE.$emplacement;
 
-    if ($annexe = trim((string)$this->getAnnexe()))
+    if ($annexe = trim((string) $this->getAnnexe()))
       $facets []= Class_CodifAnnexe::CODE_FACETTE.$annexe;
 
     if ($this->isDisponible(true))
@@ -724,7 +727,7 @@ class Class_Exemplaire extends Storm_Model_Abstract {
     if ($novelty = Class_CodifThesaurus::findForItemAnnexeNovelty($this))
       $facets[] = $novelty;
 
-    return $facets;
+    return array_filter($facets);
   }
 
 
@@ -741,7 +744,6 @@ class Class_Exemplaire extends Storm_Model_Abstract {
 
 
   public function requiresCalendarHold() : bool {
-
     return (($intbib = $this->getIntBib())
             && $intbib->isCommKoha()
             && (bool) $this->_withILSWsItemDo(fn($ils_ws_item) => $ils_ws_item->requiresCalendarHold()));
@@ -761,6 +763,57 @@ class Class_Exemplaire extends Storm_Model_Abstract {
   }
 
 
+  protected function _isCentralizedCompatible(Class_Users $user): bool
+  {
+    if ( ! $central_library_id = Class_CosmoVar::get('centralized_hold_mode'))
+      return false;
+
+    if ( ! $item_intbib = $this->getIdIntBib())
+      return false;
+
+    if ( ! $user_intbib = $user->getIntBib())
+      return false;
+
+    if ( $central_library_id != $item_intbib)
+      return false;
+
+    return (bool) $user_intbib->getUseCardNumber();
+  }
+
+
+  public function shouldUseCentralizedHold(Class_Users $user): bool
+  {
+    if ( ! $this->_isCentralizedCompatible($user))
+      return false;
+
+    return $this->getIntBib()->getUrlServer() != $user->getUrlServer();
+  }
+
+
+  public function canBeHeldBy(Class_Users $user): bool
+  {
+    return ($server = $this->getUrlServer())
+      && ($server == $user->getUrlServer());
+  }
+
+
+  public function getUrlServer(): string
+  {
+    return ($int_bib = $this->getIntBib())
+      ? $int_bib->getUrlServer() ?? ''
+      : '';
+  }
+
+
+  public function canBeHeldFromCentralizedLibrary(): bool
+  {
+    if ( ! $central_library_id = Class_CosmoVar::get('centralized_hold_mode'))
+      return false;
+
+    return $this->getIdIntBib() == $central_library_id;
+  }
+
+
   public function isFromKoha(): bool
   {
     return 1 === Class_IntBib::query()
@@ -780,5 +833,14 @@ class Class_Exemplaire extends Storm_Model_Abstract {
   {
     $barcode = explode('-', $this->getCodeBarres());
     return array_shift($barcode);
+
+  }
+
+
+  public function annexeCode(): string
+  {
+    return ($annexe = Class_CodifAnnexe::find($this->getAnnexe()))
+      ? $annexe->getCode()
+      : '';
   }
 }
diff --git a/library/Class/Hold/Notify/User.php b/library/Class/Hold/Notify/User.php
index 8144b6a0b06..ba4f31f733d 100644
--- a/library/Class/Hold/Notify/User.php
+++ b/library/Class/Hold/Notify/User.php
@@ -22,7 +22,7 @@
 
 class Class_Hold_Notify_User
   extends Class_Hold_AbstractNotify {
-  protected string $_template_name = 'NOTIFICATION_TEMPLATE_NEW_RESERVATION_USER_LIBRARY';
+  protected string $_template_name = 'NOTIFICATION_TEMPLATE_NEW_RESERVATION_USER';
 
 
   public function __construct(Class_Users $user, Class_Exemplaire $item, string $annexe)
diff --git a/library/Class/Hold/NotifyUser.php b/library/Class/Hold/NotifyUser.php
index 5b8c5e46773..9a638d9d9d0 100644
--- a/library/Class/Hold/NotifyUser.php
+++ b/library/Class/Hold/NotifyUser.php
@@ -24,7 +24,8 @@ class Class_Hold_NotifyUser extends Class_Hold_AbstractNotify {
   protected string $_template_name ='NOTIFICATION_TEMPLATE_NEW_RESERVATION';
 
   public function getPickupLibraryData(string $annexe_code){
-    return Class_CodifAnnexe::findFirstBy(['id_origine' => $annexe_code]);
+    return Class_CodifAnnexe::findByCodeAndBib($annexe_code,
+                                               $this->_item ? $this->_item->getIdBib() : '');
   }
 
 
diff --git a/library/Class/IntBib.php b/library/Class/IntBib.php
index dcabea7f576..137d5a49905 100644
--- a/library/Class/IntBib.php
+++ b/library/Class/IntBib.php
@@ -94,7 +94,9 @@ class IntBibLoader extends Storm_Model_Loader {
 
 
   public function findAllWithWebServices() {
-    return Class_IntBib::findAllBy(['comm_sigb' => Class_IntBib::allCommSigbCodes()]);
+    return Class_IntBib::query()
+      ->in('comm_sigb', Class_IntBib::allCommSigbCodes())
+      ->fetchAll();
   }
 
 
@@ -136,9 +138,10 @@ class IntBibLoader extends Storm_Model_Loader {
     $combo =
       $this->_findAllWithPreRegistration(fn($int_bib) => $int_bib->isPreRegistrationNanookEnabled())
            ->injectInto([],
-                        function($combo, $library)
-                        {
-                          $combo[$library->getId()] = $library->getLabel();
+                        function($combo, $library) {
+                          foreach (Class_CodifAnnexe::findByIdBib($library->getId()) as $branch)
+                            $combo[$branch->getId()] = $branch->getLibelle();
+
                           return $combo;
                         });
 
@@ -658,9 +661,6 @@ class Class_IntBib extends Storm_Model_Abstract {
 
 
   public function getUseCardNumber() : string{
-    if(!$this->isOrphee() ||!$this->isCommKoha())
-      return '';
-
     $params = $this->getCommParamsAsArray();
     return ($params['use_card_number'] ?? '');
   }
diff --git a/library/Class/IntBib/ForUrl.php b/library/Class/IntBib/ForUrl.php
new file mode 100644
index 00000000000..1928399de47
--- /dev/null
+++ b/library/Class/IntBib/ForUrl.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Copyright (c) 2012-2024, 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_IntBib_ForUrl extends Class_IntBib_SingleSigb
+{
+
+  public function intBibsForUrlServer(string $url_server): array
+  {
+    $rows = Class_IntBib::query()
+      ->select(['id_bib', 'sigb', 'comm_params'])
+      ->not_eq('sigb', Class_IntBib::SIGB_NONE)
+      ->fetchAll();
+    $int_bibs = $this->_groupByAttributes($rows);
+
+    $this->_unique_url = $this->_url(['url_serveur' => $url_server]);
+
+    $ids = [];
+    foreach ($int_bibs as $id => $comm_params)
+      if ($id && $this->_isSameUrl($comm_params))
+        $ids [] = $id;
+
+    return $ids;
+  }
+
+
+  protected function _groupByAttributes(array $rows): array
+  {
+    $models = [];
+
+    foreach ($rows as $row) {
+      $id = $row->getIdBib();
+
+      $models [$id] ??= [];
+      $models [$id][] = $row->getCommParams();
+    }
+
+    return $models;
+  }
+}
diff --git a/library/Class/IntBib/SingleSigb.php b/library/Class/IntBib/SingleSigb.php
index f68e56d452f..9f927d16f4a 100644
--- a/library/Class/IntBib/SingleSigb.php
+++ b/library/Class/IntBib/SingleSigb.php
@@ -29,7 +29,8 @@ class Class_IntBib_SingleSigb
   public function isSingle(): bool
   {
     $this->_unique_url = null;
-    $int_bibs = $this->_intBibs();
+    $int_bibs = $this->_intBibs(Class_IntBib::query()
+                                ->select(['sigb', 'comm_params']));
 
     if ( ! $this->_isSizeValid($int_bibs))
       return false;
@@ -128,16 +129,22 @@ class Class_IntBib_SingleSigb
   }
 
 
-  protected function _intBibs(): array
+  protected function _intBibs(Storm_Query_Abstract $query): array
   {
-    $rows = Class_IntBib::query()
-      ->select(['sigb', 'comm_params'])
+    $rows = $query
       ->not_eq('sigb', Class_IntBib::SIGB_NONE)
       ->group('sigb')
       ->group('comm_params')
       ->fetchAll();
 
+    return $this->_groupByAttributes($rows);
+  }
+
+
+  protected function _groupByAttributes(array $rows): array
+  {
     $models = [];
+
     foreach ($rows as $row) {
       $sigb = $row->getSigb();
 
diff --git a/library/Class/IntProfilDonnees.php b/library/Class/IntProfilDonnees.php
index d1d2f46f671..819b6101631 100644
--- a/library/Class/IntProfilDonnees.php
+++ b/library/Class/IntProfilDonnees.php
@@ -38,6 +38,13 @@ class IntProfilDonneesLoader extends Storm_Model_Loader {
   }
 
 
+  public function getLabel(?int $id_profile) : string {
+    return ($profile = $this->find($id_profile))
+      ? $profile->getLibelle()
+      : '';
+  }
+
+
   public function getItemFields() {
     return [Class_IntProfilDonnees::FIELD_ITEM_ZONE => '995',
             Class_IntProfilDonnees::FIELD_ITEM_BARCODE => '',
@@ -1498,4 +1505,13 @@ class Class_IntProfilDonnees extends Storm_Model_Abstract {
       return [];
     return $a;
   }
+
+
+  public function hasCodifRules(): bool
+  {
+    return (in_array($this->getFormat(), [Class_IntProfilDonnees::FORMAT_UNIMARC,
+                                             Class_IntProfilDonnees::FORMAT_UNIMARC_XML,
+                                             Class_IntProfilDonnees::FORMAT_MARC21])
+            && $this->getTypeFichier() == Class_IntProfilDonnees::FT_RECORDS);
+  }
 }
diff --git a/library/Class/Migration/BranchFromCodeToIdNotice.php b/library/Class/Migration/BranchFromCodeToIdNotice.php
new file mode 100644
index 00000000000..d8106c21401
--- /dev/null
+++ b/library/Class/Migration/BranchFromCodeToIdNotice.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * Copyright (c) 2012-2024, 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_Migration_BranchFromCodeToIdNotice
+{
+  use Trait_MemoryCleaner, Trait_Translator;
+
+  const SEPCHAR = ';';
+
+  protected
+    $_items_done = [],
+    $_records_id_annexe = [],
+    $_adapter,
+    $_count = 0,
+    $_total = 0,
+    $_annexes = [],
+    $_elem_per_page = 1000,
+    $_field = '',
+    $_table = '',
+    $_separator = '';
+
+
+  protected array $_annexes_cache = [];
+
+  public function __construct() {
+    $this->_adapter = Zend_Db_Table::getDefaultAdapter();
+  }
+
+
+
+  public function run()
+  {
+    if ($this->_adapter->query("SELECT `clef` from `variables` WHERE `CLEF` = 'CODIFS_MIGRATED';")->fetchAll())
+      return;
+
+    $annexes = $this->_adapter->query("SELECT `id_annexe`, `code` from `codif_annexe`;")->fetchAll();
+
+    $this->_annexes = array_combine(array_map(fn($ann) => $ann['code'],
+                                              $annexes),
+                                    array_map(fn($ann) => $ann['id_annexe'],
+                                              $annexes));
+
+    Class_Notice::dropIndexMotsNotice();
+    $this->_updateItemsAnnexes();
+    $this->_emptyRecordsAnnexes();
+    $this->_updateRecordsAnnexes();
+    Class_Notice::createIndexMotsNotice();
+
+    $this->_table = 'catalogue';
+    $this->_field = 'annexe';
+    $this->_separator = ';';
+    $this->_updateModelAnnexes();
+
+    $this->_table = 'album';
+    $this->_field = 'annexes';
+    $this->_separator = ' ';
+    $this->_updateModelAnnexes();
+
+    $this->_table = 'codif_type_doc';
+    $this->_field = 'annexes';
+    $this->_separator = ';';
+    $this->_updateModelAnnexes();
+
+    $this->_table = 'bib_admin_profil';
+    $this->_field = 'sel_annexe';
+    $this->_separator = ';';
+    $this->_updateModelAnnexes();
+
+
+    $this->_adapter
+      ->query('insert into variables (`clef`, `valeur`) values ("CODIFS_MIGRATED", "1");');
+  }
+
+
+  protected function _updateModelAnnexes(): void
+  {
+    $models = $this->_adapter->query(sprintf('SELECT DISTINCT(`%s`) FROM `%s`'
+                                             . ' WHERE `%s` > ""',
+                                             $this->_field,
+                                             $this->_table,
+                                             $this->_field))
+                             ->fetchAll();
+
+    foreach ($models as $model)
+      $this->_mapAnnexesCodeToID(trim($model[$this->_field]));
+  }
+
+
+  protected function _mapAnnexesCodeToID(string $annexes): void
+  {
+    if ( ! $annexes)
+      return;
+
+    $new_annexes = [];
+    foreach (explode($this->_separator, $annexes) as $annexe)
+      $new_annexes[] = $this->_annexes[$annexe] ?? null;
+
+    $this->_adapter->query(sprintf('UPDATE `%s` SET `%s` = "%s" WHERE `%s` = "%s"',
+                                   $this->_table,
+                                   $this->_field,
+                                   implode($this->_separator, array_filter($new_annexes)),
+                                   $this->_field,
+                                   $annexes));
+  }
+
+
+  protected function _updateRecordsAnnexes(): void
+  {
+    foreach ($this->_records_id_annexe as $id_notice => $annexes_ids)
+      if ($annexes_ids = array_filter(array_unique($annexes_ids)))
+        $this->_adapter->query(sprintf("UPDATE notices set `facets` = CONCAT(`facets`, '%s'), `facettes` = CONCAT(`facettes`, '%s') WHERE id_notice = %d;",
+                                       ' F_' . implode(' F_', $annexes_ids),
+                                       ' ' . implode(' ', $annexes_ids),
+                                       $id_notice));
+  }
+
+
+  protected function _updateItemsAnnexes(): void
+  {
+    foreach ($this->_annexes as $code => $id)
+      $this->_replaceFor($code, $id);
+  }
+
+
+  protected function _emptyRecordsAnnexes(): void
+  {
+    $this->_adapter->query("UPDATE notices set `facets` = CLEAN_SPACES(REGEXP_REPLACE(`facets`, '\\\\bF_Y[a-zA-Z0-9]*\\\\b', '')), `facettes` = CLEAN_SPACES(REGEXP_REPLACE(`facettes`, '\\\\bY[a-zA-Z0-9]*\\\\b', ''));");
+
+  }
+
+
+  protected function _replaceFor(string $code, int $id): void
+  {
+    if ( ! $code)
+      return;
+
+    $items = $this->_adapter->query('SELECT id, id_notice FROM exemplaires WHERE `annexe` = \''
+                                    . $code . '\'')->fetchAll();
+    $items_done = array_map(fn($item) => $item['id'],
+                            $items);
+    $records_id = array_map(fn($item) => $item['id_notice'],
+                            $items);
+    foreach ($records_id as $record_id) {
+      $this->_records_id_annexe [$record_id] ??= [];
+      $this->_records_id_annexe [$record_id] [] = 'Y' . $id;
+    }
+
+    $sql = sprintf("UPDATE `exemplaires` SET `annexe` = '%s' WHERE `annexe` = '%s'%s;",
+                   $id,
+                   $code,
+                   $this->_items_done
+                   ? (' AND id NOT IN (' . implode(',', $this->_items_done) . ')')
+                   : '');
+    $this->_adapter->query($sql);
+
+    $this->_items_done = array_merge($this->_items_done, $items_done);
+  }
+}
diff --git a/library/Class/MoteurRecherche/RecordRequest.php b/library/Class/MoteurRecherche/RecordRequest.php
index 88067d8b88d..33e82ce839a 100644
--- a/library/Class/MoteurRecherche/RecordRequest.php
+++ b/library/Class/MoteurRecherche/RecordRequest.php
@@ -63,6 +63,8 @@ class Class_MoteurRecherche_RecordByIdSigbRequest
   protected
     $_id_sigb,
     $_id_site,
+    $_id_bib,
+    $_id_annexe,
     $_record_type,
     $_id_int_bib,
     $_error_message;
@@ -72,6 +74,7 @@ class Class_MoteurRecherche_RecordByIdSigbRequest
     $this->_error_message = null;
     $this->_id_sigb = $request->getParam('id_sigb');
     $this->_id_site = $request->getParam('id_site');
+    $this->_id_bib = $request->getParam('id_bib');
     $this->_id_int_bib = $request->getParam('id_int_bib');
     $this->_record_type = $request->getParam('record_type');
 
@@ -81,11 +84,8 @@ class Class_MoteurRecherche_RecordByIdSigbRequest
     if (!$this->_id_sigb)
       $this->_error_message = $this->_('Paramètre id_sigb obligatoire');
 
-    if (!$this->_id_site && !$this->_id_int_bib)
-      $this->_error_message = $this->_('Paramètre id_site obligatoire');
-
-    if ($this->_id_site && $this->_id_int_bib)
-      $this->_error_message = $this->_('Paramètre id_site et id_int_bib ne peuvent pas être renseignés en même temps');
+    if (!$this->_id_site && !$this->_id_int_bib && !$this->_id_bib)
+      $this->_error_message = $this->_('Paramètre de site obligatoire');
 
     return null !== $this->_error_message;
   }
@@ -103,20 +103,30 @@ class Class_MoteurRecherche_RecordByIdSigbRequest
 
 
   protected function _findItem() : ?Class_Exemplaire {
-    $clauses = array_merge(($this->_id_int_bib
-                            ? ['id_int_bib' => $this->_id_int_bib]
-                            : ['id_int_bib not' => 0]),
-                           ['id_origine' => $this->_id_sigb,
-                            'type' => $this->_record_type]);
-
-    if (!$this->_id_site
-        || static::ALL_SITES === $this->_id_site)
-      return Class_Exemplaire::findFirstBy($clauses);
-
-    return ($annex = Class_CodifAnnexe::findFirstBy(['id_origine' => $this->_id_site]))
-      ? Class_Exemplaire::findFirstBy(array_merge(['id_bib' => $annex->getIdBib()],
-                                                  $clauses))
-      : null;
+    $annex = null;
+    if ( $this->_id_site
+         && $this->_id_site != static::ALL_SITES
+         && (! $annex = Class_CodifAnnexe::find($this->_id_site)))
+      return null;
+
+    $query =  Class_Exemplaire::query()
+      ->eq('id_origine', $this->_id_sigb)
+      ->eq('type', $this->_record_type)
+      ->not_eq('id_int_bib', 0);
+
+    if ( $annex )
+      $query
+        ->eq('annexe', $annex->getId());
+
+    if ( $this->_id_bib )
+      $query
+        ->eq('id_bib', $this->_id_bib);
+
+    if ( $this->_id_int_bib )
+      $query
+        ->eq('id_int_bib', $this->_id_int_bib);
+
+    return $query->fetchFirst();
   }
 }
 
diff --git a/library/Class/Notice/Permalink.php b/library/Class/Notice/Permalink.php
index ebbd28dc67d..7ff2e215f52 100644
--- a/library/Class/Notice/Permalink.php
+++ b/library/Class/Notice/Permalink.php
@@ -95,19 +95,23 @@ class Class_Notice_Permalink {
       return '';
 
     if (!($id_sigb = $first_exemplaire->getIdOrigine())
-        || !($id_int_bib = $first_exemplaire->getIdIntBib()))
+        || !($id_int_bib = $first_exemplaire->getIdIntBib())
+        || !($id_annexe = $first_exemplaire->getAnnexe()))
       return '';
 
-    $type = (Class_Notice::TYPE_BIBLIOGRAPHIC === $record->getType())
-      ? []
-      : ['type' => $record->getType()];
+    $params = ['id_sigb' => $id_sigb];
+    if ( $id_int_bib )
+      $params['id_int_bib'] = $id_int_bib;
+    $params['id_site'] = $id_annexe ?:'*';
+
+    if (Class_Notice::TYPE_BIBLIOGRAPHIC === $record->getType())
+      $params['record_type'] = $record->getType();
 
     return Class_Url::relative(array_merge(['module' => 'opac',
                                             'controller' => 'recherche',
                                             'action' => 'viewnotice',
-                                            'id_sigb' => $id_sigb,
-                                            'id_int_bib' => $id_int_bib],
-                                           $type), null, true);
+                                            ],
+                                           $params), null, true);
   }
 
 
diff --git a/library/Class/ProfilePrefs.php b/library/Class/ProfilePrefs.php
index 1bfd60678b7..5ff3f98faf7 100644
--- a/library/Class/ProfilePrefs.php
+++ b/library/Class/ProfilePrefs.php
@@ -219,6 +219,24 @@ class Class_ProfilePrefs {
   }
 
 
+  public function getSections() : array {
+    return (new Class_ProfilePrefs_Codification_Section)
+      ->multiInputValuesFrom($this->_prefs);
+  }
+
+
+  public function getLocations() : array {
+    return (new Class_ProfilePrefs_Codification_Location)
+      ->multiInputValuesFrom($this->_prefs);
+  }
+
+
+  public function getSorts() : array {
+    return (new Class_ProfilePrefs_Codification_Sort)
+      ->multiInputValuesFrom($this->_prefs);
+  }
+
+
   public function addThumbsZoneField($zone, $field) {
     $this->_prefs = (new Class_ProfilePrefs_Thumbs)
       ->addZoneFieldInto($zone, $field, $this->_prefs);
diff --git a/library/Class/ProfilePrefs/Codification.php b/library/Class/ProfilePrefs/Codification.php
new file mode 100644
index 00000000000..8c6e8e743ce
--- /dev/null
+++ b/library/Class/ProfilePrefs/Codification.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_ProfilePrefs_Codification {
+  use Trait_Translator;
+
+  public function multiInputValuesFrom($prefs) {
+    return $this->extractFrom($prefs[static::PROFILE_NAME] ?? []);
+  }
+
+
+  public function multiInputFieldsDefinition() : array {
+    return [['name' => static::LABEL_KEY,
+             'label' => $this->_('Libellé de la section')],
+            ['name' => static::ZONE_KEY,
+             'label' => $this->_('Zone marc')],
+            ['name' => static::MODE_KEY,
+             'label' => $this->_('Mode'),
+             'type' => 'select',
+             'options' => ['=' => $this->_('Égal'),
+                           '/' => $this->_('Commence par'),
+                           '*' => $this->_('Contient')]],
+            ['name' => static::VALUE_KEY,
+             'label' => $this->_('Valeur(s), séparées par ;')],
+            ['name' => static::SPACE_KEY,
+             'label' => $this->_('Avec espace'),
+             'type' => 'checkbox']];
+  }
+
+
+  public function extractFrom(array $datas) : array {
+    $labels = $datas[static::LABEL_KEY] ?? [];
+    $zones = $datas[static::ZONE_KEY] ?? [];
+    $modes = $datas[static::MODE_KEY] ?? [];
+    $values = $datas[static::VALUE_KEY] ?? [];
+    $spaces = $datas[static::SPACE_KEY] ?? [];
+
+    $empties = [];
+    foreach ($labels as $k => $label)
+      if ('' === $label || '' === ($zones[$k] ?? '') || '' === ($values[$k] ?? ''))
+        $empties[] = $k;
+
+    foreach($empties as $k)
+      unset($labels[$k], $zones[$k], $modes[$k], $values[$k], $spaces[$k]);
+
+    return [static::LABEL_KEY => array_values($labels),
+            static::ZONE_KEY => array_values($zones),
+            static::MODE_KEY => array_values($modes),
+            static::VALUE_KEY => array_values($values),
+            static::SPACE_KEY => array_values($spaces)];
+  }
+}
diff --git a/library/Class/ProfilePrefs/Codification/Location.php b/library/Class/ProfilePrefs/Codification/Location.php
new file mode 100644
index 00000000000..167dd8f82ce
--- /dev/null
+++ b/library/Class/ProfilePrefs/Codification/Location.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_ProfilePrefs_Codification_Location extends Class_ProfilePrefs_Codification {
+
+  const
+    PROFILE_NAME = 'locations',
+    LABEL_KEY = 'locations_label',
+    ZONE_KEY = 'locations_zone',
+    MODE_KEY = 'locations_mode',
+    VALUE_KEY = 'locations_value',
+    SPACE_KEY = 'locations_whitespace';
+}
diff --git a/library/Class/ProfilePrefs/Codification/Section.php b/library/Class/ProfilePrefs/Codification/Section.php
new file mode 100644
index 00000000000..5c16795a363
--- /dev/null
+++ b/library/Class/ProfilePrefs/Codification/Section.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_ProfilePrefs_Codification_Section extends Class_ProfilePrefs_Codification {
+
+  const
+    PROFILE_NAME = 'sections',
+    LABEL_KEY = 'sections_label',
+    ZONE_KEY = 'sections_zone',
+    MODE_KEY = 'sections_mode',
+    VALUE_KEY = 'sections_value',
+    SPACE_KEY = 'sections_whitespace',
+    SKIP_KEY = 'sections_skipitems';
+}
diff --git a/library/Class/ProfilePrefs/Codification/Sort.php b/library/Class/ProfilePrefs/Codification/Sort.php
new file mode 100644
index 00000000000..ca6a99aee9d
--- /dev/null
+++ b/library/Class/ProfilePrefs/Codification/Sort.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_ProfilePrefs_Codification_Sort extends Class_ProfilePrefs_Codification {
+
+  const
+    PROFILE_NAME = 'sorts',
+    LABEL_KEY = 'sorts_label',
+    ZONE_KEY = 'sorts_zone',
+    MODE_KEY = 'sorts_mode',
+    VALUE_KEY = 'sorts_value',
+    SPACE_KEY = 'sorts_whitespace';
+}
diff --git a/library/Class/ProfileSerializer/UnimarcRecord.php b/library/Class/ProfileSerializer/UnimarcRecord.php
index 97f63bdb39d..809be57193e 100644
--- a/library/Class/ProfileSerializer/UnimarcRecord.php
+++ b/library/Class/ProfileSerializer/UnimarcRecord.php
@@ -32,6 +32,9 @@ class Class_ProfileSerializer_UnimarcRecord extends Class_ProfileSerializer_Abst
        6 => $this->_extractInterests(),
        7 => $this->_extractFileIndexation(),
        8 => $this->_extractThumbs(),
+       Class_ProfilePrefs_Codification_Section::PROFILE_NAME => $this->_extractSections(),
+       Class_ProfilePrefs_Codification_Location::PROFILE_NAME => $this->_extractLocations(),
+       Class_ProfilePrefs_Codification_Sort::PROFILE_NAME => $this->_extractSorts(),
        Class_IntProfilDonnees::PROFILE_RECORD_IDENTIFIERS => $this->_extractIdentifiers(),
        Class_IntProfilDonnees::PROFILE_RECORD_AUTHORITY_IDENTIFIERS => $this->_extractAuthorityIdentifiers(),
     ];
@@ -66,6 +69,9 @@ class Class_ProfileSerializer_UnimarcRecord extends Class_ProfileSerializer_Abst
       ->populateInterests()
       ->populateFileIndexation()
       ->populateThumbs()
+      ->populateSections()
+      ->populateLocations()
+      ->populateSorts()
       ->populateIdentifiers()
       ->populateAuthorityIdentifiers();
   }
@@ -177,4 +183,19 @@ class Class_ProfileSerializer_UnimarcRecord extends Class_ProfileSerializer_Abst
 
     return implode(';', $this->_datas['nouveaute_valeurs']);
   }
+
+
+  protected function _extractSections() {
+    return (new Class_ProfilePrefs_Codification_Section)->extractFrom($this->_datas);
+  }
+
+
+  protected function _extractLocations() {
+    return (new Class_ProfilePrefs_Codification_Location)->extractFrom($this->_datas);
+  }
+
+
+  protected function _extractSorts() {
+    return (new Class_ProfilePrefs_Codification_Sort)->extractFrom($this->_datas);
+  }
 }
diff --git a/library/Class/Users.php b/library/Class/Users.php
index bdc0fdc9d83..46a4cc9d3e2 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -1487,9 +1487,10 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  public function shouldUseUniquePatron() :bool {
+  public function shouldUseUniquePatron(Class_Exemplaire $item) :bool {
     return (($central_library_id = Class_CosmoVar::get('centralized_hold_mode'))
             && ($this->getIdIntBib() != $central_library_id)
+            && ($item->getIdIntBib() == $central_library_id)
             && ($cardnumber = $this->getUseCardNumber()));
   }
 
@@ -2261,4 +2262,12 @@ class Class_Users extends Storm_Model_Abstract {
 
     return $emprunteur->getExternalAjaxHistoryLoansHelperClassname();
   }
+
+
+  public function getUrlServer(): string
+  {
+    return ($int_bib = $this->getIntBib())
+      ? $int_bib->getUrlServer()
+      : '';
+  }
 }
diff --git a/library/Class/WebService/SIGB/AbstractRESTService.php b/library/Class/WebService/SIGB/AbstractRESTService.php
index d5bd41f0235..64a699d633b 100644
--- a/library/Class/WebService/SIGB/AbstractRESTService.php
+++ b/library/Class/WebService/SIGB/AbstractRESTService.php
@@ -28,6 +28,7 @@ abstract class Class_WebService_SIGB_AbstractRESTService
   protected $_server_root;
   protected $_web_client;
   protected $_has_network_error = false;
+  protected int $_id_int_bib = 0;
 
   /**
    * @param string $server_root
@@ -48,6 +49,17 @@ abstract class Class_WebService_SIGB_AbstractRESTService
   }
 
 
+  public function setIdIntBib(int $id) : self {
+    $this->_id_int_bib = $id;
+    return $this;
+  }
+
+
+  public function getIdIntBib() : int {
+    return $this->_id_int_bib;
+  }
+
+
   /**
    * @param string $server_root
    * @return Class_WebService_SIGB_AbstractRESTService
@@ -234,9 +246,11 @@ abstract class Class_WebService_SIGB_AbstractRESTService
   }
 
 
-  protected function _getTagData(?string $xml, string $tag) : string{
+  protected function _getTagData(?string $xml, string $tag): string
+  {
     if ( ! $xml)
       return '';
+
     $matches = [];
     $string_to_search = sprintf('/%s>([^<]*)<\/%s/', $tag, $tag);
     if (preg_match($string_to_search,
@@ -315,9 +329,11 @@ abstract class Class_WebService_SIGB_AbstractRESTService
     if ($this->_getTagData($xml, $error_tag))
       return null;
 
-    return $reader
-      ->setEmprunteur(Class_WebService_SIGB_Emprunteur::nullInstance())
+    $reader
+      ->setEmprunteur(Class_WebService_SIGB_Emprunteur::nullInstance()->setService($this))
       ->parseXML($xml);
+
+    return $reader;
   }
 
 
diff --git a/library/Class/WebService/SIGB/AbstractService.php b/library/Class/WebService/SIGB/AbstractService.php
index 4b09b4ab94c..18549ea6db5 100644
--- a/library/Class/WebService/SIGB/AbstractService.php
+++ b/library/Class/WebService/SIGB/AbstractService.php
@@ -144,7 +144,8 @@ abstract class Class_WebService_SIGB_AbstractService {
   }
 
 
-  public function providesPickupLocations() {
+  public function providesPickupLocations(): bool
+  {
     return false;
   }
 
@@ -248,7 +249,8 @@ abstract class Class_WebService_SIGB_AbstractService {
 
 
   public function setDefaultLibraryToEmprunteur($emprunteur, int $library_id) : self{
-    $emprunteur->setIdIntBib($library_id);
+    if ( ! $emprunteur->isNullInstance() && ! $emprunteur->getIdIntBib())
+      $emprunteur->setIdIntBib($library_id);
     return $this;
   }
 
@@ -389,4 +391,12 @@ abstract class Class_WebService_SIGB_AbstractService {
   abstract public function prolongerPret($user, $pret_id);
 
   abstract public function getNotice($id);
+
+
+  public function codifAnnexesForUrlServer(): array
+  {
+    $int_bibs = (new Class_IntBib_ForUrl)->intBibsForUrlServer($this->getServerRoot());
+
+    return Class_CodifAnnexe::findAllByIdBibs($int_bibs);
+  }
 }
diff --git a/library/Class/WebService/SIGB/Decalog.php b/library/Class/WebService/SIGB/Decalog.php
index 1a1c7e3af25..db32668e672 100644
--- a/library/Class/WebService/SIGB/Decalog.php
+++ b/library/Class/WebService/SIGB/Decalog.php
@@ -28,7 +28,8 @@ class Class_WebService_SIGB_Decalog extends Class_WebService_SIGB_Abstract {
       self::$service = Class_WebService_SIGB_Decalog_Service::getService($params['url_serveur'],
                                                                          $params['key'],
                                                                          $params['institution_code'],
-                                                                         $params['record_id_prefix']);
+                                                                         $params['record_id_prefix'])
+        ->initializeUseCardNumber($params);
     }
 
     return self::$service;
diff --git a/library/Class/WebService/SIGB/Decalog/BookableResponseReader.php b/library/Class/WebService/SIGB/Decalog/BookableResponseReader.php
index 40c92253514..cf59fdb0935 100644
--- a/library/Class/WebService/SIGB/Decalog/BookableResponseReader.php
+++ b/library/Class/WebService/SIGB/Decalog/BookableResponseReader.php
@@ -39,7 +39,7 @@ class Class_WebService_SIGB_Decalog_BookableResponseReader
 
 
   protected function _isBookable() : bool {
-    return $this->_data['isBookable'];
+    return (bool) $this->_data['isBookable'] ?? false;
   }
 
 
diff --git a/library/Class/WebService/SIGB/Emprunteur.php b/library/Class/WebService/SIGB/Emprunteur.php
index 148ae30147b..33910f277ee 100644
--- a/library/Class/WebService/SIGB/Emprunteur.php
+++ b/library/Class/WebService/SIGB/Emprunteur.php
@@ -43,7 +43,7 @@ class Class_WebService_SIGB_Emprunteur
   protected string $_mobile = '';
   protected string $_date_naissance = '';
   protected string $_library_code = '';
-  protected string $_id_int_bib = '';
+  protected int $_id_int_bib = 0;
   protected string $_public_comment = '';
   protected string $_favorite_sending_channel = '';
 
@@ -55,8 +55,8 @@ class Class_WebService_SIGB_Emprunteur
   protected array $_sending_channels = [];
 
   protected int $_nb_reservations;
-  protected int $_nb_emprunts;
-  protected int $_nb_retards;
+  protected ?int $_nb_emprunts;
+  protected ?int $_nb_retards;
   protected int $_multimedia_access = -1;
   protected int $_parental_authorization = -1;
 
@@ -729,23 +729,47 @@ class Class_WebService_SIGB_Emprunteur
   }
 
 
-  public function setLibraryCode(string $library_code): self
+  public function getService() : ?Class_WebService_SIGB_AbstractService {
+    return  ($this->_service instanceOf Class_Webservice_SIGB_AbstractService)
+      ? $this->_service
+      : null;
+  }
+
+
+  public function getIdIntBibByService() : int {
+    return $this->_service
+      ? $this->_service->getIdIntBib()
+      : 0;
+  }
+
+
+  public function setLibraryCode(string $library_code) : self
   {
     $this->_library_code = $library_code;
     return $this;
   }
 
 
-  public function getLibrary(): Class_CodifAnnexe
-  {
-    return ($library = Class_CodifAnnexe::findFirstBy(['id_origine' => $this->_library_code]))
-      ? $library
+  public function getLibrary() : Class_CodifAnnexe {
+    $url_service = ($service = $this->getService())
+      ? $service->getServerRoot()
+      : '';
+    return ($branch = Class_CodifAnnexe::findByCodeAndBib($this->_library_code,
+                                                          $this->_id_int_bib,
+                                                          $url_service))
+      ? $branch
       : Class_CodifAnnexe::newInstance();
   }
 
 
-  public function getLibraryCode()
-  {
+  public function getLibraryIntBibId() : int {
+    return ($library = $this->getLibrary())
+      ? $library->getIntBibId()
+      : 0;
+  }
+
+
+  public function getLibraryCode() {
     return $this->_library_code;
   }
 
@@ -908,8 +932,9 @@ class Class_WebService_SIGB_Emprunteur
         && ($bib = $annexe->getBib()))
       $user->setBib($bib);
 
-    if ($id_int_bib = $this->getIdIntBib())
-      $user->setIntBib(Class_IntBib::find($id_int_bib));
+    // Has to be processed after Bib
+    if ($int_bib = $this->_checkForIntBibId($user))
+      $user->setIdIntBib($int_bib);
 
     if ( Class_Users::UNDEFINED_DATA !== $this->_multimedia_access )
       $user->setMultimediaAccess($this->_multimedia_access);
@@ -921,6 +946,17 @@ class Class_WebService_SIGB_Emprunteur
   }
 
 
+  protected function _checkForIntBibId(Class_Users $user): ?int
+  {
+    if (($id_bib = $user->getIdSite())
+        && ($int_bib = Class_IntBib::find($id_bib))
+        && $int_bib->getUrlServer())
+      return $id_bib;
+
+    return $this->getIdIntBib() ?: null;
+  }
+
+
   public function updateFromUser(Class_Users $user): self
   {
     $this
@@ -938,7 +974,6 @@ class Class_WebService_SIGB_Emprunteur
       ->setIsContactSms($user->getIsContactSms() ?? false)
       ->setIsContactEmail($user->getIsContactMail() ?? false);
 
-
     return $this;
   }
 
diff --git a/library/Class/WebService/SIGB/Koha/CommunityService.php b/library/Class/WebService/SIGB/Koha/CommunityService.php
index b3ee5f1fc6b..f94ed1d1a97 100644
--- a/library/Class/WebService/SIGB/Koha/CommunityService.php
+++ b/library/Class/WebService/SIGB/Koha/CommunityService.php
@@ -273,7 +273,8 @@ class Class_WebService_SIGB_Koha_CommunityService
   }
 
 
-  public function providesPickupLocations() {
+  public function providesPickupLocations(): bool
+  {
     return true;
   }
 
diff --git a/library/Class/WebService/SIGB/Koha/PatronInfoReader.php b/library/Class/WebService/SIGB/Koha/PatronInfoReader.php
index 32fdd947a3b..591549b5f54 100644
--- a/library/Class/WebService/SIGB/Koha/PatronInfoReader.php
+++ b/library/Class/WebService/SIGB/Koha/PatronInfoReader.php
@@ -98,6 +98,9 @@ class Class_WebService_SIGB_Koha_PatronInfoReader
   public function endHoldingbranch($data) {
     if ($this->_currentLoan)
       $this->updateBibliothequeWith($data);
+
+    if ($this->_currentHold)
+      $this->updateBibliothequeWith($data);
   }
 
 
@@ -168,23 +171,24 @@ class Class_WebService_SIGB_Koha_PatronInfoReader
   }
 
 
-  public function updateBibliothequeWith($data) {
-    if(!$site = $this->findSite($data))
-      return;
+  public function updateBibliothequeWith(string $branch_code) : self {
+    if (! $branch = Class_CodifAnnexe::findByCodeAndBib(trim($branch_code),
+                                                        $this->_getIdIntBib()))
+      return $this;
 
-    $this->_current_operation->getExemplaire()->setBibliotheque($site->getLibelle());
+    $this->_current_operation
+      ->getExemplaire()
+      ->setBibliotheque($branch->getLibelle());
 
     if ($this->_currentHold)
-      $this->_currentHold->setPickupLocationLabel($site->getLibelle())
-                         ->setLocationId($site->getLibraryId());
+      $this->_currentHold->setPickupLocationLabel($branch->getLibelle())
+                         ->setLocationId($branch->getBibId());
+    return $this;
   }
 
 
-  protected function findSite($code) {
-    $code = trim($code);
-    if (!$code || !($site = Class_CodifAnnexe::findFirstBy(['id_origine' => $code])))
-      return null;
-    return $site;
+  protected function _getIdIntBib() : int {
+    return $this->_emprunteur ? $this->_emprunteur->getIdIntBibByService() : 0;
   }
 
 
diff --git a/library/Class/WebService/SIGB/Koha/Service.php b/library/Class/WebService/SIGB/Koha/Service.php
index 2cbbc4bed90..c800dbc500d 100644
--- a/library/Class/WebService/SIGB/Koha/Service.php
+++ b/library/Class/WebService/SIGB/Koha/Service.php
@@ -39,6 +39,7 @@ class Class_WebService_SIGB_Koha_Service extends Class_WebService_SIGB_AbstractR
   public static function getService($params) {
     return static::newInstance()
       ->setServerRoot($params['url_serveur'])
+      ->setIdIntBib((int) ($params['id_bib'] ?? 0))
       ->setInterdireResaDocDispo($params['Interdire_reservation_doc_dispo']==='1')
       ->setApiUser($params['api_user'] ?? '')
       ->setApiPass($params['api_pass'] ?? '')
@@ -342,6 +343,7 @@ class Class_WebService_SIGB_Koha_Service extends Class_WebService_SIGB_AbstractR
   public function reserverExemplaire($user, $exemplaire, $code_annexe) {
     if ($exemplaire->requiresCalendarHold())
       return $this->_error('La durée de réservation est requise');
+
     if (1 == Class_AdminVar::get('KOHA_MULTI_SITES'))
       return $this->holdItem($user, $exemplaire, $code_annexe);
 
@@ -409,8 +411,9 @@ class Class_WebService_SIGB_Koha_Service extends Class_WebService_SIGB_AbstractR
 
 
   protected function _setPickupLocation($args, $code) {
-    if (trim($code) && $annexe = Class_CodifAnnexe::findByCode($code))
+    if (trim($code) && $annexe = Class_CodifAnnexe::findByCodeAndBib($code, (int)$args['bib_id'] ?? 0))
       $args['pickup_location'] = $annexe->getIdOrigine();
+
     return $args;
   }
 
diff --git a/library/Class/WebService/SIGB/KohaLegacy/Service.php b/library/Class/WebService/SIGB/KohaLegacy/Service.php
index af710d6d3fb..4db3e2a80eb 100644
--- a/library/Class/WebService/SIGB/KohaLegacy/Service.php
+++ b/library/Class/WebService/SIGB/KohaLegacy/Service.php
@@ -38,6 +38,7 @@ class Class_WebService_SIGB_KohaLegacy_Service extends Class_WebService_SIGB_Koh
   public static function getService($params) {
     return static::newInstance()
       ->setServerRoot($params['url_serveur'])
+      ->setIdIntBib((int) ($params['id_bib'] ?? 0))
       ->setInterdireResaDocDispo($params['Interdire_reservation_doc_dispo']==='1')
       ->setRestful($params['restful']==='1')
       ->setPreRegistration($params['pre-registration'] === '1')
@@ -47,11 +48,6 @@ class Class_WebService_SIGB_KohaLegacy_Service extends Class_WebService_SIGB_Koh
   }
 
 
-  protected function _getPatronReader(){
-    return Class_WebService_SIGB_Koha_PatronInfoReader::newInstance();
-  }
-
-
   public function holdsForItem($item) {
     if (!$this->restful) {
       $response = $this->_error($this->_('Koha Restful désactivé'));
@@ -102,6 +98,12 @@ class Class_WebService_SIGB_KohaLegacy_Service extends Class_WebService_SIGB_Koh
   }
 
 
+  public function providesItemInformationService() : bool
+  {
+    return false;
+  }
+
+
   public function suggestionsOf(Class_Users $user): array
   {
     if (!$this->providesSuggestions())
diff --git a/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php b/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php
index 7a13820c2ae..4d3092fd338 100644
--- a/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php
+++ b/library/Class/WebService/SIGB/Nanook/PatronInfoReader.php
@@ -332,8 +332,10 @@ class Class_WebService_SIGB_Nanook_PatronInfoReader
 
 
   public function endSite($data) {
-    $library = Class_Bib::find($data);
-    $this->_current_suggest->setLibrary($library ? $library->getLibelle() : $data);
+    $libelle = ($annexe = Class_CodifAnnexe::findByCodeAndCurrentUser($data))
+      ? $annexe->getLibraryLabel()
+      : $data;
+    $this->_current_suggest->setLibrary($libelle);
   }
 
 
diff --git a/library/Class/WebService/SIGB/Nanook/PreRegistration.php b/library/Class/WebService/SIGB/Nanook/PreRegistration.php
index 22a21aec844..a2ebf44e12b 100644
--- a/library/Class/WebService/SIGB/Nanook/PreRegistration.php
+++ b/library/Class/WebService/SIGB/Nanook/PreRegistration.php
@@ -34,8 +34,12 @@ class Class_WebService_SIGB_Nanook_Preregistration extends Class_WebService_SIGB
 
   public function send(Class_WebService_SIGB_PreRegistration_AbstractData $data) : self {
     $id = $data->getBranchId();
+    if ( ! $branch = Class_CodifAnnexe::find($id)) {
+      $this->getLogger()->log($this->_('Échec de la préinscription, la médiathèque sélectionnée n\'existe pas.'));
+      return $this;
+    }
 
-    if(!$int_bib = Class_IntBib::find($id)) {
+    if(!$int_bib = ($branch->getIntBib())) {
       $this->getLogger()->log($this->_('Échec de la préinscription, la médiathèque sélectionnée n\'existe pas.'));
       return $this;
     }
@@ -47,7 +51,7 @@ class Class_WebService_SIGB_Nanook_Preregistration extends Class_WebService_SIGB
       return $this;
     }
 
-    return $this->_updateUser($id, $response);
+  return $this->_updateUser($int_bib->getId(), $response);
   }
 
 
diff --git a/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php b/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php
index 5f047ec3024..5c0b52e8232 100644
--- a/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php
+++ b/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php
@@ -45,4 +45,9 @@ class Class_WebService_SIGB_Nanook_Preregistration_Data extends Class_WebService
   public function getEmail() : string {
     return $this->_data_as_array['mail'] ?? '';
   }
+
+
+  public function getLibrary() : ?Class_Bib {
+    return Class_CodifAnnexe::getLibrary($this->getBranchId());
+  }
 }
diff --git a/library/Class/WebService/SIGB/Nanook/Service.php b/library/Class/WebService/SIGB/Nanook/Service.php
index 85e3bb346b9..8dd45f7aac8 100644
--- a/library/Class/WebService/SIGB/Nanook/Service.php
+++ b/library/Class/WebService/SIGB/Nanook/Service.php
@@ -196,6 +196,9 @@ class Class_Webservice_SIGB_Nanook_Service
     if ($user->getPassword())
       $emprunteur->setPassword($user->getPassword());
 
+    if ($emprunteur->getLibraryCode() && $id = $emprunteur->getLibraryIntBibId())
+      $emprunteur->setIdIntBib($id);
+
     return $emprunteur;
   }
 
@@ -362,10 +365,9 @@ class Class_Webservice_SIGB_Nanook_Service
    */
   public function reserverExemplaire($user, $exemplaire, $code_bib_or_annexe) {
     $code_annexe = $code_bib_or_annexe;
-    if (
-        ($annexe = Class_CodifAnnexe::findFirstBy(['id_origine' => $code_bib_or_annexe]))
-        ||
-        ($annexe = Class_CodifAnnexe::findFirstBy(['id_bib' => $code_bib_or_annexe])))
+    if (($annexe = Class_CodifAnnexe::findByCodeAndBib($code_bib_or_annexe,
+                                                       $exemplaire->getIdIntBib()))
+        || ($annexe = Class_CodifAnnexe::findFirstBy(['id_bib' => $code_bib_or_annexe])))
       $code_annexe = $annexe->getIdOrigine();
 
     return $this->ilsdiHoldTitle(['bibId' => $exemplaire->getIdOrigine(),
diff --git a/library/Class/WebService/SIGB/Opsys/Service.php b/library/Class/WebService/SIGB/Opsys/Service.php
index 40399cca913..98da4d49140 100644
--- a/library/Class/WebService/SIGB/Opsys/Service.php
+++ b/library/Class/WebService/SIGB/Opsys/Service.php
@@ -357,7 +357,9 @@ class Class_WebService_SIGB_Opsys_Service extends Class_WebService_SIGB_Abstract
 
 
   public function getServerRoot() {
-    return $this->search_client->getWsdlUrl();
+    return $this->search_client
+      ? $this->search_client->getWsdlUrl()
+      : '';
   }
 
 
diff --git a/library/Class/WebService/SIGB/Orphee/Emprunteur.php b/library/Class/WebService/SIGB/Orphee/Emprunteur.php
index aea2da22fcf..60e1cd63038 100644
--- a/library/Class/WebService/SIGB/Orphee/Emprunteur.php
+++ b/library/Class/WebService/SIGB/Orphee/Emprunteur.php
@@ -113,7 +113,7 @@ class Class_WebService_SIGB_Orphee_Emprunteur
                                                                  ]);
       return $this->_nb_retards;
     }
-    return count(array_filter( $this->getLoans(), fn( $loan) => $loan->enRetard()));
+    return 0;
   }
 
 
@@ -137,6 +137,11 @@ class Class_WebService_SIGB_Orphee_Emprunteur
   }
 
 
+  public function nbRetards() :int {
+    return $this->_nb_retards ?? 0;
+  }
+
+
   public function ensureService(Class_Users $user) :Class_WebService_SIGB_Emprunteur {
     parent::ensureService($user);
     if ($this->_service)
diff --git a/library/Class/WebService/SIGB/Orphee/GetInfoUserCarteResponseReader.php b/library/Class/WebService/SIGB/Orphee/GetInfoUserCarteResponseReader.php
index ff14f836cee..017094e1389 100644
--- a/library/Class/WebService/SIGB/Orphee/GetInfoUserCarteResponseReader.php
+++ b/library/Class/WebService/SIGB/Orphee/GetInfoUserCarteResponseReader.php
@@ -162,15 +162,6 @@ class Class_WebService_SIGB_Orphee_GetInfoUserCarteResponseReader {
   }
 
 
-  public function endAnx(string $data) : void {
-    $this->_emprunteur->setLibraryCode(trim($data));
-
-    if (!$annexe = Class_CodifAnnexe::findByCode(trim($data)))
-      return;
-    $this->_emprunteur->setIdIntBib($annexe->getIdBib());
-  }
-
-
   public function endIsDepot(string $data) :void {
     $this->_emprunteur->setIsWarehouse((bool)trim($data));
   }
@@ -180,4 +171,9 @@ class Class_WebService_SIGB_Orphee_GetInfoUserCarteResponseReader {
     $this->_emprunteur->setWarehouseId(trim($data));
     $this->_emprunteur->setIsWarehouse(true);
   }
+
+
+  public function endAnx(string $data) : void {
+    $this->_emprunteur->setLibraryCode(trim($data));
+  }
 }
diff --git a/library/Class/WebService/SIGB/Orphee/Service.php b/library/Class/WebService/SIGB/Orphee/Service.php
index 60310ef514a..94f836e2f1e 100644
--- a/library/Class/WebService/SIGB/Orphee/Service.php
+++ b/library/Class/WebService/SIGB/Orphee/Service.php
@@ -228,15 +228,13 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
   public function providesChangePasswordService() :bool {
     return
       $this->getSearchClient()->hasFunction('SetPwdAdh')
-      && Class_AdminVar::isLoginThroughSigbOnlyEnabled()
       && !$this->getUseCardNumber();
   }
 
 
   public function providesAuthentication() :bool {
     return $this->_identification_provider
-      && $this->hasGetAdh()
-      && Class_AdminVar::isLoginThroughSigbOnlyEnabled();
+      && $this->hasGetAdh();
   }
 
 
@@ -272,7 +270,8 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
   }
 
 
-  public function getUniquePatron(string $cardnumber) : Class_WebService_SIGB_Emprunteur {
+  public function getUniquePatron(string $cardnumber): Class_WebService_SIGB_Emprunteur
+  {
     $result = $this->_search_client->GetAdh(GetAdh::WithNo($cardnumber));
     if (!$emprunteur = Class_WebService_SIGB_Orphee_GetInfoUserCarteResponseReaderAsUniquePatron
         ::getInstanceWith($cardnumber)
@@ -287,25 +286,29 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
   }
 
 
-  public function setDefaultLibraryToEmprunteur($emprunteur, int $library_id) : self{
+  public function setDefaultLibraryToEmprunteur($emprunteur, int $library_id): self
+  {
     if (!$emprunteur->getIdIntBib())
       $emprunteur->setIdIntBib($library_id);
     return $this;
   }
 
 
-  public function getUserAnnexe($user) {
+  public function getUserAnnexe($user)
+  {
     return null;
   }
 
 
-  protected function hasGetAdh() {
+  protected function hasGetAdh()
+  {
     return $this->getSearchClient()->hasFunction('GetAdh');
   }
 
 
-  public function newNullEmprunteur() {
-    return Class_WebService_SIGB_Emprunteur::newInstance();
+  public function newNullEmprunteur()
+  {
+    return Class_WebService_SIGB_Emprunteur::nullInstance();
   }
 
 
@@ -550,7 +553,9 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
       $params['count'] = 1;
 
     $this->getEmpruntsOf($emprunteur, $params);
-    return $emprunteur->nbEmprunts();
+    return ('late' === ($params['filter'] ?? ''))
+      ? $emprunteur->nbRetards()
+      : $emprunteur->nbEmprunts();
   }
 
 
@@ -581,7 +586,7 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
     } catch (SoapFault $e) {
       $this->_logError($this->_wsdl, $e);
       return ['statut' => false,
-              'erreur' => 'Le SIGB Orphée a retourné l\'erreur suivante: '.$e->getMessage()];
+              'erreur' => $this->_("Le SIGB Orphée a retourné l'erreur suivante: %s",$e->getMessage())];
     }
 
     if ($message = $errorClosure($result)) {
@@ -605,7 +610,7 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
                      $datas = simplexml_load_string($result->getXml());
                      return ($datas->msg->code == 1) ? false : (string)$datas->msg->libelle;
                    },
-                   fn($user) => ($user->shouldUseUniquePatron())
+                   fn($user) => $user->shouldUseUniquePatron($exemplaire)
                      ? $this->getUniquePatron($user->getUseCardNumber())
                      : $this->getEmprunteur($user)
       );
@@ -643,7 +648,10 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
                              $user, $notice_id,
                              fn($id, $emprunteur) => $this->getSearchClient()
                                          ->DelRsv(DelRsv::withNoticeUserNo($id, $emprunteur->getId())),
-                             fn($result) => ($result->DelRsvResult == 1) ? false : 'La suppression a échoué');
+                             fn($result) => ($result->DelRsvResult == 1)
+                             ? false
+                             : $this->_('La suppression a échoué')
+    );
   }
 
 
diff --git a/library/Class/WebService/SIGB/PreRegistration/AbstractData.php b/library/Class/WebService/SIGB/PreRegistration/AbstractData.php
index 7b978d5c6ce..d2ef2f9b893 100644
--- a/library/Class/WebService/SIGB/PreRegistration/AbstractData.php
+++ b/library/Class/WebService/SIGB/PreRegistration/AbstractData.php
@@ -85,9 +85,7 @@ abstract class Class_WebService_SIGB_PreRegistration_AbstractData {
 
 
   public function getLibraryId() : int {
-    return ($annexe = Class_CodifAnnexe::findFirstBy(['code' => $this->_getBranchCode()]))
-      ? $annexe->getIdBib()
-      : 0;
+    return ($library = $this->getLibrary()) ? $library->getId() : 0;
   }
 
 
diff --git a/library/ZendAfi/Auth/Adapter/CommSigb.php b/library/ZendAfi/Auth/Adapter/CommSigb.php
index 198b7b4ea9d..08197414b64 100644
--- a/library/ZendAfi/Auth/Adapter/CommSigb.php
+++ b/library/ZendAfi/Auth/Adapter/CommSigb.php
@@ -167,6 +167,7 @@ class ZendAfi_Auth_Adapter_CommSigb extends ZendAfi_Auth_Adapter_Abstract {
 
     if (!$loaner = $service->getEmprunteur($user))
       return null;
+
     $this->_called_services->append($service);
 
     if (!$loaner->isValid())
diff --git a/library/ZendAfi/Controller/Action/Helper/NotifyError.php b/library/ZendAfi/Controller/Action/Helper/NotifyError.php
new file mode 100644
index 00000000000..fc65052cd2a
--- /dev/null
+++ b/library/ZendAfi/Controller/Action/Helper/NotifyError.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 2012, 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_NotifyError extends ZendAfi_Controller_Action_Helper_Notify
+{
+
+
+  protected $_options = ['status' => 'error'];
+
+
+  public function notifyError($message, $options=[]) {
+    $this->_controller
+      ->getHelper('flashMessenger')
+      ->addNotification($message, array_merge($this->_options, $options));
+  }
+}
diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Emplacement.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Emplacement.php
index 724cf40b073..2c99f750ff8 100644
--- a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Emplacement.php
+++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Emplacement.php
@@ -28,7 +28,8 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Emplacement
       ['model' => ['class' => 'Class_CodifEmplacement',
                    'name' => 'emplacement',
                    'order' => 'libelle',
-                   'model_id' => 'id_emplacement'],
+                   'model_id' => 'id_emplacement',
+                   'filter' => [Storm_Query_Clause::greater('regles', '')]],
 
        'listViewMode' => ['helper_method' => 'ListViewMode_Codification_Flat',
                           'label' => $this->_('Emplacements'),
diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Genre.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Genre.php
index 9c13c55a71c..8e331bedbaf 100644
--- a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Genre.php
+++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Genre.php
@@ -28,7 +28,8 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Genre
       ['model' => ['class' => 'Class_CodifGenre',
                    'name' => 'genre',
                    'order' => 'libelle',
-                   'model_id' => 'id_genre'],
+                   'model_id' => 'id_genre',
+                   'filter' => [Storm_Query_Clause::greater('regles', '')]],
 
        'listViewMode' => ['helper_method' => 'ListViewMode_Codification_Flat',
                           'label' => $this->_('Genres'),
diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Section.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Section.php
index 4077e4055a9..414964b81e5 100644
--- a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Section.php
+++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Section.php
@@ -27,7 +27,8 @@ class ZendAfi_Controller_Plugin_ResourceDefinition_Section
     return ['model' => ['class' => 'Class_CodifSection',
                         'name' => 'section',
                         'order' => 'libelle',
-                        'model_id' => 'id_section'],
+                        'model_id' => 'id_section',
+                        'filter' => [Storm_Query_Clause::greater('regles', '')]],
 
             'listViewMode' => ['helper_method' => 'ListViewMode_Codification_Flat',
                                'label' => $this->_('Sections'),
diff --git a/library/ZendAfi/Form/Cosmo/CodifsImporter.php b/library/ZendAfi/Form/Cosmo/CodifsImporter.php
new file mode 100644
index 00000000000..a7442e845bc
--- /dev/null
+++ b/library/ZendAfi/Form/Cosmo/CodifsImporter.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_Form_Cosmo_CodifsImporter extends ZendAfi_Form {
+
+  protected $_target;
+
+  public function __construct(int $target) {
+    $this->_target = $target;
+    parent::__construct();
+  }
+
+
+  public function init() {
+    parent::init();
+
+    Class_ScriptLoader::getInstance()
+      ->addAdminScripts(['global'])
+      ->addJqueryReady('formSelectToggleVisibilityForElement("#from", $("#profile").closest("tr"), ["2"]);');
+
+    $this->addElement('select',
+                      'from',
+                      ['label' => $this->_('Depuis'),
+                       'multiOptions' => ['1' => $this->_('Les règles globales'),
+                                          '2' => $this->_('Un profil de données')]]);
+
+    $this->addElement('select', 'profile',
+                      ['label' => $this->_('Profil de données'),
+                       'multiOptions' => $this->_getProfilesCandidates()]);
+
+    $this->addElement('multiCheckboxDropdown',
+                      'codif_types',
+                      ['label' => $this->_('Les codifications suivantes'),
+                       'multiOptions' => ['sorts' => $this->_('Genre'),
+                                          'sections' => $this->_('Section'),
+                                          'locations' => $this->_('Emplacement')]]);
+
+    $this->addUniqDisplayGroup('alone');
+  }
+
+
+  protected function _getProfilesCandidates() : array {
+    $profiles = [];
+
+    $profiles_in_db = Class_IntProfilDonnees::query()
+      ->select(['id_profil', 'libelle'])
+      ->not_eq('id_profil', $this->_target)
+      ->eq('type_fichier', Class_IntProfilDonnees::FT_RECORDS)
+      ->in('format', [Class_IntProfilDonnees::FORMAT_UNIMARC,
+                      Class_IntProfilDonnees::FORMAT_UNIMARC_XML,
+                      Class_IntProfilDonnees::FORMAT_MARC21])
+      ->fetchAll();
+
+    foreach($profiles_in_db as $profile)
+      $profiles[$profile->getIdProfil()] = $profile->getLibelle();
+
+    return $profiles;
+  }
+}
diff --git a/library/ZendAfi/Form/Cosmo/DataProfile.php b/library/ZendAfi/Form/Cosmo/DataProfile.php
index 8fd3f0ed041..25307828abf 100644
--- a/library/ZendAfi/Form/Cosmo/DataProfile.php
+++ b/library/ZendAfi/Form/Cosmo/DataProfile.php
@@ -151,6 +151,9 @@ class ZendAfi_Form_Cosmo_DataProfile extends ZendAfi_Form {
     return $this
       ->_recordItemFields()
       ->addDocTypeGroup(['label' => $this->_('Label'), 'zone' => $this->_('Zone exemplaire')])
+      ->_addSectionsGroup()
+      ->_addLocationsGroup()
+      ->_addSortsGroup()
       ->_recordNoveltyDate()
       ->_recordItemSerial()
       ->_recordIndexation()
@@ -173,6 +176,48 @@ class ZendAfi_Form_Cosmo_DataProfile extends ZendAfi_Form {
   }
 
 
+  protected function _addSectionsGroup() {
+    $sections_prefs = new Class_ProfilePrefs_Codification_Section;
+
+    $this->addElement('multiInput', 'sections_field',
+                      ['label' => $this->_('Rechercher les sections dans'),
+                       'fields' => $sections_prefs->multiInputFieldsDefinition()]);
+
+    return $this->addDisplayGroup(['sections_field'],
+                                  'section_group',
+                                  ['legend' => $this->_('Sections')]);
+
+  }
+
+
+  protected function _addLocationsGroup() {
+    $locations_prefs = new Class_ProfilePrefs_Codification_Location;
+
+    $this->addElement('multiInput', 'locations_field',
+                      ['label' => $this->_('Rechercher les emplacements dans'),
+                       'fields' => $locations_prefs->multiInputFieldsDefinition()]);
+
+    return $this->addDisplayGroup(['locations_field'],
+                                  'location_group',
+                                  ['legend' => $this->_('Emplacements')]);
+
+  }
+
+
+  protected function _addSortsGroup() {
+    $sorts_prefs = new Class_ProfilePrefs_Codification_Sort;
+
+    $this->addElement('multiInput', 'sorts_field',
+                      ['label' => $this->_('Rechercher les genres dans'),
+                       'fields' => $sorts_prefs->multiInputFieldsDefinition()]);
+
+    return $this->addDisplayGroup(['sorts_field'],
+                                  'sort_group',
+                                  ['legend' => $this->_('Genres')]);
+
+  }
+
+
   public function addOaiSetsGroup() : self {
     $sets = (new ZendAfi_Form_Admin_OaiSets)->getElement();
     $this->addElement($sets);
@@ -556,6 +601,39 @@ class ZendAfi_Form_Cosmo_DataProfile extends ZendAfi_Form {
   }
 
 
+  public function populateSections() : self {
+    if (!$this->_profile_prefs)
+      return $this;
+
+    if ($this->sections_field)
+      $this->sections_field->setValues($this->_profile_prefs->getSections());
+
+    return $this;
+  }
+
+
+  public function populateLocations() : self {
+    if (!$this->_profile_prefs)
+      return $this;
+
+    if ($this->locations_field)
+      $this->locations_field->setValues($this->_profile_prefs->getLocations());
+
+    return $this;
+  }
+
+
+  public function populateSorts() : self {
+    if (!$this->_profile_prefs)
+      return $this;
+
+    if ($this->sorts_field)
+      $this->sorts_field->setValues($this->_profile_prefs->getSorts());
+
+    return $this;
+  }
+
+
   public function populateIdentifiers() : self {
     $this->getElement('identifiers')->setValues($this->_profile_prefs->getIdentifiers());
     return $this;
diff --git a/library/ZendAfi/View/Helper/BoutonIco.php b/library/ZendAfi/View/Helper/BoutonIco.php
index 71a1ea221d1..de51d642781 100644
--- a/library/ZendAfi/View/Helper/BoutonIco.php
+++ b/library/ZendAfi/View/Helper/BoutonIco.php
@@ -85,6 +85,7 @@ class ZendAfi_View_Helper_BoutonIco extends ZendAfi_View_Helper_BaseHelper
                 'VISIBLE' => ['hide', $this->_('Rendre visible')],
                 'INVISIBLE' => ['show', $this->_('Archiver')],
                 'DUPLICATE' => ['copy', $this->_('Dupliquer')],
+                'COPY_CODIFS' => ['copy', $this->_('Importer les codifications')],
     ];
 
     $type = strtoupper($type);
diff --git a/library/ZendAfi/View/Helper/Cosmo/CodifsImporter.php b/library/ZendAfi/View/Helper/Cosmo/CodifsImporter.php
new file mode 100644
index 00000000000..6392642bda9
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Cosmo/CodifsImporter.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Copyright (c) 2012-2023, 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_Cosmo_CodifsImporter extends ZendAfi_View_Helper_BaseHelper {
+
+  public function Cosmo_CodifsImporter() : string {
+    $html = $this->_tag('h1', $this->view->titre);
+
+    $form = (new ZendAfi_Form_Cosmo_CodifsImporter($this->view->target_profile))
+      ->setMethod('post')
+      ->setAction($this->view->url(['module' => 'cosmo',
+                                    'controller' => 'data-profile',
+                                    'action' => 'copy-codif-confirm',
+                                    'id' => $this->view->target_profile], null, true));
+
+    return $html
+      . $this->view->renderForm($form);
+  }
+}
diff --git a/library/ZendAfi/View/Helper/TagListeCoches.php b/library/ZendAfi/View/Helper/TagListeCoches.php
index a57bad66cca..c21819830bc 100644
--- a/library/ZendAfi/View/Helper/TagListeCoches.php
+++ b/library/ZendAfi/View/Helper/TagListeCoches.php
@@ -301,11 +301,6 @@ class ZendAfi_View_Helper_TagListeCochesSourceAnnexe extends ZendAfi_View_Helper
   public function __construct() {
     parent::__construct('Class_CodifAnnexe');
   }
-
-
-  protected function getKey($instance) {
-    return $instance->getCode();
-  }
 }
 
 
diff --git a/library/digital_resources/Numel/tests/NumelTest.php b/library/digital_resources/Numel/tests/NumelTest.php
index 76a35c1c44b..1b4c9a85a38 100644
--- a/library/digital_resources/Numel/tests/NumelTest.php
+++ b/library/digital_resources/Numel/tests/NumelTest.php
@@ -152,10 +152,14 @@ class NumelDashboardActivatedTest extends NumelActivatedTestCase
                             "SELECT `codif_thesaurus`.* FROM `codif_thesaurus` WHERE (`codif_thesaurus`.`code` = 'Digital resources' AND `codif_thesaurus`.`id_thesaurus` LIKE 'DRDR0001%') ORDER BY `codif_thesaurus`.`id_thesaurus` DESC LIMIT 1",
                             "SELECT `codif_thesaurus`.* FROM `codif_thesaurus` WHERE (`codif_thesaurus`.`code` = 'Digital resources' AND `codif_thesaurus`.`id_thesaurus` LIKE 'DRDR00010002%') ORDER BY `codif_thesaurus`.`id_thesaurus` DESC LIMIT 1",
                             "SELECT `codif_tags`.* FROM `codif_tags` WHERE (`codif_tags`.`a_moderer` > '')",
+
                             "SELECT COUNT(*) AS `numberof` FROM `notices` WHERE (MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+F_B3' IN BOOLEAN MODE) AND `notices`.`type` = 1)",
+
                             "SELECT COUNT(*) AS `numberof` FROM `notices` WHERE (MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+F_T0 +F_B3' IN BOOLEAN MODE) AND `notices`.`type` = 1)",
-                            "SELECT COUNT(*) AS `numberof` FROM `notices` WHERE (MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+F_HDRDR000100020001 +F_YArchivesdeMelun +F_B3' IN BOOLEAN MODE) AND `notices`.`type` = 1)",
-                            "SELECT COUNT(*) AS `numberof` FROM `notices` WHERE (`notices`.`type` = 1 AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+F_HDRDR000100020001 -F_YArchivesdeMelun +F_B3' IN BOOLEAN MODE))"]);
+
+                            "SELECT COUNT(*) AS `numberof` FROM `notices` WHERE (MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+F_HDRDR000100020001 +F_Y233 +F_B3' IN BOOLEAN MODE) AND `notices`.`type` = 1)",
+
+                            "SELECT COUNT(*) AS `numberof` FROM `notices` WHERE (`notices`.`type` = 1 AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+F_HDRDR000100020001 -F_Y233 +F_B3' IN BOOLEAN MODE))"]);
   }
 
 
diff --git a/tests/TearDown.php b/tests/TearDown.php
index 0a4c2e88cce..ca8ffb8d7f8 100644
--- a/tests/TearDown.php
+++ b/tests/TearDown.php
@@ -45,6 +45,7 @@ class TearDown {
     Class_Album::setFileSystem(null);
     Class_Article_Loader::reset();
 
+    Class_CodifAnnexe::reset();
     Class_TypeDoc::reset();
     Class_Codification::reset();
     Class_Codification::resetInstance();
diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php
index 3e001864ab2..f49d816d8f2 100644
--- a/tests/application/modules/AbstractControllerTestCase.php
+++ b/tests/application/modules/AbstractControllerTestCase.php
@@ -866,6 +866,12 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe
   }
 
 
+  public function assertXPathCountFromJson(string $path, int $count, string $message = ''): void
+  {
+    $this->_assertXPathFromJson('assertXpathCount', $path, $count, $message);
+  }
+
+
   public function assertNotXpathContentContainsFromJson(string $path,
                                                         string $match,
                                                         string $message = ''): void
diff --git a/tests/application/modules/admin/controllers/CatalogueControllerTest.php b/tests/application/modules/admin/controllers/CatalogueControllerTest.php
index a4222a2633d..f3b51c4a87a 100644
--- a/tests/application/modules/admin/controllers/CatalogueControllerTest.php
+++ b/tests/application/modules/admin/controllers/CatalogueControllerTest.php
@@ -802,13 +802,13 @@ class CatalogueControllerEditCatalogueTest extends CatalogueControllerEditCatalo
 
   /** @test */
   public function annexe12ChoiceShouldBePresent() {
-    $this->assertXPath('//div[@id="annexe_saisie"]//input[@type="checkbox"][@data-clef=12]');
+    $this->assertXPath('//div[@id="annexe_saisie"]//input[@type="checkbox"][@data-clef=1]');
   }
 
 
   /** @test */
   public function annexe24ChoiceShouldBePresent() {
-    $this->assertXPath('//div[@id="annexe_saisie"]//input[@type="checkbox"][@data-clef=24]');
+    $this->assertXPath('//div[@id="annexe_saisie"]//input[@type="checkbox"][@data-clef=2]');
   }
 
 
diff --git a/tests/application/modules/admin/controllers/TypeDocsControllerTest.php b/tests/application/modules/admin/controllers/TypeDocsControllerTest.php
index 26167aa0a64..5a5c80d2ddb 100644
--- a/tests/application/modules/admin/controllers/TypeDocsControllerTest.php
+++ b/tests/application/modules/admin/controllers/TypeDocsControllerTest.php
@@ -170,6 +170,7 @@ class TypeDocsControllerEditEPubTest extends AbstractTypeDocsControllerTestCase
                                          'libelle' => 'News',
                                          'invisible' => 0,
                                          'id_origine' => 4]);
+
     $this->fixture('Class_CodifAnnexe', ['id' => 93,
                                          'libelle' => 'Bd',
                                          'invisible' => 0,
@@ -182,12 +183,13 @@ class TypeDocsControllerEditEPubTest extends AbstractTypeDocsControllerTestCase
     $this->fixture('Class_CodifTypeDoc', ['id' => 102,
                                           'famille_id' => Class_CodifTypeDoc::LIVRE,
                                           'bibliotheques' => '2;3',
-                                          'annexes' => '4;8',
+                                          'annexes' => '73;93',
                                           'sections' => '9;10']);
 
-    $this->dispatch('/admin/type-docs/edit/id/102',true);
+    $this->dispatch('/admin/type-docs/edit/id/102');
   }
 
+
   /** @test */
   public function titreShouldBeModificationDuTypeDocEPUB() {
     $this->assertXPathContentContains('//h1','Modification du type de document: E-Books');
@@ -214,7 +216,7 @@ class TypeDocsControllerEditEPubTest extends AbstractTypeDocsControllerTestCase
 
   /** @test */
   public function annexeShouldBeDisplayedForEpub() {
-    $this->assertXPath('//form//input[@id="annexes"][@value="4;8"]');
+    $this->assertXPath('//form//input[@id="annexes"][@value="73;93"]');
   }
 
 
diff --git a/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php b/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php
index fadf79adbb4..a41ec65545a 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerSuggestionAchatNanookTest.php
@@ -49,9 +49,14 @@ abstract class AbstractAbonneControllerSuggestionAchatNanookTestCase extends Abs
                                                    'provide_suggest' => '1'],
                                  'comm_sigb' => Class_IntBib::COM_NANOOK]);
 
-    $this->fixture('Class_Bib', ['id' => 12,
-                                          'libelle' => 'Tatim bib',
-                                          'int_bib' => $sigb_conf]);
+    $this->fixture(Class_CodifAnnexe::class, ['id' => 876,
+                                              'code' => 12,
+                                              'id_origine' => 12,
+                                              'id_bib' => 12]);
+
+    $tatim = $this->fixture(Class_Bib::class, ['id' => 12,
+                                               'libelle' => 'Tatim bib',
+                                               'int_bib' => $sigb_conf]);
 
     $sigb_conf->setIdBib(12);
 
diff --git a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php
index c8da92eba50..43c083e88fc 100644
--- a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php
@@ -25,6 +25,13 @@ class AuthControllerPreRegistrationNanookDispatchTest
 
   public function setUp(): void   {
     parent::setUp();
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 99,
+                    'id_bib' => 1,
+                    'id_origine' => '1',
+                    'libelle' => 'Arcadia']);
+
     $this->fixture(Class_Bib::class,
                    ['id' => 1, 'libelle' => 'Arcadia']);
 
@@ -67,7 +74,7 @@ class AuthControllerPreRegistrationNanookDispatchTest
 
   /** @test */
   public function selectSiteShouldContainsOptionAcardia() {
-    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="1"]', 'Arcadia');
+    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="99"]', 'Arcadia');
   }
 
 
diff --git a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php
index 1d11dcbf3d5..b1fc204cf19 100644
--- a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php
@@ -346,21 +346,36 @@ abstract class AuthControllerPreRegistrationNanookTestCase
     parent::setUp();
 
     $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 1,
+                   ['id' => 99,
                     'id_bib' => 1,
                     'id_origine' => '1',
                     'libelle' => 'ARCADIA']);
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 100,
+                    'id_bib' => 2,
+                    'id_origine' => '2',
+                    'libelle' => 'Guiclan']);
+
 
     $this->fixture(Class_Bib::class,
                    ['id' => 1, 'libelle' => 'Arcadia']);
 
+    $this->fixture(Class_Bib::class,
+                   ['id' => 2, 'libelle' => 'Guiclan']);
+
     $this->fixture(Class_IntBib::class,
                    ['id' => 1,
                     'comm_params' => ['url_serveur' => 'http://super.nano.ok/ilsdi/arcadia',
                                       'pre-registration' => 1],
                     'comm_sigb' => Class_IntBib::COM_NANOOK]);
 
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 2,
+                    'comm_params' => ['url_serveur' => 'http://super.nano2.ok/ilsdi/arcadia',
+                                      'pre-registration' => 1],
+                    'comm_sigb' => Class_IntBib::COM_NANOOK]);
+
     Class_IntBib::find(1)->getSIGBComm()->setWebClient($this->mock_web_client);
   }
 }
@@ -407,6 +422,79 @@ class AuthControllerRegistrationWithConditionsTest extends AuthControllerPreRegi
 
 
 
+class AuthControllerPreRegistrationNanookGetDispatchTest
+  extends AuthControllerPreRegistrationNanookTestCase {
+
+  public function setUp(): void   {
+    parent::setUp();
+    $this->dispatch('/opac/auth/pre-registration', true);
+  }
+
+
+  /** @test */
+  public function shouldNotRedirect() {
+    $this->assertNotRedirect();
+  }
+
+
+  /** @test */
+  public function siteListShouldContainArcadiaOption()
+  {
+    $this->assertXpath('//form//select[@name="site"]/option[@value="99"]');
+  }
+
+
+  /** @test */
+  public function siteListShouldContainGuiclanOption()
+  {
+    $this->assertXpath('//form//select[@name="site"]/option[@value="100"]');
+  }
+
+
+  /** @test */
+  public function formShouldContainsEmail() {
+    $this->assertXpath('//form//input[@name="mail"][@required="required"]');
+  }
+
+
+  /** @test */
+  public function inputForBirthDateShouldBeRequired() {
+    $this->assertXPath('//form//input[@name="birthDate"][@required="required"][@type="date"]');
+  }
+
+
+  /** @test */
+  public function formShouldContainsPassword() {
+    $this->assertXpath('//form//input[@name="password"]');
+  }
+
+
+  /** @test */
+  public function selectSiteShouldContainsOption99() {
+    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="99"]', 'ARCADIA');
+  }
+
+
+  /** @test */
+  public function formShouldContainsCaptcha() {
+    $this->assertXPath('//input[@name="website"][@data-spambots="true"][@autocomplete="off"]');
+  }
+
+
+  /** @test */
+  public function birthdateShouldHaveMinAttribute() {
+    $this->assertXPath('//input[@name="birthDate"][@min="' . date('Y-m-d',strtotime('now -150 year')) . '"]');
+  }
+
+
+  /** @test */
+  public function birthdateShouldHaveMaxAttribute() {
+    $this->assertXPath('//input[@name="birthDate"][@max="' . date('Y-m-d',strtotime('now')) . '"]');
+  }
+}
+
+
+
 
 class AuthControllerPreRegistrationNanookPostDispatchTest
   extends AuthControllerPreRegistrationNanookTestCase {
@@ -420,7 +508,7 @@ class AuthControllerPreRegistrationNanookPostDispatchTest
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 1,
+             ['site' => 99,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -434,7 +522,7 @@ class AuthControllerPreRegistrationNanookPostDispatchTest
     Class_AdminVar::set('NOTIFICATION_TEMPLATE_PREREGISTRATION',"Bonjour {user.prenom} {user.nom},<br><br><p>Votre demande d'inscription à <b>{library.libelle}</b> a bien été enregistrée.</p><p>Afin de finaliser votre inscription, merci de vous rendre dans votre médiathèque : <b>{library.adresse} {library.cp} {library.ville}</b></p>");
 
     $this->postDispatch('/opac/auth/pre-registration',
-                        ['site' => '1',
+                        ['site' => '99',
                          'lastName' => 'Jiro',
                          'firstName' => 'Tom',
                          'mail' => 'test@test.fr',
@@ -499,7 +587,7 @@ abstract class AuthControllerPreRegistrationNanookPostErrorTestCase
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 1,
+             ['site' => 99,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -512,7 +600,7 @@ abstract class AuthControllerPreRegistrationNanookPostErrorTestCase
                 . $this->_errorCode()
                 . '</error></>');
 
-    $this->postDispatch('/opac/auth/pre-registration', ['site' => '1',
+    $this->postDispatch('/opac/auth/pre-registration', ['site' => '99',
                                                         'lastName' => 'Jiro',
                                                         'firstName' => 'Tom',
                                                         'mail' => 'test@test.fr',
@@ -614,7 +702,7 @@ class AuthControllerPreRegistrationNanookPostDispatchErrorPatronPasswordNotSecur
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 1,
+             ['site' => 99,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -625,7 +713,7 @@ class AuthControllerPreRegistrationNanookPostDispatchErrorPatronPasswordNotSecur
               'address' => '123'])
       ->answers('<?xml version="1.0" encoding="UTF-8"?><error>PatronPasswordNotSecure</error><securePasswordLabel>Le mot de passe doit comporter au minimum 6 caractères et doit être constitué d\'au moins un chiffre et une lettre.</securePasswordLabel>');
 
-    $this->postDispatch('/opac/auth/pre-registration', ['site' => '1',
+    $this->postDispatch('/opac/auth/pre-registration', ['site' => '99',
                                                         'lastName' => 'Jiro',
                                                         'firstName' => 'Tom',
                                                         'mail' => 'test@test.fr',
@@ -733,6 +821,10 @@ class AuthControllerPreRegistrationWithNoPreRegistrationTest extends AuthControl
     parent::setUp();
     Class_IntBib::find(1)->setCommParams(['url_serveur' => 'http://super.nano.ok/ilsdi/arcadia',
                                           'pre-registration' => 0])->save();
+
+    Class_IntBib::find(2)->setCommParams(['url_serveur' => 'http://super.nano.ok/ilsdi/arcadia',
+                                          'pre-registration' => 0])->save();
+
     $this->dispatch('/opac/auth/pre-registration');
   }
 
@@ -842,7 +934,7 @@ class AuthControllerPreRegistrationNanookDispatchAsInviteAndPostDispatchTest ext
 <GetPatronInfo><patronId>5352</patronId><barcode></barcode><lastName>Ro</lastName><firstName>Toto</firstName><siteId>1</siteId></GetPatronInfo>');
 
     $this->postDispatch('/opac/auth/pre-registration',
-                        ['site' => '1',
+                        ['site' => '99',
                          'lastName' => 'Ro',
                          'firstName' => 'Toto',
                          'mail' => 'to@to.ro',
@@ -1136,7 +1228,7 @@ class AuthControllerPreRegistrationDefaultTemplateContentPostDispatchTest
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 1,
+             ['site' => 99,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -1150,7 +1242,7 @@ class AuthControllerPreRegistrationDefaultTemplateContentPostDispatchTest
     Class_AdminVar::set('NOTIFICATION_TEMPLATE_PREREGISTRATION', '');
 
     $this->postDispatch('/opac/auth/pre-registration',
-                        ['site' => '1',
+                        ['site' => '99',
                          'lastName' => 'Jiro',
                          'firstName' => 'Tom',
                          'mail' => 'test@test.fr',
diff --git a/tests/application/modules/opac/controllers/AuthControllerTest.php b/tests/application/modules/opac/controllers/AuthControllerTest.php
index e0dbc93ac89..ed9d027606b 100644
--- a/tests/application/modules/opac/controllers/AuthControllerTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerTest.php
@@ -2834,43 +2834,37 @@ class AuthControllerPostWithSameIdSigbTest extends AbstractControllerTestCase {
 
 class AuthControllerPostLoginWithDifferentIdIntBibTest
   extends AbstractControllerTestCase {
-  protected $_storm_default_to_volatile = true;
+
 
   public function setUp(): void   {
     parent::setUp();
 
     ZendAfi_Auth::getInstance()->clearIdentity();
 
-    $pasc_library = $this->fixture(Class_Bib::class,
-                                   ['id' => 987,
-                                    'libelle' => 'Pasc Library']);
-
-    $this->fixture(Class_CodifAnnexe::class,
-                           ['id' => 15,
-                            'libelle' => 'pasc',
-                            'id_origine' => 'PASC',
-                            'bib' => $pasc_library]);
-
-    $emprunteur = Class_WebService_SIGB_Emprunteur::newInstance(789, 'koha');
-    $emprunteur->setPassword('bar')
-               ->setLibraryCode('PASC');
-    $emprunteur->beValid();
+    $user =
+      $this->fixture(Class_Users::class,
+                     ['id' => 5,
+                      'login' => 'foo',
+                      'password' => 'bar',
+                      'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                      'idabon' => 'foo',
+                      'id_site' => 56,
+                      'id_int_bib' => 56,
+                      'id_sigb' => null]);
 
-    $service = (new Class_Testing_WebService_SIGB_KohaLegacy_Service())
-      ->whenCalled('getEmprunteur')
-      ->answers($emprunteur)
-      ->whenCalled('isConnected')
-      ->answers(true)
-      ->whenCalled('providesAuthentication')
-      ->answers(true);
+    Class_HttpClientFactory::forTest()
+      ->addPostRequestWithResponse('http://mon-koha-de-test.org:80',
+                                   ['service' => 'AuthenticatePatron',
+                                    'username' => 'foo',
+                                    'password' => 'bar'],
+                                   KohaFixtures::xmlAuthenticatePatronJameBondOk())
+      ->addRequestWithResponse('http://mon-koha-de-test.org:80?service=GetPatronInfo&patron_id=007&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoJamesBond());
 
     $params = ['url_serveur' => 'http://mon-koha-de-test.org',
                'id_bib' => 56,
                'type' => Class_IntBib::COM_KOHA_LEGACY];
 
-    Class_WebService_SIGB_KohaLegacy::setService($params,
-                                                 $service);
-
     $this->fixture(Class_Bib::class,
                    ['id' => 56,
                     'libelle' => 'Library']);
@@ -2885,31 +2879,39 @@ class AuthControllerPostLoginWithDifferentIdIntBibTest
                    ['id' => 987,
                     'id_bib' => 987,
                     'comm_sigb' => Class_IntBib::COM_KOHA_LEGACY,
-                    'comm_params' => serialize($params)]);
+                    'comm_params' => serialize(array_merge($params, ['id_bib' => 987]))]);
 
-    $this->fixture(Class_Users::class,
-                   ['id' => 5,
-                    'login' => 'foo',
-                    'password' => 'bar',
-                    'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
-                    'idabon' => 'foo',
-                    'id_site' => 56,
-                    'id_int_bib' => 56,
-                    'id_sigb' => null]);
+    $this->fixture(Class_Bib::class,
+                   ['id' => 987,
+                    'libelle' => 'VS Library',
+                    'visibilite' => Class_Bib::V_DATA]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                           ['id' => 15,
+                            'libelle' => 'VS',
+                            'id_origine' => 'VS',
+                            'id_bib' => 987]);
 
     $this->postDispatch('/opac/auth/login', ['username' => 'foo',
                                              'password' => 'bar']);
   }
 
 
-  /** @test */
-  public function userFooShouldBeLoggedAndCreated() {
+  public function fooData() : array {
+    return [[667, fn($user) => $user->getId()],
+            ['foo', fn($user) => $user->getLogin()],
+            ['VS Library', fn($user) => $user->getLibelleBib()],
+            [987, fn($user) => $user->getIdIntBib()]];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider fooData
+   */
+  public function newUserFooShouldBeSavedWithExpectedData($expected, $foo_data) {
     $user = Class_Users::getIdentity();
-    $this->assertNotNull($user);
-    $this->assertEquals('foo', $user->getLogin());
-    $this->assertNotEquals(5, $user->getId());
-    $this->assertEquals('Pasc Library', $user->getLibelleBib());
-    $this->assertEquals(56, $user->getIdIntBib());
+    $this->assertEquals($expected, $foo_data($user));
   }
 }
 
@@ -2929,7 +2931,7 @@ class AuthControllerPostLoginOpsysWithExceptionTest extends AbstractControllerTe
     $this->fixture(Class_IntBib::class,
                    ['id' => 44 ,
                     'comm_sigb' => Class_IntBib::COM_OPSYS,
-                    'comm_params' => serialize(['url_serveur' => ''])]);
+                    'comm_params' => serialize(['url_serveur' => 'http://astrolabe.com/opsys.wsdl'])]);
     $service = (new Class_Testing_WebService_SIGB_Opsys_Service(null))
       ->whenCalled('getEmprunteur')
       ->answers(Class_WebService_SIGB_Emprunteur::newInstance(2233, 'harlock')->beValid())
@@ -2944,7 +2946,9 @@ class AuthControllerPostLoginOpsysWithExceptionTest extends AbstractControllerTe
       ->whenCalled('providesAuthentication')
       ->answers(true);
 
-    Class_WebService_SIGB_Opsys::setService(['url_serveur' => '','id_bib' => 44,'type' => 2],
+    Class_WebService_SIGB_Opsys::setService(['url_serveur' => 'http://astrolabe.com/opsys.wsdl',
+                                             'id_bib' => 44,
+                                             'type' => 2],
                                             $service);
 
     $this->postDispatch('/opac/auth/login',
@@ -3116,3 +3120,95 @@ class AuthControllerOrpheeTest
                         Class_Users::findFirstBy([ 'login' => '0010900000753'])->getIdIntBib());
   }
 }
+
+
+
+
+class AuthControllerLoggedActivateWithKohaCodifAnnexeWithEmptyComParamSuccessTest extends AuthControllerNobodyLoggedTestCase {
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 10,
+                    'libelle'=>'Myriam',
+                   ]);
+
+    $params = ['url_serveur' => 'http://mon-koha-de-test.org',
+               'api_user' => 'bokeh.ap',
+               'api_pass' => 'scrt',
+               'loans_per_page' => 20];
+
+    $int_bib = $this->fixture(Class_IntBib::class,
+                              ['id' => 10,
+                               'id_bib' => 10,
+                               'sigb' =>  Class_IntBib::SIGB_KOHA,
+                               'nom_court' => 'Myriam',
+                               'comm_sigb' => Class_IntBib::COM_KOHA,
+                               'comm_params' => $params
+                              ]);
+    $int_bib->assertSave();
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 14,
+                    'libelle'=>'Joinville-le-point',
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 123,
+                    'code' => 'BDM',
+                    'libelle'=> 'Blog du Modérateur',
+                    'id_origine' => 'BDM',
+                    'id_bib' => 14,
+                   ]);
+
+    $empty_bib = $this->fixture(Class_IntBib::class,
+                                ['id' => 14,
+                                 'id_bib'=> 14,
+                                 'nom_court' => 'JoinVille',
+                                 'comm_sigb' => Class_IntBib::SIGB_NONE,
+                                 'sigb' => Class_IntBib::SIGB_NONE,
+                                ]);
+    Class_HttpClientFactory::forTest()
+      ->addPostRequestWithResponse('http://mon-koha-de-test.org:80',
+                                   ['service' => 'AuthenticatePatron',
+                                    'username' => 'JohnDoe',
+                                    'password' => 's3cr3t'],
+                                   KohaFixtures::xmlAuthenticatePatronOk()
+      )
+      ->addRequestWithResponse('http://mon-koha-de-test.org:80?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               kohaFixtures::xmlGetPatronInfoLaure()
+      )
+      ->addRequestWithResponse('http://mon-koha-de-test.org:80?service=GetPatronInfo&patron_id=572&show_contact=1&show_loans=0&show_holds=1',
+                               kohaFixtures::xmlGetPatronInfoLaure())
+      ->addRequestWithResponse('http://mon-koha-de-test.org:80?service=GetPatronInfo&patron_id=572&show_contact=0&show_loans=1&show_holds=0&loans_per_page=20&loans_page=1',
+                               kohaFixtures::xmlGetPatronInfoLaure()
+                               );
+    Class_CommSigb::shouldThrowError(true);
+    Class_Webservice_Sigb_AbstractRESTService::shouldThrowError(true);
+
+    $this->postDispatch('/opac/auth/boite-login',
+                        [
+                         'username'=>'JohnDoe',
+                         'password' => 's3cr3t']);
+
+    Class_Users::clearCache();
+  }
+
+
+  /** @test */
+  public function userCreatedShouldBeLinkedToIntBib10() {
+    $user = Class_Users::findFirstBy(['login' => 'JohnDoe']);
+    $this->assertEquals(10, $user->getIdIntBib());
+    return $user;
+  }
+
+
+  /**
+   * @depends userCreatedShouldBeLinkedToIntBib10
+   * @test
+   */
+  public function userCreatedShouldBeLinkedToBib14($user) {
+    $this->assertEquals(14, $user->getIdSite());
+  }
+}
diff --git a/tests/application/modules/opac/controllers/AuthControllerWithNanookTest.php b/tests/application/modules/opac/controllers/AuthControllerWithNanookTest.php
index 7f540cdb7bd..a478a4c4135 100644
--- a/tests/application/modules/opac/controllers/AuthControllerWithNanookTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerWithNanookTest.php
@@ -233,13 +233,14 @@ class AuthControllerWithNanookPostSecurePasswordWithMailAndUnsecurePassword
                                    ,
                                    '<?xml version="1.0" encoding="utf-8"?><GetPatronInfo><patronId>9876</patronId><mail>name@server.tld</mail><barcode>03456</barcode></GetPatronInfo>');
 
-    $this->postDispatch('/opac/auth/do-secure-password', ['card' => 'ZBTIC1234',
-                                                          'current_password' => '1987',
-                                                          'secure_password' => 'rox@r#1',
-                                                          'confirm_password' => 'rox@r#1',
-                                                          'pattern' => '/^(?=.*[^0-9])(?=.*\d).{6,}$/',
-                                                          'password_hint' => 'doit faire au moins 6',
-                                                          ]);
+    $this->postDispatch('/opac/auth/do-secure-password',
+                        ['card' => 'ZBTIC1234',
+                         'current_password' => '1987',
+                         'secure_password' => 'rox@r#1',
+                         'confirm_password' => 'rox@r#1',
+                         'pattern' => '/^(?=.*[^0-9])(?=.*[0-9]).{6,}$/',
+                         'password_hint' => 'doit faire au moins 6',
+                        ]);
   }
 
 
@@ -289,13 +290,14 @@ class AuthControllerWithNanookPostDoSecurePasswordWithWrongCard
       ->addRequestWithResponse($this->nanook_service_url.'service/AuthenticatePatron/username/ZBTIC1234/password/1987',
                                '<?xml version="1.0" encoding="UTF-8"?><AuthenticatePatron><error>PatronNotFound</error></AuthenticatePatron>');
 
-    $this->postDispatch('/opac/auth/do-secure-password', ['card' => 'ZBTIC1234',
-                                                          'current_password' => '1987',
-                                                          'secure_password' => 'rox@r#1',
-                                                          'confirm_password' => 'rox@r#1',
-                                                          'pattern' => '/^(?=.*[^0-9])(?=.*\d).{6,}$/',
-                                                          'password_hint' => 'doit faire au moins 6',
-                                                          ]);
+    $this->postDispatch('/opac/auth/do-secure-password',
+                        ['card' => 'ZBTIC1234',
+                         'current_password' => '1987',
+                         'secure_password' => 'rox@r#1',
+                         'confirm_password' => 'rox@r#1',
+                         'pattern' => '/^(?=.*[^0-9])(?=.*[0-9]).{6,}$/',
+                         'password_hint' => 'doit faire au moins 6',
+                        ]);
   }
 
 
@@ -337,26 +339,28 @@ class AuthControllerWithNanookPostDoSecurePasswordWithErrorsTest
 
   /** @test */
   public function formShouldDisplayErrorPasswordDoesNotMatch() {
-    $this->postDispatch('/opac/auth/do-secure-password', ['card' => 'ZBTIC1234',
-                                                          'current_password' => '1987',
-                                                          'secure_password' => 'rox@r#1',
-                                                          'confirm_password' => 'rouk1',
-                                                          'pattern' => '/^(?=.*[^0-9])(?=.*\d).{6,}$/',
-                                                          'password_hint' => 'doit faire au moins 6',
-                                                          ]);
+    $this->postDispatch('/opac/auth/do-secure-password',
+                        ['card' => 'ZBTIC1234',
+                         'current_password' => '1987',
+                         'secure_password' => 'rox@r#1',
+                         'confirm_password' => 'rouk1',
+                         'pattern' => '/^(?=.*[^0-9])(?=.*[0-9]).{6,}$/',
+                         'password_hint' => 'doit faire au moins 6',
+                        ]);
     $this->assertXPathContentContains('//li', 'Les champs \'Mot de passe\' sont différents');
   }
 
 
   /** @test */
   public function formShouldDisplayErrorPasswordDoesNotFollowPattern() {
-    $this->postDispatch('/opac/auth/do-secure-password', ['card' => 'ZBTIC1234',
-                                                          'current_password' => '1987',
-                                                          'secure_password' => 'pouet',
-                                                          'confirm_password' => 'pouet',
-                                                          'pattern' => '/^(?=.*[^0-9])(?=.*\d).{6,}$/',
-                                                          'password_hint' => 'doit faire au moins 6',
-                                                          ]);
+    $this->postDispatch('/opac/auth/do-secure-password',
+                        ['card' => 'ZBTIC1234',
+                         'current_password' => '1987',
+                         'secure_password' => 'pouet',
+                         'confirm_password' => 'pouet',
+                         'pattern' => '/^(?=.*[^0-9])(?=.*[0-9]).{6,}$/',
+                         'password_hint' => 'doit faire au moins 6',
+                        ]);
     $this->assertXPathContentContains('//li', 'doit faire au moins 6');
   }
 }
@@ -503,7 +507,6 @@ class AuthControllerWithNanookPostAxelLoginNoMemberships
 
 class AuthControllerWithNanookPostAxelLoginWithExistingMemberships
   extends AuthControllerWithNanookPostAxelLoginTestCase {
-
   protected function _prepareMemberships(){
     parent::_prepareMemberships();
     $this->fixture(Class_Membership::class,
@@ -662,3 +665,144 @@ class AuthControllerWithNanookPostAxelLoginWithRoleLevelModoBibWithoutLabelButEx
     $this->assertEquals(0, Class_User_Membership::count());
   }
 }
+
+
+
+
+abstract class AuthControllerWithNanookPostAxelLoginWithCodifAnnexeTestCase
+  extends AuthControllerNanookTestCase {
+  public function setUp() :void
+  {
+    parent::setUp();
+    Class_WebService_SIGB_AbstractRESTService::shouldThrowError(true);
+
+    $params = ['url_serveur' => 'http://localhost:8080/afi_Nanook/ilsdi/',
+               'id_bib' => 6,
+               'type' => Class_IntBib::COM_NANOOK];
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 6,
+                    'libelle' => 'Cran']);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 6,
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => serialize($params)]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 7,
+                    'code' => 1,
+                    'id_origine' => 1,
+                    'id_bib' => 6,
+                    'libelle' => 'Cran']);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 5,
+                    'code' => 2,
+                    'id_origine' => 2,
+                    'id_bib' => 5,
+                    'libelle' => 'Seynod']);
+
+    Class_WebService_SIGB_Nanook::reset();
+
+    $this->_web_client = $this->mock()
+                              ->whenCalled('open_url')
+                              ->with('http://localhost:8080/afi_Nanook/ilsdi/service/AuthenticatePatron/username/Axel/password/2022')
+                              ->answers(NanookFixtures::axelAuthenticatePatron())
+
+                              ->whenCalled('open_url')
+                              ->with('http://localhost:8080/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/8')
+                              ->answers(NanookFixtures::axelPatronInfo())
+
+                              ->whenCalled('hasErrors')
+                              ->answers(false)
+
+                              ->beStrict();
+
+
+    $this->_service = Class_WebService_SIGB_Nanook_Service::newInstance()
+      ->setServerRoot('http://localhost:8080/afi_Nanook/ilsdi/')
+      ->setWebClient($this->_web_client);
+
+    $params = ['url_serveur' => 'http://localhost:8080/afi_Nanook/ilsdi/',
+               'id_bib' => 5,
+               'type' => Class_IntBib::COM_NANOOK];
+
+    Class_WebService_SIGB_Nanook::setService($params, $this->_service);
+
+    $params = ['url_serveur' => 'http://localhost:8080/afi_Nanook/ilsdi/',
+               'id_bib' => 6,
+               'type' => Class_IntBib::COM_NANOOK];
+
+    Class_WebService_SIGB_Nanook::setService($params, $this->_service);
+
+    $this->postDispatch('/opac/auth/login',
+                        ['username' => 'Axel',
+                         'password' => '2022']);
+  }
+}
+
+
+
+
+class AuthControllerWithNanookPostAxelLoginWithTwoPossibleIdIntBibTest
+  extends  AuthControllerWithNanookPostAxelLoginWithCodifAnnexeTestCase {
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->postDispatch('/opac/auth/login',
+                        ['username' => 'Axel',
+                         'password' => '2022']);
+  }
+
+
+  /** @test */
+  public function oneUserShouldBeCreatedWithIdIntBib6() {
+    $this->assertEquals(Class_Users::findFirstBy(['login' => 'Axel'])->getIdIntBib(), 6);
+  }
+}
+
+
+
+class AuthControllerWithNanookPostAxelLoginWithTwoDifferentNanokNetworkTest
+  extends  AuthControllerWithNanookPostAxelLoginWithCodifAnnexeTestCase {
+  public function setUp() :void
+  {
+    parent::setUp();
+
+    $params = ['url_serveur' => 'http://Myotherhost:8080/afi_Nanook/ilsdi/',
+               'id_bib' => 7,
+               'type' => Class_IntBib::COM_NANOOK];
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 7,
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => serialize($params)]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 7,
+                    'libelle' => 'Barberaz']);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 1,
+                    'code' => 1,
+                    'id_origine' => 1,
+                    'id_bib' => 7,
+                    'libelle' => 'Barberaz']);
+
+    $this->postDispatch('/opac/auth/login',
+                        ['username' => 'Axel',
+                         'password' => '2022']);
+  }
+
+
+  /** @test */
+  public function oneUserShouldBeCreatedWith() {
+    $this->assertEquals(Class_Users::findFirstBy(['login' => 'Axel'])->getIdIntBib(), 6);
+  }
+}
diff --git a/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php b/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php
index ae390fe0959..1ee619935bd 100644
--- a/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php
+++ b/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php
@@ -901,7 +901,7 @@ class NoticeAjaxControllerExemplairesWithOtherAnnexTest
                                                             'id_int_bib' => 6,
                                                             'id_notice' => 123,
                                                             'id_origine' => '369667',
-                                                            'annexe' => 'MOUL',
+                                                            'annexe' => 86,
                                                             'section' => 'A9',
                                                             'emplacement' => '42',
                                                             'cote' => 'VOD-T-DLJ',
@@ -921,16 +921,21 @@ class NoticeAjaxControllerExemplairesWithOtherAnnexTest
                                                       'id_notice' => 125,
                                                       'cote' => 'VOD',
                                                       'dispo' => 'Disponible'])]]);
+
+    $this->fixture(Class_CodifAnnexe::class, ['id' => 86,
+                                              'id_bib' => 6,
+                                              'id_origine' => 21,
+                                              'code' => 21]);
   }
 
 
   /** @test */
-  public function facetsShouldBeUpdatedWithAnnex21() {
+  public function facetsShouldBeUpdatedWithAnnex86() {
     $this->dispatch('noticeajax/exemplaires/id/123');
     Class_Notice::clearCache();
-    $this->assertEquals('D78092 A3029 A9751 A117014 A117015 M37414 G464 HNNNN0002 HNANA0002 T0 B6 SA9 E42 Y21 V6 HNRNR0001',
+    $this->assertEquals('D78092 A3029 A9751 A117014 A117015 M37414 G464 HNNNN0002 HNANA0002 T0 B6 SA9 E42 Y86 V6 HNRNR0001',
                         Class_notice::find(123)->getFacettes());
-    $this->assertEquals('F_D78092 F_A3029 F_A9751 F_A117014 F_A117015 F_M37414 F_G464 F_HNNNN0002 F_HNANA0002 F_T0 F_B6 F_SA9 F_E42 F_Y21 F_V6 F_HNRNR0001',
+    $this->assertEquals('F_D78092 F_A3029 F_A9751 F_A117014 F_A117015 F_M37414 F_G464 F_HNNNN0002 F_HNANA0002 F_T0 F_B6 F_SA9 F_E42 F_Y86 F_V6 F_HNRNR0001',
                         Class_notice::find(123)->getFacets());
   }
 }
diff --git a/tests/application/modules/opac/controllers/RechercheControllerIdentifiersTest.php b/tests/application/modules/opac/controllers/RechercheControllerIdentifiersTest.php
index 4dc0baf9310..7e9a2b91cd8 100644
--- a/tests/application/modules/opac/controllers/RechercheControllerIdentifiersTest.php
+++ b/tests/application/modules/opac/controllers/RechercheControllerIdentifiersTest.php
@@ -26,15 +26,26 @@ class RechercheControllerIdentifiersIdSigbTest
     parent::setUp();
 
     $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 1,
-                    'id_bib' => 456,
+                   ['id' => 3,
+                    'id_bib' => 4,
+                    'code' => 2,
                     'id_origine' => 2]);
 
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 34,
                     'id_bib' => 456,
+                    'id_int_bib' => 456,
                     'id_notice' => 345,
+                    'id_origine' => 12,
+                    'code_barres' => '12256663233656',
+                   ]);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 348989,
+                    'id_bib' => 4,
                     'id_int_bib' => 1,
+                    'annexe' => 3,
+                    'id_notice' => 345,
                     'id_origine' => 12,
                     'code_barres' => '12256663233656',
                    ]);
@@ -54,7 +65,7 @@ class RechercheControllerIdentifiersIdSigbTest
 
   /** @test */
   public function withRightIdsResponseShouldRedirectToViewNoticeId345() {
-    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/2');
+    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/3');
     $this->assertRedirectTo('/recherche/viewnotice/id/345');
   }
 
@@ -68,7 +79,7 @@ class RechercheControllerIdentifiersIdSigbTest
 
   /** @test */
   public function withIdSIB12AndRecordTypeAuthorityShouldRedirectToViewNoticeId678() {
-    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/2/record_type/2');
+    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_bib/456/record_type/2');
     $this->assertRedirectTo('/recherche/viewnotice/id/678');
   }
 
@@ -95,9 +106,9 @@ class RechercheControllerIdentifiersIdSigbTest
 
 
   /** @test */
-  public function withUnknownIdSiteShouldThrowError() {
+  public function withUndefinedIdSiteShouldThrowError() {
     try {
-      $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/3');
+      $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/45');
     } catch (Zend_Controller_Action_Exception $e){
       $this->assertTrue(true);
     }
@@ -126,14 +137,6 @@ class RechercheControllerIdentifiersIdSigbTest
     $this->dispatch('/recherche/viewnotice/id_sigb/12', false);
     $this->assertResponseCode(400);
   }
-
-
-  /** @test */
-  public function withIdIntBib1AndIdSite2ShouldThrow400() {
-    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/2/id_int_bib/1',
-                    false);
-    $this->assertResponseCode(400);
-  }
 }
 
 
@@ -148,12 +151,14 @@ class RechercheControllerIdentifiersIdSigbFilteredTest
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 1,
                     'id_bib' => 456,
+                    'code' => 2,
                     'id_origine' => 2]);
 
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 5,
                     'id_notice' => 883,
                     'id_bib' => 456,
+                    'annexe' => 1,
                     'id_int_bib' => 0,
                     'id_origine' => 12,
                     'code_barres' => null,
@@ -163,8 +168,9 @@ class RechercheControllerIdentifiersIdSigbFilteredTest
                    ['id' => 34,
                     'id_bib' => 456,
                     'id_notice' => 345,
-                    'id_int_bib' => 1,
+                    'id_int_bib' => 456,
                     'id_origine' => 12,
+                    'annexe' => 1,
                     'code_barres' => '12256663233656',
                    ]);
   }
@@ -178,8 +184,8 @@ class RechercheControllerIdentifiersIdSigbFilteredTest
 
 
   /** @test */
-  public function withIdSigbAndIdSite2ShouldRedirectToViewNoticeId345() {
-    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/2');
+  public function withIdSigb12AndIdSite2ShouldRedirectToViewNoticeId345() {
+    $this->dispatch('/recherche/viewnotice/id_sigb/12/id_site/1');
     $this->assertRedirectTo('/recherche/viewnotice/id/345');
   }
 }
diff --git a/tests/application/modules/opac/controllers/RechercheControllerReservationPergameTest.php b/tests/application/modules/opac/controllers/RechercheControllerReservationPergameTest.php
index 02d1c90b2f7..04fbce972bb 100644
--- a/tests/application/modules/opac/controllers/RechercheControllerReservationPergameTest.php
+++ b/tests/application/modules/opac/controllers/RechercheControllerReservationPergameTest.php
@@ -26,47 +26,62 @@ abstract class RechercheControllerReservationPergameTestCase extends AbstractCon
   public function setUp(): void   {
     parent::setUp();
 
-    $this->fixture('Class_Bib',
+    $bib_toulon = $this->fixture(Class_Bib::class,
                                  ['id' => 1]);
 
-    $int_bib_toulon = $this->fixture('Class_IntBib', ['id' => 1,
-                                                      'comm_sigb' => Class_IntBib::COM_PERGAME,
-                                                      'comm_params' => ['url_serveur' => 'bib-toulon.sud',
-                                                                        'Autoriser_docs_disponibles' => 1],
-                                                      'id_bib' => 1]);
+    $int_bib_toulon = $this->fixture(Class_IntBib::class,
+                                     ['id' => 1,
+                                      'comm_sigb' => Class_IntBib::COM_PERGAME,
+                                      'comm_params' => ['url_serveur' => 'bib-toulon.sud',
+                                                        'Autoriser_docs_disponibles' => 1],
+                                      'id_bib' => 1]);
 
-    $this->fixture('Class_Bib',
+    $bib_can = $this->fixture(Class_Bib::class,
                               ['id' => 2]);
 
-    $this->fixture('Class_IntBib', ['id' => 2,
-                                                     'comm_sigb' => Class_IntBib::COM_PERGAME,
-                                                     'comm_params' => ['url_serveur' => 'bib-canne.sud',
-                                                                       'Autoriser_docs_disponibles' => 1],
-                                                     'id_bib' => 2]);
+    $int_bib_canne = $this->fixture(Class_IntBib::class,
+                                    ['id' => 2,
+                                     'comm_sigb' => Class_IntBib::COM_PERGAME,
+                                     'comm_params' => ['url_serveur' => 'bib-canne.sud',
+                                                       'Autoriser_docs_disponibles' => 1],
+                                     'id_bib' => 2]);
 
-    $bib_mars = $this->fixture('Class_Bib', ['id' => 3]);
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 2,
+                    'libelle' => 'Canne',
+                    'id_bib' => 2,
+                    'id_origine' => 2]);
 
-    $jacques = $this->fixture('Class_Users', ['id' => 1,
-                                              'login' => 'jacques',
-                                              'password' => 'secret',
-                                              'idabon' => '1414',
-                                              'ordreabon' => '1',
-                                              'bib' => $bib_mars,
-                                              'int_bib' => $int_bib_toulon]);
+    $bib_mars = $this->fixture(Class_Bib::class, ['id' => 3]);
 
+    $jacques = $this->fixture(Class_Users::class,
+                              ['id' => 1,
+                               'login' => 'jacques',
+                               'password' => 'secret',
+                               'idabon' => '1414',
+                               'ordreabon' => '1',
+                               'bib' => $bib_mars,
+                               'int_bib' => $int_bib_toulon]);
 
-    ZendAfi_Auth::getInstance()->logUser($jacques);
 
-    $this->fixture('Class_Notice', ['id' => 31084,
-                                    'titres' => 'TITEUF TITEF FILLES FIL C00  EST NUL']);
+    ZendAfi_Auth::getInstance()->logUser($jacques);
 
-    $this->fixture('Class_Exemplaire', ['id' => 1142445,
-                                        'id_notice' => 31084,
-                                        'code_barres' => '0229923321',
-                                        'id_origine' => '00021243',
-                                        'id_int_bib' => 2,
-                                        'id_bib' => 2,
-                                        'annexe' => 2]);
+    $this->fixture(Class_Notice::class,
+                   ['id' => 31084,
+                    'titres' => 'TITEUF TITEF FILLES FIL C00  EST NUL']);
+
+    $this->fixture(Class_CodifAnnexe::class, ['id' => 2,
+                                              'code' => 'Deux',
+                                              'id_origine' => 'Deux']);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 1142445,
+                    'id_notice' => 31084,
+                    'code_barres' => '0229923321',
+                    'id_origine' => '00021243',
+                    'id_int_bib' => 2,
+                    'id_bib' => 2,
+                    'annexe' => 2]);
   }
 }
 
@@ -78,10 +93,9 @@ class RechercheControllerReservationPergameWithPickupToItemLibrary
   public function setUp(): void   {
     parent::setUp();
 
-    $this->fixture('Class_CosmoVar', ['id' => 'site_retrait_resa',
-                                      'valeur' => 0]);
+    Class_CosmoVar::setValueOf('site_retrait_resa', Class_CosmoVar::PICKUP_LOCATION_ITEM);
 
-    $this->dispatch('recherche/reservationajax/id_bib/2/copy_id/1142445/code_annexe/2',true);
+    $this->dispatch('recherche/reservationajax/id_bib/2/copy_id/1142445/code_annexe/2');
   }
 
 
@@ -89,7 +103,7 @@ class RechercheControllerReservationPergameWithPickupToItemLibrary
   public function holdShouldHaveBeenCreatedWithCanneLibrary() {
     $reservation = Class_Reservation::findFirstBy(['id_notice_origine' => '00021243',
                                                    'idabon' => 1414]);
-    $this->assertEquals(2, $reservation->getIdSite());
+    $this->assertEquals('Deux', $reservation->getIdSite());
   }
 }
 
@@ -101,10 +115,9 @@ class RechercheControllerReservationPergameWithPickupToPatronLibrary
   public function setUp(): void   {
     parent::setUp();
 
-    $this->fixture('Class_CosmoVar', ['id' => 'site_retrait_resa',
-                                      'valeur' => 2]);
+    Class_CosmoVar::setValueOf('site_retrait_resa', Class_CosmoVar::PICKUP_LOCATION_PATRON);
 
-    $this->dispatch('recherche/reservationajax/id_bib/8/copy_id/1142445/code_annexe/18',true);
+    $this->dispatch('recherche/reservationajax/id_bib/8/copy_id/1142445/code_annexe/18');
   }
 
 
diff --git a/tests/application/modules/opac/controllers/RechercheControllerReservationTest.php b/tests/application/modules/opac/controllers/RechercheControllerReservationTest.php
index 5d1c2bd5e4a..10690fcb73f 100644
--- a/tests/application/modules/opac/controllers/RechercheControllerReservationTest.php
+++ b/tests/application/modules/opac/controllers/RechercheControllerReservationTest.php
@@ -20,6 +20,10 @@
  */
 
 
+require_once 'tests/fixtures/NanookFixtures.php';
+require_once 'tests/fixtures/KohaFixtures.php';
+
+
 abstract class RechercheControllerReservationTestCase
   extends AbstractControllerTestCase {
 
@@ -28,8 +32,20 @@ abstract class RechercheControllerReservationTestCase
   public function setUp(): void   {
     parent::setUp();
 
-    Class_CosmoVar::setValueOf('site_retrait_resa',
-                               1);
+    Class_CosmoVar::setValueOf('site_retrait_resa', Class_CosmoVar::PICKUP_LOCATION_CHOICE);
+
+    $pierre =
+      $this->fixture(Class_Users::class,
+                     ['id' => 789789,
+                      'login' => 'Pierre',
+                      'password' => 'secret!@',
+                      'idabon' => 789789,
+                      'id_sigb' => 8798,
+                      'id_site' => 12,
+                      'id_int_bib' => 12,
+                      'role_level' => 2]);
+
+    ZendAfi_Auth::getInstance()->logUser($pierre);
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 2,
@@ -43,13 +59,46 @@ abstract class RechercheControllerReservationTestCase
                     'id_origine' => '37',
                     'libelle' => 'Cran']);
 
+    $this->fixture(Class_Notice::class,
+                   ['id' => 124,
+                    'titre' => 'Aimer'
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 12,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://valensol.net',
+                                      'provides_pickup_locations' => '1']]);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 13425,
+                    'id_origine' => 12,
+                    'id_int_bib' => 12,
+                    'notice_id' => 124,
+                    'code_barres' => '1242',
+                    'annexe' => 2
+                   ]);
+
+    Class_HttpClientFactory::forTest();
+
     $this->_xpath = new Storm_Test_XPath;
   }
+
+
+  /** @test @see http://forge.afi-sa.fr/issues/90710 */
+  public function nanookShouldHaveBeenCalled() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls( $this);
+  }
 }
 
 
 
 
+
+
+
 abstract class RechercheControllerReservationWithPickupChoiceTestCase
   extends RechercheControllerReservationTestCase {
 
@@ -65,13 +114,21 @@ abstract class RechercheControllerReservationWithPickupChoiceTestCase
 
 
 class RechercheControllerReservationPickupAjaxActionWithChosenPickupTest
-  extends RechercheControllerReservationWithPickupChoiceTestCase {
+  extends RechercheControllerReservationTestCase {
 
   public function setUp(): void   {
     parent::setUp();
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=36');
-
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran());
+
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=36&copy_id=13425');
     $this->_json = json_decode($this->_response->getBody());
   }
 
@@ -101,14 +158,26 @@ class RechercheControllerReservationPickupAjaxActionWithChosenPickupTest
 
 
 class RechercheControllerReservationPickupAjaxActionPostTest
-  extends RechercheControllerReservationWithPickupChoiceTestCase {
+  extends RechercheControllerReservationTestCase {
+
+
 
   protected $_response;
   /** @test */
   public function responseShouldRedirectToReservationajaxAction() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran());
+
     $this->postDispatch('recherche/reservation-pickup-ajax',
                         ['id_bib' => 2,
                          'id_origine' => 12,
+                         'copy_id' => 13425,
                          'code_annexe' => '36']);
 
     $this->assertRedirectTo('/recherche/reservationajax/code_annexe/36');
@@ -117,10 +186,20 @@ class RechercheControllerReservationPickupAjaxActionPostTest
 
   /** @test */
   public function withoutAvailablePickupLocationPostResponseShouldAnswerError() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran());
+
     Class_CodifAnnexe::deleteBy([]);
     $this->postDispatch('recherche/reservation-pickup-ajax',
                         ['id_bib' => 2,
                          'id_origine' => 12,
+                         'copy_id' => 13425,
                          'code_annexe' => '36']);
     $json = json_decode($this->_response->getBody(), true);
     $this->assertContains('Aucun site de retrait disponible', $json['content']);
@@ -129,8 +208,17 @@ class RechercheControllerReservationPickupAjaxActionPostTest
 
   /** @test */
   public function withoutAvailablePickupLocationGETResponseShouldAnswerError() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran());
+
     Class_CodifAnnexe::deleteBy([]);
-    $this->dispatch('recherche/reservation-pickup-ajax/id_bib/2/id_origine/12');
+    $this->dispatch('recherche/reservation-pickup-ajax/id_bib/2/id_origine/12/copy_id/13425');
     $json = json_decode($this->_response->getBody(), true);
     $this->assertContains('Aucun site de retrait disponible', $json['content']);
   }
@@ -138,11 +226,19 @@ class RechercheControllerReservationPickupAjaxActionPostTest
 
   /** @test */
   public function withPopupAndInspectorGadgetResponseShouldStillSendJSON() {
-    Zend_Registry::get('session')->inspectorCalls = [serialize(new Class_InspectorGadget_ServiceCall('I', 'love', 'milk', 'coffee', '418'))];
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran());
 
     $this->postDispatch('recherche/reservation-pickup-ajax/render/popup/inspector_gadget/1',
                         ['id_bib' => 2,
                          'id_origine' => 12,
+                         'copy_id' => 13425,
                          'code_annexe' => '36']);
     $this->assertNotNull($json = json_decode($this->_response->getBody(), true));
     $this->assertContains('<script>location.reload();</script>', $json['content']);
@@ -151,9 +247,19 @@ class RechercheControllerReservationPickupAjaxActionPostTest
 
   /** @test */
   public function responseShouldContainsJsonRedirectToReservationajaxAction() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran());
+
     $this->postDispatch('recherche/reservation-pickup-ajax/render/popup',
                         ['id_bib' => 2,
                          'id_origine' => 12,
+                         'copy_id' => 13425,
                          'code_annexe' => '36']);
     $json = json_decode($this->_response->getBody(), true);
     $this->assertContains('<script>location.reload();</script>', $json['content']);
@@ -164,7 +270,7 @@ class RechercheControllerReservationPickupAjaxActionPostTest
 
 
 class RechercheControllerReservationPickupAjaxActionTestWithChosenPickupDispatch
-  extends RechercheControllerReservationWithPickupChoiceTestCase {
+  extends RechercheControllerReservationTestCase {
 
   public $jajm;
   public $nanook;
@@ -173,47 +279,27 @@ class RechercheControllerReservationPickupAjaxActionTestWithChosenPickupDispatch
   public function setUp(): void   {
     parent::setUp();
 
-    $bib = $this
-      ->fixture(Class_IntBib::class,
-                ['id' => 1,
-                 'comm_sigb' => Class_IntBib::COM_NANOOK,
-                 'comm_params' => ['url_serveur' => 'http://bib.valensol.net']]);
-
-    $this->jajm = $this
-      ->fixture(Class_Users::class,
-                ['id' => 1,
-                 'login' => 'jajm',
-                 'password' => 'secret',
-                 'idabon' => '0000007',
-                 'int_bib' => $bib,
-                ]);
-
-    $this->nanook = (new Class_Testing_WebService_SIGB_Nanook_Service())
-      ->whenCalled('reserverExemplaire')->answers(['erreur' => '', 'statut' => true])
-      ->whenCalled('isConnected')->answers(true);
-
-    Class_WebService_SIGB_Nanook::setService(['url_serveur' => 'http://bib.valensol.net',
-                                              'id_bib' => 1,
-                                              'type' => Class_IntBib::COM_NANOOK,],
-                                             $this->nanook);
-
-    ZendAfi_Auth::getInstance()->logUser($this->jajm);
-
-    $this->fixture('Class_Exemplaire', ['id' => 12]);
-
     $mock_transport = new MockMailTransport();
     Zend_Mail::setDefaultTransport($mock_transport);
 
-    $this->dispatch('recherche/reservationajax/id/11760/id_int_bib/23/id_bib/23/id_origine/594105/code_annexe/23/render/popup/copy_id/12');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/HoldTitle/bibId/12/patronId/8798/pickupLocation/36',
+                               NanookFixtures::xmlHoldTitleSuccess());
+
+    $this->dispatch('recherche/reservationajax/id/11760/id_int_bib/12/id_bib/12/id_origine/12/code_annexe/36/render/popup/copy_id/13425');
 
+    $this->_json = json_decode($this->_response->getBody());
     $this->_sent_mails = $mock_transport->getSentMails();
   }
 
 
   /** @test */
-  public function parameterCodeAnnexeShouldBeUsedForReservation() {
-    $this->assertEquals('23',
-                        $this->nanook->getAttributesForLastCallOn('reserverExemplaire')[2]);
+  public function responseShouldContaisSuccessHeldMessage() {
+    $this->assertEquals('Votre réservation est enregistrée.<br>Nous vous informerons quand le document sera disponible',
+                        $this->_json->content);
   }
 
 
@@ -235,46 +321,25 @@ class RechercheControllerReservationPickupAjaxActionTestWithPatronLibraryPickup
   public function setUp(): void
   {
     parent::setUp();
-
-    $bib = $this
-      ->fixture(Class_IntBib::class,
-                ['id' => 12,
-                 'comm_sigb' => Class_IntBib::COM_NANOOK,
-                 'comm_params' => ['url_serveur' => 'http://bib.valensol.net']]);
-
-    $this->jajm = $this->fixture(Class_Users::class,
-                                 ['id' => 1,
-                                  'login' => 'jajm',
-                                  'password' => 'secret',
-                                  'idabon' => '0000007',
-                                  'int_bib' => $bib]);
-
-    $this->nanook = (new Class_Testing_WebService_SIGB_Nanook_Service)
-      ->whenCalled('isConnected')->answers(true)
-      ->whenCalled('providesPickupLocations')->answers(false)
-      ->whenCalled('getUserAnnexe')->answers('12');
-
-    Class_WebService_SIGB_Nanook::setService(['url_serveur' => 'http://bib.valensol.net',
-                                              'id_bib' => 12,
-                                              'type' => Class_IntBib::COM_NANOOK,],
-                                             $this->nanook);
-
-    ZendAfi_Auth::getInstance()->logUser($this->jajm);
-
     Class_CosmoVar::setValueOf('site_retrait_resa',
                                Class_CosmoVar::PICKUP_LOCATION_PATRON);
 
-    $this->fixture(Class_Exemplaire::class,
-                   ['id' => 12]);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/HoldTitle/bibId/12/patronId/8798/pickupLocation/12',
+                               NanookFixtures::xmlHoldTitleSuccess());
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=36&copy_id=12');
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=36&copy_id=13425');
+    $this->_json = json_decode($this->_response->getBody());
   }
 
 
   /** @test */
-  public function getUserAnnexeResultShouldBeUsedForReservation() {
-    $this->assertEquals('12',
-                        $this->nanook->getAttributesForLastCallOn('reserverExemplaire')[2]);
+  public function responseShouldContaisSuccessHeldMessage() {
+    $this->assertEquals('Votre réservation est enregistrée.<br>Nous vous informerons quand le document sera disponible',
+        $this->_json->content);
   }
 }
 
@@ -290,46 +355,34 @@ class RechercheControllerReservationPickupAjaxActionTestWithItemLibraryPickup
   public function setUp(): void   {
     parent::setUp();
 
-    $bib = $this->fixture(Class_IntBib::class,
-                          ['id' => 1,
-                           'comm_sigb' => Class_IntBib::COM_NANOOK,
-                           'comm_params' => ['url_serveur' => 'http://bib.valensol.net']]);
-
-    $this->jajm = $this->fixture(Class_Users::class,
-                                 ['id' => 1,
-                                  'login' => 'jajm',
-                                  'password' => 'secret',
-                                  'idabon' => '0000007',
-                                  'int_bib' => $bib
-                                 ]);
-
-    $this->nanook = (new Class_Testing_WebService_SIGB_Nanook_Service())
-      ->whenCalled('isConnected')->answers(true)
-      ->whenCalled('providesPickupLocations')->answers(false)
-      ->whenCalled('reserverExemplaire')->answers(['statut' => true, 'erreur' => '']);
-
-    Class_WebService_SIGB_Nanook::setService(['url_serveur' => 'http://bib.valensol.net',
-                                              'id_bib' => 1,
-                                              'type' => Class_IntBib::COM_NANOOK,],
-                                             $this->nanook);
-
-    ZendAfi_Auth::getInstance()->logUser($this->jajm);
-
     Class_CosmoVar::setValueOf('site_retrait_resa',
                                Class_CosmoVar::PICKUP_LOCATION_ITEM);
 
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/HoldTitle/bibId/12/patronId/8798/pickupLocation/36',
+                               NanookFixtures::xmlHoldTitleSuccess());
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 36,
+                    'code' => '36',
+                    'id_origine' => '36']);
+
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 12,
                     'annexe' => 36]);
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=36&copy_id=12');
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=36&copy_id=13425');
+    $this->_json = json_decode($this->_response->getBody());
   }
 
 
   /** @test */
-  public function itemAnnexeShouldBeUsedForReservation() {
-    $this->assertEquals('36',
-                        $this->nanook->getAttributesForLastCallOn('reserverExemplaire')[2]);
+  public function responseShouldContaisSuccessHeldMessage() {
+    $this->assertEquals('Votre réservation est enregistrée.<br>Nous vous informerons quand le document sera disponible',
+        $this->_json->content);
   }
 }
 
@@ -442,173 +495,166 @@ class RechercheControllerReservationWithWebServiceKohaTest
   {
     parent::setUp();
 
-    $webservice = 'http://bib.valensol.net';
-    $bib = $this->fixture(Class_IntBib::class,
-                          ['id' => 1,
-                           'comm_sigb' => Class_IntBib::COM_KOHA_LEGACY,
-                           'comm_params' => ['url_serveur' => $webservice]]);
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 1,
+                    'comm_sigb' => Class_IntBib::COM_KOHA_LEGACY,
+                    'comm_params' => ['url_serveur' => 'http://bib.valensol.net']]);
+
+    $jajm =
+      $this->fixture(Class_Users::class,
+                     ['id' => 1,
+                      'login' => 'jajm',
+                      'password' => 'secret',
+                      'id_int_bib' => 1,
+                      'id_site' => 1,
+                      'role_level' => 2,
+                      'idabon' => 12890,
+                      'id_sigb' => 7891738
+                     ]);
+
+    ZendAfi_Auth::getInstance()->logUser($jajm);
 
-    $this->jajm = $this->fixture(Class_Users::class,
-                                 ['id' => 1,
-                                  'login' => 'jajm',
-                                  'password' => 'secret',
-                                  'int_bib' => $bib,
-                                 ]);
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 123,
+                    'code' => 'VS',
+                    'libelle' => 'Valensole',
+                    'id_int_bib' => 1,
+                    'id_origine' => 'VS']);
 
-    ZendAfi_Auth::getInstance()->logUser($this->jajm);
-    $this->koha = new Class_Testing_WebService_SIGB_KohaLegacy_Service();
-    $this->koha->setServerRoot( $webservice);
+    $koha_profile = Class_IntProfilDonnees::forKoha();
+    $koha_profile->save();
 
-    Class_WebService_SIGB_KohaLegacy::setService(['url_serveur' => $webservice,
-                                                  'id_bib' => 1,
-                                                  'type' => Class_IntBib::COM_KOHA_LEGACY],
-                                                 $this->koha);
-  }
+    $this->fixture(Class_Notice::class,
+                   ['id' => 54,
+                    'type_doc' => Class_Typedoc::LIVRE,
+                    'titre_principal' => 'Robinson Crusoe',
+                    'auteur_principal' => 'Daniel Dafoe']);
 
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 456,
+                    'code_barres' => 'MALLE00010',
+                    'id_origine' => 789,
+                    'id_notice' => 54,
+                    'id_origine' => 32468,
+                    'id_int_bib' => 1,
+                    'id_data_profile' => $koha_profile->getId(),
+                    'annexe' => 123])
+         ->setZone995(serialize([['code' => '9',
+                                  'valeur' => '446297']]));
 
-  protected function dispatchAndCheckContentEquals($expected) {
-    $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
-    $this->json = json_decode($this->_response->getBody());
-    $this->assertEquals($expected, $this->json->content);
+    Class_HttpClientFactory::forTest();
   }
 
 
   /** @test */
-  public function reservationAjaxWithItemRequiringCalendarHoldShouldRedirectToReservationCalendarAjax() {
-    $item = $this->fixture(Class_Exemplaire::class,
-                           ['id' => 456,
-                            'code_barres' => 123,
-                            'id_int_bib' => 1,
-                            'annexe' => 'VS',
-                            'notice' => $this->fixture(Class_Notice::class,
-                                                       ['id' => 8890,
-                                                        'titre_principal' => 'Elementaire mon cher polar',
-                                                        'auteur_principal' => 'Conan Doyle'])]);
-
-    $item->setSigbExemplaire((Class_WebService_SIGB_Koha_Exemplaire::newInstance())
-                             ->doRequiresCalendarHold()
-                             ->setId(2)
-                             ->beReservable())->save();
-
-    $this->jajm->setIdabon(395749);
+  public function reservationAjaxWithItemRequiringCalendarHoldShouldRedirectToReservationCalendarAjax()
+  {
+    Class_IntBib::find(1)
+      ->setCommParams(['url_serveur' => 'http://bib.valensol.net',
+                       'grouped_holds_itypes' => 'pretnormal'])
+      ->assertSave();
 
-    $this->koha
-      ->whenCalledDo('reserverExemplaire', [], fn() => $this->_error('error'));
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1&pickup_location=VS',
+                               KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetPatronInfo&patron_id=7891738&show_contact=1&show_loans=0&show_holds=1',
+                               kohaFixtures::xmlGetPatronInfoJamesBond())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetRecords&id=32468',
+                               KohaFixtures::xmlGetRecordLeChatLePlusMignonDuMonde());
 
     $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
-
     $this->assertRedirectTo('/recherche/reservation-calendar-ajax/id_bib/1/copy_id/456/code_annexe/VS');
   }
 
 
   /** @test */
   public function withMatchingHoldMessageShouldContainsPickupLocation() {
-    $item = $this->fixture(Class_Exemplaire::class,
-                           ['id' => 456,
-                            'code_barres' => 123,
-                            'id_int_bib' => 1]);
-
-    $response = $this->_prepareEmprunteurHolding($item, 'Valensole');
-    $expected_message = 'Votre réservation est enregistrée.<br>Nous vous informerons quand le document sera disponible pour être retiré à : Valensole';
-    $this->_dispatchWithEmprunteurAndAssertContentEquals($response,
-                                                         $expected_message);
-  }
-
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetRecords&id=32468',
+                               KohaFixtures::xmlGetRecordLeChatLePlusMignonDuMonde())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1',
+                               KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1&pickup_location=VS',
+                               KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetPatronInfo&patron_id=7891738&show_contact=1&show_loans=0&show_holds=1',
+                               kohaFixtures::xmlGetPatronInfoJamesBond());
 
-  /** @test */
-  public function withMatchingHoldAndRecordWithBRInTitleMessageShouldContainsPickupLocationAndRecordTitleAuthor() {
-    $exemplaire = $this->fixture(Class_Exemplaire::class,
-                                 ['id' => 456,
-                                  'code_barres' => 123,
-                                  'id_int_bib' => 1,
-                                  'notice' => $this->fixture(Class_Notice::class,
-                                                             ['id' => 8890,
-                                                              'titre_principal' => 'Albator <br /> Arcadia',
-                                                              'auteur_principal' => 'Matsumoto'])]);
-    $this->_dispatchWithEmprunteurAndAssertContentEquals(
-                                                         $this->_prepareEmprunteurHolding($exemplaire, 'Valensole'),
-                                                         'Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'Albator  Arcadia / Matsumoto\' sera disponible pour être retiré à : Valensole');
+    $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
+    $this->json = json_decode($this->_response->getBody());
+    $this->assertEquals('Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'Robinson Crusoe / Daniel Dafoe\' sera disponible pour être retiré à : Valensole',
+                        $this->json->content);
   }
 
 
   /** @test */
   public function withMatchingHoldAndRecordWithoutAuthorMessageShouldContainsPickupLocationAndRecordTitleOnly() {
-    $exemplaire = $this->fixture(Class_Exemplaire::class,
-                                 ['id' => 456,
-                                  'code_barres' => 123,
-                                  'id_int_bib' => 1,
-                                  'notice' => $this->fixture(Class_Notice::class,
-                                                             ['id' => 8890,
-                                                              'titre_principal' => 'Arcadia'])]);
-    $this->_dispatchWithEmprunteurAndAssertContentEquals(
-                                                         $this->_prepareEmprunteurHolding($exemplaire, 'Valensole'),
-                                                         'Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'Arcadia\' sera disponible pour être retiré à : Valensole');
-  }
+    Class_Notice::find(54)->setAuteurPrincipal('');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetRecords&id=32468',
+                               KohaFixtures::xmlGetRecordLeChatLePlusMignonDuMonde())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1&pickup_location=VS',
+                               KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1',
+                               KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetPatronInfo&patron_id=7891738&show_contact=1&show_loans=0&show_holds=1',
+                               kohaFixtures::xmlGetPatronInfoJamesBond());
 
+    $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
 
-  /** @test */
-  public function withoutMatchingHoldMessageShouldContainsDocumentNotFound() {
-    $this->_dispatchWithEmprunteurAndAssertContentEquals(
-                                                         $this->_prepareEmprunteurHolding(null, 'Valensole'),
-                                                         'Document introuvable');
+    $this->json = json_decode($this->_response->getBody());
+    $this->assertEquals('Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'Robinson Crusoe\' sera disponible pour être retiré à : Valensole',
+                        $this->json->content);
   }
 
 
   /** @test */
-  public function withMatchingHoldAndNoPickupLocationShouldNotContainsPickupLocation() {
-    $this->_dispatchWithEmprunteurAndAssertContentEquals($this->_prepareEmprunteurHolding($this->fixture(Class_Exemplaire::class,
-                                                                                                         ['id' => 456]),
-                                                                                          null),
-                                                         'Votre réservation est enregistrée.<br>Nous vous informerons quand le document sera disponible');
-  }
-
-
-  protected function _prepareEmprunteurHolding($item, $pickup_label) {
-    $this->jajm->setIdabon(395749);
-
-    $exemplaire = (new Class_WebService_SIGB_Exemplaire(2))
-      ->setExemplaireOPAC($item);
-    $hold = new Class_WebService_SIGB_Reservation(2, $exemplaire);
-    $hold->setPickupLocationLabel($pickup_label)
-         ->setCodeBarre(123);
-
-    $emprunteur_jajm = Class_WebService_SIGB_Emprunteur::newInstance(2, 'jajm');
-    $emprunteur_jajm->reservationsAdd($hold);
-    return $emprunteur_jajm;
-  }
-
+  public function withoutMatchingHoldMessageShouldContainsDocumentNotFound() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetRecords&id=32468',
+                               KohaFixtures::xmlGetRecordLeChatLePlusMignonDuMonde())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1',
+                               KohaFixtures::holdTitleResponseError())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=HoldTitle&patron_id=7891738&bib_id=32468&request_location=127.0.0.1&pickup_location=VS',
+                               KohaFixtures::holdTitleResponseError());
 
-  protected function _dispatchWithEmprunteurAndAssertContentEquals($emprunteur, $content) {
-    $this->koha
-      ->whenCalledDefaultAnswers('reserverExemplaire', ['statut' => true,
-                                                        'erreur' => '',
-                                                        'popup' => false])
-      ->whenCalledDefaultAnswers('getEmprunteur',$emprunteur);
+    $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
+    $this->json = json_decode($this->_response->getBody());
 
-    $this->dispatchAndCheckContentEquals($content);
+    $this->assertEquals('Réservation impossible',
+                        $this->json->content);
   }
 
 
   /** @test */
   public function popupResultContentWithWebServiceErrorShouldContainsErreurDeCommunication() {
-    $this->jajm->setIdabon(395749);
+    Class_IntBib::find(1)
+      ->clearInstanceCache()
+      ->setCommSigb('n\'importe quoi');
 
-    $this->_prepareEmprunteurHolding($this->fixture(Class_Exemplaire::class,
-                                                    ['id' => 456]),
-                                     'Valensole');
-    $this->koha->whenCalled('isConnected')->answers(false);
-
-    $this->dispatchAndCheckContentEquals("Une erreur de communication avec le serveur a fait échouer la requête. Merci de signaler ce problème à la bibliothèque.");
+    $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
+    $this->json = json_decode($this->_response->getBody());
+    $this->assertEquals('Impossible de contacter le serveur de votre bibliothèque.',
+                        $this->json->content);
   }
 
 
   /** @test */
   public function withoutIdAbonpopupResultContentShouldContainsVousDevezVousConnecterSousVotreNumeroDeCarte() {
-    $this->_prepareEmprunteurHolding($this->fixture(Class_Exemplaire::class,
-                                                    ['id' => 456]),
-                                     'Valensole');
-    $this->jajm->setIdabon(null)->save();
-    $this->dispatchAndCheckContentEquals("Vous devez vous connecter sous votre numéro de carte pour effectuer une réservation.");
+    Class_Users::find(1)->setIdabon('')->save();
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetRecords&id=32468',
+                               KohaFixtures::xmlGetRecordLeChatLePlusMignonDuMonde());
+
+    $this->dispatch('/recherche/reservationajax/id_bib/1/copy_id/456/code_annexe/VS');
+    $this->json = json_decode($this->_response->getBody());
+    $this->assertEquals('Vous devez vous connecter sous votre numéro de carte pour effectuer une réservation.',
+                        $this->json->content);
   }
 }
 
@@ -1151,8 +1197,10 @@ class RechercheControllerReservationWithMailFormValidPostTest
 
 
 
-abstract class RechercheControllerReservationPickupAjaxWithNanookPickupLocationsTestCase
-  extends RechercheControllerReservationTestCase{
+
+class RechercheControllerReservationPickupAjaxNanookPickupLocationsItemLibraryHoldTest
+  extends RechercheControllerReservationTestCase
+{
 
   public $jajm;
   public $nanook;
@@ -1161,54 +1209,35 @@ abstract class RechercheControllerReservationPickupAjaxWithNanookPickupLocations
   {
     parent::setUp();
 
-    $bib = $this
-      ->fixture(Class_IntBib::class,
-                ['id' => 12,
-                 'comm_sigb' => Class_IntBib::COM_NANOOK,
-                 'comm_params' => ['url_serveur' => 'http://bib.valensol.net']]);
+    Class_CosmoVar::setValueOf('site_retrait_resa', Class_CosmoVar::PICKUP_LOCATION_PATRON);
 
-    $this->jajm = $this->fixture(Class_Users::class,
-                                 ['id' => 1,
-                                  'login' => 'jajm',
-                                  'password' => 'secret',
-                                  'idabon' => '0000007',
-                                  'int_bib' => $bib]);
-
-    $this->nanook =(new Class_Testing_WebService_SIGB_Nanook_Service())
-      ->whenCalled('isConnected')->answers(true)
-      ->whenCalled('providesPickupLocations')->answers(true)
-
-      ->whenCalled('pickupLocationsFor')
-      ->answers(['36' => 'Annecy',
-                 '37' => 'Cran',
-                 '45' => 'Istres',
-                 '99' => 'Lunel <strong>(Disponible)</strong>']);
-
-    Class_WebService_SIGB_Nanook::setService(['url_serveur' => 'http://bib.valensol.net',
-                                              'id_bib' => 12,
-                                              'type' => Class_IntBib::COM_NANOOK,],
-                                             $this->nanook);
+    $this->fixture(Class_Notice::class,
+                   ['id' => 7,
+                    'type_doc' => Class_Typedoc::LIVRE,
+                    'titre_principal' => 'Robinson Crusoe',
+                    'auteur_principal' => 'Daniel Dafoe']);
 
-    ZendAfi_Auth::getInstance()->logUser($this->jajm);
+    Class_Exemplaire::find('13425')
+      ->setIdOrigine('7307')
+      ->setIdNotice(7)
+      ->assertSave();
 
-    $this->fixture(Class_Exemplaire::class,
-                   ['id' => 12]);
-  }
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/HoldTitle/bibId/7307/patronId/8798/pickupLocation/12',
+                               NanookFixtures::xmlHoldTitleSuccess())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::xmlGetPatronChristelDelpeyroux());
 
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=36&copy_id=13425');
 
-  public function choices() {
-    return [[36], [37], [45]];
+    $this->_json = json_decode($this->_response->getBody());
   }
 
 
-  /**
-   * @test
-   * @dataProvider choices
-   */
-  public function choiceShouldBePresent($location_id) {
-    $this->_xpath
-      ->assertXPath($this->_json->content,
-                    '//input[@type="radio"][@value="' . $location_id . '"]');
+  /** @test */
+  public function jsonReturnedVotreReservationEstEnregistree() {
+    $this->assertEquals('Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'Robinson Crusoe / Daniel Dafoe\' sera disponible pour être retiré à : Vanne', $this->_json->content);
   }
 }
 
@@ -1216,7 +1245,7 @@ abstract class RechercheControllerReservationPickupAjaxWithNanookPickupLocations
 
 
 class RechercheControllerReservationPickupAjaxNanookPickupLocationsPrecheckPatronTest
-  extends RechercheControllerReservationPickupAjaxWithNanookPickupLocationsTestCase {
+  extends RechercheControllerReservationTestCase {
 
   public $nanook;
 
@@ -1224,12 +1253,28 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsPrecheckPatro
   {
     parent::setUp();
 
-    $this->nanook->whenCalled('getUserAnnexe')->answers('45')
-                 ->whenCalled('reserverExemplaire')->answers(['status' => true,
-                                                              'erreur' => '']);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel());
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 5,
+                    'code' => '45',
+                    'id_origine' => '45',
+                    'libelle' => 'Istres']);
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=37&copy_id=12');
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 45,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://valensol.net',
+                                      'provides_pickup_locations' => '1']]);
 
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=45&copy_id=13425');
     $this->_json = json_decode($this->_response->getBody());
   }
 
@@ -1245,26 +1290,48 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsPrecheckPatro
 
 
 class RechercheControllerReservationPickupAjaxNanookPickupLocationsPrecheckRequestedTest
-  extends RechercheControllerReservationPickupAjaxWithNanookPickupLocationsTestCase {
+  extends RechercheControllerReservationTestCase
+{
 
-  public $nanook;
-
-  public function setUp(): void
+  public function setUp() : void
   {
     parent::setUp();
 
-    $this->nanook->whenCalled('getUserAnnexe')->answers('66');
+    Class_CosmoVar::setValueOf('site_retrait_resa', Class_CosmoVar::PICKUP_LOCATION_CHOICE);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel());
+
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=36&copy_id=13425');
+    $this->_json = json_decode($this->_response->getBody());
+  }
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=37&copy_id=12');
 
-    $this->_json = json_decode($this->_response->getBody());
+  public function choices() {
+    return [[36], [37], [45], [99]];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider choices
+   */
+  public function choiceShouldBePresent($location_id) {
+    $this->_xpath
+      ->assertXPath($this->_json->content,
+                    '//input[@type="radio"][@value="' . $location_id . '"]');
   }
 
 
   /** @test */
   public function cranShouldBeChecked() {
     $this->_xpath
-      ->assertXPath($this->_json->content, '//input[@type="radio"][@value="37"][@checked]');
+      ->assertXPath($this->_json->content, '//input[@type="radio"][@value="36"][@checked]');
   }
 }
 
@@ -1272,7 +1339,7 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsPrecheckReque
 
 
 class RechercheControllerReservationPickupAjaxNanookPickupLocationsNoPrecheckTest
-  extends RechercheControllerReservationPickupAjaxWithNanookPickupLocationsTestCase {
+  extends RechercheControllerReservationTestCase {
 
   public $nanook;
 
@@ -1280,9 +1347,16 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsNoPrecheckTes
   {
     parent::setUp();
 
-    $this->nanook->whenCalled('getUserAnnexe')->answers('66');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel());
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=77&copy_id=12');
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=77&copy_id=13425');
 
     $this->_json = json_decode($this->_response->getBody());
   }
@@ -1302,7 +1376,7 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsNoPrecheckTes
 
 
 class RechercheControllerReservationPickupAjaxNanookPickupLocationsStrongInLabel
-  extends RechercheControllerReservationPickupAjaxWithNanookPickupLocationsTestCase {
+  extends RechercheControllerReservationTestCase {
 
   public $nanook;
 
@@ -1310,9 +1384,22 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsStrongInLabel
   {
     parent::setUp();
 
-    $this->nanook->whenCalled('getUserAnnexe')->answers('66');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::axelPatronInfo())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPickupLocation/bibId/12/patronId/8798/siteId/36',
+                               NanookFixtures::pickupLocationsAnswersAnnecyAndCranIstresLunel());
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 5,
+                    'code' => '99',
+                    'id_origine' => '99',
+                    'libelle' => 'Lunel']);
 
-    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=2&id_origine=12&code_annexe=99&copy_id=12');
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=99&copy_id=13425');
 
     $this->_json = json_decode($this->_response->getBody());
   }
@@ -1322,9 +1409,54 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsStrongInLabel
    * @see https://forge.afi-sa.net/issues/94332
    * @test
    */
-  public function LunelShouldContainStrong() {
-    $this->_xpath
-      ->assertXPath($this->_json->content, '//strong',$this->_json->content);
+  public function formShouldContainsStrongLunel() {
+    $this->assertContains('Lunel <strong>(Disponible)</strong>',
+                          $this->_json->content);
+  }
+}
+
+
+
+class RechercheControllerReservationPickupAjaxLocationsByPatronStrongInLocationLabel
+  extends RechercheControllerReservationTestCase {
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    Class_CosmoVar::setValueOf('site_retrait_resa', Class_CosmoVar::PICKUP_LOCATION_PATRON);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 7,
+                    'type_doc' => Class_Typedoc::LIVRE,
+                    'titre_principal' => 'Robinson Crusoe',
+                    'auteur_principal' => 'Daniel Dafoe']);
+
+    Class_Exemplaire::find('13425')
+      ->setIdOrigine('12501')
+      ->setIdNotice(7)
+      ->assertSave();
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('http://valensol.net:80/service/HoldTitle/bibId/12501/patronId/8798/pickupLocation/12',
+                               NanookFixtures::xmlHoldTitleSuccess())
+      ->addRequestWithResponse('http://valensol.net:80/service/GetPatronInfo/patronId/8798',
+                               NanookFixtures::xmlGetPatronChristelDelpeyroux());
+
+    $this->dispatch('recherche/reservation-pickup-ajax?id_bib=12&id_origine=12&code_annexe=37&copy_id=13425');
+
+    $this->_json = json_decode($this->_response->getBody());
+  }
+
+
+  /**
+   * @see https://forge.afi-sa.net/issues/94332
+   * @test
+   */
+  public function formShouldContainsStrongLunel() {
+    $this->assertEquals($this->_json->content,
+                        'Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'Robinson Crusoe / Daniel Dafoe\' sera disponible pour être retiré à : Lunel <strong>(Disponible)</strong>');
   }
 }
 
@@ -1332,7 +1464,7 @@ class RechercheControllerReservationPickupAjaxNanookPickupLocationsStrongInLabel
 
 
 abstract class RechercheControllerReservationPickupAjaxAndNotifyByMailEnabledTestCase
-  extends RechercheControllerReservationWithPickupChoiceTestCase {
+  extends RechercheControllerReservationTestCase {
 
   public $jajm;
   public $mock;
@@ -1363,6 +1495,7 @@ abstract class RechercheControllerReservationPickupAjaxAndNotifyByMailEnabledTes
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 34,
+                    'id_bib' => 1,
                     'libelle'=>'BDM',
                     'id_origine' => 23]);
 
@@ -1390,6 +1523,7 @@ abstract class RechercheControllerReservationPickupAjaxAndNotifyByMailEnabledTes
                    <title>Mireille l\'abeille</title>
                    <pickup_location>Bibliothèque Départementale de la Meuse</pickup_location>
                  </HoldTitle>')
+      ->whenCalled('getServerRoot')->answers('http://bib.valensol.net/cgi-bin/koha/ilsdi.pl')
       ->whenCalled('reserverExemplaire')->answers(['statut'=>true,'erreur'=>'']);
 
     Class_WebService_SIGB_KohaLegacy::setService(['url_serveur' => 'http://bib.valensol.net/cgi-bin/koha/ilsdi.pl',
@@ -1401,9 +1535,10 @@ abstract class RechercheControllerReservationPickupAjaxAndNotifyByMailEnabledTes
 
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 12,
+                    'id_bib' => 1,
                     'id_origine' =>13,
                     'code_barres' =>12341,
-                    'id_bib' => 1,
+                    'int_bib' => $this->_intbib,
                     'notice'=> $this
                     ->fixture(Class_Notice::class,
                               ['id'=>54,
@@ -1547,18 +1682,17 @@ class RechercheControllerReservationPickupAjaxAndNotifyByMailWithMailEnabledWith
 
 
 
-require_once 'tests/fixtures/KohaFixtures.php';
-
 class RechercheControllerReservationWithWebServiceKohaAndItypesTest
   extends AbstractControllerTestCase {
 
   protected $_response;
   protected $_xpath;
+  protected $json;
 
   public function setUp(): void   {
     parent::setUp();
 
-    Class_WebService_SIGB_AbstractRESTService::shouldThrowError(true);
+    Class_CosmoVar::setValueOf('site_retrait_resa', 1);
 
     $koha_service_params = ['url_serveur' => 'http://bib.valensol.net',
                             'grouped_holds_itypes' => " PRET_MALLE
@@ -1570,31 +1704,22 @@ PRET_VALISE",
                             'id_bib' => 1,
                             'type' => 5];
 
-    $bib = $this->fixture(Class_IntBib::class,
+    $this->fixture(Class_IntBib::class,
                           ['id' => 1,
                            'comm_sigb' => Class_IntBib::COM_KOHA_LEGACY,
                            'comm_params' => $koha_service_params]);
 
-    $http_client = (new Class_Testing_WebService_SimpleWebClient)
-      ->whenCalled('open_url')
-      ->with('http://bib.valensol.net?service=GetRecords&id=678')
-      ->answers(KohaFixtures::getRecordsWithGroupHoldsItypes());
-
-    Class_WebService_SIGB_KohaLegacy::getService($koha_service_params)
-      ->setWebClient($http_client);
-
-    $record =
-      $this->fixture(Class_Notice::class,
-                     ['id' => 8890,
-                      'titre_principal' => 'Elementaire mon cher polar',
-                      'auteur_principal' => 'Conan Doyle']);
+    $this->fixture(Class_Notice::class,
+                   ['id' => 8890,
+                    'titre_principal' => 'Elementaire mon cher polar',
+                    'auteur_principal' => 'Conan Doyle']);
 
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 456,
                     'id_origine' => 678,
                     'code_barres' => 5360246264,
                     'id_int_bib' => 1,
-                    'notice' => $record]);
+                    'id_notice' => 8890]);
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 3,
@@ -1610,35 +1735,44 @@ PRET_VALISE",
                     'libelle' => 'Annecy',
                     'no_pickup' => '0']);
 
-    Class_CosmoVar::setValueOf('site_retrait_resa', 1);
+    $user =
+      $this->fixture(Class_Users::class,
+                     ['id' => 1,
+                      'login' => 'jajm',
+                      'role_level' => 2,
+                      'password' => 'secret',
+                      'id_int_bib' => 1,
+                      'id_site' => 1,
+                      'idabon' => '395749'
+                     ]);
 
-    $user = $this->fixture(Class_Users::class,
-                           ['id' => 1,
-                            'login' => 'jajm',
-                            'password' => 'secret',
-                            'int_bib' => $bib,
-                            'id_site' => 3,
-                            'idabon' => '395749'
-                           ]);
+    ZendAfi_Auth::getInstance()->logUser($user);
 
     $this->_xpath = new Storm_Test_XPath;
-    ZendAfi_Auth::getInstance()->logUser($user);
+
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetRecords&id=678',
+                               KohaFixtures::getRecordsWithGroupHoldsItypes())
+      ->addPostRequestWithResponse('http://bib.valensol.net:80',
+                                   ['service' => 'AuthenticatePatron',
+                                    'username' => 'jajm',
+                                    'password' => 'secret'],
+                                   KohaFixtures::xmlAuthenticatePatronOk())
+      ->addRequestWithResponse('http://bib.valensol.net:80?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoJamesBond());
+
   }
 
 
   /** @test */
   public function reservationShouldSelectUserAnnexeByDefault() {
-    $emprunteur = new Class_WebService_SIGB_Emprunteur('395749', 'jajm');
-    $emprunteur->setLibraryCode('VS');
-    Class_CommSigb::setInstance(( new Class_Testing_CommSigb)
-                                ->whenCalledAnswers('ficheAbonne',[]
-                                                    ,['fiche' => $emprunteur]));
-
-
     $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/1/copy_id/456/code_annexe/AN');
+
     $json = json_decode($this->_response->getBody());
-    $this->_xpath
-      ->assertXPath($json->content, '//input[@type="radio"][@name="code_annexe"][ @value="VS"][ @checked="checked"]', $json->content);
+    $this->_xpath->assertXPath($json->content,
+                               '//input[@type="radio"][@name="code_annexe"][ @value="VS"][ @checked="checked"]',
+                               $json->content);
   }
 
 
@@ -1723,6 +1857,7 @@ class RechercheControllerReservationPickupAjaxWithMailPostAction
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 45,
                     'id_bib' => 4,
+                    'id_int_bib' => 1,
                     'id_notice' => 4,
                     'code_barres' => '1234'
                    ]);
diff --git a/tests/application/modules/opac/controllers/RechercheControllerTest.php b/tests/application/modules/opac/controllers/RechercheControllerTest.php
index da2d5260af6..9ec2c8fe17d 100644
--- a/tests/application/modules/opac/controllers/RechercheControllerTest.php
+++ b/tests/application/modules/opac/controllers/RechercheControllerTest.php
@@ -2774,13 +2774,29 @@ abstract class RechercheControllerPermalinkWithIdIntBibTestCase
     Class_CosmoVar::setValueOf('mode_doublon',
                                '' . Class_CosmoVar::DOUBLE_SEARCH_NONE);
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 3,
+                    'code' => 'MPL',
+                    'libelle' => 'Montpellier',
+                    'id_origine' => 'MPL',
+                    'id_bib' => 4
+                   ]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 4,
+                    'libelle' => 'Montpellier',
+                    'annexe' => 3,
+                    'id_origine' => 'MPL'
+                   ]);
+
     Class_Notice::find(15)
       ->setType($this->_getNoticeType())
       ->setExemplaires([$this->fixture(Class_Exemplaire::class,
                                        ['id' => 999,
                                         'type' => $this->_getNoticeType(),
-                                        'id_bib' => 1,
+                                        'id_bib' => 4,
                                         'id_int_bib' => 1,
+                                        'annexe' => 3,
                                         'id_origine' => 1111])])
       ->assertSave();
 
@@ -2811,37 +2827,37 @@ class RechercheControllerPermalinkWithIdIntBibAndTypeBibliographicTest
 
   /** @test */
   public function permalinkShouldBeDisplay() {
-    $this->assertXPath('//img[contains(@class, "permalink")][contains(@data-url, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1")]');
+    $this->assertXPath('//img[contains(@class, "permalink")][contains(@data-url, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1/id_site/3/record_type/1")]');
   }
 
 
   /** @test */
   public function contextfacebookLinkShouldContainsIdIntBib() {
-    $this->assertXPath('//div[contains(@class, "reseaux-sociaux")]/img[contains(@onclick, "id_int_bib")]');
+    $this->assertXPath('//div[contains(@class, "reseaux-sociaux")]/img[contains(@onclick, "id_site")]');
   }
 
 
   /** @test */
   public function permalinkShouldNotBeDisplayedWithContextExpressionRecherche() {
-    $this->assertNotXPath('//img[contains(@class, "permalink")][contains(@data-url, "id_sigb/1111/id_int_bib/1/expressionRecherche")]');
+    $this->assertNotXPath('//img[contains(@class, "permalink")][contains(@data-url, "id_sigb/1111/id_site/3/expressionRecherche")]');
   }
 
 
   /** @test */
   public function permalinkShouldNotBeDisplayedWithIdProfil() {
-    $this->assertNotXPath('//img[contains(@class, "permalink")][contains(@data-url, "id_sigb/1111/id_int_bib/1?id_profil")]');
+    $this->assertNotXPath('//img[contains(@class, "permalink")][contains(@data-url, "id_sigb/1111/id_site/3?id_profil")]');
   }
 
 
   /** @test */
   public function othersLinkShouldNotBeDisplayedWithIdIntBib() {
-    $this->assertNotXPath('//a[contains(@href, "id_sigb/1111/id_int_bib/1")]');
+    $this->assertNotXPath('//a[contains(@href, "id_sigb/1111/id_site/3")]');
   }
 
 
   /** @test */
   public function permalinkWithIdIntBibShouldBeDisplayOnlyOnce() {
-    $this->assertXPathCount('//img[contains(@class, "permalink")][contains(@data-url, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1")]', 1);
+    $this->assertXPathCount('//img[contains(@class, "permalink")][contains(@data-url, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1/id_site/3")]', 1);
   }
 }
 
@@ -2864,13 +2880,13 @@ class RechercheControllerPermalinkWithIdIntBibAndTypeBibliographicInTemplateTest
 
   /** @test */
   public function permalinkViewNoticeWithIdSigb1111AndIdIntBib1ShouldBeDisplay() {
-    $this->assertXPath('//body[@data-template="INTONATION"]//a[contains(@class, "view_permalink permalink")][contains(@href, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1")]');
+    $this->assertXPath('//body[@data-template="INTONATION"]//a[contains(@class, "view_permalink permalink")][contains(@href, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1/id_site/3")]');
   }
 
 
   /** @test */
   public function onlyOneLinkShouldContainIdIntBibOne() {
-    $this->assertXPathCount('//a[contains(@href, "id_int_bib/1")]', 1);
+    $this->assertXPathCount('//a[contains(@href, "id_site/3")]', 1);
   }
 }
 
@@ -2901,13 +2917,13 @@ class RechercheControllerViewNoticePermalinkWithIdIntBibInTemplateTest
 
   /** @test */
   public function permalinkViewNoticeWithIdSigb1111AndIdIntBib1ShouldBeDisplay() {
-    $this->assertXPath('//body[@data-template="INTONATION"]//a[contains(@class, "view_permalink permalink")][contains(@href, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1")]');
+    $this->assertXPath('//body[@data-template="INTONATION"]//a[contains(@class, "view_permalink permalink")][contains(@href, "/recherche/viewnotice/id_sigb/1111/id_int_bib/1/id_site/3")]');
   }
 
 
   /** @test */
   public function onlyOneLinkShouldContainIdIntBibOne() {
-    $this->assertXPathCount('//a[contains(@href, "id_int_bib/1")]', 1);
+    $this->assertXPathCount('//a[contains(@href, "id_site/3")]', 1);
   }
 }
 
diff --git a/tests/application/modules/telephone/controllers/RechercheControllerHarryPotterTest.php b/tests/application/modules/telephone/controllers/RechercheControllerHarryPotterTest.php
index 7f40f776c10..8219c6978ea 100644
--- a/tests/application/modules/telephone/controllers/RechercheControllerHarryPotterTest.php
+++ b/tests/application/modules/telephone/controllers/RechercheControllerHarryPotterTest.php
@@ -418,7 +418,7 @@ class Telephone_RechercheControllerHarryPotterExemplaireReservableTest
     $item = $this->fixture(Class_Exemplaire::class,
                            ['id' => 33,
                             'cote' => 'JRROW',
-                            'annexe' => 'MOUL',
+                            'annexe' => 4,
                             'bib' => $florilege,
                             'int_bib' => $int_bib_florilege,
                             'id_int_bib' => 2,
diff --git a/tests/application/modules/telephone/controllers/RechercheControllerTest.php b/tests/application/modules/telephone/controllers/RechercheControllerTest.php
index 70661eb8435..e43498a3015 100644
--- a/tests/application/modules/telephone/controllers/RechercheControllerTest.php
+++ b/tests/application/modules/telephone/controllers/RechercheControllerTest.php
@@ -353,7 +353,9 @@ class Telephone_RechercheControllerReservationNemoInfoTest extends TelephoneAbst
                       'titre_principal' => 'Nemo']);
 
     $nemo_item_sigb = new Class_WebService_SIGB_Exemplaire(1);
-    $nemo_item_sigb->beReservable();
+    $nemo_item_sigb
+      ->setCodeAnnexe('PLOP')
+      ->beReservable();
 
     $nemo_item =
       $this->fixture(Class_Exemplaire::class,
@@ -362,7 +364,7 @@ class Telephone_RechercheControllerReservationNemoInfoTest extends TelephoneAbst
                       'bib' => $bib,
                       'int_bib' => $int_bib,
                       'cote' => 123,
-                      'annexe' => 'PLOP']);
+                      'annexe' => 1]);
 
     $nemo->setExemplaires([$nemo_item])->save();
 
diff --git a/tests/fixtures/KohaFixtures.php b/tests/fixtures/KohaFixtures.php
index 0f43a1ffa9c..6109eb29111 100644
--- a/tests/fixtures/KohaFixtures.php
+++ b/tests/fixtures/KohaFixtures.php
@@ -984,6 +984,14 @@ class KohaFixtures {
   }
 
 
+  public static function xmlAuthenticatePatronJameBondOk() {
+    return '<?xml version="1.0" encoding="UTF-8" ?>
+              <AuthenticatePatron>
+              <id>007</id>
+            </AuthenticatePatron>';
+  }
+
+
   public static function xmlAuthenticatePatronLostCard() {
     return '<?xml version="1.0" encoding="UTF-8" ?>
               <AuthenticatePatron>
@@ -4075,7 +4083,9 @@ class KohaFixtures {
         <GetPatronInfo>
           <borrowernumber>007</borrowernumber>
           <cardnumber>007</cardnumber>
+          <branchcode>VS</branchcode>
           <itype>I_WILL_CRASH_YOU</itype>
+          <branchcode>VS</branchcode>
           <holds>
               <hold>
                 <expirationdate>2019-12-31</expirationdate>
@@ -4090,12 +4100,12 @@ class KohaFixtures {
                   <title>Élémentaire mon cher polar</title>
                   <notforloan>0</notforloan>
                   <itemtype>MAL_EXPO</itemtype>
-                  <homebranch>BDY</homebranch>
-                  <holdingbranch>BDY</holdingbranch>
+                  <homebranch>VS</homebranch>
+                  <holdingbranch>VS</holdingbranch>
                   <itype>PRET_MALLE</itype>
                   <biblioitemnumber>96629</biblioitemnumber>
                 </item>
-                <branchname>BDP</branchname>
+                <branchname>VS</branchname>
                 <reserve_id>1124</reserve_id>
                 <itemnumber>112194</itemnumber>
                 <status>NEW</status>
@@ -5766,6 +5776,20 @@ class KohaFixtures {
 </HoldItem>';
   }
 
+
+  public static function holdTitleResponseWithEmptyPickupLocation() {
+    return '<?xml version="1.0" encoding="UTF-8" ?>
+<HoldItem>
+  <pickup_location></pickup_location>
+</HoldItem>';
+  }
+
+
+  public static function holdTitleResponseError() {
+    return '<?xml version="1.0" encoding="UTF-8" ?>';
+  }
+
+
   public static function xmlGetRecordLeChatLePlusMignonDuMonde() {
     return '<?xml version="1.0" encoding="UTF-8" ?>
 <GetRecords>
diff --git a/tests/fixtures/NanookFixtures.php b/tests/fixtures/NanookFixtures.php
index 142bc38226a..9f93f2f2837 100644
--- a/tests/fixtures/NanookFixtures.php
+++ b/tests/fixtures/NanookFixtures.php
@@ -338,7 +338,8 @@ class NanookFixtures {
       <itemId>7105</itemId>
       <title>Contes des quatre vents</title>
       <author>Natha Caputo</author>
-      <locationLabel>Site Principal</locationLabel>
+      <locationId>36</locationId>
+      <locationLabel>Vanne</locationLabel>
       <priority>1</priority>
       <availabilityDate>15/06/2012</availabilityDate>
     </hold>
@@ -354,7 +355,7 @@ class NanookFixtures {
       <itemId>14586</itemId>
       <title>Le Chant du lac</title>
       <author>Olympe Bhêly-Quénum</author>
-      <locationLabel>Site Principal</locationLabel>
+      <locationLabel><![CDATA[Lunel <strong>(Disponible)</strong>]]></locationLabel>
       <priority>53</priority>
       <available>1</available>
     </hold>
@@ -769,6 +770,44 @@ class NanookFixtures {
   }
 
 
+  public static function pickupLocationsOkAnswersAnnecyAndCran() {
+    return '<?xml version="1.0" encoding="UTF-8"?>
+            <pickup_locations>
+              <pickup_location>
+                <pickup_location_label>Annecy</pickup_location_label>
+                <pickup_location_id>36</pickup_location_id>
+              </pickup_location>
+              <pickup_location>
+                <pickup_location_label>Cran library</pickup_location_label>
+                <pickup_location_id>37</pickup_location_id>
+              </pickup_location>
+            </pickup_locations>';
+  }
+
+
+    public static function pickupLocationsAnswersAnnecyAndCranIstresLunel() {
+      return '<?xml version="1.0" encoding="UTF-8"?>
+            <pickup_locations>
+              <pickup_location>
+                <pickup_location_label>Annecy</pickup_location_label>
+                <pickup_location_id>36</pickup_location_id>
+              </pickup_location>
+              <pickup_location>
+                <pickup_location_label>Cran library</pickup_location_label>
+                <pickup_location_id>37</pickup_location_id>
+              </pickup_location>
+<pickup_location>
+                <pickup_location_label>Istres</pickup_location_label>
+                <pickup_location_id>45</pickup_location_id>
+              </pickup_location>
+<pickup_location>
+                <pickup_location_label><![CDATA[Lunel <strong>(Disponible)</strong>]]></pickup_location_label>
+                <pickup_location_id>99</pickup_location_id>
+              </pickup_location>
+            </pickup_locations>';
+  }
+
+
   public static function pickupLocationsErrorAnswer() {
     return '<?xml version="1.0" encoding="UTF-8"?>
             <GetPickupLocation>
diff --git a/tests/library/Class/CommSigbTest.php b/tests/library/Class/CommSigbTest.php
index a5f30afaf05..52ff8390761 100644
--- a/tests/library/Class/CommSigbTest.php
+++ b/tests/library/Class/CommSigbTest.php
@@ -20,6 +20,7 @@
  */
 
 abstract class CommSigbTestCase extends ModelTestCase {
+  protected string $_url_server = '';
 
   public $comm_sigb;
   public $florence;
@@ -158,31 +159,59 @@ abstract class CommSigbTestCase extends ModelTestCase {
   }
 
 
-  public function createMockForService($name) {
+  public function createMockForService(string $name = '') {
     $class_name = 'Class_Testing_WebService_SIGB_'.$name.'_Service';
     $this->mock_service = new  $class_name(null, '', '','');
+    $this->mock_service
+      ->whenCalled('getServerRoot')
+      ->answers($this->_url_server);
     return $this->mock_service;
   }
 
 
+  public function successStatus(): array
+  {
+    return ['statut' => 1,
+            'erreur' =>''];
+  }
+
+
+  public function prepareLibraryAndService(array $params,
+                                           int $comm_type,
+                                           string $sigb_name): void
+  {
+    Class_IntBib::getLoader()
+      ->newInstanceWithId(5)
+      ->setCommParams($params)
+      ->setCommSigb($comm_type);
+    $webservice_class = 'Class_WebService_SIGB_'.$sigb_name;
+    $webservice_class::setService(array_merge($params,
+                                              ['id_bib' => 5,
+                                               'type' => $comm_type]),
+                                  $this->createMockForService($sigb_name)
+    );
+  }
+
+
   /** @test */
   public function reserverExemplaireShouldReturnAnArrayWithStatus() {
     $this->mock_service
       ->whenCalledAnswers('reserverExemplaire',
-                     [$this->userModel,
-                      $this
-                      ->fixture(Class_Exemplaire::class,
-                                ['id' => '123', 'id_notice' => 7888]),
-                      'ABC'],
-                     ['statut' => 1,
-                      'erreur' => '']);
-
-    $this->assertEquals(['statut' => 1,
-                         'erreur' => ''],
+                          [$this->userModel,
+                           $this
+                           ->fixture(Class_Exemplaire::class,
+                                     ['id' => '123',
+                                      'id_notice' => 7888,
+                                      'id_int_bib' => 5]),
+                           'ABC'],
+                          $this->successStatus());
+
+    $this->assertEquals($this->successStatus(),
                         $this->comm_sigb
                         ->reserveItem($this
                                       ->fixture(Class_Exemplaire::class,
                                                 ['id' => '123',
+                                                 'id_int_bib' => 5,
                                                  'id_notice' => 7888]),
                                       'ABC'));
 
@@ -229,11 +258,10 @@ abstract class CommSigbTestCase extends ModelTestCase {
   /** @test */
   public function supprimerReservationShouldReturnStatutOK() {
     $this->mock_service->whenCalledAnswers('supprimerReservation',
-                                      [$this->userModel, 345],
-                                      ['statut' => 1,
-                                       'erreur' => '']);
+                                           [$this->userModel, 345],
+                                           $this->successStatus());
 
-    $this->assertEquals(['statut' => 1, 'erreur' => ''],
+    $this->assertEquals($this->successStatus(),
                         $this->comm_sigb->supprimerReservation($this->florence, 345));
     return $this->zend_cache;
   }
@@ -252,10 +280,10 @@ abstract class CommSigbTestCase extends ModelTestCase {
   public function prolongerPretShouldReturnStatutOK() {
     $this->mock_service
       ->whenCalledAnswers('prolongerPret',
-                     [$this->userModel, 456], ['statut' => 1,
-                                               'erreur' => '']);
+                          [$this->userModel, 456],
+                          $this->successStatus());
 
-    $this->assertEquals(['statut' => 1, 'erreur' => ''],
+    $this->assertEquals($this->successStatus(),
                         $this->comm_sigb->prolongerPret($this->florence, 456));
     return $this->zend_cache;
   }
@@ -281,14 +309,16 @@ class CommSigbAstrolabeOpsysTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_astro = Class_IntBib::getLoader()
-      ->newInstanceWithId(5)
-      ->setCommParams(["url_serveur" => 'http://astrolabe.com/opsys.wsdl'])
-      ->setCommSigb(2);
+    $this->_url_server = 'http://astrolabe.com/opsys.wsdl';
+    $this->prepareLibraryAndService(["url_serveur" => $this->_url_server],
+                                    2,
+                                    'Opsys');
+    $this->mock_service
+      ->setSearchClient($this->mock()
+                        ->whenCalled('getWsdlUrl')
+                        ->answers($this->_url_server));
 
-    Class_WebService_SIGB_Opsys::setService(["url_serveur" => 'http://astrolabe.com/opsys.wsdl',
-                                             'id_bib' => 5,
-                                             'type' => 2], $this->createMockForService('Opsys'));
+    $this->bib_astro = Class_IntBib::find(5);
   }
 
 
@@ -315,15 +345,13 @@ class CommSigbMoulinsVSmartTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_moulins = $this->fixture(Class_IntBib::class,
-                                        ['id' => 5,
-                                         'comm_params' => ["url_serveur" => 'http://vpn.agglo-moulins.fr/production/'],
-                                         'comm_sigb' => 4]);
+    $this->_url_server = 'http://vpn.agglo-moulins.fr/production/';
 
-    Class_WebService_SIGB_VSmart::setService(["url_serveur" => 'http://vpn.agglo-moulins.fr/production/',
-                                              'id_bib' => 5,
-                                              'type' => 4
-],$this->createMockForService('VSmart'));
+    $this->prepareLibraryAndService(["url_serveur" => $this->_url_server],
+                                    4,
+                                    'VSmart');
+
+    $this->bib_moulins = Class_IntBib::find(5);
   }
 
 
@@ -472,39 +500,36 @@ class CommSigbMoulinsVSmartTest extends CommSigbTestCase {
 
 
 class CommSigbMeuseKohaTest extends CommSigbTestCase {
-  public $bib_koha;
-  public $comm_sigb;
-  public $userModel;
-  public $mock_service;
-  protected $_service;
 
   public function setUp(): void   {
     parent::setUp();
 
-    $params = ['url_serveur' => 'http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl',
+    $this->_url_server = 'http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl';
+
+    $params = ['url_serveur' => $this->_url_server,
                'Codification_disponibilites' => "1:En prêt\r\n2:Réservable"];
 
-    $this->bib_koha = Class_IntBib::getLoader()
-      ->newInstanceWithId(5)
-      ->setCommParams(serialize($params))
-      ->setCommSigb(5);
-    $this->_service = $this->createMockForService('KohaLegacy') ;
-    Class_WebService_SIGB_KohaLegacy::setService(array_merge($params,
-                                                             ['id_bib' => 5,
-                                                              'type' => Class_IntBib::COM_KOHA_LEGACY]),
-                                                 $this->_service);
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_KOHA_LEGACY,
+                                    'KohaLegacy');
+
+    $this->bib_koha = Class_IntBib::find(5);
   }
 
 
   /** @test */
   public function reserverExemplaireWithParamsReserveAndExpirationDataShouldPassParamsToKohaService() {
-    $item = $this->fixture(Class_Exemplaire::class, ['id' => '123', 'id_notice' => 7888]);
+    $item = $this->fixture(Class_Exemplaire::class,
+                           ['id' => '123',
+                            'id_notice' => 7888,
+                            'id_int_bib' => 5]);
+
     $this->comm_sigb->reserveItem(
                                   $item,
                                   'ABC'
     );
 
-    $this->assertTrue($this->_service
+    $this->assertTrue($this->mock_service
                       ->assertMethodWith('reserverExemplaire',
                                          [$this->userModel,
                                           $item,
@@ -520,9 +545,9 @@ class CommSigbMeuseKohaTest extends CommSigbTestCase {
                             'int_bib' => $this->bib_koha]);
     $this->mock_service
       ->whenCalledAnswers('holdsForItem',
-                     [ $item],
-                     ['statut' => true,
-                      'holds' => []]);
+                          [ $item],
+                          ['statut' => true,
+                           'holds' => []]);
 
     $this->assertEquals(['statut' => true,
                          'holds' => []],
@@ -547,15 +572,16 @@ class CommSigbLocalNanookTest extends CommSigbTestCase {
   public function setUp(): void
   {
     parent::setUp();
-    $this->bib_pontault = Class_IntBib::newInstanceWithId(5)
-      ->setCommParams(["url_serveur" => 'http://192.168.2.3:9080/afi_Nanook-0.7.5/ilsdi/'])
-      ->setCommSigb(7);
 
+    $this->_url_server = 'http://192.168.2.3:9080/afi_Nanook-0.7.5/ilsdi/';
+
+    $params = ["url_serveur" => $this->_url_server];
 
-    Class_WebService_SIGB_Nanook::setService(['url_serveur' => 'http://192.168.2.3:9080/afi_Nanook-0.7.5/ilsdi/',
-                                              'id_bib' => 5,
-                                              'type' => Class_IntBib::COM_NANOOK,],
-                                             $this->createMockForService('Nanook'));
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_NANOOK,
+                                    'Nanook');
+
+    $this->bib_pontault = Class_IntBib::find(5);
   }
 
 
@@ -579,15 +605,15 @@ class CommSigbWaterbearTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_pontault = $this->fixture('Class_IntBib',
-                                         ['id' => 5,
-                                          'comm_params' => ['url_serveur' => 'http://waterbear.info/bib_ws.php'],
-                                          'comm_sigb' => Class_IntBib::COM_WATERBEAR]);
+    $this->_url_server = 'http://waterbear.info/bib_ws.php';
+
+    $params = ["url_serveur" => $this->_url_server];
 
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_WATERBEAR,
+                                    'Nanook');
 
-    Class_WebService_SIGB_Waterbear::setService( ['url_serveur' => 'http://waterbear.info/bib_ws.php',
-                                                 'id_bib' => 5,
-                                                 'type' => 14], $this->createMockForService('Waterbear'));
+    $this->bib_pontault = Class_IntBib::find(5);
   }
 
 
@@ -611,14 +637,15 @@ class CommSigbCarthameTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_ifr = Class_IntBib::getLoader()
-      ->newInstanceWithId(5)
-      ->setCommParams(["url_serveur" => 'http://ifr.ro/webservices/index.php'])
-      ->setCommSigb(6);
+    $this->_url_server = 'http://ifr.ro/webservices/index.php';
+
+    $params = ["url_serveur" => $this->_url_server];
 
-    Class_WebService_SIGB_Carthame::setService([ "url_serveur" => 'http://ifr.ro/webservices/index.php',
-                                                 'id_bib' => 5,
-                                                 'type' => 6], $this->createMockForService('Carthame'));
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_CARTHAME,
+                                    'Carthame');
+
+    $this->bib_ifr = Class_IntBib::find(5);
   }
 
 
@@ -642,21 +669,20 @@ class CommSigbOrpheeTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $comm_params = ['url_serveur' => 'http://213.144.218.252:8080/wsOrphee/service.asmx?WSDL'];
-    $this->bib_stomer = Class_IntBib::getLoader()
-      ->newInstanceWithId(5)
-      ->setCommParams($comm_params)
-      ->setCommSigb(8);
+    $this->_url_server = 'http://213.144.218.252:8080/wsOrphee/service.asmx?WSDL';
 
-    $emprunteur_florence = Class_WebService_SIGB_Orphee_Emprunteur::newInstance('0123456789', 'Florence');
+    $params = ["url_serveur" => $this->_url_server];
 
-    $this->createMockForService('Orphee')
-         ->whenCalledAnswers('getEmprunteur', [$this->userModel], $emprunteur_florence);
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_ORPHEE,
+                                    'Orphee');
 
-    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
-                                                         ['id_bib' => 5,
-                                                          'type' => Class_IntBib::COM_ORPHEE]),
-                                             $this->mock_service);
+    $this->bib_stomer = Class_IntBib::find(5);
+
+    $emprunteur_florence = Class_WebService_SIGB_Orphee_Emprunteur::newInstance('0123456789', 'Florence');
+
+    $this->mock_service
+      ->whenCalledAnswers('getEmprunteur', [$this->userModel], $emprunteur_florence);
   }
 
 
@@ -680,14 +706,15 @@ class CommSigbMicrobibTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_maze = Class_IntBib::getLoader()
-      ->newInstanceWithId(5)
-      ->setCommParams(["url_serveur" => 'http://80.11.188.93/webservices/ws_maze.wsdl'])
-      ->setCommSigb(9);
+    $this->_url_server = 'http://80.11.188.93/webservices/ws_maze.wsdl';
+
+    $params = ["url_serveur" => $this->_url_server];
 
-    Class_WebService_SIGB_Microbib::setService([ "url_serveur" => 'http://80.11.188.93/webservices/ws_maze.wsdl',
-                                                 'id_bib' => 5,
-                                                 'type' => 9], $this->createMockForService('Microbib'));
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_MICROBIB,
+                                    'Microbib');
+
+    $this->bib_maze = Class_IntBib::find(5);
   }
 
 
@@ -709,14 +736,15 @@ class CommSigbBiblixNetTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_wormhout = Class_IntBib::getLoader()
-      ->newInstanceWithId(5)
-      ->setCommParams(["url_serveur" => 'http://mediathequewormhout.biblixnet.com/exporte_afi'])
-      ->setCommSigb(10);
+    $this->_url_server = 'http://mediathequewormhout.biblixnet.com/exporte_afi';
+
+    $params = ["url_serveur" => $this->_url_server];
 
-    Class_WebService_SIGB_BiblixNet::setService([ "url_serveur" => 'http://mediathequewormhout.biblixnet.com/exporte_afi',
-                                                  'id_bib' => 5,
-                                                  'type' => 10], $this->createMockForService('BiblixNet'));
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_BIBLIXNET,
+                                    'BiblixNet');
+
+    $this->bib_wormhout = Class_IntBib::find(5);
   }
 
 
@@ -741,17 +769,18 @@ class CommSigbDynixTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_pc94 = Class_IntBib::newInstanceWithId(5)
-      ->setCommParams(["url_serveur" => 'http://www.dynix.fr:8080/capcvm/',
-                       'client_id' => 'SymWS',
-                       'mails_bib' => 'ALFA:alfa@here.fr'])
-      ->setCommSigb(11);
+    $this->_url_server = 'http://www.dynix.fr:8080/capcvm/';
+
+    $params = ["url_serveur" => $this->_url_server,
+               'client_id' => 'SymWS',
+               'mails_bib' => 'ALFA:alfa@here.fr'];
+
 
-    Class_WebService_SIGB_Dynix::setService([ "url_serveur" => 'http://www.dynix.fr:8080/capcvm/',
-                                              'client_id' => 'SymWS',
-                                              'mails_bib' => 'ALFA:alfa@here.fr',
-                                              'id_bib' => 5,
-                                              'type' => 11], $this->createMockForService('Dynix'));
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_DYNIX,
+                                    'Dynix');
+
+    $this->bib_pc94 = Class_IntBib::find(5);
 
     $this->mock_service
       ->whenCalledAnswers('setClientId',[], $this->mock_service)
@@ -834,19 +863,22 @@ class CommSigbWithNotAbonneTest extends ModelTestCase {
   /** @test */
   public function reserveItemShouldReturnError() {
     $item = $this->fixture(Class_Exemplaire::class, ['id' => 12]);
-    $this->assertEquals(['erreur' => 'Communication SIGB indisponible'],
+    $this->assertEquals(['erreur' => 'Impossible de contacter le serveur de votre bibliothèque.'],
                         $this->comm_sigb->reserveItem($item, 12, 0));
   }
 
 
   /** @test */
   public function getDispoExemplairesShouldReturnNonReservable() {
-    $this->assertFalse($this->comm_sigb->getDispoExemplaires([$this->fixture(Class_Exemplaire::class,
-                                                                             ['id' => 2,
-                                                                              'id_origine' => 0,
-                                                                              'code_barres' => 0,
-                                                                              'id_int_bib' => 0])
-                                                              ])[0]->isReservable());
+    $this->assertFalse($this
+                       ->comm_sigb
+                       ->getDispoExemplaires([$this->fixture(Class_Exemplaire::class,
+                                                             ['id'=> 19,
+                                                              'id' => 2,
+                                                              'id_origine' => 0,
+                                                              'code_barres' => 0,
+                                                              'id_int_bib' => 0])
+                                              ])[0]->isReservable());
   }
 }
 
@@ -909,21 +941,18 @@ class CommSigbFloraTest extends CommSigbTestCase {
   {
     parent::setUp();
 
-    $this->bib_maze = $this
-      ->fixture(Class_IntBib::class,
-                ['id' => 5,
-                 'comm_params' => ["url_serveur" => 'http://flora.org',
-                                   'login' => 'api',
-                                   'password' => 'secret'],
-                 'comm_sigb' => Class_IntBib::COM_FLORA
-                ]);
-
-    Class_WebService_SIGB_Flora::setService(["url_serveur" => 'http://flora.org',
-                                             'login' => 'api',
-                                             'password' => 'secret',
-                                             'id_bib' => 5,
-                                             'type' => 15],
-                                            $this->createMockForService('Flora'));
+    $this->_url_server = 'http://flora.org';
+
+    $params = ["url_serveur" => $this->_url_server,
+               'login' => 'api',
+               'password' => 'secret'];
+
+
+    $this->prepareLibraryAndService($params,
+                                    Class_IntBib::COM_FLORA,
+                                    'Flora');
+
+    $this->bib_maze = Class_IntBib::find(5);
   }
 
 
diff --git a/tests/library/Class/Cosmogramme/Integration/PhaseItemFacetsTest.php b/tests/library/Class/Cosmogramme/Integration/PhaseItemFacetsTest.php
index 8d3623f462f..ab4dde401aa 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhaseItemFacetsTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhaseItemFacetsTest.php
@@ -225,7 +225,7 @@ class PhaseItemFacetsExecutePhaseTest extends PhaseItemFacetsTestCase {
 
   protected function _prepareFixtures() {
     $this->fixture(Class_Notice::class,
-                             ['id' => 1]);
+                   ['id' => 1]);
 
     $album = $this->fixture(Class_Album::class,
                             ['id' => 2,
@@ -245,3 +245,95 @@ class PhaseItemFacetsExecutePhaseTest extends PhaseItemFacetsTestCase {
     $this->assertEquals('TAssimil V0 HNRNR0001 Lfre Y1', Class_Notice::find(1)->getFacettes());
   }
 }
+
+
+
+
+/* @see https://forge.afi-sa.net/issues/208000 */
+class PhaseItemFacetsExemplaireWithAnnexeExecutePhaseTest extends PhaseItemFacetsTestCase {
+
+  protected function _getPreviousPhase() {
+    Class_Cosmogramme_Integration_PhaseAbstract::shouldThrowError(true);
+    return (new Class_Cosmogramme_Integration_Phase(4))
+      ->beCron();
+  }
+
+
+  protected function _prepareFixtures() {
+    $notice = $this->fixture(Class_Notice::class,
+                   ['id' => 1,
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                    'titres' => 'La plus que vive',
+                    'authors' => 'Christian Bobin',
+                    'date_maj' => '2024-01-01 00:00:00',
+                    'exemplaires' => [$this->fixture(Class_Exemplaire::class,
+                                                     ['id' => 123,
+                                                      'annexe' => 3,
+                                                      'code_barres' => '123',
+                                                      'type_doc' => Class_TypeDoc::LIVRE
+                                                     ])]
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 3,
+                    'id_bib' => 2,
+                    'code' => '1',
+                    'id_origine' => '1',
+                    'libelle' => 'Mirepoix'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 6,
+                    'id_bib' => 2,
+                    'code' => '3',
+                    'id_origine' => '3',
+                    'libelle' => 'Pau'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 5,
+                    'id_bib' => 4,
+                    'code' => '1',
+                    'id_origine' => '1',
+                    'libelle' => 'Foix'
+                   ]);
+  }
+
+
+  /** @test */
+  public function facettesShouldContainsY3() {
+    $this->assertContains(' Y3 ', Class_Notice::find(1)->getFacettes());
+  }
+}
+
+
+
+
+/* @see https://forge.afi-sa.net/issues/211651 */
+class PhaseItemFacetsExemplaireWithMultipleAnnexeExecutePhaseTest extends PhaseItemFacetsTestCase {
+  protected $_record;
+
+  protected function _getPreviousPhase() {
+    Class_Cosmogramme_Integration_PhaseAbstract::shouldThrowError(true);
+    return (new Class_Cosmogramme_Integration_Phase(4))
+      ->beCron();
+  }
+
+
+  protected function _prepareFixtures() {
+    $this->_record = unserialize(file_get_contents( __DIR__ . '/bizarre_notice.txt'));
+
+    $this
+      ->onLoaderOfModel('Class_Notice')
+      ->whenCalled('findAllAfter')
+      ->with(0, '0000-00-00 00:00:00')
+      ->answers([$this->_record]);
+    xdebug_break();
+  }
+
+
+  /** @test */
+  public function facettesShouldContainsAllDistinctItemFacets() {
+    $this->assertContains('B1 S17 E43 Y2 V1 B36 G214 S16 Y20 V36 HNRNR0001', $this->_record->getFacettes());
+  }
+}
diff --git a/tests/library/Class/Cosmogramme/Integration/PhaseNoticeTest.php b/tests/library/Class/Cosmogramme/Integration/PhaseNoticeTest.php
index 901890936fc..d288aa94bd4 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhaseNoticeTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhaseNoticeTest.php
@@ -846,3 +846,70 @@ class PhaseNoticeAuthorRenamedTest extends PhaseNoticeTestCase {
     $this->assertEquals('Oscar Wilde', Class_CodifAuteur::find(11)->getLibelle());
   }
 }
+
+
+
+
+/* hotline: https://forge.afi-sa.net/issues/208000 */
+class PhaseNoticeFacetteSiteTest
+  extends PhaseNoticeTestCase
+{
+  protected function _prepareFixtures() {
+    parent::_prepareFixtures();
+
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 1,
+                    'libelle' => 'BDA'
+                   ]);
+
+    Class_IntBib::find(2)
+      ->setSigb(Class_IntBib::SIGB_ORPHEE)
+      ->save();
+
+    $orphee = array_merge(Class_IntProfilDonnees::forOrphee()->getRawAttributes(),
+                          [ 'accents' => Class_IntProfilDonnees::ENCODING_UTF8]);
+    $attributs = unserialize($orphee['attributs']);
+    $attributs[0][Class_IntProfilDonnees::FIELD_ITEM_ANNEXE] = 'h';
+    $attributs[Class_IntProfilDonnees::FIELD_ITEM_ANNEXE] = 'h';
+    $orphee['attributs'] = serialize($attributs);
+    Class_IntProfilDonnees::find(102)
+      ->updateAttributes($orphee)
+      ->save();
+
+    Class_Cosmogramme_Integration::deleteBy([]);
+    $value = $this->fixture(Class_Cosmogramme_Integration::class,
+                   ['id' => 123,
+                    'bib' => Class_IntBib::find(2),
+                    'profil_donnees' => Class_IntProfilDonnees::find(102),
+                    'type_operation' => Class_Cosmogramme_Integration::TYPE_OPERATION_TOTAL,
+                    'traite' => 'non',
+                    'fichier' => 'cabret.txt',
+                    'pointeur_reprise' => 0]);
+    $value->assertSave();
+
+    Class_Cosmogramme_Integration_PhaseAbstract::shouldThrowError(true);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 3,
+                    'id_bib' => 2,
+                    'code' => '1',
+                    'id_origine' => '1',
+                    'libelle' => 'Mirepoix'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 2,
+                    'id_bib' => 4,
+                    'code' => '1',
+                    'id_origine' => '1',
+                    'libelle' => 'Foix'
+                   ]);
+  }
+
+
+  /** @test */
+  public function annexeShouldContainsBe3() {
+    $this->assertEquals(3, Class_Exemplaire::find(1)->getAnnexe());
+  }
+}
diff --git a/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php b/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php
index 6357dbf4183..262461b1c19 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php
@@ -51,6 +51,12 @@ abstract class PhasePatronsTestCase extends Class_Cosmogramme_Integration_PhaseT
                     'code' => 'SEYNOD',
                     'id_bib' => 2]);
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 36,
+                    'id_origine' => 'SEYNOD',
+                    'code' => 'SEYNOD',
+                    'id_bib' => 1]);
+
     $this->fixture(Class_IntBib::class,
                    ['id' => 1,
                     'sigb' => Class_IntBib::COM_NANOOK,
@@ -280,9 +286,9 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /** @test */
-  public function chiengPinkLibraryShouldBeSeynod() {
+  public function chiengPinkLibraryShouldBeAnnecy() {
     $chhieng = Class_Users::findFirstBy(['login' => 'A-000893']);
-    $this->assertEquals('Seynod', $chhieng->getBib()->getLibelle());
+    $this->assertEquals('Annecy', $chhieng->getBib()->getLibelle());
   }
 
 
diff --git a/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php b/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php
index eb9330eb4b9..81adf9ba5fb 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php
@@ -408,6 +408,12 @@ abstract class PhasePrepareIntegrationsNanookStandardTestCase
                     'type_operation' => Class_Cosmogramme_Integration::TYPE_OPERATION_TOTAL,
                     'profil' => 102]);
 
+    Zend_Registry::set('sql', $this->mock()
+                       ->whenCalled('query')
+                       ->with('ALTER TABLE `notices` ADD FULLTEXT KEY IF NOT EXISTS `mots_notice` (`titres`, `auteurs`, `editeur`, `collection`, `matieres`, `dewey`, `other_terms`, `facets`)')
+                       ->answers(true)
+                       ->beStrict());
+
     $storm_file_system = (new Storm_FileSystem_Volatile)
       ->mkdir('ftp/my-library.net/integration')
       ->mkdir('ftp/my-library.net/transferts/foo');
@@ -512,7 +518,7 @@ abstract class PhasePrepareIntegrationsNanookStandardTestCase
                     'date_maj' => '2012-03-06 00:00:00']);
 
     $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 8,
+                   ['id' => 34,
                     'id_origine' => '34',
                     'libelle' => 'Library 1']);
 
@@ -589,7 +595,8 @@ class PhasePrepareIntegrationsNanookWithRemovedLibraryTest
 
     $this->fixture(Class_IntMajAuto::class,
                    ['id' => 1,
-                    'id_bib' => 3]);
+                    'id_bib' => 3,
+                    'nom_fichier' => '']);
 
     $this->fixture(Class_Cosmogramme_Integration::class,
                    ['id' => 555,
diff --git a/tests/library/Class/Cosmogramme/Integration/PhasePseudoRecordCmsTest.php b/tests/library/Class/Cosmogramme/Integration/PhasePseudoRecordCmsTest.php
index 49ae146ca63..6fd94afeab1 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhasePseudoRecordCmsTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhasePseudoRecordCmsTest.php
@@ -33,6 +33,10 @@ class PhasePseudoRecordCmsUnindexTest extends Class_Cosmogramme_Integration_Phas
 
     Zend_Registry::set('sql',
                        $this->mock()
+                       ->whenCalled('query')
+                       ->with('ALTER TABLE `notices` ADD FULLTEXT KEY IF NOT EXISTS `mots_notice` (`titres`, `auteurs`, `editeur`, `collection`, `matieres`, `dewey`, `other_terms`, `facets`)')
+                       ->answers(true)
+
                        ->whenCalled('fetchAllByColumn')->answers([])
 
                        ->whenCalled('fetchAllByColumn')
@@ -194,6 +198,10 @@ class PhasePseudoRecordCmsIndexWithTranslatedRecordTest
                        ->with('select n.id_notice from notices as n inner join exemplaires as e on (n.id_notice=e.id_notice and n.type=1 and n.type_doc="8") inner join cms_article as a on a.id_article=e.id_origine or a.parent_id = e.id_origine where (a.debut is not null and date(a.debut) > date(now())) or (a.fin is not null and date(a.fin) < date(now()))')
                        ->answers([])
 
+                       ->whenCalled('fetchAllByColumn')
+                       ->with('select id_article from cms_article where id_article not in (select a.id_article from notices as n inner join exemplaires as e on (n.id_notice=e.id_notice and n.type=1 and n.type_doc="8") inner join cms_article as a on a.id_article=e.id_origine or a.parent_id = e.id_origine) and ((debut is null or date(debut) <= date(now())) and (fin is null or date(fin) >= date(now()))) and status=3 and indexation=1 order by id_article')
+                       ->answers([])
+
                        ->whenCalled('fetchAllByColumn')
                        ->with('select id_article from cms_article where id_article not in (select a.id_article from notices as n inner join exemplaires as e on (n.id_notice=e.id_notice and n.type=1 and n.type_doc="8") inner join cms_article as a on a.id_article=e.id_origine) and ((debut is null or date(debut) <= date(now())) and (fin is null or date(fin) >= date(now()))) and status=3 and indexation=1')
                        ->answers([22299])
@@ -202,6 +210,10 @@ class PhasePseudoRecordCmsIndexWithTranslatedRecordTest
                        ->with('select id_article from cms_article where id_article not in (select a.id_article from notices as n inner join exemplaires as e on (n.id_notice=e.id_notice and n.type=1 and n.type_doc="8") inner join cms_article as a on a.id_article=e.id_origine or a.parent_id = e.id_origine) and ((debut is null or date(debut) <= date(now())) and (fin is null or date(fin) >= date(now()))) and status=3 and indexation=1')
                        ->answers([])
 
+                       ->whenCalled('query')
+                       ->with('ALTER TABLE `notices` ADD FULLTEXT KEY IF NOT EXISTS `mots_notice` (`titres`, `auteurs`, `editeur`, `collection`, `matieres`, `dewey`, `other_terms`, `facets`)')
+                       ->answers(true)
+
                        ->beStrict());
 
     $this->fixture(Class_Notice::class,
diff --git a/tests/library/Class/Cosmogramme/Integration/bizarre_notice.txt b/tests/library/Class/Cosmogramme/Integration/bizarre_notice.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3c7cfb7df6f877f3c4d9606427cd2f6d22447c2d
GIT binary patch
literal 28874
zcmeHQ>2ljR65bcth^s$awN<ux>C7)Vwi4HP?5v#3*3Q%vi=rr*GnA;1lyg<yXJ=m{
zFScJdz)K`#=a{RORf$Of=tcu<0F4g~;CgJB<L;BtO%mrSN(0aDj?KaNeKIx%<L+Pn
z@fRm`=b`Ux<6!N^Z@Z6^@o3x)7S2W_#&5$G3Eeb}gZVD?lkQ`8Yz>JCcn6E_<6vy)
z=Fk|KHS%)IaNJGbZhdDFd6bVa?i&2WiyLllT_+9F*ykX~aC-6mWHvk3vZpobaTKx?
zsa=S&i#bdmch4rzF16Y9<noL{`pAlyLDXbC0LgPF<5Rq4A=&fE_2uRHlNT4)SK9RY
z^5R-UmLhxYo9oN-Z*CC7{2&46&D;$g75UuZ{GU54fmR43UY|~Go=;}l`Q^pU<n;W-
zGff7ZPG+a)vx^(;*~R7AM9Y7kO+;BFMHZn^Dk*`#2=d}XfTxqo%ZqO(voo;f7fNQJ
zUc4v_eJ>574TYGfm|8I_g~LZ$yJ_Idx&sHI>y5h$|FzHhVdHyUUBCS99JO_WD*twJ
z1^I7YT+c54`SM&7QJM(&hKRv(>3V+3nuBixo|6Hp7JvHMq-WSY?Q$9W+EW98PYqkw
zUKrY`q1_k*?cBDsf1BEOhW6}gb~V#=-6WPM2D9WZFvPE^!z}sH*YIGrQ->K(9cE1r
zFrPYLKXs77)S)b<4rMZRkj>OVMpLJf6|~=rQdUdU^Nc>IHYXVn5hQc2R>K;1%_Dtq
zq+5L`&5=d%2=iS;U*VnF5E_*it=E3+1#ZavWaUD9#LYKLqLN|!=|X?$u&#4<&|;~n
z@^N%TdNw<v9P{sgWQ>Nxqr72x#3S-|ccNjL_>f8_lneD1Oyk|UTi@MrScB4VIAl$u
zwO`1HBFkoqeB8~(=VGGlBovXL24C&m5UNX^MvpQC6aURNin$-+u7pCtY2l{6<HbI-
zQ7Es0UI7ZAAMrm;*T?*i_(H`v`P%)7B|}P6H|-<CIMQuRw<sH3mlEtY)YowzG@=U8
zbv!oiTGvby`ZXA6nr8QOWMJT5kFw-f-5k+NPe&@<7~qd>>n76BZ@q8e4MFsGh<AP>
zorx5b0O<`2q2Ol>>3v}6eT3t`(d#jb5xw^flhYu**&7ghy^mOPV4Eb1ZRxsY^-UA+
z7SdRQkp*Fyr)~5(dmH?1%eH$q=)k}pnk21mgHMmB@U)PoXZOTA=zBwiAT5=#ZwwzT
z<Lely?2+B;_lJ)(t>acSyfZ(Ik2?z!jg2Fxv(8)}4o$t^*Ykk0OO2G#8re37ks9bj
zy|<tj)UvkLw6{9i{##^iQNY}Nq$4W?NhbVwi<o!#k21I}hs2KzBs&e>xiNZ<ziIRA
zt+Sj(Zn)6S+}p6TN@I6%tSx*k{JXnZg>H~&TQ}A|{t^a>JNMID?Wvnkh@WcDf^gw#
zr*1s=;~?Q&ez=NucgIM+IQ~%tJa-ZIW5;{yh9POuqQhnB_{Q+)#tk;<(PZsz7M<0_
zdK({Wr@JHxKK@6R#?lAc_PFDD%rywZW6j@a_!XiijiL5NGYoIdC3}*0+uAe=gQ)XH
zAM}UlVl1<Bhc-si%tr&%_dxVHgGZXtu`Jt!HrRS6-DhxI=U{Zw8#Lc&gX#%>_O-Xs
zj+q$7a5S{7VaMf4oO6YCmd42FqjwqU9UGa-lp`pWJ?glk8awl%W;^!4lBrC6Xbwk6
zl~rzM&Xp~b;6sx3oL)^kUZx?P)ns~gJ$v+5Zq68S-l5RC6Z<I{DZyA5oylhI+yx1G
zsd5O0E;kFI{%jYANVVF~2*jNQx}4iI-6rFclh^({x$}<QWpI?Z$4GL5S0~!Ko|E|T
z9qgs#1mAgu$4~C79S>qB>FsVkPfmuWZH>(S@lRWSMSRieP*$9ryBhR%UzE>0Sk*G&
zfq^<jf8n^P99>X9QF`mgPU^>N9urLj;S=fxnsd+Apcbe;dL_ZoA(J7NcebdbWdI}4
zi#Dmh0n>aK;xR7aF^&hO4=^F{LUv<e4m!ccSzt(PjY6`>TBEQv3R|PFH40&4wMJoU
z6t+fTYZU$;7=^HX$tb*a69@C3w~f}J!LTC?Q~!;>-eNMw7!$0<@q04gug3-f4GdMD
z=P9~lO#9yh5r8q>2v@>*B?uBJV60XRQ!+_Svw~Ba_7D?VgOCS^T*5iaK&1p^0@$<&
zCe{$zix$2!r|G7&@L*u?Th+}#5g>+@zaany+(ry~Kxm+#yEUwBs)lT~f-)u<SckL&
zWDR7)1$=kMkO?Hg@2$T}wU7S=ys2$tJU{-z#ixp;YV~|#ODt^*3{V(CV&3r72Wc2T
zqq<_iWP_<FU>-3sqPYc)S^{7prV!{I=7%GRy^eIkb;edtd@<B6RW5i@<**QlrYADJ
z1ISgtv*oJ#K$Y4^%eY#F7RAjli0)$$n$3g^=2Qq#(o3eWnkiUiDina6P(G%yn`s!J
ztI(i+azqf)^_uB;c37c<bb#nU(Ql@>tyf}3F=(a;)D**J3P#GRQsMgpqhia2&@#w3
zYNicqxq%TuRr-j@4855ys?o8|4Nvs`2>?4$Cx${hwvEKb;6xu0F{ZW<*idN5t7c;3
zM7Kd~3<R-4S$LJ9R#j&2MDLZvR%i{+r)$K71=5V%Kzk&y6{_>PE;ct|B2nnhn<f%j
zvkK*5T)!$}o{K_z-W|wAp)|a7QO!jt9Al+=pDV&tXwGRZKL(p6kwRmh)QCvkOlS;!
zHyw~P>d{naOtz3zbY#fU=!u#%6`Jz2o+HKyk&i+@o*f`jXvI|%39_crD750biR7gq
zQE0{UCK6;#xhS;arbbfgRZWFnyr>c7-J_+@1_-`C$YlMarO<`%Y9uAmEQJpIt436a
zrqF=zn@B_>w-g$HK69@$XeDCAL~VbdSkhZqD^VP*f{hqzG3-`B`cgUo(ayYNzmSoX
zIPRSrgtXk7*)^yEAeI6|ggKN(&cY9Uu{c8rE3&8ng&6%I1Plik3&s&)hRg|lE}K)y
ztsgF(pT#m^4`U<xx0$13mCC`}DgDt)f3k&0M+Z2Wt<Ro1WS&t`2*S)cI=d~~EA)rU
z331CSJpFNRG&U_%*I|URH3wUBu*qs|&B4|j#0b)wgXEUenu87V+SVMTakpx|!x5-8
z2gQWIYR$nvsyX-&Y*HV;C(98Au>!^<EKwvPmx6MlB8HraC}!@5(Pou6u&0Yv@*-IV
zR2Io74wxD@ugvjM0%Tyh2Drw6OU;!N4U#;iVt)yLy^RN#fPj}CtI#G2hD1CaXkd!8
z0&_j3`GSHWnmxIwQ3`M)5HOtY)+9FflocT{fCz>Ykw(FCe?|kom@(uNmnP1rc)|H%
zlOB>*vd(#dHC0`8-qMf(CI>|Z&v}A#7Lga5Kzy5O+Cb53Q=$))Atr333w|6&<P(r-
zj|oJR!WaJ1-GynPAP#Vbo$#%di1*%<kNpTyxfLLq0`YM&gqPEx6xGSzJad=H!R6e+
z1L8|;nn+Q?d_G8+cLh(*X{w+wq?7|grXj)?rvhBFT#cBsB!?E3W}iaf{m&@)HrBX^
z+MBN8X9ma@x#TlS5?Th|JJY0zYt8FWY88FVW$=dD9wre|a(ogBhu3ukZ%=YygbG}b
zO^9@pM)BJDmfEy4DaXU|dJnx(kW#oHEj+urn4t602^C*}>eNLElw7=7a6e^rM9w6U
z@b-qYqnJh0H_n$@gqF+v!^mC0p~$1<7^<qGZ4_%Wmz;(>4d@ZEU&YaG3qPWrSb!i5
zA!`WBzz-K-MXEscK*`BnY8fGy9hOx9S?u6*C0&|ir=0|=xl@QJBcZ2UnAqJUAtM4P
zB~`$0vxd}zE8Nhg)tPEX3kh;JcvNH29;`l9#D3cqbyIk0QAL(?w!F5$eQv&pWEwW{
zqb}|KMI_3)(iTC9-w$3yqHl=8WIYT%9w9IM(IG5@GM~1HWcE>O=zXQ`%6^y&e%~Te
z?$E|orvm8XQ4A6nxGjqN*^5XRc%fMe<L-Ay4Fs;JfP5PlkpzVT@oij05)=x+w{d+!
zSi}mn_t5f$0_{CCA5|#9q2&n$)_Y1iUC|qKGuB9~Fc=CAXkKB!no?y=N`d<}!uV66
z+Y03W0F19%4>|zjV}-e~`Q0>C#YmTnWC~Sigz<&)7z$lD0OPCHffO*l5Y1F*!vPpy
zwfb`a##gQV)M0#dJSIHx$qg4@^$US~F^JTRI~w+?mRr@9YBY@$Mw9^^`ie+C4U(iu
z_u5xElHaFY9oVGQBfSA33^W?(i&<3RYrK!-w}Wy!D8saYNz&YTVND4GxuPfjs-7L9
z&ua(e_xr9v?Vy|+xb2|aZ3kt{c^<U9{s$YB<<$J3uV;&=I)@O-5;_1g2jEGIrwZh{
z(J*cCR2b0}45!6Yfh4wg>aUEan!gdADtz+gC<oLuTLM$!sfT>>OBmq;;nY91Pd>0<
zKmzc|$1Vw)>Nny!3ZMK^RWW+rNc(l+)JvX)DZsSfedZ>B%TJlP2?_;_)(EEx3I&FC
zj}w-d!z#eEpKJ48T6)>{GgoK`uP;}?Pf09x14tN80k2^pcn>)xV%@5OpbFTP7p!YJ
zTkynU-+BS;so>Y<{RF0U0#B~Ot6wqc-!_VE>Q%3P1rYnHkr=?QVN%ZGOQ|5)xRKZ#
zi3-a$BPg`0zMlZKM|&>}06^4^6}t7ho+;o-L84HpI#!P<i(&Mn*S$iQ4#4#)fVL4C
z5VENN+K25XQ1mnhv3^sbFU?p#b`!9&WxEL!`f^sQeXK6zy9pFs&Y|4|3hg+wn?Ru$
zjmr>nH-SPi4lF~cT=owj9x9jpS*^|h?}-jU<*=_<xe(1!<)hWOav}O@mCOFiCeb8`
zLIV^l7g98Z1{?rMEQJOfSh=ti8c<)kK$r5h)-BjrYgo3Rh2^rt$(Jl!eC-Vg=-@tO
z*#bvQec1+t*7p4zSAy2|-M72SJ|i#%6g|;O-FYdxp+;8%(cLL}q1N_&z^2sJ_9acE
z$wF)Umi(1q<+QeMYy0lcsax9@4MW=4iczPveZPG3?HAg<?do8=+oIiVL3>%+-4@0E
zeX{dl<~?Xy-R`#d6YRF2HTiqC%iVL{S3DkCylW)}WZlP``8dYGA|tV11%MHGer8T4
zw1MgXKlY(0_Q?Ug5*x*ey(*-60I%an9KDVd*(#%$fGqs2zgb`t3Ko{x@XAiY;DXBS
zEya$#l1pl_x2t($pWHPF?WY2L&VDd)VFMfhit7`ES1&n^h*Q?dAppe_2h!6IAZz*Q
z^C)a_8-i?w=$}fL7|1}{qOy08aFM)p<A4l=M7;k)p1Phf!LxenK#}~Os|89Nst3!L
zBKUOr{8aob0_w7VY<(7mGVPs$FJmBn`CbAUipnak7RY#jJ?&L0pCT=zVD3;%e&};X
zf%@mVDIZ{;#o@`pr6Zj8UBMu9!->~4t}0Gy#`i3ptWVe~LP{sybKZZhEo=A^)MEB`
zqR}+~qIF;K9VJd_8yc#SNUFQsP!Xc7bJM2HPn1RkIe8Uf>)0OSm-3!~>;yv532+;9
zJx_EMq8-Y^331xIhdpz7B)cPH!h)v|Y`F_VoMG-oF)wwuw-4}51nunu?d=19;M)g8
zf3eyH!g-tauBvVfp{9=B2HgwoHK$*)%NLX4{js0+?jvo0Aw1?&wRl54>;)dav7j%l
z9@)vD|EV7M^$3giA}KBXvw<X%n#k|qjU725?c^*GQb$IFmD@au#8rW4cTSeLeA9<;
zDbUF~04AG|h%ODHwAuX*2X6q849MFgXMnGeT?`_l$)6;T&gG^$A-^$N`p9Fc&!^n0
z_lU@pwB(Ve#F#?~W5EVz1ePO5i2b#%9butx157#J?5k9+BM}Mtpv^*h*asy351)Na
ziR4<z-iDeI*9Tr-B3PIe78bq_)m*W6U2&}m6XOb#O7X?l+q>?BQ{uXta!Q+0e&RJK
zf=`cpKJt2`4Qt?2zaE7X<1U?YV)DPEOdob_isT@jp+w`rU22lUeb=ev2lt^b;5-js
zt-_gM_hDJwtZz_WxdI}x6Gh)7j1>n{BQ|5kU2!3c=;Y$n8)?nd5};1dv+<t)QXE<p
zI19Uuh@0aD^JF&{(Q_Y19?IN1^rWu?=EOjqNc6b#F!Fwe*_WTbA#KVnb`<*Yrl9v+
z?-oZQuwS*1eSveCJ)G6R-b}I^mu8&ku;qx5v6gnF?92<|EM5pCj!mSDX#NbgD2O8_
zVsr=J#tXJCeDO#QC<RC2f*UEA454{$EG~d4;v_4aPK{HgWqM(-Iw7t;+WAjFB^hP;
zE`vBp>!&M-@{400*X~v_6Z(L&9YUPAz&CP{kn&sL&~ID<gL2%`X%0nQ(y0+thy({t
a(1kjIFBK*$lK7O8G7FSh!JIA~@9;k!P*c1B

literal 0
HcmV?d00001

diff --git a/tests/library/Class/Cosmogramme/Integration/cabret.txt b/tests/library/Class/Cosmogramme/Integration/cabret.txt
new file mode 100644
index 00000000000..3cd60378ce6
--- /dev/null
+++ b/tests/library/Class/Cosmogramme/Integration/cabret.txt
@@ -0,0 +1 @@
+01923cam  2200229   450 001001500000010004700015100004100062101001300103102000700116105001800123200013500141210005400276215005500330330038900385345001800774700003600792702004000828801003300868995026400901995026401165995026401429frOr0293414425  a978-2-7470-2423-5bRel. ss jaq.d17.90 EUR  a20081121d2008    a  |0fre|01  ||||ba0 afreceng  aFR  aaa      0||y|1 aL'invention de Hugo Cabreteroman en mots et en imagesfde Brian Selznickgtraduit de l'anglais par (Etats-Unis) Danielle Laruelle  aMontrouge (Hauts-de-Seine)cBayard Jeunessed2008  a533 p.cillustrations en noir et blancd22 x 15 cm  aLe père d'Hugo Cabret est mort dans l'incendie du musée où il travaillait en tant qu'horloger. Hugo a pour compagnon un automate et un oncle alcoolique qui l'héberge. Le jour où il disparaît, Hugo se cache et continue son travail de réglage d'horloges. Il répare l'automate de son père, persuadé qu'il va lui délivrer un important message. Prix Caldecott 2007 (Etats-Unis).  b9782747024235 1aSelznickbBrianf1966-....4070 139836000133aLaruellebDanièle4730  aFRbElectrec20081121gAFNOR00a4419070054b293414425cLd905fJ SEL ig1h1i1j1301700011k100022l100056m03/06/2024n28/02/2027o04/11/2016p23/02/2024q04/04/2014r04/07/2014t1300200008w1x1022y2z2A1300100008B1790C17/02/2009D151G6H6NGOUP7R7S905T1U11V905W11Z2944190700a4419080054b293414425cLd905fJ SEL ig1h1i1j1301700011k100026l100025m14/11/2023n31/12/2026o01/12/2022p08/12/2022q22/09/2022r01/12/2022t1315500001w1x1026y2z2A1300100008B1790C17/02/2009D151G8H8NGOUP7R1S905T1U11V905W11Z2944190800a4419090054b293414425cLd905fJ SEL ig1h1i1j1301700011k1300200002l100030m30/06/2022n31/12/2025o04/12/2018p31/05/2022q03/11/2015r04/12/2018t100004w1x1131y2z2A1300100008B1790C17/02/2009D151G6H6NGOUP7R1S905T1U11V905W11Z29441909
\ No newline at end of file
diff --git a/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php b/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php
index 1b75ec477cf..106cdc9d71b 100644
--- a/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php
+++ b/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php
@@ -25,9 +25,9 @@ abstract class MoteurRechercheFacettesTestCase extends ModelTestCase {
   public $facettes;
   protected array $_facets =
     [
-     'M6567 HCCCC0001 B3 Lfre A4656 T4 Y2 V1 A37135',
+     'M6567 HCCCC0001 B3 Lfre A4656 T4 Y20 V1 A37135',
      'HTES10002 Lfre T1 Y7MA V1 A556 A5242',
-     'HTES100010003 Lfre B3 T2 Y2 V1 A5242 A4656',
+     'HTES100010003 Lfre B3 T2 Y20 V1 A5242 A4656',
      'M6567 Lfre T4 Y7MA V1 A4656',
      'M6567 Lfre T4 Y7MA V0 A4656 A1234',
      'Lfre T2 Y7MA V1 A37135 A4656',
@@ -124,13 +124,15 @@ abstract class MoteurRechercheFacettesTestCase extends ModelTestCase {
                     'libelle_facette' => null]);
 
     $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => '7MA',
+                   ['id' => 7,
                     'id_origine' => '7MA',
+                    'code' => '7MA',
                     'libelle' => 'Maison environnement']);
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 20,
                     'id_origine' => '2',
+                    'code' => '2',
                     'libelle' => 'Seynod']);
 
     $this->fixture(Class_CodifAuteur::class,
@@ -228,7 +230,7 @@ class MoteurRechercheFacettesSearchAlbumsPommesNewsTest extends MoteurRechercheF
                          'T' => ['T4' => 4,
                                  'T1' => 1,
                                  'T2' => 2],
-                         'Y' => ['Y2' => 2,
+                         'Y' => ['Y20' => 2,
                                  'Y7MA' => 4],
                          'B' => ['B3' => 2,
                                  'B2' => 1],
@@ -441,9 +443,10 @@ class MoteurRechercheFacettesSearchEmptyWithSpecificSettingsToUserTest
 
   /** @test */
   public function emptySearchWithDefaultBookmarkedLibShoulSucced() {
+    $f = Class_CodifAnnexe::find(20);
     $this->assertEquals([['id' => 'HCCCC0001',
                           'label' => 'Domaines : Games (1)'],
-                         ['id' => 'Y2',
+                         ['id' => 'Y20',
                           'label' => 'Site : Seynod (2)']],
                         $this->facettes['bookmarks']);
   }
diff --git a/tests/library/Class/MoteurRechercheTest.php b/tests/library/Class/MoteurRechercheTest.php
index 514937445fa..23ccba53f9c 100644
--- a/tests/library/Class/MoteurRechercheTest.php
+++ b/tests/library/Class/MoteurRechercheTest.php
@@ -708,11 +708,11 @@ class MoteurRechercheCatalogueTest extends MoteurRechercheTestCase
        'type_doc'  => '1;4']);
 
     $this->fixture(Class_Catalogue::class,
-      ['id'        => 89,
-       'libelle'   => 'Nouveautés Annecy, Seynod',
-       'nouveaute' => 1,
-       'annexe'    => 'ANNECY;SEYNOD',
-       'genre'     => '26;25']);
+                   ['id' => 89,
+                    'libelle' => 'Nouveautés Annecy, Seynod',
+                    'nouveaute' => 1,
+                    'annexe' => '2;7',
+                    'genre' => '26;25']);
 
     $this->fixture(Class_Catalogue::class,
       ['id'          => 99,
@@ -733,7 +733,7 @@ class MoteurRechercheCatalogueTest extends MoteurRechercheTestCase
     return
       [
        [['id_catalogue' => 89],
-        'sql' => "SELECT `notices`.`id_notice`, `notices`.`facettes` FROM `notices` WHERE ((`notices`.`date_creation` >= '2012-05-03' AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+(F_G25 F_G26) +(F_YANNECY F_YSEYNOD) +(F_HNANA0001 F_HNANA0002)' IN BOOLEAN MODE)) AND `notices`.`type` = 1) ORDER BY `notices`.`annee` DESC, `notices`.`alpha_titre` ASC"],
+        'sql' => "SELECT `notices`.`id_notice`, `notices`.`facettes` FROM `notices` WHERE ((`notices`.`date_creation` >= '2012-05-03' AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+(F_G25 F_G26) +(F_Y2 F_Y7) +(F_HNANA0001 F_HNANA0002)' IN BOOLEAN MODE)) AND `notices`.`type` = 1) ORDER BY `notices`.`annee` DESC, `notices`.`alpha_titre` ASC"],
 
        [['id_catalogue' => 88],
         'sql' => "SELECT `notices`.`id_notice`, `notices`.`facettes` FROM `notices` WHERE ((`notices`.`date_creation` >= '2012-05-03' AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+(F_T1 F_T4)' IN BOOLEAN MODE)) AND `notices`.`type` = 1) ORDER BY `notices`.`annee` DESC, `notices`.`alpha_titre` ASC"],
@@ -769,8 +769,9 @@ class MoteurRechercheCatalogueTest extends MoteurRechercheTestCase
     (new Class_MoteurRecherche)
       ->lancerRecherche($criteres_recherche);
 
-    $this->assertSqlEquals(['SELECT `codif_thesaurus`.* FROM `codif_thesaurus` WHERE (`codif_thesaurus`.`code` = \'Nouveauté par annexe\' AND `codif_thesaurus`.`id_thesaurus` LIKE \'NANA%\') ORDER BY `codif_thesaurus`.`id_thesaurus` DESC LIMIT 1',
-                            "SELECT `notices`.`id_notice`, `notices`.`facettes` FROM `notices` WHERE ((`notices`.`date_creation` >= '2012-05-03' AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+(F_HNANA0001 F_HNANA0002) +F_F565 +(F_G25 F_G26) +(F_YANNECY F_YSEYNOD)' IN BOOLEAN MODE)) AND `notices`.`type` = 1) ORDER BY `notices`.`annee` DESC, `notices`.`alpha_titre` ASC"]);
+    $this->assertSqlEquals(['SELECT `codif_annexe`.* FROM `codif_annexe` WHERE (`codif_annexe`.`id_annexe` IN (\'2\', \'7\'))',
+                            'SELECT `codif_thesaurus`.* FROM `codif_thesaurus` WHERE (`codif_thesaurus`.`code` = \'Nouveauté par annexe\' AND `codif_thesaurus`.`id_thesaurus` LIKE \'NANA%\') ORDER BY `codif_thesaurus`.`id_thesaurus` DESC LIMIT 1',
+                            "SELECT `notices`.`id_notice`, `notices`.`facettes` FROM `notices` WHERE ((`notices`.`date_creation` >= '2012-05-03' AND MATCH(`notices`.`titres`, `notices`.`auteurs`, `notices`.`editeur`, `notices`.`collection`, `notices`.`matieres`, `notices`.`dewey`, `notices`.`other_terms`, `notices`.`facets`) AGAINST('+(F_HNANA0001 F_HNANA0002) +F_F565 +(F_G25 F_G26) +(F_Y2 F_Y7)' IN BOOLEAN MODE)) AND `notices`.`type` = 1) ORDER BY `notices`.`annee` DESC, `notices`.`alpha_titre` ASC"]);
     $this->assertEquals(['HNANA0001', 'HNANA0002', 'F565'],
       $criteres_recherche->getMultiFacets());
   }
diff --git a/tests/library/Class/NoticeTest.php b/tests/library/Class/NoticeTest.php
index 6c303531416..e3722d4652f 100644
--- a/tests/library/Class/NoticeTest.php
+++ b/tests/library/Class/NoticeTest.php
@@ -1055,6 +1055,7 @@ class NoticeUpdateFacetsFromItemsTest extends ModelTestCase {
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 43,
+                    'id_bib' => 10,
                     'libelle' => 'Roubaix',
                     'id_origine' => 'ROUB']);
 
@@ -1067,7 +1068,7 @@ class NoticeUpdateFacetsFromItemsTest extends ModelTestCase {
                     'id_notice' => 5,
                     'id_bib' => 10,
                     'date_nouveaute' => '2016-05-11',
-                    'annexe' => 'ROUB']);
+                    'annexe' => 43]);
 
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 56,
@@ -1081,9 +1082,9 @@ class NoticeUpdateFacetsFromItemsTest extends ModelTestCase {
 
   /** @test */
   public function recordFacetsShouldContainsNoveltyForRoubaixAndNoveltyForLille() {
-    $this->assertEquals('A1 T0 B10 YROUB HNNNN0001 HNANA0001 B11 HNNNN0002 HNRNR0001',
+    $this->assertEquals('A1 T0 B10 Y43 HNNNN0001 HNANA0001 B11 HNNNN0002 HNRNR0001',
                         Class_Notice::find(5)->getFacettes());
-    $this->assertEquals('F_A1 F_T0 F_B10 F_YROUB F_HNNNN0001 F_HNANA0001 F_B11 F_HNNNN0002 F_HNRNR0001',
+    $this->assertEquals('F_A1 F_T0 F_B10 F_Y43 F_HNNNN0001 F_HNANA0001 F_B11 F_HNNNN0002 F_HNRNR0001',
                         Class_Notice::find(5)->getFacets());
   }
 }
diff --git a/tests/library/Class/Testing/WebService/SIGB/Opsys/Service.php b/tests/library/Class/Testing/WebService/SIGB/Opsys/Service.php
index e2340f9cf4b..969657e5a93 100644
--- a/tests/library/Class/Testing/WebService/SIGB/Opsys/Service.php
+++ b/tests/library/Class/Testing/WebService/SIGB/Opsys/Service.php
@@ -22,6 +22,8 @@
 
 class Class_Testing_WebService_SIGB_Opsys_Service extends Class_WebService_SIGB_Opsys_Service{
   use Trait_WebService;
+  protected  $search_client;
+
   public function connect() {
     return $this;
   }
@@ -29,4 +31,11 @@ class Class_Testing_WebService_SIGB_Opsys_Service extends Class_WebService_SIGB_
   public function disconnect() {
     return $this;
   }
+
+  // Testing
+  public function setSearchClient( $mock_object): self
+  {
+    $this->search_client = $mock_object;
+    return $this;
+  }
 }
diff --git a/tests/library/Class/WebService/SIGB/KohaLegacyTest.php b/tests/library/Class/WebService/SIGB/KohaLegacyTest.php
index 4b5f0c240fb..5b9082d8d5b 100644
--- a/tests/library/Class/WebService/SIGB/KohaLegacyTest.php
+++ b/tests/library/Class/WebService/SIGB/KohaLegacyTest.php
@@ -110,10 +110,9 @@ abstract class KohaLegacyTestCase extends ModelTestCase {
 
     Class_AdminVar::set('KOHA_MULTI_SITES', '');
 
-    $this->service = Class_WebService_SIGB_KohaLegacy::getService(['url_serveur' => 'http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl']);
+    $this->service = Class_WebService_SIGB_KohaLegacy::getService(['url_serveur' => 'http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl', 'id_bib' => 1]);
     $this->mock_web_client = (new Class_Testing_WebService_SimpleWebClient);
 
-
     $this->service->setWebClient($this->mock_web_client);
   }
 
@@ -585,22 +584,24 @@ class KohaLegacyGetEmprunteurLaureAfondTest extends KohaLegacyTestCase {
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 33,
+                    'id_bib' => 1,
                     'libelle' => 'Biblio Meuse',
                     'id_origine' => 'BDM']);
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 34,
+                    'id_bib' => 1,
                     'libelle' => 'Montmedy',
                     'id_origine' => 'MON']);
 
     $this->fixture(Class_Bib::class,
-                   ['id' => 88]);
+                   ['id' => 1]);
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 35,
                     'libelle' => 'Médiathèque publique',
                     'id_origine' => 'MPU',
-                    'id_bib' => 88]);
+                    'id_bib' => 1]);
 
     $this->mock_web_client
       ->whenCalled('postData')
@@ -679,9 +680,9 @@ class KohaLegacyGetEmprunteurLaureAfondTest extends KohaLegacyTestCase {
 
 
   /** @test */
-  public function firstHoldWaitingToBePulledLocationIdShouldBe88() {
+  public function firstHoldWaitingToBePulledLocationIdShouldBe1() {
     $waiting_holds = $this->laurent->getHoldsWaitingToBePulled();
-    $this->assertEquals(88, $waiting_holds[0]->getLocationId());
+    $this->assertEquals(1, $waiting_holds[0]->getLocationId());
   }
 
 
@@ -1134,6 +1135,7 @@ class KohaLegacyGetEmprunteurJeanAndreWithIdSIGBTest extends KohaLegacyTestCase
 
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 33,
+                    'id_bib' => 1,
                     'libelle' => 'Testing branch',
                     'id_origine' => 'BIB']);
 
@@ -1304,8 +1306,9 @@ class KohaLegacyGetRecordElementaireMonCherPolarTest extends KohaLegacyTestCase
 
   /** @test */
   public function reserverExemplaireWithReserveAndExpirationDateShouldRequestHoldTitle() {
-    $this->fixture('Class_CodifAnnexe',
+    $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 8,
+                    'id_bib' => 1,
                     'code' => '007',
                     'id_origine' => '007']);
 
@@ -1606,8 +1609,9 @@ public function tearDown(): void  {
 
 /** @test */
   function reserverExemplaireShouldSendPickupLocationIfGiven() {
-    $this->fixture('Class_CodifAnnexe',
+    $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 56,
+                    'id_bib' => 89863,
                     'id_origine' => 'BLV',
                     'libelle' => 'bib LV']);
 
diff --git a/tests/library/Class/WebService/SIGB/NanookTest.php b/tests/library/Class/WebService/SIGB/NanookTest.php
index 7036e8a26ab..9bb2959e7e0 100644
--- a/tests/library/Class/WebService/SIGB/NanookTest.php
+++ b/tests/library/Class/WebService/SIGB/NanookTest.php
@@ -119,12 +119,13 @@ abstract class NanookTestCase extends ModelTestCase {
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 15,
                     'libelle' => 'MaBib',
+                    'code' => '11',
                     'id_origine' => '11']);
 
     $this->fixture(Class_Exemplaire::class,
                    ['id' => '1234',
                     'id_origine' => '2',
-                    'annexe' => '11']);
+                    'annexe' => 15]);
 
   }
 
@@ -1414,22 +1415,22 @@ class NanookOperationsTest extends NanookTestCase {
 
   /** @test */
   public function reserverExemplaireOnExistingAnnexeOfLibraryWithTwoAnnexesShouldReturnSuccess() {
-    $this->fixture(Class_CodifAnnexe::class,
+    $this->fixture(Class_CodifAnnexe::class ,
                    ['id' => 5,
                     'libelle' => 'Archives',
-                    'id_bib' => 3,
-                    'id_origine' => 3]);
+                    'id_bib' => 182,
+                    'id_origine' => 182]);
 
     $this->_mock_web_client
       ->whenCalled('open_url')
-      ->with('http://localhost:8080/afi_Nanook/ilsdi/service/HoldTitle/bibId/196895/patronId/1/pickupLocation/3')
+      ->with('http://localhost:8080/afi_Nanook/ilsdi/service/HoldTitle/bibId/196895/patronId/1/pickupLocation/182')
       ->answers(NanookFixtures::xmlHoldTitleSuccess());
 
     $this->assertEquals(['statut' => true, 'erreur' => ''],
                         $this->_service->reserverExemplaire(
-                          Class_Users::getLoader()->newInstance() ->setIdSigb('1'),
+                          Class_Users::getLoader()->newInstance()->setIdSigb('1'),
                           Class_Exemplaire::getLoader()->newInstance()->setIdOrigine('196895'),
-                          3
+                          182
                           ));
   }
 
diff --git a/tests/library/Class/WebService/SIGB/OpsysServiceTest.php b/tests/library/Class/WebService/SIGB/OpsysServiceTest.php
index decb156c911..04cdbc8673c 100644
--- a/tests/library/Class/WebService/SIGB/OpsysServiceTest.php
+++ b/tests/library/Class/WebService/SIGB/OpsysServiceTest.php
@@ -476,6 +476,7 @@ abstract class OpsysServiceWithSessionTestCase extends ModelTestCase {
     $donnes_infos->ValeursDonnees->string = ['Tin', 'Tin', '10/12/2012', '23 45 67 89', '1', '1', 'X00987', '10/12/1983'];
 
     $this->search_client
+      ->whenCalled('getWsdlUrl')->answers('true')
       ->whenCalled('OuvrirSession')->answers($this->ouvre_session_res)
       ->whenCalled('FermerSession')->answers(null)
       ->whenCalled('EmprAuthentifier')->answers($auth_response)
diff --git a/tests/library/Class/WebService/SIGB/OrpheeFixtures.php b/tests/library/Class/WebService/SIGB/OrpheeFixtures.php
index c662b23e9f2..025ed68ee2b 100644
--- a/tests/library/Class/WebService/SIGB/OrpheeFixtures.php
+++ b/tests/library/Class/WebService/SIGB/OrpheeFixtures.php
@@ -479,10 +479,11 @@ class OrpheeFixtures {
   }
 
 
-  public static function xmlGetLstPretHenryDupont() {
-    return '<datas>
+  public static function xmlGetLstPretHenryDupont(int $nb_prets = 2) {
+    return <<<XML_PRETS
+    <datas>
   <infos>
-    <nb_res> <![CDATA[2]]> </nb_res> <!-- Nombre de résultats -->
+    <nb_res> <![CDATA[$nb_prets]]> </nb_res> <!-- Nombre de résultats -->
   </infos>
   <documents>
     <document>
@@ -513,7 +514,8 @@ class OrpheeFixtures {
       <tit></tit>  <!-- Titre -->
     </document>
   </documents>
-</datas> ';
+</datas>
+XML_PRETS;
   }
 
   public static function xmlGetLstPretHenryDupontVersion092015() {
diff --git a/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php b/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php
index 74aa9b04069..cdf3b8bae03 100644
--- a/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php
+++ b/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php
@@ -168,6 +168,13 @@ abstract class OrpheeServiceTestCase extends ModelTestCase {
   }
 
 
+  public function tearDown() : void
+  {
+    Class_WebService_SIGB_Orphee::reset();
+    parent::tearDown();
+  }
+
+
   public function _beforeOrpheeServiceCreate(){
     $this->_search_client
       ->whenCalled('hasFunction')->with('GetId')->answers(true)
@@ -213,20 +220,7 @@ class OrpheeServiceBorrowerSoapActionDetectionTest extends OrpheeServiceTestCase
 
 
   /** @test */
-  public function withGetAdhButNotLoginThroughSIGBOnlyProvidesChangePasswordShouldAnswerFalse() {
-    Class_AdminVar::set('LOGIN_THROUGH_SIGB_ONLY', 0);
-    $this->_search_client
-      ->whenCalled('hasFunction')
-      ->with('SetPwdAdh')
-      ->answers(true);
-
-    $this->assertFalse($this->_orphee->providesChangePasswordService());
-  }
-
-
-  /** @test */
-  public function withGetAdhAndLoginThroughSIGBOnlyProvidesChangePasswordShouldAnswerTrue() {
-    Class_AdminVar::set('LOGIN_THROUGH_SIGB_ONLY', 1);
+  public function withSetPwdAdhProvidesChangePasswordShouldAnswerTrue() {
     $this->_search_client
       ->whenCalled('hasFunction')
       ->with('SetPwdAdh')
@@ -1004,6 +998,7 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 3,
                     'id_origine' => 'A3',
+                    'code' => 'A3',
                     'libelle' => 'Annecy Bonlieu',
                     'id_bib' => 3]);
 
@@ -1013,6 +1008,22 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
                     'libelle' => 'Romains']);
 
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 22,
+                    'code' => 17,
+                    'id_origine' => 17,
+                    'id_bib' => 42,
+                    'libelle' => 'Bib départementale ariège']);
+
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 23,
+                    'code' => 17,
+                    'id_origine' => 17,
+                    'id_bib' => 6,
+                    'libelle' => 'Mediatheque Foix']);
+
+
     $this->fixture(Class_Bib::class,
                    ['id' => 3,
                     'libelle' => 'Bonlieu']);
@@ -1023,6 +1034,7 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
                                  'code_barres' => '123456',
                                  'annexe' => 'A3',
                                  'id_bib' => 3,
+                                 'annexe' => 3,
                                  'notice' => $this->fixture(Class_Notice::class,
                                                             ['id' => 5,
                                                              'titre_principal' => 'Harry Potter',
@@ -1033,6 +1045,7 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
                                 ['id' => 32,
                                  'code_barres' => '98374',
                                  'id_bib' => 3,
+                                 'annexe' => 3,
                                  'notice' => $this->fixture(Class_Notice::class,
                                                             ['id' => '974898302',
                                                              'titre_principal' => 'Le Chemin',
@@ -1042,6 +1055,7 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
                                     Class_Exemplaire::class,
                                     ['id' => 33,
                                      'code_barres' => '678',
+                                     'annexe' => 3,
                                      'id_bib' => 3]);
 
 
@@ -1062,15 +1076,25 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
       ->answers(true);
 
     $this->emprunteur = $this->_orphee->getEmprunteur($this->_henry_dupont);
+    $this->emprunteur->setIdIntBib(6);
     $this->_reservations = $this->emprunteur->getReservations();
   }
 
 
+  public function tearDown() : void
+  {
+    $this->emprunteur = null;
+    $this->_reservations = null;
+    parent::tearDown();
+  }
+
+
   /** @test */
   public function emprunteurShouldNotBeEmpty() {
     $this->assertNotEmpty($this->emprunteur);
   }
 
+
   /** @test */
   public function emprunteurShouldNotBeBlocked() {
     $this->assertFalse($this->emprunteur->isBlocked());
@@ -1097,6 +1121,12 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
   }
 
 
+  /** @test */
+  public function emprunteurLibraryNameShouldBeMediathequeFoix() {
+    $this->assertEquals('Mediatheque Foix', $this->emprunteur->getLibraryLabel());
+  }
+
+
   /** @test */
   public function firstWaitingToBePulledHoldShouldHaveLocationId3ForBonlieu() {
     $this->assertEquals(3, $this->emprunteur->getHoldsWaitingToBePulled()[0]->getLocationId());
@@ -1544,6 +1574,7 @@ class OrpheeServiceGetInfoUserCarteHenryDupontVersion092015Test extends OrpheeSe
 
 
     $this->emprunteur = $this->_orphee->getEmprunteur($this->_henry_dupont);
+    $this->emprunteur->setIdIntBib(6);
     $this->_reservations = $this->emprunteur->getReservations();
   }
 }
diff --git a/tests/library/ZendAfi/View/Helper/Abonne/ResumeTest.php b/tests/library/ZendAfi/View/Helper/Abonne/ResumeTest.php
index f7a191e9dc3..09e0598cb26 100644
--- a/tests/library/ZendAfi/View/Helper/Abonne/ResumeTest.php
+++ b/tests/library/ZendAfi/View/Helper/Abonne/ResumeTest.php
@@ -40,7 +40,7 @@ class View_Helper_Abonne_ResumeAsAbonneSIGBTest extends ViewHelperTestCase
     // Salut moi de 2024, le 15 mars 2022 on faisait tomber les masques contre le COVID 19 et ce test plantait… Alors on rajoute 2 ans pour mieux nous retrouver. À suivre  …
     $emprunteur
       ->empruntsAddAll([Class_WebService_SIGB_Emprunt::newInstanceWithEmptyExemplaire()->setDateRetour('13/03/2011'),
-                        Class_WebService_SIGB_Emprunt::newInstanceWithEmptyExemplaire()->setDateRetour('13/03/2024')]);
+                        Class_WebService_SIGB_Emprunt::newInstanceWithEmptyExemplaire()->setDateRetour('13/07/2024')]);
 
     $user = Class_Users::newInstanceWithId(3,
                                            ['fiche_sigb' => ['fiche' => $emprunteur],
diff --git a/tests/library/ZendAfi/View/Helper/FacettesTest.php b/tests/library/ZendAfi/View/Helper/FacettesTest.php
index 3fda5cf2101..f0a73cf73f0 100644
--- a/tests/library/ZendAfi/View/Helper/FacettesTest.php
+++ b/tests/library/ZendAfi/View/Helper/FacettesTest.php
@@ -110,15 +110,18 @@ abstract class ZendAfi_View_Helper_FacettesTestCase extends ViewHelperTestCase {
     $this->_facettes = ['T' => ['T1' => 34],
                         'M' => ['M6567' => 22],
                         'HGENR' => ['HGENR0001' => 1],
-                        'Y' => ['YMDE' => 1,
-                                'YSEY' => 1,
-                                'YMEY' => 1],
+
+                        'Y' => ['Y1' => 1,
+                                'Y2' => 1,
+                                'Y3' => 1
+                        ],
                         'S' => ['S1' => 1,
                                 'S2' => 1,
                                 'S3' => 1],
                         'G' => ['G4' => 1,
                                 'G5' => 1,
                                 'G6' => 1],
+
                         'A' => ['A4656' => 58,
                                 'A4657' => 40,
                                 'A6752' => 21,
@@ -174,7 +177,7 @@ class ZendAfi_View_Helper_FacettesRestrainedTest extends ZendAfi_View_Helper_Fac
   /** @test */
   public function linkForFacetMaisonEnvironnementShouldBePresentAtSecondPosition() {
     $this->assertXPathContentContains($this->_html,
-                                      '//div[@class="facette"]//ul/li[2]//ul//a[@class="facette"][contains(@href, "/recherche/simple/facette/YMDE")]',
+                                      '//div[@class="facette"]//ul/li[2]//ul//a[@class="facette"][contains(@href, "/recherche/simple/facette/Y1")]',
                                       'Maison environnement');
   }
 
@@ -318,6 +321,7 @@ class ZendAfi_View_Helper_DisabledMultiFacettesTest extends ZendAfi_View_Helper_
 
 
 
+
 class ZendAfi_View_Helper_FacettesFromAdvancedSearchTest
   extends ZendAfi_View_Helper_FacettesTestCase {
 
@@ -345,23 +349,9 @@ class ZendAfi_View_Helper_FacettesFromAdvancedSearchTest
 
 
   /** @test */
-  public function checkboxForFacetYMDEshouldNotBeChecked() {
-    $this->assertXPath($this->_html,
-                       '//li[@class="facet_item facette"]//input[@name="multifacet_YMDE"][@value="0"]');
-  }
-
-
-  /** @test */
-  public function checkboxForFacetYSEYshouldBeChecked() {
-    $this->assertXPath($this->_html,
-                       '//li[@class="facet_item facette"]//input[@name="multifacet_YSEY"][@value="1"]');
-  }
-
-
-  /** @test */
-  public function checkboxForFacetYMEYshouldBeChecked() {
+  public function checkboxForFacetYOneshouldNotBeChecked() {
     $this->assertXPath($this->_html,
-                       '//li[@class="facet_item facette"]//input[@name="multifacet_YMEY"][@value="1"]');
+                       '//li[@class="facet_item facette"]//input[@name="multifacet_Y1"][@value="0"]');
   }
 
 
diff --git a/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php b/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php
index 70dbcf1ac68..f6a012093cb 100644
--- a/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php
+++ b/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php
@@ -20,6 +20,7 @@
  */
 
 require_once 'tests/fixtures/ChamberyKohaFixtures.php';
+require_once 'tests/fixtures/KohaFixtures.php';
 
 abstract class HandleBranchcodeTestCase extends AbstractControllerTestCase {
 
@@ -127,8 +128,9 @@ class HandleBranchcodeHoldItemTest extends HandleBranchcodeTestCase {
       ->with($this->ilsdi . '?service=HoldItem&patron_id=18&bib_id=16&item_id=&pickup_location=CHY-GB')
       ->willDo(function(): void {$this->expected_call = true;});
 
-    $this->fixture('Class_Exemplaire',
+    $this->fixture(Class_Exemplaire::class,
                    ['id' => 16,
+                    'id_int_bib' => 3,
                     'id_origine' => 16]);
   }
 
@@ -151,83 +153,173 @@ class HandleBranchcodeHoldItemTest extends HandleBranchcodeTestCase {
 
 
 
-class HandleBranchcodeHoldItemAndHoldTitleTest extends HandleBranchcodeTestCase {
+abstract class HandleBranchcodeHoldsWithItemTestCase extends AbstractControllerTestCase {
+  public function setUp() : void
+  {
+    parent::setUp();
+    Class_AdminVar::set('KOHA_TRY_HOLD_ITEM', 1);
 
-  protected $expected_call = [];
+    $koha_profile = Class_IntProfilDonnees::forKoha();
+    $koha_profile->save();
 
+    Class_AdminVar::newInstanceWithId('KOHA_MULTI_SITES', ['valeur' => '' ]);
+    Class_CosmoVar::setValueOf('site_retrait_resa', 2);
+    $this->ilsdi = 'http://chamb.com/koha/ilsdi.pl';
 
-  /** @test */
-  public function wsKohaShouldBeCallTwiceWithUnavailabeStatus() {
-    Class_AdminVar::set('KOHA_TRY_HOLD_ITEM', 1);
+    $this->service = Class_WebService_SIGB_KohaLegacy::getService(['url_serveur' => $this->ilsdi,
+                                                                   'id_bib' => 3,
+                                                                   'type' => Class_IntBib::COM_KOHA_LEGACY]);
 
-    $this->mock_web_client
-      ->whenCalled('open_url')
-      ->with($this->ilsdi . '?service=HoldItem&patron_id=18&item_id=&pickup_location=CHY-GB')
-      ->willDo(function(): void {$this->expected_call [] = 1;})
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 58,
+                    'code' => 'CHYGB',
+                    'libelle' => 'Bibliothèque Georges Brassens',
+                    'id_origine' => 'CHY-GB']);
 
-      ->whenCalled('open_url')
-      ->with($this->ilsdi . '?service=HoldTitle&patron_id=18&request_location=127.0.0.1&pickup_location=CHY-GB')
-      ->willDo(function(): void {$this->expected_call [] = 2;})
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 15,
+                    'code' => 'VS',
+                    'libelle' => 'Valensole',
+                    'id_origine' => 'CHY-GB']);
 
-      ->beStrict();
+    $sigb_gb = $this->fixture(Class_IntBib::class,
+                              [
+                               'id' => 3,
+                               'comm_params' => ['url_serveur' => $this->ilsdi],
+                               'comm_sigb' => Class_IntBib::COM_KOHA_LEGACY
+                              ]);
 
-    $this->fixture('Class_Exemplaire',
-                   ['id' => 16]);
+    $this->fixture(Class_Bib::class,
+                   ['id' => 12,
+                    'libelle' => 'Bibliothèque Georges Brassens',
+                    'annexe' => 15,
+                    'int_bib' => $sigb_gb]);
 
-    $this->dispatch('/opac/recherche/reservation-pickup-ajax/id_notice/15/id_int_bib/1/id_bib/1/copy_id/16/code_annexe/CHYGB', true);
+    $this->user = $this->fixture(Class_Users::class,
+                                 ['id' => 78,
+                                  'login' => 'Chambelle',
+                                  'password' => 'upw',
+                                  'idabon' => '93658',
+                                  'id_site' => 12,
+                                  'int_bib' => $sigb_gb,
+                                  'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB]);
+
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 16,
+                    'id_origine' => 1342,
+                    'code_barres' => 45679,
+                    'id_data_profile' => $koha_profile->getId(),
+                    'zone_995' => serialize(['code' => '9',
+                                             'valeur'=> '09876']),
+                    'id_int_bib' => 3]);
 
-    $this->assertEquals([1,2], $this->expected_call);
+    ZendAfi_Auth::getInstance()->logUser($this->user);
   }
+}
 
 
-  /** @test */
-  public function wsKohaShouldBeCallTwiceWithItemHoldNotAllowed() {
-    Class_AdminVar::set('KOHA_TRY_HOLD_ITEM', 1);
 
-    $this->mock_web_client
-      ->whenCalled('open_url')
-      ->with($this->ilsdi . '?service=HoldItem&patron_id=18&item_id=&pickup_location=CHY-GB')
-      ->willDo(function() {$this->expected_call [] = 1;
-                           return '<HoldItem>
-                                      <code>OpacItemHoldNotAllowed</code>
-                                   </HoldItem>';
-                          })
 
-      ->whenCalled('open_url')
-      ->with($this->ilsdi . '?service=HoldTitle&patron_id=18&request_location=127.0.0.1&pickup_location=CHY-GB')
-      ->willDo(function(): void {$this->expected_call [] = 2;})
+class HandleBranchcodeHoldItemAndHoldTitleTest extends HandleBranchcodeHoldsWithItemTestCase {
 
-      ->beStrict();
+  protected $expected_call = [];
+
+
+  /** @test */
+  public function wsKohaShouldBeCallTwiceWithUnavailabeStatus() {
+
+    Class_HttpClientFactory::forTest()
+      ->addPostRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl',
+                                   ['service'=>'AuthenticatePatron',
+                                    'username'=>'Chambelle',
+                                    'password'=>'upw'],
+                                   KohaFixtures::xmlAuthenticatePatronOk())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoJamesBond())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=GetRecords&id=1342',
+                               KohaFixtures::xmlGetRecordsWithTransfert())
+      //  ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldItem&patron_id=96138&bib_id=&item_id=&pickup_location=CHY-GB',
+      //                       KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldItem&patron_id=96138&bib_id=1342&item_id=',
+                               KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+                               ;
 
-    $this->fixture('Class_Exemplaire',
-                   ['id' => 16]);
 
     $this->dispatch('/opac/recherche/reservation-pickup-ajax/id_notice/15/id_int_bib/1/id_bib/1/copy_id/16/code_annexe/CHYGB', true);
 
-    $this->assertEquals([1,2], $this->expected_call);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls( $this);
+  }
+
+
+  /** @test */
+  public function wsKohaShouldBeCallTwiceWithItemHoldNotAllowed() {
+
+    Class_HttpClientFactory::forTest()
+      ->addPostRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl',
+                                   ['service'=>'AuthenticatePatron',
+                                    'username'=>'Chambelle',
+                                    'password'=>'upw'],
+                                   KohaFixtures::xmlAuthenticatePatronOk())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoJamesBond())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=GetRecords&id=1342',
+                               KohaFixtures::xmlGetRecordsWithTransfert())
+      //      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldItem&patron_id=96138&bib_id=&item_id=&pickup_location=CHY-GB',
+      //                      '<HoldItem>
+        //                           <code>OpacItemHoldNotAllowed</code>
+      //                        </HoldItem>')
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldItem&patron_id=96138&bib_id=1342&item_id=',
+                               '<HoldItem>
+                                   <code>OpacItemHoldNotAllowed</code>
+                                </HoldItem>')
+            ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldTitle&patron_id=96138&bib_id=1342&request_location=127.0.0.1',
+                             KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+      //->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl??service=HoldTitle&patron_id=96138&bib_id=&request_location=127.0.0.1',
+      //                       KohaFixtures::holdTitleResponseWithItemHoldsItypeOut())
+                               ;
+
+    $this->dispatch('/opac/recherche/reservation-pickup-ajax/id_notice/15/id_int_bib/1/id_bib/1/copy_id/16/code_annexe/CHYGB');
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls( $this);
   }
 
 
   /** @test */
   public function wsKohaShouldBeCallOnceWithItemHold() {
-    Class_AdminVar::set('KOHA_TRY_HOLD_ITEM', 1);
 
-    $this->mock_web_client
-      ->whenCalled('open_url')
-      ->with($this->ilsdi . '?service=HoldItem&patron_id=18&item_id=&pickup_location=CHY-GB')
-      ->willDo(function() {$this->expected_call [] = 1;
-                           return '<HoldTitle>
+    Class_HttpClientFactory::forTest()
+      ->addPostRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl',
+                                   ['service'=>'AuthenticatePatron',
+                                    'username'=>'Chambelle',
+                                    'password'=>'upw'],
+                                   KohaFixtures::xmlAuthenticatePatronOk())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoJamesBond())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=GetRecords&id=1342',
+                               KohaFixtures::xmlGetRecordsWithTransfert())
+      ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldItem&patron_id=96138&bib_id=1342&item_id=',
+                               '<HoldTitle>
                                    <title>Mireille l\'abeille</title>
                                    <pickup_location>Bibliothèque Départementale de la Meuse</pickup_location>
-                                 </HoldTitle>';
-                           })
-      ->beStrict();
-
-    $this->fixture('Class_Exemplaire',
-                   ['id' => 16]);
+                                 </HoldTitle>'
+                               )
+      //  ->addRequestWithResponse('http://chamb.com:80/koha/ilsdi.pl?service=HoldItem&patron_id=18&item_id=&pickup_location=CHY-GB',
+      //                       '<HoldTitle>
+      //                             <title>Mireille l\'abeille</title>
+      //                             <pickup_location>Bibliothèque Départementale de la Meuse</pickup_location>
+      //                           </HoldTitle>'
+      //                         )
+      ;
 
     $this->dispatch('/opac/recherche/reservation-pickup-ajax/id_notice/15/id_int_bib/1/id_bib/1/copy_id/16/code_annexe/CHYGB', true);
 
-    $this->assertEquals([1], $this->expected_call);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls( $this);
   }
 }
diff --git a/tests/scenarios/SearchResult/SearchResultTest.php b/tests/scenarios/SearchResult/SearchResultTest.php
index 22a7f67b36e..dc4ea884dc2 100644
--- a/tests/scenarios/SearchResult/SearchResultTest.php
+++ b/tests/scenarios/SearchResult/SearchResultTest.php
@@ -118,14 +118,14 @@ class SearhResultFilterDomainsFromProfilTest extends AbstractControllerTestCase
 
     $this->fixture(Class_CodifThesaurus::class,
                    ['id' => 4,
-                    'id_thesaurus' => 'CCCC0001',
+                    'id_thesaurus' => 'CCCC1',
                     'id_origine' => 4,
                     'code' => 'Catalogue',
                     'libelle' => 'Youths']);
 
     $this->fixture(Class_CodifThesaurus::class,
                    ['id' => 5,
-                    'id_thesaurus' => 'CCCC0002',
+                    'id_thesaurus' => 'CCCC2',
                     'id_origine' => 5,
                     'code' => 'Catalogue',
                     'libelle' => 'Adults']);
diff --git a/tests/scenarios/Templates/TemplatesAbonneCentralizedHoldsTest.php b/tests/scenarios/Templates/TemplatesAbonneCentralizedHoldsTest.php
index 3aa7b6188c1..89def629e53 100644
--- a/tests/scenarios/Templates/TemplatesAbonneCentralizedHoldsTest.php
+++ b/tests/scenarios/Templates/TemplatesAbonneCentralizedHoldsTest.php
@@ -29,11 +29,13 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
   protected
     $_search_client,
     $_orphee,
+    $_orphee2,
     $_henry_dupont,
     $_john_lemon,
     $_foix;
 
-  public function setUp(): void   {
+  public function setUp(): void
+  {
     parent::setUp();
     Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
 
@@ -42,6 +44,38 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
     $comm_params = ['url_serveur' => 'tests/fixtures/orphee.wsdl',
                     'allow_hold_available_items' => true];
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 82,
+                    'id_bib' => 34,
+                    'code' => 34,
+                    'id_origine' => 34,
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 83,
+                    'id_bib' => 37,
+                    'code' => 37,
+                    'id_origine' => 37,
+                    'libelle' => 'WonderTown'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 84,
+                    'id_bib' => 36,
+                    'code' => 'Fx',
+                    'id_origine' => 'Fx',
+                    'libelle' => 'Foix'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 85,
+                    'id_bib' => 36,
+                    'code' => 'MTG',
+                    'id_origine' => 'MTG',
+                    'libelle' => 'Montgaillard'
+                   ]);
+
     $this->fixture(Class_Bib::class,
                    ['id' => 34,
                     'mail' => 'test@jolieville.net',
@@ -53,6 +87,7 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                     'id_bib' => 34,
                     'nom' => 'JolieVille',
                     'nom_court' => 'JV',
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
                     'comm_sigb' => Class_IntBib::COM_ORPHEE,
                     'comm_params' => $comm_params
                    ]);
@@ -69,9 +104,55 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                     'nom' => 'WonderTown',
                     'nom_court' => 'WT',
                     'mail' => 'intbib@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
                     'comm_sigb' => Class_IntBib::COM_ORPHEE,
-                    'comm_params' => array_merge($comm_params,
-                                                 ['use_card_number' => '1301500012'])]);
+                    'comm_params' => ['url_serveur' => 'tests/fixtures/orphee2.wsdl',
+                                      'allow_hold_available_items' => true,
+                                      'use_card_number' => '1301500012']]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 37,
+                    'id_bib' => 37,
+                    'nom' => 'Eightth Wonder',
+                    'nom_court' => '8THW',
+                    'mail' => 'intbibqsdf@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://bib.valensol.net',
+                                      'provide_pickup_locations' => true,
+                                      'use_card_number' => '1301500012']]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 38,
+                    'id_bib' => 38,
+                    'nom' => 'Bibliothèque Sans réservations centralisées',
+                    'nom_court' => 'BSRC',
+                    'mail' => 'bsrc@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://bib.valenciel.net']]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 40,
+                    'id_bib' => 40,
+                    'nom' => 'Bibliothèque Sans réservations centralisées',
+                    'nom_court' => 'BSRC',
+                    'mail' => 'bsrc@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://bib.valenciel.net']]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 39,
+                    'id_bib' => 39,
+                    'nom' => 'Médiathèque Dun',
+                    'nom_court' => 'Dun',
+                    'mail' => 'intbibqsdf@dunedain.net',
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://bib.valensol.net',
+                                      'provide_pickup_locations' => true,
+                                      'use_card_number' => '1301500014']]);
 
     $this->_john_lemon =
       $this->fixture(Class_Users::class,
@@ -83,10 +164,9 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                       'idabon' => '130425',
                       'password' => 'secret',
                       'mail' => 'johnlemon@abbeyroad.net',
-                      'id_int_bib' => 36,
                       'int_bib' => $intbib_wondertown,
                       'bib' => $bib_wondertown,
-                      'id_bib' => 36
+                      'id_sigb' => '789789789'
                      ]);
 
     $this->_foix =
@@ -118,11 +198,18 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                    ['id' => 2345,
                     'id_notice' => 3,
                     'id_bib' => 34,
+                    'annexe' => 34,
                     'id_int_bib' => 34,
                     'type_doc' => Class_TypeDoc::LIVRE,
                     'id_origine' => 312412,
                     'code_barres' => '012345']);
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 37,
+                    'libelle' => 'Paris',
+                    'id_bib' => 37,
+                    'id_origine' => 10]);
+
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 2346,
                     'id_notice' => 3,
@@ -130,8 +217,19 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                     'id_int_bib' => 37,
                     'type_doc' => Class_TypeDoc::LIVRE,
                     'id_origine' => 312413,
+                    'annexe' =>37,
                     'code_barres' => '012346']);
 
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2348,
+                    'id_notice' => 3,
+                    'id_bib' => 38,
+                    'id_int_bib' => 38,
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                    'id_origine' => 312415,
+                    'annexe' => 38,
+                    'code_barres' => '012348']);
+
     $this->fixture(Class_Exemplaire::class,
                    ['id' => 2347,
                     'id_notice' => 3,
@@ -141,16 +239,62 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                     'id_origine' => 312414,
                     'code_barres' => '012347']);
 
-    $this->_search_client = $this->mock();
-    $this->_search_client
+    $this->_search_client = $this->_setupSearchClient();
+
+    $allow_hold_available_items=true;
+    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee.wsdl', null, $allow_hold_available_items);
+    $this->_orphee->setSearchClient($this->_search_client);
+    $this->_orphee->isConnected();
+
+    $this->_henry_dupont =
+      $this->fixture(Class_Users::class,
+                     ['id' => 2,
+                      'login' => '10900000753',
+                      'idabon' => '100753',
+                      'password' => 'secret',
+                      'id_int_bib' => 34,
+                      'id_site' => 34]);
+
+    $this->_henry_dupont->beAbonneSIGB()->assertSave();
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 34,
+                                                          'type' => Class_IntBib::COM_ORPHEE]),
+                                             $this->_orphee);
+
+    $this->_orphee2 = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee2.wsdl',
+                                                                       null,
+                                                                       $allow_hold_available_items);
+    $this->_orphee2->setSearchClient($this->_search_client);
+    $this->_orphee2->beHoldModeItem();
+    $this->_orphee2->isConnected();
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 36,
+                                                          'url_serveur' => 'tests/fixtures/orphee2.wsdl',
+                                                          'type' => Class_IntBib::COM_ORPHEE,
+                                                          'use_card_number' => '1301500012']),
+                                             $this->_orphee2);
+  }
+
+
+  protected function _setupSearchClient()
+  {
+    return $this
+      ->mock()
+
       ->whenCalled('__getLastRequest')
       ->answers('last request')
+
       ->whenCalled('__getLastResponse')
       ->answers('last response')
+
       ->whenCalled('__getLastResponseHeaders')
       ->answers('last response  headers')
+
       ->whenCalled('__getLastRequestHeaders')
       ->answers('last response')
+
       ->whenCalled('hasFunction')
       ->answers(false)
 
@@ -164,6 +308,7 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
 
       ->whenCalled('GetId')
       ->with(new GetId())
+
       ->answers(GetIdResponse::withIdResult('1234'))
 
       ->whenCalled('hasFunction')->with('GetAdh')->answers(true)
@@ -174,6 +319,12 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
                 ->whenCalled('getXml')
                 ->answers(OrpheeFixtures::xmlGetInfoUserCarteHenryDupont()))
 
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('uselogon'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetInfoUserCarteHenryDupont()))
+
       ->whenCalled('GetAdh')
       ->with(GetAdh::withNo('1301500012'))
       ->answers(Storm_Test_ObjectWrapper::mock()
@@ -185,40 +336,38 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
       ->answers(Storm_Test_ObjectWrapper::mock()
                 ->whenCalled('getXml')
                 ->answers(OrpheeFixtures::xmlGetAdhFoix()))
+
       ->whenCalled('GetLstDmt')
       ->with(GetLstDmt::withNtcAndFas('312412', 0))
       ->answers(GetLstDmtResponse::withResult(OrpheeFixtures::xmlGetLstDmtMillenium()))
 
-      ->whenCalled('RsvNtcAdh')
-      ->with(RsvNtcAdh::withNoticeUserNo('312412', '1301500012', 0))
-      ->answers(RsvNtcAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'));
+      ->whenCalled('GetLstDmt')
+      ->with(GetLstDmt::withNtcAndFas('312414', 0))
+      ->answers(GetLstDmtResponse::withResult(OrpheeFixtures::xmlGetLstDmtMillenium()))
 
-    $allow_hold_available_items=true;
-    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee.wsdl', null, $allow_hold_available_items);
-    $this->_orphee->setSearchClient($this->_search_client);
-    $this->_orphee->isConnected();
+      ->whenCalled('GetLstRsv')
+      ->with(GetLstRsv::withAdhAndCountPerPage(1301500012, 200))
+      ->answers(GetLstRsvResponse::withResult(OrpheeFixtures::xmlGetLstRsvHenryDupont()))
 
-    $this->_henry_dupont =
-      $this->fixture(Class_Users::class,
-                     ['id' => 2,
-                      'login' => '10900000753',
-                      'idabon' => '100753',
-                      'password' => 'secret',
-                      'id_int_bib' => 34,
-                      'id_site' => 34]);
+      ->whenCalled('RsvNtcAdh')
+      ->with(RsvNtcAdh::withNoticeUserNo('312414', '1301500012', 0))
+      ->answers(RsvNtcAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
 
-    $this->_henry_dupont->beAbonneSIGB()->assertSave();
+      ->whenCalled('RsvNtcAdh')
+      ->with(RsvNtcAdh::withNoticeUserNo('312412', '1301500012', 0))
+      ->answers(RsvNtcAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
 
-    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
-                                                         ['id_bib' => 34,
-                                                          'type' => Class_IntBib::COM_ORPHEE]),
-                                             $this->_orphee);
+      ->whenCalled('RsvDmtAdh')
+      ->with(RsvDmtAdh::withNoticeItemUser('312414', '', '100753'))
+      ->answers(RsvDmtAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
 
-    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
-                                                         ['id_bib' => 36,
-                                                          'type' => Class_IntBib::COM_ORPHEE,
-                                                          'use_card_number' => '1301500012']),
-                                             $this->_orphee);
+      ->whenCalled('RsvDmtAdh')
+      ->with(RsvDmtAdh::withNoticeItemUser('312414', '', '1301500012'))
+      ->answers(RsvDmtAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
+
+      ->whenCalled('GetLstRsv')
+      ->with(GetLstRsv::withAdhAndCountPerPage('100753', 200))
+      ->answers(GetLstRsvResponse::withResult(OrpheeFixtures::xmlGetLstRsvHenryDupont()));
   }
 
 
@@ -229,24 +378,67 @@ abstract class TemplatesAbonneCentralizedHoldsTestCase
 }
 
 
+
+
+class TemplatesAbonneCentralizedHoldsWithHoldModeFailuresTest extends TemplatesAbonneCentralizedHoldsTestCase {
+
+  public function _setHoldMode() {
+    $this->_orphee->beHoldModeItem();
+    return $this;
+  }
+
+
+  /** @test */
+  public function johnLemonReserveItem2345ShouldReturnDocumentUnavailable() {
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
+    ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
+    $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2345/id_origine/312412/id/123');
+    $this->assertContains('Votre réservation est enregistrée.<br>Nous vous informerons quand le document \'La disparition / PEREC, Georges\' sera disponible',
+                          json_decode($this->_response->getBody())->content);
+  }
+
+
+  /** @test */
+  public function noParameterForCentralizedLibraryShouldReturnError() {
+    Class_CosmoVar::setValueOf('centralized_hold_mode',39);
+    ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
+    $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2346/id_origine/312412/id/123');
+    $this->assertContains('Vous ne pouvez pas réserver cet exemplaire !',
+                          json_decode($this->_response->getBody())->content);
+  }
+
+
+  /** @test */
+  public function foixUserReservationPickupAjaxShouldReturnReservationEnregistree() {
+    Class_CosmoVar::setValueOf('centralized_hold_mode',34);
+    ZendAfi_Auth::getInstance()->logUser($this->_foix);
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/34/copy_id/2345/id_origine/312412/id/123');
+    $this->assertContains('Votre réservation est enregistrée.<br>Nous vous informerons',
+                          json_decode($this->_response->getBody())->content);
+  }
+}
+
+
+
+
 class TemplatesAbonneCentralizedHoldsFailuresTest extends TemplatesAbonneCentralizedHoldsTestCase {
 
   protected $_response;
   /** @test */
   public function HenryDupontShouldUseUniquePatronShouldBeFalse() {
-    $this->assertFalse($this->_henry_dupont->shouldUseUniquePatron());
+    $this->assertFalse($this->_henry_dupont->shouldUseUniquePatron(Class_Exemplaire::find(2346)));
   }
 
 
   /** @test */
   public function JohnLemonShouldUseUniquePatronShouldBeTrue() {
-    $this->assertTrue($this->_john_lemon->shouldUseUniquePatron());
+    $this->assertTrue($this->_john_lemon->shouldUseUniquePatron(Class_Exemplaire::find(2345)));
   }
 
 
   /** @test */
   public function FoixShouldUseUniquePatronShouldBeFalse() {
-    $this->assertFalse($this->_foix->shouldUseUniquePatron());
+    $this->assertFalse($this->_foix->shouldUseUniquePatron(Class_Exemplaire::find(2345)));
   }
 
 
@@ -260,7 +452,16 @@ class TemplatesAbonneCentralizedHoldsFailuresTest extends TemplatesAbonneCentral
   public function johnLemonReserveItem2346ShouldReturnReservationImpossibleNonReservable() {
     ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
     $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2346/id_origine/312412/id/123');
-    $this->assertContains('Réservation impossible : l\'exemplaire n\'est pas réservable',
+    $this->assertContains('Vous ne pouvez pas réserver cet exemplaire !',
+                          json_decode($this->_response->getBody())->content);
+  }
+
+
+  /** @test */
+  public function johnLemonReserveItem23247ShouldReturnReservationEnregistree() {
+    ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
+    $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2347/id_origine/312414/id/123');
+    $this->assertContains('Votre réservation est enregistrée.',
                           json_decode($this->_response->getBody())->content);
   }
 
@@ -268,9 +469,10 @@ class TemplatesAbonneCentralizedHoldsFailuresTest extends TemplatesAbonneCentral
   /** @test */
   public function henryDupontReservItem2345ShouldReturnReservationImpossibleDocumentBibliothequeCentrale() {
     Class_CosmoVar::setValueOf('centralized_hold_mode',36);
+    $this->_henry_dupont->setIntBib(Class_IntBib::find(38));
     ZendAfi_Auth::getInstance()->logUser($this->_henry_dupont);
     $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2347/id_origine/312412/id/123');
-    $this->assertContains('votre bibliothèque n\'autorise pas la réservation des documents de la bibliotheque centrale',
+    $this->assertContains('Vous ne pouvez pas réserver cet exemplaire !',
                           json_decode($this->_response->getBody())->content);
 
   }
@@ -287,6 +489,9 @@ class TemplatesAbonneCentralizedHoldsReservationSuccessTest
     $this->_mock_transport = new MockMailTransport();
     Zend_Mail::setDefaultTransport($this->_mock_transport);
     Class_Profil::getCurrentProfil()->setMailSite('laurent@afi-sa.fr');
+    $this->_john_lemon
+      ->setIdIntBib(37)
+      ->setIntBib(Class_IntBib::find(37));
     ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
     $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2345/id_origine/312412/id/123');
   }
@@ -319,7 +524,7 @@ class TemplatesAbonneCentralizedHoldsReservationSuccessTest
 
   public function getRecipients(){
     return [[0, 'test@jolieville.net'],
-            [1, 'intbib@WonderTown.net'],
+            [1, 'intbibqsdf@WonderTown.net'],
             [2,'johnlemon@abbeyroad.net']];
   }
 
@@ -370,12 +575,10 @@ HTML_LIBRARY;
 
     $mail_to_user= <<<HTML_USER
 <p>Bonjour,</p>
-<p>Une réservation a été effectuée par un utilisateur de votre bibliothèque sur le portail BDP.</p>
-<p>Il s'agit du document : La disparition de Code Barre : 012345</p>
-<p>Pour l'utilisateur : John Lemon</p>
-<p>Pour la bibliothèque : WonderTown.
-La réservation a été reportée dans le Orphée BDP au nom de votre bibliothèque avec le code adhérent 1301500012.
-Le document doit vous être envoyé par la BDP.
+<p>Vous avez demandé à faire venir le document La disparition de la Bibliothèque Départemantale de Prêt à la bibliothèque WonderTown.</p>
+<p>Votre bibliothèque vous informera lorsque ce document sera disponible.</p>
+<p>Cordialement,</p>
+<p>La Bibliothèque Départementale de l'Ariège.</p>
 HTML_USER;
 
     return [[0, $mail_to_central_library],
@@ -390,3 +593,866 @@ HTML_USER;
     $this->assertEquals(quoted_printable_decode($this->_mock_transport->getSentMails()[$email_count]->getBodyHtml()->getContent()), $mail_content);
   }
 }
+
+
+
+
+include_once 'tests/fixtures/NanookFixtures.php';
+
+class TemplatesAbonneCentralizedHoldsReservationPickupAjaxNetworkTest
+  extends TemplatesAbonneCentralizedHoldsTestCase{
+  protected
+    $_mock_web_client,
+    $_service;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this
+      ->_john_lemon
+      ->setIntBib(Class_IntBib::find(37));
+
+    $this->_mock_web_client = $this->mock()
+                                   ->whenCalled('open_url')
+                                   ->with('http://bib.valensol.net/service/GetPickupLocation/bibId/0/patronId/0/siteId/0')
+                                   ->answers(NanookFixtures::pickupLocationsOkAnswer())
+
+                                   ->whenCalled('open_url')
+                                   ->with('http://bib.valensol.net/service/GetPickupLocation/bibId/312413/patronId/789789789/siteId/37')
+                                   ->answers(NanookFixtures::pickupLocationsOkAnswer())
+
+                                   ->whenCalled('open_url')
+                                   ->with('http://bib.valensol.net/service/HoldTitle/bibId/312413/patronId/789789789/pickupLocation/')
+                                   ->answers(NanookFixtures::pickupLocationsOkAnswer())
+
+                                   ->whenCalled('open_url')
+                                   ->with('http://bib.valensol.net/service/GetPickupLocation/bibId/312413/patronId/789789789/siteId/10')
+                                   ->answers(NanookFixtures::pickupLocationsOkAnswer())
+
+                                   ->whenCalled('hasErrors')
+                                   ->answers(false);
+
+    $_service = Class_WebService_SIGB_Nanook_Service::newInstance()
+      ->setServerRoot('http://bib.valensol.net')
+      ->setUseCardNumber('1301500012')
+      ->setWebClient($this->_mock_web_client);
+
+    $this
+      ->_service = Class_WebService_SIGB_Nanook::setService(['url_serveur' => 'http://bib.valensol.net',
+                                                             'provide_pickup_locations' => true,
+                                                             'use_card_number' => '1301500012',
+                                                             'id_bib' => 37,
+                                                             'type' => 7],
+                                                            $_service);
+
+    Class_WebService_SIGB_AbstractRESTService::shouldThrowError(true);
+
+    ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
+  }
+
+
+  public function tearDown() : void
+  {
+    $this->_service = null;
+    $this->_mock_web_client = null;
+    Class_WebService_SIGB_Nanook::reset();
+    Class_WebService_SIGB_AbstractRESTService::shouldThrowError(false);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function reservationPickupAjaxForItemDifferentIlsShouldDisplayReservationImpossible() {
+    Class_CosmoVar::setValueOf('site_retrait_resa', 0);
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/36/copy_id/2347/id_origine/312414/id/123');
+    $this->assertContains(json_decode( $this->_response->getBody())->content,'Vous ne pouvez pas réserver cet exemplaire !');
+    $this->assertContains(json_decode( $this->_response->getBody())->title,'Réservation impossible');
+  }
+
+
+  /** @test */
+  public function reservationPickupAjaxForItemCentralLibraryShouldDisplayReservationEnregistree() {
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/34/copy_id/2345/id_origine/312412/id/123');
+    $this->assertContains('Votre réservation est enregistrée',json_decode( $this->_response->getBody())->content);
+    $this->assertContains(json_decode( $this->_response->getBody())->title,'Réservation');
+  }
+
+
+  /** @test */
+  public function reservationPickupAjaxForItemSameIlsShouldShowIlsPickupLibraries() {
+    Class_CosmoVar::setValueOf('site_retrait_resa', 1);
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/37/copy_id/2346/id_origine/312413/id/123');
+    $this->assertContains('First library', json_decode($this->_response->getBody())->content);
+  }
+
+
+  /** @test */
+  public function reservationPickupAjaxForItemNotInIlsCentralizedHoldModeShouldCallCentralizedHold() {
+    Class_CosmoVar::setValueOf('site_retrait_resa', 0);
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 36);
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/36/copy_id/2347/id_origine/312414/id/123');
+    $this->assertContains('Votre réservation est enregistrée',json_decode( $this->_response->getBody())->content);
+  }
+
+
+  /** @test */
+  public function reservationPickupAjaxForItemNotInIlsCentralizedHoldModeAndSiteRetraitResaEnabledShouldCallCentralizedHold() {
+    Class_CosmoVar::setValueOf('site_retrait_resa', 1);
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 36);
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/36/copy_id/2347/id_origine/312414/id/123');
+    $this->assertContains('Votre réservation est enregistrée',json_decode( $this->_response->getBody())->content);
+  }
+
+
+  /** @test */
+  public function reservationPickupAjaxWithUnknownItemShouldContainsError() {
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/36/copy_id/7897892347/id_origine/312414/id/123');
+    $this->assertContains('Document introuvable',json_decode( $this->_response->getBody())->content);
+  }
+}
+
+
+
+
+class TemplatesAbonneCentralizedHoldsPickupLocationForOrpheeTest
+  extends TemplatesAbonneCentralizedHoldsTestCase
+{
+  protected $_json;
+  protected $_xpath;
+  public function setUp() : void
+  {
+    parent::setUp();
+    $user = $this->fixture(Class_Users::class,
+                   ['id' => 98,
+                    'nom' => 'foix',
+                    'prénom' => 'Madame',
+                    'idAbon' => '130154',
+                    'id_int_bib' => 36,
+                    'id_bib' => 36,
+                    'password' => '@qsdlkfj',
+                    'login' => 'mfoix',
+                    'id_origine' => 2243,
+                    'mail' => 'test@exemple.com'
+                   ]);
+    $comm_params = ['url_serveur' => 'tests/fixtures/orphee4.wsdl',
+                    'allow_hold_available_items' => true,
+                    'use_card_number' => '1301500012'];
+    Class_IntBib::find(36)->setCommParams($comm_params)->assertSave();
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee4.wsdl', null, true);
+    $this->_orphee->setSearchClient($this->_search_client);
+    $this->_orphee->isConnected();
+
+    Class_CosmoVar::setValueOf('site_retrait_resa', 1);
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 36,
+                                                          'type' => Class_IntBib::COM_ORPHEE,
+                                                          'use_card_number' => '1301500012']),
+                                             $this->_orphee);
+
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/36/copy_id/2347/id_origine/312414/id/2243');
+    $this->_xpath = new Storm_Test_XPath;
+    $this->_json = (json_decode($this->_response->getBody()))->content;
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTwoChoices()
+  {
+    $this->_xpath->assertXPathCount($this->_json,
+                                    '//label[contains(@class,"multi-element-label col-form-label")]',
+                                    2
+    );
+  }
+
+
+  /** @test */
+  public function firstLabelShouldContainsFoix()
+  {
+    $this->_xpath->assertXPathContentContains($this->_json,
+                                              '(//label[contains(@class,"multi-element-label col-form-label")])[1]',
+                                              'Foix');
+  }
+
+
+  /** @test */
+  public function secondLabelShouldContainsMontgaillard()
+  {
+    $this->_xpath->assertXPathContentContains($this->_json,
+                                              '(//label[contains(@class,"multi-element-label col-form-label")])[2]',
+                                              'Montgaillard');
+  }
+}
+
+
+
+
+abstract class TemplatesAbonneCentralizedHoldsForOrpheeMultipleIntBibsTestCase
+  extends TemplatesAbonneCentralizedHoldsTestCase
+{
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this->fixture(Class_Bib::class,
+                   ['id' => 41,
+                    'mail' => 'test@WonderTown.net',
+                    'libelle' => 'WonderTown'
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 41,
+                    'id_bib' => 41,
+                    'nom' => 'SleepyHollowTown',
+                    'nom_court' => 'WT',
+                    'mail' => 'intbib@slTown.net',
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
+                    'comm_sigb' => Class_IntBib::COM_ORPHEE,
+                    'comm_params' => ['url_serveur' => 'tests/fixtures/orphee4.wsdl',
+                                      'allow_hold_available_items' => true,
+                                      'use_card_number' => '1301500012']]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 42,
+                    'mail' => 'test@wonkaTown.net',
+                    'libelle' => 'WonkaTown'
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 42,
+                    'id_bib' => 42,
+                    'nom' => 'WonkaTown',
+                    'nom_court' => 'WT',
+                    'mail' => 'intbib@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
+                    'comm_sigb' => Class_IntBib::COM_ORPHEE,
+                    'comm_params' => ['url_serveur' => 'tests/fixtures/orphee4.wsdl',
+                                      'allow_hold_available_items' => true,
+                                      'use_card_number' => '1301500012']]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 75,
+                    'id_bib' => 41,
+                    'id_origine' => 25,
+                    'code' => 25,
+                    'libelle' => 'SleepyHollowTown',
+                    'no_pickup' => 0,]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 76,
+                    'id_bib' => 41,
+                    'id_origine' => 26,
+                    'code' => 26,
+                    'libelle' => 'SleepyHollowTown 2',
+                    'no_pickup' => 0,]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 77,
+                    'id_bib' => 41,
+                    'id_origine' => 27,
+                    'code' => 'SL3NotShown',
+                    'libelle' => 'SleepyHollowTown Not shown',
+                    'no_pickup' => 1,]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 78,
+                    'id_bib' => 42,
+                    'id_origine' => 28,
+                    'code' => 'WKT',
+                    'libelle' => 'WonkaTown',
+                    'no_pickup' => 0,]);
+
+    $user = $this->fixture(Class_Users::class,
+                   ['id' => 98,
+                    'nom' => 'foix',
+                    'prénom' => 'Madame',
+                    'idAbon' => '10900000753',
+                    'id_int_bib' => 36,
+                    'id_bib' => 36,
+                    'password' => '@qsdlkfj',
+                    'login' => '10900000753',
+                    'id_origine' => 2243
+                   ]);
+
+    $comm_params = ['url_serveur' => 'tests/fixtures/orphee4.wsdl',
+                    'allow_hold_available_items' => true,
+                    'use_card_number' => '1301500012'];
+    Class_IntBib::find(36)->setCommParams($comm_params)->assertSave();
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee4.wsdl', null, true);
+    $this->_search_client
+      ->whenCalled('GetLstDmt')
+      ->with(GetLstDmt::withNtcAndFas('312414', 0))
+      ->answers(GetLstDmtResponse::withResult(OrpheeFixtures::xmlGetLstDmtMillenium()))
+
+      ->whenCalled('RsvDmtAdh')
+      ->with(RsvDmtAdh::withNoticeItemUser('312414', '', '100753'))
+      ->answers(RsvDmtAdhResponse::withResult('<?xml version="1.0" encoding="utf-8"?><datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[YESS ]]></libelle></msg></datas>'))
+
+      ->whenCalled('GetLstRsv')
+      ->with(GetLstRsv::withAdhAndCountPerPage(100753, 200))
+      ->answers(GetLstRsvResponse::withResult(OrpheeFixtures::xmlGetLstRsvHenryDupont()));
+
+
+    $this->_orphee->setSearchClient($this->_search_client);
+    $this->_orphee->beHoldModeItem();
+    $this->_orphee->isConnected();
+
+    Class_CosmoVar::setValueOf('site_retrait_resa', 1);
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 36,
+                                                          'type' => Class_IntBib::COM_ORPHEE,
+                                                          'use_card_number' => '1301500012']),
+                                             $this->_orphee);
+  }
+}
+
+
+
+
+class TemplatesAbonneCentralizedHoldsPickupLocationForOrpheeMultipleIntBibsTest
+  extends TemplatesAbonneCentralizedHoldsForOrpheeMultipleIntBibsTestCase
+{
+  protected $_xpath, $_json;
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/36/copy_id/2347/id_origine/312414/id/2243');
+    $this->_xpath = new Storm_Test_XPath;
+    $this->_json = (json_decode($this->_response->getBody()))->content;
+  }
+
+
+  /** @test */
+  public function pageShouldContainsFiveChoices()
+  {
+    $this->_xpath->assertXPathCount($this->_json,
+                                    '//label[contains(@class,"multi-element-label")]',
+                                    5
+    );
+  }
+
+
+  /** @test */
+  public function thirdLabelShouldContainsSleepyHollowTown()
+  {
+    $this->_xpath->assertXPathContentContains($this->_json,
+                                              '(//label[contains(@class,"multi-element-label col-form-label")])[3]',
+                                              'SleepyHollowTown');
+  }
+
+
+  /** @test */
+  public function fourthLabelShouldContainsSleepyHollowTown2()
+  {
+    $this->_xpath->assertXPathContentContains($this->_json,
+                                              '(//label[contains(@class,"multi-element-label col-form-label")])[4]','SleepyHollowTown 2');
+  }
+
+
+  /** @test */
+  public function fifthLabelShouldContainsWonkaTown()
+  {
+    $this->_xpath->assertXPathContentContains($this->_json,
+                                              '(//label[contains(@class,"multi-element-label col-form-label")])[5]','WonkaTown');
+  }
+}
+
+
+
+
+class TemplatesAbonneCentralizedHoldsReservationAjaxForOrpheeMultipleIntBibsTest
+  extends TemplatesAbonneCentralizedHoldsForOrpheeMultipleIntBibsTestCase
+{
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $this->dispatch('recherche/reservationajax/id_bib/36/copy_id/2347/id_origine/312414/id/2243/code_annexe/76');
+  }
+
+
+  /** @test */
+  public function bodyContentShouldContainsReservationEnregistree()
+  {
+    $this->assertContains('Votre réservation est enregistrée.<br>Nous vous informerons quand le document ',
+                          json_decode( $this->_response->getBody())->content);
+  }
+}
+
+
+
+
+class TemplatesAbonneCentralizedHoldsCentralisedHoldWithErrorsTest
+  extends AbstractControllerTestCase
+{
+  protected $_john_lemon,
+    $_orphee,
+    $_search_client;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+
+    $comm_params = ['url_serveur' => 'tests/fixtures/orphee.wsdl',
+                    'allow_hold_available_items' => true];
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 82,
+                    'id_bib' => 34,
+                    'code' => 34,
+                    'id_origine' => 34,
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 84,
+                    'id_bib' => 36,
+                    'code' => 'Fx',
+                    'id_origine' => 'Fx',
+                    'libelle' => 'Foix'
+                   ]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 34,
+                    'mail' => 'test@jolieville.net',
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 34,
+                    'id_bib' => 34,
+                    'nom' => 'JolieVille',
+                    'nom_court' => 'JV',
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
+                    'comm_sigb' => Class_IntBib::COM_ORPHEE,
+                    'comm_params' => $comm_params
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 37,
+                    'id_bib' => 37,
+                    'nom' => 'Eightth Wonder',
+                    'nom_court' => '8THW',
+                    'mail' => 'intbibqsdf@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_NANOOK,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://bib.valensol.net',
+                                      'provide_pickup_locations' => true,
+                                      'use_card_number' => '1301500012']]);
+
+    $this->_john_lemon =
+      $this->fixture(Class_Users::class,
+                     ['id' => 142,
+                      'idAbon' => '',
+                      'nom' => 'Lemon',
+                      'prenom' => 'John',
+                      'login' => 'uselogon',
+                      'idabon' => '130425',
+                      'password' => 'secret',
+                      'mail' => 'johnlemon@abbeyroad.net',
+                      'id_int_bib' => 37,
+                      'id_bib' => 37,
+                      'id_sigb' => '789789789'
+                     ]);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 3,
+                    'titre' => 'La disparition',
+                    'auteurs' => 'Georges PEREC',
+                    'unimarc' => (new Class_NoticeUnimarc_Fluent)
+                    ->zoneWithChildren('200', ['a' => 'La disparition',
+                                               'f' => 'PEREC, Georges',
+                                               't' => 't. 1'])
+                    ->render(),
+
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                   ]);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2345,
+                    'id_notice' => 3,
+                    'id_bib' => 34,
+                    'annexe' => 34,
+                    'id_int_bib' => 34,
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                    'id_origine' => 312412,
+                    'code_barres' => '00106001488142']);
+
+    $this->_search_client = $this->_setupSearchClient();
+
+    $allow_hold_available_items=true;
+    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee.wsdl', null, $allow_hold_available_items);
+    $this->_orphee->setSearchClient($this->_search_client);
+    $this->_orphee
+      ->beHoldModeItem()
+      ->isConnected();
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 34,
+                                                          'type' => Class_IntBib::COM_ORPHEE]),
+                                             $this->_orphee);
+
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
+    ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
+    $this->dispatch('/recherche/reservationajax/id_bib/36/copy_id/2345/id_origine/312412/id/123');
+  }
+
+
+  protected function _setupSearchClient()
+  {
+    return
+      $this->mock()
+           ->whenCalled('__getLastRequest')
+           ->answers('last request')
+
+           ->whenCalled('__getLastResponse')
+           ->answers('last response')
+
+           ->whenCalled('__getLastResponseHeaders')
+           ->answers('last response  headers')
+
+           ->whenCalled('__getLastRequestHeaders')
+           ->answers('last response')
+
+           ->whenCalled('hasFunction')
+           ->answers(false)
+
+           ->whenCalled('__setCookie')
+           ->answers(null)
+
+           ->whenCalled('EndSession')
+           ->answers(new EndSessionResponse())
+
+           ->whenCalled('hasFunction')->with('GetId')->answers(true)
+
+           ->whenCalled('GetId')
+           ->with(new GetId())
+
+           ->answers(GetIdResponse::withIdResult('1234'))
+
+           ->whenCalled('hasFunction')->with('GetAdh')->answers(true)
+
+           ->whenCalled('GetAdh')
+           ->with(GetAdh::withNo('10900000753'))
+           ->answers(Storm_Test_ObjectWrapper::mock()
+                     ->whenCalled('getXml')
+                     ->answers(OrpheeFixtures::xmlGetInfoUserCarteHenryDupont()))
+
+           ->whenCalled('GetAdh')
+           ->with(GetAdh::withNo('1301500012'))
+           ->answers(Storm_Test_ObjectWrapper::mock()
+                     ->whenCalled('getXml')
+                     ->answers(OrpheeFixtures::xmlGetAdhFoix()))
+
+           ->whenCalled('GetLstDmt')
+           ->with(GetLstDmt::withNtcAndFas('312412', 0))
+           ->answers(GetLstDmtResponse::withResult(OrpheeFixtures::xmlGetLstDmtMillenium()))
+
+           ->whenCalled('RsvDmtAdh')
+           ->with(RsvDmtAdh::withNoticeItemUser('312412', '148814', '1301500012'))
+           ->answers(RsvDmtAdhResponse::withResult('<?xml version="1.0" encoding="utf-8"?><datas><msg><code><![CDATA[0]]></code><libelle><![CDATA[Réservation impossible : Maximum de réservations atteint par support ]]></libelle></msg></datas>'));
+  }
+
+
+  /** @test */
+  public function responseShouldContainsReservationImpossible()
+  {
+    $this->assertContains('Réservation impossible : Maximum de réservations atteint par support',
+                          json_decode( $this->_response->getBody())->content,
+                          );
+  }
+}
+
+
+
+
+/* @see https://forge.afi-sa.net/issues/209507 */
+require_once 'tests/fixtures/DecalogFixtures.php';
+class TemplatesAbonneCentralizedHoldsDecalogTest
+  extends AbstractControllerTestCase{
+  protected
+    $_search_client,
+    $_mock_web_client,
+    $_orphee,
+    $_john_lemon;
+
+  public function setUp() : void
+  {
+    parent::setUp();
+    Class_CosmoVar::setValueOf('centralized_hold_mode', 34);
+
+    $this->_buildTemplateProfil(['id'=>1]);
+
+    $comm_params = ['url_serveur' => 'tests/fixtures/orphee.wsdl',
+                    'allow_hold_available_items' => true];
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 82,
+                    'id_bib' => 34,
+                    'code' => 34,
+                    'id_origine' => 34,
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 83,
+                    'id_bib' => 37,
+                    'code' => 37,
+                    'id_origine' => 37,
+                    'libelle' => 'WonderTown'
+                   ]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 34,
+                    'mail' => 'test@jolieville.net',
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 34,
+                    'id_bib' => 34,
+                    'nom' => 'JolieVille',
+                    'nom_court' => 'JV',
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
+                    'comm_sigb' => Class_IntBib::COM_ORPHEE,
+                    'comm_params' => $comm_params
+                   ]);
+
+    $this->_john_lemon =
+      $this->fixture(Class_Users::class,
+                     ['id' => 142,
+                      'idAbon' => '',
+                      'nom' => 'Lemon',
+                      'prenom' => 'John',
+                      'login' => 'uselogon',
+                      'idabon' => '130425',
+                      'password' => 'secret',
+                      'mail' => 'johnlemon@abbeyroad.net',
+                      'id_int_bib' => 37,
+                      'id_bib' => 37,
+                      'id_sigb' => '789789789'
+                     ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 37,
+                    'id_bib' => 37,
+                    'nom' => 'Eightth Wonder',
+                    'nom_court' => '8THW',
+                    'mail' => 'intbibqsdf@WonderTown.net',
+                    'sigb' => Class_IntBib::SIGB_DECALOG,
+                    'comm_sigb' => Class_IntBib::COM_DECALOG,
+                    'comm_params' => ['url_serveur' => 'http://bib.mondecalog/xxx',
+                                      'key' => 'my-key',
+                                      'institution_code' => 'my-institution-code',
+                                      'record_id_prefix' => 'EPPK-',
+                                      'provide_pickup_locations' => false,
+                                      'use_card_number' => '1301500012']]);
+
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 3,
+                    'titre' => 'La disparition',
+                    'auteurs' => 'Georges PEREC',
+                    'unimarc' => (new Class_NoticeUnimarc_Fluent)
+                    ->zoneWithChildren('200', ['a' => 'La disparition',
+                                               'f' => 'PEREC, Georges',
+                                               't' => 't. 1'])
+                    ->render(),
+
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                   ]);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2345,
+                    'id_notice' => 3,
+                    'id_bib' => 34,
+                    'annexe' => 34,
+                    'id_int_bib' => 34,
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                    'id_origine' => 312412,
+                    'code_barres' => '012345']);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 37,
+                    'libelle' => 'Paris',
+                    'id_bib' => 37,
+                    'id_origine' => 10]);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2346,
+                    'id_notice' => 3,
+                    'id_bib' => 37,
+                    'id_int_bib' => 37,
+                    'type_doc' => Class_TypeDoc::LIVRE,
+                    'id_origine' => 312413,
+                    'annexe' =>37,
+                    'code_barres' => '012346']);
+
+    $this->_search_client = $this->_setupSearchClient();
+
+    $allow_hold_available_items=true;
+    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee.wsdl', null, $allow_hold_available_items);
+    $this->_orphee->setSearchClient($this->_search_client);
+    $this->_orphee->isConnected();
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 34,
+                                                          'type' => Class_IntBib::COM_ORPHEE]),
+                                             $this->_orphee);
+
+
+    $this->_mock_web_client = $this->mock()
+      ->whenCalled('postRawData')
+      ->with('http://decalog-sigb/my-institution-code/user/login',
+             'username=a%40a.fr&password=T4vEyXK',
+             'application/x-www-form-urlencoded',
+             ['headers' => 'apikey:my-key'])
+      ->answers(DecalogFixtures::jsonUserLoginFrodon())
+      ->whenCalled('postRawData')
+      ->with('http://decalog-sigb/my-institution-code/user/login',
+             'username=a%40a.fr&password=secret',
+             'application/x-www-form-urlencoded',
+             ['headers' => 'apikey:my-key'])
+      ->answers(DecalogFixtures::jsonUserLoginFrodon())
+
+      ->whenCalled('postRawData')
+      ->with('http://decalog-sigb/my-institution-code/user/login',
+             'username=a%40a.fr&password=9xTJa%2BwMw%2BgA%2FYdugRgj7A',
+             'application/x-www-form-urlencoded',
+             ['headers' => 'apikey:my-key'])
+      ->answers(DecalogFixtures::jsonUserLoginFrodon())
+
+      ->whenCalled('open_url')
+      ->with('http://decalog-sigb/my-institution-code/user/3396790941099222416',
+             ['headers' => 'apikey:my-key'])
+      ->answers(DecalogFixtures::jsonUserDetailFrodon());
+
+    Class_WebService_SIGB_Decalog::setService([], null);
+    Class_WebService_SIGB_Decalog
+      ::getService(['url_serveur' => 'http://bib.mondecalog/xxx',
+                    'key' => 'my-key',
+                    'institution_code' => 'my-institution-code',
+                    'record_id_prefix' => 'EPPK-',
+                    'provide_pickup_locations' => false,
+                    'use_card_number' => '1301500012'
+                    ])
+      ->setWebClient($this->_mock_web_client);
+
+    Class_WebService_SIGB_AbstractRESTService::shouldThrowError(true);
+
+    ZendAfi_Auth::getInstance()->logUser($this->_john_lemon);
+    $this->dispatch('/recherche/reservation-pickup-ajax/id_bib/34/copy_id/2345/id_origine/312412/id/2243');
+  }
+
+
+  protected function _setupSearchClient()
+  {
+    return $this
+      ->mock()
+
+      ->whenCalled('__getLastRequest')
+      ->answers('last request')
+
+      ->whenCalled('__getLastResponse')
+      ->answers('last response')
+
+      ->whenCalled('__getLastResponseHeaders')
+      ->answers('last response  headers')
+
+      ->whenCalled('__getLastRequestHeaders')
+      ->answers('last response')
+
+      ->whenCalled('hasFunction')
+      ->answers(false)
+
+      ->whenCalled('__setCookie')
+      ->answers(null)
+
+      ->whenCalled('EndSession')
+      ->answers(new EndSessionResponse())
+
+      ->whenCalled('hasFunction')->with('GetId')->answers(true)
+
+      ->whenCalled('GetId')
+      ->with(new GetId())
+
+      ->answers(GetIdResponse::withIdResult('1234'))
+
+      ->whenCalled('hasFunction')->with('GetAdh')->answers(true)
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('10900000753'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetInfoUserCarteHenryDupont()))
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('uselogon'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetInfoUserCarteHenryDupont()))
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('1301500012'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetAdhFoix()))
+
+      ->whenCalled('GetLstDmt')
+      ->with(GetLstDmt::withNtcAndFas('312412', 0))
+      ->answers(GetLstDmtResponse::withResult(OrpheeFixtures::xmlGetLstDmtMillenium()))
+
+      ->whenCalled('GetLstDmt')
+      ->with(GetLstDmt::withNtcAndFas('312414', 0))
+      ->answers(GetLstDmtResponse::withResult(OrpheeFixtures::xmlGetLstDmtMillenium()))
+
+      ->whenCalled('GetLstRsv')
+      ->with(GetLstRsv::withAdhAndCountPerPage(1301500012, 200))
+      ->answers(GetLstRsvResponse::withResult(OrpheeFixtures::xmlGetLstRsvHenryDupont()))
+
+      ->whenCalled('RsvNtcAdh')
+      ->with(RsvNtcAdh::withNoticeUserNo('312414', '1301500012', 0))
+      ->answers(RsvNtcAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
+
+      ->whenCalled('RsvNtcAdh')
+      ->with(RsvNtcAdh::withNoticeUserNo('312412', '1301500012', 0))
+      ->answers(RsvNtcAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
+
+      ->whenCalled('RsvDmtAdh')
+      ->with(RsvDmtAdh::withNoticeItemUser('312414', '', '100753'))
+      ->answers(RsvDmtAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
+
+      ->whenCalled('RsvDmtAdh')
+      ->with(RsvDmtAdh::withNoticeItemUser('312414', '', '1301500012'))
+      ->answers(RsvDmtAdhResponse::withResult('<datas><msg><code><![CDATA[1]]></code><libelle><![CDATA[Réservation mise en attente]]></libelle></msg></datas>'))
+
+      ->whenCalled('GetLstRsv')
+      ->with(GetLstRsv::withAdhAndCountPerPage('100753', 200))
+      ->answers(GetLstRsvResponse::withResult(OrpheeFixtures::xmlGetLstRsvHenryDupont()));
+  }
+
+
+  public function tearDown() : void
+  {
+    Class_WebService_SIGB_Decalog::setService([], null);
+    $this->_mock_web_client = null;
+    Class_WebService_SIGB_Decalog::reset();
+    Class_WebService_SIGB_Orphee::reset();
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function bodyContentShouldContainsReservationEnregistree()
+  {
+    $this->assertContains('Votre réservation est enregistrée.<br>Nous vous informerons quand le document ',
+                          json_decode( $this->_response->getBody())->content);
+  }
+}
diff --git a/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php b/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php
new file mode 100644
index 00000000000..48d4312ce3a
--- /dev/null
+++ b/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php
@@ -0,0 +1,200 @@
+<?php
+/**
+ * Copyright (c) 2012-2024, 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
+ */
+
+include_once('Class/WebService/SIGB/Orphee/SessionStrategy.php');
+
+include_once 'tests/library/Class/WebService/SIGB/OrpheeFixtures.php';
+
+class TemplatesAbonneOrpheeBadgePretsTest
+  extends AbstractControllerTestCase
+{
+  protected
+    $_search_client,
+    $_orphee;
+
+
+  public function setUp() : void
+  {
+    parent::setUp();
+    $this->_buildTemplateProfil(['id'=>1]);
+
+    $comm_params = ['url_serveur' => 'tests/fixtures/orphee.wsdl',
+                    'allow_hold_available_items' => true];
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 82,
+                    'code' => 34,
+                    'id_origine' => 34,
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 34,
+                    'mail' => 'test@jolieville.net',
+                    'libelle' => 'JolieVille'
+                   ]);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 34,
+                    'id_bib' => 34,
+                    'nom' => 'JolieVille',
+                    'nom_court' => 'JV',
+                    'comm_sigb' => Class_IntBib::COM_ORPHEE,
+                    'comm_params' => $comm_params
+                   ]);
+
+    $user = $this->fixture(Class_Users::class,
+                           ['id' => 143,
+                            'idAbon' => '1301500012',
+                            'role_level' => 2,
+                            'nom' => 'FOIX',
+                            'prenom' => 'FPV',
+                            'login' => '1301500012',
+                            'password' => 's3cr3t',
+                            'id_int_bib' => 34,
+                            'id_site' => 34
+                           ]);
+
+    $this->_search_client = $this->mock();
+    $this->_search_client
+      ->whenCalled('__getLastRequest')
+      ->answers('last request')
+
+      ->whenCalled('__getLastResponse')
+      ->answers('last response')
+
+      ->whenCalled('__getLastResponseHeaders')
+      ->answers('last response  headers')
+
+      ->whenCalled('__getLastRequestHeaders')
+      ->answers('last response')
+
+      ->whenCalled('hasFunction')
+      ->answers(false)
+
+      ->whenCalled('__setCookie')
+      ->answers(null)
+
+      ->whenCalled('EndSession')
+      ->with(new EndSession)
+      ->answers(new EndSessionResponse())
+
+      ->whenCalled('hasFunction')->with('GetId')->answers(true)
+
+      ->whenCalled('GetId')
+      ->with(new GetId())
+      ->answers(GetIdResponse::withIdResult('1234'))
+
+      ->whenCalled('hasFunction')
+      ->with('GetAdh')
+      ->answers(true)
+
+      ->whenCalled('hasFunction')
+      ->with('GetToken')
+      ->answers(false)
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('10900000753'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetInfoUserCarteHenryDupont()))
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('1301500012'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetAdhFoix()))
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withCardAndPassword('1301500012',$user->getPassword()))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetAdhFoix()))
+
+      ->whenCalled('GetLstPret')
+      ->with(GetLstPret::withAdhAndCountPerPage('1301500012',1))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetLstPretHenryDupont(45)))
+
+      ->whenCalled('SetCritPrets')
+      ->with(SetCritPrets::withCodeAndVal(1,37))
+      ->answers(true)
+
+      ->whenCalled('setTri')
+      ->with(SetTri::withTypeTriAndOrdre(4,2))
+      ->answers(true)
+
+      ->whenCalled('GetLstRsv')
+      ->with(GetLstRsv::withAdhAndCountPerPage('1301500012',200))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetLstRsvHenryDupont()))
+
+      ->whenCalled('GetLstPret')
+      ->with(GetLstPret::withAdhAndCountPerPage('1301500012',1))
+      ->willDo(fn() =>
+               (1 == $this->_search_client->methodCallCount('GetLstPret'))
+               ? Storm_Test_ObjectWrapper::mock()
+               ->whenCalled('getXml')
+               ->answers(OrpheeFixtures::xmlGetLstPretHenryDupont(12))
+               : Storm_Test_ObjectWrapper::mock()
+               ->whenCalled('getXml')
+               ->answers(OrpheeFixtures::xmlGetLstPretHenryDupont(5)))
+      ->beStrict();
+
+    Class_CommSigb::shouldThrowError(true);
+
+    $this->_orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee.wsdl', null, true);
+    $this->_orphee->setSearchClient($this->_search_client);
+    $this->_orphee->isConnected();
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+                                                         ['id_bib' => 34,
+                                                          'type' => Class_IntBib::COM_ORPHEE]),
+                                             $this->_orphee);
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+
+    $this->dispatch('/abonne/fiche/id/143');
+  }
+
+
+  public function tearDown() : void
+  {
+    Class_WebService_SIGB_Orphee::reset();
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function badgeNbPretsEnCoursShouldContains12PretsEnCours()
+  {
+    $this->assertXPathContentContains('//span[contains(@class, "badge_text")]', '12 prêt(s) en cours', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function badgeNbPretsEnRetardShouldContains5PretsEnRetard()
+  {
+    $this->assertXPathContentContains('//span[contains(@class, "badge_text")]', '5 prêt(s) en retard');
+  }
+}
diff --git a/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php b/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php
index d5f673767ba..13cd44f9aa9 100644
--- a/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php
+++ b/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php
@@ -46,6 +46,11 @@ abstract class TemplatesAuthPreRegisterTestCase extends AbstractControllerTestCa
                                       'pre-registration' => 1],
                     'comm_sigb' => Class_IntBib::COM_NANOOK]);
 
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 9983,
+                    'id_bib' => 1,
+                    'libelle' => 'Tombouctou']);
+
     return $this;
   }
 }
@@ -71,7 +76,7 @@ class TemplatesAuthPreRegisterActionDispatchTest
   /** @test */
   public function pageShouldContainsSelectedSiteWithEmptyOptionAndOrdered() {
     $this->assertXPath('//select[@class="zendafi_form_preregistration_nanook_site form-control form-control-sm custom-select custom-select-sm"][@required="required"]/option[not(text())]');
-    $this->assertXPath('//select[@class="zendafi_form_preregistration_nanook_site form-control form-control-sm custom-select custom-select-sm"][@required="required"]/option[@label="Tombouctou"][@value="1"][@selected="selected"][text()="Tombouctou"][1]');
+    $this->assertXPath('//select[@class="zendafi_form_preregistration_nanook_site form-control form-control-sm custom-select custom-select-sm"][@required="required"]/option[@label="Tombouctou"][@value="9983"][@selected="selected"][text()="Tombouctou"][1]', $this->_response->getBody());
   }
 }
 
diff --git a/tests/scenarios/Templates/TemplatesSearchTest.php b/tests/scenarios/Templates/TemplatesSearchTest.php
index 3055fd99009..11b9081f55b 100644
--- a/tests/scenarios/Templates/TemplatesSearchTest.php
+++ b/tests/scenarios/Templates/TemplatesSearchTest.php
@@ -20,6 +20,7 @@
  */
 
 require_once 'TemplatesTest.php';
+require_once 'tests/fixtures/NanookFixtures.php';
 
 abstract class TemplatesSearchWithSortParameterInWidgetTestCase
   extends AbstractControllerTestCase {
@@ -173,14 +174,14 @@ class TemplatesDispatchIntonationSearchTest extends TemplatesIntonationTestCase
                    ['id' => 1,
                     'titres' => 'POMME',
                     'other_terms' => 'HSUJE_RENNES',
-                    'facettes' => 'T1 Y1',
+                    'facettes' => 'T1 Y4',
                     'type' => 1,
                     'unimarc' => file_get_contents(ROOT_PATH . 'tests/fixtures/unimarc_vernon.txt')]);
     $this->fixture(Class_Notice::class,
                    ['id' => 2,
                     'titres' => 'POMME',
                     'other_terms' => 'HSUJE_RENNES',
-                    'facettes' => 'T1 Y1',
+                    'facettes' => 'T1 Y4',
                     'type' => 1,
                     'unimarc' => file_get_contents(ROOT_PATH . 'tests/fixtures/unimarc_terry_pratchet.txt')]);
 
@@ -193,7 +194,7 @@ class TemplatesDispatchIntonationSearchTest extends TemplatesIntonationTestCase
                    ['id' => 1,
                     'libelle' => 'Rennes']);
 
-    $this->dispatch('/opac/recherche/simple/expressionRecherche/pomme/id_profil/72/multifacets/T1-Y1/facette/Y1/section/1/page/1/rech_thesaurus_SUJE/rennes');
+    $this->dispatch('/opac/recherche/simple/expressionRecherche/pomme/id_profil/72/multifacets/T1-Y4/facette/Y4/section/1/page/1/rech_thesaurus_SUJE/rennes');
   }
 
 
@@ -897,17 +898,36 @@ class TemplatesSearchReservationPickupAjaxConnectedUserTest
     $user = $this->fixture(Class_Users::class,
                            ['id' => 8989,
                             'login' => ',eui',
+                            'role_level' => 2,
+                            'id_int_bib' => 1,
+                            'id_site' => 1,
+                            'idabon' => 'uRSTI89V798',
+                            'id_sigb' => '897',
                             'password' => 'ier']);
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 1,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'http://bib.valensol.net',
+                                      'provide_pickup_locations' => true]
+                   ]);
+
+    Class_Exemplaire::find(12)
+      ->setIdOrigine(789789)
+      ->setAnnexe(3);
+
     Class_CosmoVar::setValueOf('site_retrait_resa',
                                Class_CosmoVar::PICKUP_LOCATION_CHOICE);
 
     ZendAfi_Auth::getInstance()->logUser($user);
+
     Class_Profil::find(1)
       ->setBoiteOfTypeInDivision(4,
                                  Intonation_Library_Widget_Login_Definition::CODE,
                                  ['tri' => Class_CriteresRecherche::SORT_NOVELTY_DESC,
                                   'IntonationFormStyle' => 'toggle'])
       ->assertSave();
+
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 3,
                     'code' => '37',
@@ -915,7 +935,18 @@ class TemplatesSearchReservationPickupAjaxConnectedUserTest
                     'libelle' => 'Cran']);
 
     Class_AdminVar::set('HOLD_SITE_SELECTION_CUSTOM_MESSAGE', '<b>This is an additional text message</b>')->save();
-    $this->dispatch('/recherche/reservation-pickup-ajax/id/34/id_notice/34/id_int_bib/2/id_bib/23');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('http://bib.valensol.net:80/service/GetPickupLocation/bibId/0/patronId/0/siteId/0',
+                               NanookFixtures::pickupLocationsOkAnswer())
+
+      ->addRequestWithResponse('http://bib.valensol.net:80/service/GetPickupLocation/bibId/789789/patronId/897/siteId/37',
+                               NanookFixtures::pickupLocationsOkAnswersAnnecyAndCran())
+
+      ->addRequestWithResponse('http://bib.valensol.net:80/service/GetPatronInfo/patronId/897',
+                               NanookFixtures::axelPatronInfo());
+
+    $this->dispatch('/recherche/reservation-pickup-ajax/id/34/id_notice/34/copy_id/12/id_int_bib/2/id_bib/23');
 
     $this->_xpath = new Storm_Test_XPath();
     $this->_html = json_decode($this->_response->getBody(), true)['content'];
diff --git a/tests/scenarios/Templates/TemplatesWidgetTest.php b/tests/scenarios/Templates/TemplatesWidgetTest.php
index f22d1016b1b..0a824280c2c 100644
--- a/tests/scenarios/Templates/TemplatesWidgetTest.php
+++ b/tests/scenarios/Templates/TemplatesWidgetTest.php
@@ -697,7 +697,7 @@ class TemplatesWidgetSearchDefaultStyleTest extends TemplatesIntonationTestCase
 
   /** @test */
   public function searchOptionForAnnexeShouldDisplayTous() {
-    $this->assertXPath('//div[contains(@class,"boite rech_simple")]//form//select[@name="custom_multifacets_annexe"]//option[1][@value="0"][@label="tous"]');
+    $this->assertXPath('//div[contains(@class,"boite rech_simple")]//form//select[@name="custom_multifacets_annexe"]//option[1][@value=""][@label="tous"]');
   }
 
 
diff --git a/tests_db/UpgradeDBTest.php b/tests_db/UpgradeDBTest.php
index 6996558d846..2123aec0303 100644
--- a/tests_db/UpgradeDBTest.php
+++ b/tests_db/UpgradeDBTest.php
@@ -75,8 +75,8 @@ abstract class UpgradeDBTestCase extends TestCase
   protected function _getPatchLevel()
   {
     preg_match('/UpgradeDB_(\d+)_Test/',
-      get_class($this),
-      $matches);
+               get_class($this),
+               $matches);
 
     return $matches[1];
   }
@@ -101,7 +101,7 @@ abstract class UpgradeDBTestCase extends TestCase
     } catch (Exception $e) {
       $table_exists = false;
       $message = $message ?: sprintf('Failed asserting that "%s" table EXISTS',
-        $table);
+                                     $table);
     }
 
     $this->assertTrue($table_exists, $message);
@@ -113,13 +113,13 @@ abstract class UpgradeDBTestCase extends TestCase
 
     try {
       $this->query(sprintf('select `%s` from `%s` limit 1',
-        $column,
-        $table));
+                           $column,
+                           $table));
     } catch (Exception $e) {
       $column_exists = false;
       $message = $message ?: sprintf('Failed asserting that "%s" table CONTAINS a "%s" column.',
-        $table,
-        $column);
+                                     $table,
+                                     $column);
     }
 
     $this->assertTrue($column_exists, $message);
@@ -131,8 +131,8 @@ abstract class UpgradeDBTestCase extends TestCase
 
     try {
       $this->query(sprintf('select `%s` from `%s` limit 1',
-        $column,
-        $table));
+                           $column,
+                           $table));
     } catch (Exception $e) {
       $column_exists = false;
     }
@@ -140,16 +140,16 @@ abstract class UpgradeDBTestCase extends TestCase
     if ($column_exists)
       $message =
         $message ?: sprintf('Failed asserting that "%s" table does NOT contains column "%s".',
-          $table,
-          $column);
+                            $table,
+                            $column);
 
     $this->assertFalse($column_exists, $message);
   }
 
   protected function assertIndex(string $table,
-    string $name,
-    string $type = 'BTREE',
-    string $message = ''): void
+                                 string $name,
+                                 string $type = 'BTREE',
+                                 string $message = ''): void
   {
     $index_exists = false;
     $keys = [];
@@ -170,9 +170,9 @@ abstract class UpgradeDBTestCase extends TestCase
 
     if (!$index_exists)
       $message = $message ?: sprintf('Failed asserting that "%s" table CONTAINS an index "%s" of type "%s".',
-        $table,
-        $name,
-        $type)
+                                     $table,
+                                     $name,
+                                     $type)
         . "\n" . json_encode($keys, JSON_PRETTY_PRINT);
 
     $this->assertTrue($index_exists, $message);
@@ -200,8 +200,8 @@ abstract class UpgradeDBTestCase extends TestCase
     $message = $primary_exists
       ? ''
       : (sprintf('Failed asserting that "%s" table CONTAINS a PRIMARY on column "%s".',
-        $table,
-        $name)
+                 $table,
+                 $name)
          . "\n" . json_encode($keys, JSON_PRETTY_PRINT));
 
     $this->assertTrue($primary_exists, $message);
@@ -224,16 +224,16 @@ abstract class UpgradeDBTestCase extends TestCase
 
     if ($index_exists)
       $message = $message ?: sprintf('Failed asserting that "%s" table NOT CONTAINS index "%s" .',
-        $table,
-        $name);
+                                     $table,
+                                     $name);
 
     $this->assertFalse($index_exists, $message);
   }
 
   protected function assertFieldType(string $table,
-    string $name,
-    string $type,
-    string $message = ''): void
+                                     string $name,
+                                     string $type,
+                                     string $message = ''): void
   {
     $this->assertField($table, $name, $type, 'Type', $message);
   }
@@ -244,32 +244,32 @@ abstract class UpgradeDBTestCase extends TestCase
   }
 
   protected function assertFieldNotNullable(string $table,
-    string $column,
-    string $message = ''): void
+                                            string $column,
+                                            string $message = ''): void
   {
     $this->assertField($table, $column, 'NO', 'Null', $message);
   }
 
   protected function assertFieldDefault(string $table,
-    string $column,
-    string $default,
-    string $message = ''): void
+                                        string $column,
+                                        string $default,
+                                        string $message = ''): void
   {
     $this->assertField($table, $column, $default, 'Default', $message);
   }
 
   protected function assertField(string $table,
-    string $column,
-    string $expected,
-    string $field,
-    string $message = ''): void
+                                 string $column,
+                                 string $expected,
+                                 string $field,
+                                 string $message = ''): void
   {
     $field_exists = false;
     $fields = [];
 
     try {
       foreach ($this->query(sprintf('show fields from `%s` where field=\'%s\'',
-        $table, $column))->fetchAll() as $row) {
+                                    $table, $column))->fetchAll() as $row) {
         $fields[] = $row;
         if ($expected == $row[$field]) {
           $field_exists = true;
@@ -284,11 +284,11 @@ abstract class UpgradeDBTestCase extends TestCase
 
     if (!$field_exists)
       $message = $message ?: sprintf("Failed asserting that TABLE %s contains COLUMN %s with field %s = %s\n\n%s",
-        $table,
-        $column,
-        $field,
-        $expected,
-        json_encode($fields, JSON_PRETTY_PRINT));
+                                     $table,
+                                     $column,
+                                     $field,
+                                     $expected,
+                                     json_encode($fields, JSON_PRETTY_PRINT));
 
     $this->assertTrue($field_exists, $message);
   }
@@ -301,7 +301,7 @@ abstract class UpgradeDBTestCase extends TestCase
   protected function dropFieldFrom($table, $field)
   {
     return $this->silentQuery(sprintf('alter table %s drop column %s',
-      $table, $field));
+                                      $table, $field));
   }
 
   protected function dropFieldsFrom($table, $fields)
@@ -377,7 +377,7 @@ class UpgradeDB_268_Test extends UpgradeDBTestCase
     $this->query('update bib_admin_users set pseudo="' . $hundred_chars_pseudo . '" where id_user=' . static::$user_backup['id_user']);
 
     $this->assertEquals($hundred_chars_pseudo,
-      $this->query('select pseudo from bib_admin_users where id_user=' . static::$user_backup['id_user'])->fetch()['pseudo']);
+                        $this->query('select pseudo from bib_admin_users where id_user=' . static::$user_backup['id_user'])->fetch()['pseudo']);
   }
 }
 
@@ -393,7 +393,7 @@ class UpgradeDB_274_Test extends UpgradeDBTestCase
   {
     $data = $this->query('select valeur from bib_admin_var where clef="CNIL_CONSENT_ENABLE"')->fetch();
     $this->assertEquals('1',
-      $data['valeur']);
+                        $data['valeur']);
   }
 }
 
@@ -527,7 +527,7 @@ class UpgradeDB_283_Test extends UpgradeDBTestCase
   public function shouldHaveAvenioImportFormat()
   {
     $this->assertContains('8:CSV Avenio',
-      $this->query("select liste from variables where clef='import_format'")->fetch()['liste']);
+                          $this->query("select liste from variables where clef='import_format'")->fetch()['liste']);
   }
 }
 
@@ -585,8 +585,8 @@ class UpgradeDB_284_Test extends UpgradeDBTestCase
   public function albumUsageConstraintsShouldHaveColumnItemIdThatReferencesFirstAlbumItemId()
   {
     $this->assertEquals(['serialized_datas' => 'for tests'],
-      $this->query('select serialized_datas from album_usage_constraints where item_id=' . static::$item_id)->fetch(),
-      'item id: ' . static::$item_id);
+                        $this->query('select serialized_datas from album_usage_constraints where item_id=' . static::$item_id)->fetch(),
+                        'item id: ' . static::$item_id);
   }
 
   /** @test */
@@ -787,7 +787,7 @@ class UpgradeDB_293_Test extends UpgradeDBTestCase
   public function dedupModeNoneShouldExists()
   {
     $this->assertContains("\r\n2:aucun",
-      $this->query("select liste from variables where clef='mode_doublon'")->fetch()['liste']);
+                          $this->query("select liste from variables where clef='mode_doublon'")->fetch()['liste']);
   }
 }
 
@@ -825,8 +825,8 @@ class UpgradeDB_295_Test extends UpgradeDBTestCase
     $facets = 'HSOUS0001 HSOUS0002 HLANG0001 A191462 A155897 A191463 A133747 A23754 A191464 A81181 A191465 A47146 A191466 A133767 A134066 A191467 A191468 A191469 A191470 A191471 A191472 A191473 A191474 A191475 A191476 A191477 A191478 A103360 A103361 A191479 A191480 A65390 Lfre Leng G60 G1 G5 T4 B1 S1 YROU HCCCC0002 HCCCC0002011500320011';
 
     $this->assertEquals($facets,
-      $this->query('select clean_spaces("' . $facets . '") as facets')
-      ->fetch()['facets']);
+                        $this->query('select clean_spaces("' . $facets . '") as facets')
+                        ->fetch()['facets']);
   }
 }
 
@@ -909,7 +909,7 @@ class UpgradeDB_299_Test extends UpgradeDBTestCase
   public function urlWebSvcShouldPortOnAfiDotSa()
   {
     $this->assertEquals('https://websvc.afi-sa.net/afi_opac_services/main.php',
-      Class_CosmoVar::get('url_services'));
+                        Class_CosmoVar::get('url_services'));
   }
 }
 
@@ -964,8 +964,8 @@ class UpgradeDB_302_Test extends UpgradeDBTestCase
   public function barcodeFieldsShouldContains949Dollar6()
   {
     $this->assertContains("\r\n" . '949:949$6',
-      $this->query('select liste from variables where clef=\'champ_code_barres\'')
-      ->fetch()['liste']);
+                          $this->query('select liste from variables where clef=\'champ_code_barres\'')
+                          ->fetch()['liste']);
   }
 }
 
@@ -1370,7 +1370,7 @@ class UpgradeDB_317_Test extends UpgradeDBTestCase
   public function datas()
   {
     return array_map(fn($item) => [$item],
-      $this->_columns);
+                     $this->_columns);
   }
 
   /**
@@ -1760,7 +1760,7 @@ class UpgradeDB_332_Test extends UpgradeDBTestCase
   public function datas()
   {
     return array_map(fn($item) => [$item],
-      $this->_columns);
+                     $this->_columns);
   }
 
   /**
@@ -1828,9 +1828,9 @@ class UpgradeDB_335_Test extends UpgradeDBTestCase
     }
 
     foreach ([ 'MOISSONAGE_JAMENDO',
-              Class_Batch_PanierUser::TYPE] as $type)
-                if (!$this->fetchBatchByType($type))
-                  $this->query('insert into batchs (type, last_run) values("' . $type . '", "")');
+               Class_Batch_PanierUser::TYPE] as $type)
+      if (!$this->fetchBatchByType($type))
+        $this->query('insert into batchs (type, last_run) values("' . $type . '", "")');
   }
 
   /** @test */
@@ -2110,7 +2110,7 @@ class UpgradeDB_341_Test extends UpgradeDBTestCase
                   ->fetch();
 
     $this->assertEquals('http://pad.philharmoniedeparis.fr/EXPLOITATION/oaiserver.ashx',
-      $datas['valeur']);
+                        $datas['valeur']);
   }
 
   /** @test */
@@ -2118,7 +2118,7 @@ class UpgradeDB_341_Test extends UpgradeDBTestCase
   {
     $datas = $this->query('select url_origine from album where id=' . static::$cite_mus_album_id)->fetch();
     $this->assertEquals('http://pad.philharmoniedeparis.fr/EXPLOITATION/oaiserver.ashx',
-      $datas['url_origine']);
+                        $datas['url_origine']);
   }
 
   /** @test */
@@ -2126,7 +2126,7 @@ class UpgradeDB_341_Test extends UpgradeDBTestCase
   {
     $datas = $this->query('select url_origine from album where id=' . static::$big_bug_album_id)->fetch();
     $this->assertEquals('http://media.citedesbugs.fr',
-      $datas['url_origine']);
+                        $datas['url_origine']);
   }
 
   /** @test */
@@ -2488,7 +2488,7 @@ class UpgradeDB_357_Test extends UpgradeDBTestCase
                          'ordre'       => 2,
                          'verrou'      => 'checked',
                          'hidden'      => 0],
-      $other_index_fields);
+                        $other_index_fields);
   }
 }
 
@@ -2843,7 +2843,7 @@ class UpgradeDB_373_Test extends UpgradeDBTestCase
   {
     $this->assertEquals(['0' => 'Code-barres uniquement',
                          '1' => 'Bibliothèque + codes-barres'],
-      Class_CosmoVar::getList('unicite_code_barres'));
+                        Class_CosmoVar::getList('unicite_code_barres'));
   }
 }
 
@@ -2879,14 +2879,14 @@ class UpgradeDB_375_Test extends UpgradeDBTestCase
   public function variableNewRegistrerationShouldContainsBRInsteadOfNL()
   {
     $this->assertEquals('Hello<br><br>MyFriend',
-      $this->query('select valeur from bib_admin_var where clef="NOTIFICATION_TEMPLATE_NEW_REGISTRATION"')->fetch()['valeur']);
+                        $this->query('select valeur from bib_admin_var where clef="NOTIFICATION_TEMPLATE_NEW_REGISTRATION"')->fetch()['valeur']);
   }
 
   /** @test */
   public function variableNewUserShouldContainsBRInsteadOfNL()
   {
     $this->assertEquals('Hello<br><br>You have a new friend',
-      $this->query('select valeur from bib_admin_var where clef="NOTIFICATION_TEMPLATE_NEW_USER"')->fetch()['valeur']);
+                        $this->query('select valeur from bib_admin_var where clef="NOTIFICATION_TEMPLATE_NEW_USER"')->fetch()['valeur']);
   }
 }
 
@@ -3216,7 +3216,7 @@ class UpgradeDB_383_Test extends UpgradeDBTestCase
                     ->fetch()['filters'];
 
     $this->assertEquals('{"search_role_level":"2", "search_id_site":"7"}',
-      $filters);
+                        $filters);
   }
 }
 
@@ -3385,8 +3385,8 @@ class UpgradeDB_386_Test extends UpgradeDBTestCase
   public function newslettersMailSubjectShouldDefaultToTitle()
   {
     $this->assertEquals('db testing',
-      current($this->query("select mail_subject from newsletters where titre='db testing'")
-              ->fetch()));
+                        current($this->query("select mail_subject from newsletters where titre='db testing'")
+                                ->fetch()));
   }
 }
 
@@ -3608,7 +3608,7 @@ class UpgradeDB_395_Test extends UpgradeDBTestCase
   public function columns()
   {
     return array_map(fn($column) => [$column],
-      $this->_columns);
+                     $this->_columns);
   }
 
   /**
@@ -3809,7 +3809,7 @@ class UpgradeDB_401_Test extends UpgradeDBTestCase
   public function idsThenAlphaKeyShouldExists()
   {
     $this->assertContains("\r\n3:identifiants puis clef alpha",
-      $this->query("select liste from variables where clef='mode_doublon'")->fetch()['liste']);
+                          $this->query("select liste from variables where clef='mode_doublon'")->fetch()['liste']);
   }
 }
 
@@ -3945,7 +3945,7 @@ class UpgradeDB_407_Test extends UpgradeDBTestCase
   public function adultsShouldBeSetToOneByDefault()
   {
     $this->assertEquals(0,
-      $this->query('select count(*) from session_activity_inscriptions where (adults=0 and children=0) ')->fetch()['count(*)']);
+                        $this->query('select count(*) from session_activity_inscriptions where (adults=0 and children=0) ')->fetch()['count(*)']);
   }
 }
 
@@ -3960,7 +3960,7 @@ class UpgradeDB_408_Test extends UpgradeDBTestCase
   public function tableVariablesShouldHaveFlushBeforeFullVariableInGroupe4()
   {
     $this->assertEquals(4,
-      $this->query("select * from variables where clef='flush_before_full'")->fetch()['groupe']);
+                        $this->query("select * from variables where clef='flush_before_full'")->fetch()['groupe']);
   }
 }
 
@@ -4109,9 +4109,9 @@ class UpgradeDB_414_Test extends UpgradeDBTestCase
   public function statusReportPushUrlShouldBeHttps()
   {
     $this->assertEquals('https://pola.afi-sa.net/smile.php',
-      current($this
-              ->query('select valeur from bib_admin_var where clef="STATUS_REPORT_PUSH_URL"')
-              ->fetch()));
+                        current($this
+                                ->query('select valeur from bib_admin_var where clef="STATUS_REPORT_PUSH_URL"')
+                                ->fetch()));
   }
 }
 
@@ -4282,7 +4282,7 @@ class UpgradeDB_423_Test extends UpgradeDBTestCase
   {
     Class_UserGroupCategorie::deleteBy(['libelle' => 'Accès aux lettres d\'informations']);
     Class_UserGroup::deleteBy(['libelle' => array_map(fn($fixture) => $fixture[0],
-      $this->datas())]);
+                                                      $this->datas())]);
   }
 
   /** @test */
@@ -4307,7 +4307,7 @@ class UpgradeDB_423_Test extends UpgradeDBTestCase
                                            'role_level' => $role,
                                            'id_cat'     => $parent_group->getId(),
                                            'filters'    => json_encode(
-                                             ['search_role_level' => $role]),
+                                                                       ['search_role_level' => $role]),
                                            ]);
     $this->assertTrue($group->hasRight(Class_UserGroup::RIGHT_GERER_LETTRESINFO));
   }
@@ -4545,8 +4545,8 @@ class UpgradeDB_428_Test extends UpgradeDBTestCase
   public function prepare()
   {
     $this->query('replace into bib_admin_var(clef, valeur) values(?, ?)',
-      ['PELLICULE_SETTINGS',
-       '{"Provider":["electre_rest_2"],"Login":["api_cat_login"],"Password":["?login@cat_api!"]}']);
+                 ['PELLICULE_SETTINGS',
+                  '{"Provider":["electre_rest_2"],"Login":["api_cat_login"],"Password":["?login@cat_api!"]}']);
   }
 
   /** @test */
@@ -4556,7 +4556,7 @@ class UpgradeDB_428_Test extends UpgradeDBTestCase
                 ->fetch();
 
     $this->assertEquals('{"Provider":["electre_ng"],"Login":["api_cat_login"],"Password":["?login@cat_api!"]}',
-      $row['valeur'] ?? '');
+                        $row['valeur'] ?? '');
   }
 }
 
@@ -4648,7 +4648,7 @@ class UpgradeDB_430_Test extends UpgradeDBTestCase
                            'IdField'       => '',
                            'FilterField'   => '',
                            'FilterValue'   => '']],
-      JSON_PRETTY_PRINT);
+                         JSON_PRETTY_PRINT);
 
     Class_CodifThesaurus::newInstance(['id_thesaurus' => 'TST1',
                                        'libelle'      => 'tstlib1',
@@ -4688,10 +4688,10 @@ class UpgradeDB_430_Test extends UpgradeDBTestCase
   public function codifThesaurusIdTST3andTST30004CShouldHaveCodeUNIMARC()
   {
     $this->assertEquals(['TST3', 'TST30004'],
-      array_map(fn($thesaurus) => $thesaurus->getIdThesaurus(),
-        Class_CodifThesaurus::findAllBy(['code'         => Class_CodifThesaurusFixed::CODE_UNIMARC_FACET,
-                                         'id_thesaurus' => static::$_id_thesaurus,
-                                         'order'        => 'id_thesaurus'])));
+                        array_map(fn($thesaurus) => $thesaurus->getIdThesaurus(),
+                                  Class_CodifThesaurus::findAllBy(['code'         => Class_CodifThesaurusFixed::CODE_UNIMARC_FACET,
+                                                                   'id_thesaurus' => static::$_id_thesaurus,
+                                                                   'order'        => 'id_thesaurus'])));
   }
 
   protected function _deleteThesaurusForTests()
@@ -4719,7 +4719,7 @@ class UpgradeDB_431_Test extends UpgradeDBTestCase
     $this->_last_user_id = $this->lastInsertId();
 
     $this->silentQuery('INSERT INTO user_groups (libelle, group_type, filters) VALUES ("Super admins dynamic", 1, ?)',
-      [json_encode(['search_role_level' => 7])]);
+                       [json_encode(['search_role_level' => 7])]);
 
     $this->_last_user_group_id = $this->lastInsertId();
   }
@@ -4738,12 +4738,12 @@ class UpgradeDB_431_Test extends UpgradeDBTestCase
   {
     $rows =
       $this->query('select user_id from user_group_memberships where user_group_id =?',
-        [$this->_last_user_group_id])
+                   [$this->_last_user_group_id])
            ->fetchAll();
 
     $this->assertEquals([['user_id' => 1],
                          ['user_id' => $this->_last_user_id]],
-      $rows);
+                        $rows);
   }
 }
 
@@ -4760,7 +4760,7 @@ class UpgradeDB_432_Test extends UpgradeDBTestCase
   public function columns()
   {
     return array_map(fn($column) => [$column],
-      $this->_columns);
+                     $this->_columns);
   }
 
   /**
@@ -4975,7 +4975,7 @@ class UpgradeDB_437_Test extends UpgradeDBTestCase
                          'NOTUPGRADABLE------1',
                          'NOTEXISTINGARTICLE------8',
                          ],
-      explode(';', $row['notices']));
+                        explode(';', $row['notices']));
   }
 
   protected function _createRecordWithClefAlpha(string $clef_alpha): int
@@ -5237,14 +5237,14 @@ class UpgradeDB_446_Test extends UpgradeDBTestCase
   public function tableVariablesShouldHaveMaxItemsVariableInGroupe4()
   {
     $this->assertEquals(4,
-      $this->query("select * from variables where clef='max_items'")->fetch()['groupe']);
+                        $this->query("select * from variables where clef='max_items'")->fetch()['groupe']);
   }
 
   /** @test */
   public function tableVariablesShouldHaveMaxItemsVariableSetTo1000()
   {
     $this->assertEquals(1000,
-      $this->query("select * from variables where clef='max_items'")->fetch()['valeur']);
+                        $this->query("select * from variables where clef='max_items'")->fetch()['valeur']);
   }
 }
 
@@ -5543,14 +5543,14 @@ class UpgradeDB_454_Test extends UpgradeDBTestCase
     $this->assertIndex('notices', 'mots_notice', 'FULLTEXT');
 
     $this->assertEquals('HPUBL0002 HSUPP0001 A5333',
-      Class_Notice::find($this->_first_inserted_record)->getFacettes());
+                        Class_Notice::find($this->_first_inserted_record)->getFacettes());
     $this->assertEquals('Lfre G1 T1 B1 S27',
-      Class_Notice::find($this->_second_inserted_record)->getFacettes());
+                        Class_Notice::find($this->_second_inserted_record)->getFacettes());
 
     $this->assertEquals('F_HPUBL0002 F_HSUPP0001 F_A5333',
-      Class_Notice::find($this->_first_inserted_record)->getFacets());
+                        Class_Notice::find($this->_first_inserted_record)->getFacets());
     $this->assertEquals('F_Lfre F_G1 F_T1 F_B1 F_S27',
-      Class_Notice::find($this->_second_inserted_record)->getFacets());
+                        Class_Notice::find($this->_second_inserted_record)->getFacets());
 
     $this->assertNotIndex('notices', 'mots_notice_2');
     $this->assertNotIndex('notices', 'facettes');
@@ -5766,13 +5766,11 @@ class UpgradeDB_460_Test extends UpgradeDBTestCase
   }
 }
 
+
+
+
 class UpgradeDB_461_Test extends UpgradeDBTestCase
 {
-  public function tearDown(): void
-  {
-    $this->query('delete from payfip where reference="1234767889" and id_op="toto";');
-    parent::tearDown();
-  }
 
   public function prepare()
   {
@@ -5780,6 +5778,14 @@ class UpgradeDB_461_Test extends UpgradeDBTestCase
     $this->query("insert into payfip(id_op, bib_id,user_id, reference, amount,paid_on, result) values ('toto with bib',1, 336,'1234767889', 1634, '2023-06-01 12:00:28','C')");
   }
 
+
+  public function tearDown(): void
+  {
+    $this->query('delete from payfip where reference="1234767889" and id_op="toto";');
+    parent::tearDown();
+  }
+
+
   /** @test */
   public function sigbStatusShouldBeSetForNonBibIdPayfip()
   {
@@ -5841,8 +5847,8 @@ abstract class AbstractCafeyn_TestCase extends UpgradeDBTestCase
     if (!$this
         ->query('select count(*) from bib_admin_var where clef ="Cafeyn_ID"')
         ->fetch()['count(*)'])
-          $this
-            ->query('insert into bib_admin_var (valeur, clef) values ("", "Cafeyn_ID")');
+      $this
+        ->query('insert into bib_admin_var (valeur, clef) values ("", "Cafeyn_ID")');
 
     $this->_cafeyn_id = $this
       ->query('select valeur from bib_admin_var where clef ="Cafeyn_ID"')
@@ -5867,9 +5873,9 @@ class UpgradeDB_462_Test_BatchEnabledTest extends AbstractCafeyn_TestCase
   public function cafeynAlbumsShouldBeDeleted()
   {
     $this->assertEquals(0,
-      $this
-      ->query('select count(*) from album where type_doc_id ="Cafeyn"')
-      ->fetch()['count(*)']);
+                        $this
+                        ->query('select count(*) from album where type_doc_id ="Cafeyn"')
+                        ->fetch()['count(*)']);
   }
 
   /** @test
@@ -5877,9 +5883,9 @@ class UpgradeDB_462_Test_BatchEnabledTest extends AbstractCafeyn_TestCase
   public function arteVodAlbumsShouldNotBeDeleted()
   {
     $this->assertEquals($this->_album_artevod_count + 1,
-      $this
-      ->query('select count(*) from album where type_doc_id ="ArteVod"')
-      ->fetch()['count(*)']);
+                        $this
+                        ->query('select count(*) from album where type_doc_id ="ArteVod"')
+                        ->fetch()['count(*)']);
   }
 }
 
@@ -5898,9 +5904,9 @@ class UpgradeDB_462_Test_BatchDisabledTest extends AbstractCafeyn_TestCase
   public function cafeynAlbumsShouldNotBeDeleted()
   {
     $this->assertEquals(2,
-      $this
-      ->query('select count(*) from album where type_doc_id ="Cafeyn" ')
-      ->fetch()['count(*)']);
+                        $this
+                        ->query('select count(*) from album where type_doc_id ="Cafeyn" ')
+                        ->fetch()['count(*)']);
   }
 
   /** @test
@@ -5908,9 +5914,9 @@ class UpgradeDB_462_Test_BatchDisabledTest extends AbstractCafeyn_TestCase
   public function arteVodAlbumsShouldNotBeDeleted()
   {
     $this->assertEquals($this->_album_artevod_count + 1,
-      $this
-      ->query('select count(*) from album where type_doc_id ="ArteVod" ')
-      ->fetch()['count(*)']);
+                        $this
+                        ->query('select count(*) from album where type_doc_id ="ArteVod" ')
+                        ->fetch()['count(*)']);
   }
 }
 
@@ -6055,8 +6061,8 @@ class UpgradeDB_466_Test extends UpgradeDBTestCase
     if (!$this
         ->query('select count(*) from bib_admin_var where clef ="Cafeyn_SSO_URL"')
         ->fetch()['count(*)'])
-          $this
-            ->query('insert into bib_admin_var (valeur, clef) values ("myoldurl", "Cafeyn_SSO_URL")');
+      $this
+        ->query('insert into bib_admin_var (valeur, clef) values ("myoldurl", "Cafeyn_SSO_URL")');
   }
 
   /** @test */
@@ -6229,14 +6235,14 @@ class UpgradeDB_470_Test extends UpgradeDBTestCase
     $this->id_customfield_values[] = $this->lastInsertId();
 
     $cfg['modules'][3] = [
-                         'division'    => '2',
-                         'id_module'   => 3,
-                         'type_module' => 'LIBRARY',
-                         'preferences' => ['titre'               => 'Mes Bibliotheques',
-                                           'libraries'           => '',
-                                           'nb_aff'              => 2,
-                                           'allow_link_on_title' => true,
-                                           'default_filters'     => ['custom_field_' . $this->_cf_restauration_id => ['restauration unique3']]]];
+                          'division'    => '2',
+                          'id_module'   => 3,
+                          'type_module' => 'LIBRARY',
+                          'preferences' => ['titre'               => 'Mes Bibliotheques',
+                                            'libraries'           => '',
+                                            'nb_aff'              => 2,
+                                            'allow_link_on_title' => true,
+                                            'default_filters'     => ['custom_field_' . $this->_cf_restauration_id => ['restauration unique3']]]];
     $data = $this->query('select id_profil, CFG_ACCUEIL from bib_admin_profil order by id_profil desc')->fetchAll();
     $this->_id_profil = $data[0]['id_profil'];
 
@@ -6462,14 +6468,14 @@ class UpgradeDB_472_Test extends UpgradeDBTestCase
   public function superAdminGroupShouldHaveAdminProfileRight()
   {
     $this->assertTrue(in_array(Class_UserGroup::RIGHT_ADMIN_PROFILE,
-      $this->_super_group->getRights()));
+                               $this->_super_group->getRights()));
   }
 
   /** @test */
   public function adminPortalGroupShouldHaveAdminProfileRight()
   {
     $this->assertTrue(in_array(Class_UserGroup::RIGHT_ADMIN_PROFILE,
-      $this->_admin_group->getRights()));
+                               $this->_admin_group->getRights()));
   }
 }
 
@@ -6487,6 +6493,7 @@ class UpgradeDB_473_Test extends UpgradeDBTestCase
   }
 }
 
+
 class UpgradeDB_474_Test extends UpgradeDBTestCase
 {
   public function prepare()
@@ -6517,6 +6524,7 @@ class UpgradeDB_474_Test extends UpgradeDBTestCase
   }
 }
 
+
 class UpgradeDB_475_Test extends UpgradeDBTestCase
 {
   protected
@@ -6583,3 +6591,341 @@ class UpgradeDB_475_Test extends UpgradeDBTestCase
     $this->assertNotColumn('activity', 'visible');
   }
 }
+
+
+class UpgradeDB_476_Test extends UpgradeDBTestCase
+{
+
+  protected string $_bmi_id,
+    $_bda_id,
+    $_bdp_id,
+    $_three_id,
+    $_six_id,
+    $_five_id,
+    $_cdif1_id,
+    $_cdif2_id,
+    $_rec_one,
+    $_rec_two,
+    $_rec_three,
+    $_rec_four,
+    $_rec_five,
+    $_rec_six,
+    $_cat_one,
+    $_cat_two,
+    $_profile_one;
+
+
+  public function prepare() {
+    $this
+      ->query('insert album (titre, annexes, type_doc_id) values ("Test Album 467", "CODIF1 CODIF2", 1)');
+
+    $this
+      ->query('delete from variables where clef=\'CODIFS_MIGRATED\'');
+
+    $this
+      ->query('insert codif_type_doc (type_doc_id, annexes) values ("Test Typedoc 467", "CODIF1;CODIF2")');
+
+    $this->_bmi_id = $this->_insertAnnexe('BMI');
+    $this->_bda_id = $this->_insertAnnexe('BDA');
+    $this->_bdp_id = $this->_insertAnnexe('BDP');
+    $this->_three_id = $this->_insertAnnexe('3');
+    $this->_six_id = $this->_insertAnnexe('6');
+    $this->_five_id = $this->_insertAnnexe((string) $this->_six_id);
+    $this->_cdif1_id = $this->_insertAnnexe('CODIF1');
+    $this->_cdif2_id = $this->_insertAnnexe('CODIF2');
+
+    $this->query('UPDATE `codif_annexe` set `code` = "' . $this->_five_id . '" WHERE `id_annexe` = ' . $this->_six_id);
+
+    $this->_rec_one = $this->_insertRecordFor(['T1', 'YBMI']);
+    $this->_rec_two = $this->_insertRecordFor(['T1', 'YBMI', 'B1', 'HNRNR0002']);
+    $this->_rec_three = $this->_insertRecordFor(['T1', 'YBDA', 'YBMI', 'B1', 'HNRNR0002', 'LYBMX']);
+    $this->_rec_four = $this->_insertRecordFor(['YBDA', 'T1', 'B1', 'HNRNR0002']);
+    $this->_rec_five = $this->_insertRecordFor(['T1', 'Y' . $this->_bdp_id, 'YBDP']);
+    $this->_rec_six = $this->_insertRecordFor(['T1', 'Y' . $this->_five_id, 'Y' . $this->_six_id]);
+
+    $this->_cat_one = $this->_insertDomain('CODIF1;CODIF2');
+    $this->_cat_two = $this->_insertDomain($this->_five_id . ';' . $this->_six_id);
+
+    $this->_insertItem($this->_rec_one, 'BMI', '[341]');
+    $this->_insertItem($this->_rec_two, 'BMI', '[345]');
+    $this->_insertItem($this->_rec_three, 'BDA', '[342]');
+    $this->_insertItem($this->_rec_three, 'BMI', '[343]');
+    $this->_insertItem($this->_rec_four, 'BDA', '[346]');
+    $this->_insertItem($this->_rec_five, '3', '[344]');
+    $this->_insertItem($this->_rec_five, 'BDP', '[347]');
+    $this->_insertItem($this->_rec_six, $this->_six_id, '[348]');
+    $this->_insertItem($this->_rec_six, $this->_five_id, '[349]');
+
+    $this->_profile_one = $this->_insertProfile('CODIF1;CODIF2');
+
+    $this->silentQuery('ALTER TABLE `codif_annexe` DROP INDEX IF EXISTS `id_bib`;');
+  }
+
+
+  protected function _insertProfile(string $annexes): int
+  {
+    $this->query('insert into bib_admin_profil (libelle, sel_annexe)'
+                 . 'values ("Test profil 467", "' . $annexes . '")');
+
+    return $this->lastInsertId();
+  }
+
+
+  protected function _insertDomain(string $annexes): int
+  {
+    $this->query('insert catalogue (libelle, annexe) values ('
+                 . '"Test Catalogue 467", "' . $annexes . '")');
+
+    return $this->lastInsertId();
+  }
+
+
+  protected function _insertRecordFor(array $facets): int
+  {
+    $this->query('insert into notices (titres, type_doc, facettes, facets) values (' . '"[UN TITRE POUR TESTS]", 1, "' . implode(' ', $facets) . '", " F_' . implode(' F_', $facets) . '")');
+
+    return $this->lastInsertId();
+  }
+
+
+  protected function _insertItem(int $id_notice,
+                                 string $annexe,
+                                 string $id_origine): self
+  {
+    $this->query('insert into exemplaires '
+                 . '(id_notice, annexe, id_int_bib, id_bib, id_origine) values ('
+                 . $id_notice . ', "' . $annexe . '", 1, 1, "'
+                 . $id_origine . '");');
+
+    return $this;
+  }
+
+
+  protected function _insertAnnexe(string $code): int
+  {
+    $this->query('insert codif_annexe (code, id_origine) values ("'
+                 . $code . '", "' . $code . '")');
+
+    return $this->lastInsertId();
+  }
+
+
+  public function tearDown() : void
+  {
+    $this->query("DELETE FROM catalogue where libelle = 'Test Catalogue 467';");
+    $this->query("DELETE FROM album where titre = 'Test Album 467';");
+    $this->query("DELETE FROM codif_type_doc where type_doc_id = 'Test Typedoc 467';");
+    $this->query("DELETE FROM bib_admin_profil where libelle = 'Test profil 467';");
+
+    $this->query("DELETE FROM codif_annexe where id_annexe IN ("
+                 .implode(',', [$this->_bda_id, $this->_bdp_id, $this->_bmi_id,
+                                $this->_three_id, $this->_six_id, $this->_five_id,
+                                $this->_cdif2_id, $this->_cdif1_id]) . ");");
+
+    $this->query("DELETE FROM notices where id_notice IN ("
+                 .implode(',', [$this->_rec_one, $this->_rec_two, $this->_rec_three, $this->_rec_four, $this->_rec_five, $this->_rec_six]) . ")");
+
+    $this->query("DELETE FROM exemplaires where id_notice IN ("
+                 .implode(',', [$this->_rec_one, $this->_rec_two, $this->_rec_three, $this->_rec_four, $this->_rec_five, $this->_rec_six]) . ")");
+
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function profileOneShouldGetAnnexesIds()
+  {
+    $data = $this->query('SELECT `sel_annexe` FROM `bib_admin_profil` WHERE `id_profil` ='
+                         . $this->_profile_one . ';')->fetch();
+    $this->assertEquals($this->_cdif1_id . ';' . $this->_cdif2_id, $data['sel_annexe']);
+  }
+
+  /** @test **/
+  public function albumShouldGetAnnexesIds() {
+    $data = $this->query('SELECT `annexes` FROM `album` WHERE `titre` ='
+                         . '\'Test Album 467\';')->fetch();
+    $this->assertEquals($this->_cdif1_id . ' ' . $this->_cdif2_id, $data['annexes']);
+  }
+
+
+  /** @test **/
+  public function cosmoVarCodifsMigratedValueShouldBe1() {
+    $data = $this->query('SELECT `valeur` FROM `variables` WHERE `clef` ='
+                         . '\'CODIFS_MIGRATED\';')->fetch();
+    $this->assertEquals(1, $data['valeur']);
+  }
+
+
+  /** @test **/
+  public function domain1ShouldGetAnnexesIds() {
+    $data = $this->query('SELECT `annexe` FROM `catalogue` WHERE `id_catalogue` ='
+                         . $this->_cat_one)->fetch();
+    $this->assertEquals($this->_cdif1_id .';' . $this->_cdif2_id, $data['annexe']);
+  }
+
+
+  /** @test **/
+  public function domain2ShouldGetAnnexesIds() {
+    $data = $this->query('SELECT `annexe` FROM `catalogue` WHERE `id_catalogue` ='
+                         . $this->_cat_two)->fetch();
+    $this->assertEquals($this->_six_id .';' . $this->_five_id, $data['annexe']);
+  }
+
+
+  /** @test **/
+  public function typeDocShouldGetAnnexesIds() {
+    $data = $this->query('SELECT `annexes` FROM `codif_type_doc` WHERE `type_doc_id` ='
+                         . '\'Test Typedoc 467\';')->fetch();
+    $this->assertEquals($this->_cdif1_id .';' . $this->_cdif2_id, $data['annexes']);
+  }
+
+  /** @test **/
+  public function indexIdBibInTableCodifAnnexeShouldBeCreated()
+  {
+    $this->assertIndex('codif_annexe', 'id_bib');
+  }
+
+
+  /** @test **/
+  public function notice1FacettesYShouldBeY1()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_one)->fetch();
+    $this->assertEquals('T1 Y' . $this->_bmi_id, $data['facettes']);
+    $this->assertEquals('F_T1 F_Y' . $this->_bmi_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice2FacettesYShouldBeY1()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_two)->fetch();
+    $this->assertEquals('T1 B1 HNRNR0002 Y' . $this->_bmi_id, $data['facettes']);
+    $this->assertEquals('F_T1 F_B1 F_HNRNR0002 F_Y' . $this->_bmi_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice3FacettesYShouldBeY1And2()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_three)->fetch();
+    $this->assertEquals('T1 B1 HNRNR0002 LYBMX Y' . $this->_bda_id
+                        . ' Y' . $this->_bmi_id, $data['facettes']);
+    $this->assertEquals('F_T1 F_B1 F_HNRNR0002 F_LYBMX F_Y'
+                        . $this->_bda_id . ' F_Y' . $this->_bmi_id,
+                        $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice4FacettesYShouldBeY2()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` ='
+                         . $this->_rec_four)->fetch();
+    $this->assertEquals('T1 B1 HNRNR0002 Y' . $this->_bda_id, $data['facettes']);
+    $this->assertEquals('F_T1 F_B1 F_HNRNR0002 F_Y' . $this->_bda_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice5FacetsAndFacettesShouldBeUpdated()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_five)->fetch();
+    $this->assertEquals('T1 Y' . $this->_three_id . ' Y' . $this->_bdp_id, $data['facettes']);
+    $this->assertEquals('F_T1 F_Y' . $this->_three_id . ' F_Y' . $this->_bdp_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice6FacetsAndFacettesShouldBeUpdated()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_six)->fetch();
+    $this->assertEquals('T1 Y' . $this->_five_id . ' Y' . $this->_six_id, $data['facettes']);
+    $this->assertEquals('F_T1 F_Y' . $this->_five_id . ' F_Y' . $this->_six_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function item341AnnexeShouldBe1()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[341]\';')
+                 ->fetch();
+    $this->assertEquals($this->_bmi_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item342AnnexeShouldBe2()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[342]\';')
+                 ->fetch();
+    $this->assertEquals($this->_bda_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item343AnnexeShouldBe1()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[343]\';')
+                 ->fetch();
+    $this->assertEquals($this->_bmi_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item344AnnexeShouldBe4()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[344]\';')
+                 ->fetch();
+    $this->assertEquals($this->_three_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item345AnnexeShouldBe1()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[345]\';')
+                 ->fetch();
+    $this->assertEquals($this->_bmi_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item346AnnexeShouldBe2()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[346]\';')
+                 ->fetch();
+    $this->assertEquals($this->_bda_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item347AnnexeShouldBe3()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[347]\';')
+                 ->fetch();
+    $this->assertEquals($this->_bdp_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item348AnnexeShouldBe6()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[348]\';')
+                 ->fetch();
+    $this->assertEquals($this->_five_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item349AnnexeShouldBe5()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[349]\';')
+                 ->fetch();
+    $this->assertEquals($this->_six_id, $data['annexe']);
+  }
+}
-- 
GitLab


From 3eb7ed7b494dcb20254935a5fa2f0c5e26cfa8d2 Mon Sep 17 00:00:00 2001
From: Alex Arnaud <alex.arnaud@biblibre.com>
Date: Fri, 31 Jan 2025 09:32:34 +0100
Subject: [PATCH 3/7] dev#217256 : Site facets upgrade : handle code as id

---
 .../Migration/BranchFromCodeToIdNotice.php    | 11 ++-
 tests_db/UpgradeDBTest.php                    | 90 ++++++++++++++++++-
 2 files changed, 94 insertions(+), 7 deletions(-)

diff --git a/library/Class/Migration/BranchFromCodeToIdNotice.php b/library/Class/Migration/BranchFromCodeToIdNotice.php
index d8106c21401..c7657a5e848 100644
--- a/library/Class/Migration/BranchFromCodeToIdNotice.php
+++ b/library/Class/Migration/BranchFromCodeToIdNotice.php
@@ -153,8 +153,13 @@ class Class_Migration_BranchFromCodeToIdNotice
     if ( ! $code)
       return;
 
+    $not_in = $this->_items_done
+      ? ' AND id NOT IN (' . implode(',', $this->_items_done) . ')'
+      : '';
+
     $items = $this->_adapter->query('SELECT id, id_notice FROM exemplaires WHERE `annexe` = \''
-                                    . $code . '\'')->fetchAll();
+                                    . $code . '\'' . $not_in)->fetchAll();
+
     $items_done = array_map(fn($item) => $item['id'],
                             $items);
     $records_id = array_map(fn($item) => $item['id_notice'],
@@ -167,9 +172,7 @@ class Class_Migration_BranchFromCodeToIdNotice
     $sql = sprintf("UPDATE `exemplaires` SET `annexe` = '%s' WHERE `annexe` = '%s'%s;",
                    $id,
                    $code,
-                   $this->_items_done
-                   ? (' AND id NOT IN (' . implode(',', $this->_items_done) . ')')
-                   : '');
+                   $not_in);
     $this->_adapter->query($sql);
 
     $this->_items_done = array_merge($this->_items_done, $items_done);
diff --git a/tests_db/UpgradeDBTest.php b/tests_db/UpgradeDBTest.php
index 2123aec0303..7ab7f32068e 100644
--- a/tests_db/UpgradeDBTest.php
+++ b/tests_db/UpgradeDBTest.php
@@ -6604,12 +6604,17 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
     $_five_id,
     $_cdif1_id,
     $_cdif2_id,
+    $_cdif3_id,
+    $_cdif4_id,
     $_rec_one,
     $_rec_two,
     $_rec_three,
     $_rec_four,
     $_rec_five,
     $_rec_six,
+    $_rec_seven,
+    $_rec_eight,
+    $_rec_nine,
     $_cat_one,
     $_cat_two,
     $_profile_one;
@@ -6633,6 +6638,12 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
     $this->_five_id = $this->_insertAnnexe((string) $this->_six_id);
     $this->_cdif1_id = $this->_insertAnnexe('CODIF1');
     $this->_cdif2_id = $this->_insertAnnexe('CODIF2');
+    $this->_cdif3_id = $this->_insertAnnexe('PLOP');
+    $this->_cdif4_id = $this->_insertAnnexe('PLIP');
+    $this->query('UPDATE codif_annexe SET code = ' . $this->_cdif3_id -1
+                 . ' WHERE id_annexe = ' . $this->_cdif3_id);
+    $this->query('UPDATE codif_annexe SET code = ' . $this->_cdif4_id -1
+                 . ' WHERE id_annexe = ' . $this->_cdif4_id);
 
     $this->query('UPDATE `codif_annexe` set `code` = "' . $this->_five_id . '" WHERE `id_annexe` = ' . $this->_six_id);
 
@@ -6642,6 +6653,10 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
     $this->_rec_four = $this->_insertRecordFor(['YBDA', 'T1', 'B1', 'HNRNR0002']);
     $this->_rec_five = $this->_insertRecordFor(['T1', 'Y' . $this->_bdp_id, 'YBDP']);
     $this->_rec_six = $this->_insertRecordFor(['T1', 'Y' . $this->_five_id, 'Y' . $this->_six_id]);
+    $this->_rec_seven = $this->_insertRecordFor(['Y' . $this->_cdif3_id -1]);
+    $this->_rec_eight = $this->_insertRecordFor(['Y' . $this->_cdif4_id -1]);
+    $this->_rec_nine = $this->_insertRecordFor(['Y' . $this->_cdif4_id -1,
+                                                'Y' . $this->_cdif3_id -1]);
 
     $this->_cat_one = $this->_insertDomain('CODIF1;CODIF2');
     $this->_cat_two = $this->_insertDomain($this->_five_id . ';' . $this->_six_id);
@@ -6655,6 +6670,10 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
     $this->_insertItem($this->_rec_five, 'BDP', '[347]');
     $this->_insertItem($this->_rec_six, $this->_six_id, '[348]');
     $this->_insertItem($this->_rec_six, $this->_five_id, '[349]');
+    $this->_insertItem($this->_rec_seven, $this->_cdif3_id -1, '[350]');
+    $this->_insertItem($this->_rec_eight, $this->_cdif4_id -1, '[351]');
+    $this->_insertItem($this->_rec_nine, $this->_cdif4_id -1, '[352]');
+    $this->_insertItem($this->_rec_nine, $this->_cdif3_id -1, '[352]');
 
     $this->_profile_one = $this->_insertProfile('CODIF1;CODIF2');
 
@@ -6720,13 +6739,20 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
     $this->query("DELETE FROM codif_annexe where id_annexe IN ("
                  .implode(',', [$this->_bda_id, $this->_bdp_id, $this->_bmi_id,
                                 $this->_three_id, $this->_six_id, $this->_five_id,
-                                $this->_cdif2_id, $this->_cdif1_id]) . ");");
+                                $this->_cdif2_id, $this->_cdif1_id, $this->_cdif3_id,
+                                $this->_cdif4_id]) . ");");
 
     $this->query("DELETE FROM notices where id_notice IN ("
-                 .implode(',', [$this->_rec_one, $this->_rec_two, $this->_rec_three, $this->_rec_four, $this->_rec_five, $this->_rec_six]) . ")");
+                 .implode(',', [$this->_rec_one, $this->_rec_two, $this->_rec_three,
+                                $this->_rec_four, $this->_rec_five, $this->_rec_six,
+                                $this->_rec_seven, $this->_rec_eight, $this->_rec_nine])
+                 . ")");
 
     $this->query("DELETE FROM exemplaires where id_notice IN ("
-                 .implode(',', [$this->_rec_one, $this->_rec_two, $this->_rec_three, $this->_rec_four, $this->_rec_five, $this->_rec_six]) . ")");
+                 .implode(',', [$this->_rec_one, $this->_rec_two, $this->_rec_three,
+                                $this->_rec_four, $this->_rec_five, $this->_rec_six,
+                                $this->_rec_seven, $this->_rec_eight, $this->_rec_nine])
+                 . ")");
 
     parent::tearDown();
   }
@@ -6849,6 +6875,36 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
   }
 
 
+  /** @test */
+  public function notice7FacetsAndFacettesShouldBeUpdated()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_seven)->fetch();
+    $this->assertEquals(' Y' . $this->_cdif3_id, $data['facettes']);
+    $this->assertEquals(' F_Y' . $this->_cdif3_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice8FacetsAndFacettesShouldBeUpdated()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_eight)->fetch();
+    $this->assertEquals(' Y' . $this->_cdif4_id, $data['facettes']);
+    $this->assertEquals(' F_Y' . $this->_cdif4_id, $data['facets']);
+  }
+
+
+  /** @test */
+  public function notice9FacetsAndFacettesShouldBeUpdated()
+  {
+    $data = $this->query('SELECT `facettes`, `facets` FROM `notices` WHERE `id_notice` = '
+                         . $this->_rec_nine)->fetch();
+    $this->assertEquals(' Y' . $this->_cdif3_id . ' Y' . $this->_cdif4_id, $data['facettes']);
+    $this->assertEquals(' F_Y' . $this->_cdif3_id . ' F_Y' . $this->_cdif4_id, $data['facets']);
+  }
+
+
   /** @test */
   public function item341AnnexeShouldBe1()
   {
@@ -6928,4 +6984,32 @@ class UpgradeDB_476_Test extends UpgradeDBTestCase
                  ->fetch();
     $this->assertEquals($this->_six_id, $data['annexe']);
   }
+
+
+  /** @test */
+  public function item350AnnexeShouldBeCdif3Id()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[350]\';')
+                 ->fetch();
+    $this->assertEquals($this->_cdif3_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item351AnnexeShouldBeCdif4Id()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[351]\';')
+                 ->fetch();
+    $this->assertEquals($this->_cdif4_id, $data['annexe']);
+  }
+
+
+  /** @test */
+  public function item352AnnexeShouldBeCdif4Id()
+  {
+    $data = $this->query('SELECT `annexe` FROM `exemplaires` WHERE `id_origine` = \'[352]\';')
+                 ->fetchAll();
+    $this->assertEquals($this->_cdif3_id, $data[0]['annexe']);
+    $this->assertEquals($this->_cdif4_id, $data[1]['annexe']);
+  }
 }
-- 
GitLab


From 94d76f49a0aa7cd79df5ad4f687a2babe46ad81c Mon Sep 17 00:00:00 2001
From: Alex Arnaud <alex.arnaud@biblibre.com>
Date: Fri, 7 Feb 2025 14:26:16 +0100
Subject: [PATCH 4/7] dev#217114 : Set correct cache key in
 CodifAnnexe::findByCodeAndBib

---
 .../php/classes/classe_notice_integration.php |  3 +-
 library/Class/CodifAnnexe.php                 |  8 +--
 .../CodifAnnexeFindByCodeAndBibCacheTest.php  | 50 +++++++++++++++++++
 3 files changed, 56 insertions(+), 5 deletions(-)
 create mode 100644 tests/library/Class/CodifAnnexeFindByCodeAndBibCacheTest.php

diff --git a/cosmogramme/php/classes/classe_notice_integration.php b/cosmogramme/php/classes/classe_notice_integration.php
index c20b85a5253..52ad5d1f372 100644
--- a/cosmogramme/php/classes/classe_notice_integration.php
+++ b/cosmogramme/php/classes/classe_notice_integration.php
@@ -513,7 +513,8 @@ class notice_integration {
 
     $this->notice_sgbd->add_field('995', '  ', $table_champs);
     $this->notice_sgbd->update();
-    $this->traiteNotice($this->notice_sgbd->getFullRecord(), new notice_unimarc);
+    $this->traiteNotice($this->notice_sgbd->getFullRecord(),
+                        new notice_unimarc($this->id_bib));
     $this->flag_koha = false;
   }
 
diff --git a/library/Class/CodifAnnexe.php b/library/Class/CodifAnnexe.php
index b5cdbfdae2e..75507a01fe7 100644
--- a/library/Class/CodifAnnexe.php
+++ b/library/Class/CodifAnnexe.php
@@ -68,11 +68,11 @@ class CodifAnnexeLoader extends Storm_Model_Loader
 
   public function findByCodeAndBib(?string $code,
                                    ?int $id_bib = 0,
-                                   ?string $service ='') : ?Class_CodifAnnexe
+                                   ?string $service = ''): ?Class_CodifAnnexe
   {
-    $key = $code ?? ''
-      . $id_bib ?? 0
-      . $service ?? '';
+    $key = ($code ?? '')
+      . $id_bib
+      . $service;
 
     if ( isset($this->_annexe_by_code_id_bib_service_cache[$key]))
       return $this->_annexe_by_code_id_bib_service_cache[$key];
diff --git a/tests/library/Class/CodifAnnexeFindByCodeAndBibCacheTest.php b/tests/library/Class/CodifAnnexeFindByCodeAndBibCacheTest.php
new file mode 100644
index 00000000000..b3e3baa3f2c
--- /dev/null
+++ b/tests/library/Class/CodifAnnexeFindByCodeAndBibCacheTest.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Copyright (c) 2012-2024, 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 CodifAnnexeFindByCodeAndBibCacheTest extends ModelTestCase
+{
+  public function setUp(): void
+  {
+    parent::setUp();
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 100,
+                    'code' => '2',
+                    'id_origine' => '2',
+                    'id_bib' => 30]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 101,
+                    'code' => '2',
+                    'id_origine' => '2',
+                    'id_bib' => 5]);
+
+
+    Class_CodifAnnexe::findByCodeAndBib('2', 30);
+  }
+
+  /** @test */
+  public function code2AndBib5ShouldGetAnnexeId101()
+  {
+    $this->assertEquals(101, Class_CodifAnnexe::findByCodeAndBib('2', 5)->getId());
+  }
+}
-- 
GitLab


From 5936fea578170d44a109bdaaa7427cdf312aa2f7 Mon Sep 17 00:00:00 2001
From: Alex Arnaud <alex.arnaud@biblibre.com>
Date: Wed, 29 Jan 2025 14:47:03 +0100
Subject: [PATCH 5/7] dev#217027 : send library origin id to Nanook register
 service instead of Bokeh id

---
 library/Class/IntBib.php                      |  4 +--
 .../SIGB/Nanook/PreRegistration.php           |  9 ++---
 .../SIGB/Nanook/PreRegistration/Data.php      | 18 +++++++++-
 ...ollerPreRegistrationNanookDispatchTest.php |  2 +-
 .../AuthControllerPreRegistrationTest.php     | 33 ++++++++++---------
 .../TemplatesAuthPreRegisterTest.php          |  7 +++-
 6 files changed, 46 insertions(+), 27 deletions(-)

diff --git a/library/Class/IntBib.php b/library/Class/IntBib.php
index 137d5a49905..b3dad4afc8e 100644
--- a/library/Class/IntBib.php
+++ b/library/Class/IntBib.php
@@ -139,9 +139,7 @@ class IntBibLoader extends Storm_Model_Loader {
       $this->_findAllWithPreRegistration(fn($int_bib) => $int_bib->isPreRegistrationNanookEnabled())
            ->injectInto([],
                         function($combo, $library) {
-                          foreach (Class_CodifAnnexe::findByIdBib($library->getId()) as $branch)
-                            $combo[$branch->getId()] = $branch->getLibelle();
-
+                          $combo[$library->getId()] = $library->getLabel();
                           return $combo;
                         });
 
diff --git a/library/Class/WebService/SIGB/Nanook/PreRegistration.php b/library/Class/WebService/SIGB/Nanook/PreRegistration.php
index a2ebf44e12b..099182bd10e 100644
--- a/library/Class/WebService/SIGB/Nanook/PreRegistration.php
+++ b/library/Class/WebService/SIGB/Nanook/PreRegistration.php
@@ -34,17 +34,14 @@ class Class_WebService_SIGB_Nanook_Preregistration extends Class_WebService_SIGB
 
   public function send(Class_WebService_SIGB_PreRegistration_AbstractData $data) : self {
     $id = $data->getBranchId();
-    if ( ! $branch = Class_CodifAnnexe::find($id)) {
-      $this->getLogger()->log($this->_('Échec de la préinscription, la médiathèque sélectionnée n\'existe pas.'));
-      return $this;
-    }
 
-    if(!$int_bib = ($branch->getIntBib())) {
+    if(!$int_bib = Class_IntBib::find($id)) {
       $this->getLogger()->log($this->_('Échec de la préinscription, la médiathèque sélectionnée n\'existe pas.'));
       return $this;
     }
 
-    $response = $int_bib->getSIGBComm()->preRegistration($data);
+    $response = $int_bib->getSIGBComm()
+                        ->preRegistration($data->prepareForWebService());
 
     if (!$response['statut']) {
       $this->getLogger()->log($response['erreur']);
diff --git a/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php b/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php
index 5c0b52e8232..fd9350b4a82 100644
--- a/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php
+++ b/library/Class/WebService/SIGB/Nanook/PreRegistration/Data.php
@@ -21,6 +21,7 @@
 
 class Class_WebService_SIGB_Nanook_Preregistration_Data extends Class_WebService_SIGB_PreRegistration_AbstractData {
 
+  protected ?Class_Bib $_library;
 
   protected static array $_fields_map =
     ['prenom' => 'firstName',
@@ -31,6 +32,11 @@ class Class_WebService_SIGB_Nanook_Preregistration_Data extends Class_WebService
      'code_postal' => 'zipcode',
      'adresse' => 'address'];
 
+  public function __construct(array $data) {
+    parent::__construct($data);
+    $this->_library = Class_IntBib::find($this->_getBranchCode())?->getBib();
+  }
+
 
   protected function _getBranchCode() : string {
     return $this->_data_as_array['site'] ?? '0';
@@ -42,12 +48,22 @@ class Class_WebService_SIGB_Nanook_Preregistration_Data extends Class_WebService
   }
 
 
+  public function prepareForWebService(): self {
+    if (!$this->_library)
+      return $this;
+
+    $this->_data_as_array['site'] = $this->_library->getIdOrigine() ?? 0;
+
+    return $this;
+  }
+
+
   public function getEmail() : string {
     return $this->_data_as_array['mail'] ?? '';
   }
 
 
   public function getLibrary() : ?Class_Bib {
-    return Class_CodifAnnexe::getLibrary($this->getBranchId());
+    return $this->_library;
   }
 }
diff --git a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php
index 43c083e88fc..df729d7a052 100644
--- a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationNanookDispatchTest.php
@@ -74,7 +74,7 @@ class AuthControllerPreRegistrationNanookDispatchTest
 
   /** @test */
   public function selectSiteShouldContainsOptionAcardia() {
-    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="99"]', 'Arcadia');
+    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="1"]', 'Arcadia');
   }
 
 
diff --git a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php
index b1fc204cf19..26e0c173962 100644
--- a/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerPreRegistrationTest.php
@@ -348,7 +348,7 @@ abstract class AuthControllerPreRegistrationNanookTestCase
     $this->fixture(Class_CodifAnnexe::class,
                    ['id' => 99,
                     'id_bib' => 1,
-                    'id_origine' => '1',
+                    'id_origine' => '8',
                     'libelle' => 'ARCADIA']);
 
     $this->fixture(Class_CodifAnnexe::class,
@@ -359,13 +359,16 @@ abstract class AuthControllerPreRegistrationNanookTestCase
 
 
     $this->fixture(Class_Bib::class,
-                   ['id' => 1, 'libelle' => 'Arcadia']);
+                   ['id' => 1,
+                    'libelle' => 'Arcadia',
+                    'id_origine' => 8]);
 
     $this->fixture(Class_Bib::class,
                    ['id' => 2, 'libelle' => 'Guiclan']);
 
     $this->fixture(Class_IntBib::class,
                    ['id' => 1,
+                    'id_bib' => 1,
                     'comm_params' => ['url_serveur' => 'http://super.nano.ok/ilsdi/arcadia',
                                       'pre-registration' => 1],
                     'comm_sigb' => Class_IntBib::COM_NANOOK]);
@@ -440,14 +443,14 @@ class AuthControllerPreRegistrationNanookGetDispatchTest
   /** @test */
   public function siteListShouldContainArcadiaOption()
   {
-    $this->assertXpath('//form//select[@name="site"]/option[@value="99"]');
+    $this->assertXpath('//form//select[@name="site"]/option[@value="1"]');
   }
 
 
   /** @test */
   public function siteListShouldContainGuiclanOption()
   {
-    $this->assertXpath('//form//select[@name="site"]/option[@value="100"]');
+    $this->assertXpath('//form//select[@name="site"]/option[@value="2"]');
   }
 
 
@@ -471,7 +474,7 @@ class AuthControllerPreRegistrationNanookGetDispatchTest
 
   /** @test */
   public function selectSiteShouldContainsOption99() {
-    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="99"]', 'ARCADIA');
+    $this->assertXPathContentContains('//form//select[@name="site"]//option[@value="1"]', 'Arcadia');
   }
 
 
@@ -508,7 +511,7 @@ class AuthControllerPreRegistrationNanookPostDispatchTest
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 99,
+             ['site' => 8,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -522,7 +525,7 @@ class AuthControllerPreRegistrationNanookPostDispatchTest
     Class_AdminVar::set('NOTIFICATION_TEMPLATE_PREREGISTRATION',"Bonjour {user.prenom} {user.nom},<br><br><p>Votre demande d'inscription à <b>{library.libelle}</b> a bien été enregistrée.</p><p>Afin de finaliser votre inscription, merci de vous rendre dans votre médiathèque : <b>{library.adresse} {library.cp} {library.ville}</b></p>");
 
     $this->postDispatch('/opac/auth/pre-registration',
-                        ['site' => '99',
+                        ['site' => '1',
                          'lastName' => 'Jiro',
                          'firstName' => 'Tom',
                          'mail' => 'test@test.fr',
@@ -587,7 +590,7 @@ abstract class AuthControllerPreRegistrationNanookPostErrorTestCase
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 99,
+             ['site' => 8,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -600,7 +603,7 @@ abstract class AuthControllerPreRegistrationNanookPostErrorTestCase
                 . $this->_errorCode()
                 . '</error></>');
 
-    $this->postDispatch('/opac/auth/pre-registration', ['site' => '99',
+    $this->postDispatch('/opac/auth/pre-registration', ['site' => '1',
                                                         'lastName' => 'Jiro',
                                                         'firstName' => 'Tom',
                                                         'mail' => 'test@test.fr',
@@ -702,7 +705,7 @@ class AuthControllerPreRegistrationNanookPostDispatchErrorPatronPasswordNotSecur
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 99,
+             ['site' => 8,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -713,7 +716,7 @@ class AuthControllerPreRegistrationNanookPostDispatchErrorPatronPasswordNotSecur
               'address' => '123'])
       ->answers('<?xml version="1.0" encoding="UTF-8"?><error>PatronPasswordNotSecure</error><securePasswordLabel>Le mot de passe doit comporter au minimum 6 caractères et doit être constitué d\'au moins un chiffre et une lettre.</securePasswordLabel>');
 
-    $this->postDispatch('/opac/auth/pre-registration', ['site' => '99',
+    $this->postDispatch('/opac/auth/pre-registration', ['site' => '1',
                                                         'lastName' => 'Jiro',
                                                         'firstName' => 'Tom',
                                                         'mail' => 'test@test.fr',
@@ -934,7 +937,7 @@ class AuthControllerPreRegistrationNanookDispatchAsInviteAndPostDispatchTest ext
 <GetPatronInfo><patronId>5352</patronId><barcode></barcode><lastName>Ro</lastName><firstName>Toto</firstName><siteId>1</siteId></GetPatronInfo>');
 
     $this->postDispatch('/opac/auth/pre-registration',
-                        ['site' => '99',
+                        ['site' => '1',
                          'lastName' => 'Ro',
                          'firstName' => 'Toto',
                          'mail' => 'to@to.ro',
@@ -1228,7 +1231,7 @@ class AuthControllerPreRegistrationDefaultTemplateContentPostDispatchTest
     $this->mock_web_client
       ->whenCalled('postData')
       ->with('http://super.nano.ok/ilsdi/arcadia/service/pre-register',
-             ['site' => 99,
+             ['site' => 8,
               'lastName' => 'Jiro',
               'firstName' => 'Tom',
               'mail' => 'test@test.fr',
@@ -1242,7 +1245,7 @@ class AuthControllerPreRegistrationDefaultTemplateContentPostDispatchTest
     Class_AdminVar::set('NOTIFICATION_TEMPLATE_PREREGISTRATION', '');
 
     $this->postDispatch('/opac/auth/pre-registration',
-                        ['site' => '99',
+                        ['site' => '1',
                          'lastName' => 'Jiro',
                          'firstName' => 'Tom',
                          'mail' => 'test@test.fr',
@@ -1258,7 +1261,7 @@ class AuthControllerPreRegistrationDefaultTemplateContentPostDispatchTest
 
 
   /** @test */
-  public function responseShouldRedirectToPreRegistrationSuccessIdBibOne() {
+  public function responsePlopShouldRedirectToPreRegistrationSuccessIdBibOne() {
     $this->assertRedirectTo('/auth/pre-registration-success/id_bib/1');
   }
 }
diff --git a/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php b/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php
index 13cd44f9aa9..190b048ef3d 100644
--- a/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php
+++ b/tests/scenarios/Templates/TemplatesAuthPreRegisterTest.php
@@ -40,8 +40,13 @@ abstract class TemplatesAuthPreRegisterTestCase extends AbstractControllerTestCa
 
 
   protected function _fixturesIntBib() : self {
+    $this->fixture(Class_Bib::class,
+                   ['id' => 1,
+                    'libelle' => 'Tombouctou']);
+
     $this->fixture(Class_IntBib::class,
                    ['id' => 1,
+                    'id_bib' => 1,
                     'comm_params' => ['url_serveur' => 'http://super.nano.ok/ilsdi/arcadia',
                                       'pre-registration' => 1],
                     'comm_sigb' => Class_IntBib::COM_NANOOK]);
@@ -76,7 +81,7 @@ class TemplatesAuthPreRegisterActionDispatchTest
   /** @test */
   public function pageShouldContainsSelectedSiteWithEmptyOptionAndOrdered() {
     $this->assertXPath('//select[@class="zendafi_form_preregistration_nanook_site form-control form-control-sm custom-select custom-select-sm"][@required="required"]/option[not(text())]');
-    $this->assertXPath('//select[@class="zendafi_form_preregistration_nanook_site form-control form-control-sm custom-select custom-select-sm"][@required="required"]/option[@label="Tombouctou"][@value="9983"][@selected="selected"][text()="Tombouctou"][1]', $this->_response->getBody());
+    $this->assertXPath('//select[@class="zendafi_form_preregistration_nanook_site form-control form-control-sm custom-select custom-select-sm"][@required="required"]/option[@label="Tombouctou"][@value="1"][@selected="selected"][text()="Tombouctou"][1]', $this->_response->getBody());
   }
 }
 
-- 
GitLab


From eadb31fe4d23104d21187e6a811ceaaf0bdeed84 Mon Sep 17 00:00:00 2001
From: Alex Arnaud <alex.arnaud@biblibre.com>
Date: Fri, 21 Mar 2025 15:57:38 +0100
Subject: [PATCH 6/7] dev#221102 : BDP : loans order now works on Orphee

---
 .../SIGB/AbstractExternalAjaxLoansHelper.php  |   9 +-
 .../Class/WebService/SIGB/Orphee/Service.php  |   7 +-
 .../WebService/SIGB/OrpheeServiceTest.php     |   2 +-
 .../TemplatesAbonneOrpheeBadgePretsTest.php   |   2 +-
 .../TemplatesOrpheeOrderLoansTest.php         | 159 ++++++++++++++++++
 5 files changed, 174 insertions(+), 5 deletions(-)
 create mode 100644 tests/scenarios/Templates/TemplatesOrpheeOrderLoansTest.php

diff --git a/library/Class/WebService/SIGB/AbstractExternalAjaxLoansHelper.php b/library/Class/WebService/SIGB/AbstractExternalAjaxLoansHelper.php
index ced958600a7..24018a7b71d 100644
--- a/library/Class/WebService/SIGB/AbstractExternalAjaxLoansHelper.php
+++ b/library/Class/WebService/SIGB/AbstractExternalAjaxLoansHelper.php
@@ -45,7 +45,8 @@ abstract class Class_WebService_SIGB_AbstractExternalAjaxLoansHelper
                                          'date_to' => $this->getDateTo(),
                                          'date_from' => $this->getDateFrom(),
                                          'page' => $page,
-                                         'size' => $page_size])->getArrayCopy();
+                                         'size' => $page_size,
+                                         'order' => $this->getOrder()])->getArrayCopy();
     $collection =
       array_map(fn($loan) => new Intonation_Library_View_Wrapper_Loan($loan, $this->_view),                            $loans);
 
@@ -162,6 +163,12 @@ abstract class Class_WebService_SIGB_AbstractExternalAjaxLoansHelper
   }
 
 
+  public function getOrder(): string
+  {
+    return $this->_order ?? '';
+  }
+
+
   public function setDateTo(string $filter) :self {
     $this->_date_to = $filter;
     return $this;
diff --git a/library/Class/WebService/SIGB/Orphee/Service.php b/library/Class/WebService/SIGB/Orphee/Service.php
index 94f836e2f1e..0865355db41 100644
--- a/library/Class/WebService/SIGB/Orphee/Service.php
+++ b/library/Class/WebService/SIGB/Orphee/Service.php
@@ -349,7 +349,9 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
 
 
   protected function _processOrderParams(array $params) :self{
-    if (!isset($params['order']) ||(null === ($params['order'] ?? null)))
+    if (!isset($params['order'])
+        || (null === ($params['order'] ?? null))
+        || ('' === $params['order'] ) )
       $params['order'] = SetTri::SORT_ISSUE .'_'. SetTri::ORDER_DESC;
 
     if ($settri_params = $this->_setTriOptionsFromParams($params['order']) )
@@ -773,7 +775,8 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
                        return;
 
                      $this->getSearchClient()
-                          ->setTri(SetTri::withTypeTriAndOrdre($sort_type, $sort_order));
+                          ->SetTri(SetTri::withTypeTriAndOrdre((int) $sort_type,
+                                                               (int) $sort_order));
                    });
   }
 
diff --git a/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php b/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php
index cdf3b8bae03..72b95ce6726 100644
--- a/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php
+++ b/tests/library/Class/WebService/SIGB/OrpheeServiceTest.php
@@ -1071,7 +1071,7 @@ class OrpheeServiceGetInfoUserCarteHenryDupontTest extends OrpheeServiceTestCase
       ->whenCalled('GetLstRsv')
       ->with(GetLstRsv::withAdhAndCountPerPage('100753', -1))
       ->answers(GetLstRsvResponse::withResult(OrpheeFixtures::xmlGetLstRsvHenryDupont()))
-      ->whenCalled('setTri')
+      ->whenCalled('SetTri')
       ->with(SetTri::withTypeTriAndOrdre(SetTri::SORT_ISSUE, SetTri::ORDER_DESC))
       ->answers(true);
 
diff --git a/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php b/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php
index 48d4312ce3a..fc15d00bc83 100644
--- a/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php
+++ b/tests/scenarios/Templates/TemplatesAbonneOrpheeBadgePretsTest.php
@@ -139,7 +139,7 @@ class TemplatesAbonneOrpheeBadgePretsTest
       ->with(SetCritPrets::withCodeAndVal(1,37))
       ->answers(true)
 
-      ->whenCalled('setTri')
+      ->whenCalled('SetTri')
       ->with(SetTri::withTypeTriAndOrdre(4,2))
       ->answers(true)
 
diff --git a/tests/scenarios/Templates/TemplatesOrpheeOrderLoansTest.php b/tests/scenarios/Templates/TemplatesOrpheeOrderLoansTest.php
new file mode 100644
index 00000000000..3c118e6c300
--- /dev/null
+++ b/tests/scenarios/Templates/TemplatesOrpheeOrderLoansTest.php
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * Copyright (c) 2012-2025, 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
+ */
+
+include_once 'tests/library/Class/WebService/SIGB/OrpheeFixtures.php';
+
+class TemplatesOrpheeOrderLoansTest extends AbstractControllerTestCase
+{
+  protected $_search_client;
+
+  public function setUp(): void
+  {
+    parent::setUp();
+
+    $this->_buildTemplateProfil(['id' => 72]);
+
+    $comm_params = ['url_serveur'                => 'tests/fixtures/orphee.wsdl',
+                    'allow_hold_available_items' => 0];
+
+    $this->fixture(Class_IntBib::class,
+                   ['id'          => 34,
+                    'comm_sigb'   => Class_IntBib::COM_ORPHEE,
+                    'sigb' => Class_IntBib::SIGB_ORPHEE,
+                    'comm_params' => $comm_params]);
+
+    $user = $this->fixture(Class_Users::class,
+                           ['id'         => 143,
+                            'idAbon'     => '1301500012',
+                            'role_level' => 2,
+                            'nom'        => 'FOIX',
+                            'prenom'     => 'FPV',
+                            'login'      => '1301500012',
+                            'password'   => 's3cr3t',
+                            'id_int_bib' => 34,
+                            'id_site'    => 34]);
+
+    $this->_search_client = $this
+      ->mock()
+      ->whenCalled('__getLastRequest')
+      ->answers('last request')
+
+      ->whenCalled('__getLastResponse')
+      ->answers('last response')
+
+      ->whenCalled('__getLastResponseHeaders')
+      ->answers('last response  headers')
+
+      ->whenCalled('__getLastRequestHeaders')
+      ->answers('last response')
+
+      ->whenCalled('hasFunction')
+      ->answers(true)
+
+      ->whenCalled('__setCookie')
+      ->answers(null)
+
+      ->whenCalled('EndSession')
+      ->with(new EndSession)
+      ->answers(new EndSessionResponse())
+
+      ->whenCalled('hasFunction')
+      ->answers(true)
+      ->whenCalled('hasFunction')->with('GetAdh')->answers(true)
+      ->whenCalled('hasFunction')->with('GetId')->answers(true)
+
+      ->whenCalled('SetCritPrets')
+      ->with(SetCritPrets::withCodeAndVal(1,37))
+      ->answers(true)
+
+      ->whenCalled('SetTri')
+      ->with(SetTri::withTypeTriAndOrdre(7,2))
+      ->answers(true)
+
+      ->whenCalled('SetTri')
+      ->with(SetTri::withTypeTriAndOrdre(4,2))
+      ->answers(true)
+
+      ->whenCalled('GetLstPret')
+      ->with(GetLstPret::withAdhAndCountPerPage('1301500012',20))
+      ->willDo(fn() =>
+               (1 == $this->_search_client->methodCallCount('GetLstPret'))
+               ? Storm_Test_ObjectWrapper::mock()
+               ->whenCalled('getXml')
+               ->answers(OrpheeFixtures::xmlGetLstPretHenryDupont(12))
+               : Storm_Test_ObjectWrapper::mock()
+               ->whenCalled('getXml')
+               ->answers(OrpheeFixtures::xmlGetLstPretHenryDupont(5)))
+
+      ->whenCalled('GetAdh')
+      ->with(GetAdh::withNo('1301500012'))
+      ->answers(Storm_Test_ObjectWrapper::mock()
+                ->whenCalled('getXml')
+                ->answers(OrpheeFixtures::xmlGetAdhFoix()))
+                                 ->beStrict();
+
+    $orphee = Class_WebService_SIGB_Orphee_Service::getService('tests/fixtures/orphee.wsdl', null, true);
+    $orphee->setSearchClient($this->_search_client);
+    $orphee->setSessionStrategy( Storm_Test_ObjectWrapper::on(new Class_WebService_SIGB_TestingService())
+                                        ->whenCalled('isConnected')->answers(true)
+                                        ->whenCalled('disconnect')->answers(true));
+
+    Class_WebService_SIGB_Orphee::setService(array_merge($comm_params,
+      ['id_bib' => 34,
+       'type'   => Class_IntBib::COM_ORPHEE]),
+      $orphee);
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+
+
+  }
+
+  public function tearDown(): void
+  {
+    Class_WebService_SIGB_Orphee::reset();
+    parent::tearDown();
+  }
+
+  /** @test */
+  public function setCritPretShouldBeCalledWithOrder72()
+  {
+    $this->dispatch('opac/abonne/ajax-loans/id/143/order/7_2');
+    $this->assertTrue($this->_search_client
+                      ->methodHasBeenCalledWithParams('SetTri',
+                                                      [SetTri::withTypeTriAndOrdre(7, 2)]));
+  }
+
+  /** @test */
+  public function whenCalledWithoutOrderSetTriShouldBeCalledThreeTimesWithOrder42()
+  {
+    $this->dispatch('opac/abonne/ajax-loans/id/143/order/');
+    $this->assertEquals($this->_search_client->methodCallCount('SetTri'),
+                        3);
+  }
+
+  /** @test */
+  public function whenCalledWithoutOrderSetTriShouldNotBeCalledsWithOrderEmpty2()
+  {
+    $this->dispatch('opac/abonne/ajax-loans/id/143/order/');
+    $this->assertFalse($this->_search_client->methodHasBeenCalledWithParams('SetTri',[SetTri::withTypeTriAndOrdre(0, 2)]));
+  }
+}
-- 
GitLab


From 1e87951b96a9667e611ba09de27289c435534df4 Mon Sep 17 00:00:00 2001
From: Alex Arnaud <alex.arnaud@biblibre.com>
Date: Thu, 27 Mar 2025 08:41:48 +0100
Subject: [PATCH 7/7] dev#221102 : build SOAP test with HttpClientFactory

---
 library/Class/WebService/MappedSoapClient.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/library/Class/WebService/MappedSoapClient.php b/library/Class/WebService/MappedSoapClient.php
index 5d17f5cd53e..fcfa7824179 100644
--- a/library/Class/WebService/MappedSoapClient.php
+++ b/library/Class/WebService/MappedSoapClient.php
@@ -154,7 +154,7 @@ class Class_WebService_MappedSoapClient extends SoapClient {
   {
     if (static::$_testing)
       return $this->getWebClient()
-                  ->open_url($action, ['request' => $request,
+                  ->postData($action, ['request' => $request,
                                        'location' => $location,
                                        'version' => $version]);
 
-- 
GitLab