diff --git a/soitool/coder.py b/soitool/coder.py
index c0423364f2a1454eda1c1d35340449506ffe6b89..5e36925ac509e70ac493e4910ed1cac8c6509ed4 100644
--- a/soitool/coder.py
+++ b/soitool/coder.py
@@ -6,20 +6,22 @@ import string
 import secrets
 
 
-def get_code(code_length, mode="ascii", space_interval=0):
+def get_code(code_length, mode="ascii", space_interval=0, space_amount=1):
     """
     Generate a single random code.
 
     Parameters
     ----------
     code_length : int
-        The length of the code
+        The length of the code.
     mode : string
         'ascii' for letters, 'digits' for digits and 'combo' for
         combination of letters and digits, by default 'ascii'.
     space_interval : int
         Spaces will be inserted into code each interval for readability if not
         0, by default 0.
+    space_amount : int
+        Amount of spaces per interval, by default 1.
 
     Return
     ------
@@ -47,12 +49,14 @@ def get_code(code_length, mode="ascii", space_interval=0):
 
     # Add spaces to code if interval is given
     if space_interval > 0:
-        code = insert_spaces(code, space_interval)
+        code = insert_spaces(code, space_interval, space_amount)
 
     return code
 
 
-def get_code_set(count, code_length, mode="ascii", space_interval=0):
+def get_code_set(
+    count, code_length, mode="ascii", space_interval=0, space_amount=1
+):
     """
     Generate a set of unique, random codes.
 
@@ -68,6 +72,8 @@ def get_code_set(count, code_length, mode="ascii", space_interval=0):
     space_interval : int
         Spaces will be inserted into code each interval for readability if not
         0, by default 0.
+    space_amount : int
+        Amount of spaces per interval, by default 1.
 
     Return
     ------
@@ -77,7 +83,7 @@ def get_code_set(count, code_length, mode="ascii", space_interval=0):
     codes = set()
 
     while len(codes) < count:
-        code = get_code(code_length, mode, space_interval)
+        code = get_code(code_length, mode, space_interval, space_amount)
         codes.add(code)
 
     return codes
@@ -105,7 +111,7 @@ def get_code_length_needed(number_of_entries):
     return code_length
 
 
-def insert_spaces(code, interval):
+def insert_spaces(code, interval, space_amount=1):
     """Insert space after every x'th character, x = interval.
 
     Parameters
@@ -114,6 +120,8 @@ def insert_spaces(code, interval):
         String to add spaces to.
     interval : int
         Interval for inserting spaces.
+    space_amount : int
+        Amount of spaces per interval, by default 1.
 
     Returns
     -------
@@ -122,7 +130,7 @@ def insert_spaces(code, interval):
     """
     # Convert to list to insert spaces between characters
     code = list(code)
-    for i in range(interval - 1, len(code), interval):
-        code[i] += " "
+    for i in range(interval - 1, len(code) - 1, interval):
+        code[i] += " " * space_amount
 
     return "".join(code)
diff --git a/soitool/main_window.py b/soitool/main_window.py
index e322fda586b0e8860fa8ba7e0eaf688363337e2f..01567b6739600253ed433d1544207ced22c23319 100644
--- a/soitool/main_window.py
+++ b/soitool/main_window.py
@@ -20,6 +20,8 @@ from soitool.codebook_to_pdf import generate_codebook_pdf
 from soitool.dialog_wrappers import exec_info_dialog
 from soitool.codebook_widget import CodebookWidget
 from soitool.codebook_model_view import CodebookTableModel
