Repository: PnX-SI/TaxHub (language: python)

Issue:
Develop > Master / Preparation 1.7.0

install_db.sh, compatibilité UHV1 ou UHV2 ?
Dans les SQL de TaxHub, on ajoute les aspects liés à UsersHub. 
Pour la V2, un script ajoutait ce qu'il faut au schéma utilisateurs mais aussi les vues de compatibilité UHV1. 
On a mis ces vues dans un autre SQL à part pour pouvoir ne pas créer ces vues : https://github.com/PnX-SI/TaxHub/tree/develop/data

Ajouté dans ``install_db.sh`` mais pas bien sur...
https://github.com/PnX-SI/TaxHub/commit/c63baf57674645d4a7fb43edd8c50469bbbf5d8f

A revoir si besoin avec @gildeluermoz 
Dysfonctionnement de la recherche par famille
La recherche d'une famille dans l'onglet taxref n'est pas fonctionnelle. La saisie renvoie un code parmi les choix proposés, et ce code filtre tout jusqu'à l'ordre mais ne filtre pas la famille. 

Je ne sais pas si c'est très clair, mais globalement ce qui marche pour les autres rangs ne marche pas sur la famille...
Login impossible v1.7.0
Il y a un soucis erreur 308 lors de la récupération de la route config
API taxref/search: récupérer les champs désirés
Ajouter un paramètre "`fields`" au web service "`/taxref/search/(str:field)/(str:ilike)`" afin de pouvoir récupérer dans la réponse des champs supplémentaires selon les besoins.
Le paramètre "`fields`" contiendra des noms de champs de la la table Taxref séparés par des virgules.

Exemple : 
`/taxref/search/lb_nom/acer?fields=lb_auteur,nom_complet_html`


Discussion / hints:

Fix patch:
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..5f1187bd
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include VERSION
+include LICENCE
+include README.rst
+include requirements.in
diff --git a/README.rst b/README.rst
index 2d9df726..6e10b068 100644
--- a/README.rst
+++ b/README.rst
@@ -18,7 +18,7 @@ Documentation
 
 La documentation d'installation de TaxHub est disponible sur http://taxhub.readthedocs.io.
 
-TaxHub peut aussi être installé à partir du script d'installation globale de GeoNature : http://geonature.readthedocs.io/fr/latest/install_all/README.html.
+TaxHub peut aussi être installé à partir du script d'installation globale de GeoNature : http://docs.geonature.fr/installation-all.html.
 
 Principes
 =========
@@ -31,7 +31,7 @@ Une partie correspond au TAXREF complet tel que fourni par le MNHN. Son contenu
 
 Une partie correspond à la partie spécifique à chaque structure. Il faut commencer par renseigner la table ``bib_noms`` en selectionnant les taxons qui nous intéressent dans le TAXREF.
 
-Il faut ensuite y greffer des informations spécifiques grace à ``bib_attributs`` (patrimonialité, marqueurs, autres selon les besoins) et définir des sous-listes de taxons (amphibiens, ....) dans ``bib_listes`` en fonction des besoins.
+Il faut ensuite y greffer des informations spécifiques grace à ``bib_attributs`` (patrimonialité, marqueurs, autres selon les besoins) et définir des listes de taxons (espèces d'un protocole, ....) dans ``bib_listes`` en fonction des besoins.
 
 Technologies
 ------------
@@ -39,14 +39,14 @@ Technologies
 - Langages : Python, HTML, JS, CSS
 - BDD : PostgreSQL, PostGIS
 - Serveur : Debian ou Ubuntu
-- Framework python : flask
+- Framework python : Flask
 - Framework JS : AngularJS
 - Framework CSS : Bootstrap
 
 Gestion des droits
 ------------------
 
-Elle est centralisée dans l'application `UsersHub <https://github.com/PnX-SI/UsersHub>`_. Il faut donc disposer d'une application ``TaxHub`` dans UsersHub et y intégrer des groupes et/ou utilisateurs.
+Elle est centralisée dans l'application `UsersHub <https://github.com/PnX-SI/UsersHub>`_. Il faut donc disposer de l'application ``TaxHub`` dans UsersHub et y intégrer des groupes et/ou utilisateurs.
 
 Niveaux de droits :
 
@@ -67,11 +67,11 @@ Auteurs
 - Thomas Lebard
 - Camille Monchicourt
 
-License
+Licence
 -------
 
 * OpenSource - GPL V3
-* Copyright (c) 2014-2019 - Parc National des Écrins - Parc national des Cévennes
+* Copyright (c) 2014-2020 - Parc National des Écrins - Parc national des Cévennes
 
 
 .. image:: http://geonature.fr/img/logo-pne.jpg
diff --git a/VERSION b/VERSION
index 2ac2d702..5cf054ee 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.7.0.dev0
+1.7.4.dev0
diff --git a/apptax/taxonomie/filemanager.py b/apptax/taxonomie/filemanager.py
index b2493fed..6e4402d3 100644
--- a/apptax/taxonomie/filemanager.py
+++ b/apptax/taxonomie/filemanager.py
@@ -1,31 +1,42 @@
-# coding: utf8
-
-import requests
-import os
-import unicodedata
 import re
-# import numpy as np
+import os
 import math
+import boto3
+import botocore
+import logging
+import requests
+import unicodedata
+
+from abc import ABC
 
 from shutil import rmtree
 from PIL import Image, ImageOps
+import io
+
 from werkzeug.utils import secure_filename
+
 from flask import current_app
 
 try:
     from urllib.request import urlopen
-except Exception as e:
+except Exception:
     from urllib2 import urlopen
 
 
+logger = logging.getLogger()
+
+
 def remove_dir(dirpath):
-    if(dirpath == '/'):
-        raise Exception('rm / is not possible')
+    """
+    Fonction de suppression d'un répertoire
+    """
+    if dirpath == "/":
+        raise Exception("rm / is not possible")
 
-    if (not os.path.exists(dirpath)):
-        raise FileNotFoundError('not exists {}'.format(dirpath))
-    if (not os.path.isdir(dirpath)):
-        raise FileNotFoundError('not isdir {}'.format(dirpath))
+    if not os.path.exists(dirpath):
+        raise FileNotFoundError("not exists {}".format(dirpath))
+    if not os.path.isdir(dirpath):
+        raise FileNotFoundError("not isdir {}".format(dirpath))
 
     try:
         rmtree(dirpath)
@@ -33,82 +44,232 @@ def remove_dir(dirpath):
         raise e
 
 
-def remove_file(filepath):
-    try:
-        os.remove(os.path.join(current_app.config['BASE_DIR'], filepath))
-    except Exception as e:
-        pass
-
-
-def rename_file(old_chemin, old_title, new_title):
-    new_chemin = old_chemin.replace(
-        removeDisallowedFilenameChars(old_title),
-        removeDisallowedFilenameChars(new_title)
-    )
-    os.rename(
-        os.path.join(current_app.config['BASE_DIR'],old_chemin),
-        os.path.join(current_app.config['BASE_DIR'], new_chemin)
-    )
-    return new_chemin
-
-
-def upload_file(file, id_media, cd_ref, titre):
-    filename = "{cd_ref}_{id_media}_{file_name}.{ext}".format(
-        cd_ref=str(cd_ref),
-        id_media=str(id_media),
-        file_name=removeDisallowedFilenameChars(titre),
-        ext=file.filename.rsplit('.', 1)[1]
-    )
-    filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
-    file.save(os.path.join(current_app.config['BASE_DIR'], filepath))
-    return filepath
-
-
 def removeDisallowedFilenameChars(uncleanString):
     cleanedString = secure_filename(uncleanString)
-    cleanedString = unicodedata.normalize('NFKD', uncleanString)
-    cleanedString = re.sub('[ ]+', '_', cleanedString)
-    cleanedString = re.sub('[^0-9a-zA-Z_-]', '', cleanedString)
+    cleanedString = unicodedata.normalize("NFKD", uncleanString)
+    cleanedString = re.sub("[ ]+", "_", cleanedString)
+    cleanedString = re.sub("[^0-9a-zA-Z_-]", "", cleanedString)
     return cleanedString
 
+
+class FileManagerServiceInterface(ABC):
+    """
+    Abstract class to media file manipulation functions
+    Class who inherite of this class must implement the following abstract methods:
+    - remove_file
+    - rename_file
+    - upload_file
+    """
+
+    def __init__(self):
+        self.dir_thumb_base = os.path.join(
+            current_app.config["BASE_DIR"], current_app.config["UPLOAD_FOLDER"], "thumb"
+        )
+
+    def _get_new_chemin(self, old_chemin, old_title, new_title):
+        return old_chemin.replace(
+            removeDisallowedFilenameChars(old_title),
+            removeDisallowedFilenameChars(new_title),
+        )
+
+    def _generate_file_name(self, file, id_media, cd_ref, titre):
+        return "{cd_ref}_{id_media}_{file_name}.{ext}".format(
+            cd_ref=str(cd_ref),
+            id_media=str(id_media),
+            file_name=removeDisallowedFilenameChars(titre),
+            ext=file.filename.rsplit(".", 1)[1],
+        )
+
+    def _get_image_object(self, media):
+        if media.chemin:
+            img = Image.open(os.path.join(current_app.config["BASE_DIR"], media.chemin))
+        else:
+            img = url_to_image(media.url)
+
+        return img
+
+    def remove_file(self, filepath):
+        try:
+            os.remove(os.path.join(current_app.config["BASE_DIR"], filepath))
+        except Exception:
+            pass
+
+    def rename_file(self, old_chemin, old_title, new_title):
+        new_chemin = self._get_new_chemin(old_chemin, old_title, new_title)
+
+        os.rename(
+            os.path.join(current_app.config["BASE_DIR"], old_chemin),
+            os.path.join(current_app.config["BASE_DIR"], new_chemin),
+        )
+        return new_chemin
+
+    def upload_file(self, file, id_media, cd_ref, titre):
+        filename = self._generate_file_name(file, id_media, cd_ref, titre)
+        filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], filename)
+        file.save(os.path.join(current_app.config["BASE_DIR"], filepath))
+        return filepath
+
+    def remove_thumb(self, id_media):
+        # suppression des thumbnails
+        try:
+            remove_dir(
+                os.path.join(
+                    current_app.config["UPLOAD_FOLDER"], "thumb", str(id_media)
+                )
+            )
+        except (FileNotFoundError, IOError, OSError) as e:
+            logger.error(e)
+            pass
+
+    def create_thumb(self, media, size, regenerate=False):
+        id_media = media.id_media
+        thumbdir = os.path.join(self.dir_thumb_base, str(id_media))
+        thumbpath = os.path.join(thumbdir, "{}x{}.jpg".format(size[0], size[1]))
+
+        if regenerate:
+            self.remove_file(thumbpath)
+
+        # Test if media exists
+        if os.path.exists(thumbpath):
+            return thumbpath
+
+        # Get Image
+        img = self._get_image_object(media)
+
+        # Création du thumbnail
+        resizeImg = resizeAndPad(img, size)
+
+        # Sauvegarde de l'image
+        if not os.path.exists(thumbdir):
+            os.makedirs(thumbdir)
+
+        resizeImg.save(thumbpath)
+        return thumbpath
+
+    def remove_media_files(self, id_media, filepath):
+        # suppression du fichier principal
+        # S'il existe
+        if filepath:
+            self.remove_file(filepath)
+
+        # Suppression des thumbnails
+        self.remove_thumb(id_media)
+
+
+class LocalFileManagerService(FileManagerServiceInterface):
+    pass
+
+
+class S3FileManagerService(FileManagerServiceInterface):
+    """
+    Class permettant de manipuler des fichiers stockés
+        dans un cloud S3 (AWS, OVH, etc..)
+    """
+
+    def __init__(self):
+        super().__init__()
+        self.s3 = boto3.client(
+            "s3",
+            aws_access_key_id=current_app.config["S3_KEY"],
+            aws_secret_access_key=current_app.config["S3_SECRET"],
+            endpoint_url=current_app.config["S3_ENDPOINT"],
+            region_name=current_app.config["S3_REGION_NAME"],
+        )
+
+    def _get_image_object(self, media):
+        if media.chemin:
+            img = url_to_image(os.path.join(current_app.config['S3_PUBLIC_URL'], media.chemin))
+        else:
+            img = url_to_image(media.url)
+
+        return img
+
+    def remove_file(self, filepath):
+        try:
+            # TODO prévoir un message d'erreur si echec suppression du bucket ?
+            self.s3.delete_object(
+                Bucket=current_app.config["S3_BUCKET_NAME"], Key=filepath
+            )
+        except botocore.exceptions.ParamValidationError:  # filepath is None (upload)
+            pass
+
+    def rename_file(self, old_chemin, old_title, new_title):
+        new_chemin = self._get_new_chemin(old_chemin, old_title, new_title)
+
+        self.s3.copy_object(
+            Bucket=current_app.config["S3_BUCKET_NAME"],
+            CopySource=os.path.join(current_app.config["S3_BUCKET_NAME"], old_chemin),
+            Key=new_chemin,
+        )
+        self.s3.delete_object(
+            Bucket=current_app.config["S3_BUCKET_NAME"], Key=old_chemin
+        )
+        return new_chemin
+
+    def upload_file(self, file, id_media, cd_ref, titre):
+        filename = self._generate_file_name(file, id_media, cd_ref, titre)
+        self.s3.upload_fileobj(
+            file,
+            current_app.config["S3_BUCKET_NAME"],
+            os.path.join(current_app.config["S3_FOLDER"], filename),
+            ExtraArgs={
+                "ACL": "public-read",
+                "ContentType": file.content_type,  # sans ça le content type est par défaut binary/octet-stream
+            },
+        )
+        return os.path.join(current_app.config["S3_FOLDER"], filename)
+
+
+if current_app.config["S3_BUCKET_NAME"]:  # Use S3 upload
+    FILEMANAGER = S3FileManagerService()
+else:
+    FILEMANAGER = LocalFileManagerService()
+
+
 # METHOD #2: PIL
 def url_to_image(url):
+    """
+        Récupération d'une image à partir d'une url
+    """
     r = requests.get(url, stream=True)
-    image = Image.open(r.raw)
-    return image
+    try:
+        img = Image.open(io.BytesIO(r.content))
+    except IOError:
+        raise Exception("Media is not an image")
+    return img
 
 
 def add_border(img, border, color=0):
-    '''
-        Ajout d'une bordure à une image
-    '''
+    """
+    Ajout d'une bordure à une image
+    """
     if isinstance(border, int) or isinstance(border, tuple):
         bimg = ImageOps.expand(img, border=border, fill=color)
     else:
-        raise RuntimeError('Border is not an integer or tuple!')
+        raise RuntimeError("Border is not an integer or tuple!")
 
     return bimg
 
 
 def calculate_border(initial_size, new_size, aspect):
     """
-        Calcul de la taille de l'image et de ces bordures
+    Calcul de la taille de l'image et de ces bordures
     """
     i_h, i_w = initial_size
     n_h, n_w = new_size
     if aspect > 1:
         # horizontal image
         f_w = n_w
-        f_h = round(i_h*(n_w/i_w))
+        f_h = round(i_h * (n_w / i_w))
 
-        pad_vert = abs((f_h-n_h)/2)
+        pad_vert = abs((f_h - n_h) / 2)
         pad_top, pad_bot = math.floor(pad_vert), math.ceil(pad_vert)
         pad_left, pad_right = 0, 0
     elif aspect < 1:
         # vertical image
         f_h = n_h
-        f_w = round(i_w*(f_h/i_h))
-        pad_horz = abs((f_w-n_w)/2)
+        f_w = round(i_w * (f_h / i_h))
+        pad_horz = abs((f_w - n_w) / 2)
         pad_left, pad_right = math.floor(pad_horz), math.ceil(pad_horz)
         pad_top, pad_bot = 0, 0
     else:
@@ -126,15 +287,15 @@ def resizeAndPad(img, new_size, pad=True, padColor=0):
     pad_left, pad_top, pad_right, pad_bot = (0, 0, 0, 0)
 
     # aspect ratio of image
-    aspect = inital_w/inital_h
+    aspect = inital_w / inital_h
 
-    if (new_size[1] == -1):  # Si largeur non spécifé
+    if new_size[1] == -1:  # Si largeur non spécifé
         final_h = new_size[0]
-        final_w = round(inital_w*(final_h/inital_h))
+        final_w = round(inital_w * (final_h / inital_h))
         pad = False
-    elif (new_size[0] == -1):  # Si hauteur non spécifé
+    elif new_size[0] == -1:  # Si hauteur non spécifé
         final_w = new_size[1]
-        final_h = round(inital_h*(final_w/inital_w))
+        final_h = round(inital_h * (final_w / inital_w))
         pad = False
     else:
         final_h, final_w = new_size
@@ -149,12 +310,7 @@ def resizeAndPad(img, new_size, pad=True, padColor=0):
 
     if pad:
         scaled_img = add_border(
-            scaled_img,
-            (pad_left, pad_top, pad_right, pad_bot),
-            color=0
+            scaled_img, (pad_left, pad_top, pad_right, pad_bot), color=0
         )
 
     return scaled_img
-
-
-
diff --git a/apptax/taxonomie/models.py b/apptax/taxonomie/models.py
index 13d1caa1..6b984142 100644
--- a/apptax/taxonomie/models.py
+++ b/apptax/taxonomie/models.py
@@ -1,11 +1,14 @@
 # coding: utf8
+from flask import current_app
 from flask_sqlalchemy import SQLAlchemy
 from sqlalchemy import ForeignKey, Sequence
+from sqlalchemy.ext.hybrid import hybrid_property
 
 from ..utils.genericmodels import serializableModel
 
 from . import db
 
+import os.path
 
 class BibNoms(serializableModel, db.Model):
     __tablename__ = "bib_noms"
@@ -144,6 +147,7 @@ class BibListes(serializableModel, db.Model):
     __tablename__ = "bib_listes"
     __table_args__ = {"schema": "taxonomie"}
     id_liste = db.Column(db.Integer, primary_key=True)
+    code_liste = db.Column(db.Unicode)
     nom_liste = db.Column(db.Unicode)
     desc_liste = db.Column(db.Text)
     picto = db.Column(db.Unicode)
@@ -201,6 +205,7 @@ class VMTaxrefListForautocomplete(serializableModel, db.Model):
     cd_ref = db.Column(db.Integer)
     nom_valide = db.Column(db.Unicode)
     lb_nom = db.Column(db.Unicode)
+    nom_vern = db.Column(db.Unicode)
     regne = db.Column(db.Unicode)
     group2_inpn = db.Column(db.Unicode)
 
@@ -268,6 +273,7 @@ class VMTaxrefHierarchie(serializableModel, db.Model):
     phylum = db.Column(db.Unicode)
     classe = db.Column(db.Unicode)
     ordre = db.Column(db.Unicode)
+    famille = db.Column(db.Unicode)
     lb_nom = db.Column(db.Unicode)
     id_rang = db.Column(db.Unicode)
     nb_tx_fm = db.Column(db.Integer)
@@ -308,3 +314,94 @@ class BibTaxrefLR(serializableModel, db.Model):
     categorie_lr = db.Column(db.Unicode)
     nom_categorie_lr = db.Column(db.Unicode)
     desc_categorie_lr = db.Column(db.Unicode)
+
+
+class TaxrefBdcStatutType(serializableModel, db.Model):
+    __tablename__ = "bdc_statut_type"
+    __table_args__ = {"schema": "taxonomie"}
+    cd_type_statut = db.Column(db.Unicode, primary_key=True)
+    lb_type_statut = db.Column(db.Unicode)
+    regroupement_type = db.Column(db.Unicode)
+    thematique = db.Column(db.Unicode)
+    type_value = db.Column(db.Unicode)
+
+    text = db.relationship("TaxrefBdcStatutText", lazy="select")
+
+
+class TaxrefBdcStatutText(serializableModel, db.Model):
+    __tablename__ = "bdc_statut_text"
+    __table_args__ = {"schema": "taxonomie"}
+    id_text = db.Column(db.Integer, primary_key=True)
+    cd_st_text = db.Column(db.Unicode)
+    cd_type_statut = db.Column(
+        db.Unicode,
+        ForeignKey("taxonomie.bdc_statut_type.cd_type_statut"), nullable=False
+    )
+    cd_sig = db.Column(db.Unicode)
+    cd_doc = db.Column(db.Unicode)
+    niveau_admin = db.Column(db.Unicode)
+    cd_iso3166_1 = db.Column(db.Unicode)
+    cd_iso3166_2 = db.Column(db.Unicode)
+    lb_adm_tr = db.Column(db.Unicode)
+    full_citation = db.Column(db.Unicode)
+    doc_url = db.Column(db.Unicode)
+    enable = db.Column(db.Boolean)
+
+    type_statut = db.relationship("TaxrefBdcStatutType", lazy="select")
+    cor_text = db.relationship("TaxrefBdcStatutCorTextValues", lazy="select")
+
+
+class TaxrefBdcStatutValues(serializableModel, db.Model):
+    __tablename__ = "bdc_statut_values"
+    __table_args__ = {"schema": "taxonomie"}
+    id_value = db.Column(db.Integer, primary_key=True)
+    code_statut = db.Column(db.Unicode)
+    label_statut = db.Column(db.Unicode)
+
+class TaxrefBdcStatutCorTextValues(serializableModel, db.Model):
+    __tablename__ = "bdc_statut_cor_text_values"
+    __table_args__ = {"schema": "taxonomie"}
+    id_value_text = db.Column(db.Integer, primary_key=True)
+    id_value = db.Column(db.Unicode, ForeignKey("taxonomie.bdc_statut_values.id_value"), nullable=False)
+    id_text = db.Column(db.Unicode, ForeignKey("taxonomie.bdc_statut_text.id_text"), nullable=False)
+
+    text = db.relationship("TaxrefBdcStatutText", lazy="select")
+    value = db.relationship("TaxrefBdcStatutValues", lazy="select")
+
+    taxon = db.relationship("TaxrefBdcStatutTaxon", lazy="select")
+
+class TaxrefBdcStatutTaxon(serializableModel, db.Model):
+    __tablename__ = "bdc_statut_taxons"
+    __table_args__ = {"schema": "taxonomie"}
+    id = db.Column(db.Integer, primary_key=True)
+    id_value_text = db.Column(db.Integer, ForeignKey("taxonomie.bdc_statut_cor_text_values.id_value_text"), nullable=False)
+    cd_nom = db.Column(db.Integer)
+    cd_ref = db.Column(db.Integer)
+    rq_statut = db.Column(db.Unicode)
+
+    value_text = db.relationship("TaxrefBdcStatutCorTextValues", lazy="select")
+
+
+class VBdcStatus(serializableModel, db.Model):
+    __tablename__ = "v_bdc_status"
+    __table_args__ = {"schema": "taxonomie", 'info': dict(is_view=True)}
+    cd_nom = db.Column(db.Integer, primary_key=True)
+    cd_ref = db.Column(db.Integer)
+    rq_statut = db.Column(db.Unicode)
+    code_statut = db.Column(db.Unicode, primary_key=True)
+    label_statut = db.Column(db.Unicode)
+    cd_type_statut = db.Column(db.Unicode, primary_key=True)
+    lb_type_statut = db.Column(db.Unicode)
+    regroupement_type = db.Column(db.Unicode)
+    thematique = db.Column(db.Unicode)
+    cd_st_text = db.Column(db.Unicode, primary_key=True)
+    cd_sig = db.Column(db.Unicode)
+    cd_doc = db.Column(db.Unicode)
+    niveau_admin = db.Column(db.Unicode)
+    cd_iso3166_1 = db.Column(db.Unicode)
+    cd_iso3166_2 = db.Column(db.Unicode)
+    full_citation = db.Column(db.Unicode, primary_key=True)
+    doc_url = db.Column(db.Unicode)
+    type_value = db.Column(db.Unicode)
+
+
diff --git a/apptax/taxonomie/repositories.py b/apptax/taxonomie/repositories.py
new file mode 100644
index 00000000..b25fdf61
--- /dev/null
+++ b/apptax/taxonomie/repositories.py
@@ -0,0 +1,259 @@
+import logging
+import os.path
+
+from flask import current_app
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.orm import joinedload
+
+from .filemanager import FILEMANAGER
+
+from . import db
+from ..log import logmanager
+from ..utils.utilssqlalchemy import dict_merge
+from .models import (
+    TaxrefBdcStatutCorTextValues,
+    TaxrefBdcStatutTaxon,
+    TaxrefBdcStatutText,
+    TaxrefBdcStatutType,
+    TaxrefBdcStatutValues,
+    VBdcStatus,
+    TMedias
+)
+
+
+logger = logging.getLogger()
+
+
+class MediaRepository:
+    s3_storage = False
+
+    def __init__(self, DBSession, s3_bucket_name):
+        self.session = DBSession
+        if s3_bucket_name:
+            self.s3_storage = True
+
+    def _format_media(self, media, force_path):
+        f_media = {**media.as_dict(), **media.types.as_dict()}
+        if self.s3_storage:
+            if not force_path:
+                f_media["chemin"] = None
+            try :
+                f_media["url"] = os.path.join(current_app.config['S3_PUBLIC_URL'], media.chemin)
+            except TypeError: #file is an URL
+                f_media["url"] = media.url
+        return f_media
+
+    def _populate_data_media(self, media, data):
+        # TODO Change add default value in DB
+        if "supprime" not in data:
+            data["supprime"] = False
+        else:
+            data["supprime"] = bool(data["supprime"])
+
+        if "is_public" in data:
+            data["is_public"] = bool(data["is_public"])
+
+        if data.get("chemin", False):
+            data["url"] = None
+
+        for k in data:
+            if hasattr(TMedias, k) and not data[k] == "null":
+                setattr(media, k, data[k])
+        return media
+
+    def get_media_filter_by(self, filters):
+        q = self.session.query(TMedias)
+        if filters:
+            q = q.filter_by(**filters)
+        return q.all()
+
+    def get_and_format_media_filter_by(self, filters, force_path=False):
+        results = self.get_media_filter_by(filters)
+        medias = []
+        for media in results:
+            medias.append(self._format_media(media, force_path))
+        return medias
+
+    def get_one_media(self, id):
+        return self.session.query(TMedias).get(id)
+
+    def get_and_format_one_media(self, id, force_path=False):
+        media = self.get_one_media(id)
+        if media:
+            return self._format_media(media, force_path)
+        else:
+            return None
+
+    def import_media(self, data, upload_file, id_media=None):
+        # case update
+        if id_media:
+            # Todo add test id media doesn't exists
+            media = self.get_one_media(id_media)
+            old_media = self.update_media_file(media)
+            action = "UPDATE"
+        else:
+            media = TMedias()
+            action = "INSERT"
+            old_media = None
+
+        # update data media
+        media = self._populate_data_media(media, data)
+        self.session.add(media)
+        self._process_comit()
+
+        # Process file
+        media = self.process_media_file(upload_file, media, bool(data["isFile"]), old_media)
+        self.session.add(media)
+        self._process_comit()
+
+        return (media, action)
+
+    def process_media_file(self, file, media, is_file, old_media_data):
+        if file and is_file:
+            # Cas 1 : upload media
+            media.url = None
+            old_chemin = media.chemin
+            filepath = FILEMANAGER.upload_file(
+                file, media.id_media, media.cd_ref, media.titre
+            )
+            media.chemin = filepath
+            if (old_chemin != "") and (old_chemin != media.chemin):
+                FILEMANAGER.remove_file(old_chemin)
+        elif (not media.chemin and media.url and not is_file):
+            # Cas 2 : URL
+            if media.chemin:
+                FILEMANAGER.remove_file(media.chemin)
+                media.chemin = None
+        elif old_media_data["titre"] != media.titre:
+            # Cas 3 : Changement du titre du média
+            filepath = FILEMANAGER.rename_file(media.chemin, old_media_data["titre"], media.titre)
+            media.chemin = filepath
+
+        return media
+
+    def update_media_file(self, media):
+        # Keep old data
+        old_media = media.as_dict()
+        # Remove thumb
+        FILEMANAGER.remove_thumb(media.id_media)
+
+        return old_media
+
+    def persist(self, entity):
+        self.session.add(entity)
+        self.session.flush()
+        return entity
+
+    def delete(self, id):
+        media = self.get_one_media(id)
+        chemin = None
+        if media.chemin != "":
+            chemin = media.chemin
+
+        # Suppression de l'entrée en base
+        try:
+            self.session.delete(media)
+            self.session.commit()
+        except Exception as e:
+            raise (e)
+
+        # Suppression des fichiers
+        FILEMANAGER.remove_media_files(id, chemin)
+
+        return media
+
+    def _process_comit(self, rollback=True):
+        try:
+            self.session.commit()
+        except IntegrityError as e:
+            logger.error(e)
+            if rollback:
+                self.session.rollback()
+            raise e
+        except Exception as e:
+            logger.error(e)
+            if rollback:
+                self.session.rollback()
+            raise e
+
+class BdcStatusRepository:
+
+    def get_status(self, cd_ref: int, type_statut : str, enable=True, format=False):
+        """
+            Retourne la liste des statuts associés à un taxon
+            sous forme hiérarchique
+
+        Args:
+            cd_ref (int): cd_ref
+            type_statut (str): code du type de statut
+            enable (bool, optional): ne retourner que les statuts actifs Defaults to True.
+            format (bool, optional): retourne les données formatées. Defaults to False.
+
+        Returns:
+            listes des statuts du taxon
+        """
+        q = (
+            db.session.query(TaxrefBdcStatutTaxon)
+            .join(TaxrefBdcStatutCorTextValues)
+            .join(TaxrefBdcStatutText)
+            .filter(
+                TaxrefBdcStatutTaxon.cd_ref == cd_ref
+            ).filter(
+                TaxrefBdcStatutText.enable == enable
+            )
+        )
+        if type_statut:
+            q = q.filter(
+                TaxrefBdcStatutText.cd_type_statut == type_statut
+            )
+
+        q = (
+            q.options(
+                joinedload(TaxrefBdcStatutTaxon.value_text)
+                .joinedload(TaxrefBdcStatutCorTextValues.value)
+            ).options(
+                joinedload(TaxrefBdcStatutTaxon.value_text)
+                .joinedload(TaxrefBdcStatutCorTextValues.text)
+                .joinedload(TaxrefBdcStatutText.type_statut)
+            )
+        )
+        data = q.all()
+
+        # Retour des données sous forme formatées ou pas
+        if format:
+            return self.format_hierarchy_status(data)
+        else:
+            return data
+
+    def format_hierarchy_status(self, data):
+        """
+            Formatage des données sous la forme d'un dictionnaire
+            type de statut : {
+                [text : {
+                    [valeurs]
+                }]
+            }
+
+        Args:
+            data ([resultProxy]): Données à formater
+
+        Returns:
+            [type]: [description]
+        """
+        results = {}
+
+        for d in data:
+            cd_type_statut = d.value_text.text.type_statut.cd_type_statut
+            res = {** d.value_text.text.type_statut.as_dict(), **{"text": {}}}
+            id_text = d.value_text.text.id_text
+            res["text"][id_text] = {**d.value_text.text.as_dict(), **{"values": {}}}
+
+            res["text"][id_text]["values"][d.value_text.id_value_text] = {
+                **d.as_dict(), **d.value_text.value.as_dict()
+            }
+
+            if cd_type_statut in results:
+                dict_merge(results[cd_type_statut], res)
+            else:
+                results[cd_type_statut] = res
+        return results
diff --git a/apptax/taxonomie/routesbdcstatuts.py b/apptax/taxonomie/routesbdcstatuts.py
new file mode 100644
index 00000000..c10d0fc7
--- /dev/null
+++ b/apptax/taxonomie/routesbdcstatuts.py
@@ -0,0 +1,52 @@
+# coding: utf8
+
+import os
+import logging
+from flask import jsonify
+from flask import Blueprint, request, current_app
+from sqlalchemy import func, or_
+from sqlalchemy.orm import joinedload
+
+from pypnusershub import routes as fnauth
+
+from . import filemanager
+from . import db
+from ..log import logmanager
+from ..utils.utilssqlalchemy import json_resp, csv_resp, dict_merge
+from ..utils.genericfunctions import calculate_offset_page
+from .repositories import BdcStatusRepository
+adresses = Blueprint("bdc_status", __name__)
+logger = logging.getLogger()
+
+
+@adresses.route("/list/<cd_ref>", methods=["GET"])
+@json_resp
+def get_bdcstatus_list_for_one_taxon(cd_ref=None):
+    """
+        retourne la listes des statuts associés à un taxon
+    """
+    q = db.session.query(VBdcStatus).filter_by(
+        cd_ref=cd_ref
+    )
+    print(q)
+    data = q.all()
+    return [d.as_dict() for d in data]
+
+
+@adresses.route("/hierarchy/<cd_ref>", methods=["GET"])
+@json_resp
+def get_bdcstatus_hierarchy(cd_ref=None):
+    """
+        retourne la listes des statuts associés sous forme hierarchique
+    """
+    # get parameters type:
+    type_statut = request.args.get("type_statut")
+
+    results = BdcStatusRepository().get_status(
+        cd_ref=cd_ref,
+        type_statut=type_statut,
+        enable=True,
+        format=True
+    )
+
+    return results
diff --git a/apptax/taxonomie/routesbiblistes.py b/apptax/taxonomie/routesbiblistes.py
index a75d0f88..0ff7805a 100644
--- a/apptax/taxonomie/routesbiblistes.py
+++ b/apptax/taxonomie/routesbiblistes.py
@@ -12,6 +12,7 @@
 from . import db
 from ..log import logmanager
 from ..utils.utilssqlalchemy import json_resp, csv_resp
