diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py
index b24865e8b15a6daf9671e4b05d9e017f19b1cb7d..733a910d5e8194c62cf0928aa7a5fdc4b6fc363f 100644
--- a/src/View/Network/Table.py
+++ b/src/View/Network/Table.py
@@ -25,6 +25,7 @@ from Model.Network.Graph import Graph
 from View.ASubWindow import ASubWindow
 from View.Network.GraphWidget import GraphWidget
 from View.Network.UndoCommand import *
+from View.Tools.PamhyrTable import PamhyrTableModel
 
 from PyQt5.QtCore import (
     Qt, QRect, QVariant, QAbstractTableModel, pyqtSlot, pyqtSignal,
@@ -69,151 +70,105 @@ class ComboBoxDelegate(QItemDelegate):
     def currentItemChanged(self):
         self.commitData.emit(self.sender())
 
-class TrueFalseComboBoxDelegate(QItemDelegate):
-    def __init__(self, parent=None):
-        super(TrueFalseComboBoxDelegate, self).__init__(parent)
-
-    def createEditor(self, parent, option, index):
-        self.editor = QComboBox(parent)
-        self.editor.addItems(["true", "false"])
-        self.editor.setCurrentText("true" if index.data(Qt.DisplayRole) else "false")
-        return self.editor
-
-    def setEditorData(self, editor, index):
-        value = str(index.data(Qt.DisplayRole))
-        self.editor.currentTextChanged.connect(self.currentItemChanged)
-
-    def setModelData(self, editor, model, index):
-        value = str(editor.currentText()) == "true"
-        model.setData(index, value)
-        editor.close()
-        editor.deleteLater()
-
-    def updateEditorGeometry(self, editor, option, index):
-        r = QRect(option.rect)
-        if self.editor.windowFlags() & Qt.Popup and 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 GraphTableModel(QAbstractTableModel):
-    def __init__(self, headers=[], graph=None, undo = None, rows_type="nodes"):
-        super(QAbstractTableModel, self).__init__()
-        self.headers = headers
-        self.graph = graph
-        self._type = rows_type
-        self._undo = undo
-
-        if self._type == "nodes":
-            self.rows = graph.nodes()
-        elif self._type == "edges":
-            self.rows = graph.edges()
-
-    def flags(self, index):
-        options = Qt.ItemIsEnabled | Qt.ItemIsSelectable
-
-        if self.headers[index.column()] != "type":
-            options |= Qt.ItemIsEditable
-
-        return options
-
-    def rowCount(self, parent):
-        return len(self.rows)
-
-    def columnCount(self, parent):
-        return len(self.headers)
+class NodeTableModel(PamhyrTableModel):
+    def _setup_lst(self):
+        self._lst = self._data.nodes()
 
     def data(self, index, role):
         if role != Qt.ItemDataRole.DisplayRole:
             return QVariant()
 
-        if self.headers[index.column()] == "type":
-            node = self.rows[index.row()]
+        if self._headers[index.column()] == "type":
+            node = self._lst[index.row()]
             ret = "internal"
 
-            if not self.graph.is_enable_node(node):
+            if not self._data.is_enable_node(node):
                 ret = "disable"
-            elif self.graph.is_upstream_node(node):
+            elif self._data.is_upstream_node(node):
                 ret = "upstream"
-            elif self.graph.is_downstream_node(node):
+            elif self._data.is_downstream_node(node):
                 ret = "downstream"
 
             return ret
 
-        return self.rows[index.row()][self.headers[index.column()]]
-
-    def headerData(self, section, orientation, role):
-        if (role == Qt.ItemDataRole.DisplayRole and
-            orientation == Qt.Orientation.Horizontal):
-            return self.headers[section].capitalize()
-
-        return QVariant()
+        return self._lst[index.row()][self._headers[index.column()]]
 
     @pyqtSlot()
     def setData(self, index, value, role=Qt.EditRole):
-        if index.isValid():
-            if role == Qt.EditRole:
-                try:
-                    if (self.headers[index.column()] == "node1" or
-                        self.headers[index.column()] == "node2"):
-                        node = self.graph.node(value)
-                        self._undo.push(
-                            SetNodeCommand(
-                                self.graph,
-                                self.rows[index.row()],
-                                self.headers[index.column()],
-                                node
-                            )
-                        )
-                        # elif self.headers[index.column()] == "enable":
-                        #     self._undo.push(
-                        #         EnableEdgeCommand(
-                        #             self.rows[index.row()], value
-                        #         )
-                        #     )
-                    else:
-                        self._undo.push(
-                            SetCommand(
-                                self.rows[index.row()],
-                                self.headers[index.column()],
-                                value
-                            )
-                        )
-                except Exception as e:
-                    logger.info(e)
-                    logger.debug(traceback.format_exc())
+        if not index.isValid():
+            return False
+
+        if role == Qt.EditRole:
+            try:
+                self._undo.push(
+                    SetCommand(
+                        self._lst[index.row()],
+                        self._headers[index.column()],
+                        value
+                    )
+                )
+            except Exception as e:
+                logger.info(e)
+                logger.debug(traceback.format_exc())
 
                 self.dataChanged.emit(index, index, [Qt.DisplayRole])
                 self.layoutChanged.emit()
                 return True
 
             self.dataChanged.emit(index, index)
-        else:
-            return False
 
     def update(self):
-        if self._type == "nodes":
-            self.rows = self.graph.nodes()
-        elif self._type == "edges":
-            self.rows = self.graph.edges()
-
+        self._lst = self._data.nodes()
         self.layoutChanged.emit()
 
-    def reverse_edge(self, index):
-        if self._type == "edges":
-            tmp = self.rows[index.row()].node1
-            self.rows[index.row()].node1 = self.rows[index.row()].node2
-            self.rows[index.row()].node2 = tmp
+class EdgeTableModel(PamhyrTableModel):
+    def _setup_lst(self):
+        self._lst = self._data.edges()
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        return self._lst[index.row()][self._headers[index.column()]]
+
+    @pyqtSlot()
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid():
+            return False
+
+        if role != Qt.EditRole:
+            return QVariant()
+
+        try:
+            if (self._headers[index.column()] == "node1" or
+                self._headers[index.column()] == "node2"):
+                node = self.graph.node(value)
+                self._undo.push(
+                    SetNodeCommand(
+                        self._data,
+                        self._lst[index.row()],
+                        self._headers[index.column()],
+                        node
+                    )
+                )
+            else:
+                self._undo.push(
+                    SetCommand(
+                        self._lst[index.row()],
+                        self._headers[index.column()],
+                        value
+                    )
+                )
+        except Exception as e:
+            logger.info(e)
+            logger.debug(traceback.format_exc())
+
             self.dataChanged.emit(index, index, [Qt.DisplayRole])
             self.layoutChanged.emit()
+            return True
 
-    def undo(self):
-        self._undo.undo()
-        self.layoutChanged.emit()
+        self.dataChanged.emit(index, index)
 
-    def redo(self):
-        self._undo.redo()
+    def update(self):
+        self._lst = self._data.edges()
         self.layoutChanged.emit()
diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py
index 0505186287d9f67354e5536e022923d52c9f9e5d..3fc91d797b8f3f72f3f0350901367f043e161e9a 100644
--- a/src/View/Network/Window.py
+++ b/src/View/Network/Window.py
@@ -37,8 +37,9 @@ from Model.River import RiverNode, RiverReach, River
 from View.ASubWindow import ASubMainWindow
 from View.Network.GraphWidget import GraphWidget
 from View.Network.UndoCommand import *
+from View.Network.translate import *
 from View.Network.Table import (
-    GraphTableModel, ComboBoxDelegate, TrueFalseComboBoxDelegate,
+    ComboBoxDelegate, NodeTableModel, EdgeTableModel,
 )
 
 class NetworkWindow(ASubMainWindow):
@@ -70,42 +71,38 @@ class NetworkWindow(ASubMainWindow):
 
     def setup_table(self):
         # Nodes table
-
-        self._nodes_model = GraphTableModel(
-            headers = ["name", "type"],
-            graph = self._graph,
-            rows_type = "nodes",
+        table = self.find(QTableView, "tableView_nodes")
+        self._nodes_model = NodeTableModel(
+            table_view = table,
+            table_headers = table_headers_node,
+            editable_headers = ["name"],
+            data = self._graph,
             undo = self._undo_stack,
         )
-        table = self.find(QTableView, "tableView_nodes")
         table.setModel(self._nodes_model)
-        #table.resizeColumnsToContents()
 
         table.setSelectionBehavior(QAbstractItemView.SelectRows)
         table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
 
         # Edges table
-
-        self._reachs_model = GraphTableModel(
-            headers = ["name", # "enable",
-                       "node1", "node2"],
-            graph = self._graph,
-            rows_type = "edges",
-            undo = self._undo_stack,
-        )
+        table = self.find(QTableView, "tableView_reachs")
         self.delegate_combobox = ComboBoxDelegate(
             graph = self._graph,
             parent = self,
         )
-        self.delegate_true_false_combobox = TrueFalseComboBoxDelegate(
-            parent = self,
-        )
 
-        table = self.find(QTableView, "tableView_reachs")
+        self._reachs_model = EdgeTableModel(
+            table_view = table,
+            table_headers = table_headers_edge,
+            editable_headers = ["name", "node1", "node2"],
+            delegates = {
+                "node1": self.delegate_combobox,
+                "node2": self.delegate_combobox,
+            },
+            data = self._graph,
+            undo = self._undo_stack,
+        )
         table.setModel(self._reachs_model)
-        # table.setItemDelegateForColumn(1, self.delegate_true_false_combobox)
-        table.setItemDelegateForColumn(1, self.delegate_combobox)
-        table.setItemDelegateForColumn(2, self.delegate_combobox)
         table.setSelectionBehavior(QAbstractItemView.SelectRows)
         table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
         #table.resizeColumnsToContents()
diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d266ca2eb3dc5243fee205d4918ea192b761fc1
--- /dev/null
+++ b/src/View/Tools/PamhyrTable.py
@@ -0,0 +1,152 @@
+# PamhyrTable.py -- Pamhyr abstract table model
+# 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 Model.Except import NotImplementedMethodeError
+
+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, QStyledItemDelegate,
+)
+
+logger = logging.getLogger()
+
+
+class PamhyrTextDelegate(QStyledItemDelegate):
+    def __init__(self, parent=None):
+        super(PamhyrTextDelegate, self).__init__(parent)
+
+    def createEditor(self, parent, option, index):
+        index.model().data(index, Qt.DisplayRole)
+        return QLineEdit(parent)
+
+    def setEditorData(self, editor, index):
+        value = index.model().data(index, Qt.DisplayRole)
+        editor.setText(str(value))
+
+    def setModelData(self, editor, model, index):
+        model.setData(index, editor.text())
+
+    def updateEditorGeometry(self, editor, option, index):
+        editor.setGeometry(option.rect)
+
+class PamhyrTableModel(QAbstractTableModel):
+    def _setup_delegates(self):
+        if self._table_view is None:
+            return
+
+        for h in self._headers:
+            if h in self._delegates:
+                self._table_view.setItemDelegateForColumn(
+                    self._headers.index(h), self._delegates[h]
+                )
+            else:
+                self._table_view.setItemDelegateForColumn(
+                    self._headers.index(h), PamhyrTextDelegate(
+                        parent=self
+                    )
+                )
+
+    def __init__(self,
+                 table_view=None,
+                 table_headers={},
+                 editable_headers=[],
+                 delegates = {},
+                 data=None,
+                 undo=None):
+        super(PamhyrTableModel, self).__init__()
+
+        self._table_view = table_view
+
+        self._table_headers = table_headers
+        self._headers = list(table_headers.keys())
+        self._editable_headers = editable_headers
+        self._delegates = delegates
+
+        self._data = data
+        self._undo = undo
+        self._lst = []
+
+        self._setup_delegates()
+        self._setup_lst()
+
+    def _setup_lst(self):
+        self._lst = self.data
+
+    def flags(self, index):
+        column = index.column()
+
+        options = Qt.ItemIsEnabled | Qt.ItemIsSelectable
+
+        if self._headers[column] in self._editable_headers:
+            options |= Qt.ItemIsEditable
+
+        return options
+
+    def rowCount(self, parent):
+        return len(self._lst)
+
+    def columnCount(self, parent):
+        return len(self._headers)
+
+    def headerData(self, section, orientation, role):
+        if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
+            return self._table_headers[self._headers[section]]
+
+        return QVariant()
+
+    def data(self, index, role):
+        raise NotImplementedMethodeError(self, self.data)
+
+    def setData(self, index, value, role=Qt.EditRole):
+        raise NotImplementedMethodeError(self, self.setData)
+
+    def undo(self):
+        self._undo.undo()
+        self.layoutChanged.emit()
+
+    def redo(self):
+        self._undo.redo()
+        self.layoutChanged.emit()
+
+    def add(self, row, parent=QModelIndex()):
+        raise NotImplementedMethodeError(self, self.add)
+
+    def delete(self, rows, parent=QModelIndex()):
+        raise NotImplementedMethodeError(self, self.delete)
+
+    def sort(self, _reverse, parent=QModelIndex()):
+        raise NotImplementedMethodeError(self, self.sort)
+
+    def move_up(self, row, parent=QModelIndex()):
+        raise NotImplementedMethodeError(self, self.move_up)
+
+    def move_down(self, index, parent=QModelIndex()):
+        raise NotImplementedMethodeError(self, self.move_down)