diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py
new file mode 100644
index 0000000000000000000000000000000000000000..97a6311318e23e56d52c931f5272ad037ad376d2
--- /dev/null
+++ b/src/Model/Reservoir/Reservoir.py
@@ -0,0 +1,268 @@
+# Reservoir.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from Model.Tools.PamhyrDB import SQLSubModel
+
+logger = logging.getLogger()
+
+
+class Reservoir(SQLSubModel):
+    _sub_classes = []
+    _id_cnt = 0
+
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(Reservoir, self).__init__()
+
+        self._status = status
+
+        if id == -1:
+            self.id = Reservoir._id_cnt
+        else:
+            self.id = id
+
+        self._name = name
+        self._node = None
+        self._data = []
+
+        Reservoir._id_cnt = max(Reservoir._id_cnt + 1, self.id)
+
+    @classmethod
+    def _db_create(cls, execute):
+        execute("""
+          CREATE TABLE reservoir(
+            id INTEGER NOT NULL PRIMARY KEY,
+            name TEXT NOT NULL,
+            node INTEGER,
+            FOREIGN KEY(node) REFERENCES river_node(id)
+          )
+        """)
+
+        execute("""
+          CREATE TABLE reservoir_data(
+            id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+            ind INTEGER NOT NULL,
+            elevation REAL NOT NULL,
+            surface REAL NOT NULL,
+            reservoir INTEGER,
+            FOREIGN KEY(reservoir) REFERENCES reservoir(id)
+          )
+        """)
+
+        return cls._create_submodel(execute)
+
+    @classmethod
+    def _db_update(cls, execute, version):
+        major, minor, release = version.strip().split(".")
+        if major == minor == "0":
+            if int(release) < 5:
+                cls._db_create(execute)
+
+        return True
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = []
+
+        table = execute(
+            "SELECT id, name, node " +
+            "FROM reservoir "
+        )
+
+        for row in table:
+            id = row[0]
+            name = row[1]
+            node_id = row[2]
+            new_reservoir = cls(id, name, status=data["status"])
+
+            new_reservoir._node = None
+            if node_id != -1:
+                new_reservoir._node = next(filter(lambda n: n.id == node_id, data["nodes"]))
+
+            new_data = []
+            table = execute(
+                "SELECT elevation, surface " +
+                "FROM reservoir_data " +
+                f"WHERE reservoir = {id} " +
+                "ORDER BY ind ASC"
+            )
+            for t in table:
+                new_data.append((t[0], t[1]))
+
+            new_reservoir._data = new_data
+
+            new.append(new_reservoir)
+
+        return new
+
+    def _db_save(self, execute, data=None):
+
+        execute(f"DELETE FROM reservoir WHERE id = {self.id}")
+        execute(f"DELETE FROM reservoir_data WHERE reservoir = {self.id}")
+
+        node_id = -1
+        if self._node is not None:
+            node_id = self._node.id
+
+        sql = (
+            "INSERT INTO " +
+            "reservoir(id, name, node) " +
+            "VALUES (" +
+            f"{self.id}, '{self._db_format(self._name)}', " +
+            f"{node_id}" +
+            ")"
+        )
+        execute(sql)
+
+        ind = 0
+        for d in self._data:
+            sql = (
+                "INSERT INTO " +
+                "reservoir_data(ind, elevation, surface, reservoir) " +
+                f"VALUES ({ind}, '{d[0]}', {d[1]}, {self.id})"
+            )
+            execute(sql)
+            ind += 1
+
+        return True
+
+    def __len__(self):
+        return len(self._data)
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+        self._status.modified()
+
+    @property
+    def node(self):
+        return self._node
+
+    @node.setter
+    def node(self, node):
+        self._node = node
+        self._status.modified()
+
+    def has_node(self):
+        return self._node is not None
+
+    @property
+    def data(self):
+        return self._data.copy()
+
+    @property
+    def _default_elevation(self):
+        return 0.0
+
+    @property
+    def _default_surface(self):
+        return 0.0
+
+    def is_define(self):
+        return len(self._data) != 0
+
+    def new_from_data(self, data):
+
+        try:
+            new_0 = float(data[0])
+            new_1 = float(data[1])
+        except Exception as e:
+            logger.error(e)
+            new_0 = None
+            new_1 = None
+
+        return (new_0, new_1)
+
+    def add(self, index: int):
+        value = (self._default_elevation, self._default_surface)
+        self._data.insert(index, value)
+        self._status.modified()
+        return value
+
+    def insert(self, index: int, value):
+        self._data.insert(index, value)
+        self._status.modified()
+
+    def delete_i(self, indexes):
+        self._data = list(
+            map(
+                lambda e: e[1],
+                filter(
+                    lambda e: e[0] not in indexes,
+                    enumerate(self.data)
+                )
+            )
+        )
+        self._status.modified()
+
+    def delete(self, els):
+        self._data = list(
+            filter(
+                lambda e: e not in els,
+                self.data
+            )
+        )
+        self._status.modified()
+
+    def sort(self, _reverse=False, key=None):
+        if key is None:
+            self._data.sort(reverse=_reverse)
+        else:
+            self._data.sort(reverse=_reverse, key=key)
+        self._status.modified()
+
+    def get_i(self, index):
+        return self.data[index]
+
+    def get_range(self, _range):
+        lst = []
+        for r in _range:
+            lst.append(r)
+        return lst
+
+    def _set_i_c_v(self, index, column, value):
+        v = list(self._data[index])
+        v[column] = value
+        self._data[index] = tuple(v)
+        self._status.modified()
+
+    def set_i_elevation(self, index: int, value):
+        self._set_i_c_v(index, 0, value)
+
+    def set_i_surface(self, index: int, value):
+        self._set_i_c_v(index, 1, value)
+
+    def move_up(self, index):
+        if index < len(self):
+            next = index - 1
+            d = self._data
+            d[index], d[next] = d[next], d[index]
+            self._status.modified()
+
+    def move_down(self, index):
+        if index >= 0:
+            prev = index + 1
+            d = self._data
+            d[index], d[prev] = d[prev], d[index]
+            self._status.modified()
diff --git a/src/Model/Reservoir/ReservoirList.py b/src/Model/Reservoir/ReservoirList.py
new file mode 100644
index 0000000000000000000000000000000000000000..60d2b98ef5989433b5e4cc1cd8066eff2e8e3c0b
--- /dev/null
+++ b/src/Model/Reservoir/ReservoirList.py
@@ -0,0 +1,87 @@
+# ReservoirList.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from copy import copy
+from tools import trace, timer
+
+from Model.Tools.PamhyrList import PamhyrModelList
+from Model.Reservoir.Reservoir import Reservoir
+
+
+class ReservoirList(PamhyrModelList):
+    _sub_classes = [
+        Reservoir,
+    ]
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = cls(status=data['status'])
+
+        new._lst = Reservoir._db_load(
+            execute, data
+        )
+
+        return new
+
+    def _db_save(self, execute, data=None):
+        execute("DELETE FROM reservoir")
+        execute("DELETE FROM reservoir_data")
+
+        if data is None:
+            data = {}
+
+        for reservoir in self._lst:
+            reservoir._db_save(execute, data=data)
+
+        return True
+
+    def new(self, index):
+        r = Reservoir(status=self._status)
+        self._lst.insert(index, r)
+        self._status.modified()
+        return r
+
+    def __copy__(self):
+        new = ReservoirList()
+
+        new._lst = self._lst.copy()
+
+        return new
+
+    def __deepcopy__(self):
+        new = ReservoirList()
+
+        new._lst = self._lst.deepcopy()
+
+        return new
+
+    def copy(self):
+        return copy(self)
+
+    def get_assoc_to_node(self, node):
+        assoc = list(
+            filter(
+                lambda i: i.node is node,
+                self._lst
+            )
+        )
+
+        if len(assoc) > 0:
+            return assoc[0]
+
+        return None
diff --git a/src/Model/River.py b/src/Model/River.py
index 5047b43578bb2c2e136a06902f6bf24fd4324c25..84836625f83f2d7e69031a744d07b3a6aa5803e6 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -36,6 +36,7 @@ from Model.Stricklers.StricklersList import StricklersList
 from Model.Friction.FrictionList import FrictionList
 from Model.SolverParameters.SolverParametersList import SolverParametersList
 from Model.SedimentLayer.SedimentLayerList import SedimentLayerList
