diff --git a/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py
index b74eb9a5b3acf0534ffd7cefbed7c54fd2389066..4176350c1a56e3acfd5fdc8a08c48567f8799087 100644
--- a/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py
+++ b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py
@@ -36,7 +36,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
     _id_cnt = 0
 
     def __init__(self, id: int = -1,
-                 status=None):
+                 pollutant: int = -1, status=None):
         super(BoundaryConditionAdisTS, self).__init__()
 
         self._status = status
@@ -48,7 +48,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
 
         self._type = ""
         self._node = None
-        self._pollutant = None
+        self._pollutant = pollutant
         self._data = []
         self._header = []
         self._types = [self.time_convert, float]
@@ -87,37 +87,39 @@ class BoundaryConditionAdisTS(SQLSubModel):
     @classmethod
     def _db_load(cls, execute, data=None):
         new = []
-        pollutant_id = data["pollutant_id"]
 
         table = execute(
             "SELECT id, pollutant, type, node " +
-            "FROM boundary_condition_adists " +
-            f"WHERE tab = '{pollutant_id}'"
+            "FROM boundary_condition_adists"
         )
 
-        for row in table:
-            bc = cls(
-                id=row[0],
-                status=data['status']
-            )
+        if table is not None:
+            for row in table:
+                bc = cls(
+                    id=row[0],
+                    pollutant=row[1],
+                    status=data['status']
+                )
 
-            bc.node = None
-            if row[3] != -1:
-                bc.node = next(filter(lambda n: n.id == row[3], data["nodes"]))
+                bc.type = row[2]
 
-            values = execute(
-                "SELECT data0, data1 FROM boundary_condition_data_adists " +
-                f"WHERE bc = '{bc.id}'"
-            )
+                bc.node = None
+                if row[3] != -1:
+                    bc.node = next(filter(lambda n: n.id == row[3], data["nodes"]))
 
-            # Write data
-            for v in values:
-                data0 = bc._types[0](v[1])
-                data1 = bc._types[1](v[2])
-                # Replace data at pos ind
-                bc._data.append((data0, data1))
+                values = execute(
+                    "SELECT data0, data1 FROM boundary_condition_data_adists " +
+                    f"WHERE bc = '{bc.id}'"
+                )
 
-            new.append(bc)
+                # Write data
+                for v in values:
+                    data0 = bc._types[0](v[1])
+                    data1 = bc._types[1](v[2])
+                    # Replace data at pos ind
+                    bc._data.append((data0, data1))
+
+                new.append(bc)
 
         return new
 
@@ -194,6 +196,11 @@ class BoundaryConditionAdisTS(SQLSubModel):
     def type(self):
         return self._type
 
+    @type.setter
+    def type(self, type):
+        self._type = type
+        self._status.modified()
+
     @property
     def data(self):
         return self._data.copy()
diff --git a/src/Model/BoundaryConditionsAdisTS/BoundaryConditionsAdisTSList.py b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionsAdisTSList.py
new file mode 100644
index 0000000000000000000000000000000000000000..4246905fd2f6c164801c8efd0ccad0cc02572ac6
--- /dev/null
+++ b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionsAdisTSList.py
@@ -0,0 +1,63 @@
+# BoundaryConditionsAdisTSList.py -- Pamhyr
+# Copyright (C) 2023-2024  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.Except import NotImplementedMethodeError
+
+from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import BoundaryConditionAdisTS
+
+class BoundaryConditionsAdisTSList(PamhyrModelList):
+    _sub_classes = [
+        BoundaryConditionAdisTS,
+    ]
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = cls(status=data['status'])
+
+        if data is None:
+            data = {}
+
+        new._lst = BoundaryConditionAdisTS._db_load(
+            execute, data
+        )
+
+        return new
+
+    def _db_save(self, execute, data=None):
+        execute("DELETE FROM boundary_condition_adists")
+
+        if data is None:
+            data = {}
+
+        for bc in self._lst:
+            bc._db_save(execute, data=data)
+
+        return True
+
+    def new(self, index, pollutant):
+        n = BoundaryConditionAdisTS(pollutant=pollutant, status=self._status)
+        self._lst.insert(index, n)
+        self._status.modified()
+        return n
+
+
+
diff --git a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py
index 6430f738e6534cb73612e5559712f545e02c7c4e..e45aba048da24c1e082d114572a5a148532c68fc 100644
--- a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py
+++ b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py
@@ -41,7 +41,6 @@ class InitialConditionsAdisTSList(PamhyrModelList):
         return new
 
     def _db_save(self, execute, data=None):