+from ..utils.genericfunctions import calculate_offset_page
 from .models import BibListes, CorNomListe, Taxref, BibNoms
 
 
@@ -153,8 +154,10 @@ def insertUpdate_biblistes(id_liste=None, id_role=None):
 def getNoms_bibtaxons(idliste):
     # Traitement des parametres
     parameters = request.args
-    limit = int(parameters.get("limit")) if parameters.get("limit") else 100
-    page = int(parameters.get("page")) - 1 if parameters.get("page") else 0
+    limit = parameters.get("limit", 100, int)
+    page = parameters.get("page", 1, int)
+    offset = parameters.get("offset", 0, int)
+    (limit, offset, page) = calculate_offset_page(limit, offset, page)
 
     # Récupération du groupe de la liste
     (regne, group2_inpn) = (
@@ -230,7 +233,8 @@ def getNoms_bibtaxons(idliste):
         q = q.order_by(orderCol)
 
     nbResults = q.count()
-    data = q.limit(limit).offset(page * limit).all()
+    data = q.limit(limit).offset(offset).all()
+
     results = []
     for row in data:
         data_as_dict = {}
diff --git a/apptax/taxonomie/routesbibnoms.py b/apptax/taxonomie/routesbibnoms.py
index 553b7149..8910cf4b 100644
--- a/apptax/taxonomie/routesbibnoms.py
+++ b/apptax/taxonomie/routesbibnoms.py
@@ -1,10 +1,11 @@
 # coding: utf8
 import logging
 
-from flask import json, Blueprint, request
+from flask import json, Blueprint, request, current_app
 from sqlalchemy import func
 
 from ..utils.utilssqlalchemy import json_resp
+from ..utils.genericfunctions import calculate_offset_page
 from ..log import logmanager
 from .models import (
     BibNoms,
@@ -12,10 +13,9 @@
     CorTaxonAttribut,
     BibThemes,
     CorNomListe,
-    TMedias,
     BibAttributs,
 )
-
+from .repositories import MediaRepository
 from pypnusershub import routes as fnauth
 from . import db
 
@@ -23,6 +23,9 @@
 logger = logging.getLogger()
 
 
+media_repo = MediaRepository(db.session, current_app.config["S3_BUCKET_NAME"])
+
+
 @adresses.route("/", methods=["GET"])
 @json_resp
 def get_bibtaxons():
@@ -37,9 +40,12 @@ def get_bibtaxons():
     )
 
     nbResultsWithoutFilter = q.count()
+
     # Traitement des parametres
-    limit = int(parameters.get("limit")) if parameters.get("limit") else 100
-    page = int(parameters.get("page")) - 1 if parameters.get("page") else 0
+    limit = parameters.get("limit", 20, int)
+    page = parameters.get("page", 1, int)
+    offset = parameters.get("offset", 0, int)
+    (limit, offset, page) = calculate_offset_page(limit, offset, page)
 
     # Order by
     if "orderby" in parameters:
@@ -81,7 +87,7 @@ def get_bibtaxons():
             )
 
     nbResults = q.count()
-    data = q.limit(limit).offset(page * limit).all()
+    data = q.limit(limit).offset(offset).all()
     results = []
     for row in data:
         data_as_dict = row.BibNoms.as_dict()
@@ -111,8 +117,6 @@ def getOne_bibtaxonsInfo(cd_nom):
         - id_attribut(integer): id_attribut
                 (Possibilité de passer plusiers id_attribut)
     """
-    params = dict(request.args)
-
     # Récupération du cd_ref à partir du cd_nom
     cd_ref = db.session.query(Taxref.cd_ref).filter_by(cd_nom=cd_nom).first()
     obj = {}
@@ -121,17 +125,17 @@ def getOne_bibtaxonsInfo(cd_nom):
     obj["attributs"] = []
     q = db.session.query(CorTaxonAttribut).filter_by(cd_ref=cd_ref)
     join_on_bib_attr = False
-    if "id_theme" in params:
+    if "id_theme" in request.args.keys() :
         q = q.join(
             BibAttributs, BibAttributs.id_attribut == CorTaxonAttribut.id_attribut
-        ).filter(BibAttributs.id_theme.in_(params["id_theme"]))
+        ).filter(BibAttributs.id_theme.in_( request.args.getlist("id_theme") ))
         join_on_bib_attr = True
-    if "id_attribut" in params:
+    if "id_attribut" in request.args.keys() :
         if not join_on_bib_attr:
             q = q.join(
                 BibAttributs, BibAttributs.id_attribut == CorTaxonAttribut.id_attribut
             )
-        q = q.filter(BibAttributs.id_attribut.in_(params["id_attribut"]))
+        q = q.filter(BibAttributs.id_attribut.in_( request.args.getlist("id_attribut") ))
     bibAttr = q.all()
     for attr in bibAttr:
         o = dict(attr.as_dict().items())
@@ -140,16 +144,13 @@ def getOne_bibtaxonsInfo(cd_nom):
         theme = db.session.query(BibThemes).filter_by(id_theme=id).first()
         o["nom_theme"] = theme.as_dict()["nom_theme"]
         obj["attributs"].append(o)
-
     # Ajout des medias
-    medias = db.session.query(TMedias).filter_by(cd_ref=cd_ref).all()
-    obj["medias"] = []
-    for medium in medias:
-        o = dict(medium.as_dict().items())
-        o.update(dict(medium.types.as_dict().items()))
-        obj["medias"].append(o)
-    return obj
+    obj["medias"] = media_repo.get_and_format_media_filter_by(
+        filters={"cd_ref": cd_ref},
+        force_path=request.args.get('forcePath', False)
+    )
 
+    return obj
 
 @adresses.route("/simple/<int:id_nom>", methods=["GET"])
 @json_resp
diff --git a/apptax/utils/genericfunctions.py b/apptax/utils/genericfunctions.py
new file mode 100644
index 00000000..f7b57048
--- /dev/null
+++ b/apptax/utils/genericfunctions.py
@@ -0,0 +1,24 @@
+'''
+    Fichier contenant des fonctions utilisées
+    par l'ensemble de l'application
+'''
+
+
+def calculate_offset_page(limit, offset, page):
+    """
+        fonction qui calcul les paramètres
+            offset et page
+        Si un offset est défini
+            il prend le pas sur le paramètre page
+        Le paramètre page est seulement indicatif, il commence à 1 et ne peut être < 1
+        Le offset commence à 0 et ne peut pas être négatif
+    """
+    if offset:
+        if offset < 0:
+            offset = 0
+        page = int(offset / limit)
+        return (limit, offset, page)
+    else:
+        page = 1 if page < 1  else page
+        offset = (page-1) * limit
+        return (limit, offset, page)
diff --git a/apptax/utils/routesconfig.py b/apptax/utils/routesconfig.py
index 74a253b2..5621eb5c 100644
--- a/apptax/utils/routesconfig.py
+++ b/apptax/utils/routesconfig.py
@@ -11,7 +11,8 @@
 
 from ..database import db
 
-@adresses.route("/", methods=["GET"])
+
+@adresses.route("", methods=["GET"])
 @json_resp
 def get_config(id=None):
     """
