diff --git a/VERSIONS b/VERSIONS
index 2ea7c475d51d81c9c1db40ebae584e779d82ad16..d58d557c7df74d530c491b0e840f6b1063541984 100644
--- a/VERSIONS
+++ b/VERSIONS
@@ -1,3 +1,8 @@
+02/03/2017 - v7.9.4
+
+ - ticket #56994 : Outils de migration de comptes lecteurs
+
+
 27/02/2017 - v7.9.3
 
  - ticket #50215 : Ressource Numériques : ajout du connecteur et du moissonnage de Bibliondemand.
diff --git a/cosmogramme/php/classes/classe_abonne.php b/cosmogramme/php/classes/classe_abonne.php
index 1deeb3986df7327660c0533e8237adbc6626e427..d9ef75ad53fb0df70c0c513d8598e9660577ffe6 100644
--- a/cosmogramme/php/classes/classe_abonne.php
+++ b/cosmogramme/php/classes/classe_abonne.php
@@ -152,6 +152,7 @@ class abonne
 
     $user
       ->updateAttributes($data)
+      ->setStatut(0)
       ->saveWithoutValidation();
 
     Class_Users::clearCache();
diff --git a/library/Class/AvisNotice.php b/library/Class/AvisNotice.php
index 597f9c427024988c6391ae4b0e65c4887c512c97..0e372babdf624b3b62493fd78c776a5e9c09342b 100644
--- a/library/Class/AvisNotice.php
+++ b/library/Class/AvisNotice.php
@@ -198,6 +198,14 @@ class AvisNoticeLoader extends Storm_Model_Loader {
     foreach ($comments as $comment)
       $comment->fixLostUserId();
   }
+
+
+  public function keyForUser($user) {
+    return $user
+      ? sprintf('%s--%s--%s',
+                $user->getIdabon(), $user->getIdSite(), $user->getLogin())
+      : '';
+  }
 }
 
 
@@ -412,8 +420,7 @@ class Class_AvisNotice  extends Storm_Model_Abstract {
 
     $user = $this->getUser();
     if (null !== $user)
-      $this->setUserKey(sprintf('%s--%s--%s',
-                                $user->getIdabon(), $user->getIdSite(), $user->getLogin()));
+      $this->setUserKey($this->getLoader()->keyForUser($user));
 
     $this->setAbonOuBib((null !== $user) && $user->isBibliothecaire() ? 1 : 0);
   }