-        print("db save ic adists")
         execute("DELETE FROM initial_conditions")
 
         if data is None:
diff --git a/src/Model/River.py b/src/Model/River.py
index 96dfb6b01260977e40c9749ebe53a3a8df051986..4b129344dacc56f79280213d2ccd608f9994a9fd 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -48,6 +48,7 @@ from Solver.Solvers import solver_type_list
 from Model.OutputKpAdists.OutputKpListAdists import OutputKpAdistsList
 from Model.Pollutants.PollutantsList import PollutantsList
 from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList import InitialConditionsAdisTSList
+from Model.BoundaryConditionsAdisTS.BoundaryConditionsAdisTSList import BoundaryConditionsAdisTSList
 
 
 class RiverNode(Node, SQLSubModel):
@@ -235,6 +236,7 @@ class River(Graph, SQLSubModel):
         OutputKpAdistsList,
         PollutantsList,
         InitialConditionsAdisTSList,
+        BoundaryConditionsAdisTSList,
     ]
 
     def __init__(self, status=None):
@@ -261,6 +263,7 @@ class River(Graph, SQLSubModel):
         self._Output_kp_adists = OutputKpAdistsList(status=self._status)
         self._Pollutants = PollutantsList(status=self._status)
         self._InitialConditionsAdisTS = InitialConditionsAdisTSList(status=self._status)
+        self._BoundaryConditionsAdisTS = BoundaryConditionsAdisTSList(status=self._status)
 
     @classmethod
     def _db_create(cls, execute):
@@ -343,6 +346,8 @@ class River(Graph, SQLSubModel):
 
         new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load(execute, data)
 
+        new._BoundaryConditionsAdisTS = BoundaryConditionsAdisTSList._db_load(execute, data)
+
         return new
 
     def _db_save(self, execute, data=None):
@@ -365,6 +370,7 @@ class River(Graph, SQLSubModel):
         objs.append(self._Output_kp_adists)
         objs.append(self._Pollutants)
         objs.append(self._InitialConditionsAdisTS)
+        objs.append(self._BoundaryConditionsAdisTS)
 
         self._save_submodel(execute, objs, data)
         return True