+from Model.Reservoir.ReservoirList import ReservoirList
 
 from Solver.Solvers import solver_type_list
 
@@ -217,6 +218,7 @@ class River(Graph, SQLSubModel):
         StricklersList,
         SolverParametersList,
         SedimentLayerList,
+        ReservoirList,
     ]
 
     def __init__(self, status=None):
@@ -234,6 +236,7 @@ class River(Graph, SQLSubModel):
         self._stricklers = StricklersList(status=self._status)
         self._parameters = {}
         self._sediment_layers = SedimentLayerList(status=self._status)
+        self._reservoir = ReservoirList(status=self._status)
 
     @classmethod
     def _db_create(cls, execute):
@@ -294,6 +297,12 @@ class River(Graph, SQLSubModel):
             data
         )
 
+        # Reservoir
+        new._reservoir = ReservoirList._db_load(
+            execute,
+            data
+        )
+
         # Parameters
         new._parameters = SolverParametersList._db_load(
             execute,
@@ -309,6 +318,7 @@ class River(Graph, SQLSubModel):
         objs.append(self._lateral_contribution)
         objs.append(self._sediment_layers)
         objs.append(self._stricklers)
+        objs.append(self._reservoir)
 
         for solver in self._parameters:
             objs.append(self._parameters[solver])
@@ -349,6 +359,10 @@ class River(Graph, SQLSubModel):
 
         return ret[0]
 
+    @property
+    def reservoir(self):
+        return self._reservoir
+
     @property
     def parameters(self):
         return self._parameters
diff --git a/src/Model/Study.py b/src/Model/Study.py
index e9ccdb41b2527e1c5ba6b6e6929e4d5f3b2031be..b163088b8d93312297d38c7ae469f800233269df 100644
--- a/src/Model/Study.py
+++ b/src/Model/Study.py
@@ -41,7 +41,7 @@ class Study(SQLModel):
 
     def __init__(self, filename=None, init_new=True):
         # Metadata
-        self._version = "0.0.4"
+        self._version = "0.0.5"
         self.creation_date = datetime.now()
         self.last_modification_date = datetime.now()
         self.last_save_date = datetime.now()
diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py
index 3a785e4e022743ee23753b21598bbf2389b3cc03..a62a66650a9111f915a51f02748c49f5b774eea1 100644
--- a/src/Solver/Mage.py
+++ b/src/Solver/Mage.py
@@ -412,6 +412,36 @@ class Mage(CommandLineSolver):
             files.append(f"{name}.INI")
         return files
 
+    @timer
+    def _export_CAS(self, study, repertory, qlog, name="0"):
+        files = []
+
+        reservoirs = study.river.reservoir.lst
+        if len(reservoirs) == 0:
+            return files
+
+        if qlog is not None:
+            qlog.put("Export CAS file")
+
+        with mage_file_open(os.path.join(repertory, f"{name}.CAS"), "w+") as f:
+            files.append(f"{name}.CAS")
+
+            for reservoir in reservoirs:
+                reservoir.sort()
+                node = reservoir.node
+                name = f"{node.id:3}".replace(" ", "x")
+                f.write(f"* {node.name} ({name}) Reservoir\n")
+                f.write(f"${name}\n")
+                f.write(f"*{'Elev(m)':>9}|{'Area(ha)':>10}\n")
+
+                for d in reservoir.data:
+                    v0 = d[0]
+                    v1 = d[1]
+
+                    f.write(f"{v0:>10.3f}{v1:>10.3f}\n")
+
+        return files
+
     @timer
     def _export_REP(self, study, repertory, files, qlog, name="0"):
         if qlog is not None:
@@ -653,6 +683,7 @@ class Mage8(Mage):
             self._export_bound_cond(study, repertory, qlog, name=name)
         files = files + self._export_RUG(study, repertory, qlog, name=name)
         files = files + self._export_INI(study, repertory, qlog, name=name)
+        files = files + self._export_CAS(study, repertory, qlog, name=name)
         self._export_REP(study, repertory, files, qlog, name=name)
 
         return True
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 57ad99960079bf920343ff80ced700d74ca89247..04ed5f94267aad9aad4e2419b6aea1fab59b47f1 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -47,6 +47,7 @@ from View.About.Window import AboutWindow
 from View.Network.Window import NetworkWindow
 from View.Geometry.Window import GeometryWindow
 from View.BoundaryCondition.Window import BoundaryConditionWindow
+from View.Reservoir.Window import ReservoirWindow
 from View.LateralContribution.Window import LateralContributionWindow
 from View.InitialConditions.Window import InitialConditionsWindow
 from View.Stricklers.Window import StricklersWindow
@@ -101,7 +102,7 @@ define_model_action = [
     "action_menu_boundary_conditions", "action_menu_initial_conditions",
     "action_menu_edit_friction", "action_menu_edit_lateral_contribution",
     "action_menu_run_solver", "action_menu_sediment_layers",
-    "action_menu_edit_reach_sediment_layers"
+    "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs"
 ]
 
 action = (
@@ -191,6 +192,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
             "action_menu_edit_network": self.open_network,
             "action_menu_edit_geometry": self.open_geometry,
             "action_menu_boundary_conditions": self.open_boundary_cond,
+            "action_menu_edit_reservoirs": self.open_reservoir,
             "action_menu_initial_conditions": self.open_initial_conditions,
             "action_menu_edit_friction": self.open_frictions,
             "action_menu_edit_lateral_contribution": self.open_lateral_contrib,
@@ -616,6 +618,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
         bound = BoundaryConditionWindow(study=self._study, parent=self)
         bound.show()
 
+    def open_reservoir(self):
+        if self.sub_window_exists(
+            ReservoirWindow,
+            data=[self._study, None]
+        ):
+            return
+
+        reservoir = ReservoirWindow(study=self._study, parent=self)
+        reservoir.show()
+
     def open_lateral_contrib(self):
         if self.sub_window_exists(
             LateralContributionWindow,
diff --git a/src/View/Reservoir/Edit/Plot.py b/src/View/Reservoir/Edit/Plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f8f49b858228c34d3b143ec17bea797afa9f146
--- /dev/null
+++ b/src/View/Reservoir/Edit/Plot.py
@@ -0,0 +1,101 @@
+# Plot.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from datetime import datetime
+
+from tools import timer, trace
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from PyQt5.QtCore import (
+    QCoreApplication
+)
+
+from View.BoundaryCondition.Edit.translate import BCETranslate
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class Plot(PamhyrPlot):
+    def __init__(self, mode="time", data=None,
+                 trad=None, canvas=None, toolbar=None,
+                 parent=None):
+        super(Plot, self).__init__(
+            canvas=canvas,
+            trad=trad,
+            data=data,
+            toolbar=toolbar,
+            parent=parent
+        )
+
+        self._table_headers = self._trad.get_dict("table_headers")
+        self._mode = mode
+
+    @timer
+    def draw(self):
+        self.canvas.axes.cla()
+        self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
+
+        if len(self.data) == 0:
+            self._init = False
+            return
+
+        # Plot data
+        x = list(map(lambda v: v[0], self.data.data))
+        y = list(map(lambda v: v[1], self.data.data))
+        self._line, = self.canvas.axes.plot(
+            x, y,
+            color='r', lw=1.,
+            markersize=5, marker='+',
+            picker=30,
+        )
+
+        # Plot label
+        #header = self.data.header
+        self.canvas.axes.set_xlabel(
+            self._table_headers["z"], color='black', fontsize=10
+        )
+        self.canvas.axes.set_ylabel(
+            self._table_headers["Area"], color='black', fontsize=10
+        )
+
+        self.canvas.axes.autoscale_view(True, True, True)
+        self.canvas.figure.tight_layout()
+        self.canvas.figure.canvas.draw_idle()
+        # self.toolbar.update()
+
+        self._init = True
+
+    @timer
+    def update(self, ind=None):
+        if not self._init:
+            self.draw()
+            return
+
+        x = list(map(lambda v: v[0], self.data.data))
+        y = list(map(lambda v: v[1], self.data.data))
+
+        self._line.set_data(x, y)
+
+        self.canvas.axes.relim()
+        self.canvas.axes.autoscale()
+        self.canvas.figure.tight_layout()
+        self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/Reservoir/Edit/Table.py b/src/View/Reservoir/Edit/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..21e0f91bc95bbdcaf2103685df6a22354851df64
--- /dev/null
+++ b/src/View/Reservoir/Edit/Table.py
@@ -0,0 +1,141 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from datetime import date, time, datetime, timedelta
+
+from tools import trace, timer
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect, QTime, QDateTime,
+)
+
+from PyQt5.QtWidgets import (
+    QTableView, QAbstractItemView, QSpinBox, QItemDelegate,
+)
+
+from View.Reservoir.Edit.UndoCommand import (
+    SetDataCommand, AddCommand, DelCommand,
+    SortCommand, PasteCommand,
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class TableModel(PamhyrTableModel):
+    def data(self, index, role):
+        if role == Qt.TextAlignmentRole:
+            return Qt.AlignHCenter | Qt.AlignVCenter
+
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        value = QVariant()
+
+        if 0 <= column < 2:
+            v = self._data.get_i(row)[column]
+            value = f"{v:.4f}"
+
+        return value
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        row = index.row()
+        column = index.column()
+
+        try:
+            self._undo.push(
+                SetDataCommand(
+                    self._data, row, column, float(value)
+                )
+            )
+        except Exception as e:
+            logger.info(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._data, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._data, rows
+            )
+        )
+
+        self.endRemoveRows()
+
+    def sort(self, _reverse, parent=QModelIndex()):
+        self.layoutAboutToBeChanged.emit()
+
+        self._undo.push(
+            SortCommand(
+                self._data, _reverse
+            )
+        )
+
+        self.layoutAboutToBeChanged.emit()
+        self.layoutChanged.emit()
+
+    def paste(self, row, data):
+        if len(data) == 0:
+            return
+
+        self.layoutAboutToBeChanged.emit()
+
+        self._undo.push(
+            PasteCommand(
+                self._data, row,
+                list(
+                    map(
+                        lambda d: self._data.new_from_data(d),
+                        data
+                    )
+                )
+            )
+        )
+
+        self.layoutAboutToBeChanged.emit()
+        self.layoutChanged.emit()
diff --git a/src/View/Reservoir/Edit/Translate.py b/src/View/Reservoir/Edit/Translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7660885faa2fe83bbce0ad9550efb8397152b21
--- /dev/null
+++ b/src/View/Reservoir/Edit/Translate.py
@@ -0,0 +1,35 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Tools.PamhyrTranslate import PamhyrTranslate
+
+from View.Reservoir.Translate import ReservoirTranslate
+
+_translate = QCoreApplication.translate
+
+
+class EditReservoirTranslate(ReservoirTranslate):
+    def __init__(self):
+        super(EditReservoirTranslate, self).__init__()
+
+        self._sub_dict["table_headers"] = {
+            "z": _translate("Reservoir", "Elevation (m)"),
+            "Area": _translate("Reservoir", "Area (hectare)"),
+        }
diff --git a/src/View/Reservoir/Edit/UndoCommand.py b/src/View/Reservoir/Edit/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..22b7a459657530f8edca01a44d972b746a75b4bc
--- /dev/null
+++ b/src/View/Reservoir/Edit/UndoCommand.py
@@ -0,0 +1,135 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.Reservoir.Reservoir import Reservoir
+
+logger = logging.getLogger()
+
+
+class SetDataCommand(QUndoCommand):
+    def __init__(self, data, index, column, new_value):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._index = index
+        self._column = column
+        self._old = self._data.get_i(self._index)[self._column]
+        self._new = new_value
+
+    def undo(self):
+        self._data._set_i_c_v(self._index, self._column, self._old)
+
+    def redo(self):
+        self._data._set_i_c_v(self._index, self._column, self._new)
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, data, index):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._data.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._data.add(self._index)
+        else:
+            self._data.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, data, rows):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._rows = rows
+
+        self._point = []
+        for row in rows:
+            self._point.append((row, self._data.get_i(row)))
+        self._point.sort()
+
+    def undo(self):
+        for row, el in self._point:
+            self._data.insert(row, el)
+
+    def redo(self):
+        self._data.delete_i(self._rows)
+
+
+class SortCommand(QUndoCommand):
+    def __init__(self, data, _reverse):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._reverse = _reverse
+
+        self._old = self._data.data
+        self._indexes = None
+
+    def undo(self):
+        ll = self._data.data
+        self._data.sort(
+            key=lambda x: self._indexes[ll.index(x)]
+        )
+
+    def redo(self):
+        self._data.sort(
+            _reverse=self._reverse,
+            key=lambda x: x[0]
+        )
+        if self._indexes is None:
+            self._indexes = list(
+                map(
+                    lambda p: self._old.index(p),
+                    self._data.data
+                )
+            )
+            self._old = None
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, data, row, hs):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._row = row
+        self._h = hs
+        self._h.reverse()
+
+    def undo(self):
+        self._data.delete_i(
+            range(self._row, self._row + len(self._h))
+        )
+
+    def redo(self):
+        for h in self._h:
+            self._data.insert(self._row, h)
diff --git a/src/View/Reservoir/Edit/Window.py b/src/View/Reservoir/Edit/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..34724bea63ab44e4e37aec5557fd02a32a2bd9f5
--- /dev/null
+++ b/src/View/Reservoir/Edit/Window.py
@@ -0,0 +1,217 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+from View.Tools.PamhyrWidget import PamhyrWidget
+
+from PyQt5.QtGui import (
+    QKeySequence,
+)
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel, QCoreApplication,
+    pyqtSlot, pyqtSignal,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QHeaderView, QDoubleSpinBox, QVBoxLayout,
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.Reservoir.Edit.Translate import EditReservoirTranslate
+from View.Reservoir.Edit.Table import TableModel
+from View.Reservoir.Edit.Plot import Plot
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class EditReservoirWindow(PamhyrWindow):
+    _pamhyr_ui = "Reservoir"
+    _pamhyr_name = "Edit Reservoir"
+
+    def __init__(self, data=None, study=None, config=None, parent=None):
+        self._data = data
+        trad = EditReservoirTranslate()
+
+        name = self._pamhyr_name
+        if self._data is not None:
+            node_name = (self._data.node.name if self._data.node is not None
+                         else _translate("Reservoir", "Not associated"))
+            name = (
+                _translate("Edit Reservoir", self._pamhyr_name) +
+                f" - {study.name} " +
+                f" - {self._data.name} ({self._data.id}) " +
+                f"({node_name})"
+            )
+
+        super(EditReservoirWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=trad,
+            parent=parent
+        )
+
+        self._hash_data.append(data)
+
+        self.setup_table()
+        self.setup_plot()
+        self.setup_connections()
+
+    def setup_table(self):
+        headers = {}
+        table_headers = self._trad.get_dict("table_headers")
+        #for h in self._data.header:
+            #headers[h] = table_headers[h]
+
+        table = self.find(QTableView, "tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=table_headers,
+            editable_headers=table_headers,
+            #editable_headers=self._data.header,
+            delegates={
+                #"time": self._delegate_time,
+            },
+            data=self._data,
+            undo=self._undo_stack,
+            opt_data=self._study.time_system
+        )
+
+        table.setModel(self._table)
+        table.setSelectionBehavior(QAbstractItemView.SelectRows)
+        table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+        table.setAlternatingRowColors(True)
+
+    def setup_plot(self):
+        self.canvas = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas.setObjectName("canvas")
+        self.toolbar = PamhyrPlotToolbar(
+            self.canvas, self
+        )
+        self.verticalLayout.addWidget(self.toolbar)
+        self.verticalLayout.addWidget(self.canvas)
+
+        self.plot = Plot(
+            canvas=self.canvas,
+            data=self._data,
+            mode=self._study.time_system,
+            trad=self._trad,
+            toolbar=self.toolbar,
+        )
+        self.plot.draw()
+
+    def setup_connections(self):
+        self.find(QAction, "action_add").triggered.connect(self.add)
+        self.find(QAction, "action_delete").triggered.connect(self.delete)
+        self.find(QAction, "action_sort").triggered.connect(self.sort)
+
+        self._table.dataChanged.connect(self.update)
+
+    def update(self):
+        self.plot.update()
+
+    def index_selected_row(self):
+        table = self.find(QTableView, "tableView")
+        return table.selectionModel()\
+                    .selectedRows()[0]\
+                    .row()
+
+    def index_selected_rows(self):
+        table = self.find(QTableView, "tableView")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+        if len(self._data) == 0 or len(rows) == 0:
+            self._table.add(0)
+        else:
+            self._table.add(rows[0])
+
+        self.plot.update()
+
+    def delete(self):
+        rows = self.index_selected_rows()
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+        self.plot.update()
+
+    def sort(self):
+        self._table.sort(False)
+        self.plot.update()
+
+    def _copy(self):
+        rows = self.index_selected_rows()
+
+        table = []
+        #table.append(self._data.header)
+        table.append(self._trad.get_dict("table_headers"))
+
+        data = self._data.data
+        for row in rows:
+            table.append(list(data[row]))
+
+        self.copyTableIntoClipboard(table)
+
+    def _paste(self):
+        header, data = self.parseClipboardTable()
+
+        logger.debug(f"paste: h:{header}, d:{data}")
+
+        if len(data) == 0:
+            return
+
+        row = 0
+        rows = self.index_selected_rows()
+        if len(rows) != 0:
+            row = rows[0]
+
+        self._table.paste(row, data)
+        self.plot.update()
+
+    def _undo(self):
+        self._table.undo()
+        self.plot.update()
+        self.widget_update()
+
+    def _redo(self):
+        self._table.redo()
+        self.plot.update()
+        self.widget_update()
diff --git a/src/View/Reservoir/Table.py b/src/View/Reservoir/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..66cdae82109f2aa212ea68b249ad310fb6e984be
--- /dev/null
+++ b/src/View/Reservoir/Table.py
@@ -0,0 +1,170 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from tools import trace, timer
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QComboBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.Reservoir.UndoCommand import (
+    SetNameCommand, SetNodeCommand,
+    AddCommand, DelCommand, PasteCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+    def __init__(self, data=None, trad=None, parent=None):
+        super(ComboBoxDelegate, self).__init__(parent)
+
+        self._data = data
+        self._trad = trad
+
+    def createEditor(self, parent, option, index):
+        self.editor = QComboBox(parent)
+
+        self.editor.addItems(
+            [_translate("Reservoir", "Not associated")] +
+            self._data.nodes_names()
+        )
+
+        self.editor.setCurrentText(index.data(Qt.DisplayRole))
+        return self.editor
+
+    def setEditorData(self, editor, index):
+        value = index.data(Qt.DisplayRole)
+        self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+    def setModelData(self, editor, model, index):
+        text = str(editor.currentText())
+        model.setData(index, text)
+        editor.close()
+        editor.deleteLater()
+
+    def updateEditorGeometry(self, editor, option, index):
+        r = QRect(option.rect)
+        if self.editor.windowFlags() & Qt.Popup:
+            if editor.parent() is not None:
+                r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+        editor.setGeometry(r)
+
+    @pyqtSlot()
+    def currentItemChanged(self):
+        self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+    def _setup_lst(self):
+        self._lst = self._data.reservoir
+
+    def rowCount(self, parent):
+        return len(self._lst)
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        if self._headers[column] == "name":
+            return self._lst.get(row).name
+        elif self._headers[column] == "node":
+            n = self._lst.get(row).node
+            if n is None:
+                return _translate("Reservoir", "Not associated")
+            return n.name
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        row = index.row()
+        column = index.column()
+
+        try:
+            if self._headers[column] == "name":
+                self._undo.push(
+                    SetNameCommand(
+                        self._lst, row, value
+                    )
+                )
+            elif self._headers[column] == "node":
+                self._undo.push(
+                    SetNodeCommand(
+                        self._lst, row, self._data.node(value)
+                    )
+                )
+        except Exception as e:
+            logger.info(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._lst, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._lst, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def undo(self):
+        self._undo.undo()
+        self.layoutChanged.emit()
+
+    def redo(self):
+        self._undo.redo()
+        self.layoutChanged.emit()
diff --git a/src/View/Reservoir/Translate.py b/src/View/Reservoir/Translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..556c9381e0c12575b3b360914e460fb2701b0694
--- /dev/null
+++ b/src/View/Reservoir/Translate.py
@@ -0,0 +1,33 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Tools.PamhyrTranslate import PamhyrTranslate
+
+_translate = QCoreApplication.translate
+
+
+class ReservoirTranslate(PamhyrTranslate):
+    def __init__(self):
+        super(ReservoirTranslate, self).__init__()
+
+        self._sub_dict["table_headers"] = {
+            "name": _translate("Reservoir", "Name"),
+            "node": _translate("Reservoir", "Node")
+        }
diff --git a/src/View/Reservoir/UndoCommand.py b/src/View/Reservoir/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..078c31d316484ced6a57b611a666f0402272eac8
--- /dev/null
+++ b/src/View/Reservoir/UndoCommand.py
@@ -0,0 +1,127 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.Reservoir.Reservoir import Reservoir
+from Model.Reservoir.ReservoirList import ReservoirList
+
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, reservoir_lst, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._reservoir_lst = reservoir_lst
+        self._index = index
+        self._old = self._reservoir_lst.get(self._index).name
+        self._new = str(new_value)
+
+    def undo(self):
+        self._reservoir_lst.get(self._index).name = self._old
+
+    def redo(self):
+        self._reservoir_lst.get(self._index).name = self._new
+
+
+class SetNodeCommand(QUndoCommand):
+    def __init__(self, reservoir_lst, index, node):
+        QUndoCommand.__init__(self)
+
+        self._reservoir_lst = reservoir_lst
+        self._index = index
+        self._old = self._reservoir_lst.get(self._index).node
+        self._new = node
+        self._prev_assoc_to_node = self._reservoir_lst.get_assoc_to_node(node)
+
+    def _previous_assoc_node(self, node):
+        if self._prev_assoc_to_node is not None:
+            self._prev_assoc_to_node.node = node
+
+    def undo(self):
+        self._reservoir_lst.get(self._index).node = self._old
+        self._previous_assoc_node(self._new)
+
+    def redo(self):
+        self._reservoir_lst.get(self._index).node = self._new
+        self._previous_assoc_node(None)
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, reservoir_lst, index):
+        QUndoCommand.__init__(self)
+
+        self._reservoir_lst = reservoir_lst
+
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._reservoir_lst.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._reservoir_lst.new(self._index)
+        else:
+            self._reservoir_lst.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, reservoir_lst, rows):
+        QUndoCommand.__init__(self)
+
+        self._reservoir_lst = reservoir_lst
+
+        self._rows = rows
+
+        self._reservoir = []
+        for row in rows:
+            self._reservoir.append((row, self._reservoir_lst.get(row)))
+        self._reservoir.sort()
+
+    def undo(self):
+        for row, el in self._reservoir:
+            self._reservoir_lst.insert(row, el)
+
+    def redo(self):
+        self._reservoir_lst.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, reservoir_lst, row, reservoir):
+        QUndoCommand.__init__(self)
+
+        self._reservoir_lst = reservoir_lst
+
+        self._row = row
+        self._reservoir = deepcopy(reservoir)
+        self._reservoir.reverse()
+
+    def undo(self):
+        self._reservoir_lst.delete_i(
+            self._tab,
+            range(self._row, self._row + len(self._reservoir))
+        )
+
+    def redo(self):
+        for r in self._reservoir:
+            self._reservoir_lst.insert(self._row, r)
diff --git a/src/View/Reservoir/Window.py b/src/View/Reservoir/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..147c05d157563d79859b80936123c280b63ddc1e
--- /dev/null
+++ b/src/View/Reservoir/Window.py
@@ -0,0 +1,173 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import trace, timer
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5.QtGui import (
+    QKeySequence,
+)
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
+)
+
+from View.Reservoir.Table import (
+    TableModel, ComboBoxDelegate
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.Reservoir.Translate import ReservoirTranslate
+from View.Reservoir.Edit.Window import EditReservoirWindow
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class ReservoirWindow(PamhyrWindow):
+    _pamhyr_ui = "ReservoirList"
+    _pamhyr_name = "Reservoir"
+
+    def __init__(self, study=None, config=None, parent=None):
+        name = self._pamhyr_name + " - " + study.name
+
+        super(ReservoirWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=ReservoirTranslate(),
+            parent=parent
+        )
+
+        self._reservoir_lst = self._study.river.reservoir
+
+        self.setup_table()
+        self.setup_graph()
+        self.setup_connections()
+
+    def setup_table(self):
+        self._table = None
+
+        self._delegate_node = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            parent=self
+        )
+
+        table = self.find(QTableView, f"tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers"),
+            editable_headers=["name", "node"],
+            delegates={
+                "node": self._delegate_node,
+            },
+            trad=self._trad,
+            data=self._study.river,
+            undo=self._undo_stack,
+        )
+
+    def setup_graph(self):
+        self.graph_widget = GraphWidget(
+            self._study.river,
+            min_size=None, size=(200, 200),
+            only_display=True,
+            parent=self
+        )
+        self.graph_layout = self.find(QVBoxLayout, "verticalLayout")
+        self.graph_layout.addWidget(self.graph_widget)
+
+    def setup_connections(self):
+        self.find(QAction, "action_add").triggered.connect(self.add)
+        self.find(QAction, "action_delete").triggered.connect(self.delete)
+        self.find(QAction, "action_edit").triggered.connect(self.edit)
+
+    def index_selected_row(self):
+        table = self.find(QTableView, "tableView")
+        return table.selectionModel()\
+                    .selectedRows()[0]\
+                    .row()
+
+    def index_selected_rows(self):
+        table = self.find(QTableView, "tableView")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+        if len(self._reservoir_lst) == 0 or len(rows) == 0:
+            self._table.add(0)
+        else:
+            self._table.add(rows[0])
+
+    def delete(self):
+        rows = self.index_selected_rows()
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+
+    def _copy(self):
+        logger.info("TODO: copy")
+
+    def _paste(self):
+        logger.info("TODO: paste")
+
+    def _undo(self):
+        self._table.undo()
+
+    def _redo(self):
+        self._table.redo()
+
+    def edit(self):
+        rows = self.index_selected_rows()
+        for row in rows:
+            data = self._reservoir_lst.get(row)
+
+            if self.sub_window_exists(
+                EditReservoirWindow,
+                data=[self._study, None, data]
+            ):
+                continue
+
+            win = EditReservoirWindow(
+                data=data,
+                study=self._study,
+                parent=self
+            )
+            win.show()
diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui
index f33e6dcfb8892aa3c87f5fc6f0dc2ada6e31d474..9476708ceb809a9bf54dd129f6bc2330648a93ef 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -131,6 +131,7 @@
     <addaction name="action_menu_initial_conditions"/>
     <addaction name="action_menu_edit_friction"/>
     <addaction name="action_menu_edit_lateral_contribution"/>