+from soitool.soi_db_widget import SOIDbWidget
+from soitool.soi_model_view import SOITableModel
 from soitool.database import Database, DBPATH
 from soitool.help_actions import ShortcutsHelpDialog, BasicUsageHelpDialog
 from soitool.serialize_export_import_soi import (
@@ -117,6 +119,7 @@ class MainWindow(QMainWindow):
         open_file_db = QAction("Åpne fra DB", self)
         open_file_db.setShortcut("Ctrl+d")
         open_file_db.setStatusTip("Åpne en SOI fra databasen")
+        open_file_db.triggered.connect(self.show_soi_db)
         file_menu.addAction(open_file_db)
 
         # Preview SOI
@@ -235,8 +238,8 @@ class MainWindow(QMainWindow):
         """
         widget_in_tab = self.tabs.widget(index)
 
-        # Close db-connection if tab is a codebook-tab
-        if isinstance(widget_in_tab, CodebookWidget):
+        # Close db-connection if tab is a CodebookWidget or SOIDbWidget
+        if isinstance(widget_in_tab, (CodebookWidget, SOIDbWidget)):
             widget_in_tab.view.close_db_connection()
 
         self.tabs.removeTab(index)
@@ -335,7 +338,24 @@ class MainWindow(QMainWindow):
 
         # If tab contains an SOI
         if isinstance(tab_widget, SOIWorkspaceWidget):
+            # Update tab showing SOI's in db if it is open,
+            # and pause database-lock by codebook-tab if it is open
+            soi_db_view = None
+            codebook_db_view = None
+            for i in range(self.tabs.count()):
+                if self.tabs.tabText(i) == "SOI'er i DB":
+                    soi_db_view = self.tabs.widget(i).view
+                    soi_db_view.setModel(None)
+                elif self.tabs.tabText(i) == "Kodebok":
+                    codebook_db_view = self.tabs.widget(i).view
+                    codebook_db_view.setModel(None)
+
             self.database.insert_or_update_soi(tab_widget.soi)
+
+            if soi_db_view is not None:
+                soi_db_view.setModel(SOITableModel())
+            if codebook_db_view is not None:
+                codebook_db_view.setModel(CodebookTableModel())
         else:
             exec_info_dialog(
                 "Valgt tab er ingen SOI-tab",
@@ -343,6 +363,23 @@ class MainWindow(QMainWindow):
                 "Riktig tab må velges for å lagre en SOI i DB.",
             )
 
+    def show_soi_db(self):
+        """Open and select tab containing SOIDbWidget.
+
+        Select tab if it is already open,
+        create and select tab if not open.
+        """
+        # Loop through tabs to look for existing SOI-db-tab:
+        for i in range(self.tabs.count()):
+            if self.tabs.tabText(i) == "SOI'er i DB":
+                self.tabs.setCurrentIndex(i)
+                break
+        # SOI-db-tab does not exist, create, add and select tab
+        else:
+            tab = SOIDbWidget(self.database, self.tabs)
+            self.tabs.addTab(tab, "SOI'er i DB")
+            self.tabs.setCurrentWidget(tab)
+
     def open_shortcut_help(self):
         """Open shortcut dialog."""
         self.popup_shortcut_help.setWindowTitle("Hurtigtaster")
diff --git a/soitool/media/authenticationboardmodule.PNG b/soitool/media/authenticationboardmodule.PNG
index 0a92c03e3e403e6cb1bd41716b7feacc5d050b69..9df0e912d0c92a22c40cbe72641897146749ab5f 100644
Binary files a/soitool/media/authenticationboardmodule.PNG and b/soitool/media/authenticationboardmodule.PNG differ
diff --git a/soitool/media/subtractorcodesmodule.PNG b/soitool/media/subtractorcodesmodule.PNG
new file mode 100644
index 0000000000000000000000000000000000000000..a74e8c1a2b9bcec0b1a55e6205c137c53ca8aabc
Binary files /dev/null and b/soitool/media/subtractorcodesmodule.PNG differ
diff --git a/soitool/module_list.py b/soitool/module_list.py
index 9c4b9ba9795ce4dca060f7b839e5236cbb7b6a75..64c62e84200bf56fad6bb145357433b1bf463fff 100644
--- a/soitool/module_list.py
+++ b/soitool/module_list.py
@@ -171,10 +171,10 @@ class ModuleList(QListWidget):
 
             # Update module/attachment priority (order in list):
             if ModuleType(self.type) == ModuleType.MAIN_MODULE:
-                moving_module = self.parent.soi.modules.pop(origin)
+                moving_module = self.soi.modules.pop(origin)
                 self.soi.modules.insert(destination, moving_module)
             elif ModuleType(self.type) == ModuleType.ATTACHMENT_MODULE:
-                moving_module = self.parent.soi.attachments.pop(origin)
+                moving_module = self.soi.attachments.pop(origin)
                 self.soi.attachments.insert(destination, moving_module)
 
             self.soi.reorganize()
diff --git a/soitool/modules/code_table_base.py b/soitool/modules/code_table_base.py
new file mode 100644
index 0000000000000000000000000000000000000000..f00585f7f2fdd66083af9aaf3a2d3b8bdc1ccd87
--- /dev/null
+++ b/soitool/modules/code_table_base.py
@@ -0,0 +1,224 @@
+"""Module containing parent-class for code-table-modules.
+
+Parent-class for AuthenticationBoardModule and SubtractorCodesModule.
+"""
+from PySide2.QtWidgets import QTableWidget, QTableWidgetItem
+from PySide2.QtCore import Qt
+from soitool.coder import get_code_set, get_code
+from soitool.modules.module_base import (
+    ModuleBase,
+    get_table_size,
+    resize_table,
+    HEADLINE_FONT,
+)
+
+AUTHENTICATIONBOARD_MODULE = "AuthenticationBoardModule"
+SUBTRACTORCODES_MODULE = "SubtractorcodesModule"
+
+
+class Meta(type(ModuleBase), type(QTableWidget)):
+    """Used as a metaclass to enable multiple inheritance."""
+
+
+class CodeTableBase(ModuleBase, QTableWidget, metaclass=Meta):
+    """Parent-class for AuthenticationBoardModule and SubtractorcodesModule.
+
+    Inherits from ModuleBase and QTableWidget.
+    ModuleBase is used as an interface, it's methods are overridden.
+    """
+
+    def __init__(self, size, data):
+        if self.type not in [
+            AUTHENTICATIONBOARD_MODULE,
+            SUBTRACTORCODES_MODULE,
+        ]:
+            raise ValueError(
+                "Invalid value for class-variable type: "
+                "'{}'".format(self.type)
+            )
+        QTableWidget.__init__(self)
+        ModuleBase.__init__(self)
+
+        # Remove headers and scrollbars
+        self.horizontalHeader().hide()
+        self.verticalHeader().hide()
+        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+
+        # Resize table when headline changes
+        self.cellChanged.connect(
+            lambda: resize_table(
+                self,
+                resize_rows=False,
+                resize_columns=False,
+                has_headline=True,
+            )
+        )
+        # If parameters are None, generate new table
+        if size is None and data is None:
+            self.generate_table()
+            self.resizeColumnsToContents()
+            self.insert_headline(self.headline)
+
+            # Resize height of rows and set size of window
+            resize_table(self, resize_columns=False, has_headline=True)
+        else:
+            self.setColumnCount(len(data[1]))
+            self.setRowCount(len(data) - 1)  # - 1 to skip headline
+
+            # Set cell-items
+            for i in range(self.rowCount()):
+                for j in range(self.columnCount()):
+                    item = QTableWidgetItem(data[i + 1][j])  # +1 skip headline
+                    item.setTextAlignment(Qt.AlignCenter)
+                    self.setItem(i, j, item)
+
+            self.resizeColumnsToContents()
+            self.insert_headline(data[0])
+
+            resize_table(self, resize_columns=False, has_headline=True)
+            self.setFixedWidth(size["width"])
+            self.setFixedHeight(size["height"])
+
+    def generate_table(self):
+        """Insert row identifiers and codes."""
+        # Set number of rows and columns
+        self.setRowCount(self.start_no_of_codes)
+        self.setColumnCount(3)
+        self.insert_row_identifiers()
+
+        # Generate codes
+        codes = list(
+            get_code_set(
+                self.start_no_of_codes,
+                self.code_length,
+                self.code_character_type,
+                self.space_interval,
+                self.space_amount,
+            )
+        )
+        # Insert codes
+        for i in range(self.start_no_of_codes):
+            # Insert non-editable code in third column
+            item_third = QTableWidgetItem(codes[i])
+            if self.type == SUBTRACTORCODES_MODULE:
+                item_third.setTextAlignment(Qt.AlignCenter)
+            item_third.setFlags(item_third.flags() ^ Qt.ItemIsEditable)
+            self.setItem(i, 2, item_third)
+
+    def insert_headline(self, text=None):
+        """Insert headline text.
+
+        Parameters
+        ----------
+        text : string, optional
+            The headline text, self.headline is used if None,
+            by default None.
+        """
+        headline = self.headline if text is None else text
+
+        item_headline = QTableWidgetItem(headline)
+        item_headline.setTextAlignment(Qt.AlignHCenter)
+        item_headline.setFont(HEADLINE_FONT)
+
+        self.insertRow(0)
+        self.setItem(0, 0, item_headline)
+        self.setSpan(0, 0, 1, self.columnCount())  # Make cell span all columns
+
+    def generate_unique_authentication_code(self):
+        """Generate authentication-code that does not already exist.
+
+        Returns
+        -------
+        string
+            Generated, unique authentication-code.
+        """
+        # Get existing codes
+        existing_codes = self.get_codes()
+
+        # Randomly generate a new code until it is unique
+        unique_code = False
+        while not unique_code:
+            code = get_code(
+                self.code_length,
+                self.code_character_type,
+                self.space_interval,
+                self.space_amount,
+            )
+            unique_code = code not in existing_codes
+
+        return code
+
+    def get_codes(self):
+        """Get all codes in table.
+
+        Returns
+        -------
+        List
+            List containing codes.
+        """
+        codes = []
+
+        # Start from 1 to skip headline-row
+        for i in range(1, self.rowCount()):
+            codes.append(self.item(i, 2).text())
+
+        return codes
+
+    def keyPressEvent(self, event):
+        """Add or remove row when 'Ctrl + +' and 'Ctrl + -' are pressed."""
+        if (
+            event.modifiers() == Qt.ControlModifier
+            and event.key() == Qt.Key_Plus
+        ):
+            # If a row is selected
+            row_index = self.currentRow()
+            if row_index != -1:
+                self.add_row(row_index)
+        elif (
+            event.modifiers() == Qt.ControlModifier
+            and event.key() == Qt.Key_Underscore
+        ):
+            # If at least one row (+ headline-row) exists and a row other than
+            # headline-row is selected
+            row_index = self.currentRow()
+            if self.rowCount() > 2 and row_index != 0 and row_index != -1:
+                self.remove_row(row_index)
+        else:
+            super().keyPressEvent(event)
+
+    def get_size(self):
+        """Return size of table."""
+        return get_table_size(self)
+
+    def get_data(self):
+        """Return list containing all data.
+
+        Returns
+        -------
+        List
+            List[0] contains headline,
+            list[x][y] represents value of row x, column y.
+        """
+        data = []
+        item_headline = self.item(0, 0)
+        if item_headline is not None:
+            data.append(item_headline.text())
+
+        for i in range(1, self.rowCount()):
+            row = []
+            for j in range(self.columnCount()):
+                row.append(self.item(i, j).text())
+            data.append(row)
+
+        return data
+
+    @staticmethod
+    def get_icon():
+        """Abstract method, should be implemented by derived class."""
+        raise NotImplementedError
+
+    @staticmethod
+    def get_user_friendly_name():
+        """Abstract method, should be implemented by derived class."""
+        raise NotImplementedError
diff --git a/soitool/modules/module_authentication_board.py b/soitool/modules/module_authentication_board.py
index 92a35dd08f7fe9f9b030def1940564da9bd4b369..e1dc99965b92e0c85d5c973da40d8a504b96c4e3 100644
--- a/soitool/modules/module_authentication_board.py
+++ b/soitool/modules/module_authentication_board.py
@@ -1,278 +1,146 @@
 """Module containing SOI-module 'Autentiseringstavle'."""
 import string
-from PySide2.QtWidgets import QTableWidget, QTableWidgetItem
+from PySide2.QtWidgets import QTableWidgetItem
 from PySide2 import QtGui
 from PySide2.QtCore import Qt
-from soitool.coder import get_code_set, get_code
-from soitool.modules.module_base import (
-    ModuleBase,
-    get_table_size,
-    resize_table,
-    HEADLINE_FONT,
-)
+from soitool.modules.module_base import resize_table
+from soitool.modules.code_table_base import CodeTableBase
 
-START_NO_OF_AUTHENTICATION_CODES = 10
+# Maximum number of codes is len(ROW_IDENTIFIERS)
+START_NO_OF_CODES = 10
 CODE_LENGTH = 25
 
-# Has to be 'ascii', 'digits' or 'combo'. Codes will consist of A-Z if
-# 'ascii', 0-9 if 'digits' and A-Z+0-9 if 'combo'.
-CODE_CHARACTERS = "ascii"
+# Has to be 'ascii', 'digits' or 'combo'
+# Codes will consist of A-Z if 'ascii', 0-9 if 'digits' and A-Z+0-9 if 'combo'.
+CODE_CHARACTER_TYPE = "ascii"
 
-# Characters for first column. It's length determines maximum number of
-# codes (rows).
+# Characters for first column,
+# it's length determines maximum number of codes (rows).
 ROW_IDENTIFIERS = string.ascii_uppercase
 
-# Adds space between sets of characters, 0 => no spaces
+# Adds space between sets of characters, 0 => no spaces.
 # If code is 123456 and interval is 2, code will be 12 34 56
 SPACE_INTERVAL = 5
+SPACE_AMOUNT = 2
 
 HEADLINE_TEXT = "Autentiseringstavle"
 
 
-class Meta(type(ModuleBase), type(QTableWidget)):
-    """Used as a metaclass to enable multiple inheritance."""
-
-
-class AuthenticationBoardModule(ModuleBase, QTableWidget, metaclass=Meta):
-    """Modified QTableWidget representing a 'Autentiseringstavle'.
+class AuthenticationBoardModule(CodeTableBase):
+    """Modified QTableWidget representing SOI-module 'Autentiseringstavle'.
 
     By default, the widget initializes with a headline, a row-count of
-    START_NO_OF_AUTHENTICATION_CODES and three columns.
+    START_NO_OF_CODES and three columns.
     Row x in the first column contains the character ROW_IDENTIFIERS[x].
     Row x in the second column contains the character x.
-    Row x in the third column contains an authentication code.
+    Row x in the third column contains an authentication code of length
+    CODE_LENGTH, spaced out for readability if SPACE_INTERVAL and SPACE_AMOUNT
+    is larger than 0.
 
     If parameters are given, the widget initializes accordingly:
     'size' is a dict: {"width": int, "height": int},
     'data' is a 2D list where data[0] is the headline,
-    and data[x][y] represents row x, column y.
+    and data[x][y] represents the value in row x, column y.
 
     The widget does not use more room than needed, and resizes dynamically.
     It has shortcuts for adding and removing rows.
 
-    Inherits from ModuleBase and QTableWidget.
-    ModuleBase is used as an interface, it's methods are overridden.
+    Codes are not horizontally centered for readability because 'BGD' is wider
+    than 'III' (example) in certain fonts.
     """
 
     def __init__(self, size=None, data=None):
         self.type = "AuthenticationBoardModule"
-        QTableWidget.__init__(self)
-        ModuleBase.__init__(self)
 
-        if CODE_CHARACTERS == "ascii":
+        if CODE_CHARACTER_TYPE == "ascii":
             self.code_characters = string.ascii_uppercase
-        elif CODE_CHARACTERS == "digits":
+        elif CODE_CHARACTER_TYPE == "digits":
             self.code_characters = string.digits
-        elif CODE_CHARACTERS == "combo":
+        elif CODE_CHARACTER_TYPE == "combo":
             self.code_characters = string.ascii_uppercase + string.digits
         else:
             raise ValueError(
-                "Invalid value for CONSTANT 'CODE_CHARACTERS': "
-                "'{}'".format(CODE_CHARACTERS)
-            )
-
-        # Remove headers and scrollbars
-        self.horizontalHeader().hide()
-        self.verticalHeader().hide()
-        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-
-        # If parameters are None, generate new table
-        if size is None and data is None:
-            self.generate_table()
-            self.resizeColumnsToContents()
-            self.insert_headline()
-
-            # Resize height of rows and set size of window
-            resize_table(self, resize_column=False)
-        else:
-            self.setColumnCount(len(data[1]))
-            self.setRowCount(len(data) - 1)  # - 1 to skip headline
-
-            # Set cell-items
-            for i in range(self.rowCount()):
-                for j in range(self.columnCount()):
-                    item = QTableWidgetItem(data[i + 1][j])  # +1 skip headline
-                    self.setItem(i, j, item)
-
-            self.resizeColumnsToContents()
-            resize_table(self, resize_column=False)
-            self.setFixedWidth(size["width"])
-            self.setFixedHeight(size["height"])
-
-            self.insert_headline(data[0])
-
-    def generate_table(self):
-        """Insert row identifiers and authentication codes."""
-        # Set number of rows and columns
-        self.setRowCount(START_NO_OF_AUTHENTICATION_CODES)
-        self.setColumnCount(3)
-
-        # Generate codes
-        codes = list(
-            get_code_set(
-                START_NO_OF_AUTHENTICATION_CODES,
-                CODE_LENGTH,
-                CODE_CHARACTERS,
-                SPACE_INTERVAL,
+                "Invalid value for CONSTANT 'CODE_CHARACTER_TYPE': "
+                "'{}'".format(CODE_CHARACTER_TYPE)
             )
-        )
-
-        # Insert table data
-        for i in range(START_NO_OF_AUTHENTICATION_CODES):
+        self.start_no_of_codes = START_NO_OF_CODES
+        self.code_length = CODE_LENGTH
+        self.space_interval = SPACE_INTERVAL
+        self.space_amount = SPACE_AMOUNT
+        self.code_character_type = CODE_CHARACTER_TYPE
+        self.headline = HEADLINE_TEXT
+
+        CodeTableBase.__init__(self, size, data)
+
+    def insert_row_identifiers(self):
+        """Insert values in column one and two."""
+        for i in range(self.rowCount()):
             # Insert non-editable row identifier in first column
             item_first = QTableWidgetItem(ROW_IDENTIFIERS[i])
+            item_first.setTextAlignment(Qt.AlignCenter)
             item_first.setFlags(item_first.flags() ^ Qt.ItemIsEditable)
             self.setItem(i, 0, item_first)
 
             # Insert non-editable row identifier (int) in second column
             item_second = QTableWidgetItem(str(i))
+            item_second.setTextAlignment(Qt.AlignCenter)
             item_second.setFlags(item_second.flags() ^ Qt.ItemIsEditable)
             self.setItem(i, 1, item_second)
 
-            # Insert non-editable code in third column
-            item_third = QTableWidgetItem(codes[i])
-            item_third.setFlags(item_third.flags() ^ Qt.ItemIsEditable)
-            self.setItem(i, 2, item_third)
-
-    def insert_headline(self, text=HEADLINE_TEXT):
-        """Insert headline text.
+    def add_row(self, selected_row_index):
+        """Insert row below the selected row and add data.
 
         Parameters
         ----------
-        text : string, optional
-            The headline text, by default HEADLINE_TEXT
+        selected_row_index : int
+            Index of the selected row.
         """
-        item_headline = QTableWidgetItem(text)
-        item_headline.setTextAlignment(Qt.AlignHCenter)
-        item_headline.setFont(HEADLINE_FONT)
-
-        self.insertRow(0)
-        self.setItem(0, 0, item_headline)
-        self.setSpan(0, 0, 1, self.columnCount())  # Make cell span all columns
-
-    def generate_unique_authentication_code(self):
-        """Generate authentication-code that does not already exist.
-
-        Returns
-        -------
-        string
-            Generated, unique authentication-code
-        """
-        # Get existing codes
-        existing_codes = self.get_codes()
-
-        # Randomly generate a new code until it is unique
-        unique_code = False
-        while not unique_code:
-            code = get_code(CODE_LENGTH, CODE_CHARACTERS, SPACE_INTERVAL)
-            unique_code = code not in existing_codes
-
-        return code
-
-    def keyPressEvent(self, event):
-        """Add or remove row when 'Ctrl + +' and 'Ctrl + -' are pressed."""
-        if (
-            event.modifiers() == Qt.ControlModifier
-            and event.key() == Qt.Key_Plus
-        ):
-            self.add_row()
-        elif (
-            event.modifiers() == Qt.ControlModifier
-            and event.key() == Qt.Key_Underscore
-        ):
-            self.remove_row()
-        else:
-            super(AuthenticationBoardModule, self).keyPressEvent(event)
-
-    def add_row(self):
-        """Insert row below the selected row and add data."""
-        row_index = self.currentRow()
-        # If maximum amount of rows not reached and a row is selected
-        # (+ 1 to skip row containing headline)
-        if self.rowCount() < len(ROW_IDENTIFIERS) + 1 and row_index != -1:
+        # If maximum amount of rows not reached (- 1 to skip headline)
+        if self.rowCount() - 1 < len(ROW_IDENTIFIERS):
             # Generate unique code and insert row
             code = self.generate_unique_authentication_code()
-            self.insertRow(row_index + 1)
+            self.insertRow(selected_row_index + 1)
 
             # Loop through all rows starting with the new row
-            for i in range(row_index + 1, self.rowCount()):
+            for i in range(selected_row_index + 1, self.rowCount()):
                 # Insert row identifier in first column
                 item_first = QTableWidgetItem(self.code_characters[i - 1])
+                item_first.setTextAlignment(Qt.AlignCenter)
                 item_first.setFlags(item_first.flags() ^ Qt.ItemIsEditable)
                 self.setItem(i, 0, item_first)
 
                 # Insert row identifier (int) in second column
                 item_second = QTableWidgetItem(str(i - 1))
+                item_second.setTextAlignment(Qt.AlignCenter)
                 item_second.setFlags(item_second.flags() ^ Qt.ItemIsEditable)
                 self.setItem(i, 1, item_second)
 
             # Insert authentication-code in third column
             item_third = QTableWidgetItem(code)
             item_third.setFlags(item_third.flags() ^ Qt.ItemIsEditable)
-            self.setItem(row_index + 1, 2, item_third)
+            self.setItem(selected_row_index + 1, 2, item_third)
 
             # Resize code-column in case it got wider
             # Example: 'BGD' is wider than 'III' (depending on font)
             self.resizeColumnToContents(2)
-            resize_table(self, resize_column=False)
-
-    def remove_row(self):
-        """Remove selected row."""
-        row_index = self.currentRow()
-        # If at least one row (+ headline-row) exists and a row other than
-        # headline-row is selected
-        if self.rowCount() > 2 and row_index != 0 and row_index != -1:
-            # Remove row
-            self.removeRow(row_index)
-            # 'Decrease' row identifiers below the removed row
-            # If first row is removed, identifier A,B,C becomes A,B (not B,C)
-            for i in range(row_index, self.rowCount()):
-                self.item(i, 0).setText(self.code_characters[i - 1])
-                self.item(i, 1).setText(str(i - 1))
-        resize_table(self, resize_column=False)
+            resize_table(self, resize_columns=False, has_headline=True)
 
-    def get_codes(self):
-        """Get all authentication-codes in table.
-
-        Returns
-        -------
-        List
-            List containing authentication-codes.
-        """
-        codes = []
+    def remove_row(self, row_index):
+        """Remove selected row.
 
-        # Start with row 1 to skip headline-row
-        for i in range(1, self.rowCount()):
-            codes.append(self.item(i, 2).text())
-
-        return codes
-
-    def get_size(self):
-        """Return size of widget."""
-        return get_table_size(self)
-
-    def get_data(self):
-        """Return list containing all data.
-
-        Returns
-        -------
-        List
-            List[0] contains headline,
-            list[x][y] represents value of row x, column y.
+        Parameters
+        ----------
+        row_index : int
+            Index of the row to remove.
         """
-        content = []
-        item_headline = self.item(0, 0)
-        if item_headline is not None:
-            content.append(item_headline.text())
-
-        for i in range(1, self.rowCount()):
-            row = []
-            for j in range(self.columnCount()):
-                row.append(self.item(i, j).text())
-            content.append(row)
-
-        return content
+        self.removeRow(row_index)
+
+        # 'Decrease' row identifiers below the removed row
+        # If first row is removed, identifier A,B,C becomes A,B (not B,C)
+        for i in range(row_index, self.rowCount()):
+            self.item(i, 0).setText(self.code_characters[i - 1])
+            self.item(i, 1).setText(str(i - 1))
+        resize_table(self, resize_columns=False, has_headline=True)
 
     @staticmethod
     def get_user_friendly_name():
diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py
index c608a17177d43a16371b675d8f5a815f82a87e27..7b87d96b67669e18bd34a5315ea358b38a4f9ae7 100644
--- a/soitool/modules/module_base.py
+++ b/soitool/modules/module_base.py
@@ -15,6 +15,7 @@ class ModuleBase(ABC):
     type = None
 
     def __init__(self):
+        """Class-variable 'type' should be set by derived class."""
         if self.type is None:
             raise NotImplementedError
 
@@ -37,27 +38,50 @@ class ModuleBase(ABC):
         raise NotImplementedError
 
 
-def resize_table(widget, resize_row=True, resize_column=True):
-    """Calculate and set the size of a QTableWidget.
+def resize_table(
+    table, resize_rows=True, resize_columns=True, has_headline=False
+):
+    """Resize a given QTableWidget.
 
     Parameters
     ----------
-    widget : QTableWidget
-        QTablewidget-instance to calculate and set size.
-    resize_row : bool
+    table : QTableWidget
+        QTablewidget-instance to resize.
+    resize_rows : bool
         Resizes rows to contents if True, by default True.
-    resize_column : bool
+    resize_columns : bool
         Resizes columns to contents if True, by default True.
+    has_headline : bool
+        True if the table has a headline, by default False.
+        Last column is widened if headline is wider than table.
     """
-    if resize_row:
-        widget.resizeRowsToContents()
-    if resize_column:
-        widget.resizeColumnsToContents()
-
-    width, height = get_table_size(widget)
-
-    widget.setFixedWidth(width)
-    widget.setFixedHeight(height)
+    if resize_columns:
+        table.resizeColumnsToContents()
+    if resize_rows:
+        table.resizeRowsToContents()
+
+    # If table has a headline, make sure table is wide enough to fit it.
+    if has_headline:
+        last_column_index = table.columnCount() - 1
+        table.resizeColumnToContents(last_column_index)
+        width, height = get_table_size(table)
+
+        # Get width of headline
+        headline = table.item(0, 0).text()
+        headline_width = (
+            QtGui.QFontMetricsF(HEADLINE_FONT).horizontalAdvance(headline) + 10
+        )
+        # If headline is wider than table
+        if width < headline_width:
+            difference = headline_width - width
+            width += difference
+            old_width = table.columnWidth(last_column_index)
+            table.setColumnWidth(last_column_index, old_width + difference)
+    else:
+        width, height = get_table_size(table)
+
+    table.setFixedWidth(width)
+    table.setFixedHeight(height)
 
 
 def get_table_size(widget):
@@ -73,7 +97,6 @@ def get_table_size(widget):
     Tuple
         Total (width, height)
     """
-    # Calculate total width and height of columns and rows
     width = 0
     height = 0
 
diff --git a/soitool/modules/module_subtractorcodes.py b/soitool/modules/module_subtractorcodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b04ab28ef7d67fcb7944d731c9ed37e989d4988
--- /dev/null
+++ b/soitool/modules/module_subtractorcodes.py
@@ -0,0 +1,141 @@
+"""Module containing SOI-module 'Subtraktorkoder'."""
+import string
+from PySide2.QtWidgets import QTableWidgetItem
+from PySide2 import QtGui
+from PySide2.QtCore import Qt
+from soitool.modules.module_base import resize_table
+from soitool.modules.code_table_base import CodeTableBase
+
+# Maximum number of codes is len(ROW_IDENTIFIERS)/2 columns = 13
+START_NO_OF_CODES = 7
+CODE_LENGTH = 8
+
+# Characters for first and second column
+ROW_IDENTIFIERS = string.ascii_uppercase
+
+# Adds space between sets of characters, 0 => no spaces.
+# If code is 12345678 and interval is 2, code will be 1234 5678
+SPACE_INTERVAL = 4
+SPACE_AMOUNT = 5
+
+HEADLINE_TEXT = "Subtraktorkoder"
+
+
+class SubtractorcodesModule(CodeTableBase):
+    """Modified QTablewidget representing SOI-module 'Subtraktorkoder'.
+
+    By default, the widget initializes with a headline, a row-count of
+    START_NO_OF_CODES and three columns.
+    If there are 2 rows and ROW_IDENTIFIERS is the alphabet, the first
+    column will contain A and B, and the second column will contain C and D.
+    The third column contains subtractorcodes of length CODE_LENGTH, spaced out
+    for readability if SPACE_INTERVAL and SPACE_AMOUNT larger than 0.
+
+    If parameters are given, the widget initializes accordingly:
+    'size' is a dict: {"width": int, "height": int},
+    'data' is a 2D list where data[0] is the headline,
+    and data[x][y] represents the value in row x, column y.
+
+    The widget does not use more room than needed, and resizes dynamically.
+    It has shortcuts for adding and removing rows.
+    """
+
+    def __init__(self, size=None, data=None):
+        self.type = "SubtractorcodesModule"
+
+        if START_NO_OF_CODES > 13:
+            raise ValueError(
+                "Invalid value for CONSTANT 'START_NO_OF_CODES': "
+                "'{}'".format(START_NO_OF_CODES)
+            )
+        self.headline = HEADLINE_TEXT
+        self.code_length = CODE_LENGTH
+        self.start_no_of_codes = START_NO_OF_CODES
+        self.space_interval = SPACE_INTERVAL
+        self.space_amount = SPACE_AMOUNT
+        self.code_character_type = "digits"
+
+        CodeTableBase.__init__(self, size, data)
+
+    def insert_row_identifiers(self, has_headline=False):
+        """Insert row identifiers in first and second column.
+
+        Parameters
+        ----------
+        has_headline : bool, optional
+            True if a headline-row exists, by default False.
+        """
+        start_row = 1 if has_headline else 0
+
+        # Get row identifiers (start-index to skip headline)
+        # for column one and two
+        last_id_index_one = self.rowCount() - start_row
+        start_id_index_two = self.rowCount() - start_row
+        last_id_index_two = self.rowCount() * 2 - 2 * start_row
+
+        # If table has 3 rows and ROW_IDENTIFIERS are A-Z,
+        # identifiers_one = A, B, C and identifiers_two = C, D, E
+        identifiers_one = ROW_IDENTIFIERS[0:last_id_index_one]
+        identifiers_two = ROW_IDENTIFIERS[start_id_index_two:last_id_index_two]
+
+        # Insert identifiers in column one and two
+        for i in range(2):
+            for j in range(start_row, self.rowCount()):
+                text = (
+                    identifiers_one[j - start_row]
+                    if i == 0
+                    else identifiers_two[j - start_row]
+                )
+                item = QTableWidgetItem(text)
+                item.setTextAlignment(Qt.AlignCenter)
+                item.setFlags(item.flags() ^ Qt.ItemIsEditable)
+                self.setItem(j, i, item)
+
+    def add_row(self, selected_row_index):
+        """Insert row below the selected row and add data.
+
+        Parameters
+        ----------
+        selected_row_index : int
+            Index of the selected row.
+        """
+        # If maximum amount of rows not reached (- 1 to skip headline)
+        if self.rowCount() - 1 < len(ROW_IDENTIFIERS) / 2:
+
+            # Generate unique code and insert row
+            code = self.generate_unique_authentication_code()
+            self.insertRow(selected_row_index + 1)
+
+            # Insert row identifiers
+            self.insert_row_identifiers(has_headline=True)
+
+            # Insert code
+            item_code = QTableWidgetItem(code)
+            item_code.setTextAlignment(Qt.AlignCenter)
+            item_code.setFlags(item_code.flags() ^ Qt.ItemIsEditable)
+            self.setItem(selected_row_index + 1, 2, item_code)
+
+            resize_table(self, resize_columns=False, has_headline=True)
+
+    def remove_row(self, row_index):
+        """Remove the selected row.
+
+        Parameters
+        ----------
+        row_index : int
+            Index of the row to remove.
+        """
+        self.removeRow(row_index)
+
+        self.insert_row_identifiers(has_headline=True)
+        resize_table(self, resize_columns=False, has_headline=True)
+
+    @staticmethod
+    def get_user_friendly_name():
+        """Get user-friendly name of module."""
+        return "Subtraktorkoder"
+
+    @staticmethod
+    def get_icon():
+        """Get icon of module."""
+        return QtGui.QIcon("soitool/media/subtractorcodesmodule.png")
diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py
index 030ad405ddfc7142d84f0ac45697af09e7c685e3..a4b917ab9911bd4d9d9961d35f92f9f24a4323f1 100644
--- a/soitool/modules/module_table.py
+++ b/soitool/modules/module_table.py
@@ -50,7 +50,7 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
             self.setRowCount(START_ROWS)
 
             # Resize width and height of rows, columns and window
-            resize_table(self, resize_row=True, resize_column=True)
+            resize_table(self, resize_rows=True, resize_columns=True)
 
             # Set header-items
             for i in range(self.columnCount()):
@@ -75,7 +75,7 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
             self.setFixedHeight(size["height"])
 
         self.cellChanged.connect(
-            lambda: resize_table(self, resize_row=True, resize_column=True)
+            lambda: resize_table(self, resize_rows=True, resize_columns=True)
         )
 
     def keyPressEvent(self, event):
@@ -131,24 +131,24 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
         """Add column to the right of selected column."""
         self.insertColumn(self.currentColumn() + 1)
         self.set_header_item(self.currentColumn() + 1, "")
-        resize_table(self, resize_row=True, resize_column=True)
+        resize_table(self, resize_rows=True, resize_columns=True)
 
     def remove_column(self):
         """Remove selected column if two or more columns exist."""
         if self.columnCount() > 1:
             self.removeColumn(self.currentColumn())
-            resize_table(self, resize_row=True, resize_column=True)
+            resize_table(self, resize_rows=True, resize_columns=True)
 
     def add_row(self):
         """Add row below selected row."""
         self.insertRow(self.currentRow() + 1)
-        resize_table(self, resize_row=True, resize_column=True)
+        resize_table(self, resize_rows=True, resize_columns=True)
 
     def remove_row(self):
         """Remove selected row if two or more rows exist (including header)."""
         if self.rowCount() > 2 and self.currentRow() != 0:
             self.removeRow(self.currentRow())
-            resize_table(self, resize_row=True, resize_column=True)
+            resize_table(self, resize_rows=True, resize_columns=True)
 
     def get_size(self):
         """Return size of widget."""
diff --git a/soitool/new_module_dialog.py b/soitool/new_module_dialog.py
index acc3746ca017a7d8869694a71d58ee7788843338..c0b33101cbae9df2a79d25d94c3bd22933575236 100644
--- a/soitool/new_module_dialog.py
+++ b/soitool/new_module_dialog.py
@@ -16,6 +16,7 @@ from soitool.modules.module_table import TableModule
 from soitool.modules.module_authentication_board import (
     AuthenticationBoardModule,
 )
+from soitool.modules.module_subtractorcodes import SubtractorcodesModule
 from soitool.modules.module_freetext import FreeTextModule
 
 
@@ -25,6 +26,7 @@ from soitool.modules.module_freetext import FreeTextModule
 MODULE_CHOICES = [
     TableModule,
     AuthenticationBoardModule,
+    SubtractorcodesModule,
     FreeTextModule,
 ]
 
diff --git a/soitool/serialize_export_import_soi.py b/soitool/serialize_export_import_soi.py
index 1f1beef2265a83a37d19710a91334d40494ab1cb..97c6cb236daa0c770853992285d5e32e4c4a3522 100644
--- a/soitool/serialize_export_import_soi.py
+++ b/soitool/serialize_export_import_soi.py
@@ -8,6 +8,7 @@ from soitool.modules.module_table import TableModule
 from soitool.modules.module_authentication_board import (
     AuthenticationBoardModule,
 )
+from soitool.modules.module_subtractorcodes import SubtractorcodesModule
 from soitool.modules.module_freetext import FreeTextModule
 
 # Valid schema for serialized SOI
@@ -254,6 +255,15 @@ def construct_soi_from_serialized(serialized, compressed=False):
                     "meta": module["meta"],
                 }
             )