@@ -19,7 +20,7 @@ def get_config(id=None):
     """
 
     data = db.session.query(Application).filter_by(code_application='TH').first()
-    
+
     return {
         "id_application": data.id_application
     }
\ No newline at end of file
diff --git a/apptax/utils/utilssqlalchemy.py b/apptax/utils/utilssqlalchemy.py
index 338e93ec..93ca880b 100644
--- a/apptax/utils/utilssqlalchemy.py
+++ b/apptax/utils/utilssqlalchemy.py
@@ -2,6 +2,7 @@
 '''
 Fonctions utilitaires
 '''
+import collections
 from flask import jsonify,  Response, current_app
 import json
 from functools import wraps
@@ -9,6 +10,7 @@
 from sqlalchemy import Table, create_engine, MetaData
 from werkzeug.datastructures import Headers
 
+
 from . import db
 
 
@@ -121,3 +123,19 @@ def _csv_resp(*args, **kwargs):
         out = '\r\n'.join(outdata)
         return Response(out, headers=headers)
     return _csv_resp
+
+def dict_merge(dct, merge_dct):
+    """ Recursive dict merge. Inspired by :meth:``dict.update()``, instead of
+    updating only top-level keys, dict_merge recurses down into dicts nested
+    to an arbitrary depth, updating keys. The ``merge_dct`` is merged into
+    ``dct``.
+    :param dct: dict onto which the merge is executed
+    :param merge_dct: dct merged into dct
+    :return: None
+    """
+    for k, v in merge_dct.items():
+        if (k in dct and isinstance(dct[k], dict)
+                and isinstance(merge_dct[k], collections.Mapping)):
+            dict_merge(dct[k], merge_dct[k])
+        else:
+            dct[k] = merge_dct[k]
\ No newline at end of file
diff --git a/config.py.sample b/config.py.sample
index e0317395..d209c898 100644
--- a/config.py.sample
+++ b/config.py.sample
@@ -21,6 +21,15 @@ import os
 BASE_DIR = os.path.abspath(os.path.dirname(__file__))
 UPLOAD_FOLDER = 'static/medias'
 
+#S3
+S3_BUCKET_NAME = ''
+S3_KEY = ''
+S3_SECRET = ''
+S3_ENDPOINT = 'https://s3.gra.cloud.ovh.net/' #URL pour l'api S3 (ex : https://s3.gra.cloud.ovh.net/ )
+S3_PUBLIC_URL = '' #URL publique qui permet au client d'accéder aux médias (stocker dans la BDD) (ex : media.mon_instance_geonature.fr)
+S3_FOLDER = 'taxons/' # dossier ou sont stocker les media de cette applicarion (ex : taxons ou taxhub)
+S3_REGION_NAME = '' #région (ex : gra )
+
 # Authentification crypting method (hash or md5)
 PASS_METHOD='hash'
 
diff --git a/data/inpn/data_inpn_taxhub.sql b/data/inpn/data_inpn_taxhub.sql
index 7394de0a..8ea5a1af 100644
--- a/data/inpn/data_inpn_taxhub.sql
+++ b/data/inpn/data_inpn_taxhub.sql
@@ -25,71 +25,10 @@ INSERT INTO bib_taxref_habitats (id_habitat, nom_habitat, desc_habitat) VALUES (
 -- Dependencies: 243
 -- Data for Name: bib_taxref_rangs; Type: TABLE DATA; Schema: taxonomie; Owner: geonatuser
 --
-
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('Dumm', 'Domaine', 1);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SPRG', 'Super-Règne', 2);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('KD  ', 'Règne', 3);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SSRG', 'Sous-Règne', 4);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('IFRG', 'Infra-Règne', 5);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('PH  ', 'Embranchement', 6);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SBPH', 'Sous-Phylum', 7);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('IFPH', 'Infra-Phylum', 8);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('DV  ', 'Division', 9);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SBDV', 'Sous-division', 10);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SPCL', 'Super-Classe', 11);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('CLAD', 'Cladus', 12);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('CL  ', 'Classe', 13);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SBCL', 'Sous-Classe', 14);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('IFCL', 'Infra-classe', 15);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('LEG ', 'Legio', 16);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SPOR', 'Super-Ordre', 17);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('COH ', 'Cohorte', 18);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('OR  ', 'Ordre', 19);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SBOR', 'Sous-Ordre', 20);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('IFOR', 'Infra-Ordre', 21);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SPFM', 'Super-Famille', 22);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('FM  ', 'Famille', 23);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SBFM', 'Sous-Famille', 24);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('PVCL', 'Parv-Classe', 25);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('TR  ', 'Tribu', 26);
-
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SSTR', 'Sous-Tribu', 27);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('GN  ', 'Genre', 28);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SSGN', 'Sous-Genre', 29);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SC  ', 'Section', 30);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SBSC', 'Sous-Section', 31);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SER', 'Série', 32);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SSER', 'Sous-Série', 33);
-
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('AGES', 'Agrégat', 34);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('ES  ', 'Espèce', 35);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SMES', 'Semi-espèce', 36);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('MES ', 'Micro-Espèce',37);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SSES', 'Sous-espèce', 38);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('NAT ', 'Natio', 39);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('VAR ', 'Variété', 40);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SVAR ', 'Sous-Variété', 41);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('FO  ', 'Forme', 42);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SSFO', 'Sous-Forme', 43);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('FOES', 'Forma species', 44);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('LIN ', 'Linea', 45);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('CLO ', 'Clône', 46);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('RACE', 'Race', 47);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('CAR ', 'Cultivar', 48);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('MO  ', 'Morpha', 49);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('AB  ', 'Abberatio',50);
---n'existe plus dans taxref V9
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang) VALUES ('CVAR', 'Convariété');
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang) VALUES ('HYB ', 'Hybride');
---non documenté dans la doc taxref
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang, tri_rang) VALUES ('SPTR', 'Supra-Tribu', 25);
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang) VALUES ('SCO ', '?');
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang) VALUES ('PVOR', '?');
-
-INSERT INTO bib_taxref_rangs (id_rang, nom_rang) VALUES ('SSCO', '?');
-
-
-
+COPY bib_taxref_rangs(tri_rang, id_rang, nom_rang, nom_rang_en)
+FROM  '/tmp/taxhub/TAXREF_v14_2020/rangs_note.csv'
+WITH  CSV HEADER
+DELIMITER E'\t'  encoding 'UTF-8';
 
 --
 -- TOC entry 3272 (class 0 OID 17765)
@@ -145,7 +84,7 @@ COPY import_taxref (regne, phylum, classe, ordre, famille, sous_famille, tribu,
        lb_auteur, nom_complet, nom_complet_html, nom_valide, nom_vern,
        nom_vern_eng, habitat, fr, gf, mar, gua, sm, sb, spm, may, epa,
        reu, sa, ta, taaf, pf, nc, wf, cli, url)
-FROM  '/tmp/taxhub/TAXREF_INPN_v13/TAXREFv13.txt'
+FROM  '/tmp/taxhub/TAXREF_v14_2020/TAXREFv14.txt'
 WITH  CSV HEADER
 DELIMITER E'\t'  encoding 'UTF-8';
 
@@ -236,14 +175,13 @@ TRUNCATE TABLE import_taxref;
 	--WHERE type_protection = 'Protection'
 --);
 
-
 ------------------------------------------------------
 ------------------------------------------------------
 --- INSERTION DES NOUVEAUX STATUTS BDC Statut
 ------------------------------------------------------
 ------------------------------------------------------
-DROP TABLE  IF EXISTS taxonomie.taxref_bdc_statut_type ;
-CREATE TABLE taxonomie.taxref_bdc_statut_type (
+DROP TABLE  IF EXISTS taxonomie.bdc_statut_type ;
+CREATE TABLE taxonomie.bdc_statut_type (
     cd_type_statut varchar(50) PRIMARY KEY,
     lb_type_statut varchar(250),
     regroupement_type varchar(250),
@@ -251,8 +189,8 @@ CREATE TABLE taxonomie.taxref_bdc_statut_type (
     type_value varchar(100)
 );
 
-DROP TABLE  IF EXISTS taxonomie.taxref_bdc_statut ;
-CREATE TABLE taxonomie.taxref_bdc_statut (
+DROP TABLE  IF EXISTS taxonomie.bdc_statut ;
+CREATE TABLE taxonomie.bdc_statut (
     cd_nom int NOT NULL,
     cd_ref int NOT NULL,
     cd_sup int,
@@ -287,12 +225,38 @@ CREATE TABLE taxonomie.taxref_bdc_statut (
 
 -- COPY DATA
 
-COPY taxonomie.taxref_bdc_statut_type
-FROM  '/tmp/taxhub/BDC_STATUTS_TYPES_13.csv'
+COPY taxonomie.bdc_statut_type
+FROM  '/tmp/taxhub/BDC-Statuts-v14/BDC_STATUTS_TYPES_14.csv'
 WITH  CSV HEADER;
 
 
-COPY taxonomie.taxref_bdc_statut
-FROM  '/tmp/taxhub/BDC_STATUTS_13.csv'
+COPY taxonomie.bdc_statut
+FROM  '/tmp/taxhub/BDC-Statuts-v14/BDC_STATUTS_14.csv'
 WITH  CSV HEADER
-  ENCODING 'ISO 8859-1';
+    ENCODING 'ISO 8859-1';
+
+ALTER TABLE taxonomie.bdc_statut ADD id serial;
+
+
+--- Suppression des données en double contenu dans la table  bdc_statut
+CREATE INDEX bdc_statut_id_idx ON taxonomie.bdc_statut (id);
+
+WITH d AS (
+    SELECT
+        count(*), min(id), array_agg(id)
+    FROM taxonomie.bdc_statut
+    GROUP BY
+        cd_nom, cd_ref, cd_sup, cd_type_statut, lb_type_statut, regroupement_type, code_statut, label_statut, rq_statut,
+        cd_sig, cd_doc, lb_nom, lb_auteur, nom_complet_html, nom_valide_html, regne, phylum, classe, ordre, famille, group1_inpn,
+        group2_inpn, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2, full_citation, doc_url, thematique, type_value
+    HAVING count(*) >1
+) , id_doublon AS (
+    SELECT min, unnest(array_agg) as to_del
+    FROM d
+)
+DELETE
+FROM  taxonomie.bdc_statut s
+USING id_doublon d
+WHERE s.id = d.to_del and not id = min;
+
+DROP INDEX taxonomie.bdc_statut_id_idx;
\ No newline at end of file
diff --git a/data/scripts/import_inpn_media/README.rst b/data/scripts/import_inpn_media/README.rst
index 6c1339ca..c85d6917 100644
--- a/data/scripts/import_inpn_media/README.rst
+++ b/data/scripts/import_inpn_media/README.rst
@@ -1,30 +1,40 @@
 
-Exemple d'utilisation de la fonctionnalité importer médias depuis INPN
+Script d'import des médias de l'INPN
+====================================
+
+| Exemple d'utilisation de la fonctionnalité importer des médias depuis l'INPN. 
+| Ressources : `Connecter TaxHub à wikidata (ou l'INPN) pour en récupérer les médias <https://github.com/PnX-SI/TaxHub/issues/150>`_ 
+
 
 Configuration
-=============
+-------------
 
-Modifier le fichier ``config.py``, en le créant à partir du fichier ``config.py.sample``
+Modifier le fichier ``config.py``, en le créant à partir du fichier ``config.py.sample`` : ``cp config.py.sample config.py``
 
 ``SQLALCHEMY_DATABASE_URI`` = Chaine de connexion à la base de données
 
 ``QUERY_SELECT_CDREF`` = Requete SQL permettant de sélectionner les cd_ref
 
 
-Usage
-=====
+Installation
+------------
 
-* Créer environnement Python :
+* Dans un terminal, se placer dans le dossier du script puis créer l'environnement virtuel Python 3 :
 
 ::
    
    virtualenv -p /usr/bin/python3 venv
+   # Alternative : python3 -m venv venv
    source venv/bin/activate
    pip install psycopg2
    pip install requests
    deactivate
 
-* Lancer le script :
+
+Usage
+-----
+
+Lancer le script :
 
 ::
    
@@ -32,7 +42,41 @@ Usage
    python import_inpn_media.py
    deactivate
 
-Librairies requises (à installer via pip dans un virtualenv de préférence) :
+Le script ajoute toutes les photos en tant que "Photo secondaire" (*id_type = 2*).
+
+L'API de l'INPN ne permet pas encore (2020-09-01) d'obtenir les votes 
+effectués sur les images sur le site de l'INPN. Cette information pourrait 
+servir à sélectionner un photo principale (*id_type = 1*).
+
+Si vous souhaitez malgré tout ajouter une photo principale vous pouvez en sélectionner 
+une aléatoirement (ici le plus petit ``id_media`` pour chaque ``cd_ref``) :
+
+.. code-block:: sql
+
+   WITH first_media AS (
+      SELECT MIN(id_media) AS first_id_media_founded, cd_ref 
+      FROM taxonomie.t_medias
+      GROUP BY cd_ref
+   )
+   UPDATE taxonomie.t_medias AS tm 
+      SET id_type = 1
+      FROM first_media AS fm
+      WHERE tm.id_media = fm.first_id_media_founded
+         AND tm.cd_ref = fm.cd_ref ;
+
+Une fois l'import des médias effectué, et à fin de **rendre visible les photos sur GeoNature-atlas**, 
+il est nécessaire de rafraichir les données de ses vues matérialisées *atlas.vm_medias* et *atlas.vm_taxons_plus_observes* : 
+
+::
+
+   REFRESH MATERIALIZED VIEW atlas.vm_medias WITH DATA ; 
+   REFRESH MATERIALIZED VIEW atlas.vm_taxons_plus_observes WITH DATA ;
+
+
+Dépendances
+-----------
+
+Ce script nécessite Python 3 et les bibliothèques suivantes (à installer via pip dans un virtualenv de préférence) :
 
 - psycopg2
 - requests
diff --git a/data/scripts/import_inpn_media/import_inpn_media.py b/data/scripts/import_inpn_media/import_inpn_media.py
index c074fba5..5959c05b 100644
--- a/data/scripts/import_inpn_media/import_inpn_media.py
+++ b/data/scripts/import_inpn_media/import_inpn_media.py
@@ -1,6 +1,4 @@
 # coding: utf8
-
-
 '''
     Script permettant l'import de médias récupérés via l'API inpn
 '''
@@ -10,7 +8,7 @@
 
 import config
 
-# ################
+
 # CONSTANTES
 API_URL = "https://taxref.mnhn.fr/api/taxa/{}/media"
 
@@ -42,24 +40,24 @@ class Media():
 
     def __init__(
         self, 
-        cd_ref, titre, auteur, desc_media, licence,
+        cd_ref, titre, nom, auteur, desc_media, licence,
         url
     ):
         self.cd_ref = cd_ref
         self.titre = titre
+        self.nom = nom
         self.auteur = auteur
         self.desc_media = desc_media
         self.licence = licence
         self.url = url
 
     def __repr__(self):
-        return "cd_ref: {}, titre: {}, url:{}".format(
-            self.cd_ref, self.titre, self.url
+        return "Nom: {}, Media: {}".format(
+            self.nom, self.url
         )
 
-# ################
-# FONCTIONS
 
+# FONCTIONS
 def runquery(cursor, sql, params, trap=False):
     '''
         Fonction permettant d'executer une requete
@@ -80,18 +78,17 @@ def runquery(cursor, sql, params, trap=False):
 
 def process_media(cur, cd_ref, media):
     '''
-        Fonction qui gère l'enregistrement du media
-        dans la base
+        Fonction qui gère l'enregistrement du media dans la base
     '''
     m_obj = Media(
         cd_ref, 
         titre = media['taxon']['referenceNameHtml'], 
+        nom = media['taxon']['scientificName'], 
         auteur =  media['copyright'],
         desc_media = media['title'],
         licence  = media['licence'],
         url = media['_links']['file']['href']
     )
-    print(m_obj)
 
     # Test si média existe déjà en base
     runquery(
@@ -103,8 +100,8 @@ def process_media(cur, cd_ref, media):
 
     if nb_r[0][0] > 0:
         # Mise à jour au cas ou les données
-        #       licence/légende/copyright aient changées
-        print('update')
+        # licence/légende/copyright aient changées
+        print(f'\t{m_obj}, Action : UPDATE')
         runquery(
             cur,
             QUERY_UPDATE_TMEDIA,
@@ -120,8 +117,8 @@ def process_media(cur, cd_ref, media):
             True
         )
     else:
-        # Si le média n'éxiste pas insertion en base
-        print('insert')
+        # Si le média n'existe pas insertion en base
+        print(f'\t{m_obj}, Action : INSERT')
         runquery(
             cur,
             QUERY_INSERT_TMEDIA,
@@ -136,12 +133,11 @@ def process_media(cur, cd_ref, media):
     DB_CONNEXION.commit()
 
 
-# ################
 # SCRIPT
 try:
     DB_CONNEXION = psycopg2.connect(config.SQLALCHEMY_DATABASE_URI)
 except Exception as exp:
-    print("Connexion à la base impossible")
+    print("ERREUR : connexion à la base impossible !")
     quit()
 
 try:
@@ -149,7 +145,7 @@ def process_media(cur, cd_ref, media):
     rows = runquery(cursor, config.QUERY_SELECT_CDREF, None, False)
     rows = cursor.fetchall()
 except Exception as exp:
-    print("Problème lors de la récupération de la liste des cd_ref")
+    print("ERREUR : problème lors de la récupération de la liste des cd_ref !")
     quit()
 
 
@@ -158,15 +154,19 @@ def process_media(cur, cd_ref, media):
     url = API_URL.format(cd_ref[0])
     r = requests.get(url)
     
+    if r.status_code != 200:
+        print(f"\tERREUR : l'URL {url} retourne le code HTTP {r.status_code} !")
+        continue
+
     if '_embedded' in r.json():
         medias = r.json()['_embedded']['media']
         if not medias:
-            print('    no media')
+            print('\tERREUR : aucun media !')
 
         for media in medias:
             if media['taxon']['referenceId'] == cd_ref[0]: 
                 process_media(cursor, cd_ref[0], media)
             else:
-                print ("    media non pris en compte car pas sur le bon taxon ",  media['taxon']['id'])
+                print(f"\tERREUR : media non pris en compte car pas sur le bon taxon {media['taxon']['id']} !")
 
 DB_CONNEXION.close()
diff --git a/data/scripts/update_taxref/README.rst b/data/scripts/update_taxref/README.rst
index 509e14bc..7e123926 100644
--- a/data/scripts/update_taxref/README.rst
+++ b/data/scripts/update_taxref/README.rst
@@ -3,7 +3,7 @@ Update Taxref
 
 Scripts de migration permettant de mettre à jour une version de Taxref à une autre.
 
-Avant de commencer : 
+Avant de commencer :
 
 * La migration d'une version de Taxref est une opération conséquente. Ce script permet d'automatiser au maximum les opérations, mais certaines parties reviennent à l'administrateur de données et il est important de comprendre les différentes étapes.
 * Il est important aussi de faire une sauvegarde avant de réaliser ces opérations et de faire des tests et vérifications des données au fur et à mesure et à la fin des opérations.
@@ -34,7 +34,7 @@ Analysez les logs (dans ``taxhub/var/log/updatetaxrefv13/``) ainsi que les fichi
 
 Toutes ces opérations peuvent être regroupés dans un fichier SQL exécuté dans le script suivant.
 
-**2. apply_changes.sh 13** : Application des modifications dues au changement de Taxref. 
+**2. apply_changes.sh 14** : Application des modifications dues au changement de Taxref.
 
 Le script ne peut s'exécuter entièrement que s'il n'y a plus de conflits. Le script vous indiquera les éventuelles corrections restant à faire. Les différents fichiers CSV du dossier ``/tmp`` seront mis à jour par ce script, ainsi qu'un fichier complémentaire ``liste_donnees_cd_nom_manquant.csv``. Lancer le script avec la commande ``./apply_changes.sh 13``.
 
@@ -54,11 +54,13 @@ Après correction des données d'observation (Occtax, Synthèse...), vous pourre
 * Répercussion dans la table ``taxonomie.cor_nom_liste`` des cd_noms remplacés et supprimés
 * Mise à jour des cd_ref de ``taxonomie.bib_noms`` en fonction des cd_noms, suppression des noms disparus, ajout des noms de références manquants
 * Répercussion des évolutions de Taxref sur les tables ``taxonomie.t_medias`` et ``taxonomie.cor_taxon_attribut`` en fonction des cas et actions définis dans la table ``tmp_taxref_changes.comp_grap``
-* Import de la BDC statuts de l'INPN v13 brute, pas encore utilisée au niveau applicatif
+* Import de la BDC statuts de l'INPN v14
+* Traitement de la BDC statuts et structuration
+.. image:: images/bdc_statut.png
 
 **3. clean_db.sh** : Suppression des tables résiduelles
 
-Les logs de ces scripts sont disponibles dans le répertoire ``montaxhub/var/log/updatetaxrefv13``.
+Les logs de ces scripts sont disponibles dans le répertoire ``montaxhub/var/log/updatetaxrefvv14``.
 
 .. image:: images/update-taxref-cas-1.jpg
 
diff --git a/data/scripts/update_taxref/apply_changes.sh b/data/scripts/update_taxref/apply_changes.sh
index 83178d01..73f826e6 100755
--- a/data/scripts/update_taxref/apply_changes.sh
+++ b/data/scripts/update_taxref/apply_changes.sh
@@ -2,7 +2,7 @@
 
 . ../../../settings.ini
 
-taxref_version="${1:-13}"
+taxref_version="${1:-14}"
 
 LOG_DIR="../../../var/log/updatetaxrefv${taxref_version}"
 mkdir -p $LOG_DIR
@@ -37,7 +37,7 @@ if [ $countfloatingcd_nom -gt 0 ]
 then
     echo "Il y a $countfloatingcd_nom données ayant un cd_nom qui a disparu de taxref"
     echo "Plus de détail dans le fichier /tmp/liste_donnees_cd_nom_manquant.csv"
-    sudo -n -u postgres -s psql -d $db_name -c "COPY tmp_taxref_changes.dps_fk_cd_nom TO '/tmp/liste_donnees_cd_nom_manquant.csv' DELIMITER ',' CSV HEADER;" &>> $LOG_DIR/apply_changes.log
+    sudo -u postgres -s psql -d $db_name -c "COPY tmp_taxref_changes.dps_fk_cd_nom TO '/tmp/liste_donnees_cd_nom_manquant.csv' DELIMITER ',' CSV HEADER;" &>> $LOG_DIR/apply_changes.log
     exit;
 fi
 
@@ -50,33 +50,67 @@ fi
 echo "Detection conflits synthese si elle existe"
 
 
+countfloatingcd_nom=`export PGPASSWORD=$user_pg_pass;psql -X -A -h $db_host -U $user_pg -d $db_name -t \
+    -c "SELECT count(*) FROM (
+            SELECT id_attribut , count(DISTINCT valeur_attribut), array_agg(DISTINCT valeur_attribut) ,  array_agg(DISTINCT i_cd_ref) , f_cd_ref--, *
+            FROM taxonomie.cor_taxon_attribut a
+            JOIN tmp_taxref_changes.comp_grap c
+            ON a.cd_ref = c.i_cd_ref OR a.cd_ref = c.f_cd_ref
+            WHERE action ilike 'Loose attributes and medium now attach to No one'
+            GROUP BY f_cd_ref, id_attribut
+            HAVING count(DISTINCT valeur_attribut) >1
+            ORDER BY C.f_cd_ref  , id_attribut
+        )a ;
+"`
+
+if [ $countfloatingcd_nom -gt 0 ]
+then
+    echo "Detection conflits éventuel de now attach to No one"
+    echo "Il y a $countfloatingcd_nom données ayant un cd_nom qui a disparu de taxref"
+    echo "Plus de détail dans le fichier /tmp/liste_donnees_cd_nom_manquant.csv"
+    sudo -u postgres -s psql -d $db_name -c "COPY SELECT id_attribut , count(DISTINCT valeur_attribut), array_agg(DISTINCT valeur_attribut) ,  array_agg(DISTINCT i_cd_ref) , f_cd_ref--, *
+            FROM taxonomie.cor_taxon_attribut a
+            JOIN tmp_taxref_changes.comp_grap c
+            ON a.cd_ref = c.i_cd_ref OR a.cd_ref = c.f_cd_ref
+            WHERE action ilike 'Loose attributes and medium now attach to No one'
+            GROUP BY f_cd_ref, id_attribut
+            HAVING count(DISTINCT valeur_attribut) >1
+            ORDER BY C.f_cd_ref  , id_attribut  TO '/tmp/conflit_modification_attributs.csv' DELIMITER ',' CSV HEADER;" &>> $LOG_DIR/apply_changes.log
+    exit;
+fi
+
+
 sudo -u postgres -s psql -d $db_name  -f scripts/1.3_taxref_changes_detections_export.sql &>> $LOG_DIR/apply_changes.log
 echo "Export des bilans réalisés dans tmp"
 
 
 
 echo "Import taxref v${taxref_version}"
-export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/3.1_taxref_change_db_structure.sql &>> $LOG_DIR/apply_changes.log
 export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/3.2_alter_taxref_data.sql &>> $LOG_DIR/apply_changes.log
 
 
 # TODO gestion des nouveaux status de protection
 echo "Mise à jour des statuts de protections"
-if [ ${taxref_version} -eq 13 ];
+if [ ${taxref_version} -eq 14 ];
 then
-    sudo -u postgres -s psql -v MYPGUSER=$user_pg -d $db_name  -f scripts/4.1_stpr_import_data_v13_raw_data.sql &>> $LOG_DIR/apply_changes.log
+    sudo -u postgres -s psql -v MYPGUSER=$user_pg -d $db_name  -f scripts/4.1_stpr_import_data_v14_raw_data.sql &>> $LOG_DIR/apply_changes.log
+    export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/4.4_format_bdc_status.sql &>> $LOG_DIR/apply_changes.log
     #TODO : spliter en deux fichiers un exécuté par postgres et l'autre par geonatadmin
+elif [ ${taxref_version} -eq 13 ];
+then
+    sudo -u postgres -s psql -v MYPGUSER=$user_pg -d $db_name  -f scripts/4.1_stpr_import_data_v13_raw_data.sql &>> $LOG_DIR/apply_changes.log
+
 else
    sudo -u postgres -s psql -d $db_name  -f scripts/4.1_stpr_import_data.sql &>> $LOG_DIR/apply_changes.log
 fi
 
 
-file_name="scripts/4.2_stpr_update_concerne_mon_territoire.sql"
-if test -e "$file_name";then
-    echo "  MAJ données concernant mon territoire"
-    export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f $file_name &>> $LOG_DIR/apply_changes.log
-fi
-echo "  Mise à jour des statuts de protections réalisée"
+# file_name="scripts/4.2_stpr_update_concerne_mon_territoire.sql"
+# if test -e "$file_name";then
+#     echo "  MAJ données concernant mon territoire"
+#     export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f $file_name &>> $LOG_DIR/apply_changes.log
+# fi
+# echo "  Mise à jour des statuts de protections réalisée"
 
 file_name="scripts/4.3_restore_local_constraints.sql"
 if test -e "$file_name";then
diff --git a/data/scripts/update_taxref/clean_db.sh b/data/scripts/update_taxref/clean_db.sh
index 01930e42..13703177 100755
--- a/data/scripts/update_taxref/clean_db.sh
+++ b/data/scripts/update_taxref/clean_db.sh
@@ -2,7 +2,9 @@
 
 . ../../../settings.ini
 
+taxref_version="${1:-14}"
 
-LOG_DIR="../../../var/log/updatetaxrefv11"
+LOG_DIR="../../../var/log/updatetaxrefv${taxref_version}"
+mkdir -p $LOG_DIR
 
 export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/5_clean_db.sql &>> $LOG_DIR/clean_db.log
diff --git a/data/scripts/update_taxref/images/bdc_statut.png b/data/scripts/update_taxref/images/bdc_statut.png
new file mode 100644
index 00000000..5151ef88
Binary files /dev/null and b/data/scripts/update_taxref/images/bdc_statut.png differ
diff --git a/data/scripts/update_taxref/import_taxref_v14.sh b/data/scripts/update_taxref/import_taxref_v14.sh
new file mode 100755
index 00000000..7b439e9f
--- /dev/null
+++ b/data/scripts/update_taxref/import_taxref_v14.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+. ../../../settings.ini
+
+START_RED="\033[0;31m"
+START_ORANGE="\033[0;33m"
+START_GREEN="\033[0;32m"
+NC="\033[0m"
+
+mkdir -p /tmp/taxhub
+sudo chown -R "$(id -u)" /tmp/taxhub
+
+
+LOG_DIR="../../../var/log/updatetaxrefv14"
+
+mkdir -p $LOG_DIR
+
+# Test si aucune donnée de bib_noms n'a de cd_ref null
+countemptycd_ref=`export PGPASSWORD=$user_pg_pass;psql -X -A -h $db_host -U $user_pg -d $db_name -t -c "SELECT count(*) FROM taxonomie.bib_noms WHERE cd_ref IS NULL;"`
+
+if [ $countemptycd_ref -gt 0 ]
+then
+    echo "Il y a $countemptycd_ref donnée(s) n'ayant aucun cd_ref dans la table bib_noms"
+    exit;
+fi
+
+echo "Import des données de taxref v14"
+
+echo "Import des données de taxref v14" > $LOG_DIR/update_taxref_v14.log
+
+array=( TAXREF_v14_2020.zip BDC-Statuts-v14.zip)
+for i in "${array[@]}"
+do
+    if [ ! -f '/tmp/taxhub/'$i ]
+    then
+        wget http://geonature.fr/data/inpn/taxonomie/$i -P /tmp/taxhub
+    else
+        echo $i exists
+    fi
+    if [ ! -f '/tmp/taxhub/'$i ]
+    then
+        printf "${START_RED}Echec de la récupération des fichiers de données taxref ${NC}\n"
+        exit
+    fi
+    unzip -o /tmp/taxhub/$i -d /tmp/taxhub &>> $LOG_DIR/update_taxref_v14.log
+done
+
+sudo -n -u postgres -s psql -d $db_name -c 'CREATE EXTENSION IF NOT EXISTS intarray;' &>> $LOG_DIR/update_taxref_v14.log
+
+
+
+# echo "Import taxref v14"
+export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/0.1.1_prepare_import_taxrefv14.sql   &>> $LOG_DIR/update_taxref_v14.log
+sudo -u postgres -s psql -d $db_name  -f scripts/0.1.2_import_taxrefv14.sql   &>> $LOG_DIR/update_taxref_v14.log
+
+echo "Export des cd_nom à modifier dans les données d'observations vers /tmp"
+sudo -u postgres -s psql -d $db_name  -f scripts/0.1.3_cd_nom_disparu_synthese_export.sql &>> $LOG_DIR/update_taxref_v14.log
+
+echo "Traitement bib_noms disparus"
+export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/0.2.1_correction_cd_nom_disparus.sql &>> $LOG_DIR/update_taxref_v14.log
+sudo -n -u postgres -s psql -d $db_name -f scripts/0.2.2_correction_cd_nom_disparus.sql &>> $LOG_DIR/update_taxref_v14.log
+
+
+echo "Detection des changements"
+export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/1.1_taxref_changes_detections.sql &>> $LOG_DIR/update_taxref_v14.log
+export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name  -f scripts/1.2_taxref_changes_detections_cas_actions.sql &>> $LOG_DIR/update_taxref_v14.log
+sudo -n -u postgres -s psql -d $db_name  -f scripts/1.3_taxref_changes_detections_export.sql &>> $LOG_DIR/update_taxref_v14.log
+
+printf "${START_GREEN}Export des bilans réalisés vers /tmp ${NC}"
+echo ""
+printf "${START_ORANGE}La clé primaire fk_bib_nom_taxref a été supprimée. Si vous abandonnez la migration en cours, par exemple après cette étape, vous pouvez la réactiver en exécutant le script sql suivant :${NC}\n"
+printf "${START_GREEN}ALTER TABLE taxonomie.bib_noms ADD CONSTRAINT fk_bib_nom_taxref FOREIGN KEY (cd_nom) REFERENCES taxonomie.taxref(cd_nom);${NC}\n"
+echo ""
+printf "${START_ORANGE}Visualisation des logs des opérations dans le fichier $LOG_DIR/update_taxref_v14.log : ${NC}\n"
diff --git a/data/scripts/update_taxref/scripts/0.1.1_prepare_import_taxrefv14.sql b/data/scripts/update_taxref/scripts/0.1.1_prepare_import_taxrefv14.sql
new file mode 100755
index 00000000..ac90766e
--- /dev/null
+++ b/data/scripts/update_taxref/scripts/0.1.1_prepare_import_taxrefv14.sql
@@ -0,0 +1,76 @@
+--Sauvegarder l'ancienne table taxref 13
+DROP TABLE IF EXISTS taxonomie.taxref_v13;
+CREATE TABLE taxonomie.taxref_v13 AS
+SELECT *
+FROM taxonomie.taxref;
+
+-- Créer la table import_taxref
+DROP TABLE IF EXISTS taxonomie.import_taxref;
+CREATE TABLE taxonomie.import_taxref
+(
+    regne character varying(20),
+    phylum character varying(50),
+    classe character varying(50),
+    ordre character varying(50),
+    famille character varying(50),
+    SOUS_FAMILLE character varying(50),
+    TRIBU character varying(50),
+    group1_inpn character varying(50),
+    group2_inpn character varying(50),
+    cd_nom integer NOT NULL,
+    cd_taxsup integer,
+    cd_sup integer,
+    cd_ref integer,
+    rang character varying(10),
+    lb_nom character varying(100),
+    lb_auteur character varying(500),
+    nom_complet character varying(500),
+    nom_complet_html character varying(500),
+    nom_valide character varying(500),
+    nom_vern text,
+    nom_vern_eng character varying(500),
+    habitat character varying(10),
+    fr character varying(10),
+    gf character varying(10),
+    mar character varying(10),
+    gua character varying(10),
+    sm character varying(10),
+    sb character varying(10),
+    spm character varying(10),
+    may character varying(10),
+    epa character varying(10),
+    reu character varying(10),
+    SA character varying(10),
+    TA character varying(10),
+    taaf character varying(10),
+    pf character varying(10),
+    nc character varying(10),
+    wf character varying(10),
+    cli character varying(10),
+    url text
+);
+
+
+-- Créer la table cdnom_disparus
+DROP TABLE IF EXISTS taxonomie.cdnom_disparu;
+CREATE TABLE taxonomie.cdnom_disparu (
+    CD_NOM	int,
+    PLUS_RECENTE_DIFFUSION character varying(50),
+    CD_NOM_REMPLACEMENT	int,
+    CD_RAISON_SUPPRESSION int,
+    RAISON_SUPPRESSION text
+);
+
+
+DROP TABLE IF EXISTS taxonomie.import_taxref_rangs;
+
+CREATE TABLE taxonomie.import_taxref_rangs (
+	level int NOT NULL,
+	rang varchar(20) NOT NULL,
+	detail_fr varchar(50) NOT NULL,
+  detail_en varchar(50) NOT NULL
+);
+
+
+-- Mise à jour de la structure de taxref
+ALTER TABLE taxonomie.bib_taxref_rangs ADD nom_rang_en character varying(50);
diff --git a/data/scripts/update_taxref/scripts/0.1.2_import_taxrefv14.sql b/data/scripts/update_taxref/scripts/0.1.2_import_taxrefv14.sql
new file mode 100755
index 00000000..262caeae
--- /dev/null
+++ b/data/scripts/update_taxref/scripts/0.1.2_import_taxrefv14.sql
@@ -0,0 +1,21 @@
+-- Importer les données depuis les CSV.
+-- Attention l'utilisateur qui exécute ce script doit être superuser
+BEGIN;
+COPY taxonomie.import_taxref FROM  '/tmp/taxhub/TAXREF_v14_2020/TAXREFv14.txt'
+WITH  CSV HEADER
+DELIMITER E'\t'  encoding 'UTF-8';
+
+COPY taxonomie.cdnom_disparu FROM  '/tmp/taxhub/TAXREF_v14_2020/CDNOM_DISPARUS.csv'
+WITH  CSV HEADER
+DELIMITER E'\t'  encoding 'UTF-8';
+
+COPY taxonomie.taxref_changes FROM  '/tmp/taxhub/TAXREF_v14_2020/TAXREF_CHANGES.txt'
+WITH  CSV HEADER
+DELIMITER E'\t'  encoding 'UTF-8';
+
+
+COPY taxonomie.import_taxref_rangs FROM  '/tmp/taxhub/TAXREF_v14_2020/rangs_note.csv'
+WITH  CSV HEADER
+DELIMITER E'\t'  encoding 'UTF-8';
+
+END;
diff --git a/data/scripts/update_taxref/scripts/2.0_detect_data_with_missing_cd_nom.sql b/data/scripts/update_taxref/scripts/2.0_detect_data_with_missing_cd_nom.sql
index cf144ac6..a538ac89 100644
--- a/data/scripts/update_taxref/scripts/2.0_detect_data_with_missing_cd_nom.sql
+++ b/data/scripts/update_taxref/scripts/2.0_detect_data_with_missing_cd_nom.sql
@@ -1,6 +1,6 @@
 
 -- PATCH à la con sensitivity lynx lynx lynx
-UPDATE gn_sensitivity.t_sensitivity_rules  r SET cd_nom = 60612 WHERE cd_nom = 60615;
+--UPDATE gn_sensitivity.t_sensitivity_rules  r SET cd_nom = 60612 WHERE cd_nom = 60615;
 
 
 
diff --git a/data/scripts/update_taxref/scripts/3.1_taxref_change_db_structure.sql b/data/scripts/update_taxref/scripts/3.1_taxref_change_db_structure_v13.sql
similarity index 98%
rename from data/scripts/update_taxref/scripts/3.1_taxref_change_db_structure.sql
rename to data/scripts/update_taxref/scripts/3.1_taxref_change_db_structure_v13.sql
index 954ff2bd..847c2deb 100755
--- a/data/scripts/update_taxref/scripts/3.1_taxref_change_db_structure.sql
+++ b/data/scripts/update_taxref/scripts/3.1_taxref_change_db_structure_v13.sql
@@ -4,10 +4,10 @@
 -- ######################################################################
 -- ######################################################################
 
-DROP VIEW taxonomie.v_taxref_hierarchie_bibtaxons;
-
 -- MISE A JOUR DES RANGS
 
+
+
 INSERT INTO  taxonomie.bib_taxref_rangs (id_rang, nom_rang)
 SELECT 'PVCL', 'Parv-Classe'
 FROM (
diff --git a/data/scripts/update_taxref/scripts/3.2_alter_taxref_data.sql b/data/scripts/update_taxref/scripts/3.2_alter_taxref_data.sql
index 28af3e26..f0f087ea 100755
--- a/data/scripts/update_taxref/scripts/3.2_alter_taxref_data.sql
+++ b/data/scripts/update_taxref/scripts/3.2_alter_taxref_data.sql
@@ -123,8 +123,8 @@ ALTER TABLE  taxonomie.cor_nom_liste DROP COLUMN tmp_id ;
 
 -- Modification de la clé étrangère
 ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_listes_bib_noms_fkey;
-ALTER TABLE taxonomie.cor_nom_liste 
-  ADD CONSTRAINT cor_nom_listes_bib_noms_fkey FOREIGN KEY (id_nom) 
+ALTER TABLE taxonomie.cor_nom_liste
+  ADD CONSTRAINT cor_nom_listes_bib_noms_fkey FOREIGN KEY (id_nom)
   REFERENCES taxonomie.bib_noms(id_nom)
   ON UPDATE CASCADE
   ON DELETE  CASCADE;
@@ -133,7 +133,7 @@ ALTER TABLE taxonomie.cor_nom_liste
 ------------------
 --- bib_noms
 ------------------
---Mise à jour des cd_ref 
+--Mise à jour des cd_ref
 UPDATE taxonomie.bib_noms n SET cd_ref = t.cd_ref
 FROM taxonomie.taxref t
 WHERE n.cd_nom = t.cd_nom;
@@ -242,17 +242,20 @@ WHERE action ilike '%Merge attributes%'  AND cd_ref = i_cd_ref;
 ALTER TABLE taxonomie.t_medias ENABLE TRIGGER USER;
 
 
+-- Suppression des potentiels doublons puis modification
 -- Suppression des potentiels doublons puis modification
 WITH grp_del AS (
-    SELECT f_cd_ref, id_attribut, count(*), array_agg( DISTINCT i_cd_ref) cd_refs, array_agg( DISTINCT valeur_attribut)
+    SELECT f_cd_ref, id_attribut, count(*), array_agg( DISTINCT i_cd_ref) cd_refs, array_agg( DISTINCT valeur_attribut) AS valeur_attribut
     FROM taxonomie.cor_taxon_attribut ia
     JOIN tmp_taxref_changes.comp_grap cg
-    ON  action ilike '%Merge attributes%' AND cd_ref = i_cd_ref
+    ON  -- action ilike '%Merge attributes%' AND
+    	cd_ref = i_cd_ref
     GROUP BY f_cd_ref, id_attribut
     HAVING count(*) > 1
 ) , del AS (
     SELECT id_attribut as at, unnest(cd_refs[2:])
     FROM grp_del
+    WHERE array_length(valeur_attribut, 1) = 1
 )
 DELETE FROM taxonomie.cor_taxon_attribut
 USING del
@@ -291,3 +294,25 @@ ALTER TABLE taxonomie.bib_noms
 
 ALTER TABLE taxonomie.cor_taxon_attribut
   ADD CONSTRAINT check_is_cd_ref CHECK (cd_ref = taxonomie.find_cdref(cd_ref));
+
+
+
+
+-- MISE A JOUR DES RANGS
+
+
+INSERT INTO taxonomie.bib_taxref_rangs (id_rang, nom_rang, nom_rang_en, tri_rang)
+SELECT rang, detail_fr, detail_en, level
+FROM taxonomie.import_taxref_rangs
+ON CONFLICT (id_rang)
+DO
+   UPDATE SET nom_rang = EXCLUDED.nom_rang,  tri_rang = EXCLUDED.tri_rang, nom_rang_en = EXCLUDED.nom_rang_en;
+
+DELETE FROM taxonomie.bib_taxref_rangs b
+WHERE b.id_rang IN (
+	SELECT b.id_rang
+	FROM taxonomie.bib_taxref_rangs b
+	LEFT JOIN taxonomie.import_taxref_rangs tr
+	ON tr.rang = b.id_rang
+	WHERE tr.rang IS NULL
+);
diff --git a/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v13_raw_data.sql b/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v13_raw_data.sql
index d1f85e9c..b05be04a 100755
--- a/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v13_raw_data.sql
+++ b/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v13_raw_data.sql
@@ -1,5 +1,5 @@
-DROP TABLE  IF EXISTS taxonomie.taxref_bdc_statut_type ;
-CREATE TABLE taxonomie.taxref_bdc_statut_type (
+DROP TABLE  IF EXISTS taxonomie.bdc_statut_type ;
+CREATE TABLE taxonomie.bdc_statut_type (
     cd_type_statut varchar(50) PRIMARY KEY,
     lb_type_statut varchar(250),
     regroupement_type varchar(250),
@@ -7,8 +7,8 @@ CREATE TABLE taxonomie.taxref_bdc_statut_type (
     type_value varchar(50)
 );
 
-DROP TABLE  IF EXISTS taxonomie.taxref_bdc_statut ;
-CREATE TABLE taxonomie.taxref_bdc_statut (
+DROP TABLE  IF EXISTS taxonomie.bdc_statut ;
+CREATE TABLE taxonomie.bdc_statut (
     cd_nom int NOT NULL,
     cd_ref int NOT NULL,
     cd_sup int,
@@ -41,8 +41,8 @@ CREATE TABLE taxonomie.taxref_bdc_statut (
     type_value varchar(50)
 );
 
-ALTER TABLE taxonomie.taxref_bdc_statut_type OWNER TO :MYPGUSER;
-ALTER TABLE taxonomie.taxref_bdc_statut OWNER TO :MYPGUSER;
+ALTER TABLE taxonomie.bdc_statut_type OWNER TO :MYPGUSER;
+ALTER TABLE taxonomie.bdc_statut OWNER TO :MYPGUSER;
 
 ---------------------------
 ---------------------------
@@ -50,12 +50,12 @@ ALTER TABLE taxonomie.taxref_bdc_statut OWNER TO :MYPGUSER;
 ---------------------------
 ---------------------------
 
-COPY taxonomie.taxref_bdc_statut_type
+COPY taxonomie.bdc_statut_type
 FROM  '/tmp/taxhub/BDC_STATUTS_TYPES_13.csv'
 WITH  CSV HEADER;
 
 
-COPY taxonomie.taxref_bdc_statut
+COPY taxonomie.bdc_statut
 FROM  '/tmp/taxhub/BDC_STATUTS_13.csv'
 WITH  CSV HEADER
   ENCODING 'ISO 8859-1';
@@ -67,7 +67,7 @@ WITH  CSV HEADER
 
 -- Suppression des doublons
 -- 773 doublons lorsque l'on test tous les champs
-ALTER TABLE taxonomie.taxref_bdc_statut ADD id serial;
+ALTER TABLE taxonomie.bdc_statut ADD id serial;
 
 
 WITH d AS (
@@ -75,7 +75,7 @@ WITH d AS (
         count(*), min(id), array_agg(id),  cd_nom, cd_ref, cd_sup, cd_type_statut, lb_type_statut, regroupement_type, code_statut, label_statut, rq_statut,
         cd_sig, cd_doc, lb_nom, lb_auteur, nom_complet_html, nom_valide_html, regne, phylum, classe, ordre, famille, group1_inpn,
         group2_inpn, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2, full_citation, doc_url, thematique, type_value
-    FROM taxonomie.taxref_bdc_statut
+    FROM taxonomie.bdc_statut
     GROUP BY
         cd_nom, cd_ref, cd_sup, cd_type_statut, lb_type_statut, regroupement_type, code_statut, label_statut, rq_statut,
         cd_sig, cd_doc, lb_nom, lb_auteur, nom_complet_html, nom_valide_html, regne, phylum, classe, ordre, famille, group1_inpn,
@@ -83,18 +83,18 @@ WITH d AS (
     HAVING count(*) >1
 ) , id_doublon AS (
     SELECT id as id_d
-    FROM taxonomie.taxref_bdc_statut
+    FROM taxonomie.bdc_statut
     JOIN (SELECT min, unnest(array_agg) as to_del FROM d) d
     ON to_del = id
     WHERE NOT id = min
 )
-DELETE FROM  taxonomie.taxref_bdc_statut
+DELETE FROM  taxonomie.bdc_statut
 WHERE id IN (SELECT id_d FROM id_doublon);
 
 
 -- correction cd_sig Deux-Sèvres, Charente-Maritime et Vienne
 -- Reste des cd_sig non associés à des données (INSEERN84, INSEENR32, TER984)
-UPDATE taxonomie.taxref_bdc_statut d SET
+UPDATE taxonomie.bdc_statut d SET
     cd_sig = a.cd_sig,
     lb_adm_tr = a.lb_adm_tr,
     niveau_admin = a.niveau_admin,
@@ -102,14 +102,14 @@ UPDATE taxonomie.taxref_bdc_statut d SET
     cd_iso3166_2 = a.cd_iso3166_2
 FROM (
     SELECT DISTINCT cd_sig, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2
-    FROM taxonomie.taxref_bdc_statut
+    FROM taxonomie.bdc_statut
     WHERE cd_sig IN ('INSEED79',  'INSEED17', 'INSEED86')
 ) a
 WHERE d.cd_sig ilike 'INSEED %' AND a.cd_sig = REPLACE(d.cd_sig, ' ', '');
 
 -- correction cd_sig manquant
 --    UEPOMACEA : Interdiction d’introduction et de propagation dans l’Union le genre Pomacea (Perry)
-UPDATE taxonomie.taxref_bdc_statut d SET
+UPDATE taxonomie.bdc_statut d SET
     cd_sig = 'ETATFRA',
     lb_adm_tr = 'France',
     niveau_admin = 'État'
diff --git a/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v14_raw_data.sql b/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v14_raw_data.sql
new file mode 100755
index 00000000..495cc726
--- /dev/null
+++ b/data/scripts/update_taxref/scripts/4.1_stpr_import_data_v14_raw_data.sql
@@ -0,0 +1,49 @@
+
+TRUNCATE TABLE  taxonomie.bdc_statut_type ;
+TRUNCATE TABLE  taxonomie.bdc_statut ;
+ALTER TABLE taxonomie.bdc_statut DROP id;
+---------------------------
+---------------------------
+-- COPY DATA
+---------------------------
+---------------------------
+
+COPY taxonomie.bdc_statut_type
+FROM  '/tmp/taxhub/BDC-Statuts-v14/BDC_STATUTS_TYPES_14.csv'
+WITH  CSV HEADER;
+
+
+COPY taxonomie.bdc_statut
+FROM  '/tmp/taxhub/BDC-Statuts-v14/BDC_STATUTS_14.csv'
+WITH  CSV HEADER
+    ENCODING 'ISO 8859-1';
+
+---------------------------
+---------------------------
+
+-- Suppression des doublons
+-- 829 doublons lorsque l'on test tous les champs
+ALTER TABLE taxonomie.bdc_statut ADD id serial;
+
+--- Suppression des données en double contenu dans la table  bdc_statut
+CREATE INDEX bdc_statut_id_idx ON taxonomie.bdc_statut (id);
+
+WITH d AS (
+    SELECT
+        count(*), min(id), array_agg(id)
+    FROM taxonomie.bdc_statut
+    GROUP BY
+        cd_nom, cd_ref, cd_sup, cd_type_statut, lb_type_statut, regroupement_type, code_statut, label_statut, rq_statut,
+        cd_sig, cd_doc, lb_nom, lb_auteur, nom_complet_html, nom_valide_html, regne, phylum, classe, ordre, famille, group1_inpn,
+        group2_inpn, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2, full_citation, doc_url, thematique, type_value
+    HAVING count(*) >1
+) , id_doublon AS (
+    SELECT min, unnest(array_agg) as to_del
+    FROM d
+)
+DELETE
+FROM  taxonomie.bdc_statut s
+USING id_doublon d
+WHERE s.id = d.to_del and not id = min;
+
+DROP INDEX taxonomie.bdc_statut_id_idx;
\ No newline at end of file
diff --git a/data/scripts/update_taxref/scripts/4.2_stpr_import_data_v13.sql b/data/scripts/update_taxref/scripts/4.2_stpr_import_data_v13.sql
index a9a09900..44407228 100755
--- a/data/scripts/update_taxref/scripts/4.2_stpr_import_data_v13.sql
+++ b/data/scripts/update_taxref/scripts/4.2_stpr_import_data_v13.sql
@@ -1,5 +1,5 @@
-DROP TABLE  IF EXISTS taxonomie.taxref_bdc_statut_type ;
-CREATE TABLE taxonomie.taxref_bdc_statut_type (
+DROP TABLE  IF EXISTS taxonomie.bdc_statut_type ;
+CREATE TABLE taxonomie.bdc_statut_type (
     cd_type_statut varchar(50) PRIMARY KEY,
     lb_type_statut varchar(250),
     regroupement_type varchar(250),
@@ -7,8 +7,8 @@ CREATE TABLE taxonomie.taxref_bdc_statut_type (
     type_value varchar(50)
 );
 
-DROP TABLE  IF EXISTS taxonomie.taxref_bdc_statut ;
-CREATE TABLE taxonomie.taxref_bdc_statut (
+DROP TABLE  IF EXISTS taxonomie.bdc_statut ;
+CREATE TABLE taxonomie.bdc_statut (
     cd_nom int NOT NULL,
     cd_ref int NOT NULL,
     cd_sup int,
@@ -47,14 +47,14 @@ CREATE TABLE taxonomie.taxref_bdc_statut (
 ---------------------------
 ---------------------------
 
-COPY taxonomie.taxref_bdc_statut_type
+COPY taxonomie.bdc_statut_type
 FROM  '/tmp/taxhub/BDC_STATUTS_TYPES_13.csv'
 WITH  CSV HEADER;
 
 
-COPY taxonomie.taxref_bdc_statut
+COPY taxonomie.bdc_statut
 FROM  '/tmp/taxhub/BDC_STATUTS_13.csv'
-WITH  CSV HEADER 
+WITH  CSV HEADER
   ENCODING 'ISO 8859-1';
 
 ---------------------------
@@ -62,14 +62,14 @@ WITH  CSV HEADER
 
 -- Suppression des doublons
 -- 773 doublons lorsque l'on test tous les champs
-ALTER TABLE taxonomie.taxref_bdc_statut ADD id serial;
+ALTER TABLE taxonomie.bdc_statut ADD id serial;
 
 WITH d AS (
     SELECT
         count(*), min(id), array_agg(id),  cd_nom, cd_ref, cd_sup, cd_type_statut, lb_type_statut, regroupement_type, code_statut, label_statut, rq_statut,
         cd_sig, cd_doc, lb_nom, lb_auteur, nom_complet_html, nom_valide_html, regne, phylum, classe, ordre, famille, group1_inpn,
         group2_inpn, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2, full_citation, doc_url, thematique, type_value
-    FROM taxonomie.taxref_bdc_statut
+    FROM taxonomie.bdc_statut
     GROUP BY
         cd_nom, cd_ref, cd_sup, cd_type_statut, lb_type_statut, regroupement_type, code_statut, label_statut, rq_statut,
         cd_sig, cd_doc, lb_nom, lb_auteur, nom_complet_html, nom_valide_html, regne, phylum, classe, ordre, famille, group1_inpn,
@@ -87,13 +87,13 @@ WITH d AS (
     FROM id_liste
     WHERE NOT id_d = min
 )
-DELETE FROM  taxonomie.taxref_bdc_statut
+DELETE FROM  taxonomie.bdc_statut
 WHERE id IN (SELECT id_d FROM id_doublon);
 
 
 -- correction cd_sig Deux-Sèvres, Charente-Maritime et Vienne
 -- Reste des cd_sig non associés à des données (INSEERN84, INSEENR32, TER984)
-UPDATE taxonomie.taxref_bdc_statut d SET
+UPDATE taxonomie.bdc_statut d SET
     cd_sig = a.cd_sig,
     lb_adm_tr = a.lb_adm_tr,
     niveau_admin = a.niveau_admin,
@@ -101,14 +101,14 @@ UPDATE taxonomie.taxref_bdc_statut d SET
     cd_iso3166_2 = a.cd_iso3166_2
 FROM (
     SELECT DISTINCT cd_sig, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2
-    FROM taxonomie.taxref_bdc_statut
+    FROM taxonomie.bdc_statut
     WHERE cd_sig IN ('INSEED79',  'INSEED17', 'INSEED86')
 ) a
 WHERE d.cd_sig ilike 'INSEED %' AND a.cd_sig = REPLACE(d.cd_sig, ' ', '')
 
 -- correction cd_sig manquant
 --    UEPOMACEA : Interdiction d’introduction et de propagation dans l’Union le genre Pomacea (Perry)
-UPDATE taxonomie.taxref_bdc_statut d SET
+UPDATE taxonomie.bdc_statut d SET
     cd_sig = 'ETATFRA',
     lb_adm_tr = 'France',
     niveau_admin = 'État'
@@ -204,7 +204,7 @@ CREATE TABLE taxonomie.taxref_statut_especes (
 
 INSERT INTO taxonomie.taxref_statut_types (cd_type_statut, lb_type_statut, regroupement_type, thematique, type_value)
 SELECT cd_type_statut, lb_type_statut, regroupement_type, thematique, type_value
-FROM taxonomie.taxref_bdc_statut_type
+FROM taxonomie.bdc_statut_type
 WHERE NOT regroupement_type IN ('SENSIBILITE', 'Statut biogéographique');
 
 
@@ -221,7 +221,7 @@ cd_sig, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2)
 SELECT DISTINCT cd_type_statut AS cd_statut,
 label_statut, NULL , tbs.full_citation, cd_doc, doc_url, SUBSTRING(full_citation, '((19|20)\d{2})') AS date_arrete,  cd_type_statut,
 cd_sig, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2
-FROM taxonomie.taxref_bdc_statut tbs
+FROM taxonomie.bdc_statut tbs
 WHERE regroupement_type IN ('Directives européennes', 'Conventions internationales', 'Protection');
 
 
@@ -232,7 +232,7 @@ CREATE TABLE taxonomie.import_znieff (
 annee int, 	taxo varchar(100),	code	varchar(100), full_citation text
 );
 COPY taxonomie.import_znieff
-FROM  '/tmp/taxhub/taxref_bdc_statut_znieff.csv'
+FROM  '/tmp/taxhub/bdc_statut_znieff.csv'
 WITH  CSV HEADER;
 
 
@@ -242,7 +242,7 @@ cd_sig, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2)
 SELECT DISTINCT cd_type_statut || '_' || SUBSTRING(cd_sig, '.\d+$') || '_' || code AS cd_statut,
 taxo AS label_statut, NULL , tbs.full_citation, cd_doc, doc_url, annee::int AS date_arrete,  cd_type_statut,
 cd_sig, lb_adm_tr, niveau_admin, cd_iso3166_1, cd_iso3166_2
-FROM taxonomie.taxref_bdc_statut tbs
+FROM taxonomie.bdc_statut tbs
 JOIN taxonomie.import_znieff iz
 ON iz.full_citation = tbs.full_citation
 WHERE regroupement_type = 'ZNIEFF';
@@ -250,7 +250,7 @@ WHERE regroupement_type = 'ZNIEFF';
 
 INSERT INTO taxonomie.taxref_statut_especes (cd_nom, cd_statut, cd_value, precisions)
 SELECT DISTINCT cd_nom, cd_statut, 'true', string_agg(tbs.rq_statut, '; ')
-FROM taxonomie.taxref_bdc_statut tbs
+FROM taxonomie.bdc_statut tbs
 JOIN taxonomie.taxref_statut_articles a
 ON a.full_citation  = tbs.full_citation AND a.cd_sig = tbs.cd_sig
 WHERE regroupement_type = 'ZNIEFF'
@@ -264,9 +264,9 @@ GROUP BY cd_nom, cd_statut, TRUE;
 -- selection des statuts de protection d'interets
 
 
-ALTER TABLE taxonomie.taxref_bdc_statut_type ADD enable boolean DEFAULT (false);
+ALTER TABLE taxonomie.bdc_statut_type ADD enable boolean DEFAULT (false);
 
-UPDATE  taxonomie.taxref_bdc_statut_type  SET enable = true
+UPDATE  taxonomie.bdc_statut_type  SET enable = true
 WHERE  (
     regroupement_type  IN ('Protection', 'Conventions internationales', 'Directives européennes')
     OR
@@ -274,7 +274,7 @@ WHERE  (
 );
 
 
-CREATE TABLE taxonomie.taxref_bdc_statut_geo  AS (
+CREATE TABLE taxonomie.bdc_statut_geo  AS (
     cd_sig varchar(50) PRIMARY KEY,
     lb_adm_tr varchar(50),
     niveau_admin varchar(250),
@@ -283,16 +283,16 @@ CREATE TABLE taxonomie.taxref_bdc_statut_geo  AS (
 
 );
 
-INSERT INTO taxonomie.taxref_bdc_statut_geo
+INSERT INTO taxonomie.bdc_statut_geo
 SELECT DISTINCT
     cd_sig,
     lb_adm_tr,
     niveau_admin,
     cd_iso3166_1,
     cd_iso3166_2
-FROM taxonomie.taxref_bdc_statut;
+FROM taxonomie.bdc_statut;
 
-ALTER TABLE taxonomie.taxref_bdc_statut_geo ADD enable boolean DEFAULT (false);
+ALTER TABLE taxonomie.bdc_statut_geo ADD enable boolean DEFAULT (false);
 
 
 
@@ -339,10 +339,10 @@ SELECT
     st.cd_iso3166_1,
     st.cd_iso3166_2
 
-FROM taxonomie.taxref_bdc_statut st
-JOIN taxonomie.taxref_bdc_statut_geo g
+FROM taxonomie.bdc_statut st
+JOIN taxonomie.bdc_statut_geo g
 ON g.cd_sig = st.cd_sig AND g.enable = true
-JOIN taxonomie.taxref_bdc_statut_type t
+JOIN taxonomie.bdc_statut_type t
 ON t.cd_type_statut = st.cd_type_statut AND t.enable = true
 
 
@@ -368,7 +368,7 @@ SELECT DISTINCT
     niveau_admin,
     cd_iso3166_1,
     cd_iso3166_2
-FROM taxonomie.taxref_bdc_statut
+FROM taxonomie.bdc_statut
 WHERE (regroupement_type  IN ('Protection', 'Conventions internationales') OR cd_type_statut IN ('REGL', 'REGLSO'))
     AND (
         cd_sig IN ('TERFXFR', 'ETATFRA')
diff --git a/data/scripts/update_taxref/scripts/4.4_format_bdc_status.sql b/data/scripts/update_taxref/scripts/4.4_format_bdc_status.sql
new file mode 100644
index 00000000..c9dbe970
--- /dev/null
+++ b/data/scripts/update_taxref/scripts/4.4_format_bdc_status.sql
@@ -0,0 +1,178 @@
+
+-- ##############################################
+--------- CREATE TABLE
+-- ##############################################
+DROP TABLE IF EXISTS taxonomie.bdc_statut_cor_text_values CASCADE;
+DROP TABLE IF EXISTS taxonomie.bdc_statut_text CASCADE;
+DROP TABLE IF EXISTS taxonomie.bdc_statut_values ;
+DROP TABLE IF EXISTS taxonomie.bdc_statut_taxons ;
+
+CREATE TABLE taxonomie.bdc_statut_text (
+	id_text serial NOT NULL PRIMARY KEY,
+	cd_st_text  varchar(50),
+	cd_type_statut varchar(50) NOT NULL,
+	cd_sig varchar(50),
+	cd_doc int4,
+	niveau_admin varchar(250),
+	cd_iso3166_1 varchar(50),
+	cd_iso3166_2 varchar(50),
+	lb_adm_tr varchar(250),
+	full_citation text,
+	doc_url TEXT,
+	ENABLE boolean DEFAULT(true)
+);
+
+ALTER TABLE taxonomie.bdc_statut_text
+	ADD CONSTRAINT bdc_statut_text_fkey FOREIGN KEY (cd_type_statut)
+REFERENCES taxonomie.bdc_statut_type(cd_type_statut) ON DELETE CASCADE ON UPDATE CASCADE;
+
+CREATE TABLE taxonomie.bdc_statut_values (
+	id_value serial NOT NULL PRIMARY KEY,
+	code_statut varchar(50) NOT NULL,
+	label_statut varchar(250)
+);
+
+CREATE TABLE taxonomie.bdc_statut_cor_text_values (
+	id_value_text serial NOT NULL PRIMARY KEY,
+	id_value int4 NOT NULL,
+	id_text int4 NOT NULL
+);
+
+ALTER TABLE taxonomie.bdc_statut_cor_text_values
+	ADD CONSTRAINT tbdc_statut_cor_text_values_id_value_fkey FOREIGN KEY (id_value)
+REFERENCES taxonomie.bdc_statut_values(id_value) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE taxonomie.bdc_statut_cor_text_values
+	ADD CONSTRAINT tbdc_statut_cor_text_values_id_text_fkey FOREIGN KEY (id_text)
+REFERENCES taxonomie.bdc_statut_text(id_text) ON DELETE CASCADE ON UPDATE CASCADE;
+
+
+CREATE TABLE taxonomie.bdc_statut_taxons (
+	id int4 NOT NULL PRIMARY KEY,
+	id_value_text int4 NOT NULL,
+	cd_nom int4 NOT NULL,
+	cd_ref int4 NOT NULL, -- TO KEEP?
+	rq_statut varchar(1000)
+);
+
+ALTER TABLE taxonomie.bdc_statut_taxons
+	ADD CONSTRAINT bdc_statut_taxons_id_value_text_fkey FOREIGN KEY (id_value_text)
+REFERENCES taxonomie.bdc_statut_cor_text_values(id_value_text) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE taxonomie.bdc_statut_taxons
+	ADD CONSTRAINT bdc_statut_taxons_cd_nom_fkey FOREIGN KEY (cd_nom)
+REFERENCES taxonomie.taxref(cd_nom) ON DELETE CASCADE ON UPDATE CASCADE;
+
+COMMENT ON TABLE taxonomie.bdc_statut_text IS 'Table contenant les textes et leur zone d''application';
+COMMENT ON TABLE taxonomie.bdc_statut_type IS 'Table des grands type de statuts';
+COMMENT ON TABLE taxonomie.bdc_statut IS 'Table initialement fournie par l''INPN. Contient tout les statuts sous leur forme brute';
+COMMENT ON TABLE taxonomie.bdc_statut_values IS 'Table contenant la liste des valeurs possible pour les textes';
+COMMENT ON TABLE taxonomie.bdc_statut_taxons IS 'Table d''association entre les textes et les taxons';
+COMMENT ON TABLE taxonomie.bdc_statut_cor_text_values IS 'Table d''association entre les textes, les taxons et la valeur';
+
+-- ##############################################"""""
+--------- POPULATE TABLE
+-- ##############################################"""""
+
+-- bdc_statut_text
+ALTER  TABLE taxonomie.bdc_statut_text ADD id int[];
+
+INSERT INTO taxonomie.bdc_statut_text
+(cd_type_statut, cd_sig, cd_doc, niveau_admin, cd_iso3166_1, cd_iso3166_2, lb_adm_tr, full_citation, doc_url,  id)
+SELECT DISTINCT  cd_type_statut,
+	-- code_statut , label_statut ,
+	cd_sig , cd_doc , niveau_admin , cd_iso3166_1 , cd_iso3166_2 , lb_adm_tr,
+	full_citation, doc_url ,
+	array_agg(DISTINCT tbs.id) id
+FROM taxonomie.bdc_statut tbs
+GROUP BY  cd_type_statut,
+	-- code_statut , label_statut ,
+	cd_sig , cd_doc , niveau_admin , cd_iso3166_1 , cd_iso3166_2 , lb_adm_tr,
+	full_citation, doc_url ;
+
+UPDATE taxonomie.bdc_statut_text tbst  SET doc_url = NULL
+WHERE doc_url ='';
+
+-- bdc_statut_values
+ALTER  TABLE taxonomie.bdc_statut_values ADD id int[];
+ALTER  TABLE taxonomie.bdc_statut_values ADD ids_text int[];
+
+INSERT INTO taxonomie.bdc_statut_values (code_statut, label_statut, ids_text, id)
+SELECT DISTINCT tbs.code_statut , label_statut,  array_agg(DISTINCT t.id_text) ids_text,  array_agg(DISTINCT tbs.id) id
+FROM taxonomie.bdc_statut tbs
+JOIN taxonomie.bdc_statut_text t
+ON t.cd_type_statut = tbs.cd_type_statut
+	AND (t.cd_sig = tbs.cd_sig OR  t.cd_sig IS NULL)
+	AND t.full_citation = tbs.full_citation
+GROUP BY  tbs.code_statut , label_statut;
+
+-- bdc_statut_cor_text_values
+INSERT INTO taxonomie.bdc_statut_cor_text_values (id_value, id_text)
+SELECT id_value, unnest(ids_text) AS id_text
+FROM taxonomie.bdc_statut_values ;
+
+-- Mise en correspondances des textes, values et taxon
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_text;
+ALTER TABLE taxonomie.bdc_statut ADD id_text int;
+
+UPDATE taxonomie.bdc_statut s SET  id_text = a.id_text
+FROM (
+	SELECT unnest(id) AS id, id_text
+	FROM  taxonomie.bdc_statut_text
+)a
+WHERE a.id = s.id;
+
+
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value ;
+ALTER TABLE taxonomie.bdc_statut ADD id_value int;
+UPDATE taxonomie.bdc_statut s SET  id_value = a.id_value
+FROM (
+	SELECT unnest(id) AS id, id_value
+	FROM  taxonomie.bdc_statut_values
+)a
+WHERE a.id = s.id;
+
+
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value_text ;
+ALTER TABLE taxonomie.bdc_statut ADD id_value_text int;
+UPDATE taxonomie.bdc_statut s SET  id_value_text = c.id_value_text
+FROM taxonomie.bdc_statut_cor_text_values  c
+WHERE c.id_text = s.id_text AND s.id_value = c.id_value;
+
+-- bdc_statut_taxons
+INSERT INTO taxonomie.bdc_statut_taxons (id, id_value_text, cd_nom, cd_ref, rq_statut)
+SELECT id, id_value_text, t.cd_nom, t.cd_ref, rq_statut
+FROM  taxonomie.bdc_statut s
+JOIN taxonomie.taxref t
+ON s.cd_nom = t.cd_nom; -- Jointure sur taxref car 3 cd_nom n'existent pas : 847285, 973500, 851332
+
+
+-- ##############################################"""""
+--------- CLEAN
+-- ##############################################"""""
+ALTER  TABLE taxonomie.bdc_statut_text DROP id;
+
+ALTER  TABLE taxonomie.bdc_statut_values DROP id;
+ALTER  TABLE taxonomie.bdc_statut_values DROP ids_text;
+
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value_text ;
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value ;
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_text;
+
+
+
+
+CREATE OR REPLACE VIEW taxonomie.v_bdc_status AS
+SELECT s.cd_nom, s.cd_ref, s.rq_statut, v.code_statut , v.label_statut,
+t.cd_type_statut, ty.thematique, ty.lb_type_statut, ty.regroupement_type,cd_st_text, t.cd_sig, t.cd_doc, t.niveau_admin, t.cd_iso3166_1, t.cd_iso3166_2,
+t.full_citation, t.doc_url, ty.type_value
+FROM taxonomie.bdc_statut_taxons s
+JOIN taxonomie.bdc_statut_cor_text_values c
+ON s.id_value_text  = c.id_value_text
+JOIN taxonomie.bdc_statut_text t
+ON t.id_text  = c.id_text
+JOIN taxonomie.bdc_statut_values v
+ON v.id_value = c.id_value
+JOIN taxonomie.bdc_statut_type ty
+ON ty.cd_type_statut = t.cd_type_statut
+WHERE t.ENABLE = true;
\ No newline at end of file
diff --git a/data/scripts/update_taxref/scripts/5_clean_db.sql b/data/scripts/update_taxref/scripts/5_clean_db.sql
index 5fad7e5d..4de64deb 100644
--- a/data/scripts/update_taxref/scripts/5_clean_db.sql
+++ b/data/scripts/update_taxref/scripts/5_clean_db.sql
@@ -2,6 +2,12 @@
 DROP SCHEMA tmp_taxref_changes CASCADE;
 TRUNCATE TABLE taxonomie.import_taxref;
 