+    <addaction name="action_menu_edit_reservoirs"/>
    </widget>
    <widget class="QMenu" name="menu_results">
     <property name="title">
@@ -492,10 +493,7 @@
     <string>Boundary conditions and one-time contributions</string>
    </property>
    <property name="font">
-    <font>
-     <weight>50</weight>
-     <bold>false</bold>
-    </font>
+    <font/>
    </property>
   </action>
   <action name="action_menu_initial_conditions">
@@ -600,11 +598,7 @@
     <string>Visualize the last results</string>
    </property>
    <property name="font">
-    <font>
-     <family>Ubuntu</family>
-     <weight>50</weight>
-     <bold>false</bold>
-    </font>
+    <font/>
    </property>
   </action>
   <action name="action_plot_limnigram">
@@ -932,6 +926,14 @@
     <string>Developers (html)</string>
    </property>
   </action>
+  <action name="action_menu_edit_reservoirs">
+   <property name="text">
+    <string>Reservoirs</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit reservoirs</string>
+   </property>
+  </action>
  </widget>
  <resources/>
  <connections>
diff --git a/src/View/ui/Reservoir.ui b/src/View/ui/Reservoir.ui
new file mode 100644
index 0000000000000000000000000000000000000000..d2dc390256c8dc70b8df08f12c872223930c3fb3
--- /dev/null
+++ b/src/View/ui/Reservoir.ui
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QSplitter" name="splitter">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <widget class="QTableView" name="tableView">
+       <property name="minimumSize">
+        <size>
+         <width>300</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="baseSize">
+        <size>
+         <width>0</width>
+         <height>0</height>
+        </size>
+       </property>
+      </widget>
+      <widget class="QWidget" name="verticalLayoutWidget">
+       <layout class="QVBoxLayout" name="verticalLayout"/>
+      </widget>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>800</width>
+     <height>22</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <widget class="QToolBar" name="toolBar">
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
+   <addaction name="action_add"/>
+   <addaction name="action_delete"/>
+   <addaction name="action_sort"/>
+  </widget>
+  <action name="action_add">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
+   </property>
+   <property name="text">
+    <string>Add</string>
+   </property>
+   <property name="toolTip">
+    <string>Add a new point</string>
+   </property>
+  </action>
+  <action name="action_delete">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
+   </property>
+   <property name="text">
+    <string>Delete</string>
+   </property>
+   <property name="toolTip">
+    <string>Delete points</string>
+   </property>
+  </action>
+  <action name="action_edit">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
+   </property>
+   <property name="text">
+    <string>Edit</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit elevation/surface law</string>
+   </property>
+  </action>
+  <action name="action_sort">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-sort-ascending.png</normaloff>ressources/gtk-sort-ascending.png</iconset>
+   </property>
+   <property name="text">
+    <string>Sort</string>
+   </property>
+   <property name="toolTip">
+    <string>Sort points by elevation</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/View/ui/ReservoirList.ui b/src/View/ui/ReservoirList.ui
new file mode 100644
index 0000000000000000000000000000000000000000..db3b822d84f932c11e38bcc2b63864d58eeeaeb3
--- /dev/null
+++ b/src/View/ui/ReservoirList.ui
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>849</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>300</width>
+    <height>0</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QSplitter" name="splitter">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <widget class="QWidget" name="verticalLayoutWidget">
+       <layout class="QVBoxLayout" name="_1">
+        <item>
+         <widget class="QTableView" name="tableView">
+          <property name="minimumSize">
+           <size>
+            <width>300</width>
+            <height>0</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="verticalLayoutWidget_2">
+       <layout class="QVBoxLayout" name="verticalLayout"/>
+      </widget>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>849</width>
+     <height>22</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <widget class="QToolBar" name="toolBar">
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
+   <addaction name="action_add"/>
+   <addaction name="action_delete"/>
+   <addaction name="action_edit"/>
+  </widget>
+  <action name="action_add">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
+   </property>
+   <property name="text">
+    <string>Add</string>
+   </property>
+   <property name="toolTip">
+    <string>Add a new reservoir</string>
+   </property>
+  </action>
+  <action name="action_delete">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
+   </property>
+   <property name="text">
+    <string>Delete</string>
+   </property>
+   <property name="toolTip">
+    <string>Delete reservoirs</string>
+   </property>
+  </action>
+  <action name="action_edit">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
+   </property>
+   <property name="text">
+    <string>Edit</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit reservoir law</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>