+        elif module_type == "SubtractorcodesModule":
+            size = module["size"]
+            data = module["data"]
+            modules.append(
+                {
+                    "widget": SubtractorcodesModule(size, data),
+                    "meta": module["meta"],
+                }
+            )
         elif module_type == "FreeTextModule":
             size = module["size"]
             data = module["data"]
diff --git a/soitool/soi_db_widget.py b/soitool/soi_db_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..343c7d4672c7b7db8557cb59c5836960d2dd317b
--- /dev/null
+++ b/soitool/soi_db_widget.py
@@ -0,0 +1,19 @@
+"""Module containing a widget for viewing and opening SOI's from database."""
+from PySide2.QtWidgets import QWidget, QHBoxLayout
+from soitool.soi_model_view import SOITableView
+
+
+class SOIDbWidget(QWidget):
+    """Widget for viewing and opening SOI's from database."""
+
+    def __init__(self, database, tab_widget):
+        super().__init__()
+
+        self.view = SOITableView(database, tab_widget)
+        self.create_and_set_layout()
+
+    def create_and_set_layout(self):
+        """Create layout, add widget and set layout."""
+        hbox = QHBoxLayout()
+        hbox.addWidget(self.view)
+        self.setLayout(hbox)
diff --git a/soitool/soi_model_view.py b/soitool/soi_model_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..680d27502fdefbec63c274fcd4c4c4ae1084b7b7
--- /dev/null
+++ b/soitool/soi_model_view.py
@@ -0,0 +1,148 @@
+"""GUI-interface towards database-table 'SOI'.
+
+Contains functionality for viewing and opening SOI's from database-table 'SOI',
+where (some) SOI's are stored.
+"""
+from PySide2.QtWidgets import QTableView
+from PySide2.QtSql import QSqlDatabase, QSqlTableModel
+from PySide2.QtCore import Qt
+from soitool.style import CODEBOOK_HEADER_FONT, CODEBOOK_HEADER_BACKGROUND_CSS
+from soitool.serialize_export_import_soi import construct_soi_from_serialized
+from soitool.soi_workspace_widget import SOIWorkspaceWidget
+
+# Name and type of database
+CONNAME = "SOIDB"
+DBTYPE = "QSQLITE"
+
+
+class SOITableView(QTableView):
+    """TableView with a model of the 'SOI'-table from database.
+
+    This modified QTableView creates a SOITableModel, which reads data from the
+    SOI-table. When the user double-clicks or presses the enter-key on a cell,
+    a tab containing SOIWorkspaceWidget, with the SOI from the current row, is
+    opened and selected.
+
+    Parameters
+    ----------
+    database : soitool.database.Database
+        Is used to create a QSqlDatabase from the database-file.
+    tab_widget : QTabWidget
+        Is used to open a new tab.
+
+    Raises
+    ------
+    RuntimeError
+        If database does not open.
+    """
+
+    def __init__(self, database, tab_widget):
+        super().__init__()
+        db = QSqlDatabase.addDatabase(DBTYPE, CONNAME)
+        db.setDatabaseName(database.db_path)
+        self.tab_widget = tab_widget
+
+        if not db.open():
+            raise RuntimeError("Could not open database.")
+
+        # Enable sorting
+        self.setSortingEnabled(True)
+
+        # Create and set model
+        model = SOITableModel()
+        self.setModel(model)
+
+        # Remove horizontal scrollbar, hide vertical header and 'SOI'-column
+        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.verticalHeader().hide()
+        self.hideColumn(2)
+
+        # Set horizontal header-text and it's style
+        self.set_horizontal_header_text()
+        header = self.horizontalHeader()
+        header.setFont(CODEBOOK_HEADER_FONT)
+        header.setStyleSheet(CODEBOOK_HEADER_BACKGROUND_CSS)
+
+        # Resize
+        self.resizeColumnsToContents()
+        width = (
+            self.columnWidth(0) + self.columnWidth(1) + self.columnWidth(3) + 2
+        )  # +2 offset
+        self.setFixedWidth(width)
+
+        self.doubleClicked.connect(self.open_soi_tab)
+
+    def set_horizontal_header_text(self):
+        """Set Norwegian names in horizontal header."""
+        self.model().setHeaderData(0, Qt.Horizontal, "Tittel")
+        self.model().setHeaderData(1, Qt.Horizontal, "Versjon")
+        self.model().setHeaderData(3, Qt.Horizontal, "Dato")
+
+    def keyPressEvent(self, event):
+        """Open SOI-tab if enter-key is pressed."""
+        if event.key() == Qt.Key_Return:
+            self.open_soi_tab()
+        super().keyPressEvent(event)
+
+    def open_soi_tab(self):
+        """Construct SOI and open SOIWorkspacewidget in new tab."""
+        # Get index of the current row and read compressed, serialized SOI
+        row = self.currentIndex().row()
+        compressed_soi = self.model().index(row, 2).data()
+
+        # Construct SOI and create SOIWorkspaceWidget
+        soi = construct_soi_from_serialized(compressed_soi, compressed=True)
+        tab = SOIWorkspaceWidget(soi)
+
+        # Add and select tab
+        self.tab_widget.addTab(tab, soi.title)
+        self.tab_widget.setCurrentWidget(tab)
+
+    def setModel(self, model):
+        """Set model, resize and hide 'SOI'-column.
+
+        Parameters
+        ----------
+        model : soitool.soi_model_view.SOITableModel or None
+            Model containing data to display.
+        """
+        super().setModel(model)
+        if model is not None:
+            self.hideColumn(2)
+            self.resizeColumnsToContents()
+            width = (
+                self.columnWidth(0)
+                + self.columnWidth(1)
+                + self.columnWidth(3)
+                + 2  # + 2 offset
+            )
+            self.setFixedWidth(width)
+            self.sortByColumn(3, Qt.DescendingOrder)  # Sort by 'Date'-column
+
+    def close_db_connection(self):
+        """Close connection to database."""
+        self.setModel(None)
+        QSqlDatabase.removeDatabase(CONNAME)
+
+
+class SOITableModel(QSqlTableModel):
+    """Uneditable QSqlTableModel of database-table 'SOI'."""
+
+    def __init__(self):
+        super().__init__(None, QSqlDatabase.database(CONNAME))
+        self.setTable("SOI")
+        self.select()
+        self.sort(3, Qt.DescendingOrder)  # Sort by 'Date'-column
+
+    def flags(self, index):
+        """Disable editing.
+
+        Parameters
+        ----------
+        index : QModelIndex
+            Is used to locate data in a model.
+        """
+        flags = super().flags(index)
+        flags ^= Qt.ItemIsEditable
+
+        return flags
diff --git a/test/test_database.py b/test/test_database.py
index 42c705285dbe516df3471c488d4032263c366fc1..219431d7ee2e89fb568771abfd6a2ab44f98afce 100644
--- a/test/test_database.py
+++ b/test/test_database.py
@@ -17,6 +17,9 @@ TESTDBPATH = os.path.join(SOITOOL_DIR, TESTDBNAME)
 
 TESTDATA_PATH = Path(__file__).parent.parent / "soitool/testdata"
 