-DROP TABLE taxonomie.import_protection_especes;
+DROP TABLE IF EXISTS taxonomie.import_protection_especes;
 
-DROP FUNCTION public.deps_test_fk_dependencies_cd_nom();
\ No newline at end of file
+DROP FUNCTION  IF EXISTS public.deps_test_fk_dependencies_cd_nom();
+
+DROP TABLE IF EXISTS taxonomie.dps_fk_cd_nom;
+
+DROP TABLE IF EXISTS taxonomie.import_taxref_rangs;
+
+DROP TABLE IF EXISTS taxonomie.cdnom_disparu;
\ No newline at end of file
diff --git a/data/taxhubdata.sql b/data/taxhubdata.sql
index e3f27604..bcfbe96c 100644
--- a/data/taxhubdata.sql
+++ b/data/taxhubdata.sql
@@ -13,8 +13,8 @@ INSERT INTO bib_themes (id_theme, nom_theme, desc_theme, ordre, id_droit)
 SELECT setval('taxonomie.bib_themes_id_theme_seq', (SELECT max(id_theme)+1 FROM taxonomie.bib_themes), true);
 
 -- Insertion d'une liste permettant de définir les noms pouvant être saisis dans le module Occtax
-INSERT INTO bib_listes (id_liste, nom_liste,desc_liste,picto)
-    VALUES (100,'Saisie Occtax','Liste des noms dont la saisie est proposée dans le module Occtax','images/pictos/nopicto.gif');
+INSERT INTO bib_listes (id_liste, code_liste,  nom_liste,desc_liste,picto)
+    VALUES (100,'OCCTAX', 'Saisie Occtax','Liste des noms dont la saisie est proposée dans le module Occtax','images/pictos/nopicto.gif');
 
 -- Insertion des types de média associables aux taxons
 INSERT INTO bib_types_media (id_type, nom_type_media, desc_type_media) VALUES (1, 'Photo_principale', 'Photo principale du taxon à utiliser pour les vignettes par exemple');
