From 9550224559ca871930f9b95c35afe1b0ae7c79e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laurent=20L=C3=A9cluse?= <laurent.lecluse@unicaen.fr>
Date: Fri, 6 May 2016 13:52:47 +0000
Subject: [PATCH] transfert du code depuis OSE et adaptation

---
 Module.php                                    |   2 -
 config/module.config.php                      | 164 ++++-
 config/unicaen-import.global.php.dist         |  10 +-
 .../Controller/ImportController.php           | 139 ++++
 .../Db/Interfaces/ImportAwareInterface.php    |  42 ++
 src/UnicaenImport/Entity/Db/Source.php        | 166 +++++
 .../Entity/Db/Traits/ImportAwareTrait.php     |  75 +++
 .../Entity/Db/Traits/SourceAwareTrait.php     |  42 ++
 .../Entity/Differentiel/Ligne.php             | 212 +++++++
 .../Entity/Differentiel/Query.php             | 552 ++++++++++++++++
 src/UnicaenImport/Entity/Schema/Column.php    |  64 ++
 src/UnicaenImport/Exception/Exception.php     |  54 ++
 .../Exception/MissingDependency.php           |  12 +
 src/UnicaenImport/Options/ModuleOptions.php   |  73 +++
 .../Options/ModuleOptionsFactory.php          |  27 +
 .../Traits/ModuleOptionsAwareTrait.php        |  56 ++
 .../Processus/ImportProcessus.php             |  80 +++
 .../Traits/ImportProcessusAwareTrait.php      |  56 ++
 .../Provider/Privilege/Privileges.php         |  20 +
 src/UnicaenImport/Service/AbstractService.php | 169 +++++
 .../Service/DifferentielService.php           |  83 +++
 .../Service/QueryGeneratorService.php         | 596 ++++++++++++++++++
 src/UnicaenImport/Service/SchemaService.php   | 146 +++++
 .../Traits/DifferentielServiceAwareTrait.php  |  56 ++
 .../QueryGeneratorServiceAwareTrait.php       |  56 ++
 .../Traits/SchemaServiceAwareTrait.php        |  56 ++
 .../DifferentielLigne/DifferentielLigne.php   | 225 +++++++
 .../View/Helper/DifferentielListe.php         | 122 ++++
 view/unicaen-import/import/config.phtml       |   3 +
 view/unicaen-import/import/index.phtml        |  10 +
 view/unicaen-import/import/show-diff.phtml    |  56 ++
 .../import/show-import-tbl.phtml              |  33 +
 .../import/update-materialized-view.php       |   8 +
 .../unicaen-import/import/update-tables.phtml |   1 +
 .../import/update-views-and-packages.phtml    |   1 +
 view/unicaen-import/import/update.phtml       |  28 +
 36 files changed, 3489 insertions(+), 6 deletions(-)
 create mode 100644 src/UnicaenImport/Controller/ImportController.php
 create mode 100644 src/UnicaenImport/Entity/Db/Interfaces/ImportAwareInterface.php
 create mode 100644 src/UnicaenImport/Entity/Db/Source.php
 create mode 100644 src/UnicaenImport/Entity/Db/Traits/ImportAwareTrait.php
 create mode 100644 src/UnicaenImport/Entity/Db/Traits/SourceAwareTrait.php
 create mode 100644 src/UnicaenImport/Entity/Differentiel/Ligne.php
 create mode 100644 src/UnicaenImport/Entity/Differentiel/Query.php
 create mode 100644 src/UnicaenImport/Entity/Schema/Column.php
 create mode 100644 src/UnicaenImport/Exception/Exception.php
 create mode 100644 src/UnicaenImport/Exception/MissingDependency.php
 create mode 100644 src/UnicaenImport/Options/ModuleOptions.php
 create mode 100644 src/UnicaenImport/Options/ModuleOptionsFactory.php
 create mode 100644 src/UnicaenImport/Options/Traits/ModuleOptionsAwareTrait.php
 create mode 100644 src/UnicaenImport/Processus/ImportProcessus.php
 create mode 100644 src/UnicaenImport/Processus/Traits/ImportProcessusAwareTrait.php
 create mode 100644 src/UnicaenImport/Provider/Privilege/Privileges.php
 create mode 100644 src/UnicaenImport/Service/AbstractService.php
 create mode 100644 src/UnicaenImport/Service/DifferentielService.php
 create mode 100644 src/UnicaenImport/Service/QueryGeneratorService.php
 create mode 100644 src/UnicaenImport/Service/SchemaService.php
 create mode 100644 src/UnicaenImport/Service/Traits/DifferentielServiceAwareTrait.php
 create mode 100644 src/UnicaenImport/Service/Traits/QueryGeneratorServiceAwareTrait.php
 create mode 100644 src/UnicaenImport/Service/Traits/SchemaServiceAwareTrait.php
 create mode 100644 src/UnicaenImport/View/Helper/DifferentielLigne/DifferentielLigne.php
 create mode 100644 src/UnicaenImport/View/Helper/DifferentielListe.php
 create mode 100644 view/unicaen-import/import/config.phtml
 create mode 100644 view/unicaen-import/import/index.phtml
 create mode 100644 view/unicaen-import/import/show-diff.phtml
 create mode 100644 view/unicaen-import/import/show-import-tbl.phtml
 create mode 100644 view/unicaen-import/import/update-materialized-view.php
 create mode 100644 view/unicaen-import/import/update-tables.phtml
 create mode 100644 view/unicaen-import/import/update-views-and-packages.phtml
 create mode 100644 view/unicaen-import/import/update.phtml

diff --git a/Module.php b/Module.php
index 4afc351..d4b4133 100644
--- a/Module.php
+++ b/Module.php
@@ -5,8 +5,6 @@ namespace UnicaenImport;
 use Zend\ModuleManager\Feature\ConfigProviderInterface;
 use Zend\Mvc\MvcEvent;
 