+# Tolerance for timed tests. Tolerating DELTA seconds of difference
+DELTA = 10
+
 
 class DatabaseTest(unittest.TestCase):
     """Database tests."""
@@ -141,7 +144,7 @@ class DatabaseTest(unittest.TestCase):
         # Calculates difference in seconds
         time_diff = (second_time - first_time).total_seconds()
         # Compares difference with sleep_time
-        self.assertAlmostEqual(time_diff, sleep_time, delta=0.2)
+        self.assertAlmostEqual(time_diff, sleep_time, delta=DELTA)
 
     def test_get_categories(self):
         """Assert function get_categories works as expected."""
@@ -276,7 +279,7 @@ class DatabaseTest(unittest.TestCase):
         expected_time = seconds_in_24h - sleep_time
         actual_time = self.database.seconds_to_next_update(seconds_in_24h)
         # Compares expected and function return value with
-        self.assertAlmostEqual(expected_time, actual_time, delta=0.2)
+        self.assertAlmostEqual(expected_time, actual_time, delta=DELTA)
 
     def teset_seconds_to_next_update_complete_period(self):
         """Check that seconds to next update can returns 0 and not negative."""
diff --git a/test/test_module_authentication_and_subtractor.py b/test/test_module_authentication_and_subtractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..412bab95d15d7ca830fc5d445e2bd3bbc9ba936d
--- /dev/null
+++ b/test/test_module_authentication_and_subtractor.py
@@ -0,0 +1,243 @@
+"""Test AuthenticationBoardModule."""
+import unittest
+import string
+from PySide2 import QtGui
+from PySide2.QtWidgets import QApplication
+from PySide2.QtCore import Qt
+from PySide2.QtTest import QTest
+from soitool.modules.module_authentication_board import (
+    AuthenticationBoardModule,
+    START_NO_OF_CODES as START_NO_OF_CODES_AUTHENTICATION,
+    HEADLINE_TEXT as HEADLINE_TEXT_AUTHENTICATION,
+    ROW_IDENTIFIERS as ROW_IDENTIFIERS_AUTHENTICATION,
+    CODE_LENGTH as CODE_LENGTH_AUTHENTICATION,
+)
+from soitool.modules.module_subtractorcodes import (
+    SubtractorcodesModule,
+    START_NO_OF_CODES as START_NO_OF_CODES_SUBTRACTOR,
+    HEADLINE_TEXT as HEADLINE_TEXT_SUBTRACTOR,
+    ROW_IDENTIFIERS as ROW_IDENTIFIERS_SUBTRACTOR,
+    CODE_LENGTH as CODE_LENGTH_SUBTRACTOR,
+)
+from soitool.soi import SOI
+
+if isinstance(QtGui.qApp, type(None)):
+    app = QApplication([])
+else:
+    app = QtGui.qApp
+
+
+class TestDefaultAuthenticationBoardAndSubtractorcodesModule(
+    unittest.TestCase
+):
+    """TestCase for AuthenticationBoardModule and SubtractorcodesModule.
+
+    Both modules inherit from CodeTableBase. Therefore, only
+    AuthenticationBoardModule is used to test mutual functions.
+    Module-specific functionality-tests are, of course, executed.
+    """
+
+    def setUp(self):
+        """Create new AuthenticationBoardModule."""
+        self.authentication = AuthenticationBoardModule()
+        self.subtractor = SubtractorcodesModule()
+
+    def test_default_module(self):
+        """Test that module is initialized properly."""
+        # Assert correct headline
+        self.assertEqual(
+            self.authentication.item(0, 0).text(), HEADLINE_TEXT_AUTHENTICATION
+        )
+        self.assertEqual(
+            self.subtractor.item(0, 0).text(), HEADLINE_TEXT_SUBTRACTOR
+        )
+
+        # Assert correct number of rows including header
+        self.assertEqual(
+            self.authentication.rowCount(),
+            START_NO_OF_CODES_AUTHENTICATION + 1,
+        )
+        self.assertEqual(
+            self.subtractor.rowCount(), START_NO_OF_CODES_SUBTRACTOR + 1,
+        )
+        # Assert correct number of columns
+        self.assertEqual(self.authentication.columnCount(), 3)
+        self.assertEqual(self.subtractor.columnCount(), 3)
+
+        # Assert cell content in first and second column is correct
+        for i in range(1, 3):  # From 1 to skip headline-row
+            self.assertEqual(
+                self.authentication.item(i, 0).text(),
+                ROW_IDENTIFIERS_AUTHENTICATION[i - 1],
+            )
+            self.assertEqual(
+                self.authentication.item(i, 1).text(), str(i - 1),
+            )
+
+            self.assertEqual(
+                self.subtractor.item(i, 0).text(),
+                ROW_IDENTIFIERS_SUBTRACTOR[i - 1],
+            )
+            self.assertEqual(
+                self.subtractor.item(i, 1).text(),
+                ROW_IDENTIFIERS_SUBTRACTOR[
+                    i - 1 + START_NO_OF_CODES_SUBTRACTOR
+                ],
+            )
+
+        # Assert cell content in third column is correct
+        code = self.authentication.item(1, 2).text()
+        self.assertTrue(len(code) >= CODE_LENGTH_AUTHENTICATION)
+        # Assert all code characters are taken from list code_characters
+        for _, character in enumerate(code):
+            if character != " ":
+                self.assertTrue(
+                    character in self.authentication.code_characters
+                )
+        code = self.subtractor.item(1, 2).text()
+        self.assertTrue(len(code) >= CODE_LENGTH_SUBTRACTOR)
+        # Assert all code characters are digits
+        for _, character in enumerate(code):
+            if character != " ":
+                self.assertTrue(character in string.digits)
+
+        # Assert all codes are unique
+        code_list = self.authentication.get_codes()
+        code_set = set(code_list)
+        self.assertEqual(len(code_list), len(code_set))
+
+    def test_generate_unique_authentication_code(self):
+        """Test function generate_unique_authentication_module."""
+        code_list = self.authentication.get_codes()
+        generated_code = (
+            self.authentication.generate_unique_authentication_code()
+        )
+
+        # Assert code length is equal to an existing code
+        self.assertEqual(len(generated_code), len(code_list[0]))
+        # Assert generated code is not equal to any existing codes
+        self.assertFalse(generated_code in code_list)
+
+    def test_get_codes(self):
+        """Test function get_codes."""
+        # Get codes
+        code_list = self.authentication.get_codes()
+
+        # Assert codes are correct
+        for i, code in enumerate(code_list):
+            self.assertEqual(code, self.authentication.item(i + 1, 2).text())
+
+    def test_gui_add_row(self):
+        """Test adding rows with shortcuts."""
+        # Widget must be shown for shortcuts to work
+        self.authentication.show()
+        QTest.qWaitForWindowExposed(self.authentication)
+
+        # From experience it can happen that the widget is not fully
+        # initialized before the QTest.keyClicks below. By calling
+        # processEvents here we give Qt the opportunity to catch up with us
+        app.processEvents()
+
+        old_row_count = self.authentication.rowCount()
+
+        # Use shortcut 'Ctrl + +'
+        QTest.keyClicks(self.authentication, "+", Qt.ControlModifier)
+
+        # Assert a new row is added
+        new_row_count = self.authentication.rowCount()
+        self.assertEqual(old_row_count + 1, new_row_count)
+
+        # Assert new row has correct content
+        row_index = new_row_count - 1
+        self.assertEqual(
+            self.authentication.item(row_index, 0).text(),
+            ROW_IDENTIFIERS_AUTHENTICATION[row_index - 1],
+        )
+        self.assertEqual(
+            self.authentication.item(row_index, 1).text(), str(row_index - 1)
+        )
+        new_code = self.authentication.item(row_index, 2).text()
+        existing_code = self.authentication.item(1, 2).text()
+        self.assertEqual(len(new_code), len(existing_code))
+
+    def test_gui_remove_row(self):
+        """Test removing rows with shortcuts."""
+        # Widget must be shown for shortcuts to work
+        self.authentication.show()
+        QTest.qWaitForWindowExposed(self.authentication)
+
+        # From experience it can happen that the widget is not fully
+        # initialized before the QTest.keyClicks below. By calling
+        # processEvents here we give Qt the opportunity to catch up with us
+        app.processEvents()
+
+        old_row_count = self.authentication.rowCount()
+
+        # User should not be able to delete headline-row with shortcut
+        # Locate headline-item, move mouse and and click (select) item
+        item_center = self.authentication.visualItemRect(
+            self.authentication.item(0, 0)
+        ).center
+        viewport = self.authentication.viewport()
+        QTest.mouseMove(viewport, item_center())
+        QTest.mouseClick(viewport, Qt.LeftButton, Qt.NoModifier, item_center())
+
+        # Try to delete row with shortcut
+        QTest.keyClicks(self.authentication, "_", Qt.ControlModifier)
+
+        # Assert row was not removed
+        new_row_count = self.authentication.rowCount()
+        self.assertEqual(old_row_count, new_row_count)
+
+        # Move to first row (below headline), then use shortcut to delete it
+        QTest.keyClick(self.authentication, Qt.Key_Down)
+        value_to_delete = self.authentication.item(1, 1).text()
+        QTest.keyClicks(self.authentication, "_", Qt.ControlModifier)
+        # Assert row was removed and replaced by the row below
+        new_row_count = self.authentication.rowCount()
+        self.assertEqual(old_row_count - 1, new_row_count)
+        self.assertEqual(
+            self.authentication.item(1, 1).text(), value_to_delete
+        )
+
+        # Remove rows until only headline-row and a single code-row exist
+        for _ in range(1, self.authentication.rowCount() - 1):
+            QTest.keyClick(self.authentication, Qt.Key_Down)
+            QTest.keyClicks(self.authentication, "_", Qt.ControlModifier)
+        self.assertTrue(self.authentication.rowCount() == 2)
+        # Try to remove final code-row, should not work
+        QTest.keyClick(self.authentication, Qt.Key_Down)
+        QTest.keyClicks(self.authentication, "_", Qt.ControlModifier)
+        # Assert row was not removed
+        self.assertTrue(self.authentication.rowCount() == 2)
+
+    def test_add_to_soi_smoke_test(self):
+        """Test that module can be added to SOI."""
+        soi = SOI()
+        authentication_name = "Test name"
+        soi.add_module(authentication_name, self.authentication)
+        self.assertTrue(soi.module_name_taken(authentication_name))
+
+
+class TestAuthenticationBoardModuleFromData(unittest.TestCase):
+    """TestCase for initializing AuthenticationBoardModule from data."""
+
+    def test_create_from_data(self):
+        """Test creating AuthenticationBoardModule from data."""
+        test_data = [
+            "Headline text",
+            ["A", "0", "TEST CODE ONE"],
+            ["B", "1", "TEST CODE TWO"],
+        ]
+        test_size = {"width": 100, "height": 100}
+
+        module = AuthenticationBoardModule(size=test_size, data=test_data)
+
+        # Assert module contains expected data
+        self.assertEqual(module.item(0, 0).text(), "Headline text")
+        self.assertEqual(module.item(1, 0).text(), "A")
+        self.assertEqual(module.item(1, 1).text(), "0")
+        self.assertEqual(module.item(1, 2).text(), "TEST CODE ONE")
+        self.assertEqual(module.item(2, 0).text(), "B")
+        self.assertEqual(module.item(2, 1).text(), "1")
+        self.assertEqual(module.item(2, 2).text(), "TEST CODE TWO")
diff --git a/test/test_module_authentication_board.py b/test/test_module_authentication_board.py
deleted file mode 100644
index 5b3262e48055e0290b8676076e562da3b73913c5..0000000000000000000000000000000000000000
--- a/test/test_module_authentication_board.py
+++ /dev/null
@@ -1,182 +0,0 @@
-"""Test AuthenticationBoardModule."""
-import unittest
-from PySide2 import QtGui
-from PySide2.QtWidgets import QApplication
-from PySide2.QtCore import Qt
-from PySide2.QtTest import QTest
-from soitool.modules.module_authentication_board import (
-    AuthenticationBoardModule,
-    START_NO_OF_AUTHENTICATION_CODES,
-    HEADLINE_TEXT,
-    ROW_IDENTIFIERS,
-    CODE_LENGTH,
-)
-from soitool.soi import SOI
-
-if isinstance(QtGui.qApp, type(None)):
-    app = QApplication([])
-else:
-    app = QtGui.qApp
-
-
-class TestDefaultAuthenticationBoardModule(unittest.TestCase):
-    """TestCase for AuthenticationBoardModule."""
-
-    def setUp(self):
-        """Create new AuthenticationBoardModule."""
-        self.module = AuthenticationBoardModule()
-
-    def test_default_module(self):
-        """Test that module is initialized properly."""
-        # Assert correct headline
-        self.assertEqual(self.module.item(0, 0).text(), HEADLINE_TEXT)
-
-        # Assert correct number of rows including header
-        self.assertEqual(
-            self.module.rowCount(), START_NO_OF_AUTHENTICATION_CODES + 1
-        )
-        # Assert correct number of columns
-        self.assertEqual(self.module.columnCount(), 3)
-
-        # Assert cell content in first column is correct
-        self.assertEqual(self.module.item(1, 0).text(), ROW_IDENTIFIERS[0])
-        self.assertEqual(self.module.item(2, 0).text(), ROW_IDENTIFIERS[1])
-
-        # Assert cell content in second column is correct
-        self.assertEqual(self.module.item(1, 1).text(), "0")
-        self.assertEqual(self.module.item(2, 1).text(), "1")
-
-        # Assert cell content in third column is correct
-        code = self.module.item(1, 2).text()
-        self.assertTrue(len(code) >= CODE_LENGTH)
-        # Assert all code characters are taken from list code_characters
-        for _, character in enumerate(code):
-            if character != " ":
-                self.assertTrue(character in self.module.code_characters)
-        # Assert all codes are unique
-        code_list = self.module.get_codes()
-        code_set = set(code_list)
-        self.assertEqual(len(code_list), len(code_set))
-
-    def test_generate_unique_authentication_code(self):
-        """Test function generate_unique_authentication_module."""
-        code_list = self.module.get_codes()
-        generated_code = self.module.generate_unique_authentication_code()
-
-        # Assert code length is equal to an existing code
-        self.assertEqual(len(generated_code), len(code_list[0]))
-        # Assert generated code is not equal to any existing codes
-        self.assertFalse(generated_code in code_list)
-
-    def test_get_codes(self):
-        """Test function get_codes."""
-        # Get codes
-        code_list = self.module.get_codes()
-
-        # Assert codes are correct
-        for i, code in enumerate(code_list):
-            self.assertEqual(code, self.module.item(i + 1, 2).text())
-
-    def test_gui_add_row(self):
-        """Test adding rows with shortcuts."""
-        # Widget must be shown for shortcuts to work
-        self.module.show()
-        QTest.qWaitForWindowExposed(self.module)
-
-        # From experience it can happen that the widget is not fully
-        # initialized before the QTest.keyClicks below. By calling
-        # processEvents here we give Qt the opportunity to catch up with us
-        app.processEvents()
-
-        old_row_count = self.module.rowCount()
-
-        # Use shortcut 'Ctrl + +'
-        QTest.keyClicks(self.module, "+", Qt.ControlModifier)
-
-        # Assert a new row is added
-        new_row_count = self.module.rowCount()
-        self.assertEqual(old_row_count + 1, new_row_count)
-
-        # Assert new row has correct content
-        row_index = new_row_count - 1
-        self.assertEqual(
-            self.module.item(row_index, 0).text(),
-            ROW_IDENTIFIERS[row_index - 1],
-        )
-        self.assertEqual(
-            self.module.item(row_index, 1).text(), str(row_index - 1)
-        )
-        new_code = self.module.item(row_index, 2).text()
-        existing_code = self.module.item(1, 2).text()
-        self.assertEqual(len(new_code), len(existing_code))
-
-    def test_gui_remove_row(self):
-        """Test removing rows with shortcuts."""
-        # Widget must be shown for shortcuts to work
-        self.module.show()
-        QTest.qWaitForWindowExposed(self.module)
-
-        # From experience it can happen that the widget is not fully
-        # initialized before the QTest.keyClicks below. By calling
-        # processEvents here we give Qt the opportunity to catch up with us
-        app.processEvents()
-
-        old_row_count = self.module.rowCount()
-
-        # First row is selected on startup, it contains headline
-        # and user should not be able to delete it with shortcut
-        QTest.keyClicks(self.module, "_", Qt.ControlModifier)
-        # Assert row was not removed
-        new_row_count = self.module.rowCount()
-        self.assertEqual(old_row_count, new_row_count)
-
-        # Move to first row, then use shortcut to delete it
-        QTest.keyClick(self.module, Qt.Key_Down)
-        QTest.keyClicks(self.module, "_", Qt.ControlModifier)
-        # Assert row was removed and replaced by the row below
-        value_to_delete = self.module.item(1, 1).text()
-        new_row_count = self.module.rowCount()
-        self.assertEqual(old_row_count - 1, new_row_count)
-        self.assertEqual(self.module.item(1, 1).text(), value_to_delete)
-
-        # Remove rows until only headline-row and a single code-row exist
-        for _ in range(1, self.module.rowCount() - 1):
-            QTest.keyClick(self.module, Qt.Key_Down)
-            QTest.keyClicks(self.module, "_", Qt.ControlModifier)
-        self.assertTrue(self.module.rowCount() == 2)
-        # Try to remove final code-row, should not work
-        QTest.keyClick(self.module, Qt.Key_Down)
-        QTest.keyClicks(self.module, "_", Qt.ControlModifier)
-        # Assert row was not removed
-        self.assertTrue(self.module.rowCount() == 2)
-
-    def test_add_to_soi_smoke_test(self):
-        """Test that module can be added to SOI."""
-        soi = SOI()
-        module_name = "Test name"
-        soi.add_module(module_name, self.module)
-        self.assertTrue(soi.module_name_taken(module_name))
-
-
-class TestAuthenticationBoardModuleFromData(unittest.TestCase):
-    """TestCase for initializing AuthenticationBoardModule from data."""
-
-    def test_create_from_data(self):
-        """Test creating AuthenticationBoardModule from data."""
-        test_data = [
-            "Headline text",
-            ["A", "0", "TEST CODE ONE"],
-            ["B", "1", "TEST CODE TWO"],
-        ]
-        test_size = {"width": 100, "height": 100}
-
-        module = AuthenticationBoardModule(size=test_size, data=test_data)
-
-        # Assert module contains expected data
-        self.assertEqual(module.item(0, 0).text(), "Headline text")
-        self.assertEqual(module.item(1, 0).text(), "A")
-        self.assertEqual(module.item(1, 1).text(), "0")
-        self.assertEqual(module.item(1, 2).text(), "TEST CODE ONE")
-        self.assertEqual(module.item(2, 0).text(), "B")
-        self.assertEqual(module.item(2, 1).text(), "1")
-        self.assertEqual(module.item(2, 2).text(), "TEST CODE TWO")