@@ -38,6 +38,7 @@ SELECT
   t.search_name,
   t.nom_valide,
   t.lb_nom,
+  t.nom_vern,
   t.regne,
   t.group2_inpn
 FROM (
@@ -48,6 +49,7 @@ FROM (
     concat(t_1.lb_nom, ' =  <i> ', t_1.nom_valide, '</i>', ' - [', t_1.id_rang, ' - ', t_1.cd_nom , ']') AS search_name,
     t_1.nom_valide,
     t_1.lb_nom,
+    t_1.nom_vern,
     t_1.regne,
     t_1.group2_inpn
   FROM taxonomie.taxref t_1
@@ -61,6 +63,7 @@ FROM (
     concat(split_part(t_1.nom_vern, ',', 1), ' =  <i> ', t_1.nom_valide, '</i>', ' - [', t_1.id_rang, ' - ', t_1.cd_ref , ']' ) AS search_name,
     t_1.nom_valide,
     t_1.lb_nom,
+    t_1.nom_vern,
     t_1.regne,
     t_1.group2_inpn
   FROM taxonomie.taxref t_1
@@ -80,3 +83,92 @@ CREATE INDEX i_tri_vm_taxref_list_forautocomplete_search_name
   ON vm_taxref_list_forautocomplete
   USING gist
   (search_name  gist_trgm_ops);
+
+-- ##############################################"""""
+--------- BDC statuts
+-- ##############################################"""""
+
+
+--- ### populate
+-- bdc_statut_text
+ALTER  TABLE taxonomie.bdc_statut_text ADD id int[];
+
+INSERT INTO taxonomie.bdc_statut_text
+(cd_type_statut, cd_sig, cd_doc, niveau_admin, cd_iso3166_1, cd_iso3166_2, lb_adm_tr, full_citation, doc_url,  id)
+SELECT DISTINCT  cd_type_statut,
+	-- code_statut , label_statut ,
+	cd_sig , cd_doc , niveau_admin , cd_iso3166_1 , cd_iso3166_2 , lb_adm_tr,
+	full_citation, doc_url ,
+	array_agg(DISTINCT tbs.id) id
+FROM taxonomie.bdc_statut tbs
+GROUP BY  cd_type_statut,
+	-- code_statut , label_statut ,
+	cd_sig , cd_doc , niveau_admin , cd_iso3166_1 , cd_iso3166_2 , lb_adm_tr,
+	full_citation, doc_url ;
+
+UPDATE taxonomie.bdc_statut_text tbst  SET doc_url = NULL
+WHERE doc_url ='';
+
+-- bdc_statut_values
+ALTER  TABLE taxonomie.bdc_statut_values ADD id int[];
+ALTER  TABLE taxonomie.bdc_statut_values ADD ids_text int[];
+
+INSERT INTO taxonomie.bdc_statut_values (code_statut, label_statut, ids_text, id)
+SELECT DISTINCT tbs.code_statut , label_statut,  array_agg(DISTINCT t.id_text) ids_text,  array_agg(DISTINCT tbs.id) id
+FROM taxonomie.bdc_statut tbs
+JOIN taxonomie.bdc_statut_text t
+ON t.cd_type_statut = tbs.cd_type_statut
+	AND (t.cd_sig = tbs.cd_sig OR  t.cd_sig IS NULL)
+	AND t.full_citation = tbs.full_citation
+GROUP BY  tbs.code_statut , label_statut;
+
+-- bdc_statut_cor_text_values
+INSERT INTO taxonomie.bdc_statut_cor_text_values (id_value, id_text)
+SELECT id_value, unnest(ids_text) AS id_text
+FROM taxonomie.bdc_statut_values ;
+
+-- Mise en correspondances des textes, values et taxon
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_text;
+ALTER TABLE taxonomie.bdc_statut ADD id_text int;
+
+UPDATE taxonomie.bdc_statut s SET  id_text = a.id_text
+FROM (
+	SELECT unnest(id) AS id, id_text
+	FROM  taxonomie.bdc_statut_text
+)a
+WHERE a.id = s.id;
+
+
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value ;
+ALTER TABLE taxonomie.bdc_statut ADD id_value int;
+UPDATE taxonomie.bdc_statut s SET  id_value = a.id_value
+FROM (
+	SELECT unnest(id) AS id, id_value
+	FROM  taxonomie.bdc_statut_values
+)a
+WHERE a.id = s.id;
+
+
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value_text ;
+ALTER TABLE taxonomie.bdc_statut ADD id_value_text int;
+UPDATE taxonomie.bdc_statut s SET  id_value_text = c.id_value_text
+FROM taxonomie.bdc_statut_cor_text_values  c
+WHERE c.id_text = s.id_text AND s.id_value = c.id_value;
+
+-- bdc_statut_taxons
+INSERT INTO taxonomie.bdc_statut_taxons (id, id_value_text, cd_nom, cd_ref, rq_statut)
+SELECT id, id_value_text, t.cd_nom, t.cd_ref, rq_statut
+FROM  taxonomie.bdc_statut s
+JOIN taxonomie.taxref t
+ON s.cd_nom = t.cd_nom; -- Jointure sur taxref car 3 cd_nom n'existent pas : 847285, 973500, 851332
+
+
+--- ### populate
+ALTER  TABLE taxonomie.bdc_statut_text DROP id;
+
+ALTER  TABLE taxonomie.bdc_statut_values DROP id;
+ALTER  TABLE taxonomie.bdc_statut_values DROP ids_text;
+
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value_text ;
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_value ;
+ALTER TABLE taxonomie.bdc_statut DROP IF EXISTS id_text;
diff --git a/data/taxhubdb.sql b/data/taxhubdb.sql
index b3b39eba..fd782009 100644
--- a/data/taxhubdb.sql
+++ b/data/taxhubdb.sql
@@ -202,9 +202,9 @@ $BODY$
   BEGIN
       RETURN QUERY
       WITH RECURSIVE descendants AS (
-        SELECT tx1.cd_nom, tx1.cd_ref FROM taxonomie.taxref tx1 WHERE tx1.cd_taxsup = id
+        SELECT tx1.cd_nom, tx1.cd_ref FROM taxonomie.taxref tx1 WHERE tx1.cd_sup = id
       UNION ALL
-      SELECT tx2.cd_nom, tx2.cd_ref FROM descendants d JOIN taxonomie.taxref tx2 ON tx2.cd_taxsup = d.cd_nom
+      SELECT tx2.cd_nom, tx2.cd_ref FROM descendants d JOIN taxonomie.taxref tx2 ON tx2.cd_sup = d.cd_nom
       )
       SELECT * FROM descendants;
 
@@ -224,9 +224,9 @@ $BODY$
   BEGIN
       RETURN QUERY
       WITH RECURSIVE descendants AS (
-        SELECT tx1.cd_nom, tx1.cd_ref FROM taxonomie.taxref tx1 WHERE tx1.cd_taxsup = ANY(ids)
+        SELECT tx1.cd_nom, tx1.cd_ref FROM taxonomie.taxref tx1 WHERE tx1.cd_sup = ANY(ids)
       UNION ALL
-      SELECT tx2.cd_nom, tx2.cd_ref FROM descendants d JOIN taxonomie.taxref tx2 ON tx2.cd_taxsup = d.cd_nom
+      SELECT tx2.cd_nom, tx2.cd_ref FROM descendants d JOIN taxonomie.taxref tx2 ON tx2.cd_sup = d.cd_nom
       )
       SELECT * FROM descendants;
 
@@ -266,6 +266,7 @@ CREATE TABLE bib_attributs (
 
 CREATE TABLE bib_listes (
     id_liste integer NOT NULL,
+    code_liste character varying(50) NOT NULL,
     nom_liste character varying(255) NOT NULL ,
     desc_liste text,
     picto character varying(50) NOT NULL DEFAULT 'images/pictos/nopicto.gif',
@@ -277,6 +278,17 @@ COMMENT ON COLUMN bib_listes.picto IS 'Indique le chemin vers l''image du picto
 ALTER TABLE taxonomie.bib_listes
   ADD CONSTRAINT unique_bib_listes_nom_liste UNIQUE (nom_liste);
 
+ALTER TABLE taxonomie.bib_listes
+  ADD CONSTRAINT unique_bib_listes_code_liste UNIQUE (code_liste);
+
+CREATE SEQUENCE bib_listes_id_liste_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+ALTER SEQUENCE bib_listes_id_liste_seq OWNED BY bib_listes.id_liste;
+
 
 CREATE TABLE bib_noms (
     id_nom integer NOT NULL,
@@ -311,7 +323,8 @@ CREATE TABLE bib_taxref_habitats (
 
 CREATE TABLE bib_taxref_rangs (
     id_rang character(4) NOT NULL,
-    nom_rang character varying(20) NOT NULL,
+    nom_rang character varying(50) NOT NULL,
+    nom_rang_en character varying(50) NOT NULL,
     tri_rang integer
 );
 
@@ -534,6 +547,42 @@ WITH (
   OIDS=FALSE
 );
 
+CREATE TABLE taxonomie.bdc_statut_text (
+	id_text serial NOT NULL PRIMARY KEY,
+	cd_st_text  varchar(50),
+	cd_type_statut varchar(50) NOT NULL,
+	cd_sig varchar(50),
+	cd_doc int4,
+	niveau_admin varchar(250),
+	cd_iso3166_1 varchar(50),
+	cd_iso3166_2 varchar(50),
+	lb_adm_tr varchar(250),
+	full_citation text,
+	doc_url TEXT,
+	ENABLE boolean DEFAULT(true)
+);
+
+CREATE TABLE taxonomie.bdc_statut_values (
+	id_value serial NOT NULL PRIMARY KEY,
+	code_statut varchar(50) NOT NULL,
+	label_statut varchar(250)
+);
+
+CREATE TABLE taxonomie.bdc_statut_cor_text_values (
+	id_value_text serial NOT NULL PRIMARY KEY,
+	id_value int4 NOT NULL,
+	id_text int4 NOT NULL
+);
+
+
+CREATE TABLE taxonomie.bdc_statut_taxons (
+	id int4 NOT NULL PRIMARY KEY,
+	id_value_text int4 NOT NULL,
+	cd_nom int4 NOT NULL,
+	cd_ref int4 NOT NULL, -- TO KEEP?
+	rq_statut varchar(1000)
+);
+
 ALTER TABLE ONLY bib_noms ALTER COLUMN id_nom SET DEFAULT nextval('bib_noms_id_nom_seq'::regclass);
 
 ALTER TABLE ONLY bib_themes ALTER COLUMN id_theme SET DEFAULT nextval('bib_themes_id_theme_seq'::regclass);
@@ -544,6 +593,13 @@ ALTER TABLE ONLY bib_noms
     ADD CONSTRAINT bib_noms_cd_nom_key UNIQUE (cd_nom);
 
 
+COMMENT ON TABLE taxonomie.bdc_statut_text IS 'Table contenant les textes et leur zone d''application';
+COMMENT ON TABLE taxonomie.bdc_statut_type IS 'Table des grands type de statuts';
+COMMENT ON TABLE taxonomie.bdc_statut IS 'Table initialement fournie par l''INPN. Contient tout les statuts sous leur forme brute';
+COMMENT ON TABLE taxonomie.bdc_statut_values IS 'Table contenant la liste des valeurs possible pour les textes';
+COMMENT ON TABLE taxonomie.bdc_statut_taxons IS 'Table d''association entre les textes et les taxons';
+COMMENT ON TABLE taxonomie.bdc_statut_cor_text_values IS 'Table d''association entre les textes, les taxons et la valeur';
+
 ----------------
 --PRIMARY KEYS--
 ----------------
@@ -621,6 +677,8 @@ CREATE INDEX i_fk_taxref_bib_taxref_statuts ON taxref USING btree (id_statut);
 
 CREATE INDEX i_taxref_cd_ref ON taxref USING btree (cd_ref);
 
+CREATE INDEX i_taxref_cd_sup ON taxref USING btree (cd_sup);
+
 CREATE INDEX i_taxref_hierarchy ON taxref USING btree (regne, phylum, classe, ordre, famille);
 
 CREATE INDEX i_fk_taxref_group1_inpn ON taxref USING btree (group1_inpn);
@@ -682,6 +740,26 @@ ALTER TABLE bib_themes
   ADD CONSTRAINT is_valid_id_droit_theme CHECK (id_droit >= 0 AND id_droit <= 6);
 
 
+ALTER TABLE taxonomie.bdc_statut_text
+	ADD CONSTRAINT bdc_statut_text_fkey FOREIGN KEY (cd_type_statut)
+REFERENCES taxonomie.bdc_statut_type(cd_type_statut) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE taxonomie.bdc_statut_cor_text_values
+	ADD CONSTRAINT tbdc_statut_cor_text_values_id_value_fkey FOREIGN KEY (id_value)
+REFERENCES taxonomie.bdc_statut_values(id_value) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE taxonomie.bdc_statut_cor_text_values
+	ADD CONSTRAINT tbdc_statut_cor_text_values_id_text_fkey FOREIGN KEY (id_text)
+REFERENCES taxonomie.bdc_statut_text(id_text) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE taxonomie.bdc_statut_taxons
+	ADD CONSTRAINT bdc_statut_taxons_id_value_text_fkey FOREIGN KEY (id_value_text)
+REFERENCES taxonomie.bdc_statut_cor_text_values(id_value_text) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE taxonomie.bdc_statut_taxons
+	ADD CONSTRAINT bdc_statut_taxons_cd_nom_fkey FOREIGN KEY (cd_nom)
+REFERENCES taxonomie.taxref(cd_nom) ON DELETE CASCADE ON UPDATE CASCADE;
+
 ------------
 --TRIGGERS--
 ------------
diff --git a/data/update1.6.5to1.7.0.sql b/data/update1.6.5to1.7.0.sql
index 296c3f22..020f0a2e 100644
--- a/data/update1.6.5to1.7.0.sql
+++ b/data/update1.6.5to1.7.0.sql
@@ -68,3 +68,57 @@ CREATE INDEX i_tri_vm_taxref_list_forautocomplete_search_name
 -- et des fonctions triggers
 DROP FUNCTION taxonomie.trg_fct_refresh_nomfrancais_mv_taxref_list_forautocomplete();
 DROP FUNCTION taxonomie.trg_fct_refresh_mv_taxref_list_forautocomplete();
+
+-- Modification d'un trigger sur les medias
+DROP TRIGGER tri_unique_type1 ON taxonomie.t_medias;
+CREATE TRIGGER tri_unique_type1
+  AFTER INSERT OR UPDATE
+  ON taxonomie.t_medias
+  FOR EACH ROW
+  EXECUTE PROCEDURE taxonomie.unique_type1();
+
+
+-- Modification des fonction find_all_taxons_children :
+---     utilisation de cd_sup au lieu de cd_taxsup pour prendre
+--        en compte des rangs intermediares
+CREATE OR REPLACE FUNCTION taxonomie.find_all_taxons_children(id integer)
+  RETURNS TABLE (cd_nom int, cd_ref int) AS
+$BODY$
+ --Param : cd_nom ou cd_ref d'un taxon quelque soit son rang
+ --Retourne le cd_nom de tous les taxons enfants sous forme d'un jeu de données utilisable comme une table
+ --Usage SELECT taxonomie.find_all_taxons_children(197047);
+ --ou SELECT * FROM atlas.vm_taxons WHERE cd_ref IN(SELECT * FROM taxonomie.find_all_taxons_children(197047))
+  BEGIN
+      RETURN QUERY
+      WITH RECURSIVE descendants AS (
+        SELECT tx1.cd_nom, tx1.cd_ref FROM taxonomie.taxref tx1 WHERE tx1.cd_sup = id
+      UNION ALL
+      SELECT tx2.cd_nom, tx2.cd_ref FROM descendants d JOIN taxonomie.taxref tx2 ON tx2.cd_sup = d.cd_nom
+      )
+      SELECT * FROM descendants;
+
+  END;
+$BODY$
+  LANGUAGE plpgsql IMMUTABLE;
+
+
+
+CREATE OR REPLACE FUNCTION taxonomie.find_all_taxons_children(IN ids integer[])
+  RETURNS TABLE(cd_nom integer, cd_ref integer) AS
+$BODY$
+ --Param : cd_nom ou cd_ref d'un taxon quelque soit son rang
+ --Retourne le cd_nom de tous les taxons enfants sous forme d'un jeu de données utilisable comme une table
+ --Usage SELECT taxonomie.find_all_taxons_children(197047);
+ --ou SELECT * FROM atlas.vm_taxons WHERE cd_ref IN(SELECT * FROM taxonomie.find_all_taxons_children(197047))
+  BEGIN
+      RETURN QUERY
+      WITH RECURSIVE descendants AS (
+        SELECT tx1.cd_nom, tx1.cd_ref FROM taxonomie.taxref tx1 WHERE tx1.cd_sup = ANY(ids)
+      UNION ALL
+      SELECT tx2.cd_nom, tx2.cd_ref FROM descendants d JOIN taxonomie.taxref tx2 ON tx2.cd_sup = d.cd_nom
+      )
+      SELECT * FROM descendants;
+
+  END;
+$BODY$
+  LANGUAGE plpgsql IMMUTABLE;
diff --git a/data/update1.7.1to1.7.2.sql b/data/update1.7.1to1.7.2.sql
new file mode 100644
index 00000000..a78a761f
--- /dev/null
+++ b/data/update1.7.1to1.7.2.sql
@@ -0,0 +1,58 @@
+
+-- Creation d'une vue matérialis&ée de tous les noms de Taxref mis en forme pour la recherche de taxons
+--    avec le nom vernaculaire
+DROP MATERIALIZED VIEW IF EXISTS taxonomie.vm_taxref_list_forautocomplete;
+
+CREATE MATERIALIZED VIEW taxonomie.vm_taxref_list_forautocomplete AS
+SELECT
+  row_number() OVER() as gid,
+  t.cd_nom,
+  t.cd_ref,
+  t.search_name,
+  t.nom_valide,
+  t.lb_nom,
+  t.nom_vern,
+  t.regne,
+  t.group2_inpn
+FROM (
+  -- PARTIE NOM SCIENTIFIQUE : ici on prend TOUS les synonymes.
+  SELECT
+    t_1.cd_nom,
+    t_1.cd_ref,
+    concat(t_1.lb_nom, ' =  <i> ', t_1.nom_valide, '</i>', ' - [', t_1.id_rang, ' - ', t_1.cd_nom , ']') AS search_name,
+    t_1.nom_valide,
+    t_1.lb_nom,
+    t_1.nom_vern,
+    t_1.regne,
+    t_1.group2_inpn
+  FROM taxonomie.taxref t_1
+  UNION
+  -- PARTIE NOM FRANCAIS : ici on prend une seule fois (DISTINCT) dans Taxref tous les taxons de références
+  -- On ne prend pas les taxons qui n'ont pas de nom vern dans taxref,
+  -- donc si un taxon n'a pas de nom vern dans Taxref, il n'est accessible que par son nom scientifique.
+  SELECT DISTINCT
+    t_1.cd_nom,
+    t_1.cd_ref,
+    concat(split_part(t_1.nom_vern, ',', 1), ' =  <i> ', t_1.nom_valide, '</i>', ' - [', t_1.id_rang, ' - ', t_1.cd_ref , ']' ) AS search_name,
+    t_1.nom_valide,
+    t_1.lb_nom,
+    t_1.nom_vern,
+    t_1.regne,
+    t_1.group2_inpn
+  FROM taxonomie.taxref t_1
+  WHERE t_1.nom_vern IS NOT null and t_1.cd_nom = t_1.cd_ref
+) t;
+COMMENT ON MATERIALIZED VIEW taxonomie.vm_taxref_list_forautocomplete
+    IS 'Vue matérialisée permettant de faire des autocomplete construite à partir d''une requete sur tout taxref.';
+
+-- Creation des index de la table vm_taxref_list_forautocomplete
+CREATE unique index i_vm_taxref_list_forautocomplete_gid
+  ON taxonomie.vm_taxref_list_forautocomplete (gid);
+CREATE INDEX i_vm_taxref_list_forautocomplete_cd_nom
+  ON taxonomie.vm_taxref_list_forautocomplete (cd_nom ASC NULLS LAST);
+CREATE INDEX i_vm_taxref_list_forautocomplete_search_name
+  ON taxonomie.vm_taxref_list_forautocomplete (search_name ASC NULLS LAST);
+CREATE INDEX i_tri_vm_taxref_list_forautocomplete_search_name
+  ON taxonomie.vm_taxref_list_forautocomplete
+  USING gist
+  (search_name  gist_trgm_ops);
diff --git a/data/update1.7.3to1.7.4.sql b/data/update1.7.3to1.7.4.sql
new file mode 100644
index 00000000..eb4b779f
--- /dev/null
+++ b/data/update1.7.3to1.7.4.sql
@@ -0,0 +1,30 @@
+-- Ajout d'une colonne code_liste
+--      et création d'un séquence pour la colonne id_liste
+ALTER TABLE taxonomie.bib_listes
+  ADD COLUMN code_liste character varying(50);
+
+UPDATE  taxonomie.bib_listes SET code_liste = id_liste::varchar;
+
+ALTER TABLE taxonomie.bib_listes ALTER COLUMN code_liste SET NOT NULL;
+
+ALTER TABLE taxonomie.bib_listes
+  ADD CONSTRAINT unique_bib_listes_code_liste UNIQUE (code_liste);
+
+
+CREATE SEQUENCE taxonomie.bib_listes_id_liste_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+ALTER SEQUENCE taxonomie.bib_listes_id_liste_seq OWNED BY taxonomie.bib_listes.id_liste;
+
+SELECT setval('taxonomie.bib_listes_id_liste_seq', (SELECT max(id_liste) FROM taxonomie.bib_listes), true);
+
+ALTER TABLE taxonomie.bib_listes ALTER COLUMN id_liste SET DEFAULT nextval('taxonomie.bib_listes_id_liste_seq');
+
+-- Ajout index sur colonne cd_sup pour les recherches de taxons fils
+CREATE INDEX i_taxref_cd_sup
+  ON taxonomie.taxref
+  USING btree
+  (cd_sup);
\ No newline at end of file
diff --git a/docs/api/Makefile b/docs/api/Makefile
new file mode 100644
index 00000000..d0c3cbf1
--- /dev/null
+++ b/docs/api/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = source
+BUILDDIR      = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/api/make.bat b/docs/api/make.bat
new file mode 100644
index 00000000..6247f7e2
--- /dev/null
+++ b/docs/api/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs/api/source/apptax.log.rst b/docs/api/source/apptax.log.rst
new file mode 100644
index 00000000..cde68383
--- /dev/null
+++ b/docs/api/source/apptax.log.rst
@@ -0,0 +1,29 @@
+apptax.log package
+==================
+
+Submodules
+----------
+
+apptax.log.logmanager module
+----------------------------
+
+.. automodule:: apptax.log.logmanager
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.log.models module
+------------------------
+
+.. automodule:: apptax.log.models
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: apptax.log
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/apptax.rst b/docs/api/source/apptax.rst
new file mode 100644
index 00000000..9c3daa21
--- /dev/null
+++ b/docs/api/source/apptax.rst
@@ -0,0 +1,39 @@
+apptax package
+==============
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   apptax.log
+   apptax.taxonomie
+   apptax.utils
+
+Submodules
+----------
+
+apptax.database module
+----------------------
+
+.. automodule:: apptax.database
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.index module
+-------------------
+
+.. automodule:: apptax.index
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: apptax
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/apptax.taxonomie.rst b/docs/api/source/apptax.taxonomie.rst
new file mode 100644
index 00000000..6d751f84
--- /dev/null
+++ b/docs/api/source/apptax.taxonomie.rst
@@ -0,0 +1,77 @@
+apptax.taxonomie package
+========================
+
+Submodules
+----------
+
+apptax.taxonomie.filemanager module
+-----------------------------------
+
+.. automodule:: apptax.taxonomie.filemanager
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.models module
+------------------------------
+
+.. automodule:: apptax.taxonomie.models
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.routesbibattributs module
+------------------------------------------
+
+.. automodule:: apptax.taxonomie.routesbibattributs
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.routesbiblistes module
+---------------------------------------
+
+.. automodule:: apptax.taxonomie.routesbiblistes
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.routesbibnoms module
+-------------------------------------
+
+.. automodule:: apptax.taxonomie.routesbibnoms
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.routesbibtypesmedia module
+-------------------------------------------
+
+.. automodule:: apptax.taxonomie.routesbibtypesmedia
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.routestaxref module
+------------------------------------
+
+.. automodule:: apptax.taxonomie.routestaxref
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.taxonomie.routestmedias module
+-------------------------------------
+
+.. automodule:: apptax.taxonomie.routestmedias
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: apptax.taxonomie
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/apptax.utils.rst b/docs/api/source/apptax.utils.rst
new file mode 100644
index 00000000..d987fee1
--- /dev/null
+++ b/docs/api/source/apptax.utils.rst
@@ -0,0 +1,45 @@
+apptax.utils package
+====================
+
+Submodules
+----------
+
+apptax.utils.genericfunctions module
+------------------------------------
+
+.. automodule:: apptax.utils.genericfunctions
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.utils.genericmodels module
+---------------------------------
+
+.. automodule:: apptax.utils.genericmodels
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.utils.routesconfig module
+--------------------------------
+
+.. automodule:: apptax.utils.routesconfig
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+apptax.utils.utilssqlalchemy module
+-----------------------------------
+
+.. automodule:: apptax.utils.utilssqlalchemy
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: apptax.utils
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/conf.py b/docs/api/source/conf.py
new file mode 100644
index 00000000..bd23dd04
--- /dev/null
+++ b/docs/api/source/conf.py
@@ -0,0 +1,67 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../../..'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'TaxHub'
+copyright = '2020, GeoNature contributors'
+author = 'GeoNature contributors'
+
+# The full version, including alpha/beta/rc tags
+release = '1.7.4.dev0'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.viewcode",
+    "sphinxcontrib.httpdomain",
+    "sphinxcontrib.autohttp.flask",
+    "sphinxcontrib.autohttp.flaskqref",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
diff --git a/docs/api/source/index.rst b/docs/api/source/index.rst
new file mode 100644
index 00000000..2b42da46
--- /dev/null
+++ b/docs/api/source/index.rst
@@ -0,0 +1,20 @@
+.. TaxHub documentation master file, created by
+   sphinx-quickstart on Tue Oct  6 15:43:23 2020.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to TaxHub's documentation!
+==================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/api/source/modules.rst b/docs/api/source/modules.rst
new file mode 100644
index 00000000..5ac61f8f
--- /dev/null
+++ b/docs/api/source/modules.rst
@@ -0,0 +1,7 @@
+taxhub
+======
+
+.. toctree::
+   :maxdepth: 4
+
+   taxhub
diff --git a/docs/api/source/taxhub.apptax.log.rst b/docs/api/source/taxhub.apptax.log.rst
new file mode 100644
index 00000000..84e5fc02
--- /dev/null
+++ b/docs/api/source/taxhub.apptax.log.rst
@@ -0,0 +1,29 @@
+taxhub.apptax.log package
+=========================
+
+Submodules
+----------
+
+taxhub.apptax.log.logmanager module
+-----------------------------------
+
+.. automodule:: taxhub.apptax.log.logmanager
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.log.models module
+-------------------------------
+
+.. automodule:: taxhub.apptax.log.models
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: taxhub.apptax.log
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/taxhub.apptax.rst b/docs/api/source/taxhub.apptax.rst
new file mode 100644
index 00000000..01ad9d52
--- /dev/null
+++ b/docs/api/source/taxhub.apptax.rst
@@ -0,0 +1,39 @@
+taxhub.apptax package
+=====================
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   taxhub.apptax.log
+   taxhub.apptax.taxonomie
+   taxhub.apptax.utils
+
+Submodules
+----------
+
+taxhub.apptax.database module
+-----------------------------
+
+.. automodule:: taxhub.apptax.database
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.index module
+--------------------------
+
+.. automodule:: taxhub.apptax.index
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: taxhub.apptax
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/taxhub.apptax.taxonomie.rst b/docs/api/source/taxhub.apptax.taxonomie.rst
new file mode 100644
index 00000000..40ff019f
--- /dev/null
+++ b/docs/api/source/taxhub.apptax.taxonomie.rst
@@ -0,0 +1,77 @@
+taxhub.apptax.taxonomie package
+===============================
+
+Submodules
+----------
+
+taxhub.apptax.taxonomie.filemanager module
+------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.filemanager
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.models module
+-------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.models
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.routesbibattributs module
+-------------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.routesbibattributs
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.routesbiblistes module
+----------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.routesbiblistes
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.routesbibnoms module
+--------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.routesbibnoms
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.routesbibtypesmedia module
+--------------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.routesbibtypesmedia
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.routestaxref module
+-------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.routestaxref
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.taxonomie.routestmedias module
+--------------------------------------------
+
+.. automodule:: taxhub.apptax.taxonomie.routestmedias
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: taxhub.apptax.taxonomie
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/taxhub.apptax.utils.rst b/docs/api/source/taxhub.apptax.utils.rst
new file mode 100644
index 00000000..0a4a2950
--- /dev/null
+++ b/docs/api/source/taxhub.apptax.utils.rst
@@ -0,0 +1,45 @@
+taxhub.apptax.utils package
+===========================
+
+Submodules
+----------
+
+taxhub.apptax.utils.genericfunctions module
+-------------------------------------------
+
+.. automodule:: taxhub.apptax.utils.genericfunctions
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.utils.genericmodels module
+----------------------------------------
+
+.. automodule:: taxhub.apptax.utils.genericmodels
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.utils.routesconfig module
+---------------------------------------
+
+.. automodule:: taxhub.apptax.utils.routesconfig
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.apptax.utils.utilssqlalchemy module
+------------------------------------------
+
+.. automodule:: taxhub.apptax.utils.utilssqlalchemy
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: taxhub.apptax.utils
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/source/taxhub.rst b/docs/api/source/taxhub.rst
new file mode 100644
index 00000000..3d21bfc0
--- /dev/null
+++ b/docs/api/source/taxhub.rst
@@ -0,0 +1,38 @@
+taxhub package
+==============
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   taxhub.apptax
+   taxhub.tests
+
+Submodules
+----------
+
+taxhub.config module
+--------------------
+
+.. automodule:: taxhub.config
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.server module
+--------------------
+
+.. automodule:: taxhub.server
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: taxhub
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 5538bbf8..08bb5d56 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -2,39 +2,103 @@
 CHANGELOG
 =========
 
-1.7.0 (unreleased)
+1.7.4 (unreleased)
 ------------------
 
-**A vérifier avant release**
+**🚀 Nouveautés**
 
-* Voir https://github.com/PnX-SI/TaxHub/compare/develop
-* Reporter version Taxref, fonctions publics et BDC dans install_all de GeoNature ! Voir évolutions install_db.sh (https://github.com/PnX-SI/TaxHub/compare/develop#diff-3989ff5752ff641e2e850403d8021b1c)
+* Ajout d'un paramètre ``fields`` à la route ``/taxref/search/`` afin de pouvoir récupérer dans la réponse des champs supplémentaires selon les besoins (#243)
+* Support du stockage des media sur les services de stockages S3 (#248 par @jbdesbas)
+* Ajout d'un champ code liste ⚠️ Pour des raisons de compatibilité avec GéoNature 2 laisser les codes au format numérique.
+* Passage à taxref v14
+* Intégration du référentiel BDC statuts (https://inpn.mnhn.fr/telechargement/referentielEspece/bdc-statuts-especes)
 
-**Nouveautés**
+	* Pour des raisons de compatibilité avec Géonature les anciens statuts de protection et les listes rouges sont toujours présents
+
+**🐛 Corrections**
+
+* Passage à angular 1.8.0
+
+**⚠️ Notes de version**
+
+
+* Exécutez le script SQL de mise à jour de la BDD (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.7.3to1.7.4.sql)
+* La mise à jour vers taxref v14 est obligatoire  (https://github.com/PnX-SI/TaxHub/blob/master/data/scripts/update_taxref/README.rst)
+* Suivez la procédure standard de mise à jour de TaxHub : https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
+
+
+1.7.3 (2020-09-29)
+------------------
+
+**🚀 Nouveautés**
+
+* Ajout de tests unitaires
+* Mise à jour des dépendances (``psycopg2`` et ``SQLAlchemy``)
+
+**🐛 Corrections**
+
+* Correction d'un bug sur la récupération des attributs des taxons (#235 par @jbdesbas)
+* Script de récupération des médias sur INPN. Le script continue lorsqu'un appel à l'API renvoie un autre code que 200
+
+1.7.2 (2020-07-03)
+------------------
+
+**🚀 Nouveautés**
+
+* Ajout du nom vernaculaire (``nom_vern``) dans la vue matérialisée ``taxonomie.vm_taxref_list_forautocomplete`` et dans la route associée (``api/taxref/allnamebylist/``)
+
+**🐛 Corrections**
+
+* Correction de la pagination des routes quand le paramètre ``offset`` est égal à zéro (nécessaire pour Sync-mobile)
+
+**⚠️ Notes de version**
+
+* Exécutez le script SQL de mise à jour de la BDD (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.7.1to1.7.2.sql)
+* Suivez la procédure standard de mise à jour de TaxHub : https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
+
+1.7.1 (2020-07-02)
+------------------
+
+**🐛 Corrections**
+
+* Correction et homogénéisation des paramètres ``offset`` et ``page`` sur toutes les routes (#229)
+* Correction de la route de récupération de la configuration sans le "/" (#228)
+* Suppression des doublons de la route ``allnamebylist``, entrainant un nombre de résultats différent du paramètre ``limit`` fourni
+
+1.7.0 (2020-06-17)
+------------------
+
+**🚀 Nouveautés**
 
 * Mise à jour de Taxref en version 13
 * Intégration brute de la Base de connaissance des statuts des espèces correspondant à la version 13 de Taxref, en vue de la révision des statuts de protection (#157)
 * Migration de la librairie OpenCV vers PIL (plus légère) pour le redimensionnement des images et suppression de la librairie dépendante  NumPy (#209)
 * Mise à jour des librairies Python (Flask 1.1.1, Jinja 2.11.1, Werkzeug 1.0.0, gunicorn20.0.4) et Javascript (AngularJS 1.7.9, Bootstrap 3.4.1)
-* Suppression du paramètre ``id_application`` du fichier ``constants.js`` de façon à ce qu'il soit récupéré de façon dynamique
+* Suppression du paramètre ``id_application`` du fichier ``static/app/constants.js`` de façon à ce qu'il soit récupéré de façon dynamique
 * Ajout de fonctions génériques de détection, suppression et création des vues dépendantes dans le schéma ``public`` (``data/generic_drop_and_restore_deps_views.sql``)
-* Route ``allnamebylist`` enrichie avec un paramètre ``offset`` (#208) pour que l'application Occtax-mobile puisse récupérer les taxons par lots
+* Route ``allnamebylist`` enrichie avec un paramètre ``offset`` pour que l'application Occtax-mobile puisse récupérer les taxons par lots (#208)
+* Utilisation du ``cd_sup`` au lieu du ``cd_taxsup`` dans la fonction ``taxonomie.find_all_taxons_children()`` pour prendre en compte les rangs intermediaires
+* Ajout de la colonne famille au modèle ``VMTaxrefHierarchie`` (#211)
 * Ajout d'un manuel administrateur listant les fonctions SQL de la BDD (par @jbdesbas)
+* Révision et compléments de la documentation (par @ksamuel)
 * Ajout d'un lien vers le manuel utilisateur depuis la barre de navigation de l'application
-* Changement de modélisation de la table ``vm_taxref_list_forautocomplete`` qui redevient une vue matérialisée (#219) + Autre tickets réglés par cela...
+* Changement de modélisation de la table ``vm_taxref_list_forautocomplete`` qui redevient une vue matérialisée (#219). A rafraichir quand on met à jour Taxref
 
-**Corrections**
+**🐛 Corrections**
 
-* Correction d'un bug de suppression des attributs suite à une erreur d'enregistrement
+* Correction d'un bug de suppression des attributs suite à une erreur d'enregistrement (#80)
+* Correction d'un bug lors de la modification d'un média
+* Correction des doublons (#216) et des noms manquants (#194) dans la vue matérialisée ``vm_taxref_list_forautocomplete`` (#219)
 * Impossibilité d'associer une valeur nulle à un attribut
+* Nettoyage et suppression des scripts SQL et de leurs mentions à GeoNature v1 et UsersHub v1
 
-**Notes de version**
+**⚠️ Notes de version**
 
-* Vous pouvez supprimer le paramètre ``id_application`` du fichier ``constants.js`` car il n'est plus utilisé
-* Vous pouvez supprimer les anciennes listes de taxons qui correspondaient à des groupes (
-* Exécutez le script SQL de mise à jour de la BDD (``data/update1.6.5to1.7.0.sql``)
+* Vous pouvez supprimer le paramètre ``id_application`` du fichier ``static/app/constant.js`` car il n'est plus utilisé
+* Vous pouvez supprimer les anciennes listes de taxons qui correspondaient à des groupes utilisés par GeoNature v1 (Flore, Fonge, Vertébrés, Invertébrés, Amphibiens, Oiseaux, Poissons...)
+* Exécutez le script SQL de mise à jour de la BDD (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.6.5to1.7.0.sql)
 * Suivez la procédure standard de mise à jour de TaxHub : https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
-* Vous pouvez mettre à jour Taxref en version 13 avec la documentation et les scripts dans ``data/scripts/update_taxref/``
+* Vous pouvez mettre à jour Taxref en version 13 avec la documentation et les scripts du dossier ``data/scripts/update_taxref/`` (https://github.com/PnX-SI/TaxHub/tree/master/data/scripts/update_taxref)
 
 1.6.5 (2020-02-17)
 ------------------
@@ -59,7 +123,7 @@ CHANGELOG
 
 **Notes de version**
 
-* Exécuter le script de migration SQL https://github.com/PnX-SI/TaxHub/blob/master/data/update1.6.3to1.6.4.sql
+* Exécuter le script de migration SQL (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.6.3to1.6.4.sql)
 * Suivez la procédure standard de mise à jour de TaxHub : https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
 
 1.6.3 (2019-07-16)
@@ -78,10 +142,9 @@ CHANGELOG
 
 **Notes de version**
 
-* Exécuter le script de migration SQL ``data/update1.6.2to1.6.3.sql``
+* Exécuter le script de migration SQL (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.6.2to1.6.3.sql)
 * Suivez la procédure standard de mise à jour de TaxHub : https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
 
-
 1.6.2 (2019-02-27)
 ------------------
 
@@ -99,7 +162,7 @@ CHANGELOG
 
 * Afin que les logs de l'application (supervisor et gunicorn) soient tous écrits au même endroit, modifier le fichier ``taxhub-service.conf`` (``sudo nano /etc/supervisor/conf.d/taxhub-service.conf``). A la ligne ``stdout_logfile``, remplacer la ligne existante par : ``stdout_logfile = /home/<MON_USER>/taxhub/var/log/taxhub-errors.log`` (en remplaçant ``<MON_USER>`` par votre utilisateur linux)
 * Pour ne pas avoir de conflits de sessions d'authentification entre TaxHub et GeoNature, ajouter une variable ``ID_APP`` dans le fichier de configuration ``config.py`` et y mettre l'identifiant de l'application TaxHub tel qu'il est inscrit dans la table ``utilisateurs.t_applications``. Exemple : ``ID_APP = 2``
-* Exécuter le script de migration SQL: https://github.com/PnX-SI/TaxHub/blob/1.6.2/data/update1.6.1to1.6.2.sql
+* Exécuter le script de migration SQL : https://github.com/PnX-SI/TaxHub/blob/master/data/update1.6.1to1.6.2.sql
 * Suivez la procédure standard de mise à jour de TaxHub : https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
 
 1.6.1 (2019-01-21)
@@ -139,7 +202,7 @@ CHANGELOG
 
 * Exécuter la commande suivante pour ajouter l'extension PostgreSQL ``pg_trgm``, en remplaçant la variable ``$db_name`` par le nom de votre BDD : ``sudo -n -u postgres -s psql -d $db_name -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"``
 * Vous pouvez adapter la configuration Apache de TaxHub pour y intégrer la redirection sans ``/`` à la fin de l'URL (https://taxhub.readthedocs.io/fr/latest/installation.html#configuration-apache)
-* Exécutez le script de mise de la BDD : https://raw.githubusercontent.com/PnX-SI/TaxHub/1.6.0/data/update1.5.1to1.6.0.sql
+* Exécutez le script de mise de la BDD : https://github.com/PnX-SI/TaxHub/blob/master/data/update1.5.1to1.6.0.sql
 * Suivez la procédure habituelle de mise à jour de TaxHub: https://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application
 
 1.5.1 (2018-10-17)
@@ -158,8 +221,7 @@ CHANGELOG
 **Notes de versions**
 
 * Suivez la procédure classique de mise à jour de TaxHub
-* Exécutez le script de mise à jour de la BDD TaxHub ``data/update1.5.0to1.5.1.sql``
-
+* Exécutez le script de mise à jour de la BDD TaxHub (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.5.0to1.5.1.sql)
 
 1.5.0 (2018-09-19)
 ------------------
@@ -171,7 +233,6 @@ CHANGELOG
 * Installation : Ajout de paramètres permettant de mieux définir les données à intégrer et séparation des scripts SQL, notamment pour ne pas imposer d'intégrer toutes les données nécéessaires à GeoNature V1 (attributs et listes)
 * Mise à jour de Flask (0.11.1 à 1.0.2), Jinja, psycopg2 et Werkzeug
 
-
 1.4.1 (2018-08-20)
 ------------------
 
@@ -179,7 +240,6 @@ CHANGELOG
 
 * Correction de l'enregistrement lors du peuplement d'une liste
 
-
 1.4.0 (2018-07-12)
 ------------------
 
@@ -196,10 +256,9 @@ CHANGELOG
 
 - Ajouter le mode d'authentification dans ``config.py`` (https://github.com/PnX-SI/TaxHub/blob/87fbb11d360488e97eef3a0bb68f566744c54aa6/config.py.sample#L25)
 - Exécutez les scripts de migration de Taxref 9 à 11 (``data/scripts/update_taxref_v11/``) en suivant les indications de https://github.com/PnX-SI/TaxHub/issues/156
-- Exécutez le script SQL de mise à jour de la BDD ``data/update1.3.2to1.4.0.sql``
+- Exécutez le script SQL de mise à jour de la BDD (https://github.com/PnX-SI/TaxHub/blob/master/data/update1.3.2to1.4.0.sql)
 - Suivez la procédure générique de mise à jour de l'application
 
-
 1.3.2 (2017-12-15)
 ------------------
 
@@ -218,21 +277,20 @@ CHANGELOG
 
 - Ajout d'une liste vide impossible #148
 - Enregistrement d'un attribut de type select (bug de la version 1.3.1, ce n'était pas la valeur qui était enregistrée mais l'index)
- 
+
 **Note de version**
 
 - Vous pouvez directement passer de la version 1.1.2 à la 1.3.2 mais en suivant les différentes notes de version.
 - Exécutez le script SQL de mise à jour de la BDD ``data/update1.3.1to1.3.2.sql``
 - Suivez la procédure générique de mise à jour de l'application
 
-
 1.3.1  (2017-09-26)
 -------------------
 
 **Corrections**
 
 - Optimisation des performances pour le rafraichissement d'une vue matérialisée qui est devenue une table controlée (``vm_taxref_list_forautocomplete``) par trigger (``trg_refresh_mv_taxref_list_forautocomplete``). Voir #134
-- Utilisation du nom francais de la table ``bib_noms`` pour la table ``vm_taxref_list_forautocomplete``. Cette table permet de stocker les noms sous la forme ``nom_vern|lb_nom = nom_valide`` pour les formulaires de recherche d'un taxon. 
+- Utilisation du nom francais de la table ``bib_noms`` pour la table ``vm_taxref_list_forautocomplete``. Cette table permet de stocker les noms sous la forme ``nom_vern|lb_nom = nom_valide`` pour les formulaires de recherche d'un taxon.
 - Dans la liste taxref, tous les noms étaient considérés comme nouveaux (plus de possibilité de modification)
 
 **Note de version**
@@ -240,7 +298,6 @@ CHANGELOG
 - Vous pouvez directement passer de la version 1.1.2 à la 1.3.1 mais en suivant les différentes notes de version.
 - Exécutez le script SQL de mise à jour de la BDD ``data/update1.3.0to1.3.1.sql``
 
-
 1.3.0  (2017-09-20)
 -------------------
 
@@ -264,8 +321,7 @@ CHANGELOG
 - Supprimer le paramètre ``nb_results_limit`` du fichier ``static/app/constants.js`` (voir https://github.com/PnX-SI/TaxHub/blob/master/static/app/constants.js.sample)
 - Arrêter le serveur HTTP Gunicorn : ``make prod-stop``
 - Lancer le script d'installation : ``./install_app.sh``
-- Vous pouvez directement passer de la version 1.1.2 à la 1.3.0 mais en suivant les notes de version de la 1.2.0.  
-
+- Vous pouvez directement passer de la version 1.1.2 à la 1.3.0 mais en suivant les notes de version de la 1.2.0.
 
 1.2.1 (2017-07-04)
 ------------------
@@ -277,8 +333,7 @@ CHANGELOG
 
 **Note de version**
 
-- Vous pouvez directement passer de la version 1.1.2 à la 1.2.1 mais en suivant les notes de version de la 1.2.0. 
-
+- Vous pouvez directement passer de la version 1.1.2 à la 1.2.1 mais en suivant les notes de version de la 1.2.0.
 
 1.2.0 (2017-06-21)
 ------------------
@@ -310,10 +365,10 @@ CHANGELOG
 	sudo a2enmod proxy
 	sudo a2enmod proxy_http
 	sudo apache2ctl restart
-		
+
 * Supprimer la totalité de la configuration Apache concernant TaxHub et remplacez-la par celle-ci :
 ::
-  
+
 	# Configuration TaxHub
 		<Location /taxhub>
 			ProxyPass  http://127.0.0.1:8000/
@@ -321,11 +376,11 @@ CHANGELOG
 		</Location>
 	# FIN Configuration TaxHub
 
-* Redémarrer Apache : 
+* Redémarrer Apache :
 ::
 
 	sudo service apache2 restart
-	
+
 * Lancer le serveur HTTP Gunicorn :
 ::
 
@@ -335,9 +390,8 @@ CHANGELOG
 ::
 
 	make prod-stop
-		
-L'application doit être disponible à l'adresse http://monserver.ext/taxhub
 
+L'application doit être disponible à l'adresse http://monserver.ext/taxhub
 
 1.1.2 (2017-02-23)
 ------------------
@@ -359,7 +413,6 @@ L'application doit être disponible à l'adresse http://monserver.ext/taxhub
 - Si vous ne l'avez pas fait côté GeoNature, vous pouvez supprimer l'attribut ``Saisie`` après avoir récupéré les informations dans la nouvelle liste avec ces lignes de SQL : https://github.com/PnEcrins/GeoNature/blob/master/data/update_1.8.2to1.8.3.sql#L307-L314
 - Rajoutez le paramètre ``COOKIE_AUTORENEW = True`` dans le fichier ``config.py``.
 
-
 1.1.1 (2016-12-14)
 ------------------
 
@@ -372,9 +425,9 @@ L'application doit être disponible à l'adresse http://monserver.ext/taxhub
 
 - Exécutez la procédure standard de mise à jour de l'application (http://taxhub.readthedocs.io/fr/latest/installation.html#mise-a-jour-de-l-application)
 - Mettre à jour la base de données
- * Exécuter la commande suivante depuis la racine du projet TaxHub ``unzip data/inpn/LR_FRANCE.zip -d /tmp``
- * Exécuter le fichier ``data/update1.1.0to1.1.1.sql``
 
+  - Exécuter la commande suivante depuis la racine du projet TaxHub ``unzip data/inpn/LR_FRANCE.zip -d /tmp``
+  - Exécuter le fichier ``data/update1.1.0to1.1.1.sql``
 
 1.1.0 (2016-11-17)
 ------------------
@@ -391,25 +444,23 @@ L'application doit être disponible à l'adresse http://monserver.ext/taxhub
 - Compléments sur les attributs des taxons exemples
 - Ajout d'une confirmation avant la suppression d'un media
 - Champ ``auteur`` affiché au lieu du champ ``description`` dans le tableau des médias
-- Modification du type de données pour l'attribut ``milieu`` 
+- Modification du type de données pour l'attribut ``milieu``
 - Possibilité de choisir pour l'installation du schéma ``utilisateurs`` - en local ou en Foreign Data Wrapper
 - Meilleure articulation et cohérence avec UsersHub, GeoNature et GeoNature-atlas
 - Amélioration en vue d'une installation simplifiée
 
-
 1.0.0 (2016-09-06)
 ------------------
 
-Première version fonctionnelle et déployable de Taxhub (Python Flask)
+Première version fonctionnelle et déployable de TaxHub (Python Flask)
 
 **Fonctionnalités**
 
-- Visualisation de taxref
+- Visualisation de Taxref
 - Gestion du catalogue de noms d'une structure
 - Association de données attributaires aux taxons d'une structure
 - Association de médias aux taxons d'une structure
 
-
 0.1.0 (2016-05-12)
 ------------------
 
@@ -417,13 +468,12 @@ Première version fonctionnelle et déployable de Taxhub (Python Flask)
 
 Permet de lister le contenu de TaxRef, le contenu de ``taxonomie.bib_taxons``, de faire des recherches, d'ajouter un taxon à ``taxonomie.bib_taxons`` depuis TaxRef et d'y renseigner ses propres attributs.
 
-L'ajout d'un taxon dans des listes n'est pas encore développé. 
+L'ajout d'un taxon dans des listes n'est pas encore développé.
 
 Le MCD a été revu pour se baser sur ``taxonomie.bib_attributs`` et non plus sur les filtres de ``bib_taxons`` mais il reste encore à revoir le MCD pour ne pas pouvoir renseigner différemment les attributs d'un même taxon de référence - https://github.com/PnX-SI/TaxHub/issues/71
 
 A suivre : Remplacement du framework Symfony (PHP) par Flask (Python) - https://github.com/PnX-SI/TaxHub/issues/70
 
-
 0.0.1 (2015-04-01)
 ------------------
 
diff --git a/docs/installation.rst b/docs/installation.rst
index de6c743c..3f4992a4 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -61,6 +61,12 @@ La valeur la plus importante à mettre à jour est:
 
 ``monpassachanger`` doit être remplacé par la celui choisi lors de la dernière étape de l'installation serveur ``Création de 2 utilisateurs PostgreSQL``.
 
+Stockage des media
+------------------
+
+Les media associés aux taxons peuvent être stockés sur le serveur (paramètre ``UPLOAD_FOLDER``) ou sur un cloud S3. Dans les deux cas, les miniatures sont stockées sur le serveur, dans ``UPLOAD_FOLDER``.
+OVH propose une offre `Object storage <https://www.ovhcloud.com/fr/public-cloud/object-storage/>`_ compatible. `Voir ici <https://fabien.io/get-s3-credentials-ovh-public-cloud/>`_ la manipulation pour obtenir les identifants à renseigner dans le fichier de configuration (``S3_KEY`` et ``S3_SECRET``).
+Pour utiliser votre propre sous-domaine (paramètre ``S3_PUBLIC_URL``) (ex : http://media.ADRESSE_DU_SERVEUR/taxons/image.jpg) : `<https://docs.ovh.com/gb/en/storage/pcs/link-domain/>_`
 
 Configuration Apache
 ====================
diff --git a/install_db.sh b/install_db.sh
index 034951a2..762622d5 100755
--- a/install_db.sh
+++ b/install_db.sh
@@ -69,7 +69,7 @@ then
 
     echo "Décompression des fichiers du taxref..."
 
-    array=( TAXREF_INPN_v13.zip ESPECES_REGLEMENTEES_v11.zip LR_FRANCE_20160000.zip BDC_STATUTS_13.zip)
+    array=( TAXREF_v14_2020.zip ESPECES_REGLEMENTEES_v11.zip LR_FRANCE_20160000.zip BDC-Statuts-v14.zip)
     for i in "${array[@]}"
     do
         if [ ! -f '/tmp/taxhub/'$i ]
@@ -122,7 +122,6 @@ then
         export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name -f /tmp/usershub-data.sql &>> $LOG_DIR/installdb/install_db.log
         export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name -f /tmp/usershub-dataset.sql &>> $LOG_DIR/installdb/install_db.log
         export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name -f data/adds_for_usershub.sql &>> $LOG_DIR/installdb/install_db.log
-	    export PGPASSWORD=$user_pg_pass;psql -h $db_host -U $user_pg -d $db_name -f data/adds_for_usershub_views.sql &>> $LOG_DIR/installdb/install_db.log
     else
         echo "Connexion à la base Utilisateur..."
         cp data/create_fdw_utilisateurs.sql /tmp/taxhub/create_fdw_utilisateurs.sql
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 00000000..8f9a9feb
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,4 @@
+flake8==3.7.9
+pytest==5.4.3
+pytest-flask==1.0.0
+schema==0.7.2
\ No newline at end of file
diff --git a/requirements.in b/requirements.in
new file mode 100644
index 00000000..6cc30163
--- /dev/null
+++ b/requirements.in
@@ -0,0 +1,6 @@
+Flask
+Flask-Cors
+Flask-SQLAlchemy
+Flask-Migrate
+SQLAlchemy
+psycopg2
diff --git a/requirements.txt b/requirements.txt
index 02ad5bc6..4ea75e38 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,14 @@
+git+https://github.com/PnX-SI/UsersHub-authentification-module@1.4.5
+boto3==1.16.9
 click==6.6
-Flask==1.1.1
+Flask-Cors==3.0.3
 Flask-SQLAlchemy==2.3.2
+Flask==1.1.1
+gunicorn==20.0.4
 itsdangerous==0.24
 Jinja2==2.11.1
 MarkupSafe==0.23
-psycopg2==2.7.5
-SQLAlchemy==1.3.3
+Pillow==7.0.0
+psycopg2==2.8.5
+SQLAlchemy==1.3.19
 Werkzeug==1.0.0
-gunicorn==20.0.4
-Flask-Cors==3.0.3
-git+https://github.com/PnX-SI/UsersHub-authentification-module@1.4.3
-Pillow==7.0.0
\ No newline at end of file
diff --git a/server.py b/server.py
index 8b78c505..4bd98a53 100644
--- a/server.py
+++ b/server.py
@@ -4,22 +4,35 @@
 
 from apptax.database import db
 
+
+import logging
+logging.basicConfig()
+logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
+
 db = db
 
 app_globals = {}
 
 
 def init_app():
-    if app_globals.get('app', False):
-        app = app_globals['app']
+    if app_globals.get("app", False):
+        app = app_globals["app"]
     else:
         app = Flask(__name__)
 
     with app.app_context():
-        app.config.from_pyfile('config.py')
+        app.config.from_pyfile("config.py")
         db.init_app(app)
         db.app = app
-        app.config['DB'] = db
+        app.config["DB"] = db
+
+        app.config["S3_BUCKET_NAME"] = app.config.get("S3_BUCKET_NAME", None)
+        app.config["S3_KEY"] = app.config.get("S3_KEY", None)
+        app.config["S3_SECRET"] = app.config.get("S3_SECRET", None)
+        app.config["S3_ENDPOINT"] = app.config.get("S3_ENDPOINT", None)
+        app.config["S3_PUBLIC_URL"] = app.config.get("S3_PUBLIC_URL", None)
+        app.config["S3_FOLDER"] = app.config.get("S3_FOLDER", None)
+        app.config["S3_REGION_NAME"] = app.config.get("S3_REGION_NAME", None)
 
         @app.teardown_request
         def _manage_transaction(exception):
@@ -30,37 +43,49 @@ def _manage_transaction(exception):
             db.session.remove()
 
         from pypnusershub import routes
-        app.register_blueprint(routes.routes, url_prefix='/api/auth')
+
+        app.register_blueprint(routes.routes, url_prefix="/api/auth")
 
         from apptax.index import routes
-        app.register_blueprint(routes, url_prefix='/')
+
+        app.register_blueprint(routes, url_prefix="/")
 
         from apptax.taxonomie.routesbibnoms import adresses
-        app.register_blueprint(adresses, url_prefix='/api/bibnoms')
+
+        app.register_blueprint(adresses, url_prefix="/api/bibnoms")
 
         from apptax.taxonomie.routestaxref import adresses
-        app.register_blueprint(adresses, url_prefix='/api/taxref')
+
+        app.register_blueprint(adresses, url_prefix="/api/taxref")
 
         from apptax.taxonomie.routesbibattributs import adresses
-        app.register_blueprint(adresses, url_prefix='/api/bibattributs')
+
+        app.register_blueprint(adresses, url_prefix="/api/bibattributs")
 
         from apptax.taxonomie.routesbiblistes import adresses
-        app.register_blueprint(adresses, url_prefix='/api/biblistes')
+
+        app.register_blueprint(adresses, url_prefix="/api/biblistes")
 
         from apptax.taxonomie.routestmedias import adresses
-        app.register_blueprint(adresses, url_prefix='/api/tmedias')
+
+        app.register_blueprint(adresses, url_prefix="/api/tmedias")
 
         from apptax.taxonomie.routesbibtypesmedia import adresses
-        app.register_blueprint(adresses, url_prefix='/api/bibtypesmedia')
+
+        app.register_blueprint(adresses, url_prefix="/api/bibtypesmedia")
 
         from apptax.utils.routesconfig import adresses
-        app.register_blueprint(adresses, url_prefix='/api/config')
 
+        app.register_blueprint(adresses, url_prefix="/api/config")
+
+        from apptax.taxonomie.routesbdcstatuts import adresses
+
+        app.register_blueprint(adresses, url_prefix="/api/bdc_statuts")
 
     return app
 
 
 app = init_app()
 CORS(app, supports_credentials=True)
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..1d05a1a4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,25 @@
+import setuptools
+from pathlib import Path
+
+
+root_dir = Path(__file__).absolute().parent
+with (root_dir / 'VERSION').open() as f:
+    version = f.read()
+with (root_dir / 'README.rst').open() as f:
+    long_description = f.read()
+with (root_dir / 'requirements.in').open() as f:
+    requirements = f.read().splitlines()
+
+
+setuptools.setup(
+    name='taxhub',
+    description='Application web de gestion centralisée des taxons basée sur le référentiel TAXREF',
+    long_description=long_description,
+    long_description_content_type='text/x-rst',
+    maintainer='Parcs nationaux des Écrins et des Cévennes',
+    maintainer_email='geonature@ecrins-parcnational.fr',
+    url='https://github.com/PnX-SI/TaxHub',
+    version=version,
+    packages=setuptools.find_packages(where='.', include=['apptax*']),
+    install_requires=requirements,
+)
diff --git a/static/app/bib_liste/detail/bibliste-detail-tpl.html b/static/app/bib_liste/detail/bibliste-detail-tpl.html
index 375517ae..abd43e05 100644
--- a/static/app/bib_liste/detail/bibliste-detail-tpl.html
+++ b/static/app/bib_liste/detail/bibliste-detail-tpl.html
@@ -56,6 +56,7 @@ <h3> <img ng-src="./static/{{ctrl.infoListe.picto}}" class="img-circle" ng-if="c
                              Peupler
                     </button>
 
+                    <p><b>Code:</b> {{ctrl.infoListe.code_liste}}</p>
                     <p><b>Regne:</b> {{ctrl.infoListe.regne}}</p>
                     <p><b>Group2_inpn:</b> {{ctrl.infoListe.group2_inpn}}</p>
                     <p><b>Description:</b> {{ctrl.infoListe.desc_liste}}</p>
diff --git a/static/app/bib_liste/edit/bibliste-edit-controler.js b/static/app/bib_liste/edit/bibliste-edit-controler.js
index 0aabc2fb..84742153 100644
--- a/static/app/bib_liste/edit/bibliste-edit-controler.js
+++ b/static/app/bib_liste/edit/bibliste-edit-controler.js
@@ -9,6 +9,7 @@ app.controller('bibListeEditCtrl', ['$scope',  '$http', '$uibModal',
     self.pictos_propose = [];
     self.edit_detailliste = {
       "id_liste": "",
+      "code_liste": "",
       "nom_liste": "",
       "desc_liste": "",
       "picto": "images/pictos/nopicto.gif",
diff --git a/static/app/bib_liste/edit/bibliste-edit-tpl.html b/static/app/bib_liste/edit/bibliste-edit-tpl.html
index 52423e6d..1bc44c77 100644
--- a/static/app/bib_liste/edit/bibliste-edit-tpl.html
+++ b/static/app/bib_liste/edit/bibliste-edit-tpl.html
@@ -19,10 +19,17 @@ <h3><img ng-src="./static/{{ctrl.edit_detailliste.picto}}" class="img-circle" wi
 
             <form id="biblises-edit-form" class="form-horizontal" name="ctrl.form" ng-hide="ctrl.showSpinner">
                 <div class="form-group form-group-sm">
-                    <label class="col-sm-2 control-label" for="sm">ID liste</label>
+                    <label class="col-sm-2 control-label" for="sm">id liste</label>
                     <div class="col-sm-7">
-                        <input class="form-control" type="number" min="1" id="sm" ng-model="ctrl.edit_detailliste.id_liste"
-                        ng-disabled="ctrl.action == 'edit'" placeholder="ID liste" required>
+                        <input class="form-control" type="number" id="sm" ng-model="ctrl.edit_detailliste.id_liste"
+                        ng-disabled="true" placeholder="Id liste">
+                    </div>
+                </div>
+                <div class="form-group form-group-sm">
+                    <label class="col-sm-2 control-label" for="sm">code liste</label>
+                    <div class="col-sm-7">
+                        <input class="form-control" type="text" min="1" id="sm" ng-model="ctrl.edit_detailliste.code_liste"
+                            placeholder="Code liste" required>
                     </div>
                 </div>
                 <div class="form-group form-group-sm">
diff --git a/static/app/bib_liste/list/bibliste-list-controler.js b/static/app/bib_liste/list/bibliste-list-controler.js
index 1b7020bf..514652df 100644
--- a/static/app/bib_liste/list/bibliste-list-controler.js
+++ b/static/app/bib_liste/list/bibliste-list-controler.js
@@ -7,6 +7,7 @@ app.controller('listesCtrl',[ '$scope', '$http', '$filter','$uibModal','bibListe
     self.route='listes';
     self.tableCols = {
       "id_liste" : { title: "id_liste", show: true },
+      "code_liste" : { title: "code_liste", show: true },
       "nom_liste" : {title: "nom_liste", show: true },
       "desc_liste" : {title: "desc_liste", show: true },
       "picto" : {title: "picto", show: true },
@@ -69,14 +70,14 @@ app.service('bibListesSrv', ['$http', '$q', 'backendCfg', function ($http, $q, b
     };
 
     this.getDetailListe = function (idListe) {
-      return $http.get(backendCfg.api_url + 'biblistes/'+idListe);
+      return $http.get(backendCfg.api_url + 'biblistes/' + idListe);
     };
 
-    this.getbibNomsList = function (id, existing,  params) {
+    this.getbibNomsList = function (id, existing, params) {
       params = (params ? params : {});
       if (existing) {
-        params['existing'] = true
+        params['existing'] = true;
       }
-      return $http.get(backendCfg.api_url+"biblistes/taxons/"+ id, {'params':params});
+      return $http.get(backendCfg.api_url + "biblistes/taxons/" + id, {'params': params});
     };
 }]);
diff --git a/static/app/bib_liste/list/bibliste-list-tpl.html b/static/app/bib_liste/list/bibliste-list-tpl.html
index 1251d8da..0d613174 100644
--- a/static/app/bib_liste/list/bibliste-list-tpl.html
+++ b/static/app/bib_liste/list/bibliste-list-tpl.html
@@ -90,6 +90,8 @@ <h3>Liste des listes ({{ctrl.count_listes}} listes)</h3>
                           ng-if="ctrl.tableCols['nom_liste'].show">
                           <img ng-src="./static/{{lst.picto}}" class="img-circle" width="25" height="25" ng-if="lst.picto"> {{ lst.nom_liste }}
                         </td>
+                        <td data-title="'code'" sortable="'code_liste'" filter="{ 'code_liste': 'text' }" ng-if="ctrl.tableCols['code_liste'].show">{{ lst.code_liste }}
+                        </td>
                         <td data-title="'règne'" sortable="'regne'" filter="{ 'regne': 'text' }" ng-if="ctrl.tableCols['regne'].show">{{ lst.regne }}
                         </td>
                         <td data-title="'Group2 INPN'" sortable="'group2_inpn'" filter="{ 'group2_inpn': 'text' }" ng-if="ctrl.tableCols['group2_inpn'].show">{{ lst.group2_inpn }}
diff --git a/static/app/bib_nom/edit/bibNom-form-controler.js b/static/app/bib_nom/edit/bibNom-form-controler.js
index 7b60b7d1..fc8caa5f 100644
--- a/static/app/bib_nom/edit/bibNom-form-controler.js
+++ b/static/app/bib_nom/edit/bibNom-form-controler.js
@@ -31,7 +31,7 @@ function($scope, $routeParams, $http, $uibModal, locationHistoryService, $locati
     'saveError':{"title":"Erreur d'enregistrement"},
   }
   var getTaxonsInfo = function (cd_nom) {
-    $http.get(backendCfg.api_url + "bibnoms/taxoninfo/"+cd_nom).then(function(response) {
+    $http.get(backendCfg.api_url + "bibnoms/taxoninfo/"+cd_nom+"?forcePath=True").then(function(response) {
         if (response.data) {
             if (response.data.medias){
               self.disableMediasTab = false;
diff --git a/static/app/bib_nom/edit/media/createBibnomsMedias-directives.js b/static/app/bib_nom/edit/media/createBibnomsMedias-directives.js
index 786b74c0..9447892c 100644
--- a/static/app/bib_nom/edit/media/createBibnomsMedias-directives.js
+++ b/static/app/bib_nom/edit/media/createBibnomsMedias-directives.js
@@ -46,14 +46,18 @@ function ($http, toaster, backendCfg, Upload, $timeout, dialogs) {
                 initMediaForm();
             };
 
+            $scope.getUniqueString = function(){
+              return (+new Date).toString(36).slice(-5)
+            }
+
             var initMediaForm = function() {
                 $scope.showform = true;
                 my.picFile = null;
-                if ((my.selectedMedium.url)  && (my.selectedMedium.url !== ''))  {
-                  $scope.localFile = false;
+                if ((my.selectedMedium.chemin)  && (my.selectedMedium.chemin !== ''))  {
+                  $scope.localFile = true;
                 }
                 else {
-                  $scope.localFile = true;
+                  $scope.localFile = false;
                 }
             };
 
@@ -72,7 +76,7 @@ function ($http, toaster, backendCfg, Upload, $timeout, dialogs) {
               var successClb = function(data, status, headers, config) {
                   if (data.success == true) {
                       // http://localhost:5000/api/tmedias/bycdref/497
-                      $http.get(backendCfg.api_url + "tmedias/bycdref/"+my.mediasCdref).then(function(response) {
+                      $http.get(backendCfg.api_url + "tmedias/bycdref/"+my.mediasCdref+"?forcePath=True").then(function(response) {
                           my.mediasValues = response.data;
                           my.selectedMedium = {};
                           $scope.showform = false;
diff --git a/static/app/bib_nom/edit/media/createBibnomsMedias-template.html b/static/app/bib_nom/edit/media/createBibnomsMedias-template.html
index 6bded508..61c42e53 100644
--- a/static/app/bib_nom/edit/media/createBibnomsMedias-template.html
+++ b/static/app/bib_nom/edit/media/createBibnomsMedias-template.html
@@ -14,12 +14,12 @@
             </tr>
             <tr ng-repeat="(key, medium) in mediasValues track by medium.id_media">
                 <td class="col-md-1" >
-                  <a href="{{medium.chemin || medium.url}}" target="_blank">
-                    <img ng-src="{{medium.chemin || medium.url}}"
+                  <a href="{{medium.url || medium.chemin }}" target="_blank">
+                    <img ng-src="{{medium.url || medium.chemin }}?{{getUniqueString()}}"
                       class= "img-responsive" style="max-height:50px;"/>
                   </a>
                 </td>
-                <td class="col-md-2">{{medium.titre}} </td>
+                <td class="col-md-2">{{medium.titre}}</td>
                 <td class="col-md-2">{{medium.nom_type_media}}</td>
                 <td class="col-md-5">{{medium.auteur}}</td>
                 <td class="col-md-2" >
diff --git a/static/app/taxref/detail/taxrefDetailModal.html b/static/app/taxref/detail/taxrefDetailModal.html
index 36562f2b..1ab40c93 100644
--- a/static/app/taxref/detail/taxrefDetailModal.html
+++ b/static/app/taxref/detail/taxrefDetailModal.html
@@ -37,17 +37,23 @@ <h4 class="bg-info" style="padding:5px;color:#666;">Taxonomie</h4>
                     </div>
                 </div>
                 <div>
-                    <h4 class="bg-info" style="padding:5px;color:#666;">Protections réglementaires</h4>
-                    <div style="padding-left:20px;" ng-if="sp" ng-repeat="sp in monTaxon.statuts_protection">
-                        <div >
-                            <span class="cdref">{{sp.arrete}}</span>
-                            <span>{{", "+ sp.article + " "}}</span>
-                            <span>{{" ("+ sp.date_arrete + ") "}}</span>
+                    <h4 class="bg-info" style="padding:5px;color:#666;">Statuts</h4>
+                    <div style="padding-left:20px;" ng-if="st" ng-repeat="st in monTaxon.status">
+                        <h5 class= "bg-success" >{{st.lb_type_statut}}</h5>
+                        <div ng-repeat = "text in st.text">
+                            <span class="cdref" ng-bind-html="text.full_citation"></span>
+                            <span>({{ text.lb_adm_tr }} - {{ text.cd_sig }}) </span>
+                            <div ng-repeat = "value in text.values">
+                                <span ng-if ="value.code_statut != 'true'">{{ value.code_statut }}</span>
+                                {{ value.label_statut }}
+                                {{ value.rq_statut }}
+                            </div>
+                            <div ng-if="text.doc_url">
+                                <a  href="{{ text.doc_url }}" target="_blank" class="btn btn-sm">voir le texte</a>
+                            </div>
                         </div>
-                        <div>{{"Type : "+ sp.type_protection + " "}}</div>
-                        <a href="{{sp.url}}" target="_blank" class="btn btn-primary btn-sm">voir le texte</a>
                     </div>
-                    <div style="padding-left:20px;" ng-if="monTaxon.statuts_protection != []">Aucune</div>
+                    <div style="padding-left:20px;" ng-if="monTaxon.status == {}">Aucun</div>
                 </div>
                 <div >
                     <h4 class="bg-info" style="padding:5px;color:#666;">Synonymie</h4>
diff --git a/static/package-lock.json b/static/package-lock.json
index e886834a..cefc07ee 100644
--- a/static/package-lock.json
+++ b/static/package-lock.json
@@ -5,9 +5,9 @@
   "requires": true,
   "dependencies": {
     "angular": {
-      "version": "1.7.9",
-      "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.9.tgz",
-      "integrity": "sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ=="
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.0.tgz",
+      "integrity": "sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg=="
     },
     "angular-animate": {
       "version": "1.6.10",
diff --git a/static/package.json b/static/package.json
index ab550b01..18fca184 100644
--- a/static/package.json
+++ b/static/package.json
@@ -4,7 +4,7 @@
   "description": "Application web de gestion centralisée des taxons basée sur le référentiel TAXREF",
   "main": "/static/app/app.js",
   "dependencies": {
-    "angular": "^1.7.9",
+    "angular": "^1.8.0",
     "angular-animate": "~1.6.1",
     "angular-cookies": "~1.4.0",
     "angular-dialog-service": "~5.3.0",


Test patch:
diff --git a/apptax/taxonomie/routestaxref.py b/apptax/taxonomie/routestaxref.py
index f1d98ea9..9f164ea8 100644
--- a/apptax/taxonomie/routestaxref.py
+++ b/apptax/taxonomie/routestaxref.py
@@ -1,9 +1,10 @@
 from flask import jsonify, Blueprint, request
-from sqlalchemy import distinct, desc, func
+from sqlalchemy import distinct, desc, func, and_
 from sqlalchemy.orm.exc import NoResultFound
 
 
 from ..utils.utilssqlalchemy import json_resp, serializeQuery, serializeQueryOneResult
+from ..utils.genericfunctions import calculate_offset_page
 from .models import (
     Taxref,
     BibNoms,
@@ -17,8 +18,11 @@
     BibTaxrefLR,
     BibTaxrefHabitats,
     CorNomListe,
+    BibListes
 )
 
+from .repositories import BdcStatusRepository
+
 try:
     from urllib.parse import unquote
 except ImportError:
@@ -43,18 +47,27 @@ def getTaxrefBibtaxonList():
 
 @adresses.route("/search/<field>/<ilike>", methods=["GET"])
 def getSearchInField(field, ilike):
-    """
-    Get the first 20 result of Taxref table for a given field with an ilike query
-    Use trigram algo to add relevance
-
-    :params field: a Taxref column
-    :type field: str
-    :param ilike: the ilike where expression to filter
-    :type ilike:str
-
-    :query str add_rank: join on table BibTaxrefRank and add the column 'nom_rang' to the result
-    :query str rank_limit: return only the taxon where rank <= of the given rank (id_rang of BibTaxrefRang table)
-    :returns: Array of dict
+    """.. http:get:: /taxref/search/(str:field)/(str:ilike)
+    .. :quickref: Taxref;
+
+    Retourne les 20 premiers résultat de la table "taxref" pour une
+    requête sur le champ `field` avec ILIKE et la valeur `ilike` fournie.
+    L'algorithme Trigramme est utilisé pour établir la correspondance.
+
+    :query fields: Permet de récupérer des champs suplémentaire de la
+        table "taxref" dans la réponse. Séparer les noms des champs par
+        des virgules.
+    :query is_inbibnom: Ajoute une jointure sur la table "bib_noms".
+    :query add_rank: Ajoute une jointure sur la table "bib_taxref_rangs"
+        et la colonne nom_rang aux résultats.
+    :query rank_limit: Retourne seulement les taxons dont le rang est
+        supérieur ou égal au rang donné. Le rang passé doit être une
+        valeur de la colonne "id_rang" de la table "bib_taxref_rangs".
+
+    :statuscode 200: Tableau de dictionnaires correspondant aux résultats
+        de la recherche dans la table "taxref".
+    :statuscode 500: Aucun rang ne correspond à la valeur fournie.
+                     Aucune colonne ne correspond à la valeur fournie.
     """
     taxrefColumns = Taxref.__table__.columns
     if field in taxrefColumns:
@@ -72,6 +85,16 @@ def getSearchInField(field, ilike):
             .order_by(desc("idx_trgm"))
         )
 
+        if request.args.get("fields"):
+            fields = request.args["fields"].split(",")
+            for field in fields:
+                if field in taxrefColumns:
+                    column = taxrefColumns[field]
+                    q = q.add_columns(column)
+                else:
+                    msg = f"No column found in Taxref for {field}"
+                    return jsonify(msg), 500
+
         if request.args.get("is_inbibnoms"):
             q = q.join(BibNoms, BibNoms.cd_nom == Taxref.cd_nom)
         join_on_bib_rang = False
@@ -152,6 +175,10 @@ def getTaxrefDetail(id):
         }
         for row in synonymes
     ]
+
+    # Pour des questions de retrocompatibilité avec taxref
+    #  Les anciens statuts sont toujours remonté
+    #  TODO delete après refonte fiche taxon de GN2
     stprotection = db.engine.execute(
         (
             """SELECT DISTINCT pr_a.*
@@ -167,6 +194,13 @@ def getTaxrefDetail(id):
         for r in stprotection
     ]
 
+    status = BdcStatusRepository().get_status(
+        cd_ref=results.cd_ref,
+        type_statut=None,
+        format=True
+    )
+    taxon["status"] = status
+
     return jsonify(taxon)
 
 
@@ -175,7 +209,7 @@ def getDistinctField(field):
     taxrefColumns = Taxref.__table__.columns
     q = db.session.query(taxrefColumns[field]).distinct(taxrefColumns[field])
 
-    limit = request.args.get("limit") if request.args.get("limit") else 100
+    limit = request.args.get("limit", 100, int)
 
     for param in request.args:
         if param in taxrefColumns:
@@ -217,8 +251,10 @@ def genericTaxrefList(inBibtaxon, parameters):
         q = q.outerjoin(BibNoms, BibNoms.cd_nom == Taxref.cd_nom)
 
     # Traitement des parametres
-    limit = int(parameters.get("limit")) if parameters.get("limit") else 100
-    page = int(parameters.get("page")) - 1 if parameters.get("page") else 0
+    limit = parameters.get("limit", 20, int)
+    page = parameters.get("page", 1, int)
+    offset = parameters.get("offset", 0, int)
+    (limit, offset, page) = calculate_offset_page(limit, offset, page)
 
     for param in parameters:
         if param in taxrefColumns and parameters[param] != "":
@@ -248,13 +284,13 @@ def genericTaxrefList(inBibtaxon, parameters):
                 orderCol = orderCol.desc()
         q = q.order_by(orderCol)
 
-    results = q.limit(limit).offset(page * limit).all()
+    results = q.limit(limit).offset(offset).all()
     return {
         "items": [dict(d.Taxref.as_dict(), **{"id_nom": d.id_nom}) for d in results],
         "total": nbResultsWithoutFilter,
         "total_filtered": nbResults,
         "limit": limit,
-        "page": page,
+        "page": page
     }
 
 
@@ -263,7 +299,7 @@ def genericHierarchieSelect(tableHierarchy, rang, parameters):
     dfRang = tableHierarchy.__table__.columns["id_rang"]
     q = db.session.query(tableHierarchy).filter(tableHierarchy.id_rang == rang)
 
-    limit = parameters.get("limit") if parameters.get("limit") else 100
+    limit = parameters.get("limit", 100, int)
 
     for param in parameters:
         if param in tableHierarchy.__table__.columns:
@@ -302,41 +338,60 @@ def get_regneGroup2Inpn_taxref():
     return results
 
 
-@adresses.route("/allnamebylist/<int:id_liste>", methods=["GET"])
+@adresses.route("/allnamebylist/<string:code_liste>", methods=["GET"])
 @json_resp
-def get_AllTaxrefNameByListe(id_liste):
+def get_AllTaxrefNameByListe(code_liste):
     """
         Route utilisée pour les autocompletes
-        Si le paramètre search_name est passé, la requête SQL utilise l'algorithme 
+        Si le paramètre search_name est passé, la requête SQL utilise l'algorithme
         des trigrames pour améliorer la pertinence des résultats
+        Route utilisé par le mobile pour remonter la liste des taxons
         params URL:
-            - id_liste : identifiant de la liste
-        params GET:
+            - code_liste : code de la liste
+        params GET (facultatifs):
             - search_name : nom recherché. Recherche basé sur la fonction
                 ilike de sql avec un remplacement des espaces par %
             - regne : filtre sur le regne INPN
             - group2_inpn : filtre sur le groupe 2 de l'INPN
+            - limit: nombre de résultat
+            - offset: numéro de la page
     """
 
     search_name = request.args.get("search_name")
-    if not search_name:
-        return "No search pattern provided", 400
-    else:
+    # Get id_liste
+
+    try:
         q = (
-            db.session.query(
-                VMTaxrefListForautocomplete,
+            db.session.query(BibListes.id_liste)
+            .filter(BibListes.code_liste == code_liste)
+        ).one()
+        id_liste = q[0]
+    except NoResultFound:
+        return (
+            {
+                "success": False,
+                "message": "Code liste '{}' inexistant".format(code_liste)
+            },
+            500,
+        )
+
+    q = (
+        db.session.query(VMTaxrefListForautocomplete)
+        .join(BibNoms, BibNoms.cd_nom == VMTaxrefListForautocomplete.cd_nom)
+        .join(
+            CorNomListe,
+            and_(
+                CorNomListe.id_nom == BibNoms.id_nom,
+                CorNomListe.id_liste == id_liste
+            ),
+        )
+    )
+    if search_name:
+        q = q.add_columns(
                 func.similarity(
                     VMTaxrefListForautocomplete.search_name, search_name
-                ).label("idx_trgm"),
-            )
-            .join(BibNoms, BibNoms.cd_nom == VMTaxrefListForautocomplete.cd_nom)
-            .join(
-                CorNomListe,
-                CorNomListe.id_nom == BibNoms.id_nom
-                and CorNomListe.id_liste == id_liste,
-            )
+                ).label("idx_trgm")
         )
-
         search_name = search_name.replace(" ", "%")
         q = q.filter(
             VMTaxrefListForautocomplete.search_name.ilike("%" + search_name + "%")
@@ -353,10 +408,17 @@ def get_AllTaxrefNameByListe(id_liste):
     q = q.order_by(
         desc(VMTaxrefListForautocomplete.cd_nom == VMTaxrefListForautocomplete.cd_ref)
     )
-    limit = int(request.args.get("limit", 20))
-    page = int(request.args.get("offset", 0))
-    data = q.limit(limit).offset(page * limit).all()
-    return [d[0].as_dict() for d in data]
+
+    limit = request.args.get("limit", 20, int)
+    page = request.args.get("page", 1, int)
+    offset = request.args.get("offset", 0, int)
+    (limit, offset, page) = calculate_offset_page(limit, offset, page)
+    data = q.limit(limit).offset(offset).all()
+
+    if search_name:
+        return [d[0].as_dict() for d in data]
+    else:
+        return [d.as_dict() for d in data]
 
 
 @adresses.route("/bib_lr", methods=["GET"])
diff --git a/apptax/taxonomie/routestmedias.py b/apptax/taxonomie/routestmedias.py
index 15633dc0..bc1c7aba 100644
--- a/apptax/taxonomie/routestmedias.py
+++ b/apptax/taxonomie/routestmedias.py
@@ -8,55 +8,55 @@
 
 from pypnusershub import routes as fnauth
 
-from ..utils.utilssqlalchemy import json_resp
-from .models import TMedias, BibTypesMedia
+from . import db
 from . import filemanager
+from .filemanager import FILEMANAGER
 from ..log import logmanager
-from . import db
+from ..utils.utilssqlalchemy import json_resp
+from .models import TMedias, BibTypesMedia
+from .repositories import MediaRepository
 
 adresses = Blueprint("t_media", __name__)
 logger = logging.getLogger()
 
+media_repo = MediaRepository(db.session, current_app.config["S3_BUCKET_NAME"])
+
 
 @adresses.route("/", methods=["GET"])
 @adresses.route("/<int:id>", methods=["GET"])
 @json_resp
 def get_tmedias(id=None):
     if id:
-        data = db.session.query(TMedias).filter_by(id_media=id).first()
-        return data.as_dict()
-    else:
-        data = db.session.query(TMedias).all()
-        return [media.as_dict() for media in data]
+        return media_repo.get_and_format_one_media(id)
+    return media_repo.get_and_format_media_filter_by(
+        filters={}, force_path=request.args.get("forcePath", False)
+    )
 
 
 @adresses.route("/bycdref/<cdref>", methods=["GET"])
 @json_resp
 def get_tmediasbyTaxon(cdref):
-    q = db.session.query(TMedias)
+
+    filters = {}
     if cdref:
-        q = q.filter_by(cd_ref=cdref)
-    results = q.all()
-    obj = []
-    for media in results:
-        o = dict(media.as_dict().items())
-        o.update(dict(media.types.as_dict().items()))
-        obj.append(o)
+        filters = {"cd_ref": cdref}
+    obj = media_repo.get_and_format_media_filter_by(
+        filters=filters, force_path=request.args.get("forcePath", False)
+    )
     return obj
 
 
+# REM : Route utilisée ?????
+#  Rentre en conflit avec  get_tmedias
 @adresses.route("/<type>", methods=["GET"])
 @json_resp
 def get_tmediasbyType(type):
-    q = db.session.query(TMedias)
+    filters = {}
     if type:
-        q = q.filter_by(id_type=type)
-    results = q.all()
-    obj = []
-    for media in results:
-        o = dict(media.as_dict().items())
-        o.update(dict(media.types.as_dict().items()))
-        obj.append(o)
+        filters = {"id_type": type}
+    obj = media_repo.get_media_filter_by(
+        filters=filters, force_path=request.args.get("forcePath", False)
+    )
     return obj
 
 
@@ -65,106 +65,27 @@ def get_tmediasbyType(type):
 @fnauth.check_auth(2, True)
 def insertUpdate_tmedias(id_media=None, id_role=None):
     try:
+        # récupération des données du formulaire
+        upload_file = None
         if request.files:
-            file = request.files["file"]
+            upload_file = request.files["file"]
+
         data = {}
         if request.form:
             data = request.form.to_dict()
         else:
             data = request.get_json(silent=True)
 
-        # Si MAJ : récupération des données; Sinon création d'un nouvel objet
-        if id_media:
-            myMedia = db.session.query(TMedias).filter_by(id_media=id_media).first()
-            myMedia.cd_ref = data["cd_ref"]
-            old_title = myMedia.titre
-            action = "UPDATE"
-            # suppression des thumbnails
-            try:
-                filemanager.remove_dir(
-                    os.path.join(
-                        current_app.config["UPLOAD_FOLDER"], "thumb", str(id_media)
-                    )
-                )
-            except (FileNotFoundError, IOError, OSError) as e:
-                logger.error(e)
-                pass
-
-        else:
-            myMedia = TMedias(cd_ref=int(data["cd_ref"]))
-            old_title = ""
-            action = "INSERT"
-        myMedia.titre = data["titre"]
-        if "auteur" in data:
-            myMedia.auteur = data["auteur"]
-        if "desc_media" in data:
-            myMedia.desc_media = data["desc_media"]
-
-        # date_media = data['date_media'],
-        # TODO : voir le mode de gestion de la date du media (trigger ???)
-
-        if (isinstance(data["is_public"], bool)):
-            is_pub = data["is_public"]
-        else:
-            is_pub = json.loads(data["is_public"].lower())
-
-        myMedia.is_public = is_pub
-        myMedia.supprime = False
-        myMedia.id_type = data["id_type"]
-        db.session.add(myMedia)
+        # Enregistrement des données du média
         try:
-            db.session.commit()
-
-        except IntegrityError as e:
-            logger.error(e)
-            db.session.rollback()
-            return (
-                json.dumps({"success": False, "message": repr(e.args)}),
-                500,
-                {"ContentType": "application/json"},
-            )
+            myMedia, action = media_repo.import_media(data, upload_file, id_media)
         except Exception as e:
-            logger.error(e)
-            db.session.rollback()
             return (
                 json.dumps({"success": False, "message": repr(e.args)}),
                 500,
                 {"ContentType": "application/json"},
             )
 
-        if ("file" in locals()) and (
-            (data["isFile"] is True) or (data["isFile"] == "true")
-        ):
-            myMedia.url = ""
-            old_chemin = myMedia.chemin
-            filepath = filemanager.upload_file(
-                file, myMedia.id_media, myMedia.cd_ref, data["titre"]
-            )
-            myMedia.chemin = filepath
-            if (old_chemin != "") and (old_chemin != myMedia.chemin):
-                filemanager.remove_file(old_chemin)
-        elif (
-            ("url" in data)
-            and (data["url"] != "null")
-            and (data["url"] != "")
-            and (data["isFile"] is not True)
-        ):
-            myMedia.url = data["url"]
-            if myMedia.chemin != "":
-                filemanager.remove_file(myMedia.chemin)
-                myMedia.chemin = ""
-        elif old_title != myMedia.titre:
-            filepath = filemanager.rename_file(myMedia.chemin, old_title, myMedia.titre)
-            myMedia.chemin = filepath
-
-        db.session.add(myMedia)
-        db.session.commit()
-
-        # preparation de la réponse json (ajout du
-        #    nom du type de média pour affichage en front)
-        medium = myMedia.as_dict()
-        medium["nom_type_media"] = data["nom_type_media"]
-
         # Log
         logmanager.log_action(
             id_role,
@@ -176,7 +97,7 @@ def insertUpdate_tmedias(id_media=None, id_role=None):
         )
         return (
             json.dumps(
-                {"success": True, "id_media": myMedia.id_media, "media": medium}
+                {"success": True, "id_media": myMedia.id_media, "media": myMedia.as_dict()}
             ),
             200,
             {"ContentType": "application/json"},
@@ -194,20 +115,7 @@ def insertUpdate_tmedias(id_media=None, id_role=None):
 @adresses.route("/<int:id_media>", methods=["DELETE"])
 @fnauth.check_auth(2, True)
 def delete_tmedias(id_media, id_role):
-    myMedia = db.session.query(TMedias).filter_by(id_media=id_media).first()
-    if myMedia.chemin != "":
-        filemanager.remove_file(myMedia.chemin)
-
-    db.session.delete(myMedia)
-    db.session.commit()
-    # suppression des thumbnails
-    try:
-        filemanager.remove_dir(
-            os.path.join(current_app.config["UPLOAD_FOLDER"], "thumb", str(id_media))
-        )
-    except (FileNotFoundError, IOError, OSError) as e:
-        logger.error(e)
-        pass
+    myMedia = media_repo.delete(id_media)
     # Log
     logmanager.log_action(
         id_role,
@@ -227,84 +135,53 @@ def delete_tmedias(id_media, id_role):
 @adresses.route("/thumbnail/<int:id_media>", methods=["GET"])
 def getThumbnail_tmedias(id_media):
     """
-        Fonction qui génère une vignette d'un média existants
+    Fonction qui génère une vignette d'un média existants
+
+    Params
+    ------
+        id_media : identifiant du média
+        h : hauteur souhaitée
+        w : largeur souhaitée
+        regenerate : force la régénération du thumbnail
+
+    Return
+    ------
+        Image générée
+    """
 
-        Params
-        ------
-            id_media : identifiant du média
-            h : hauteur souhaitée
-            w : largeur souhaitée
-            regenerate : force la régénération du thumbnail
+    myMedia = media_repo.get_one_media(id_media)
+    if myMedia is None:
+        return (
+            json.dumps(
+                {
+                    "success": False,
+                    "id_media": id_media,
+                    "message": "Le média demandé n" "éxiste pas",
+                }
+            ),
+            400,
+            {"ContentType": "application/json"},
+        )
 
-        Return
-        ------
-            Image générée
-    """
     params = request.args
     pad = True
     size = (300, 400)
     if ("h" in params) or ("w" in params):
         size = (int(params.get("h", -1)), int(params.get("w", -1)))
 
-    thumbdir = os.path.join(
-        current_app.config["BASE_DIR"],
-        current_app.config["UPLOAD_FOLDER"],
-        "thumb",
-        str(id_media),
-    )
-    thumbpath = os.path.join(thumbdir, "{}x{}.jpg".format(size[0], size[1]))
-
+    regenerate = False
     if ("regenerate" in params) and (params.get("regenerate") == "true"):
-        filemanager.remove_file(
-            os.path.join(
-                current_app.config["UPLOAD_FOLDER"],
-                "thumb",
-                str(id_media),
-                "{}x{}.jpg".format(size[0], size[1]),
-            )
-        )
+        regenerate = True
 
-    if not os.path.exists(thumbpath):
-        myMedia = db.session.query(TMedias).filter_by(id_media=id_media).first()
-
-        if myMedia is None:
-            return (
-                json.dumps(
-                    {
-                        "success": False,
-                        "id_media": id_media,
-                        "message": "Le média demandé n" "éxiste pas",
-                    }
-                ),
-                400,
-                {"ContentType": "application/json"},
-            )
-        try:
-            if (myMedia.chemin) and (myMedia.chemin != ""):
-                img = Image.open(
-                    os.path.join(
-                        current_app.config["BASE_DIR"],
-                        myMedia.chemin
-                    )
-                )
-            else:
-                img = filemanager.url_to_image(myMedia.url)
-
-            resizeImg = filemanager.resizeAndPad(img, size)
-            # save file
-            if not os.path.exists(thumbdir):
-                os.makedirs(thumbdir)
-
-            resizeImg.save(thumbpath)
-        except Exception as e:
-            logger.error(e)
-            return (
-                json.dumps(
-                    {"success": False, "id_media": id_media, "message": repr(e)}
-                ),
-                500,
-                {"ContentType": "application/json"},
-            )
+    try:
+        thumbpath = FILEMANAGER.create_thumb(myMedia, size, regenerate)
+    except Exception as e:
+        logger.error(e)
+        return (
+            json.dumps({"success": False, "id_media": id_media, "message": repr(e)}),
+            500,
+            {"ContentType": "application/json"},
+        )
     else:
         print("file exists")
 
diff --git a/docs/api/source/taxhub.tests.rst b/docs/api/source/taxhub.tests.rst
new file mode 100644
index 00000000..f46e4707
--- /dev/null
+++ b/docs/api/source/taxhub.tests.rst
@@ -0,0 +1,37 @@
+taxhub.tests package
+====================
+
+Submodules
+----------
+
+taxhub.tests.conftest module
+----------------------------
+
+.. automodule:: taxhub.tests.conftest
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.tests.test\_taxref module
+--------------------------------
+
+.. automodule:: taxhub.tests.test_taxref
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+taxhub.tests.utils module
+-------------------------
+
+.. automodule:: taxhub.tests.utils
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: taxhub.tests
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..0e181690
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,2 @@
+import sys 
+sys.path.insert(0, '../')
\ No newline at end of file
diff --git a/tests/coccinelle.jpg b/tests/coccinelle.jpg
new file mode 100644
index 00000000..cca48132
Binary files /dev/null and b/tests/coccinelle.jpg differ
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 00000000..b6e77476
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,43 @@
+import os, logging
+import pytest
+import psycopg2
+import json
+
+from flask import url_for
+from .settings import INFO_LOGIN
+import server
+
+
+def pytest_sessionstart(session):
+    """ before session.main() is called. """
+    app = server.init_app()
+    app.config["TESTING"] = True
+    # push the app_context
+    ctx = app.app_context()
+    ctx.push()
+    logging.disable(logging.DEBUG)
+
+    # # setup test data
+    # execute_script("delete_sample_data.sql")
+    # execute_script("sample_data.sql")
+
+
+@pytest.fixture
+def app():
+    app = server.init_app()
+    app.config["TESTING"] = True
+    app.config["WTF_CSRF_ENABLED"] = False
+    return app
+
+
+@pytest.fixture
+def client(app):
+    return app.test_client()
+
+
+def login(client):
+    client.post(
+        url_for("auth.login"),
+        data=json.dumps(INFO_LOGIN),
+        headers={"Content-Type": "application/json"},
+    )
diff --git a/tests/settings.py.sample b/tests/settings.py.sample
new file mode 100644
index 00000000..2728d950
--- /dev/null
+++ b/tests/settings.py.sample
@@ -0,0 +1,3 @@
+
+
+INFO_LOGIN = {"login": "test", "password": "test", "id_application": 2}
\ No newline at end of file
diff --git a/tests/test_media.py b/tests/test_media.py
new file mode 100644
index 00000000..0a522951
--- /dev/null
+++ b/tests/test_media.py
@@ -0,0 +1,79 @@
+import pytest
+import json
+import os
+from flask import url_for
+from .conftest import login
+
+@pytest.mark.usefixtures("client_class")
+class TestAPIMedia:
+    def test_get_tmediasbyTaxon(self):
+        response = self.client.get(url_for("t_media.get_tmediasbyTaxon", cdref=67111))
+        assert response.status_code == 200
+
+    def test_get_tmedias(self):
+        response = self.client.get(url_for("t_media.get_tmedias"))
+        assert response.status_code == 200
+        response = self.client.get(url_for("t_media.get_tmedias", id=1))
+        assert response.status_code == 200
+
+    def test_insert_tmedias_url(self):
+        # LOGIN
+        login(self.client)
+
+        data = {
+            "is_public": True,
+            "auteur": "Luc Viatour",
+            "url": "https://upload.wikimedia.org/wikipedia/commons/7/77/Coccinella_septempunctata_Luc_Viatour.JPG",
+            "id_type": 2,
+            "nom_type_media": "Photo",
+            "titre": "Coccinelle test url",
+            "desc_media": "CC",
+            "cd_ref": 11165,
+            "isFile": False,
+        }
+        response = self.client.post(
+            url_for("t_media.insertUpdate_tmedias"),
+            data=json.dumps(data),
+            headers={"Content-Type": "application/json"},
+        )
+
+        assert response.status_code == 200
+
+        id_media = json.loads(response.data)["id_media"]
+        self.get_thumbnail(id_media)
+
+    def test_insert_tmedias_file(self):
+        # LOGIN
+        login(self.client)
+        # Test send file
+        file = os.path.join("coccinelle.jpg")
+        data = {
+            "is_public": True,
+            "auteur": "???",
+            "id_type": 2,
+            "nom_type_media": "Photo",
+            "titre": "Coccinelle test fichier",
+            "desc_media": "CC",
+            "cd_ref": 11165,
+            "isFile": True,
+        }
+        data["file"] = (file, "coccinelle.jpg")
+
+        response = self.client.post(
+            url_for("t_media.insertUpdate_tmedias"),
+            data=data,
+            content_type="multipart/form-data",
+        )
+
+        assert response.status_code == 200
+
+        id_media = json.loads(response.data)["id_media"]
+        self.get_thumbnail(id_media)
+
+    def get_thumbnail(self, id_media):
+
+        response = self.client.get(
+            url_for("t_media.getThumbnail_tmedias", id_media=id_media),
+
+        )
+        assert response.status_code == 200
diff --git a/tests/test_taxref.py b/tests/test_taxref.py
new file mode 100644
index 00000000..4cff1252
--- /dev/null
+++ b/tests/test_taxref.py
@@ -0,0 +1,113 @@
+
+import pytest
+from flask import url_for
+from .utils import json_of_response
+from schema import Schema, Optional, Or
+
+
+@pytest.mark.usefixtures('client_class')
+class TestAPITaxref:
+    schema_allnamebyListe = Schema([{
+        "cd_nom": int,
+        "search_name": str,
+        "cd_ref": int,
+        "nom_valide": str,
+        "nom_vern": Or(None, str),
+        "lb_nom": str,
+        "regne": str,
+        "group2_inpn": str
+    }])
+
+    def test_get_allnamebyListe_routes(self):
+        query_string = {"limit": 10}
+        response = self.client.get(
+            url_for("taxref.get_AllTaxrefNameByListe", id_liste=100),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+        data = json_of_response(response)
+        if data:
+            assert self.schema_allnamebyListe.is_valid(data)
+
+    def test_get_distinct_routes(self):
+        response = self.client.get(
+            url_for("taxref.getDistinctField", field="regne")
+        )
+        assert response.status_code == 200
+
+    def test_get_hierarchy_routes(self):
+
+        query_string = {"ilike": "pla"}
+        response = self.client.get(
+            url_for("taxref.getTaxrefHierarchie", rang="KD"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+        response = self.client.get(
+            url_for("taxref.getTaxrefHierarchieBibNoms", rang="FM"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+
+        query_string = {"ilike": "pl", "regne": "Plantae"}
+        response = self.client.get(
+            url_for("taxref.getTaxrefHierarchie", rang="FM"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+        response = self.client.get(
+            url_for("taxref.getTaxrefHierarchieBibNoms", rang="FM"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+
+        query_string = {"ilike": "pl", "regne": "Plantae"}
+
+    def test_searchfield_routes(self):
+
+        query_string = {"ilike": "pla"}
+        response = self.client.get(
+            url_for("taxref.getSearchInField", field="lb_nom", ilike="poa"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+
+    def test_taxrefDetail_routes(self):
+        response = self.client.get(
+            url_for("taxref.getTaxrefDetail", id=29708)
+        )
+        assert response.status_code == 200
+
+    def test_searchTaxref_routes(self):
+        query_string = {
+            "ilike-classe": "hex",
+            "page": 1,
+            "limit": 10
+        }
+        response = self.client.get(
+            url_for("taxref.getTaxrefList"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+        response = self.client.get(
+            url_for("taxref.getTaxrefBibtaxonList"),
+            query_string=query_string
+        )
+        assert response.status_code == 200
+
+    def test_regneGroup2Inpn_routes(self):
+        response = self.client.get(
+            url_for("taxref.get_regneGroup2Inpn_taxref")
+        )
+        assert response.status_code == 200
+
+    def test_bib_routes(self):
+        response = self.client.get(
+            url_for("taxref.get_bib_lr")
+        )
+        assert response.status_code == 200
+
+        response = self.client.get(
+            url_for("taxref.get_bib_hab")
+        )
+        assert response.status_code == 200
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 00000000..74ee3ba0
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,19 @@
+"""
+    Fonctions utilitaires pour les tests
+"""
+import json
+
+def post_json(client, url, json_dict, query_string=None):
+    """Send dictionary json_dict as a json to the specified url """
+    return client.post(
+        url,
+        data=json.dumps(json_dict),
+        content_type="application/json",
+        query_string=query_string,
+    )
+
+
+def json_of_response(response):
+    """Decode json from response"""
+    return json.loads(response.data.decode("utf8"))
+