diff --git a/library/Class/User/Datas.php b/library/Class/User/Datas.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f80c2d4b076ad809c7783d68a63773024c30e5c
--- /dev/null
+++ b/library/Class/User/Datas.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_User_Datas extends Class_Entity {
+  protected $_relations = [];
+
+  public function __construct($user) {
+    $this->setUser($user);
+
+    $this->_relations = [new Class_User_DatasRelation('notices_paniers', 'id_user'),
+                         new Class_User_DatasRelationAvisNotice(),
+                         new Class_User_DatasRelation('cms_avis', 'id_user'),
+                         new Class_User_DatasRelation('suggestion_achat', 'user_id'),
+                         new Class_User_DatasRelation('session_formation_inscriptions', 'stagiaire_id'),
+                         new Class_User_DatasRelation('user_group_memberships', 'user_id'),
+                         new Class_User_DatasRelation('formulaires', 'id_user'),
+                         new Class_User_DatasRelation('multimedia_devicehold', 'id_user')];
+  }
+
+
+  public function hasDatas() {
+    foreach($this->_relations as $relation)
+      if (0 < $this->_getCountOf($relation))
+        return true;
+
+    return false;
+  }
+
+
+  protected function _getCountOf($relation) {
+    return $relation->countFor($this->getUser());
+  }
+
+
+  public function giveTo($params, $for_real) {
+    if (!$target = $this->_getTargetWith($params))
+      return false;
+
+    if (!$for_real)
+      return $target;
+
+    foreach($this->_relations as $relation)
+      $this->_giveRelationTo($relation, $target);
+
+    return $target;
+  }
+
+
+  protected function _giveRelationTo($relation, $target) {
+    $relation->giveFromTo($this->getUser(), $target);
+    return $this;
+  }
+
+
+  public function _getTargetWith($params) {
+    $targets = Class_Users::findAllBy($params);
+    if (!$targets) {
+      $this->setError('No target found');
+      return;
+    }
+
+    if (1 < count($targets)) {
+      $this->setError('Too many targets');
+      return;
+    }
+
+    return current($targets);
+  }
+}
+
+
+
+class Class_User_DatasRelation extends Class_Entity {
+  public function __construct($table, $key) {
+    $this->setTable($table)
+         ->setKey($key)
+         ->setSql(Zend_Registry::get('sql'));
+  }
+
+
+  public function countFor($user) {
+    return $this
+      ->getSql()
+      ->fetchOne(sprintf('select count(id) from %s where %s=%s',
+                         $this->getTable(), $this->getKey(), $user->getId()));
+  }
+
+
+  public function giveFromTo($from, $to) {
+    $this
+      ->getSql()
+      ->query(sprintf('update %s set %s=' . $to->getId() . ' where %s=' . $from->getId(),
+                      $this->getTable(), $this->getKey(), $this->getKey()));
+  }
+}
+
+
+
+class Class_User_DatasRelationAvisNotice extends Class_User_DatasRelation {
+  public function __construct() {
+    parent::__construct('notices_avis', 'id_user');
+  }
+
+
+  public function giveFromTo($from, $to) {
+    parent::giveFromTo($from, $to);
+
+    $this->getSql()
+         ->query(sprintf('update %s set user_key=\'' . Class_AvisNotice::keyForUser($to) . '\' where %s=' . $to->getId(),
+                         $this->getTable(), $this->getKey()));
+  }
+}
diff --git a/library/startup.php b/library/startup.php
index 65923327f977c5f1735301185d0e5958a78d95fd..83c14f605b06b2bf1589faee91e377a718112c7a 100644
--- a/library/startup.php
+++ b/library/startup.php
@@ -83,7 +83,7 @@ class Bokeh_Engine {
 
   function setupConstants() {
     defineConstant('BOKEH_MAJOR_VERSION','7.9');
-    defineConstant('BOKEH_RELEASE_NUMBER', BOKEH_MAJOR_VERSION . '.3');
+    defineConstant('BOKEH_RELEASE_NUMBER', BOKEH_MAJOR_VERSION . '.4');
 
     defineConstant('BOKEH_REMOTE_FILES', 'http://git.afi-sa.fr/afi/opacce/');
 
diff --git a/scripts/user_data_migration.php b/scripts/user_data_migration.php
new file mode 100644
index 0000000000000000000000000000000000000000..5721bce35b43e7790208fee0e87a8ee18a5b5979
--- /dev/null
+++ b/scripts/user_data_migration.php
@@ -0,0 +1,167 @@
+<?php
+require './console.php';
+
+echo 'User Data Migration by Stl And Pat since 1854
+
+                             \         .  ./
+                           \      .:";\'.:.."   /
+                               (M^^.^~~:.\'").
+                         -   (/  .    . . \ \)  -
+  O                         ((| :. ~ ^  :. .|))
+ |\\                      -   (\- |  \ /  |  /)  -
+ |  T                         -\  \     /  /-
+/ \[_]..........................\  \   /  /
+
+This will detect user marked for deletion and
+try to give their datas to another user
+based on a rule before deleting them.
+
+';
+
+readline('press enter to continue...');
+
+$valid_count = Class_Users::countBy(['statut' => 0, 'role_level' => 2]);
+$invalid_count = Class_Users::countBy(['statut' => 1, 'role_level' => 2]);
+echo $valid_count . ' valid users in database
+' . $invalid_count . ' users marked for deletion
+
+';
+
+if (0 == $valid_count) {
+  echo 'Oups, not enough valid users, aborting...
+
+                                  ..-^~~~^-..
+                                .~           ~.
+                               (;:           :;)
+                                (:           :)
+                                  \':._   _.:\'
+                                      | |
+                                    (=====)
+                                      | |
+\O/                                   | |
+  \                                   | |
+  /\                               ((/   \))
+';
+  exit(255);
+}
+
+
+class UserDataMigrationLog extends Class_Entity {
+  public function __construct($path) {
+    $this->setPath($path)
+         ->reset();
+  }
+
+
+  public function write($content) {
+    file_put_contents($this->getPath(), implode('|', $content) . "\n", FILE_APPEND);
+    return $this;
+  }
+
+
+  public function reset() {
+    if (file_exists($this->getPath()))
+      unlink($this->getPath());
+
+    return $this;
+  }
+}
+
+
+$for_real = ('--force' == $argv[1]);
+if ($for_real)
+  readline('CAUTION : you asked to run for real, users may be deleted !');
+
+echo $for_real
+  ? 'OH MY, RUNNING FOR REAL, GOOD LUCK'
+  : 'pro tip: use --force option to run for real, with great powers comes great responsibility
+Simulation mode engaged...';
+echo '
+
+';
+
+$without_datas = 0;
+$without_target = 0;
+$success = 0;
+
+$no_datas_log = (new UserDataMigrationLog('deleted_without_datas.txt'))
+  ->write(['id_bokeh', 'carte', 'nom', 'prenom', 'naissance']);
+
+$no_target_log = (new UserDataMigrationLog('not_deleted_target_problem.txt'))
+  ->write(['id_bokeh', 'carte', 'nom', 'prenom', 'naissance', 'cause']);
+
+$success_log = (new UserDataMigrationLog('deleted_with_datas.txt'))
+  ->write(['id_bokeh_source', 'carte_source', 'nom_source', 'prenom_source', 'naissance_source',
+           'id_bokeh_dest', 'carte_dest', 'nom_dest', 'prenom_dest', 'naissance_dest',]);
+
+
+echo 'Started at ' . date('c') . "\n";
+
+foreach(Class_Users::findAllBy(['statut' => 1, 'role_level' => 2]) as $user) {
+  $datas = new Class_User_Datas($user);
+  if (!$datas->hasDatas()) {
+    $no_datas_log->write([$user->getId(),
+                          $user->getLogin(),
+                          $user->getNom(),
+                          $user->getPrenom(),
+                          $user->getNaissance()]);
+
+    if ($for_real)
+      $user->delete();
+    $without_datas++;
+    continue;
+  }
+
+  if ('CHAM' != substr(strtoupper($user->getLogin()), 0, 4)) {
+    $no_target_log->write([$user->getId(),
+                           $user->getLogin(),
+                           $user->getNom(),
+                           $user->getPrenom(),
+                           $user->getNaissance(),
+                           'Card does not starts with CHAM']);
+    $without_target++;
+    echo 'F';
+    continue;
+  }
+
+  if (!$target = $datas->giveTo(['login' => substr($user->getLogin(), 4),
+                                 'statut' => 0,
+                                 'role_level' => 2],
+                                $for_real)) {
+    $no_target_log->write([$user->getId(),
+                           $user->getLogin(),
+                           $user->getNom(),
+                           $user->getPrenom(),
+                           $user->getNaissance(),
+                           $datas->getError()]);
+    $without_target++;
+    echo 'F';
+    continue;
+  }
+
+  $success_log->write([$user->getId(),
+                       $user->getLogin(),
+                       $user->getNom(),
+                       $user->getPrenom(),
+                       $user->getNaissance(),
+
+                       $target->getId(),
+                       $target->getLogin(),
+                       $target->getNom(),
+                       $target->getPrenom(),
+                       $target->getNaissance()]);
+
+  if ($for_real)
+    $user->delete();
+  $success++;
+  echo '.';
+}
+
+echo "\nEnded at " . date('c') . "\n";
+
+printf('
+%s without datas, log in deleted_without_datas.txt
+%s without target, log in not_deleted_target_problem.txt
+%s success, log in deleted_with_datas.txt
+
+', $without_datas, $without_target, $success);
\ No newline at end of file
diff --git a/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php b/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php
index cdcd4dcbe7dce15ef6b83f757cd6cdad522e0162..c3ea0dbd4423a8a6b2df3a7af2897ae7a1010c46 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhasePatronsTest.php
@@ -62,6 +62,14 @@ abstract class PhasePatronsTestCase extends Class_Cosmogramme_Integration_PhaseT
                     'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
                     'bib' => $bib_annecy,
                     'idabon' => '666']);
+
+    $this->fixture('Class_Users',
+                   ['id' => 2,
+                    'login' => 'A-000208',
+                    'password' => '2001',
+                    'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                    'bib' => $bib_annecy,
+                    'idabon' => 'A-000208']);
   }
 
 
@@ -104,15 +112,15 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /** @test */
-  public function secondAbonneShouldBeBarryWhite() {
-    $barry = Class_Users::find(3);
+  public function barryWhiteShouldBeImported() {
+    $barry = Class_Users::findFirstBy(['login' => 'A-000033']);
     $this->assertEquals('white BARRY', $barry->getNomComplet());
     return $barry;
   }
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryIdAbonShouldBeA000033($barry) {
@@ -121,7 +129,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryIdSIGBShouldBe47($barry) {
@@ -131,7 +139,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryOrdreabonShouldBeOne($barry) {
@@ -140,7 +148,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryMailShouldBeEmpty($barry) {
@@ -149,7 +157,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryPasswordShouldBe1978($barry) {
@@ -158,7 +166,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryNaissanceShouldBe1978_05_19($barry) {
@@ -167,7 +175,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryShouldNotHaveNULLAttribute($barry) {
@@ -176,7 +184,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryFinShouldBe2006_03_23($barry) {
@@ -185,7 +193,7 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
 
 
   /**
-   * @depends secondAbonneShouldBeBarryWhite
+   * @depends barryWhiteShouldBeImported
    * @test
    */
   public function barryStatutShouldBeZero($barry) {
@@ -205,6 +213,12 @@ class PhasePatronsFullImportTest extends PhasePatronsTestCase {
     $this->assertEquals(Class_Users::STATUT_TO_BE_DELETED,
                         Class_Users::find(1)->getStatut());
   }
+
+
+  /** @test */
+  public function mieszkalskiShouldNotBeMarkedForDeletion() {
+    $this->assertEquals(0, Class_Users::find(2)->getStatut());
+  }
 }
 
 
@@ -231,7 +245,7 @@ class PhasePatronsInvalidProfilTest extends PhasePatronsTestCase {
 
     $this->_phase->run();
     $this->assertLogContains('Configuration: colonne ' . $column . ' requise');
-    $this->assertEquals(1, Class_Users::countBy([]));
+    $this->assertEquals(2, Class_Users::count());
   }
 }
 
@@ -303,13 +317,13 @@ class PhasePatronsFullImportXMLTest extends PhasePatronsTestCase {
 
 
   /** @test */
-  public function nubmerOfPatronsShouldBeTwo() {
-    $this->assertEquals(2, Class_Users::countBy([]));
+  public function nubmerOfPatronsShouldBeThree() {
+    $this->assertEquals(3, Class_Users::count());
   }
 
 
   /** @test */
-  public function secondAbonneIdAbonShouldBe0003090() {
-    $this->assertEquals('00003090', Class_Users::find(2)->getIdabon());
+  public function pirlyShouldBeImported() {
+    $this->assertNotNull($user = Class_Users::findFirstBy(['idabon' => '00003090']));
   }
 }
\ No newline at end of file
diff --git a/tests/library/Class/UserDatasTest.php b/tests/library/Class/UserDatasTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8260fc02ff1d15d7d32e50d0d6cbfa2066d5914e
--- /dev/null
+++ b/tests/library/Class/UserDatasTest.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class UserDatasTestCase extends ModelTestCase {
+  protected
+    $_storm_default_to_volatile = true,
+    $_sql;
+
+
+  public function setUp() {
+    parent::setUp();
+    Zend_Registry::set('sql', $this->_sql = $this->mock());
+  }
+}
+
+
+
+class UserDatasTest extends UserDatasTestCase {
+  protected $_datas;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_Users', ['id' => 44,
+                                   'login' => 'procyon',
+                                   'password' => 'prof']);
+
+    $this->fixture('Class_Users', ['id' => 45,
+                                   'login' => 'actarus',
+                                   'password' => 'vega4ever']);
+
+    $this->fixture('Class_Users', ['id' => 46,
+                                   'login' => 'venusia',
+                                   'password' => 'vega4ever']);
+
+
+    $this->_sql->whenCalled('fetchOne')
+               ->answers(0);
+
+    $this->_datas = new Class_User_Datas(Class_Users::find(44));
+  }
+
+
+  /** @test */
+  public function withoutDatasShouldNotHaveDatas() {
+    $this->assertFalse($this->_datas->hasDatas());
+  }
+
+
+  /** @test */
+  public function withRatingsShouldHaveDatas() {
+    $this->_sql->whenCalled('fetchOne')
+               ->with('select count(id) from cms_avis where id_user=44')
+               ->answers(35);
+
+    $this->assertTrue($this->_datas->hasDatas());
+  }
+
+
+  /** @test */
+  public function givingToUnknownShouldDoNothing() {
+    $this->assertFalse($this->_datas->giveTo(['login' => 'alcor'], true));
+    $this->assertEquals('No target found', $this->_datas->getError());
+  }
+
+
+  /** @test */
+  public function givingToMultipleTargetsShouldDoNothing() {
+    $this->assertFalse($this->_datas->giveTo(['password' => 'vega4ever'], true));
+    $this->assertEquals('Too many targets', $this->_datas->getError());
+  }
+
+
+  /** @test */
+  public function givingToActarusShouldUpdateDatabase() {
+    $queries = [['notices_paniers', 'id_user'],
+                ['notices_avis', 'id_user'],
+                ['cms_avis', 'id_user'],
+                ['suggestion_achat', 'user_id'],
+                ['session_formation_inscriptions', 'stagiaire_id'],
+                ['user_group_memberships', 'user_id'],
+                ['formulaires', 'id_user'],
+                ['multimedia_devicehold', 'id_user']
+    ];
+
+    foreach($queries as $query)
+      $this->_sql->whenCalled('query')
+                 ->with(sprintf('update %s set %s=45 where %s=44',
+                                $query[0], $query[1], $query[1]))
+                 ->answers(1);
+
+    $this->_sql->whenCalled('query')
+               ->with('update notices_avis set user_key=\'--0--actarus\' where id_user=45')
+               ->answers(1);
+
+    $this->assertEquals(45,
+                        $this->_datas->giveTo(['login' => 'actarus'], true)->getId());
+
+    $this
+      ->assertTrue($this->_sql
+                   ->methodHasBeenCalledWithParams('query',
+                                                   ['update notices_avis set id_user=45 where id_user=44']));
+
+    $this
+      ->assertTrue($this->_sql
+                   ->methodHasBeenCalledWithParams('query',
+                                                   ['update notices_avis set user_key=\'--0--actarus\' where id_user=45']));
+  }
+
+
+  /** @test */
+  public function givingToActarusNotForRealShouldDoNothing() {
+    $this->assertEquals(45,
+                        $this->_datas->giveTo(['login' => 'actarus'], false)->getId());
+  }
+}
\ No newline at end of file