diff --git a/Actions/Remove/PersonMove.php b/Actions/Remove/PersonMove.php
new file mode 100644
index 000000000..112d8daa4
--- /dev/null
+++ b/Actions/Remove/PersonMove.php
@@ -0,0 +1,130 @@
+
+ *
+ * This program 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see .
+ */
+namespace Chill\PersonBundle\Actions\Remove;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Chill\PersonBundle\Entity\Person;
+use Chill\PersonBundle\Entity\AccompanyingPeriod;
+use Doctrine\ORM\Mapping\ClassMetadata;
+
+/**
+ * Move all person to a new one, and delete the old record.
+ *
+ */
+class PersonMove
+{
+ /**
+ *
+ * @var EntityManagerInterface
+ */
+ protected $em;
+
+ public function __construct(EntityManagerInterface $em)
+ {
+ $this->em = $em;
+ }
+
+ public function getSQL(Person $from, Person $to)
+ {
+ $sqls = [];
+
+ foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) {
+ if ($metadata->isMappedSuperclass) {
+ continue;
+ }
+
+ foreach ($metadata->getAssociationMappings() as $field => $mapping) {
+ if ($mapping['targetEntity'] === Person::class) {
+ if (\in_array($metadata->getName(), $this->deleteEntities())) {
+ $sqls[] = $this->createDeleteSQL($metadata, $from, $field);
+ } else {
+ $sqls[] = $this->createMoveSQL($metadata, $from, $to, $field);
+ }
+ }
+ }
+ }
+
+ $personMetadata = $this->em->getClassMetadata(Person::class);
+ $sqls[] = sprintf("DELETE FROM %s WHERE id = %d",
+ $this->getTableName($personMetadata),
+ $from->getId());
+
+ return $sqls ?? [];
+ }
+
+ protected function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string
+ {
+ $mapping = $metadata->getAssociationMapping($field);
+
+ // Set part of the query, aka in "UPDATE table SET "
+ $sets = [];
+ foreach ($mapping["joinColumns"] as $columns) {
+ $sets[] = sprintf("%s = %d", $columns["name"], $to->getId());
+ }
+
+ $conditions = [];
+ foreach ($mapping["joinColumns"] as $columns) {
+ $conditions[] = sprintf("%s = %d", $columns["name"], $from->getId());
+ }
+
+ return \sprintf("UPDATE %s SET %s WHERE %s",
+ $this->getTableName($metadata),
+ \implode(" ", $sets),
+ \implode(" AND ", $conditions)
+ );
+ }
+
+ protected function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string
+ {
+ $mapping = $metadata->getAssociationMapping($field);
+
+ $conditions = [];
+ foreach ($mapping["joinColumns"] as $columns) {
+ $conditions[] = sprintf("%s = %d", $columns["name"], $from->getId());
+ }
+
+ return \sprintf("DELETE FROM %s WHERE %s",
+ $this->getTableName($metadata),
+ \implode(" AND ", $conditions)
+ );
+ }
+
+ /**
+ * return an array of classes where entities should be deleted
+ * instead of moved
+ *
+ * @return array
+ */
+ protected function deleteEntities(): array
+ {
+ return [
+ AccompanyingPeriod::class
+ ];
+ }
+
+ /**
+ * get the full table name with schema if it does exists
+ */
+ private function getTableName(ClassMetadata $metadata): string
+ {
+ return empty($metadata->getSchemaName()) ?
+ $metadata->getTableName() :
+ $metadata->getSchemaName().".".$metadata->getTableName();
+ }
+
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d8755d02..4266450b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,3 +39,5 @@ Branche master
==============
- Update address validation
+- Add command to move person and all data of a person to a new one, and delete the old one.
+
diff --git a/Command/ChillPersonMoveCommand.php b/Command/ChillPersonMoveCommand.php
new file mode 100644
index 000000000..6ab2c8979
--- /dev/null
+++ b/Command/ChillPersonMoveCommand.php
@@ -0,0 +1,103 @@
+mover = $mover;
+ $this->em = $em;
+ }
+
+ protected function configure()
+ {
+ $this
+ ->setName('chill:person:move')
+ ->setDescription('Move all the associated entities on a "from" person to a "to" person and remove the old person')
+ ->addOption('from', 'f', InputOption::VALUE_REQUIRED, "The person id to delete, all associated data will be moved before")
+ ->addOption('to', 't', InputOption::VALUE_REQUIRED, "The person id which will received data")
+ ->addOption('dump-sql', null, InputOption::VALUE_NONE, "dump sql to stdout")
+ ->addOption('force', null, InputOption::VALUE_NONE, "execute sql instead of dumping it")
+ ;
+ }
+
+ protected function interact(InputInterface $input, OutputInterface $output)
+ {
+ if (FALSE === ($input->hasOption('dump-sql') || $input->hasOption('force'))) {
+ $msg = "You must use \"--dump-sql\" or \"--force\"";
+ throw new RuntimeException($msg);
+ }
+
+ foreach (["from", "to"] as $name) {
+ if (empty($input->getOption($name))) {
+ throw new RuntimeException("You must set a \"$name\" option");
+ }
+ $id = $input->getOption($name);
+ if (\ctype_digit($id) === FALSE) {
+ throw new RuntimeException("The id in \"$name\" field does not contains "
+ . "only digits: $id");
+ }
+ }
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $repository = $this->em->getRepository(Person::class);
+ $from = $repository->find($input->getOption('from'));
+ $to = $repository->find($input->getOption('to'));
+
+ if ($from === NULL) {
+ throw new RuntimeException(sprintf("Person \"from\" with id %d not found", $input->getOption('from')));
+ }
+ if ($to === NULL) {
+ throw new RuntimeException(sprintf("Person \"to\" with id %d not found", $input->getOption('to')));
+ }
+
+ $sqls = $this->mover->getSQL($from, $to);
+
+ if ($input->getOption('dump-sql')) {
+ foreach($sqls as $sql) {
+ $output->writeln($sql);
+ }
+ } else {
+ $connection = $this->em->getConnection();
+ $connection->beginTransaction();
+ foreach($sqls as $sql) {
+ if ($output->isVerbose()) {
+ $output->writeln($sql);
+ }
+ $connection->executeQuery($sql);
+ }
+ $connection->commit();
+
+ }
+ }
+
+}
diff --git a/DependencyInjection/ChillPersonExtension.php b/DependencyInjection/ChillPersonExtension.php
index 4034af894..41a7ab71f 100644
--- a/DependencyInjection/ChillPersonExtension.php
+++ b/DependencyInjection/ChillPersonExtension.php
@@ -64,6 +64,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/menu.yml');
$loader->load('services/privacyEvent.yml');
$loader->load('services/command.yml');
+ $loader->load('services/actions.yml');
if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') {
$loader->load('services/exports_accompanying_period.yml');
diff --git a/Resources/config/services/actions.yml b/Resources/config/services/actions.yml
new file mode 100644
index 000000000..22b50b1f8
--- /dev/null
+++ b/Resources/config/services/actions.yml
@@ -0,0 +1,4 @@
+services:
+ Chill\PersonBundle\Actions\Remove\PersonMove:
+ arguments:
+ $em: '@Doctrine\ORM\EntityManagerInterface'
\ No newline at end of file
diff --git a/Resources/config/services/command.yml b/Resources/config/services/command.yml
index c99e488a4..fec8bdfd4 100644
--- a/Resources/config/services/command.yml
+++ b/Resources/config/services/command.yml
@@ -8,3 +8,10 @@ services:
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
tags:
- { name: console.command }
+
+ Chill\PersonBundle\Command\ChillPersonMoveCommand:
+ arguments:
+ $em: '@Doctrine\ORM\EntityManagerInterface'
+ $mover: '@Chill\PersonBundle\Actions\Remove\PersonMove'
+ tags:
+ - { name: console.command }