@@ -500,6 +506,10 @@ Last export at: @date."""
     def initial_conditions_adists(self):
         return self._InitialConditionsAdisTS
 
+    @property
+    def boundary_conditions_adists(self):
+        return self._BoundaryConditionsAdisTS
+
     def get_params(self, solver):
         if solver in self._parameters:
             return self._parameters[solver]
diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Plot.py b/src/View/BoundaryConditionsAdisTS/Edit/Plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..5405a501315a74c96839b2c6c27b8ce81355a792
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Edit/Plot.py
@@ -0,0 +1,104 @@
+# Plot.py -- Pamhyr
+# Copyright (C) 2023-2024  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
+)
+
+_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")
+
+        header = self.data.header
+        self.label_x = self._table_headers[header[0]]
+        self.label_y = self._table_headers[header[1]]
+
+        self._mode = mode
+        self._isometric_axis = False
+
+        self._auto_relim_update = True
+        self._autoscale_update = True
+
+    def custom_ticks(self):
+        if self.data.header[0] != "time":
+            return
+
+        self.set_ticks_time_formater()
+
+    @timer
+    def draw(self):
+        self.init_axes()
+
+        if len(self.data) == 0:
+            self._init = False
+            return
+
+        self.draw_data()
+        self.custom_ticks()
+
+        self.idle()
+        self._init = True
+
+    def draw_data(self):
+        # 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=self.color_plot,
+            **self.plot_default_kargs
+        )
+
+    @timer
+    def update(self, ind=None):
+        if not self._init:
+            self.draw()
+            return
+
+        self.update_data()
+
+        self.update_idle()
+
+    def update_data(self):
+        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)
diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Table.py b/src/View/BoundaryConditionsAdisTS/Edit/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..287a8fe42608c186849a2ab028dd139b56ce90ac
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Edit/Table.py
@@ -0,0 +1,193 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2024  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,
+    timestamp_to_old_pamhyr_date,
+    old_pamhyr_date_to_timestamp
+)
+
+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,
+    QTimeEdit, QDateTimeEdit, QItemDelegate,
+)
+
+from Model.BoundaryCondition.BoundaryConditionTypes import (
+    NotDefined, PonctualContribution,
+    TimeOverZ, TimeOverDischarge, ZOverDischarge
+)
+
+from View.BoundaryCondition.Edit.UndoCommand import (
+    SetDataCommand, AddCommand, DelCommand,
+    SortCommand, MoveCommand, 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]
+            if self._data.get_type_column(column) == float:
+                value = f"{v:.4f}"
+            elif self._data.header[column] == "time":
+                if self._opt_data == "time":
+                    value = timestamp_to_old_pamhyr_date(int(v))
+                else:
+                    value = str(datetime.fromtimestamp(v))
+            else:
+                value = f"{v}"
+
+        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, 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 move_up(self, row, parent=QModelIndex()):
+        if row <= 0:
+            return
+
+        target = row + 2
+
+        self.beginMoveRows(parent, row - 1, row - 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._data, "up", row
+            )
+        )
+
+        self.endMoveRows()
+        self.layoutChanged.emit()
+
+    def move_down(self, index, parent=QModelIndex()):
+        if row > len(self._data):
+            return
+
+        target = row
+
+        self.beginMoveRows(parent, row + 1, row + 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._data, "down", row
+            )
+        )
+
+        self.endMoveRows()
+        self.layoutChanged.emit()
+
+    def paste(self, row, header, 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(header, d),
+                        data
+                    )
+                )
+            )
+        )
+
+        self.layoutAboutToBeChanged.emit()
+        self.layoutChanged.emit()
diff --git a/src/View/BoundaryConditionsAdisTS/Edit/UndoCommand.py b/src/View/BoundaryConditionsAdisTS/Edit/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..02e0a48ed0ed63f6a2bf832317b9291cc49cee11
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Edit/UndoCommand.py
@@ -0,0 +1,183 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2024  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.BoundaryCondition.BoundaryCondition import BoundaryCondition
+
+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]
+        _type = self._data.get_type_column(self._column)
+        self._new = _type(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 SetMetaDataCommand(QUndoCommand):
+    def __init__(self, data, column, new_value):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._column = column
+        if self._column == "d50":
+            self._old = self._data.d50
+        elif self._column == "sigma":
+            self._old = self._data.sigma
+
+        self._new = float(new_value)
+
+    def undo(self):
+        if self._column == "d50":
+            self._data.d50 = self._old
+        elif self._column == "sigma":
+            self._data.sigma = self._old
+
+    def redo(self):
+        if self._column == "d50":
+            self._data.d50 = self._new
+        elif self._column == "sigma":
+            self._data.sigma = 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._bc = []
+        for row in rows:
+            self._bc.append((row, self._data.get_i(row)))
+        self._bc.sort()
+
+    def undo(self):
+        for row, el in self._bc:
+            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 MoveCommand(QUndoCommand):
+    def __init__(self, data, up, i):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._up = up == "up"
+        self._i = i
+
+    def undo(self):
+        if self._up:
+            self._data.move_up(self._i)
+        else:
+            self._data.move_down(self._i)
+
+    def redo(self):
+        if self._up:
+            self._data.move_up(self._i)
+        else:
+            self._data.move_down(self._i)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, data, row, bcs):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._row = row
+        self._bcs = bcs
+        self._bcs.reverse()
+
+    def undo(self):
+        self._data.delete_i(
+            range(self._row, self._row + len(self._bcs))
+        )
+
+    def redo(self):
+        for bc in self._bcs:
+            self._data.insert(self._row, bc)
diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Window.py b/src/View/BoundaryConditionsAdisTS/Edit/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..faf418964eb3dcca530f2c51eafbfd0a893359fd
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Edit/Window.py
@@ -0,0 +1,312 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2024  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 View.Tools.PamhyrDelegate import PamhyrExTimeDelegate
+
+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.BoundaryCondition.Edit.translate import BCETranslate
+from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand
+from View.BoundaryCondition.Edit.Table import TableModel
+from View.BoundaryCondition.Edit.Plot import Plot
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class WD50Sigma(PamhyrWidget):
+    _pamhyr_ui = "d50sigma"
+
+    d50Changed = pyqtSignal(float)
+    sigmaChanged = pyqtSignal(float)
+
+    def __init__(self, parent=None):
+        super(WD50Sigma, self).__init__(
+            parent=parent
+        )
+
+        self.spinBox_d50 = self.find(QDoubleSpinBox, "doubleSpinBox_d50")
+        self.spinBox_sigma = self.find(QDoubleSpinBox, "doubleSpinBox_sigma")
+
+        self.spinBox_d50.valueChanged.connect(self.valueChangedD50)
+        self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma)
+
+    def set_d50(self, d50):
+        self.spinBox_d50.valueChanged.disconnect(self.valueChangedD50)
+        self.spinBox_d50.setValue(float(d50))
+        self.spinBox_d50.valueChanged.connect(self.valueChangedD50)
+
+    def get_d50(self):
+        return float(self.spinBox_d50.value())
+
+    def set_sigma(self, sigma):
+        self.spinBox_sigma.valueChanged.disconnect(self.valueChangedSigma)
+        self.spinBox_sigma.setValue(float(sigma))
+        self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma)
+
+    def get_sigma(self):
+        return float(self.spinBox_sigma.value())
+
+    @QtCore.pyqtSlot(float)
+    def valueChangedD50(self, value):
+        self.d50Changed.emit(value)
+
+    @QtCore.pyqtSlot(float)
+    def valueChangedSigma(self, value):
+        self.sigmaChanged.emit(value)
+
+
+class EditBoundaryConditionWindow(PamhyrWindow):
+    _pamhyr_ui = "EditBoundaryConditions"
+    _pamhyr_name = "Edit Boundary Conditions"
+
+    def __init__(self, data=None, study=None, config=None, parent=None):
+        self._data = data
+        trad = BCETranslate()
+        self._long_types = trad.get_dict("long_types")
+
+        name = trad[self._pamhyr_name]
+        if self._data is not None:
+            node_name = (self._data.node.name if self._data.node is not None
+                         else trad['not_associated'])
+            name += (
+                f" - {study.name} " +
+                f" - {self._data.name} ({self._data.id}) " +
+                f"({self._long_types[self._data.bctype]} - {node_name})"
+            )
+
+        super(EditBoundaryConditionWindow, 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_data()
+        self.setup_connections()
+
+    def setup_data(self):
+        self._is_solid = self._data.bctype == "SL"
+
+        if self._is_solid:
+            layout = self.find(QVBoxLayout, "verticalLayout_table")
+            self._d50sigma = WD50Sigma(parent=self)
+            layout.addWidget(self._d50sigma)
+
+            self._d50sigma.set_d50(self._data.d50)
+            self._d50sigma.set_sigma(self._data.sigma)
+
+    def setup_table(self):
+        headers = {}
+        table_headers = self._trad.get_dict("table_headers")
+        for h in self._data.header:
+            headers[h] = table_headers[h]
+
+        self._delegate_time = PamhyrExTimeDelegate(
+            data=self._data,
+            mode=self._study.time_system,
+            parent=self
+        )
+
+        table = self.find(QTableView, "tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=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_del").triggered.connect(self.delete)
+        self.find(QAction, "action_sort").triggered.connect(self.sort)
+
+        self._table.dataChanged.connect(self.update)
+
+        if self._is_solid:
+            self._d50sigma.d50Changed.connect(self.d50_changed)
+            self._d50sigma.sigmaChanged.connect(self.sigma_changed)
+
+    def d50_changed(self, value):
+        self._undo_stack.push(
+            SetMetaDataCommand(
+                self._data,
+                "d50", value
+            )
+        )
+
+    def sigma_changed(self, value):
+        self._undo_stack.push(
+            SetMetaDataCommand(
+                self._data,
+                "sigma", value
+            )
+        )
+
+    def widget_update(self):
+        if self._is_solid:
+            self._d50sigma.set_d50(self._data.d50)
+            self._d50sigma.set_sigma(self._data.sigma)
+
+    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 move_up(self):
+        row = self.index_selected_row()
+        self._table.move_up(row)
+        self.plot.update()
+
+    def move_down(self):
+        row = self.index_selected_row()
+        self._table.move_down(row)
+        self.plot.update()
+
+    def _copy(self):
+        rows = self.index_selected_rows()
+
+        table = []
+        table.append(self._data.header)
+
+        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, header, 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/BoundaryConditionsAdisTS/Edit/translate.py b/src/View/BoundaryConditionsAdisTS/Edit/translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d8355505cf0477acafa811fe48d16f33b04f695
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Edit/translate.py
@@ -0,0 +1,44 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2024  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.Translate import MainTranslate
+
+from View.BoundaryCondition.translate import BCTranslate
+
+_translate = QCoreApplication.translate
+
+
+class BCETranslate(BCTranslate):
+    def __init__(self):
+        super(BCETranslate, self).__init__()
+
+        self._dict["Edit Boundary Conditions"] = _translate(
+            "BoundaryCondition", "Edit boundary conditions"
+        )
+
+        self._sub_dict["table_headers"] = {
+            "x": _translate("BoundaryCondition", "X"),
+            "y": _translate("BoundaryCondition", "Y"),
+            "time": self._dict["time"],
+            "date": self._dict["date"],
+            "discharge": self._dict["unit_discharge"],
+            "z": self._dict["unit_elevation"],
+            "solid": _translate("BoundaryCondition", "Solid (kg/s)"),
+        }
diff --git a/src/View/BoundaryConditionsAdisTS/Table.py b/src/View/BoundaryConditionsAdisTS/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..eec695cbadc8d81a134d01b4052a7b671b055b05
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Table.py
@@ -0,0 +1,264 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2024  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 Model.BoundaryCondition.BoundaryConditionTypes import (
+    NotDefined, PonctualContribution,
+    TimeOverZ, TimeOverDischarge, ZOverDischarge
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.BoundaryCondition.UndoCommand import (
+    SetNameCommand, SetNodeCommand, SetTypeCommand,
+    AddCommand, DelCommand, SortCommand,
+    MoveCommand, PasteCommand,
+)
+from View.BoundaryCondition.translate import BC_types
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+    def __init__(self, data=None, mode="type", tab="",
+                 trad=None, parent=None):
+        super(ComboBoxDelegate, self).__init__(parent)
+
+        self._data = data
+        self._mode = mode
+        self._tab = tab
+        self._trad = trad
+
+        self._long_types = {}
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+    def createEditor(self, parent, option, index):
+        self.editor = QComboBox(parent)
+
+        if self._mode == "type":
+            lst = list(
+                map(
+                    lambda k: self._long_types[k],
+                    filter(
+                        lambda k: self._tab in BC_types[k].compatibility(),
+                        BC_types.keys()
+                    )
+                )
+            )
+            self.editor.addItems(
+                lst
+            )
+        else:
+            self.editor.addItems(
+                [self._trad["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 __init__(self, trad=None, **kwargs):
+        self._trad = trad
+        self._long_types = {}
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+        super(TableModel, self).__init__(trad=trad, **kwargs)
+
+    def _setup_lst(self):
+        self._lst = self._data.boundary_condition
+        self._tab = self._opt_data
+
+    def rowCount(self, parent):
+        return self._lst.len(self._tab)
+
+    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(self._tab, row).name
+        elif self._headers[column] == "type":
+            t = self._lst.get(self._tab, row).bctype
+            return self._long_types[t]
+        elif self._headers[column] == "node":
+            n = self._lst.get(self._tab, row).node
+            if n is None:
+                return self._trad["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, self._tab, row, value
+                    )
+                )
+            elif self._headers[column] == "type":
+                key = next(k for k, v in self._long_types.items()
+                           if v == value)
+                self._undo.push(
+                    SetTypeCommand(
+                        self._lst, self._tab, row, BC_types[key]
+                    )
+                )
+            elif self._headers[column] == "node":
+                self._undo.push(
+                    SetNodeCommand(
+                        self._lst, self._tab, 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, self._tab, 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, self._tab, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def sort(self, _reverse, parent=QModelIndex()):
+        self.layoutAboutToBeChanged.emit()
+
+        self._undo.push(
+            SortCommand(
+                self._lst, self._tab, False
+            )
+        )
+
+        self.layoutAboutToBeChanged.emit()
+        self.layoutChanged.emit()
+
+    def move_up(self, row, parent=QModelIndex()):
+        if row <= 0:
+            return
+
+        target = row + 2
+
+        self.beginMoveRows(parent, row - 1, row - 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._lst, self._tab, "up", row
+            )
+        )
+
+        self.endMoveRows()
+        self.layoutChanged.emit()
+
+    def move_down(self, index, parent=QModelIndex()):
+        if row > len(self._lst):
+            return
+
+        target = row
+
+        self.beginMoveRows(parent, row + 1, row + 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._lst, self._tab, "down", row
+            )
+        )
+
+        self.endMoveRows()
+        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/BoundaryConditionsAdisTS/UndoCommand.py b/src/View/BoundaryConditionsAdisTS/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee3b45df49fd99a6f7e947ade7a7f5fabae8c326
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/UndoCommand.py
@@ -0,0 +1,204 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2024  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.BoundaryCondition.BoundaryCondition import BoundaryCondition
+from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList
+
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, bcs, tab, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._index = index
+        self._old = self._bcs.get(self._tab, self._index).name
+        self._new = str(new_value)
+
+    def undo(self):
+        self._bcs.get(self._tab, self._index).name = self._old
+
+    def redo(self):
+        self._bcs.get(self._tab, self._index).name = self._new
+
+
+class SetNodeCommand(QUndoCommand):
+    def __init__(self, bcs, tab, index, node):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._index = index
+        self._old = self._bcs.get(self._tab, self._index).node
+        self._new = node
+        self._prev_assoc_to_node = self._bcs.get_assoc_to_node(tab, 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._bcs.get(self._tab, self._index).node = self._old
+        self._previous_assoc_node(self._new)
+
+    def redo(self):
+        self._bcs.get(self._tab, self._index).node = self._new
+        self._previous_assoc_node(None)
+
+
+class SetTypeCommand(QUndoCommand):
+    def __init__(self, bcs, tab, index, _type):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._index = index
+        self._type = _type
+        self._old = self._bcs.get(self._tab, self._index)
+        self._new = self._bcs.get(self._tab, self._index)\
+                             .convert(self._type)
+
+    def undo(self):
+        self._bcs.set(self._tab, self._index, self._old)
+
+    def redo(self):
+        self._bcs.set(self._tab, self._index, self._new)
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, bcs, tab, index):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._bcs.delete_i(self._tab, [self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._bcs.new(self._tab, self._index)
+        else:
+            self._bcs.insert(self._tab, self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, bcs, tab, rows):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._rows = rows
+
+        self._bc = []
+        for row in rows:
+            self._bc.append((row, self._bcs.get(self._tab, row)))
+        self._bc.sort()
+
+    def undo(self):
+        for row, el in self._bc:
+            self._bcs.insert(self._tab, row, el)
+
+    def redo(self):
+        self._bcs.delete_i(self._tab, self._rows)
+
+
+class SortCommand(QUndoCommand):
+    def __init__(self, bcs, tab, _reverse):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._reverse = _reverse
+
+        self._old = self._bcs.get_tab(self._tab)
+        self._indexes = None
+
+    def undo(self):
+        ll = self._bcs.get_tab(self._tab)
+        self._bcs.sort(
+            self._tab,
+            key=lambda x: self._indexes[ll.index(x)]
+        )
+
+    def redo(self):
+        self._bcs.sort(
+            self._tab,
+            reverse=self._reverse,
+            key=lambda x: x.name
+        )
+        if self._indexes is None:
+            self._indexes = list(
+                map(
+                    lambda p: self._old.index(p),
+                    self._bcs.get_tab(self._tab)
+                )
+            )
+            self._old = None
+
+
+class MoveCommand(QUndoCommand):
+    def __init__(self, bcs, tab, up, i):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._up = up == "up"
+        self._i = i
+
+    def undo(self):
+        if self._up:
+            self._bcs.move_up(self._tab, self._i)
+        else:
+            self._bcs.move_down(self._tab, self._i)
+
+    def redo(self):
+        if self._up:
+            self._bcs.move_up(self._tab, self._i)
+        else:
+            self._bcs.move_down(self._tab, self._i)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, bcs, tab, row, bc):
+        QUndoCommand.__init__(self)
+
+        self._bcs = bcs
+        self._tab = tab
+        self._row = row
+        self._bc = deepcopy(bc)
+        self._bc.reverse()
+
+    def undo(self):
+        self._bcs.delete_i(
+            self._tab,
+            range(self._row, self._row + len(self._bc))
+        )
+
+    def redo(self):
+        for bc in self._bc:
+            self._bcs.insert(self._tab, self._row, bc)
diff --git a/src/View/BoundaryConditionsAdisTS/Window.py b/src/View/BoundaryConditionsAdisTS/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..5aae8ffb4824b157e9626306bd5f815bb049126c
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/Window.py
@@ -0,0 +1,230 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2024  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, logger_exception
+
+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,
+    QWidget,
+)
+
+from Model.BoundaryCondition.BoundaryConditionTypes import (
+    NotDefined, PonctualContribution,
+    TimeOverZ, TimeOverDischarge, ZOverDischarge
+)
+
+from View.BoundaryCondition.UndoCommand import (
+    SetNameCommand, SetNodeCommand, SetTypeCommand,
+    AddCommand, DelCommand, SortCommand,
+    MoveCommand, PasteCommand,
+)
+
+from View.BoundaryConditionsAdisTS.Table import (
+    TableModel, ComboBoxDelegate
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.BoundaryConditionsAdisTS.translate import BCAdisTSTranslate
+from View.BoundaryConditionsAdisTS.Edit.Window import EditBoundaryConditionWindow
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class BoundaryConditionWindow(PamhyrWindow):
+    _pamhyr_ui = "BoundaryConditionsAdisTS"
+    _pamhyr_name = "Boundary conditions AdisTS"
+
+    def __init__(self, study=None, config=None, parent=None):
+        trad = BCAdisTSTranslate()
+        name = (
+            trad[self._pamhyr_name] +
+            " - " + study.name
+        )
+
+        super(BoundaryConditionWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=trad,
+            parent=parent
+        )
+
+        self._bcs = self._study.river.boundary_condition
+
+        self.setup_table()
+        self.setup_graph()
+        self.setup_connections()
+
+        self.ui.setWindowTitle(self._title)
+
+    def setup_table(self):
+        self._table = {}
+
+        t = "liquid"
+
+        self._delegate_type = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            mode="type",
+            tab=t,
+            parent=self
+        )
+        self._delegate_node = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            mode="node",
+            tab=t,
+            parent=self
+        )
+
+        table = self.find(QTableView, f"tableView_{t}")
+        self._table[t] = TableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers"),
+            editable_headers=["name", "type", "node"],
+            delegates={
+                "type": self._delegate_type,
+                "node": self._delegate_node,
+            },
+            trad=self._trad,
+            data=self._study.river,
+            undo=self._undo_stack,
+            opt_data=t,
+        )
+        table.setModel(self._table[t])
+        table.setSelectionBehavior(QAbstractItemView.SelectRows)
+        table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+        table.setAlternatingRowColors(True)
+
+    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_del").triggered.connect(self.delete)
+        self.find(QAction, "action_edit").triggered.connect(self.edit)
+        self.find(QAction, "action_sort").triggered.connect(self.sort)
+
+    def index_selected_row(self):
+        tab = "liquid"
+        table = self.find(QTableView, f"tableView_{tab}")
+        return table.selectionModel()\
+                    .selectedRows()[0]\
+                    .row()
+
+    def index_selected_rows(self):
+        tab = "liquid"
+        table = self.find(QTableView, f"tableView_{tab}")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        tab = "liquid"
+        rows = self.index_selected_rows()
+        if self._bcs.len(tab) == 0 or len(rows) == 0:
+            self._table[tab].add(0)
+        else:
+            self._table[tab].add(rows[0])
+
+    def delete(self):
+        tab = "liquid"
+        rows = self.index_selected_rows()
+        if len(rows) == 0:
+            return
+
+        self._table[tab].delete(rows)
+
+    def sort(self):
+        tab = "liquid"
+        self._table[tab].sort(False)
+
+    def move_up(self):
+        tab = "liquid"
+        row = self.index_selected_row()
+        self._table[tab].move_up(row)
+
+    def move_down(self):
+        tab = "liquid"
+        row = self.index_selected_row()
+        self._table[tab].move_down(row)
+
+    def _copy(self):
+        logger.info("TODO: copy")
+
+    def _paste(self):
+        logger.info("TODO: paste")
+
+    def _undo(self):
+        tab = "liquid"
+        self._table[tab].undo()
+
+    def _redo(self):
+        tab = "liquid"
+        self._table[tab].redo()
+
+    def edit(self):
+        tab = "liquid"
+        rows = self.index_selected_rows()
+        for row in rows:
+            data = self._bcs.get(tab, row)
+
+            if self.sub_window_exists(
+                EditBoundaryConditionWindow,
+                data=[self._study, None, data]
+            ):
+                continue
+
+            win = EditBoundaryConditionWindow(
+                data=data,
+                study=self._study,
+                parent=self
+            )
+            win.show()
diff --git a/src/View/BoundaryConditionsAdisTS/translate.py b/src/View/BoundaryConditionsAdisTS/translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..967bea50728056b1f0103e1edc65bbc0b4eb7c26
--- /dev/null
+++ b/src/View/BoundaryConditionsAdisTS/translate.py
@@ -0,0 +1,46 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2024  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.Translate import MainTranslate
+
+_translate = QCoreApplication.translate
+
+class BCAdisTSTranslate(MainTranslate):
+    def __init__(self):
+        super(BCAdisTSTranslate, self).__init__()
+
+        self._dict["Boundary conditions AdisTS"] = _translate(
+            "BoundaryConditionsAdisTS", "Boundary conditions AdisTS"
+        )
+
+        self._sub_dict["long_types"] = {
+            "ND": self._dict["not_defined"],
+            "PC": _translate("BoundaryCondition", "Ponctual contribution"),
+            "TZ": _translate("BoundaryCondition", "Z(t)"),
+            "TD": _translate("BoundaryCondition", "Q(t)"),
+            "ZD": _translate("BoundaryCondition", "Q(Z)"),
+            "SL": _translate("BoundaryCondition", "Solid"),
+        }
+
+        self._sub_dict["table_headers"] = {
+            "name": self._dict["name"],
+            "type": self._dict["type"],
+            "node": _translate("BoundaryCondition", "Node")
+        }
diff --git a/src/View/Pollutants/Window.py b/src/View/Pollutants/Window.py
index 93b091474dfcb411655d67b22d713af58599f473..0914d045df969739de39d9c51c0673a9487853e5 100644
--- a/src/View/Pollutants/Window.py
+++ b/src/View/Pollutants/Window.py
@@ -44,6 +44,7 @@ from View.Pollutants.Translate import PollutantsTranslate
 from View.Pollutants.Edit.Window import EditPolluantWindow
 
 from View.InitialConditionsAdisTS.Window import InitialConditionsAdisTSWindow
+from View.BoundaryConditionsAdisTS.Window import BoundaryConditionWindow
 
 logger = logging.getLogger()
 
@@ -105,6 +106,7 @@ class PollutantsWindow(PamhyrWindow):
         self.find(QAction, "action_delete").triggered.connect(self.delete)
         self.find(QAction, "action_edit").triggered.connect(self.edit)
         self.find(QAction, "action_initial_conditions").triggered.connect(self.initial_conditions)
+        self.find(QAction, "action_boundary_conditions").triggered.connect(self.boundary_conditions)
         self._checkbox.clicked.connect(self._set_structure_state)
 
         table = self.find(QTableView, "tableView")
@@ -211,6 +213,20 @@ class PollutantsWindow(PamhyrWindow):
             )
             initial.show()
 
+    def boundary_conditions(self, tab=0):
+        if self.sub_window_exists(
+            BoundaryConditionWindow,
+            data=[self._study, None]
+        ):
+            bound = self.get_sub_window(
+                BoundaryConditionWindow,
+                data=[self._study, None]
+            )
+            return
+
+        bound = BoundaryConditionWindow(study=self._study, parent=self)
+        bound.show()
+
     def _set_checkbox_state(self):
         row = self.index_selected_row()
         if row is None:
diff --git a/src/View/ui/BoundaryConditionsAdisTS.ui b/src/View/ui/BoundaryConditionsAdisTS.ui
new file mode 100644
index 0000000000000000000000000000000000000000..b54d77f62fa91ada02de922c684836a714692298
--- /dev/null
+++ b/src/View/ui/BoundaryConditionsAdisTS.ui
@@ -0,0 +1,148 @@
+<?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>450</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <property name="locale">
+   <locale language="English" country="Europe"/>
+  </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="QTabWidget" name="tabWidget">
+       <property name="minimumSize">
+        <size>
+         <width>300</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="currentIndex">
+        <number>0</number>
+       </property>
+       <widget class="QWidget" name="tab_liquid">
+        <attribute name="title">
+         <string>Liquid</string>
+        </attribute>
+        <layout class="QGridLayout" name="gridLayout_2">
+         <item row="0" column="0">
+          <widget class="QTableView" name="tableView_liquid"/>
+         </item>
+        </layout>
+       </widget>
+      </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="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_del"/>
+   <addaction name="action_edit"/>
+   <addaction name="action_sort"/>
+  </widget>
+  <action name="action_add">
+   <property name="checkable">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
+   </property>
+   <property name="text">
+    <string>Add</string>
+   </property>
+   <property name="toolTip">
+    <string>Add a new boundary condition or punctual contribution</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+N</string>
+   </property>
+  </action>
+  <action name="action_del">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
+   </property>
+   <property name="text">
+    <string>Delete</string>
+   </property>
+   <property name="toolTip">
+    <string>Delete current selected rows</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+D</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 boundary condition or punctual contribution</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+E</string>
+   </property>
+  </action>
+  <action name="action_sort">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/sort_A-Z.png</normaloff>ressources/sort_A-Z.png</iconset>
+   </property>
+   <property name="text">
+    <string>Sort</string>
+   </property>
+   <property name="toolTip">
+    <string>Sort boundary condition by name</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/View/ui/Pollutants.ui b/src/View/ui/Pollutants.ui
index 9ed80616a81d154a69e8e93c6b283bf719f8c505..3f7a288f9839b0fd24a9d661507f1ce1b8372d0a 100644
--- a/src/View/ui/Pollutants.ui
+++ b/src/View/ui/Pollutants.ui
@@ -97,6 +97,7 @@
    <addaction name="action_delete"/>
    <addaction name="action_edit"/>
    <addaction name="action_initial_conditions"/>
+   <addaction name="action_boundary_conditions"/>
   </widget>
   <action name="action_add">
    <property name="icon">
@@ -135,6 +136,11 @@
     <string>InitialConditions</string>
    </property>
   </action>
+  <action name="action_boundary_conditions">
+   <property name="text">
+    <string>BoundaryConditions</string>
+   </property>
+  </action>
  </widget>
  <resources/>
  <connections/>