-include_once 'Functions.php';
-
 /**
  *
  *
diff --git a/config/module.config.php b/config/module.config.php
index 8cbd0ff..83e127d 100644
--- a/config/module.config.php
+++ b/config/module.config.php
@@ -1,6 +1,164 @@
 <?php
 
+namespace UnicaenImport;
+
+use UnicaenAuth\Guard\PrivilegeController;
+use UnicaenImport\Provider\Privilege\Privileges;
+
 return [
-    'unicaen-import' => [
-    ]
-];
+    'controllers' => [
+        'invokables' => [
+            'Import\Controller\Import' => Controller\ImportController::class,
+        ],
+    ],
+
+    'router' => [
+        'routes' => [
+            'import' => [
+                'type'    => 'Segment',
+                'options' => [
+                    'route'    => '/import[/:action][/:table]',
+                    'defaults' => [
+                        '__NAMESPACE__' => 'Import\Controller',
+                        'controller'    => 'Import',
+                        'action'        => 'index',
+                        'table'         => null,
+                    ],
+                ],
+            ],
+        ],
+    ],
+
+    'navigation' => [
+        'default' => [
+            'home' => [
+                'pages' => [
+                    'import' => [
+                        'label'    => 'Import',
+                        'order'    => 1,
+                        'route'    => 'import',
+                        'resource' => PrivilegeController::getResourceId('Import\Controller\Import', 'index'),
+                        'pages'    => [
+                            'showDiff'               => [
+                                'label'       => "Écarts entre les données de l'application et ses sources",
+                                'description' => "Affiche, table par table, la liste des données différentes entre l'application et ses sources de données",
+                                'route'       => 'import',
+                                'resource'    => PrivilegeController::getResourceId('Import\Controller\Import', 'showdiff'),
+                                'params'      => [
+                                    'action' => 'showDiff',
+                                ],
+                            ],
+                            'updateTables'           => [
+                                'label'       => "Mise à jour des données à partir de leurs sources",
+                                'description' => "Met à jour l'ensemble des données partir de leurs sources respectives.",
+                                'route'       => 'import',
+                                'resource'    => PrivilegeController::getResourceId('Import\Controller\Import', 'updateTables'),
+                                'params'      => [
+                                    'action' => 'updateTables',
+                                ],
+                            ],
+                            'show-import-tbl'        => [
+                                'label'       => "Tableau de bord principal",
+                                'description' => "Liste, table par table, les colonnes dont les données sont importables ou non, leur caractéristiques et l'état de l'import à leur niveau.",
+                                'route'       => 'import',
+                                'resource'    => PrivilegeController::getResourceId('Import\Controller\Import', 'show-import-tbl'),
+                                'params'      => [
+                                    'action' => 'show-import-tbl',
+                                ],
+                            ],
+                            'updateViewsAndPackages' => [
+                                'label'       => "Mise à jour des vues différentielles et des procédures de mise à jour",
+                                'description' => "Réactualise les vues différentielles d'import. Ces dernières servent à déterminer quelles données ont changé,
+        sont apparues ou ont disparues des sources de données.
+        Met également à jour les procédures de mise à jour qui actualisent les données de l'application à partir des informations
+        fournies par les vues différentielles.
+        Cette réactualisation n'est utile que si les vues sources ont été modifiées.",
+                                'route'       => 'import',
+                                'resource'    => PrivilegeController::getResourceId('Import\Controller\Import', 'updateViewsAndPackages'),
+                                'params'      => [
+                                    'action' => 'updateViewsAndPackages',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ],
+    ],
+
+    'bjyauthorize' => [
+        'guards' => [
+            PrivilegeController::class => [
+                [
+                    'controller' => 'Import\Controller\Import',
+                    'action'     => ['index'],
+                    'privileges' => [Privileges::IMPORT_ECARTS, Privileges::IMPORT_MAJ, Privileges::IMPORT_TBL, Privileges::IMPORT_VUES_PROCEDURES],
+                ],
+                [
+                    'controller' => 'Import\Controller\Import',
+                    'action'     => ['showDiff'],
+                    'privileges' => [Privileges::IMPORT_ECARTS],
+                ],
+                [
+                    'controller' => 'Import\Controller\Import',
+                    'action'     => ['show-import-tbl'],
+                    'privileges' => [Privileges::IMPORT_TBL],
+                ],
+                [
+                    'controller' => 'Import\Controller\Import',
+                    'action'     => ['update', 'updateTables'],
+                    'privileges' => [Privileges::IMPORT_MAJ],
+                ],
+                [
+                    'controller' => 'Import\Controller\Import',
+                    'action'     => ['updateViewsAndPackages'],
+                    'privileges' => [Privileges::IMPORT_VUES_PROCEDURES],
+                ],
+            ],
+        ],
+    ],
+
+    'doctrine' => [
+        'driver' => [
+            'unicaen_import_driver' => [
+                'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
+                'cache' => 'array',
+                'paths' => [
+                    __DIR__ . '/../src/UnicaenImport/Entity/Db',
+                ],
+            ],
+            'orm_default'           => [
+                'class'   => 'Doctrine\ORM\Mapping\Driver\DriverChain',
+                'drivers' => [
+                    'UnicaenImport\Entity\Db' => 'unicaen_import_driver',
+                ],
+            ],
+        ],
+    ],
+
+    'service_manager' => [
+        'invokables' => [
+            'UnicaenImport\Service\Schema'         => Service\SchemaService::class,
+            'UnicaenImport\Service\QueryGenerator' => Service\QueryGeneratorService::class,
+            'UnicaenImport\Service\Differentiel'   => Service\DifferentielService::class,
+            'UnicaenImport\Processus\Import'       => Processus\ImportProcessus::class,
+        ],
+        'factories'  => [
+            'UnicaenImport\Options\Module' => Options\ModuleOptionsFactory::class,
+        ],
+    ],
+
+    'view_helpers' => [
+        'invokables' => [
+            'differentielListe' => View\Helper\DifferentielListe::class,
+            'differentielLigne' => View\Helper\DifferentielLigne\DifferentielLigne::class,
+        ],
+    ],
+
+    'view_manager' => [
+        'template_path_stack' => [
+            'import' => __DIR__ . '/../view',
+        ],
+    ],
+
+];
\ No newline at end of file
diff --git a/config/unicaen-import.global.php.dist b/config/unicaen-import.global.php.dist
index b625128..2e85764 100644
--- a/config/unicaen-import.global.php.dist
+++ b/config/unicaen-import.global.php.dist
@@ -1,4 +1,12 @@
 <?php
 
 return [
-];
+    /* Configuration d'UnicaenImport */
+    'unicaen-import' => [
+
+        //Liste des aides de vues facilitant la lecture du différentiel (écarts de données entre l'appli et sa source)
+        'differentiel_view_helpers' => [
+            /* nom de la table (attention à la CASSE) => Nom de l'aide de vue (qui doit hériter de UnicaenImport\View\Helper\DifferentielLigne\DifferentielLigne) */
+        ],
+    ],
+];
\ No newline at end of file
diff --git a/src/UnicaenImport/Controller/ImportController.php b/src/UnicaenImport/Controller/ImportController.php
new file mode 100644
index 0000000..31d98c8
--- /dev/null
+++ b/src/UnicaenImport/Controller/ImportController.php
@@ -0,0 +1,139 @@
+<?php
+namespace UnicaenImport\Controller;
+
+use UnicaenImport\Processus\Traits\ImportProcessusAwareTrait;
+use UnicaenImport\Service\Traits\DifferentielServiceAwareTrait;
+use UnicaenImport\Service\Traits\QueryGeneratorServiceAwareTrait;
+use UnicaenImport\Service\Traits\SchemaServiceAwareTrait;
+use Zend\Mvc\Controller\AbstractActionController;
+use UnicaenImport\Entity\Differentiel\Query;
+
+/**
+ *
+ *
+ * @author Laurent Lécluse <laurent.lecluse at unicaen.fr>
+ */
+class ImportController extends AbstractActionController
+{
+    use SchemaServiceAwareTrait;
+    use QueryGeneratorServiceAwareTrait;
+    use DifferentielServiceAwareTrait;
+    use ImportProcessusAwareTrait;
+
+
+
+
+    public function indexAction()
+    {
+    }
+
+
+
+    public function updateViewsAndPackagesAction()
+    {
+        try {
+            $this->getProcessusImport()->updateViewsAndPackages();
+            $message = 'Mise à jour des vues différentielles et du paquetage d\'import terminés';
+        } catch (\Exception $e) {
+            $message = 'Une erreur a été rencontrée.';
+            throw new \UnicaenApp\Exception\LogicException("import impossible", null, $e);
+        }
+        $title = "Résultat";
+
+        return compact('message', 'title');
+    }
+
+
+
+    public function showImportTblAction()
+    {
+        $data = $this->getServiceSchema()->getSchema();
+
+        return compact('data');
+    }
+
+
+
+    public function showDiffAction()
+    {
+        $tableName = $this->params()->fromRoute('table');
+
+        $sc = $this->getServiceSchema();
+
+        $mviews = $sc->getImportMviews();
+
+        if ($tableName) {
+            $tables = [$tableName];
+        } else {
+            $tables = $sc->getImportTables();
+            sort($tables);
+        }
+
+        $data = [];
+        foreach ($tables as $table) {
+            $query = new Query($table);
+            $query->setLimit(101);
+            $data[$table] = $this->getServiceDifferentiel()->make($query)->fetchAll();
+        }
+
+        return compact('data', 'mviews');
+    }
+
+
+
+    public function updateAction()
+    {
+        $errors    = [];
+        $tableName = $this->params()->fromRoute('table');
+        $typeMaj   = $this->params()->fromPost('type-maj');
+
+        $query = new Query($tableName);
+
+        $sq = $this->getServiceQueryGenerator();
+
+        /* Mise à jour des données et récupération des éventuelles erreurs */
+        try {
+            if ('vue-materialisee' == $typeMaj) {
+                $sq->execMajVM($tableName);
+            } else {
+                $errors = $errors + $sq->syncTable($tableName);
+                //$sq->execMaj($query);
+            }
+        } catch (\Exception $e) {
+            $errors = [$e->getMessage()];
+        }
+        $query->setNotNull([]); // Aucune colonne ne doit être non nulle !!
+        $query->setLimit(101);
+        $lignes = $this->getServiceDifferentiel()->make($query)->fetchAll();
+
+        return [
+            'lignes' => $lignes,
+            'table'  => $tableName,
+            'errors' => $errors,
+        ];
+    }
+
+
+
+    public function updateTablesAction()
+    {
+        $tables = $this->getServiceSchema()->getImportTables();
+        sort($tables);
+
+        $message = '';
+        try {
+            foreach ($tables as $table) {
+                $message .= '<div>Table "' . $table . '" Mise à jour.</div>';
+                $this->getServiceQueryGenerator()->execMaj(new Query($table));
+            }
+            $message .= 'Mise à jour des données terminée';
+        } catch (\Exception $e) {
+            $message = 'Une erreur a été rencontrée.';
+            throw new \UnicaenApp\Exception\LogicException("mise à jour des données impossible", null, $e);
+        }
+
+        $title = "Résultat";
+
+        return compact('message', 'title');
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Entity/Db/Interfaces/ImportAwareInterface.php b/src/UnicaenImport/Entity/Db/Interfaces/ImportAwareInterface.php
new file mode 100644
index 0000000..d58a308
--- /dev/null
+++ b/src/UnicaenImport/Entity/Db/Interfaces/ImportAwareInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace UnicaenImport\Entity\Db\Interfaces;
+use UnicaenImport\Entity\Db\Source;
+
+
+/**
+ * Interface des entités possédant une gestion de l'import
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+interface ImportAwareInterface
+{
+    public function setSource(Source $source = null);
+
+
+
+    /**
+     * @return Source
+     */
+    public function getSource();
+
+
+
+    /**
+     * Set sourceCode
+     *
+     * @param string $sourceCode
+     *
+     * @return self
+     */
+    public function setSourceCode($sourceCode);
+
+
+
+    /**
+     * Get sourceCode
+     *
+     * @return string
+     */
+    public function getSourceCode();
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Entity/Db/Source.php b/src/UnicaenImport/Entity/Db/Source.php
new file mode 100644
index 0000000..be64a52
--- /dev/null
+++ b/src/UnicaenImport/Entity/Db/Source.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace UnicaenImport\Entity\Db;
+
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Source
+ *
+ * @ORM\Entity
+ * @ORM\Table(name="SOURCE")
+ */
+class Source
+{
+
+    /**
+     * @var string
+     * @ORM\Column(name="CODE", type="string", length=255, unique=true, nullable=false)
+     */
+    protected $code;
+
+    /**
+     * @var boolean
+     * @ORM\Column(name="IMPORTABLE", type="boolean", nullable=false)
+     */
+    protected $importable;
+
+    /**
+     * @var string
+     * @ORM\Column(name="LIBELLE", type="string", length=255, unique=true, nullable=false)
+     */
+    protected $libelle;
+
+    /**
+     * @var int
+     * @ORM\Id
+     * @ORM\Column(name="ID", type="integer")
+     * @ORM\GeneratedValue(strategy="SEQUENCE")
+     */
+    protected $id;
+
+
+
+    /**
+     * Set code
+     *
+     * @param string $code
+     *
+     * @return Source
+     */
+    public function setCode($code)
+    {
+        $this->code = $code;
+
+        return $this;
+    }
+
+
+
+    /**
+     * Get code
+     *
+     * @return string
+     */
+    public function getCode()
+    {
+        return $this->code;
+    }
+
+
+
+    /**
+     * Set importable
+     *
+     * @param boolean $importable
+     *
+     * @return Source
+     */
+    public function setImportable($importable)
+    {
+        $this->importable = $importable;
+
+        return $this;
+    }
+
+
+
+    /**
+     * Get importable
+     *
+     * @return boolean
+     */
+    public function getImportable()
+    {
+        return $this->importable;
+    }
+
+
+
+    /**
+     * Set libelle
+     *
+     * @param string $libelle
+     *
+     * @return Source
+     */
+    public function setLibelle($libelle)
+    {
+        $this->libelle = $libelle;
+
+        return $this;
+    }
+
+
+
+    /**
+     * Get libelle
+     *
+     * @return string
+     */
+    public function getLibelle()
+    {
+        return $this->libelle;
+    }
+
+
+
+    /**
+     * Get id
+     *
+     * @return integer
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+
+
+    /**
+     * Retourne la représentation littérale de cet objet.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->getLibelle();
+    }
+
+
+
+    /**
+     * @since PHP 5.6.0
+     * This method is called by var_dump() when dumping an object to get the properties that should be shown.
+     * If the method isn't defined on an object, then all public, protected and private properties will be shown.
+     *
+     * @return array
+     * @link  http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+     */
+    function __debugInfo()
+    {
+        return [
+            'libelle' => $this->libelle,
+        ];
+    }
+}
diff --git a/src/UnicaenImport/Entity/Db/Traits/ImportAwareTrait.php b/src/UnicaenImport/Entity/Db/Traits/ImportAwareTrait.php
new file mode 100644
index 0000000..9477747
--- /dev/null
+++ b/src/UnicaenImport/Entity/Db/Traits/ImportAwareTrait.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace UnicaenImport\Entity\Db\Traits;
+
+use UnicaenImport\Entity\Db\Source;
+
+/**
+ * Description of SourceAwareTrait
+ *
+ * @author UnicaenCode
+ */
+trait ImportAwareTrait
+{
+    /**
+     * @var Source
+     */
+    private $source;
+
+    /**
+     * @var string
+     */
+    private $sourceCode;
+
+
+
+    /**
+     * @param Source $source
+     *
+     * @return self
+     */
+    public function setSource(Source $source = null)
+    {
+        $this->source = $source;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return Source
+     */
+    public function getSource()
+    {
+        return $this->source;
+    }
+
+
+
+    /**
+     * Set sourceCode
+     *
+     * @param string $sourceCode
+     *
+     * @return self
+     */
+    public function setSourceCode($sourceCode)
+    {
+        $this->sourceCode = $sourceCode;
+
+        return $this;
+    }
+
+
+
+    /**
+     * Get sourceCode
+     *
+     * @return string
+     */
+    public function getSourceCode()
+    {
+        return $this->sourceCode;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Entity/Db/Traits/SourceAwareTrait.php b/src/UnicaenImport/Entity/Db/Traits/SourceAwareTrait.php
new file mode 100644
index 0000000..f4cd454
--- /dev/null
+++ b/src/UnicaenImport/Entity/Db/Traits/SourceAwareTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace UnicaenImport\Entity\Db\Traits;
+
+use UnicaenImport\Entity\Db\Source;
+
+/**
+ * Description of SourceAwareTrait
+ *
+ * @author UnicaenCode
+ */
+trait SourceAwareTrait
+{
+    /**
+     * @var Source
+     */
+    private $source;
+
+
+
+
+
+    /**
+     * @param Source $source
+     * @return self
+     */
+    public function setSource( Source $source = null )
+    {
+        $this->source = $source;
+        return $this;
+    }
+
+
+
+    /**
+     * @return Source
+     */
+    public function getSource()
+    {
+        return $this->source;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Entity/Differentiel/Ligne.php b/src/UnicaenImport/Entity/Differentiel/Ligne.php
new file mode 100644
index 0000000..a41d55c
--- /dev/null
+++ b/src/UnicaenImport/Entity/Differentiel/Ligne.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace UnicaenImport\Entity\Differentiel;
+
+use Doctrine\ORM\EntityManager;
+use UnicaenImport\Entity\Db\Source;
+
+/**
+ * Classe permettant de récupérer une ligne de différentiel
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class Ligne
+{
+
+    /**
+     * Entity Manager
+     *
+     * @var EntityManager
+     */
+    protected $entityManager;
+
+    /**
+     * Nom de la table
+     *
+     * @var string
+     */
+    protected $tableName;
+
+    /**
+     * ID
+     *
+     * @var integer
+     */
+    protected $id;
+
+    /**
+     * Action
+     *
+     * @var string
+     */
+    protected $action;
+
+    /**
+     * ID de la source
+     *
+     * @var Source
+     */
+    protected $source;
+
+    /**
+     * Code source
+     *
+     * @var string
+     */
+    protected $sourceCode;
+
+    /**
+     * Données des colonnes
+     *
+     * @var array
+     */
+    protected $values;
+
+    /**
+     * Liste des colonnes ayant changé
+     *
+     * @var boolean[]
+     */
+    protected $changed;
+
+
+
+
+    /**
+     *
+     * @param Statement $stmt
+     */
+    public function __construct(EntityManager $entityManager, $tableName, array $data)
+    {
+        $this->tableName = $tableName;
+        $this->entityManager = $entityManager;
+
+        $this->id = (integer)$data['ID'];
+        unset($data['ID']);
+
+        $this->action = $data['IMPORT_ACTION'];
+        unset($data['IMPORT_ACTION']);
+
+        $this->source = $entityManager->find(\UnicaenImport\Entity\Db\Source::class, (integer)$data['SOURCE_ID']);
+        unset($data['SOURCE_ID']);
+
+        $this->sourceCode = $data['SOURCE_CODE'];
+        unset($data['SOURCE_CODE']);
+
+        $keys = array_keys( $data );
+        foreach( $keys as $key ){
+            if (in_array('U_'.$key, $keys)){
+                $this->values[$key] = $data[$key];
+                $this->changed[$key] = $data['U_'.$key] === '1';
+            }
+        }
+    }
+
+    /**
+     * Retourne le nom de la table correspondante
+     *
+     * @return string
+     */
+    public function getTableName()
+    {
+        return $this->tableName;
+    }
+
+    /**
+     * Retourne l'ID OSE de l'enregistrement
+     *
+     * @return integer
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Retourne le type d'action prévue pour l'import
+     *
+     * @return string
+     */
+    public function getAction()
+    {
+        return $this->action;
+    }
+
+    /**
+     * Retourne lasource de données
+     *
+     * @return Source
+     */
+    public function getSource()
+    {
+        return $this->source;
+    }
+
+    /**
+     * Retourne le code de la donnée source
+     *
+     * @return string
+     */
+    public function getSourceCode()
+    {
+        return $this->sourceCode;
+    }
+
+    /**
+     * Retourne, sous forme de chaîne de caractères, la valeur de la colonne donnée
+     *
+     * @param string $colName
+     * @return string
+     */
+    public function get( $colName )
+    {
+        return $this->values[$colName];
+    }
+
+    /**
+     * Retourne l'entité Doctrine correspondante
+     *
+     * @return StdClass
+     */
+    public function getEntity()
+    {
+        $filter = new \Zend\Filter\Word\UnderscoreToCamelCase;
+        $entityClass = 'Application\\Entity\Db\\'.$filter->filter(strtolower($this->getTableName()));
+        return $this->entityManager->find($entityClass, $this->getId());
+    }
+
+    /**
+     * Retourne true si la colonne $colName a changé, false sinon
+     *
+     * @param string $colName
+     * @return boolean
+     */
+    public function hasChanged( $colName )
+    {
+        return $this->changed[$colName];
+    }
+
+    /**
+     * Retourne un tableau des colonnes ayant changé
+     *
+     * @return array
+     */
+    public function getChanges()
+    {
+        $changes = [];
+        foreach( $this->changed as $colName => $changed ){
+            if ($changed) $changes[$colName] = $this->values[$colName];
+        }
+        return $changes;
+    }
+
+    /**
+     * Retourne le gestionnaire d'entités correspondant
+     *
+     * @return EntityManager
+     */
+    public function getEntityManager()
+    {
+        return $this->entityManager;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Entity/Differentiel/Query.php b/src/UnicaenImport/Entity/Differentiel/Query.php
new file mode 100644
index 0000000..32a043a
--- /dev/null
+++ b/src/UnicaenImport/Entity/Differentiel/Query.php
@@ -0,0 +1,552 @@
+<?php
+
+namespace UnicaenImport\Entity\Differentiel;
+
+use UnicaenImport\Entity\Db\Source;
+use UnicaenImport\Exception\Exception;
+use UnicaenImport\Service\QueryGeneratorService;
+use UnicaenImport\Service\AbstractService;
+
+
+/**
+ * Classe permettant de créer une requête de récupération de différentiel
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class Query
+{
+
+    const ACTION_INSERT = 'insert';
+    const ACTION_UPDATE = 'update';
+    const ACTION_DELETE = 'delete';
+    const ACTION_UNDELETE = 'undelete';
+
+    /**
+     * Nom de la table
+     *
+     * @var string
+     */
+    protected $tableName;
+
+    /**
+     * ID
+     *
+     * @var integer|integer[]|null
+     */
+    protected $id;
+
+    /**
+     * Action
+     *
+     * @var string|string[]|null
+     */
+    protected $action;
+
+    /**
+     * Source de données
+     *
+     * @var Source|Source[]|null
+     */
+    protected $source;
+
+    /**
+     * Code source
+     *
+     * @var string|string[]|null
+     */
+    protected $sourceCode;
+
+    /**
+     * inTable
+     *
+     * @var string
+     */
+    protected $inTable;
+
+    /**
+     * Liste des colonnes ayant changé à filtrer
+     *
+     * @var string|string[]|null
+     */
+    protected $colChanged;
+
+    /**
+     * Liste des colonnes avec des valeurs spéciales à filtrer
+     *
+     * @var array
+     */
+    protected $colValues = [];
+
+    /**
+     * Liste des colonnes ne devant pas être nulles
+     *
+     * @var string[]
+     */
+    protected $notNull = [];
+
+    /**
+     * Limite au nombre d'enregistrements retournés
+     *
+     * @var integer
+     */
+    protected $limit;
+
+    /**
+     * ignoreFields
+     *
+     * @var string[]
+     */
+    protected $ignoreFields;
+
+    /**
+     * defaultSqlCriterion
+     *
+     * @var string
+     */
+    protected $defaultSqlCriterion;
+
+
+
+
+
+    /**
+     * Constructeur
+     *
+     * @param string $tableName
+     */
+    function __construct( $tableName )
+    {
+        $this->setTableName($tableName);
+    }
+
+    /**
+     *
+     * @param QueryGeneratorService $queryGenerator
+     * @return self
+     */
+    public function addDefaultSqlCriterion( QueryGeneratorService $queryGenerator )
+    {
+        if ($this->getTableName()){
+            $this->defaultSqlCriterion = $queryGenerator->getSqlCriterion($this->getTableName());
+        }
+        return $this;
+    }
+
+    /**
+     * Retourne le nom de la table correspondante
+     *
+     * @return string
+     */
+    public function getTableName()
+    {
+        return $this->tableName;
+    }
+
+    /**
+     *
+     * @param string $tableName
+     * @return self
+     */
+    public function setTableName($tableName)
+    {
+        $this->tableName = (string)$tableName;
+        return $this;
+    }
+
+    /**
+     * Retourne le ou les ID scrutés
+     *
+     * @return integer|integer[]|null
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Ajoute un ou plusieurs ID
+     *
+     * @param integer|integer[]|null $id
+     * @return self
+     */
+    public function setId($id)
+    {
+        if (empty($id)){
+            $this->id = null;
+        }elseif( is_array($id)){
+            $this->id = [];
+            foreach( $id as $i ) $this->id[] = (int)$i;
+        }else{
+            $this->id = (int)$id;
+        }
+        return $this;
+    }
+
+    /**
+     *
+     * Retourne la ou les actions scrutées
+     *
+     * @return string|string[]|null
+     */
+    public function getAction()
+    {
+        return $this->action;
+    }
+
+    /**
+     * Ajoute une ou plusieures actions
+     *
+     * @param string|string[]|null $action
+     * @return self
+     */
+    public function setAction($action)
+    {
+        $goodActions = [self::ACTION_DELETE,self::ACTION_INSERT,self::ACTION_UNDELETE,self::ACTION_UPDATE];
+
+        if (empty($action)){
+            $this->action = null;
+        }elseif( is_array($action)){
+            foreach( $action as $a ){
+                if (! in_array($a,$goodActions)){
+                    throw new Exception('Requête erronée : action "'.$a.'" invalide');
+                }
+            }
+            $this->action = $action;
+        }else{
+            if (! in_array($action,$goodActions)){
+                throw new Exception('Requête erronée : action "'.$action.'" invalide');
+            }
+            $this->action = $action;
+        }
+        return $this;
+    }
+
+    /**
+     * Retourne la ou les sources de données
+     *
+     * @return Source|Source[]|null
+     */
+    public function getSource()
+    {
+        return $this->source;
+    }
+
+    /**
+     * Ajoute un ou plusieurs sources de données
+     *
+     * @param Source|Source[]|null $source
+     * @return self
+     */
+    public function setSource($source)
+    {
+        if (empty($source)){
+            $this->source = null;
+        }elseif( is_array($source)){
+            foreach( $source as $s ){
+                if (! $s instanceof Source){
+                    throw new Exception('Requête erronée : classe source "'.get_class($s).'" invalide');
+                }
+                if (! $s->getImportable()){
+                    throw new Exception('Requête erronée : source "'.$s->getLibelle().'" non importable');
+                }
+            }
+            $this->source = $source;
+        }else{
+            if (! $source instanceof Source){
+                throw new Exception('Requête erronée : classe source "'.get_class($source).'" invalide');
+            }
+            if (! $source->getImportable()){
+                throw new Exception('Requête erronée : source "'.$source->getLibelle().'" non importable');
+            }
+            $this->source = $source;
+        }
+        return $this;
+    }
+
+    /**
+     * Ajoute un ou plusieurs enregistrements sources
+     *
+     * @return string|string[]|null
+     */
+    public function getSourceCode()
+    {
+        return $this->sourceCode;
+    }
+
+    /**
+     *
+     * @param string|string[]|null $sourceCode
+     * @return self
+     */
+    public function setSourceCode($sourceCode)
+    {
+        if (empty($sourceCode)){
+            $this->sourceCode = null;
+        }elseif( is_array($sourceCode)){
+            $this->sourceCode = [];
+            foreach( $sourceCode as $sc ) $this->sourceCode[] = (string)$sc;
+        }else{
+            $this->sourceCode = (string)$sourceCode;
+        }
+        return $this;
+    }
+
+    /**
+     * Retourne la table pour laquelle l'enregistrement doit ou peut être présent
+     *
+     * @return string
+     */
+    public function getInTable()
+    {
+        return $this->inTable;
+    }
+
+    /**
+     * Détermine si l'enregistrement doit ou peut être présent dans la table nommée ou non
+     *
+     * @param string $inTable
+     * @return self
+     */
+    public function setInTable($inTable)
+    {
+        $this->inTable = $inTable;
+        return $this;
+    }
+
+
+    /**
+     * Retourne la liste des colonnes scrutées
+     *
+     * @return string|string[]|null
+     */
+    public function getColChanged()
+    {
+        return $this->colChanged;
+    }
+
+    /**
+     * Ajoute une ou plusieurs colonnes
+     *
+     * @param string|string[]|null $colChanged
+     * @return self
+     */
+    public function setColChanged($colChanged)
+    {
+        if (empty($colChanged)){
+            $this->colChanged = null;
+        }elseif( is_array($colChanged)){
+            $this->colChanged = [];
+            foreach( $colChanged as $c ) $this->colChanged[] = (string)$c;
+        }else{
+            $this->colChanged = (string)$colChanged;
+        }
+        return $this;
+    }
+
+    /**
+     * Retourne le liste des valeurs à filtrer, colonne par colonne
+     *
+     * @return array
+     */
+    public function getColValues()
+    {
+        return $this->colValues;
+    }
+
+    /**
+     * Applique une liste de colonnes à scruter en fonction des valeurs transmises
+     *
+     * format du tableau : {Nom de colonne => Valeur(s) à scruter}
+     *
+     * @param array $colValues
+     * @return self
+     */
+    public function setColValues( array $colValues )
+    {
+        $this->colValues = $colValues;
+    }
+
+    /**
+     * Détermine une valeu à scruter pour une colonne donnée
+     *
+     * @param string $column
+     * @param mixed $value
+     */
+    public function addColValue( $column, $value )
+    {
+        $this->colValues[$column] = $value;
+    }
+
+    /**
+     * Retourne la liste des colonnes ne devant pas être nulles
+     *
+     * @return string[]
+     */
+    public function getNotNull()
+    {
+        return $this->notNull;
+    }
+
+    /**
+     * Applique une liste de colonnes ne devant pas être nulles
+     *
+     *
+     * @param string[] $notNull
+     * @return self
+     */
+    public function setNotNull( array $notNull )
+    {
+        $this->notNull = $notNull;
+    }
+
+    /**
+     * Ajoute une colonne ne devant pas être nulle
+     *
+     * @param string $column
+     */
+    public function addNotNull( $column )
+    {
+        $this->notNull[] = $column;
+        return $this;
+    }
+
+    /**
+     *
+     * @return integer
+     */
+    public function getLimit()
+    {
+        return $this->limit;
+    }
+
+    /**
+     *
+     * @param integer $limit
+     * @return self
+     */
+    public function setLimit($limit)
+    {
+        $this->limit = (int)$limit;
+        return $this;
+    }
+
+    /**
+     * Retourne la liste des champs à ignorer pour la MAJ
+     *
+     * @return string[]
+     */
+    public function getIgnoreFields()
+    {
+        return $this->ignoreFields;
+    }
+
+    /**
+     * Modifie la liste des champs à ignorer pour la MAJ
+     *
+     * @param string[] $ignoreFields
+     * @return self
+     */
+    public function setIgnoreFields(array $ignoreFields)
+    {
+        $this->ignoreFields = $ignoreFields;
+        return $this;
+    }
+
+    /**
+     * Ajoute un champ à la liste des champs à ignorer pour la MAJ
+     *
+     * @param string $ignoreField
+     * @return self
+     */
+    public function addIgnoreField($ignoreField)
+    {
+        if (! is_array($this->ignoreFields)) $this->ignoreFields = [];
+        if (! in_array($ignoreField, $this->ignoreFields)){
+            $this->ignoreFields[] = $ignoreField;
+        }
+        return $this;
+    }
+
+    /**
+     * Construit la requête SQL correspondante
+     *
+     * @return string
+     */
+    public function toSql($full=true)
+    {
+        $viewName = AbstractService::escapeKW('V_DIFF_'.$this->tableName);
+
+        $where = [];
+        if (! empty($this->id)){
+            $where[] = $viewName.'.ID'.AbstractService::equals($this->id);
+        }
+
+        if (! empty($this->action)){
+            $w = $viewName.'.IMPORT_ACTION'.AbstractService::equals($this->action);
+            if (! empty($this->inTable)){
+                $w = '('.$w.' OR '.$viewName.'.SOURCE_CODE IN (SELECT SOURCE_CODE FROM '.AbstractService::escapeKW($this->inTable).')'.')';
+            }
+            $where[] = $w;
+        }
+
+        if (! empty($this->source)){
+            if (is_array($this->source)){
+                $values = [];
+                foreach( $this->source as $value ){ $values[] = $value->getId(); }
+                $where[] = $viewName.'.SOURCE_ID'.AbstractService::equals($values);
+            }else{
+                $where[] = $viewName.'.SOURCE_ID'.AbstractService::equals($this->source->getId());
+            }
+        }
+
+        if (! empty($this->sourceCode)){
+            $where[] = $viewName.'.SOURCE_CODE'.AbstractService::equals($this->sourceCode);
+        }
+
+        if (! empty($this->colChanged)){
+            $cols = (array)$this->colChanged;
+            $cond = [];
+            foreach( $cols as $column ){
+                $cond[] = $viewName.'.'.AbstractService::escapeKW ('U_'.$column).' = 1';
+            }
+            $where[] = '('.implode( ' OR ', $cond).')';
+        }
+
+        if (! empty($this->colValues)){
+            foreach( $this->colValues as $column => $value ){
+                $where[] = $viewName.'.'.AbstractService::escapeKW($column).AbstractService::equals($value);
+            }
+        }
+
+        if (! empty($this->notNull)){
+            foreach( $this->notNull as $column ){
+                $where[] = $viewName.'.'.AbstractService::escapeKW($column).' IS NOT NULL';
+            }
+        }
+
+        if ($this->limit !== null){
+            $where[] = 'ROWNUM <= '.$this->limit;
+        }
+
+
+        if ($full){
+            $sql = 'SELECT * FROM '.$viewName.' ';
+        }else{
+            $sql = '';
+        }
+        if (! empty($where)){
+            if (! empty($this->defaultSqlCriterion)){
+                $sql .= $this->defaultSqlCriterion.' AND '.implode( ' AND ', $where );
+            }else{
+                $sql .= 'WHERE '.implode( ' AND ', $where );
+            }
+        }else{
+            if (! empty($this->defaultSqlCriterion)){
+                $sql .= $this->defaultSqlCriterion;
+            }
+        }
+
+        return $sql;
+    }
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Entity/Schema/Column.php b/src/UnicaenImport/Entity/Schema/Column.php
new file mode 100644
index 0000000..7b59c3c
--- /dev/null
+++ b/src/UnicaenImport/Entity/Schema/Column.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace UnicaenImport\Entity\Schema;
+
+
+
+/**
+ * 
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class Column
+{
+        
+    /**
+     * Type de données
+     *
+     * @var string
+     */
+    public $dataType;
+
+    /**
+     * Longueur
+     *
+     * @var integer
+     */
+    public $length;
+
+    /**
+     * Nullable
+     *
+     * @var boolean
+     */
+    public $nullable;
+
+    /**
+     * Si la colonne possède ou non une valeur par défaut
+     *
+     * @var boolean
+     */
+    public $hasDefault;
+
+    /**
+     * Nom de la table référence (si clé étrangère)
+     *
+     * @var string
+     */
+    public $refTableName;
+
+    /**
+     * Nom du champ référence (si clé étrangère)
+     *
+     * @var string
+     */
+    public $refColumnName;
+
+    /**
+     * Si l'import par synchronisation est actif ou non
+     *
+     * @var boolean
+     */
+    public $importActif;
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Exception/Exception.php b/src/UnicaenImport/Exception/Exception.php
new file mode 100644
index 0000000..6fec024
--- /dev/null
+++ b/src/UnicaenImport/Exception/Exception.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace UnicaenImport\Exception;
+
+use RuntimeException;
+
+/**
+ *
+ *
+ * @author Laurent Lécluse <laurent.lecluse at unicaen.fr>
+ */
+class Exception extends RuntimeException {
+
+    /**
+     * @param \Exception $exception
+     * @param string     $tableName
+     *
+     * @return \Doctrine\DBAL\DBALException
+     */
+    public static function duringMajMVException(\Exception $exception, $tableName)
+    {
+        if (! $exception->getPrevious() instanceof \Doctrine\DBAL\Driver\OCI8\OCI8Exception){
+            // Non gérée
+            return $exception;
+        }
+
+        $msg = $exception->getPrevious()->getMessage();
+
+        $msg = "Erreur lors de la mise à jour de la vue métarialisée liée à la table $tableName\n\n$msg";
+
+        return new self($msg, 0, $exception);
+    }
+
+    /**
+     * @param \Exception $exception
+     * @param string     $tableName
+     *
+     * @return \Doctrine\DBAL\DBALException
+     */
+    public static function duringMajException(\Exception $exception, $tableName)
+    {
+        if (! $exception->getPrevious() instanceof \Doctrine\DBAL\Driver\OCI8\OCI8Exception){
+            // Non gérée
+            return $exception;
+        }
+
+        $msg = $exception->getPrevious()->getMessage();
+
+        $msg = "Erreur lors d'une mise à jour de données dans la table $tableName\n\n$msg";
+
+        return new self($msg, 0, $exception);
+    }
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Exception/MissingDependency.php b/src/UnicaenImport/Exception/MissingDependency.php
new file mode 100644
index 0000000..a078c82
--- /dev/null
+++ b/src/UnicaenImport/Exception/MissingDependency.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace UnicaenImport\Exception;
+
+/**
+ *
+ *
+ * @author Laurent Lécluse <laurent.lecluse at unicaen.fr>
+ */
+class MissingDependency extends Exception {
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Options/ModuleOptions.php b/src/UnicaenImport/Options/ModuleOptions.php
new file mode 100644
index 0000000..cb50bed
--- /dev/null
+++ b/src/UnicaenImport/Options/ModuleOptions.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace UnicaenImport\Options;
+
+use Zend\Stdlib\AbstractOptions;
+use Zend\Stdlib\ArrayUtils;
+
+class ModuleOptions extends AbstractOptions
+{
+    /**
+     * Turn off strict options mode
+     */
+    protected $__strictMode__ = false;
+
+    /**
+     * @var string
+     */
+    protected $package = 'UNICAEN_IMPORT';
+
+    /**
+     * @var array
+     */
+    protected $differentielViewHelpers = [];
+
+
+
+    /**
+     * @return string
+     */
+    public function getPackage()
+    {
+        return $this->package;
+    }
+
+
+
+    /**
+     * @param string $package
+     *
+     * @return ModuleOptions
+     */
+    public function setPackage($package)
+    {
+        $this->package = $package;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return array
+     */
+    public function getDifferentielViewHelpers()
+    {
+        return $this->differentielViewHelpers;
+    }
+
+
+
+    /**
+     * @param array $differentielViewHelpers
+     *
+     * @return ModuleOptions
+     */
+    public function setDifferentielViewHelpers($differentielViewHelpers)
+    {
+        $this->differentielViewHelpers = $differentielViewHelpers;
+
+        return $this;
+    }
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Options/ModuleOptionsFactory.php b/src/UnicaenImport/Options/ModuleOptionsFactory.php
new file mode 100644
index 0000000..72bce9e
--- /dev/null
+++ b/src/UnicaenImport/Options/ModuleOptionsFactory.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace UnicaenImport\Options;
+
+use Zend\ServiceManager\FactoryInterface;
+use Zend\ServiceManager\ServiceLocatorInterface;
+
+/**
+ * Description of ModuleOptionsFactory
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class ModuleOptionsFactory implements FactoryInterface
+{
+    /**
+     * Create service
+     *
+     * @param ServiceLocatorInterface $serviceLocator
+     * @return mixed
+     */
+    public function createService(ServiceLocatorInterface $serviceLocator)
+    {
+        $config = $serviceLocator->get('Configuration');
+
+        return new ModuleOptions(isset($config['unicaen-import']) ? $config['unicaen-import'] : []);
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Options/Traits/ModuleOptionsAwareTrait.php b/src/UnicaenImport/Options/Traits/ModuleOptionsAwareTrait.php
new file mode 100644
index 0000000..72a6c3b
--- /dev/null
+++ b/src/UnicaenImport/Options/Traits/ModuleOptionsAwareTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace UnicaenImport\Options\Traits;
+
+use UnicaenImport\Options\ModuleOptions;
+use RuntimeException;
+
+/**
+ * Description of ModuleOptionsAwareTrait
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+trait ModuleOptionsAwareTrait
+{
+    /**
+     * @var ModuleOptions
+     */
+    private $optionsModule;
+
+
+
+    /**
+     * @param ModuleOptions $optionsModule
+     *
+     * @return self
+     */
+    public function setOptionsModule(ModuleOptions $optionsModule)
+    {
+        $this->optionsModule = $optionsModule;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return ModuleOptions
+     * @throws RuntimeException
+     */
+    public function getOptionsModule()
+    {
+        if (empty($this->optionsModule)) {
+            if (!method_exists($this, 'getServiceLocator')) {
+                throw new RuntimeException('La classe ' . get_class($this) . ' n\'a pas accès au ServiceLocator.');
+            }
+
+            $serviceLocator = $this->getServiceLocator();
+            if (method_exists($serviceLocator, 'getServiceLocator')) {
+                $serviceLocator = $serviceLocator->getServiceLocator();
+            }
+            $this->optionsModule = $serviceLocator->get('UnicaenImport\Options\Module');
+        }
+
+        return $this->optionsModule;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Processus/ImportProcessus.php b/src/UnicaenImport/Processus/ImportProcessus.php
new file mode 100644
index 0000000..15f70c7
--- /dev/null
+++ b/src/UnicaenImport/Processus/ImportProcessus.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace UnicaenImport\Processus;
+
+use UnicaenImport\Entity\Differentiel\Query;
+use UnicaenImport\Service\Traits\QueryGeneratorServiceAwareTrait;
+use Zend\ServiceManager\ServiceLocatorAwareInterface;
+use Zend\ServiceManager\ServiceLocatorAwareTrait;
+
+
+/**
+ *
+ *
+ * @author Laurent Lécluse <laurent.lecluse at unicaen.fr>
+ */
+class ImportProcessus implements ServiceLocatorAwareInterface
+{
+    use ServiceLocatorAwareTrait;
+    use QueryGeneratorServiceAwareTrait;
+
+
+    /**
+     * Mise à jour de l'existant uniquement
+     */
+    const A_UPDATE = 'update';
+
+    /**
+     * Insertion de nouvelles données ou restauration d'anciennes uniquement
+     */
+    const A_INSERT = 'insert';
+
+    /**
+     * Mise à jour globale
+     */
+    const A_ALL = 'all';
+
+
+
+    /**
+     * Mise à jour des vues différentielles et des paquetages de mise à jour des données
+     *
+     * @return self
+     */
+    public function updateViewsAndPackages()
+    {
+        $this->getServiceQueryGenerator()->updateViewsAndPackages();
+        return $this;
+    }
+
+
+
+    /**
+     * Construit et exécute la reqûete d'interrogation des vues différentielles
+     *
+     * @param string            $tableName   Nom de la table
+     * @param string            $name        Nom du champ à tester
+     * @param string|null       $value       Valeur de test du champ
+     * @param string            $action      Action
+     * @retun self
+     */
+    public function execMaj( $tableName, $name, $value=null, $action=self::A_ALL )
+    {
+        if ('SOURCE_CODE' == $name && $value !== null){
+            $value = (string)$value;
+        }
+        $query = new Query($tableName);
+        if (null !== $value) $query->addColValue($name, $value);
+        switch( $action ){
+            case 'insert':
+                $query->setAction ([Query::ACTION_INSERT,Query::ACTION_UNDELETE]);
+            break;
+            case 'update':
+                $query->setAction ([Query::ACTION_UPDATE,Query::ACTION_DELETE]);
+            break;
+        }
+        $this->getServiceQueryGenerator()->execMaj($query);
+        return $this;
+    }
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Processus/Traits/ImportProcessusAwareTrait.php b/src/UnicaenImport/Processus/Traits/ImportProcessusAwareTrait.php
new file mode 100644
index 0000000..57aaa07
--- /dev/null
+++ b/src/UnicaenImport/Processus/Traits/ImportProcessusAwareTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace UnicaenImport\Processus\Traits;
+
+use UnicaenImport\Processus\ImportProcessus;
+use RuntimeException;
+
+/**
+ * Description of ImportProcessusAwareTrait
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+trait ImportProcessusAwareTrait
+{
+    /**
+     * @var ImportProcessus
+     */
+    private $processusImport;
+
+
+
+    /**
+     * @param ImportProcessus $processusImport
+     *
+     * @return self
+     */
+    public function setProcessusImport(ImportProcessus $processusImport)
+    {
+        $this->processusImport = $processusImport;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return ImportProcessus
+     * @throws RuntimeException
+     */
+    public function getProcessusImport()
+    {
+        if (empty($this->processusImport)) {
+            if (!method_exists($this, 'getServiceLocator')) {
+                throw new RuntimeException('La classe ' . get_class($this) . ' n\'a pas accès au ServiceLocator.');
+            }
+
+            $serviceLocator = $this->getServiceLocator();
+            if (method_exists($serviceLocator, 'getServiceLocator')) {
+                $serviceLocator = $serviceLocator->getServiceLocator();
+            }
+            $this->processusImport = $serviceLocator->get('UnicaenImport\Processus\Import');
+        }
+
+        return $this->processusImport;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Provider/Privilege/Privileges.php b/src/UnicaenImport/Provider/Privilege/Privileges.php
new file mode 100644
index 0000000..15d943b
--- /dev/null
+++ b/src/UnicaenImport/Provider/Privilege/Privileges.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace UnicaenImport\Provider\Privilege;
+
+/**
+ * Description of Privileges
+ *
+ * Liste des privilèges utilisables dans votre application
+ *
+ * @author UnicaenCode
+ */
+class Privileges extends \UnicaenAuth\Provider\Privilege\Privileges
+{
+
+    const IMPORT_ECARTS                             = 'import-ecarts';
+    const IMPORT_MAJ                                = 'import-maj';
+    const IMPORT_TBL                                = 'import-tbl';
+    const IMPORT_VUES_PROCEDURES                    = 'import-vues-procedures';
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/AbstractService.php b/src/UnicaenImport/Service/AbstractService.php
new file mode 100644
index 0000000..50c13ac
--- /dev/null
+++ b/src/UnicaenImport/Service/AbstractService.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace UnicaenImport\Service;
+
+use Doctrine\ORM\EntityManager;
+use UnicaenApp\Service\EntityManagerAwareInterface;
+use UnicaenApp\Service\EntityManagerAwareTrait;
+use Zend\ServiceManager\ServiceLocatorAwareInterface;
+use Zend\ServiceManager\ServiceLocatorAwareTrait;
+use Zend\ServiceManager\ServiceManager;
+use Zend\ServiceManager\ServiceManagerAwareInterface;
+use UnicaenImport\Exception\Exception;
+use ZfcUser\Entity\UserInterface;
+use UnicaenAuth\Service\DbUserAwareInterface;
+use Application\Entity\Db\Utilisateur;
+
+/**
+ * Classe mère des services
+ *
+ * @author Laurent Lécluse <laurent.lecluse at unicaen.fr>
+ */
+class AbstractService implements ServiceLocatorAwareInterface, EntityManagerAwareInterface, DbUserAwareInterface
+{
+    use ServiceLocatorAwareTrait;
+    use EntityManagerAwareTrait;
+
+    /**
+     * utilisateur courant
+     *
+     * @var UserInterface
+     */
+    protected $currentUser;
+
+
+
+
+    /**
+     * Echappe une chaîne de caractères pour convertir en SQL
+     *
+     * @param string $string
+     *
+     * @return string
+     */
+    public static function escapeKW($string)
+    {
+        return '"' . str_replace('"', '""', strtoupper($string)) . '"';
+    }
+
+
+
+    /**
+     * Echappe une valeur pour convertir en SQL
+     *
+     * @param mixed $value
+     *
+     * @return string
+     */
+    public static function escape($value)
+    {
+        if (null === $value) return 'NULL';
+        switch (gettype($value)) {
+            case 'string':
+                return "'" . str_replace("'", "''", $value) . "'";
+            case 'integer':
+                return (string)$value;
+            case 'boolean':
+                return $value ? '1' : '0';
+            case 'double':
+                return (string)$value;
+            case 'array':
+                return '(' . implode(',', array_map('Import\Service\Service::escape', $value)) . ')';
+        }
+        throw new Exception('La valeur transmise ne peut pas être convertie en SQL');
+    }
+
+
+
+    /**
+     * Retourne le code SQL correspondant à la valeur transmise, précédé de "=", "IS" ou "IN" suivant le contexte.
+     *
+     * @param mixed $value
+     *
+     * @return string
+     */
+    public static function equals($value)
+    {
+        if (null === $value) {
+            $eq = ' IS ';
+        } elseif (is_array($value)) $eq = ' IN ';
+        else                        $eq = ' = ';
+
+        return $eq . self::escape($value);
+    }
+
+
+
+    /**
+     * Retourne une tableau des résultats de la requête transmise.
+     *
+     *
+     * @param string $sql
+     * @param array  $params
+     * @param string $colRes
+     *
+     * @return array
+     */
+    protected function query($sql, $params = null, $colRes = null)
+    {
+        $stmt   = $this->getEntityManager()->getConnection()->executeQuery($sql, $params);
+        $result = [];
+        while ($r = $stmt->fetch()) {
+            if (empty($colRes)) $result[] = $r; else $result[] = $r[$colRes];
+        }
+
+        return $result;
+    }
+
+
+
+    /**
+     * exécute un ordre SQL
+     *
+     * @param string $sql
+     *
+     * @return integer
+     */
+    protected function exec($sql)
+    {
+        return $this->getEntityManager()->getConnection()->exec($sql);
+    }
+
+
+
+    /**
+     *
+     * @return UserInterface
+     */
+    public function getDbUser()
+    {
+        if (null === $this->currentUser) {
+            $this->currentUser = $this->getAppDbUser();
+        }
+
+        return $this->currentUser;
+    }
+
+
+
+    /**
+     *  Set Current User
+     *
+     * @param UserInterface $currentUser
+     */
+    public function setDbUser(UserInterface $currentUser)
+    {
+        $this->currentUser = $currentUser;
+    }
+
+
+
+    /**
+     *
+     * @return UserInterface
+     */
+    public function getAppDbUser()
+    {
+        return $this->getEntityManager()->find(\Application\Entity\Db\Utilisateur::class, Utilisateur::APP_UTILISATEUR_ID);
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/DifferentielService.php b/src/UnicaenImport/Service/DifferentielService.php
new file mode 100644
index 0000000..fb5d735
--- /dev/null
+++ b/src/UnicaenImport/Service/DifferentielService.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace UnicaenImport\Service;
+
+use Doctrine\DBAL\Driver\Statement;
+use UnicaenImport\Entity\Differentiel\Ligne;
+use UnicaenImport\Entity\Differentiel\Query;
+use UnicaenImport\Service\Traits\QueryGeneratorServiceAwareTrait;
+
+
+/**
+ * Classe permettant de récupérer le différentiel entre une table source et une table OSE
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class DifferentielService extends AbstractService
+{
+    use QueryGeneratorServiceAwareTrait;
+
+    /**
+     * Statement
+     *
+     * @var Statement
+     */
+    protected $stmt;
+
+    /**
+     * Nom de la table courante
+     *
+     * @var string
+     */
+    protected $tableName;
+
+
+
+    /**
+     * Construit un différentiel
+     *
+     * @param string $query Requête de filtrage
+     *
+     * @return self
+     */
+    public function make(Query $query)
+    {
+        $this->tableName = $query->getTableName();
+        $query->addDefaultSqlCriterion($this->getServiceQueryGenerator());
+        $this->stmt = $this->getEntityManager()->getConnection()->executeQuery($query->toSql(), []);
+
+        return $this;
+    }
+
+
+
+    /**
+     * Récupère la prochaine ligne de différentiel
+     *
+     * @return Ligne|false
+     */
+    public function fetchNext()
+    {
+        $data = $this->stmt->fetch();
+        if ($data) return new Ligne($this->getEntityManager(), $this->tableName, $data);
+
+        return false;
+    }
+
+
+
+    /**
+     * Retourne toutes les lignes concernées
+     *
+     * @return Ligne[]
+     */
+    public function fetchAll()
+    {
+        $result = [];
+        while ($data = $this->stmt->fetch()) {
+            if ($data) $result[] = new Ligne($this->getEntityManager(), $this->tableName, $data);
+        }
+
+        return $result;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/QueryGeneratorService.php b/src/UnicaenImport/Service/QueryGeneratorService.php
new file mode 100644
index 0000000..f7f9c05
--- /dev/null
+++ b/src/UnicaenImport/Service/QueryGeneratorService.php
@@ -0,0 +1,596 @@
+<?php
+namespace UnicaenImport\Service;
+
+use UnicaenImport\Exception\Exception;
+use UnicaenImport\Entity\Differentiel\Query;
+use UnicaenImport\Options\Traits\ModuleOptionsAwareTrait;
+use UnicaenImport\Service\Traits\SchemaServiceAwareTrait;
+
+/**
+ *
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class QueryGeneratorService extends AbstractService
+{
+    use SchemaServiceAwareTrait;
+    use ModuleOptionsAwareTrait;
+
+    const AG_BEGIN          = '-- AUTOMATIC GENERATION --';
+    const AG_END            = '-- END OF AUTOMATIC GENERATION --';
+    const ANNEE_COLUMN_NAME = 'ANNEE_ID';
+
+    /**
+     * Tables
+     *
+     * @var string[]
+     */
+    protected $tables;
+
+    /**
+     * Colonnes
+     *
+     * @var array
+     */
+    protected $cols = [];
+
+
+
+    /**
+     * Retourne la liste des tables importables
+     *
+     * @return string[]
+     */
+    protected function getTables()
+    {
+        if (empty($this->tables)) {
+            $this->tables = $this->getServiceSchema()->getImportTables();
+        }
+
+        return $this->tables;
+    }
+
+
+
+    /**
+     * Retourne la liste des colonnes importables d'une table
+     *
+     * @param string $tableName
+     *
+     * @return string[]
+     */
+    protected function getCols($tableName)
+    {
+        if (!isset($this->cols[$tableName])) {
+            $this->cols[$tableName] = $this->getServiceSchema()->getImportCols($tableName);
+        }
+
+        return $this->cols[$tableName];
+    }
+
+
+
+    public function execMajVM($tableName)
+    {
+        $mviewName = $this->escape('MV_' . $tableName);
+        $sql       = "BEGIN DBMS_MVIEW.REFRESH($mviewName, 'C'); END;";
+        try {
+            $this->getEntityManager()->getConnection()->exec($sql);
+        } catch (\Doctrine\DBAL\DBALException $e) {
+            throw Exception::duringMajMVException($e, $tableName);
+        }
+    }
+
+
+
+    /**
+     * Met à jour des données d'après la requête transmise
+     *
+     * @param Query $query Requête de filtrage pour la mise à jour
+     *
+     * @retun self
+     */
+    public function execMaj(Query $query)
+    {
+        $currentUser = $this->getDbUser();
+        if (empty($currentUser)) {
+            throw new Exception('Vous devez être authentifié pour réaliser cette action');
+        }
+        $userId     = $this->escape($currentUser->getId());
+        $procName   = $this->escapeKW('MAJ_' . $query->getTableName());
+        $conditions = $query->toSql(false);
+        if (!empty($conditions)) {
+            $conditions = $this->escape($conditions);
+        } else {
+            $conditions = 'NULL';
+        }
+        $ignoreFields = $query->getIgnoreFields();
+        if (empty($ignoreFields)) {
+            $ignoreFields = 'NULL';
+        } else {
+            $ignoreFields = $this->escape(implode(',', $ignoreFields));
+        }
+
+        $sql = "BEGIN ".$this->getPackage().".SET_CURRENT_USER($userId);".$this->getPackage().".$procName($conditions,$ignoreFields); END;";
+        try {
+            $this->getEntityManager()->getConnection()->exec($sql);
+        } catch (\Doctrine\DBAL\DBALException $e) {
+            throw Exception::duringMajException($e, $query->getTableName());
+        }
+
+        return $this;
+    }
+
+
+
+    /**
+     * Synchronise une table
+     *
+     * @param string $tableName
+     *
+     * @return string[]
+     */
+    public function syncTable($tableName)
+    {
+        $currentUser = $this->getDbUser();
+        if (empty($currentUser)) {
+            throw new Exception('Vous devez être authentifié pour réaliser cette action');
+        }
+        $userId = $this->escape($currentUser->getId());
+
+        $errors    = [];
+        $lastLogId = $this->getLastLogId();
+        $sql       = "BEGIN ".$this->getPackage().".SET_CURRENT_USER($userId);".$this->getPackage()."." . $this->escapeKW('MAJ_' . $tableName) . "; END;";
+        try {
+            $this->getEntityManager()->getConnection()->exec($sql);
+        } catch (\Doctrine\DBAL\DBALException $e) {
+            $errors[] = Exception::duringMajException($e, $tableName)->getMessage();
+        }
+        $errors = $errors + $this->getLogMessages($lastLogId);
+
+        return $errors;
+    }
+
+
+
+    /**
+     * retourne le dernier ID du log de synchronisation
+     *
+     * @return int
+     */
+    protected function getLastLogId()
+    {
+        $sql  = "SELECT MAX(id) last_log_id FROM SYNC_LOG";
+        $stmt = $this->getEntityManager()->getConnection()->executeQuery($sql);
+        if ($r = $stmt->fetch()) {
+            return (int)$r['LAST_LOG_ID'];
+        }
+
+        return 0;
+    }
+
+
+
+    /**
+     * Retourne tous les messages d'erreur qui sont apparue depuis $since
+     *
+     * @param int $since
+     *
+     * @return string[]
+     */
+    protected function getLogMessages($since)
+    {
+        $since    = (int)$since;
+        $sql      = "SELECT message FROM sync_log WHERE id > :since ORDER BY id";
+        $messages = [];
+        $stmt     = $this->getEntityManager()->getConnection()->executeQuery($sql, ['since' => (int)$since]);
+        while ($r = $stmt->fetch()) {
+            $messages[] = $r['MESSAGE'];
+        }
+
+        return $messages;
+    }
+
+
+
+    /**
+     *
+     * @param string $tableName
+     *
+     * @return null|string
+     */
+    public function getSqlCriterion($tableName)
+    {
+        $sql  = 'SELECT '.$this->getPackage().'.GET_SQL_CRITERION(' . $this->escape($tableName) . ',\'\') res FROM DUAL';
+        $stmt = $this->getEntityManager()->getConnection()->executeQuery($sql);
+
+        if ($r = $stmt->fetch()) {
+            $res = $r['RES'];
+            if ($res) return $res; else return null;
+        }
+
+        return null;
+    }
+
+
+
+    /**
+     * Retourne les identifiants des données concernés
+     *
+     * @param string               $tableName
+     * @param string|string[]|null $sourceCode
+     * @param integer|null         $anneeId
+     *
+     * @return integer[]|null
+     */
+    public function getIdFromSourceCode($tableName, $sourceCode, $anneeId = null)
+    {
+        if (empty($sourceCode)) return null;
+
+        $sql = 'SELECT ID FROM ' . $this->escapeKW($tableName) . ' WHERE SOURCE_CODE IN (:sourceCode)';
+        if ($anneeId) {
+            $sql .= ' AND ANNEE_ID = ' . (string)(int)$anneeId;
+        }
+        $stmt = $this->getEntityManager()->getConnection()->executeQuery(
+            $sql,
+            ['sourceCode' => (array)$sourceCode],
+            ['sourceCode' => \Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
+        );
+        if ($r = $stmt->fetch()) {
+            ;
+
+            return (int)$r['ID'];
+        } else {
+            return null;
+        }
+    }
+
+
+
+    /**
+     * Mettre à jour toutes les infos dans la BDD
+     *
+     * @return self
+     */
+    public function updateViewsAndPackages()
+    {
+        $views = $this->makeDiffViews();
+
+        foreach ($views as $vn => $view) {
+            $this->exec($view);
+        }
+
+        $declaration = $this->makePackageDeclaration();
+        $this->exec($declaration);
+
+        $body = $this->makePackageBody();
+        $this->exec($body);
+
+        return $this;
+    }
+
+
+
+    protected function getPackage()
+    {
+        return $this->getOptionsModule()->getPackage();
+    }
+
+
+
+    /**
+     * Retourne le code source du package d'import
+     *
+     * @return string
+     */
+    protected function getPackageDeclaration()
+    {
+        $sql    = "SELECT TEXT FROM USER_SOURCE WHERE NAME = '".$this->getPackage()."' AND type = 'PACKAGE'";
+        $result = $this->query($sql, [], 'TEXT');
+
+        return implode("", $result);
+    }
+
+
+
+    /**
+     * Retourne le code source du package d'import
+     *
+     * @return string
+     */
+    protected function getPackageBody()
+    {
+        $sql    = "SELECT TEXT FROM USER_SOURCE WHERE NAME = '".$this->getPackage()."' AND type = 'PACKAGE BODY'";
+        $result = $this->query($sql, [], 'TEXT');
+
+        return implode("", $result);
+    }
+
+
+
+    /**
+     * Construit toutes les vues différentielles
+     *
+     * @return array
+     */
+    protected function makeDiffViews()
+    {
+        $tables = $this->getTables();
+        $result = [];
+        foreach ($tables as $table) {
+            $result[$table] = $this->makeDiffView($table);
+        }
+
+        return $result;
+    }
+
+
+
+    /**
+     * Construit toutes les déclarations de procédures
+     *
+     * @return array
+     */
+    protected function makeProcDeclarations()
+    {
+        $tables = $this->getTables();
+        $result = [];
+        foreach ($tables as $table) {
+            $result[$table] = $this->makeProcDeclaration($table);
+        }
+
+        return $result;
+    }
+
+
+
+    /**
+     * Construit tous les corps de procédures
+     *
+     * @return array
+     */
+    protected function makeProcBodies()
+    {
+        $tables = $this->getTables();
+        $result = [];
+        foreach ($tables as $table) {
+            $result[$table] = $this->makeProcBody($table);
+        }
+
+        return $result;
+    }
+
+
+
+    /**
+     * Constuit la nouvelle déclaration du package IMPORT
+     *
+     * @return string
+     */
+    protected function makePackageDeclaration()
+    {
+        $src  = $this->getPackageDeclaration();
+        $decl = implode("\n", $this->makeProcDeclarations());
+
+        return $this->updatePackageContent($src, $decl);
+    }
+
+
+
+    /**
+     * Constuit la nouvelle déclaration du package IMPORT
+     *
+     * @return string
+     */
+    protected function makePackageBody()
+    {
+        $src  = $this->getPackageBody();
+        $decl = implode("\n\n\n\n", $this->makeProcBodies());
+
+        return $this->updatePackageContent($src, $decl);
+    }
+
+
+
+    /**
+     * Mise à jour du contenu d'un package (déclaration ou corps)
+     *
+     * @param string $packageSource
+     * @param string $newContent
+     *
+     * @return string
+     */
+    protected function updatePackageContent($packageSource, $newContent)
+    {
+        $src = $packageSource;
+        if (null === $begin = strpos($packageSource, self::AG_BEGIN)) {
+            throw new Exception('Le tag indiquant le début de la zone automatique du package n\'a pas été trouvée');
+        }
+
+        if (null === $end = strpos($packageSource, self::AG_END)) {
+            throw new Exception('Le tag indiquant la fin de la zone automatique du package n\'a pas été trouvée');
+        }
+
+        $src = 'CREATE OR REPLACE '
+            . substr($packageSource, 0, $begin + strlen(self::AG_BEGIN))
+            . "\n\n" . $newContent . "\n\n  "
+            . substr($packageSource, $end);
+
+        return $src;
+    }
+
+
+
+    /**
+     * Génère une vue différentielle pour une table donnée
+     *
+     * @param string $tableName
+     *
+     * @return string
+     */
+    protected function makeDiffView($tableName)
+    {
+        // Pour l'annualisation :
+        $schema   = $this->getServiceSchema()->getSchema($tableName);
+        $joinCond = '';
+        $delCond  = '';
+        $depJoin  = '';
+        if (array_key_exists(self::ANNEE_COLUMN_NAME, $schema)) {
+            // Si la table courante est annualisée ...
+            if ($this->getServiceSchema()->hasColumn('V_DIFF_' . $tableName, self::ANNEE_COLUMN_NAME)) {
+                // ... et que la source est également annualisée alors concordance nécessaire
+                $joinCond = ' AND S.' . self::ANNEE_COLUMN_NAME . ' = d.' . self::ANNEE_COLUMN_NAME;
+            }
+            // destruction ssi dans l'année d'import courante
+            $delCond = ' AND d.' . self::ANNEE_COLUMN_NAME . ' = '.$this->getPackage().'.get_current_annee';
+        } else {
+            // on recherche si la table dépend d'une table qui, elle, serait annualisée
+            foreach ($schema as $columnName => $column) {
+                /* @var $column \Import\Entity\Schema\Column */
+                if (!empty($column->refTableName)) {
+                    $refSchema = $this->getServiceSchema()->getSchema($column->refTableName);
+                    if (!empty($refSchema) && array_key_exists(self::ANNEE_COLUMN_NAME, $refSchema)) {
+                        // Oui, la table dépend d'une table annualisée!!
+                        // Donc, on utilise la table référente
+                        $depJoin = "\n  LEFT JOIN " . $column->refTableName . " rt ON rt." . $column->refColumnName . " = d." . $columnName;
+                        // destruction ssi dans l'année d'import courante de la table référente
+                        $delCond = ' AND rt.' . self::ANNEE_COLUMN_NAME . ' = '.$this->getPackage().'.get_current_annee';
+
+                        break;
+                        /* on stoppe à la première table contenant une année.
+                         * S'il en existe une autre tant pis pour elle,
+                         * les années doivent de toute manière être concordantes entres sources!!!
+                         */
+                    }
+                }
+            }
+        }
+
+        // on génère ensuite la bonne requête !!!
+        $cols = $this->getCols($tableName);
+        $sql  = "CREATE OR REPLACE FORCE VIEW OSE.V_DIFF_$tableName AS
+select diff.* from (SELECT
+  COALESCE( D.id, S.id ) id,
+  COALESCE( S.source_id, D.source_id ) source_id,
+  COALESCE( S.source_code, D.source_code ) source_code,
+CASE
+    WHEN S.source_code IS NOT NULL AND D.source_code IS NULL THEN 'insert'
+    WHEN S.source_code IS NOT NULL AND D.source_code IS NOT NULL AND (D.histo_destruction IS NULL OR D.histo_destruction > SYSDATE) THEN 'update'
+    WHEN S.source_code IS NULL AND D.source_code IS NOT NULL AND (D.histo_destruction IS NULL OR D.histo_destruction > SYSDATE)$delCond THEN 'delete'
+    WHEN S.source_code IS NOT NULL AND D.source_code IS NOT NULL AND D.histo_destruction IS NOT NULL AND D.histo_destruction <= SYSDATE THEN 'undelete' END import_action,
+  " . $this->formatColQuery($cols, '  CASE WHEN S.source_code IS NULL AND D.source_code IS NOT NULL THEN D.:column ELSE S.:column END :column', ",\n  ") . ",
+  " . $this->formatColQuery($cols, '  CASE WHEN D.:column <> S.:column OR (D.:column IS NULL AND S.:column IS NOT NULL) OR (D.:column IS NOT NULL AND S.:column IS NULL) THEN 1 ELSE 0 END U_:column', ",\n  ") . "
+FROM
+  $tableName D$depJoin
+  FULL JOIN SRC_$tableName S ON S.source_id = D.source_id AND S.source_code = D.source_code$joinCond
+WHERE
+       (S.source_code IS NOT NULL AND D.source_code IS NOT NULL AND D.histo_destruction IS NOT NULL AND D.histo_destruction <= SYSDATE)
+    OR (S.source_code IS NULL AND D.source_code IS NOT NULL AND (D.histo_destruction IS NULL OR D.histo_destruction > SYSDATE))
+    OR (S.source_code IS NOT NULL AND D.source_code IS NULL)
+    OR " . $this->formatColQuery($cols, 'D.:column <> S.:column OR (D.:column IS NULL AND S.:column IS NOT NULL) OR (D.:column IS NOT NULL AND S.:column IS NULL)', "\n  OR ") . "
+) diff JOIN source on source.id = diff.source_id WHERE import_action IS NOT NULL AND source.importable = 1";
+
+        return $sql;
+    }
+
+
+
+    /**
+     * Génère une déclaration de procédure pour une table donnée
+     *
+     * @param string $tableName
+     *
+     * @return string
+     */
+    protected function makeProcDeclaration($tableName)
+    {
+        return "  PROCEDURE MAJ_$tableName(SQL_CRITERION CLOB DEFAULT '', IGNORE_UPD_COLS CLOB DEFAULT '');";
+    }
+
+
+
+    /**
+     * Génère un corps de procédure pour une table donnée
+     *
+     * @param string $tableName
+     *
+     * @return string
+     */
+    protected function makeProcBody($tableName)
+    {
+        $cols = $this->getCols($tableName);
+
+        $sql = "  PROCEDURE MAJ_$tableName(SQL_CRITERION CLOB DEFAULT '', IGNORE_UPD_COLS CLOB DEFAULT '') IS
+    TYPE r_cursor IS REF CURSOR;
+    sql_query CLOB;
+    diff_cur r_cursor;
+    diff_row V_DIFF_$tableName%ROWTYPE;
+  BEGIN
+    sql_query := 'SELECT V_DIFF_$tableName.* FROM V_DIFF_$tableName ' || get_sql_criterion('$tableName',SQL_CRITERION);
+    OPEN diff_cur FOR sql_query;
+    LOOP
+      FETCH diff_cur INTO diff_row; EXIT WHEN diff_cur%NOTFOUND;
+      BEGIN
+
+        CASE diff_row.import_action
+          WHEN 'insert' THEN
+            INSERT INTO OSE.$tableName
+              ( id, " . $this->formatColQuery($cols) . ", source_id, source_code, histo_createur_id, histo_modificateur_id )
+            VALUES
+              ( COALESCE(diff_row.id,$tableName" . "_ID_SEQ.NEXTVAL), " . $this->formatColQuery($cols, 'diff_row.:column') . ", diff_row.source_id, diff_row.source_code, get_current_user, get_current_user );
+
+          WHEN 'update' THEN
+            " . $this->formatColQuery(
+                $cols,
+                "IF (diff_row.u_:column = 1 AND IN_COLUMN_LIST(':column',IGNORE_UPD_COLS) = 0) THEN UPDATE OSE.$tableName SET :column = diff_row.:column WHERE ID = diff_row.id; END IF;"
+                , "\n          "
+            ) . "
+
+          WHEN 'delete' THEN
+            UPDATE OSE.$tableName SET histo_destruction = SYSDATE, histo_destructeur_id = get_current_user WHERE ID = diff_row.id;
+
+          WHEN 'undelete' THEN
+            " . $this->formatColQuery(
+                $cols,
+                "IF (diff_row.u_:column = 1 AND IN_COLUMN_LIST(':column',IGNORE_UPD_COLS) = 0) THEN UPDATE OSE.$tableName SET :column = diff_row.:column WHERE ID = diff_row.id; END IF;"
+                , "\n          "
+            ) . "
+            UPDATE OSE.$tableName SET histo_destruction = NULL, histo_destructeur_id = NULL WHERE ID = diff_row.id;
+
+        END CASE;
+
+      EXCEPTION WHEN OTHERS THEN
+        ".$this->getPackage().".SYNC_LOG( SQLERRM, '$tableName', diff_row.source_code );
+      END;
+    END LOOP;
+    CLOSE diff_cur;
+
+  END MAJ_$tableName;";
+
+        return $sql;
+    }
+
+
+
+    /**
+     * Retourne une chaîne SQL correspondant, pour chaque colonne donnée, au résultat du formatage donné,
+     * concaténé selon le séparateur transmis.
+     *
+     * L'opérateur $c permet de situer l'endroit où devont être placées les colonnes.
+     *
+     * @param array  $cols
+     * @param string $format
+     * @param string $separator
+     *
+     * @return string
+     */
+    protected function formatColQuery(array $cols, $format = ':column', $separator = ',')
+    {
+        $res = [];
+        foreach ($cols as $col) {
+            $res[] = str_replace(':column', $col, $format);
+        }
+
+        return implode($separator, $res);
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/SchemaService.php b/src/UnicaenImport/Service/SchemaService.php
new file mode 100644
index 0000000..0497c4f
--- /dev/null
+++ b/src/UnicaenImport/Service/SchemaService.php
@@ -0,0 +1,146 @@
+<?php
+namespace UnicaenImport\Service;
+
+use UnicaenImport\Exception\Exception;
+use UnicaenImport\Entity\Schema\Column;
+
+/**
+ *
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class SchemaService extends AbstractService
+{
+    /**
+     * Schéma
+     *
+     * @var array
+     */
+    protected $schema;
+
+
+
+
+
+    /**
+     * Retourne le schéma de la BDD
+     *
+     * @return array
+     */
+    public function getSchema( $tableName=null )
+    {
+        if (empty($this->schema)){
+            $this->schema = $this->makeSchema();
+        }
+        if (empty($tableName)){
+            return $this->schema;
+        }elseif(array_key_exists($tableName, $this->schema)){
+            return $this->schema[$tableName];
+        }else{
+            return null;
+        }
+    }
+
+
+
+    /**
+     * @return Column[][]
+     */
+    public function makeSchema()
+    {
+        $sql = 'SELECT * FROM V_IMPORT_TAB_COLS';
+        $d = $this->query( $sql, [] );
+
+        $sc = [];
+        foreach( $d as $col ){
+            $column = new Column;
+            $column->dataType        = $col['DATA_TYPE'];
+            $column->length          = (null === $col['LENGTH']) ? null : (integer)$col['LENGTH'];
+            $column->nullable        = $col['NULLABLE'] == '1';
+            $column->hasDefault      = $col['HAS_DEFAULT'] == '1';
+            $column->refTableName    = $col['C_TABLE_NAME'];
+            $column->refColumnName   = $col['C_COLUMN_NAME'];
+            $column->importActif     = $col['IMPORT_ACTIF'] == '1';
+            $sc[$col['TABLE_NAME']][$col['COLUMN_NAME']] = $column;
+        }
+        return $sc;
+    }
+
+
+
+    /**
+     * retourne la liste des tables supportées par l'import automatique
+     *
+     * @return array
+     */
+    public function getImportTables()
+    {
+        $sql = "SELECT SUBSTR(name,5) as TABLE_NAME FROM (
+            SELECT mview_name AS name FROM USER_MVIEWS
+            UNION SELECT view_name AS name FROM USER_VIEWS
+            UNION SELECT TABLE_NAME AS name FROM USER_TABLES
+        ) t JOIN user_tables ut ON (ut.table_name = SUBSTR(name,5))
+        WHERE name LIKE 'SRC_%'";
+        return $this->query( $sql, [], 'TABLE_NAME');
+    }
+
+    /**
+     * Retourne la liste des tables ayant des vues matérialisées
+     *
+     * @return string[]
+     */
+    public function getImportMviews()
+    {
+        $sql = "SELECT mview_name FROM USER_MVIEWS WHERE mview_name LIKE 'MV_%'";
+        $stmt = $this->getEntityManager()->getConnection()->query($sql);
+        $mviews = [];
+        while ($d = $stmt->fetch()){
+            $mvn = substr( $d['MVIEW_NAME'], 3 );
+            $mviews[] = $mvn;
+        }
+        return $mviews;
+    }
+
+    /**
+     * 
+     * @param string $tableName
+     * @param string $columnName
+     */
+    public function hasColumn( $tableName, $columnName )
+    {
+        $sql = "
+        SELECT
+          COUNT(*) result
+        FROM
+          USER_TAB_COLS utc
+        WHERE
+          utc.table_name = :tableName
+          AND utc.column_name = :columnName
+        ";
+        $result = $this->query( $sql, compact('tableName', 'columnName'), 'RESULT');
+        return $result[0] === '1';
+    }
+
+    /**
+     * Retourne les colonnes concernées par l'import pour une table donnée
+     */
+    public function getImportCols( $tableName )
+    {
+        $sql = "
+        SELECT
+            utc.COLUMN_NAME
+        FROM
+          USER_TAB_COLS utc
+          JOIN ALL_TAB_COLS atc ON (atc.table_name = 'SRC_' || utc.table_name AND atc.column_name = utc.column_name)
+        WHERE
+          utc.COLUMN_NAME NOT IN ('ID')
+          AND utc.COLUMN_NAME NOT LIKE 'HISTO_%'
+          AND utc.COLUMN_NAME NOT LIKE 'SOURCE_%'
+          AND utc.table_name = :tableName
+        ORDER BY
+          utc.COLUMN_NAME";
+
+        return $this->query( $sql, ['tableName' => $tableName], 'COLUMN_NAME');
+    }
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/Traits/DifferentielServiceAwareTrait.php b/src/UnicaenImport/Service/Traits/DifferentielServiceAwareTrait.php
new file mode 100644
index 0000000..59b2088
--- /dev/null
+++ b/src/UnicaenImport/Service/Traits/DifferentielServiceAwareTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace UnicaenImport\Service\Traits;
+
+use UnicaenImport\Service\DifferentielService;
+use RuntimeException;
+
+/**
+ * Description of DifferentielServiceAwareTrait
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+trait DifferentielServiceAwareTrait
+{
+    /**
+     * @var DifferentielService
+     */
+    private $serviceDifferentiel;
+
+
+
+    /**
+     * @param DifferentielService $serviceDifferentiel
+     *
+     * @return self
+     */
+    public function setServiceDifferentiel(DifferentielService $serviceDifferentiel)
+    {
+        $this->serviceDifferentiel = $serviceDifferentiel;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return DifferentielService
+     * @throws RuntimeException
+     */
+    public function getServiceDifferentiel()
+    {
+        if (empty($this->serviceDifferentiel)) {
+            if (!method_exists($this, 'getServiceLocator')) {
+                throw new RuntimeException('La classe ' . get_class($this) . ' n\'a pas accès au ServiceLocator.');
+            }
+
+            $serviceLocator = $this->getServiceLocator();
+            if (method_exists($serviceLocator, 'getServiceLocator')) {
+                $serviceLocator = $serviceLocator->getServiceLocator();
+            }
+            $this->serviceDifferentiel = $serviceLocator->get('UnicaenImport\Service\Differentiel');
+        }
+
+        return $this->serviceDifferentiel;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/Traits/QueryGeneratorServiceAwareTrait.php b/src/UnicaenImport/Service/Traits/QueryGeneratorServiceAwareTrait.php
new file mode 100644
index 0000000..2586f98
--- /dev/null
+++ b/src/UnicaenImport/Service/Traits/QueryGeneratorServiceAwareTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace UnicaenImport\Service\Traits;
+
+use UnicaenImport\Service\QueryGeneratorService;
+use RuntimeException;
+
+/**
+ * Description of QueryGeneratorServiceAwareTrait
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+trait QueryGeneratorServiceAwareTrait
+{
+    /**
+     * @var QueryGeneratorService
+     */
+    private $serviceQueryGenerator;
+
+
+
+    /**
+     * @param QueryGeneratorService $serviceQueryGenerator
+     *
+     * @return self
+     */
+    public function setServiceQueryGenerator(QueryGeneratorService $serviceQueryGenerator)
+    {
+        $this->serviceQueryGenerator = $serviceQueryGenerator;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return QueryGeneratorService
+     * @throws RuntimeException
+     */
+    public function getServiceQueryGenerator()
+    {
+        if (empty($this->serviceQueryGenerator)) {
+            if (!method_exists($this, 'getServiceLocator')) {
+                throw new RuntimeException('La classe ' . get_class($this) . ' n\'a pas accès au ServiceLocator.');
+            }
+
+            $serviceLocator = $this->getServiceLocator();
+            if (method_exists($serviceLocator, 'getServiceLocator')) {
+                $serviceLocator = $serviceLocator->getServiceLocator();
+            }
+            $this->serviceQueryGenerator = $serviceLocator->get('UnicaenImport\Service\QueryGenerator');
+        }
+
+        return $this->serviceQueryGenerator;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/Service/Traits/SchemaServiceAwareTrait.php b/src/UnicaenImport/Service/Traits/SchemaServiceAwareTrait.php
new file mode 100644
index 0000000..65f4adf
--- /dev/null
+++ b/src/UnicaenImport/Service/Traits/SchemaServiceAwareTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace UnicaenImport\Service\Traits;
+
+use UnicaenImport\Service\SchemaService;
+use RuntimeException;
+
+/**
+ * Description of SchemaServiceAwareTrait
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+trait SchemaServiceAwareTrait
+{
+    /**
+     * @var SchemaService
+     */
+    private $serviceSchema;
+
+
+
+    /**
+     * @param SchemaService $serviceSchema
+     *
+     * @return self
+     */
+    public function setServiceSchema(SchemaService $serviceSchema)
+    {
+        $this->serviceSchema = $serviceSchema;
+
+        return $this;
+    }
+
+
+
+    /**
+     * @return SchemaService
+     * @throws RuntimeException
+     */
+    public function getServiceSchema()
+    {
+        if (empty($this->serviceSchema)) {
+            if (!method_exists($this, 'getServiceLocator')) {
+                throw new RuntimeException('La classe ' . get_class($this) . ' n\'a pas accès au ServiceLocator.');
+            }
+
+            $serviceLocator = $this->getServiceLocator();
+            if (method_exists($serviceLocator, 'getServiceLocator')) {
+                $serviceLocator = $serviceLocator->getServiceLocator();
+            }
+            $this->serviceSchema = $serviceLocator->get('UnicaenImport\Service\Schema');
+        }
+
+        return $this->serviceSchema;
+    }
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/View/Helper/DifferentielLigne/DifferentielLigne.php b/src/UnicaenImport/View/Helper/DifferentielLigne/DifferentielLigne.php
new file mode 100644
index 0000000..290b5ed
--- /dev/null
+++ b/src/UnicaenImport/View/Helper/DifferentielLigne/DifferentielLigne.php
@@ -0,0 +1,225 @@
+<?php
+namespace UnicaenImport\View\Helper\DifferentielLigne;
+
+use UnicaenImport\Options\Traits\ModuleOptionsAwareTrait;
+use Zend\View\Helper\AbstractHelper;
+use UnicaenImport\Entity\Differentiel\Ligne;
+use Zend\ServiceManager\ServiceLocatorAwareInterface;
+use Zend\ServiceManager\ServiceLocatorAwareTrait;
+
+/**
+ * Aide de vue permettant d'afficher une ligne de différentiel d'import
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class DifferentielLigne extends AbstractHelper implements ServiceLocatorAwareInterface
+{
+    use ServiceLocatorAwareTrait;
+    use ModuleOptionsAwareTrait;
+
+    /**
+     * @var Ligne
+     */
+    protected $ligne;
+
+
+
+    /**
+     * Helper entry point.
+     *
+     * @return self
+     */
+    final public function __invoke(Ligne $ligne)
+    {
+        $filter = new \Zend\Filter\Word\UnderscoreToCamelCase;
+
+        $classes      = $this->getOptionsModule()->getDifferentielViewHelpers();
+        $helperObject = null;
+        if (isset($classes[$ligne->getTableName()])) {
+            $helperClass  = $classes[$ligne->getTableName()];
+            $helperObject = new $helperClass;
+            if (!$helperObject instanceof self) {
+                throw new \LogicException('L\'aide de vue Import pour la table ' . $ligne->getTableName() . ' doit hériter de ' . __CLASS__);
+            }
+        }
+
+        $helperClass = __NAMESPACE__ . '\\' . $filter->filter(strtolower($ligne->getTableName()));
+
+        if ($helperObject) {
+            $helperObject->setServiceLocator($this->getServiceLocator()); // transmission du serviceLocator
+            $helperObject->setLigne($ligne);
+            $helperObject->setView($this->getView());
+
+            return $helperObject;
+        } else {
+            $this->setLigne($ligne);
+
+            return $this;
+        }
+    }
+
+
+
+    /**
+     * Retourne le code HTML généré par cette aide de vue.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->render();
+    }
+
+
+
+    /**
+     * Génère le code HTML.
+     *
+     * @return string
+     */
+    protected function render()
+    {
+        $out     = $this->getType() . ' ' . $this->getSujet() . ' ' . $this->getAction() . ' depuis ' . $this->getSource() . '<br />';
+        $details = $this->getDetails();
+        if (!empty($details)) $out .= 'Détails : ' . implode(', ', $details) . '';
+
+        return (string)$this->getView()->messenger()->setMessage($out, \UnicaenApp\View\Helper\Messenger::WARNING);
+    }
+
+
+
+    /**
+     * Retourne le type de ligne (en fonction du nom de la table)
+     *
+     * @return string
+     */
+    public function getType()
+    {
+        $type = ucwords(str_replace('_', ' ', strtolower($this->ligne->getTableName())));
+
+        return $type;
+    }
+
+
+
+    /**
+     * Retourne le sujet de la ligne
+     *
+     * @return string
+     */
+    public function getSujet()
+    {
+        return 'Code initial : ' . $this->ligne->getSourceCode();
+    }
+
+
+
+    /**
+     * Retourne l'action à effectuer pour que la mise à jour s'effectue
+     *
+     * @return string
+     */
+    public function getAction()
+    {
+        switch ($this->ligne->getAction()) {
+            case 'insert' :
+                return 'à importer';
+            case 'update' :
+                return 'à mettre à jour';
+            case 'delete' :
+                return 'à supprimer';
+            case 'undelete' :
+                return 'à restaurer';
+        }
+
+        return 'Action non définie';
+    }
+
+
+
+    /**
+     * Retourne les détails de l'action à effectuer
+     *
+     * @return string[]
+     */
+    public function getDetails()
+    {
+        $details = [];
+        if ('update' == $this->ligne->getAction()) {
+            $changes = $this->ligne->getChanges();
+            foreach ($changes as $column => $value) {
+                $columnDetails = $this->getColumnDetails($column, $value);
+                if ($columnDetails) $details[] = $columnDetails;
+            }
+        }
+
+        return $details;
+    }
+
+
+
+    public function getColumnDetails($column, $value)
+    {
+        switch ($column) {
+            case 'VALIDITE_DEBUT':
+                if ($value) {
+                    $date = new \DateTime($value);
+
+                    return 'valide depuis le ' . $date->format('d/m/Y');
+                } else {
+                    return 'valide depuis toujours';
+                }
+            case 'VALIDITE_FIN':
+                if ($value) {
+                    $date = new \DateTime($value);
+
+                    return 'valide jusqu\'au ' . $date->format('d/m/Y');
+                } else {
+                    return 'valide pour toujours';
+                }
+            default:
+                $column = str_replace('_', ' ', strtolower($column));
+
+                return $column . ' devient ' . $value;
+        }
+    }
+
+
+
+    /**
+     * Retourne la source de données
+     *
+     * @return string
+     */
+    public function getSource()
+    {
+        return $this->ligne->getSource()->getLibelle();
+    }
+
+
+
+    /**
+     *
+     * @return Ligne
+     */
+    public function getLigne()
+    {
+        return $this->ligne;
+    }
+
+
+
+    /**
+     *
+     * @param Ligne $ligne
+     *
+     * @return DifferentielLigne
+     */
+    public function setLigne(Ligne $ligne)
+    {
+        $this->ligne = $ligne;
+
+        return $this;
+    }
+
+}
\ No newline at end of file
diff --git a/src/UnicaenImport/View/Helper/DifferentielListe.php b/src/UnicaenImport/View/Helper/DifferentielListe.php
new file mode 100644
index 0000000..d6934a0
--- /dev/null
+++ b/src/UnicaenImport/View/Helper/DifferentielListe.php
@@ -0,0 +1,122 @@
+<?php
+namespace UnicaenImport\View\Helper;
+
+use Zend\View\Helper\AbstractHelper;
+use UnicaenImport\Service\Differentiel;
+use UnicaenImport\Entity\Differentiel\Ligne;
+use UnicaenImport\Exception\Exception;
+use UnicaenImport\View\Helper\DifferentielLigne\DifferentielLigne;
+
+/**
+ * Aide de vue permettant d'afficher une liste de données différentielles
+ *
+ * @author Laurent LÉCLUSE <laurent.lecluse at unicaen.fr>
+ */
+class DifferentielListe extends AbstractHelper
+{
+    /**
+     * Lignes de différentiel
+     *
+     * @var Ligne[]
+     */
+    protected $lignes;
+
+
+
+
+
+    /**
+     * Helper entry point.
+     *
+     * @param Ligne[]|Differentiel  $lignes
+     * @return self
+     */
+    final public function __invoke( $lignes )
+    {
+        $this->setLignes($lignes);
+        return $this;
+    }
+
+    /**
+     * Retourne le code HTML généré par cette aide de vue.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->render();
+    }
+
+    /**
+     * Génère le code HTML.
+     *
+     * @return string
+     */
+    public function render(){
+        $aucunEcart = 'Il n\'y a aucun écart entre les sources de données et OSE';
+        if (empty($this->lignes)) return $aucunEcart;
+        $out = '';
+        foreach( $this->lignes as $ligne ){
+            $dl = $this->getView()->differentielLigne( $ligne );
+            if ($ligne->getAction() != 'update' || $dl->getDetails()){
+                $out .= '<tr>'
+                            .'<td>'.$dl->getType().'</td>'
+                            .'<td>'.$dl->getSujet().'</td>'
+                            .'<td>'.ucfirst($dl->getAction()).'</td>'
+                            .'<td>'.$dl->getSource().'</td>'
+                            .'<td>'.ucfirst(implode( ', ', $dl->getDetails() )).'</td>'
+                       .'</tr>'."\n";
+            }
+        }
+        if ($out){
+            $out  = '<table class="table">'."\n"
+                   .'<tr><th>Type</th><th>Sujet</th><th>Action</th><th>Source</th><th>Détails</th></tr>'
+                   .$out
+                   .'</table>'."\n";
+        }else{
+            return $aucunEcart;
+        }
+        return $out;
+    }
+
+    /**
+     * Retourne la liste des lignes
+     *
+     * @return Ligne[]
+     */
+    public function getLignes()
+    {
+        return $this->lignes;
+    }
+
+    public function addLigne( Ligne $ligne )
+    {
+        $this->lignes[] = $ligne;
+    }
+
+    /**
+     *
+     *
+     * @param Ligne[]|Differentiel $lignes
+     * @return DifferentielLigne
+     */
+    public function setLignes($lignes)
+    {
+        $this->lignes = [];
+        if( $lignes instanceof Differentiel ){
+            while( $ligne = $lignes->fetchNext() ){
+                $this->addLigne($ligne);
+            }
+        }elseif(is_array($lignes)){
+            foreach( $lignes as $ligne ){
+                if (! $ligne instanceof Ligne){
+                    throw new Exception('La ligne de différentiel transmise n\'est pas au bon format.');
+                }
+                $this->addLigne( $ligne );
+            }
+        }
+        return $this;
+    }
+
+
+}
\ No newline at end of file
diff --git a/view/unicaen-import/import/config.phtml b/view/unicaen-import/import/config.phtml
new file mode 100644
index 0000000..82cfc49
--- /dev/null
+++ b/view/unicaen-import/import/config.phtml
@@ -0,0 +1,3 @@
+<h1>Import de données</h1>
+
+TEST CONFIG = <?php echo $test ?>
\ No newline at end of file
diff --git a/view/unicaen-import/import/index.phtml b/view/unicaen-import/import/index.phtml
new file mode 100644
index 0000000..042ffd7
--- /dev/null
+++ b/view/unicaen-import/import/index.phtml
@@ -0,0 +1,10 @@
+<?php
+
+$this->headTitle()->append("Import de données");
+
+?>
+<h1 class="page-header">Import de données</h1>
+
+<?php
+
+echo $this->navigation('navigation')->menuContextuel()->setPartial('unicaen-app/menu-dl.phtml');
\ No newline at end of file
diff --git a/view/unicaen-import/import/show-diff.phtml b/view/unicaen-import/import/show-diff.phtml
new file mode 100644
index 0000000..7fb3cf2
--- /dev/null
+++ b/view/unicaen-import/import/show-diff.phtml
@@ -0,0 +1,56 @@
+<?php
+
+use UnicaenImport\Provider\Privilege\Privileges;
+
+?>
+<h1>Écarts entre l'application et ses sources</h1>
+
+<?php foreach( $data as $table => $lignes ):
+    $tableLabel = ucwords(str_replace( '_', ' ', strtolower($table)));
+?>
+<h2><?php echo $tableLabel ?></h2>
+<div id="DIV_<?php echo $table ?>" data-url="<?php echo $this->url('import', ['action' => 'update','table' => $table]) ?>">
+<?php echo $this->differentielListe( $lignes )->render(); ?>
+
+<?php if (count($lignes) > 100) : ?>
+<div>Toutes les lignes n'ont pas été affichées, leur nombre dépassant 100</div>
+<?php endif; ?>
+
+<?php if ($this->isAllowed(Privileges::getResourceId(Privileges::IMPORT_MAJ))): ?>
+<div>
+<?php if (in_array($table, $mviews)): ?>
+    <a class="import-update-mv" href="javascript:void(0)" data-table="<?php echo $table ?>">
+        <span class="glyphicon glyphicon-refresh"></span>
+        Mettre à jour la vue matérialisée
+    </a>
+<?php endif; ?>
+<?php if (count($lignes) > 0) : ?>
+    &nbsp;&nbsp;<a class="import-update" href="javascript:void(0)" data-table="<?php echo $table ?>">
+        <span class="glyphicon glyphicon-refresh"></span>
+        Mettre à jour les données
+    </a>
+<?php endif; ?>
+</div>
+<div class="alert alert-info" role="alert" id="DIV_WAIT_<?php echo $table ?>" style="display:none">
+    <span class="loading" style="padding-right:1em">&nbsp;</span>
+    Traitement en cours, merci de patienter. L'opération peut prendre jusqu'à plusieurs minutes.
+</div>
+<?php endif; ?>
+</div>
+<?php endforeach; ?>
+
+<script>
+    $(function() {
+
+        $("body").on("click", "a.import-update-mv", function(e) {
+            $( "#DIV_WAIT_"+$(this).data('table') ).show();
+            $( "#DIV_"+$(this).data('table') ).refresh({'type-maj': 'vue-materialisee'});
+        });
+
+        $("body").on("click", "a.import-update", function(e) {
+            $( "#DIV_WAIT_"+$(this).data('table') ).show();
+            $( "#DIV_"+$(this).data('table') ).refresh({'type-maj': 'donnees'});
+        });
+
+    });
+</script>
\ No newline at end of file
diff --git a/view/unicaen-import/import/show-import-tbl.phtml b/view/unicaen-import/import/show-import-tbl.phtml
new file mode 100644
index 0000000..6642a40
--- /dev/null
+++ b/view/unicaen-import/import/show-import-tbl.phtml
@@ -0,0 +1,33 @@
+
+<h1>Tableau de bord principal</h1>
+
+<?php foreach( $data as $tname => $columns ): ?>
+    <h2><?php echo $tname ?></h2>
+    <table class="table table-striped table-bordered table-hover">
+    <tr>
+        <th>Colonne</th>
+        <th>Type</th>
+        <th>Longueur</th>
+        <th>Nullable</th>
+        <th>Val. par défaut</th>
+        <th>Table de réf.</th>
+        <th>Colonne de réf.</th>
+        <th>Import actif</th>
+    </tr>
+    <?php foreach( $columns as $cname => $column ): ?>
+    <tr class="<?php 
+        if (! $column->importActif && ! $column->hasDefault && ! $column->nullable) echo 'danger';
+        elseif (! $column->importActif) echo 'warning';
+    ?>">
+        <th><?php echo $cname ?></th>
+        <td><?php echo $column->dataType ?></td>
+        <td style="text-align:center"><?php echo $column->length ?></td>
+        <td style="text-align:center"><?php echo $column->nullable ? '<span class="glyphicon glyphicon-ok"></span>' : '' ?></td>
+        <td style="text-align:center"><?php echo $column->hasDefault ? '<span class="glyphicon glyphicon-ok"></span>' : '' ?></td>
+        <td><?php echo $column->refTableName ?></td>
+        <td><?php echo $column->refColumnName ?></td>
+        <td style="text-align:center"><?php echo $column->importActif ? '<span class="glyphicon glyphicon-ok"></span>' : '' ?></td>
+    </tr>
+    <?php endforeach; ?>
+    </table>
+<?php endforeach; ?>
diff --git a/view/unicaen-import/import/update-materialized-view.php b/view/unicaen-import/import/update-materialized-view.php
new file mode 100644
index 0000000..3ac00fa
--- /dev/null
+++ b/view/unicaen-import/import/update-materialized-view.php
@@ -0,0 +1,8 @@
+<?php
+
+/* 
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
diff --git a/view/unicaen-import/import/update-tables.phtml b/view/unicaen-import/import/update-tables.phtml
new file mode 100644
index 0000000..f85e95b
--- /dev/null
+++ b/view/unicaen-import/import/update-tables.phtml
@@ -0,0 +1 @@
+<?php echo $this->messenger()->setMessage($message, \UnicaenApp\View\Helper\Messenger::SUCCESS); ?>
\ No newline at end of file
diff --git a/view/unicaen-import/import/update-views-and-packages.phtml b/view/unicaen-import/import/update-views-and-packages.phtml
new file mode 100644
index 0000000..f85e95b
--- /dev/null
+++ b/view/unicaen-import/import/update-views-and-packages.phtml
@@ -0,0 +1 @@
+<?php echo $this->messenger()->setMessage($message, \UnicaenApp\View\Helper\Messenger::SUCCESS); ?>
\ No newline at end of file
diff --git a/view/unicaen-import/import/update.phtml b/view/unicaen-import/import/update.phtml
new file mode 100644
index 0000000..8247233
--- /dev/null
+++ b/view/unicaen-import/import/update.phtml
@@ -0,0 +1,28 @@
+<?php echo $this->differentielListe( $lignes ); ?>
+<?php if (count($lignes) > 100) : ?>
+<div>Toutes les lignes n'ont pas été affichées, leur nombre dépassant 100</div>
+<?php endif; ?>
+
+<div>
+    <a class="import-update-mv" href="javascript:void(0)" data-table="<?php echo $table ?>">
+        <span class="glyphicon glyphicon-refresh"></span>
+        Mettre à jour la vue matérialisée
+    </a>
+<?php if (count($lignes) > 0) : ?>
+    &nbsp;&nbsp;<a class="import-update" href="javascript:void(0)" data-table="<?php echo $table ?>">
+        <span class="glyphicon glyphicon-refresh"></span>
+        Mettre à jour les données
+    </a>
+<?php endif; ?>
+</div>
+<div class="alert alert-info" role="alert" id="DIV_WAIT_<?php echo $table ?>" style="display:none">
+    <span class="loading" style="padding-right:1em">&nbsp;</span>
+    Traitement en cours, merci de patienter. L'opération peut prendre jusqu'à plusieurs minutes.
+</div>
+<?php
+
+if ($errors){
+    echo $this->messenger()->setMessages([UnicaenApp\View\Helper\Messenger::ERROR => $errors]);
+}else{
+    echo $this->messenger()->setMessages([UnicaenApp\View\Helper\Messenger::SUCCESS => ['Action réalisée avec succès']]);
+}
\ No newline at end of file
-- 
GitLab