diff --git a/soitool/database.py b/soitool/database.py
index 9a1c019274e6e1e8e499fc2b4e30b2256664fd6d..95d29151b0770ebcf508d016b6b5e8c8a3845d43 100644
--- a/soitool/database.py
+++ b/soitool/database.py
@@ -162,14 +162,50 @@ class Database:
 
         Returns
         -------
-        List of strings
-            Categories
+        List
+            Containing categories (string).
         """
         stmt = "SELECT DISTINCT Category FROM CategoryWords"
         queried = self.conn.execute(stmt)
-        categories = []
-        for row in queried:
-            categories.append(row["Category"])
+        categories = [row["Category"] for row in queried]
+
+        return categories
+
+    def get_random_category_word(self):
+        """Read a random word from database-table CategoryWords.
+
+        Returns
+        -------
+        String
+            Word (string).
+        """
+        stmt = "SELECT Word FROM CategoryWords ORDER BY RANDOM() LIMIT 1"
+        word = self.conn.execute(stmt).fetchone()[0]
+
+        return word
+
+    def get_categories_from_codebook(self, small=False):
+        """Read categories from full or small codebook.
+
+        Parameters
+        ----------
+        small : bool, optional
+            Categories are from small codebook if True, full codebook if
+            False, by default False.
+
+        Returns
+        -------
+        List
+            Containing categories (string).
+        """
+        stmt = "SELECT Category FROM Codebook"
+        if small:
+            stmt += " WHERE Type='Liten' GROUP BY Category"
+        else:
+            stmt += " GROUP BY Category"
+
+        queried = self.conn.execute(stmt).fetchall()
+        categories = [row["Category"] for row in queried]
 
         return categories
 
@@ -226,6 +262,31 @@ class Database:
 
         return codebook
 
+    def get_codebook_expressions_in_category(self, category, small=False):
+        """Read expressions, from full or small codebook, in a given category.
+
+        Parameters
+        ----------
+        category : string
+            Expressions in this category are returned.
+        small : bool, optional
+            Expressions are from small codebook if True, full codebook if
+            False, by default False.
+
+        Returns
+        -------
+        List
+            Containing expressions (string).
+        """
+        stmt = "SELECT Word FROM Codebook WHERE Category=?"
+        if small:
+            stmt += " AND Type='Liten'"
+        queried = self.conn.execute(stmt, (category,)).fetchall()
+
+        expressions = [row["Word"] for row in queried]
+
+        return expressions
+
     def update_codebook(self):
         """Update codes in DB."""
         # Get all the words (PK)
diff --git a/soitool/help_actions.py b/soitool/help_actions.py
index 0d99369a6082c279ce8a5f4caa29ecef07c48739..7c30dba840d5279899f202b6e5b3e9ffd0d9e8eb 100644
--- a/soitool/help_actions.py
+++ b/soitool/help_actions.py
@@ -11,6 +11,7 @@ from PySide2.QtWidgets import (
     QFormLayout,
 )
 from PySide2.QtCore import Qt
+from PySide2.QtGui import QFont
 
 
 class ShortcutsHelpDialog(QDialog):
@@ -32,6 +33,15 @@ class ShortcutsHelpDialog(QDialog):
         self.button_ok = QPushButton("Ok")
         self.button_ok.clicked.connect(self.close)
 
+        self.header_main = QLabel("Hurtigtaster for applikasjonen:")
+        self.header_main.setFont(QFont("Verdana", 10))
+        self.header_table = QLabel("Hurtigtaster for redigerbare tabeller:")
+        self.header_table.setFont(QFont("Verdana", 10))
+        self.header_modules = QLabel("Hurtigtaster for noen gitte moduler:")
+        self.header_modules.setFont(QFont("Verdana", 10))
+
+        # Main/program shortcuts
+        self.layout_label.addRow(self.header_main)
         self.layout_label.addRow(
             QLabel("Opprett en ny SOI: "), QLabel("Ctrl + N")
         )
@@ -41,18 +51,41 @@ class ShortcutsHelpDialog(QDialog):
         self.layout_label.addRow(
             QLabel("Ã…pne SOI fra database: "), QLabel("Ctrl + D")
         )
+        self.layout_label.addRow(
+            QLabel("Endre oppsett SOI: "), QLabel("Ctrl + I")
+        )
+        self.layout_label.addRow(
+            QLabel("Legg til ny modul: "), QLabel("Ctrl + M")
+        )
         self.layout_label.addRow(QLabel("Eksporter PDF: "), QLabel("Ctrl + P"))
+        self.layout_label.addRow(
+            QLabel("Eksporter komprimert SOI: "), QLabel("Ctrl + E")
+        )
         self.layout_label.addRow(
             QLabel("Lagre i database: "), QLabel("Ctrl + S")
         )
+
+        # Table shortcuts
+        self.layout_label.addRow(QLabel())
+        self.layout_label.addRow(self.header_table)
+        self.layout_label.addRow(QLabel("Legg til rad"), QLabel("Ctrl + +"))
+        self.layout_label.addRow(QLabel("Fjern rad"), QLabel("Ctrl + -"))
         self.layout_label.addRow(
-            QLabel("Eksporter komprimert SOI: "), QLabel("Ctrl + E")
+            QLabel("Legg til kolonne"), QLabel("Shift + +")
+        )
+        self.layout_label.addRow(QLabel("Fjern kolonne"), QLabel("Shift + -"))
+
+        # Other module-specific shortcuts
+        self.layout_label.addRow(QLabel())
+        self.layout_label.addRow(self.header_modules)
+        self.layout_label.addRow(
+            QLabel("Rediger forhåndsavtalte koder: "), QLabel("Ctrl + R")
         )
         self.layout_label.addRow(
-            QLabel("Legg til ny modul: "), QLabel("Ctrl + M")
+            QLabel("Rediger telefonliste: "), QLabel("Ctrl + R")
         )
         self.layout_label.addRow(
-            QLabel("Endre oppsett SOI: "), QLabel("Ctrl + I")
+            QLabel("Rediger frekvenstabell: "), QLabel("Ctrl + R")
         )
 
         self.layout_button.addWidget(self.button_ok, alignment=Qt.AlignRight)
diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py
index 5bde3a8ba14af685065fa8200d96e2148e36651a..8690ad30f8010d51768593fcbe512b337781f493 100644
--- a/soitool/inline_editable_soi_view.py
+++ b/soitool/inline_editable_soi_view.py
@@ -12,7 +12,6 @@ from PySide2.QtWidgets import (
     QGraphicsProxyWidget,
 )
 from PySide2.QtGui import (
-    QFont,
     QPixmap,
     QBrush,
     QPalette,
@@ -22,6 +21,7 @@ from PySide2.QtPrintSupport import QPrinter
 from soitool.soi import ModuleLargerThanBinError
 from soitool.dialog_wrappers import exec_warning_dialog
 from soitool.serialize_export_import_soi import generate_soi_filename
+from soitool.modules.module_base import qfont_with_pixel_size
 
 # How attachment pages should be numbered. The first page should be numbered
 # by the value of ATTACHMENT_NUMBERING_SCHEME[0], the second page
@@ -419,7 +419,7 @@ class InlineEditableSOIView(QScrollArea):
         label_title = QLabel(self.soi.title)
         label_title.move(x, y + self.soi.HEADER_HEIGHT)
         label_title.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        label_title.setFont(QFont("Times New Roman", 35))
+        label_title.setFont(qfont_with_pixel_size("Times New Roman", 60))
         proxy = self.scene.addWidget(label_title)
         proxy.setRotation(-90)
 
@@ -427,7 +427,7 @@ class InlineEditableSOIView(QScrollArea):
         label_description = QLabel(self.soi.description)
         label_description.move(x + 57, y + self.soi.HEADER_HEIGHT)
         label_description.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        label_description.setFont(QFont("Times New Roman", 15))
+        label_description.setFont(qfont_with_pixel_size("Times New Roman", 25))
         proxy = self.scene.addWidget(label_description)
         proxy.setRotation(-90)
 
@@ -435,7 +435,9 @@ class InlineEditableSOIView(QScrollArea):
         creation_date = soi_date_string_to_user_friendly_string(self.soi.date)
         label_creation_date = QLabel("Opprettet: {}".format(creation_date))
         label_creation_date.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        label_creation_date.setFont(QFont("Times New Roman", 15))
+        label_creation_date.setFont(
+            qfont_with_pixel_size("Times New Roman", 25)
+        )
         # Source: https://stackoverflow.com/a/8638114/3545896
         # CAUTION: does not work if font is set through stylesheet
         label_width = (
@@ -470,7 +472,7 @@ class InlineEditableSOIView(QScrollArea):
             "1 av N", f"{self.copy_current} av {self.copy_total}"
         )
         proxy.label.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        proxy.label.setFont(QFont("Times New Roman", 40))
+        proxy.label.setFont(qfont_with_pixel_size("Times New Roman", 70))
 
         # Need to call this when label of proxy changes. See function docstring
         proxy.update_bounding_rect()
@@ -493,7 +495,9 @@ class InlineEditableSOIView(QScrollArea):
         label_copy_number_header.setStyleSheet(
             "background-color: rgba(0,0,0,0%)"
         )
-        label_copy_number_header.setFont(QFont("Times New Roman", 15))
+        label_copy_number_header.setFont(
+            qfont_with_pixel_size("Times New Roman", 25)
+        )
         # Source: https://stackoverflow.com/a/8638114/3545896
         # CAUTION: does not work if font is set through stylesheet
         label_width = (
@@ -519,7 +523,7 @@ class InlineEditableSOIView(QScrollArea):
                 )
             )
         page_number.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        page_number.setFont(QFont("Times New Roman", 15))
+        page_number.setFont(qfont_with_pixel_size("Times New Roman", 25))
         # Source: https://stackoverflow.com/a/8638114/3545896
         # CAUTION: does not work if font is set through stylesheet
         label_width = (
@@ -534,7 +538,7 @@ class InlineEditableSOIView(QScrollArea):
         classification.setStyleSheet(
             "background-color: rgba(0,0,0,0%); " "color: red"
         )
-        classification.setFont(QFont("Times New Roman", 35))
+        classification.setFont(qfont_with_pixel_size("Times New Roman", 60))
         # Source: https://stackoverflow.com/a/8638114/3545896
         # CAUTION: does not work if font is set through stylesheet
         label_width = (
@@ -552,7 +556,7 @@ class InlineEditableSOIView(QScrollArea):
         )
         label_valid_from = QLabel("Gyldig fra: {}".format(valid_from))
         label_valid_from.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        label_valid_from.setFont(QFont("Times New Roman", 15))
+        label_valid_from.setFont(qfont_with_pixel_size("Times New Roman", 25))
         # Source: https://stackoverflow.com/a/8638114/3545896
         # CAUTION: does not work if font is set through stylesheet
         label_width = (
@@ -568,7 +572,7 @@ class InlineEditableSOIView(QScrollArea):
         valid_to = soi_date_string_to_user_friendly_string(self.soi.valid_to)
         label_valid_to = QLabel("Gyldig til: {}".format(valid_to))
         label_valid_to.setStyleSheet("background-color: rgba(0,0,0,0%)")
-        label_valid_to.setFont(QFont("Times New Roman", 15))
+        label_valid_to.setFont(qfont_with_pixel_size("Times New Roman", 25))
         # Source: https://stackoverflow.com/a/8638114/3545896
         # CAUTION: does not work if font is set through stylesheet
         label_width = (
diff --git a/soitool/main_window.py b/soitool/main_window.py
index 08190ec2bbbccbe577f84c048ee55469dda404d1..5dc81632c957243ee2a1fc6e6560a7ca1d049e92 100644
--- a/soitool/main_window.py
+++ b/soitool/main_window.py
@@ -227,7 +227,7 @@ class MainWindow(QMainWindow):
 
     def open_soi_workspace_tab(self):
         """Open and select tab containing a SOIWorkspaceWidget."""
-        tab = SOIWorkspaceWidget()
+        tab = SOIWorkspaceWidget(self.database)
         self.tabs.addTab(tab, tab.soi.title)
         self.tabs.setCurrentWidget(tab)
 
@@ -315,10 +315,10 @@ class MainWindow(QMainWindow):
         )[0]
 
         if len(file_path) > 0:
-            soi = import_soi(file_path)
+            soi = import_soi(file_path, self.database)
 
             # Create and select tab
-            tab = SOIWorkspaceWidget(soi)
+            tab = SOIWorkspaceWidget(self.database, soi)
             self.tabs.addTab(tab, soi.title)
             self.tabs.setCurrentWidget(tab)
 
diff --git a/soitool/media/predefinedcodesmodule.PNG b/soitool/media/predefinedcodesmodule.PNG
new file mode 100644
index 0000000000000000000000000000000000000000..dcf0cc1bbb7dd9924e27ef5b713069ed8c6d6170
Binary files /dev/null and b/soitool/media/predefinedcodesmodule.PNG differ
diff --git a/soitool/modules/code_table_base.py b/soitool/modules/code_table_base.py
index 9cdff1ed3d297456eb864864791c2310d442eb7d..7028a3d6ac7f04e2cca30160aff2abbcc3538847 100644
--- a/soitool/modules/code_table_base.py
+++ b/soitool/modules/code_table_base.py
@@ -11,6 +11,7 @@ from soitool.modules.module_base import (
     get_table_size,
     resize_table,
     HEADLINE_FONT,
+    TABLE_CELL_DEFAULT_FONT,
 )
 
 AUTHENTICATIONBOARD_MODULE = "AuthenticationBoardModule"
@@ -37,6 +38,7 @@ class CodeTableBase(ModuleBase, QTableWidget, metaclass=Meta):
         self.verticalHeader().hide()
         self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
         self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.setFont(TABLE_CELL_DEFAULT_FONT)
 
         # Resize table when headline changes
         self.cellChanged.connect(
@@ -92,11 +94,10 @@ class CodeTableBase(ModuleBase, QTableWidget, metaclass=Meta):
             # Set cell-items
             for i in range(self.rowCount()):
                 for j in range(self.columnCount()):
-                    item = QTableWidgetItem(
-                        cells[i + 1][j]
-                    )  # +1 skip headline
+                    # + 1 skip headline
+                    item = QTableWidgetItem(cells[i + 1][j])
+                    item.setTextAlignment(Qt.AlignCenter)
                     if j == 2:
-                        item.setTextAlignment(Qt.AlignCenter)
                         item.setFont(self.code_font)
                     self.setItem(i, j, item)
 
diff --git a/soitool/modules/module_authentication_board.py b/soitool/modules/module_authentication_board.py
index ca46c80dcfac75cb449104380700fe62e8d24916..06ba84f67f0fee4b0a46b072c50bde45af22b0ea 100644
--- a/soitool/modules/module_authentication_board.py
+++ b/soitool/modules/module_authentication_board.py
@@ -4,7 +4,11 @@ from secrets import choice
 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.module_base import (
+    resize_table,
+    qfont_with_pixel_size,
+    TABLE_CELL_DEFAULT_FONT,
+)
 from soitool.modules.code_table_base import CodeTableBase
 
 # Characters for first column,
@@ -21,7 +25,9 @@ CODE_LENGTH = 25
 CODE_CHARACTER_TYPE = "ascii"
 
 # Font for authentication codes, should be a monospaced font
-CODE_FONT = QtGui.QFont("Consolas", 10, QtGui.QFont.SansSerif)
+CODE_FONT = qfont_with_pixel_size(
+    "Consolas", TABLE_CELL_DEFAULT_FONT.pixelSize(), QtGui.QFont.SansSerif
+)
 
 # Adds space between sets of characters, 0 => no spaces
 # If code is 123456 and interval is 2, code will be 12 34 56
diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py
index 1d6f055d20dd0a418317e7a77d3e140540159b2d..91d97dbcbade3b85432fbaed1415a3a394663b1d 100644
--- a/soitool/modules/module_base.py
+++ b/soitool/modules/module_base.py
@@ -2,11 +2,34 @@
 from abc import ABC
 from PySide2 import QtGui
 
+
+def qfont_with_pixel_size(font_family, pixel_size, weight=None):
+    """Provide a QFont with given family and pixel size.
+
+    Created because QFont does not have a constructor with pixel size as a
+    parameter.
+
+    Parameters
+    ----------
+    font_family : str
+        Name of font family. Sent to https://doc.qt.io/qt-5/qfont.html#QFont-1
+    pixel_size : int
+        Pixel size. Sent to https://doc.qt.io/qt-5/qfont.html#setPixelSize
+    weight : QFont.Weight
+        Weight of font. Sent to https://doc.qt.io/qt-5/qfont.html#QFont-1
+    """
+    if weight is not None:
+        font = QtGui.QFont(font_family, weight=weight)
+    else:
+        font = QtGui.QFont(font_family)
+    font.setPixelSize(pixel_size)
+    return font
+
+
 # Font for module headline
-HEADLINE_FONT = QtGui.QFont()
-HEADLINE_FONT.setFamily("Arial")
-HEADLINE_FONT.setPointSize(12)
-HEADLINE_FONT.setWeight(100)
+HEADLINE_FONT = qfont_with_pixel_size("Arial", 24, 100)
+
+TABLE_CELL_DEFAULT_FONT = qfont_with_pixel_size("Arial", 16)
 
 
 class ModuleBase(ABC):
diff --git a/soitool/modules/module_freetext.py b/soitool/modules/module_freetext.py
index 95066bceea66e73d73013f3b930fecd92ef1484d..14ede836bcd46425bc8d10a1a198121b566759af 100644
--- a/soitool/modules/module_freetext.py
+++ b/soitool/modules/module_freetext.py
@@ -13,7 +13,11 @@ from PySide2.QtWidgets import (
 )
 from PySide2.QtCore import QSize, Qt
 from PySide2.QtGui import QIcon, QFontMetricsF
-from soitool.modules.module_base import ModuleBase, HEADLINE_FONT
+from soitool.modules.module_base import (
+    ModuleBase,
+    HEADLINE_FONT,
+    qfont_with_pixel_size,
+)
 
 
 class TextEditWithSizeOfContent(QTextEdit):
@@ -113,6 +117,9 @@ class FreeTextModule(ModuleBase, QWidget, metaclass=Meta):
         self.line_edit_header = LineEditWithSizeOfContent()
         self.line_edit_header.setFont(HEADLINE_FONT)
         self.text_edit_body = TextEditWithSizeOfContent()
+        self.text_edit_body.setFont(
+            qfont_with_pixel_size("Arial", HEADLINE_FONT.pixelSize())
+        )
 
         # When the contents of these widgets change we need to manually trigger
         # adjust of size, even on self. Without adjust of size on self the
diff --git a/soitool/modules/module_predefined_codes.py b/soitool/modules/module_predefined_codes.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c9ce9b4bc7b1ae6d9e4f89a3992a3261a11ccd2
--- /dev/null
+++ b/soitool/modules/module_predefined_codes.py
@@ -0,0 +1,629 @@
+"""SOI-module 'Forhåndsavtalte koder'."""
+import string
+from random import sample
+from PySide2.QtWidgets import (
+    QWidget,
+    QTableWidget,
+    QTableWidgetItem,
+    QHBoxLayout,
+    QVBoxLayout,
+    QLabel,
+    QLineEdit,
+    QDialog,
+    QListWidget,
+    QListWidgetItem,
+    QAbstractItemView,
+    QFormLayout,
+    QSpinBox,
+)
+from PySide2 import QtGui
+from PySide2.QtCore import Qt
+from soitool.modules.module_base import ModuleBase, HEADLINE_FONT, resize_table
+from soitool.accept_reject_dialog import AcceptRejectDialog
+
+ALPHABET = string.ascii_uppercase
+HEADLINE = "FORHÃ…NDSAVTALTE KODER"
+MAXIMUM_COLUMN_HEIGHT = 1000
+DEFAULT_COLUMN_HEIGHT = 200
+
+
+class Meta(type(ModuleBase), type(QWidget)):
+    """Used as a metaclass to enable multiple inheritance."""
+
+
+class PredefinedCodesModule(ModuleBase, QWidget, metaclass=Meta):
+    """QWidget representing SOI-module 'Forhåndsavtalte koder'.
+
+    This widget has a headline, a warning-word and a layout with x amount of
+    PredefinedCodesTable-objects containing all expressions and categories from
+    small codebook ("Type" = "Liten" in database-table "Codebook").
+
+    If parameter "data" is not given, the widget reads all expressions and
+    categories from small codebook, and a warning-word is randomly chosen from
+    database-table "CategoryWords". It then launches a dialog where the user
+    can set up the module - headline, warning_word, maximum height and category
+    order.
+    If parameter "data" is given, the widget is built based on the contents.
+
+    Finally, it creates one PredefinedCodesTable per category and inserts all
+    expressions in the category.
+
+    The shortcut "Ctrl + R" launches the setup-dialog so that the user can
+    modify and update the module. If changes have been made to small codebook,
+    the module will reflect the changes, and it will also randomly sort all
+    expressions in each category (so that a new code is assigned to each one).
+
+    The PredefinedCodesTables are placed in one or more QVBoxLayouts, which are
+    placed next to each other. The QVBoxLayouts are therefore referred to as
+    columns. The user can decide (through the dialog PredefinedCodesSettings)
+    the maximum column-height. Each time there is not enough room for all
+    tables in a column within the maximum height, a new column is created.
+    The class has an attribute 'minimum_column_height', which reflects the
+    height of the tallest table. The user is not allowed to set a maximum
+    column-height smaller than the minimum_column_height, because one or more
+    tables would not fit.
+
+    The widget does not use more room than needed, and resizes dynamically.
+
+    Parameters
+    ----------
+    database : soitool.database.Database
+        Database-instance used to read from database.
+    data : dict, optional
+        {
+            "headline": string,
+            "warning_word": string,
+            "maximum_column_height": int,
+            "categories": [strings],
+            "tables": [{
+                "table_headline": string,
+                "expressions": [strings]
+            ]}
+        }
+        By default None.
+    """
+
+    def __init__(self, database, data=None):
+        self.type = "PredefinedCodesModule"
+        QWidget.__init__(self)
+        ModuleBase.__init__(self)
+
+        self.database = database
+        self.tables = []
+        self.categories = []
+
+        if data is not None:
+            self.headline = QLabel(data["headline"])
+            self.warning_word = QLabel(data["warning_word"])
+            self.maximum_column_height = data["maximum_column_height"]
+            self.categories = data["categories"]
+            self.create_tables_from_data(data)
+            self.create_and_set_layout()
+        else:
+            # Get a random warning-word and categories from database
+            warning_word = self.database.get_random_category_word()
+            categories_unsorted = self.database.get_categories_from_codebook(
+                small=True
+            )
+            # Set up module
+            self.run_setup(warning_word, categories_unsorted)
+
+    def run_setup(self, warning_word, categories):
+        """Launch setup-dialog, read input, create tables and set layout."""
+        # Calculate height of the tallest table
+        self.minimum_column_height = self.calculate_height_of_tallest_table(
+            categories
+        )
+        # Launch dialog
+        dialog = PredefinedCodesSettings(HEADLINE, warning_word, categories)
+        dialog.edit_column_height.setMinimum(self.minimum_column_height)
+        dialog.exec_()
+
+        # Read dialog-input
+        categories_and_expressions = self.read_from_dialog(dialog)
+
+        # Create tables and set layout
+        self.create_tables(categories_and_expressions)
+        self.create_and_set_layout()
+
+    def read_from_dialog(self, dialog):
+        """Read input from dialog PredefinedCodesSettings.
+
+        Parameters
+        ----------
+        dialog : PredefinedCodesSettings
+            Dialog to read from.
+
+        Returns
+        -------
+        dict
+            With categories as keys and list of expressions as values.
+        """
+        # Read headline and warning word,
+        # create QLabel or set text on existing QLabel
+        if hasattr(self, "headline"):
+            self.headline.setText(dialog.edit_headline.text())
+        else:
+            self.headline = QLabel(dialog.edit_headline.text())
+        if hasattr(self, "warning_word"):
+            self.warning_word.setText(dialog.edit_warning_word.text())
+        else:
+            self.warning_word = QLabel(dialog.edit_warning_word.text())
+
+        self.maximum_column_height = dialog.edit_column_height.value()
+
+        # Read categories in order
+        self.categories.clear()
+        for i in range(dialog.list_category_order.count()):
+            self.categories.append(dialog.list_category_order.item(i).text())
+
+        # Create dict containing categories and their expressions
+        categories_and_expressions = {}
+        for category in self.categories:
+            expressions = self.database.get_codebook_expressions_in_category(
+                category, small=True
+            )
+            # Add expressions sorted randomly
+            categories_and_expressions[category] = sample(
+                expressions, len(expressions)
+            )
+
+        return categories_and_expressions
+
+    def create_tables(self, categories_and_expressions):
+        """Create PredefinedCodesTable-objects.
+
+        Parameters
+        ----------
+        categories_and_expressions : dict
+            With categories as keys and list of expressions as values.
+        """
+        # Delete previous tables
+        del self.tables[:]
+
+        # Create new tables
+        for i, category in enumerate(categories_and_expressions.keys()):
+            headline = " " + ALPHABET[i] + " " + category
+            table = PredefinedCodesTable(
+                headline, categories_and_expressions[category]
+            )
+            self.tables.append(table)
+
+    def create_tables_from_data(self, data):
+        """Create PredefinedCodesTable-objects from data.
+
+        Parameters
+        ----------
+        Dict
+            Contains data needed, see module description.
+        """
+        for table_data in data["tables"]:
+            table_headline = table_data["table_headline"]
+            expressions = table_data["expressions"]
+            table = PredefinedCodesTable(table_headline, expressions)
+            self.tables.append(table)
+
+        self.minimum_column_height = self.get_height_of_tallest_table()
+
+    def create_and_set_layout(self):
+        """Create, fill and set layout."""
+        self.headline.setFont(HEADLINE_FONT)
+        self.headline.setAlignment(Qt.AlignCenter)
+        self.warning_word.setFont(HEADLINE_FONT)
+        self.warning_word.setAlignment(Qt.AlignCenter)
+
+        # Layout for tables
+        table_layout = self.create_table_layout()
+
+        # Layout for warning_word
+        warning_word_layout = QHBoxLayout()
+        warning_word_label = QLabel("Varslingsord: ")
+        warning_word_layout.addWidget(warning_word_label)
+        warning_word_layout.addWidget(self.warning_word)
+        warning_word_layout.setAlignment(warning_word_label, Qt.AlignHCenter)
+        warning_word_layout.setAlignment(self.warning_word, Qt.AlignHCenter)
+
+        # Main layout
+        self.main_layout = QVBoxLayout()
+        self.main_layout.addWidget(self.headline)
+        self.main_layout.setAlignment(self.headline, Qt.AlignHCenter)
+        self.main_layout.addLayout(warning_word_layout)
+        self.main_layout.setAlignment(warning_word_layout, Qt.AlignHCenter)
+        self.main_layout.addLayout(table_layout)
+
+        # Set layout and adjust size
+        self.setLayout(self.main_layout)
+        self.adjustSize()
+
+    def create_table_layout(self):
+        """Create and return a table-layout.
+
+        Returns
+        -------
+        QHBoxLayout
+            Layout containing x amount of QVBoxLayouts with tables.
+        """
+        # Create table-layout
+        table_layout = QHBoxLayout()
+
+        # Algorithm-explanation: Create a "column" and fill it with tables
+        # until adding the next table will overshoot maximum_column_height,
+        # then add a new column and repeat the process until all tables are
+        # added.
+        i = 0
+        column_height = 0
+        # While all tables are not added
+        while i < len(self.tables):
+            # Create a "column"
+            vbox_layout = QVBoxLayout()
+            max_table_width = 0
+            # While there are more tables and there is room for the next table,
+            # add table to column and save it's width if it is the widest table
+            while (
+                i < len(self.tables)
+                and column_height + self.tables[i].height()
+                <= self.maximum_column_height
+            ):
+                table = self.tables[i]
+                if table.width() > max_table_width:
+                    max_table_width = table.width()
+                vbox_layout.addWidget(table)
+                # Increase column-height
+                column_height += table.height()
+                i += 1
+
+            # Column does not have room to fit the next table.
+            # Make all tables in column have equal width.
+            for j in range(vbox_layout.count()):
+                widget = vbox_layout.itemAt(j).widget()
+                column_one_width = widget.columnWidth(0)
+                widget.setFixedWidth(max_table_width)
+                widget.setColumnWidth(1, max_table_width - column_one_width)
+
+            # Add a QSpacerItem so that tables in column are pushed to the top
+            vbox_layout.addStretch(1)
+
+            # Add column to the table_layout and reset column-height.
+            table_layout.addLayout(vbox_layout)
+            column_height = 0
+
+        return table_layout
+
+    def new_table_layout(self):
+        """Replace current table-layout with a new one."""
+        # Get current layout and remove it from main-layout.
+        table_layout = self.main_layout.itemAt(2)
+        self.main_layout.removeItem(table_layout)
+
+        # Loop through table-layout's columns (QVBoxLayouts) and delete them
+        # along with their widgets to make them disappear.
+        columns = [table_layout.itemAt(i) for i in range(table_layout.count())]
+        for column in columns:
+            tables = [column.itemAt(i).widget() for i in range(column.count())]
+            for table in tables:
+                if table is not None:
+                    table.close()
+                    del table
+            del column
+        del table_layout
+
+        # Create new table-layout
+        table_layout = self.create_table_layout()
+
+        # Add table-layout and adjust own size
+        self.main_layout.addLayout(table_layout)
+        self.adjustSize()
+
+    def get_size(self):
+        """Return size of table.
+
+        Returns
+        -------
+        Tuple
+            width, height.
+        """
+        return self.width(), self.height()
+
+    def get_data(self):
+        """Return a dict containing all module-data.
+
+        Returns
+        -------
+        Dict
+            {
+                "headline": string,
+                "warning_word": string,
+                "maximum_column_height": int,
+                "categories": [strings],
+                "tables": [{
+                    "table_headline": string,
+                    "expressions": [strings]
+                ]}
+            }
+        """
+        tables = []
+        # Create a dict for each table and them to tables-list
+        for table in self.tables:
+            # Table-headline
+            item_headline = table.item(0, 0)
+            if item_headline is not None:
+                table_headline = item_headline.text()
+            else:
+                table_headline = ""
+            # Table-expressions
+            expressions = []
+            for i in range(1, table.rowCount()):
+                expressions.append(table.item(i, 1).text())
+            # Create dict with table-data and add it to list
+            table = {
+                "table_headline": table_headline,
+                "expressions": expressions,
+            }
+            tables.append(table)
+
+        # Create main dict and add tables-list
+        data = {
+            "headline": self.headline.text(),
+            "warning_word": self.warning_word.text(),
+            "maximum_column_height": self.maximum_column_height,
+            "categories": self.categories,
+            "tables": tables,
+        }
+
+        return data
+
+    def get_height_of_tallest_table(self):
+        """Get height of the tallest table.
+
+        Returns
+        -------
+        int
+            Height of the tallest table.
+        """
+        tallest_height = 0
+        for table in self.tables:
+            if table.height() > tallest_height:
+                tallest_height = table.height()
+
+        return tallest_height
+
+    def calculate_height_of_tallest_table(self, categories):
+        """Find what table will be tallest and return it's height.
+
+        Parameters
+        ----------
+        categories : List
+            Containing categories, is used to retrieve expressions in each
+            category.
+
+        Returns
+        -------
+        int
+            Height of the tallest table.
+        """
+        # Get expressions in each category, while noting the index of the
+        # category with the most expressions
+        all_expressions = []
+        max_length = 0
+        max_index = None
+        for i, category in enumerate(categories):
+            expressions = self.database.get_codebook_expressions_in_category(
+                category, small=True
+            )
+            all_expressions.append(expressions)
+            if len(expressions) > max_length:
+                max_index = i
+                max_length = len(expressions)
+
+        # Create the tallest table and get it's height
+        tallest_table = PredefinedCodesTable("", all_expressions[max_index])
+        tallest_height = tallest_table.height()
+
+        return tallest_height
+
+    def keyPressEvent(self, event):
+        """Launch PredefinedCodesSettings-dialog when "Ctrl + R" is pressed."""
+        if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R:
+            dialog = PredefinedCodesSettings(
+                self.headline.text(), self.warning_word.text(), self.categories
+            )
+
+            # Modify dialog
+            dialog.edit_column_height.setMinimum(self.minimum_column_height)
+            dialog.edit_column_height.setValue(self.maximum_column_height)
+            dialog.button_ok.setText("Oppdater")
+            dialog.button_cancel.show()
+
+            dialog_code = dialog.exec_()
+            # If user accepted, read dialog, create tables and set a
+            # new table-layout
+            if dialog_code == QDialog.DialogCode.Accepted:
+                categories_and_expressions = self.read_from_dialog(dialog)
+
+                self.create_tables(categories_and_expressions)
+                self.new_table_layout()
+
+        super().keyPressEvent(event)
+
+    @staticmethod
+    def get_user_friendly_name():
+        """Get user-friendly name of module."""
+        return "Forhåndsavtalte koder"
+
+    @staticmethod
+    def get_icon():
+        """Get icon of module."""
+        return QtGui.QIcon("soitool/media/predefinedcodesmodule.png")
+
+
+class PredefinedCodesTable(QTableWidget):
+    """Modified QTableWidget displaying predefined-codes in a category.
+
+    This table has a headline and two columns. The headline should consist of
+    a letter followed by a category. Each row contains a unique letter and an
+    expression. Practically speaking, the letter from the headline + the letter
+    from the row is used as a code for the expression on that row.
+
+    Parameters
+    ----------
+    headline : string
+        Will be the headline of the table, should be a letter
+        followed by a category.
+    expressions : list
+        Containing expressions (string).
+    """
+
+    def __init__(self, headline, expressions):
+        QTableWidget.__init__(self)
+
+        # Set focus-policy to prevent PredefinedCodesModule's
+        # keyPressEvent-function to be called twice when a cell is selected.
+        self.setFocusPolicy(Qt.NoFocus)
+
+        # Set row- and columncount
+        self.setRowCount(len(expressions))
+        self.setColumnCount(2)
+
+        # Remove headers and scrollbars
+        self.horizontalHeader().hide()
+        self.verticalHeader().hide()
+        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+
+        # Insert codes
+        self.insert_codes()
+
+        # Insert expressions
+        for i, expression in enumerate(expressions):
+            item_expression = QTableWidgetItem(expression)
+            item_expression.setFlags(
+                item_expression.flags() ^ Qt.ItemIsEditable
+            )
+            self.setItem(i, 1, item_expression)
+
+        # Resize columns and rows to fit contents
+        self.resizeColumnsToContents()
+        self.resizeRowsToContents()
+
+        # Insert headline and resize table
+        self.insert_headline(headline)
+        resize_table(self, rows=False, columns=False, has_headline=True)
+
+    def insert_headline(self, text):
+        """Insert headline.
+
+        Parameters
+        ----------
+        text : string
+            Text of the headline.
+        """
+        # Create QTableWidgetItem
+        item_headline = QTableWidgetItem(text)
+        item_headline.setFont(HEADLINE_FONT)
+        item_headline.setFlags(item_headline.flags() ^ Qt.ItemIsEditable)
+
+        # Insert row, item and make it span all columns
+        self.insertRow(0)
+        self.setItem(0, 0, item_headline)
+        self.setSpan(0, 0, 1, self.columnCount())
+
+    def insert_codes(self):
+        """Insert codes A-Z in first column."""
+        for i in range(self.rowCount()):
+            item_code = QTableWidgetItem(ALPHABET[i])
+            item_code.setTextAlignment(Qt.AlignCenter)
+            item_code.setFlags(item_code.flags() ^ Qt.ItemIsEditable)
+            self.setItem(i, 0, item_code)
+
+
+class PredefinedCodesSettings(AcceptRejectDialog):
+    """Dialog for setup and adjustment of PredefinedCodesModule.
+
+    Parameters
+    ----------
+    headline : string
+        Input-field for headline will be prefilled with this string.
+    warning_word : string
+        Input-field for warning-word will be prefilled with this string.
+    categories : list
+        Containing the categories (strings).
+    """
+
+    def __init__(self, headline, warning_word, categories):
+        super().__init__()
+
+        # Hide help-button, disable close-button and set window title and width
+        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
+        self.setWindowFlag(Qt.WindowCloseButtonHint, False)
+        self.setWindowTitle("Forhåndsavtalte koder")
+        self.setFixedWidth(350)
+
+        # Headline
+        self.label_headline = QLabel("Overskrift")
+        self.edit_headline = QLineEdit()
+        self.edit_headline.setText(headline)
+
+        # Warning-word
+        self.label_warning_word = QLabel("Varslingsord")
+        self.edit_warning_word = QLineEdit()
+        self.edit_warning_word.setText(warning_word)
+
+        # Maximum column-height
+        self.label_column_height = QLabel("Maksimal kolonnehøyde")
+        self.edit_column_height = QSpinBox()
+        self.edit_column_height.lineEdit().setReadOnly(True)
+        self.edit_column_height.setRange(50, MAXIMUM_COLUMN_HEIGHT)
+        self.edit_column_height.setSingleStep(50)
+        self.edit_column_height.setValue(DEFAULT_COLUMN_HEIGHT)
+
+        # Category-order
+        self.label_category_order = QLabel(
+            "Kategori-rekkefølge\n(dra og slipp)"
+        )
+        self.list_category_order = QListWidget()
+        # Enable drag-and-drop
+        self.list_category_order.setDragEnabled(True)
+        self.list_category_order.viewport().setAcceptDrops(True)
+        self.list_category_order.setDragDropMode(
+            QAbstractItemView.InternalMove
+        )
+        # Remove horizontal scrollbar
+        self.list_category_order.setHorizontalScrollBarPolicy(
+            Qt.ScrollBarAlwaysOff
+        )
+        # Add uneditable categories
+        for i, category in enumerate(categories):
+            item = QListWidgetItem(category)
+            item.setFlags(item.flags() & ~Qt.ItemIsEditable)
+            self.list_category_order.insertItem(i, item)
+
+        self.button_ok.setText("Opprett")
+
+        # Hide cancel-button, it is only used when modifying an existing
+        # PredefinedCodesModule
+        self.button_cancel.hide()
+
+        self.create_and_set_layout()
+
+    def create_and_set_layout(self):
+        """Create layouts, add widgets and set layout."""
+        # Layout for input-widgets
+        self.form_layout = QFormLayout()
+
+        # Add labels and their associated input-widgets
+        self.form_layout.addRow(self.label_headline, self.edit_headline)
+        self.form_layout.addRow(
+            self.label_warning_word, self.edit_warning_word
+        )
+        self.form_layout.addRow(
+            self.label_column_height, self.edit_column_height
+        )
+        self.form_layout.addRow(
+            self.label_category_order, self.list_category_order
+        )
+
+        # Main layout
+        self.main_layout = QVBoxLayout()
+        self.main_layout.addLayout(self.form_layout)
+
+        self.layout_content.addLayout(self.main_layout)
diff --git a/soitool/modules/module_subtractorcodes.py b/soitool/modules/module_subtractorcodes.py
index e56c0d4c38b1f0b72df23e65a11494ca7eb33d02..89d3f5c479e4f153489101b426450849a5a3db59 100644
--- a/soitool/modules/module_subtractorcodes.py
+++ b/soitool/modules/module_subtractorcodes.py
@@ -3,7 +3,11 @@ 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.module_base import (
+    resize_table,
+    qfont_with_pixel_size,
+    TABLE_CELL_DEFAULT_FONT,
+)
 from soitool.modules.code_table_base import CodeTableBase
 
 # Characters for first and second column
@@ -21,7 +25,9 @@ SPACE_INTERVAL = 4
 SPACE_AMOUNT = 3
 
 # Font for subtractorcodes
-CODE_FONT = QtGui.QFont("Arial", 10, QtGui.QFont.SansSerif)
+CODE_FONT = qfont_with_pixel_size(
+    "Arial", TABLE_CELL_DEFAULT_FONT.pixelSize(), QtGui.QFont.SansSerif
+)
 
 HEADLINE_TEXT = "Subtraktorkoder"
 
diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py
index 43bef7fb8d9e3ca625877c9ae7dfae4705b3dd93..fff51d7e058351e3df4dde972e1db077481b8316 100644
--- a/soitool/modules/module_table.py
+++ b/soitool/modules/module_table.py
@@ -6,6 +6,7 @@ from soitool.modules.module_base import (
     resize_table,
     get_table_size,
     HEADLINE_FONT,
+    TABLE_CELL_DEFAULT_FONT,
 )
 
 START_ROWS = 2
@@ -42,6 +43,7 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta):
         self.verticalHeader().hide()
         self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
         self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.setFont(TABLE_CELL_DEFAULT_FONT)
 
         # If parameters are None, start as empty table.
         if size is None and data is None:
diff --git a/soitool/new_module_dialog.py b/soitool/new_module_dialog.py
index d4e38d97d2fe40e4689973ed2512cbdcd1549b19..130b08e4e20a0b8084cf6a58b9b64ab1963d21ae 100644
--- a/soitool/new_module_dialog.py
+++ b/soitool/new_module_dialog.py
@@ -17,6 +17,7 @@ from soitool.modules.module_authentication_board import (
 from soitool.modules.module_subtractorcodes import SubtractorcodesModule
 from soitool.modules.module_freetext import FreeTextModule
 from soitool.modules.module_phonebook import PhonebookModule
+from soitool.modules.module_predefined_codes import PredefinedCodesModule
 from soitool.accept_reject_dialog import AcceptRejectDialog
 
 
@@ -28,7 +29,11 @@ MODULE_CHOICES = [
     AuthenticationBoardModule,
     SubtractorcodesModule,
     FreeTextModule,
+<<<<<<< soitool/new_module_dialog.py
     PhonebookModule,
+=======
+    PredefinedCodesModule,
+>>>>>>> soitool/new_module_dialog.py
 ]
 
 
diff --git a/soitool/serialize_export_import_soi.py b/soitool/serialize_export_import_soi.py
index 5a2033212badacf5e9d8455e83b8899d62f674f4..439704501c85865d4af1f196ae83736dbd892723 100644
--- a/soitool/serialize_export_import_soi.py
+++ b/soitool/serialize_export_import_soi.py
@@ -11,6 +11,7 @@ from soitool.modules.module_authentication_board import (
 from soitool.modules.module_subtractorcodes import SubtractorcodesModule
 from soitool.modules.module_freetext import FreeTextModule
 from soitool.modules.module_phonebook import PhonebookModule
+from soitool.modules.module_predefined_codes import PredefinedCodesModule
 
 # Valid schema for serialized SOI
 SERIALIZED_SOI_SCHEMA = Schema(
@@ -174,7 +175,7 @@ def export_soi(soi, compressed=True):
     file.close()
 
 
-def import_soi(file_path):
+def import_soi(file_path, database):
     """Import compressed or uncompressed serialized SOI.
 
     Reads content of file and decompresses it for .txt-files.
@@ -184,6 +185,8 @@ def import_soi(file_path):
     ----------
     file_path : string
         Full path to a file containing serialized SOI.
+    database : soitool.database.Database
+        Database-instance passed to specific modules.
 
     Returns
     -------
@@ -201,18 +204,22 @@ def import_soi(file_path):
         serialized = file.read()
 
         if file_path.endswith(".txt"):
-            return construct_soi_from_serialized(serialized, compressed=True)
+            return construct_soi_from_serialized(
+                serialized, database, compressed=True
+            )
 
-        return construct_soi_from_serialized(serialized)
+        return construct_soi_from_serialized(serialized, database)
 
 
-def construct_soi_from_serialized(serialized, compressed=False):
+def construct_soi_from_serialized(serialized, database, compressed=False):
     """Construct an SOI-object from a serialized SOI.
 
     Parameters
     ----------
     serialized : string
         Serialized SOI.
+    database : soitool.database.Database
+        Database-instance passed to specific modules.
     compressed : bool, optional
         True if serialized SOI is compressed, by default False.
 
@@ -237,8 +244,12 @@ def construct_soi_from_serialized(serialized, compressed=False):
         raise ValueError("Serialized SOI does not have correct format.")
 
     # Construct modules and attachments
-    modules = construct_modules_from_serialized(serialized["modules"])
-    attachments = construct_modules_from_serialized(serialized["attachments"])
+    modules = construct_modules_from_serialized(
+        serialized["modules"], database
+    )
+    attachments = construct_modules_from_serialized(
+        serialized["attachments"], database
+    )
 
     # Create SOI
     soi = SOI(
@@ -262,13 +273,15 @@ def construct_soi_from_serialized(serialized, compressed=False):
     return soi
 
 
-def construct_modules_from_serialized(serialized_modules):
+def construct_modules_from_serialized(serialized_modules, database):
     """Instantiate modules from serialized format.
 
     Parameters
     ----------
     serialized_modules : list
         Containing dicts with serialized modules or attachment-modules.
+    database : soitool.database.Database
+        Database-instance passed to specific modules.
 
     Returns
     -------
@@ -284,16 +297,14 @@ def construct_modules_from_serialized(serialized_modules):
 
     for module in serialized_modules:
         module_type = module["type"]
+        size = module["size"]
+        data = module["data"]
 
         if module_type == "TableModule":
-            size = module["size"]
-            data = module["data"]
             modules.append(
                 {"widget": TableModule(size, data), "meta": module["meta"]}
             )
         elif module_type == "AuthenticationBoardModule":
-            size = module["size"]
-            data = module["data"]
             modules.append(
                 {
                     "widget": AuthenticationBoardModule(size, data),
@@ -301,8 +312,6 @@ def construct_modules_from_serialized(serialized_modules):
                 }
             )
         elif module_type == "SubtractorcodesModule":
-            size = module["size"]
-            data = module["data"]
             modules.append(
                 {
                     "widget": SubtractorcodesModule(size, data),
@@ -310,16 +319,23 @@ def construct_modules_from_serialized(serialized_modules):
                 }
             )
         elif module_type == "FreeTextModule":
-            size = module["size"]
-            data = module["data"]
             modules.append(
                 {"widget": FreeTextModule(size, data), "meta": module["meta"]}
             )
+<<<<<<< soitool/serialize_export_import_soi.py
         elif module_type == "PhonebookModule":
             size = module["size"]
             data = module["data"]
             modules.append(
                 {"widget": PhonebookModule(size, data), "meta": module["meta"]}
+=======
+        elif module_type == "PredefinedCodesModule":
+            modules.append(
+                {
+                    "widget": PredefinedCodesModule(database, data),
+                    "meta": module["meta"],
+                }
+>>>>>>> soitool/serialize_export_import_soi.py
             )
         else:
             raise TypeError(
diff --git a/soitool/soi_model_view.py b/soitool/soi_model_view.py
index 680d27502fdefbec63c274fcd4c4c4ae1084b7b7..9f77f8b5189de45853732dd02e77da2be003966e 100644
--- a/soitool/soi_model_view.py
+++ b/soitool/soi_model_view.py
@@ -26,7 +26,8 @@ class SOITableView(QTableView):
     Parameters
     ----------
     database : soitool.database.Database
-        Is used to create a QSqlDatabase from the database-file.
+        Is used to create a QSqlDatabase from the database-file,
+        and to instantiate SOIWorkspaceWidget.
     tab_widget : QTabWidget
         Is used to open a new tab.
 
@@ -38,8 +39,10 @@ class SOITableView(QTableView):
 
     def __init__(self, database, tab_widget):
         super().__init__()
+        self.database = database
+
         db = QSqlDatabase.addDatabase(DBTYPE, CONNAME)
-        db.setDatabaseName(database.db_path)
+        db.setDatabaseName(self.database.db_path)
         self.tab_widget = tab_widget
 
         if not db.open():
@@ -91,8 +94,10 @@ class SOITableView(QTableView):
         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)
+        soi = construct_soi_from_serialized(
+            compressed_soi, self.database, compressed=True
+        )
+        tab = SOIWorkspaceWidget(self.database, soi)
 
         # Add and select tab
         self.tab_widget.addTab(tab, soi.title)
diff --git a/soitool/soi_workspace_widget.py b/soitool/soi_workspace_widget.py
index dc1e5c27163bcb62ad48953b79d17bd717a6251e..ed76226397cf2c2f8e90d2f7ebc4cdd3b0afa321 100644
--- a/soitool/soi_workspace_widget.py
+++ b/soitool/soi_workspace_widget.py
@@ -18,6 +18,10 @@ from soitool.inline_editable_soi_view import InlineEditableSOIView
 from soitool.setup_settings import Setup
 from soitool.new_module_dialog import NewModuleDialog
 from soitool.dialog_wrappers import exec_warning_dialog
+from soitool.modules.module_predefined_codes import PredefinedCodesModule
+
+# List of SOI-modules that require a database as parameter
+DATABASE_MODULES = [PredefinedCodesModule]
 
 
 class SOIWorkspaceWidget(QWidget):
@@ -25,14 +29,17 @@ class SOIWorkspaceWidget(QWidget):
 
     Creates a new SOI by default, but can receive an existing SOI through
     it's init parameter 'soi'.
+    The parameter 'database' is passed to instantiate specific modules.
 
     The widget is used inside tabs in our application, and contains a sidebar
     with a module list along with a view of the SOI.
     """
 
-    def __init__(self, soi=None):
+    def __init__(self, database, soi=None):
         super().__init__()
 
+        self.database = database
+
         if soi is None:
             self.soi = SOI()
         else:
@@ -118,8 +125,14 @@ class SOIWorkspaceWidget(QWidget):
                 )
 
             try:
+                if module_widget_implementation in DATABASE_MODULES:
+                    module = module_widget_implementation(
+                        database=self.database
+                    )
+                else:
+                    module = module_widget_implementation()
                 self.soi.add_module(
-                    module_name, module_widget_implementation(), is_attachment,
+                    module_name, module, is_attachment,
                 )
             except ModuleNameTaken:
                 exec_warning_dialog(
diff --git a/soitool/testdata/long_codebook.json b/soitool/testdata/long_codebook.json
index 1c5a0fe6bcbc8081944a64f9b9cb849abd6434a1..5423c81cf1c4ae9de2cc9032fff3970a1d516601 100644
--- a/soitool/testdata/long_codebook.json
+++ b/soitool/testdata/long_codebook.json
@@ -1,9 +1,64 @@
 [
+  {
+    "word": "0",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "1",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "2",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "3",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "4",
+    "category": "Tall",
+    "type": "Liten"
+  },
   {
     "word": "40 mm",
     "category": "V\u00e5penteknisk",
     "type": "Stor"
   },
+  {
+    "word": "5",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "6",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "7",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "8",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "9",
+    "category": "Tall",
+    "type": "Liten"
+  },
+  {
+    "word": "A",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "AG3",
     "category": "V\u00e5penteknisk",
@@ -34,6 +89,11 @@
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "B",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "BV-206",
     "category": "Kj\u00f8ret\u00f8y",
@@ -47,12 +107,17 @@
   {
     "word": "Bombe",
     "category": "V\u00e5penteknisk",
-    "type": "Stor"
+    "type": "Liten"
   },
   {
     "word": "Bro",
     "category": "Landemerker",
-    "type": "Stor"
+    "type": "Liten"
+  },
+  {
+    "word": "C",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
     "word": "CV 90",
@@ -84,6 +149,11 @@
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "D",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "Dal",
     "category": "Landemerker",
@@ -104,6 +174,11 @@
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "E",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "ERYX",
     "category": "V\u00e5penteknisk",
@@ -117,7 +192,7 @@
   {
     "word": "Elv",
     "category": "Landemerker",
-    "type": "Stor"
+    "type": "Liten"
   },
   {
     "word": "Er i rute",
@@ -129,11 +204,26 @@
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "F",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "Ferdigstilling",
     "category": "Straff",
     "type": "Stor"
   },
+  {
+    "word": "Fram",
+    "category": "Retninger",
+    "type": "Liten"
+  },
+  {
+    "word": "G",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "G-notes",
     "category": "Testkategori",
@@ -145,29 +235,24 @@
     "type": "Stor"
   },
   {
-    "word": "HK-416",
-    "category": "V\u00e5penteknisk",
-    "type": "Stor"
-  },
-  {
-    "word": "Hadza",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "H",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "Heron",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "HK-416",
+    "category": "V\u00e5penteknisk",
+    "type": "Liten"
   },
   {
-    "word": "Hindu",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Helikopter",
+    "category": "St\u00f8tte",
+    "type": "Liten"
   },
   {
     "word": "H\u00e5ndgranat",
     "category": "V\u00e5penteknisk",
-    "type": "Stor"
+    "type": "Liten"
   },
   {
     "word": "H\u00f8yde",
@@ -175,15 +260,30 @@
     "type": "Stor"
   },
   {
-    "word": "Jacqui",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "I",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
+  {
+    "word": "J",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
     "word": "Jamnagar",
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "K",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
+  {
+    "word": "K9",
+    "category": "St\u00f8tte",
+    "type": "Liten"
+  },
   {
     "word": "Khronos",
     "category": "Testkategori",
@@ -205,18 +305,13 @@
     "type": "Stor"
   },
   {
-    "word": "Leopard 2",
-    "category": "Kj\u00f8ret\u00f8y",
-    "type": "Stor"
-  },
-  {
-    "word": "Louis Quatorze",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "L",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "Lucifer",
-    "category": "Testkategori",
+    "word": "Leopard 2",
+    "category": "Kj\u00f8ret\u00f8y",
     "type": "Stor"
   },
   {
@@ -224,6 +319,11 @@
     "category": "V\u00e5penteknisk",
     "type": "Stor"
   },
+  {
+    "word": "M",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "M04",
     "category": "Bekledning",
@@ -255,19 +355,14 @@
     "type": "Stor"
   },
   {
-    "word": "MP-5",
-    "category": "V\u00e5penteknisk",
+    "word": "MP",
+    "category": "St\u00f8tte",
     "type": "Liten"
   },
   {
-    "word": "Maeander",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "Manila",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "MP-5",
+    "category": "V\u00e5penteknisk",
+    "type": "Liten"
   },
   {
     "word": "Meldingsblankett (blokker)",
@@ -275,14 +370,14 @@
     "type": "Stor"
   },
   {
-    "word": "Mendoza",
-    "category": "Testkategori",
+    "word": "Mine",
+    "category": "V\u00e5penteknisk",
     "type": "Stor"
   },
   {
-    "word": "Midwest",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "N",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
     "word": "Netting under over",
@@ -290,19 +385,19 @@
     "type": "Stor"
   },
   {
-    "word": "Nikkei index",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Nord",
+    "category": "Retninger",
+    "type": "Liten"
   },
   {
-    "word": "Orthodox Jew",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "O",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "P-80",
-    "category": "V\u00e5penteknisk",
-    "type": "Stor"
+    "word": "P",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
     "word": "PACE-batteri/BA-3090 (stk)",
@@ -329,6 +424,16 @@
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "Q",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
+  {
+    "word": "R",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "Regnt\u00f8y",
     "category": "Bekledning",
@@ -350,32 +455,22 @@
     "type": "Stor"
   },
   {
-    "word": "Saaniches",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "Sarah",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "Scylla",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "S",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "Senate",
+    "word": "Saaniches",
     "category": "Testkategori",
     "type": "Stor"
   },
   {
-    "word": "Sephardim",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Sanitet",
+    "category": "St\u00f8tte",
+    "type": "Liten"
   },
   {
-    "word": "Serpentes",
+    "word": "Sarah",
     "category": "Testkategori",
     "type": "Stor"
   },
@@ -390,44 +485,39 @@
     "type": "Stor"
   },
   {
-    "word": "Silicon Wadi",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "Siralun",
-    "category": "Testkategori",
+    "word": "Stans, -e, -et",
+    "category": "Uttrykk/tiltak/oppdrag",
     "type": "Stor"
   },
   {
-    "word": "Somerset Island",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Sti",
+    "category": "Landemerker",
+    "type": "Liten"
   },
   {
-    "word": "Squalicum's",
+    "word": "Surinamese",
     "category": "Testkategori",
     "type": "Stor"
   },
   {
-    "word": "Stans, -e, -et",
-    "category": "Uttrykk/tiltak/oppdrag",
-    "type": "Stor"
+    "word": "S\u00f8r",
+    "category": "Retninger",
+    "type": "Liten"
   },
   {
-    "word": "Sti",
-    "category": "Landemerker",
-    "type": "Stor"
+    "word": "T",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "Surinamese",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Tilbake",
+    "category": "Retninger",
+    "type": "Liten"
   },
   {
-    "word": "Tsvetaeva",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "U",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
     "word": "U-235",
@@ -444,6 +534,11 @@
     "category": "Personlig utrustning",
     "type": "Stor"
   },
+  {
+    "word": "V",
+    "category": "Bokstaver",
+    "type": "Liten"
+  },
   {
     "word": "Vann (l)",
     "category": "Etterforsyninger",
@@ -460,94 +555,29 @@
     "type": "Stor"
   },
   {
-    "word": "Vest plate",
-    "category": "Personnlig utrustning",
-    "type": "Stor"
-  },
-  {
-    "word": "Vicar of Christ",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "Visayan",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "Winterfest",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "acanthocephalan",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "acceptress",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "accountants",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "advertisers",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "aestivates",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "aetiologies",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "after-damp",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "after-dinner",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "alalia",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "alanyl",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Vest",
+    "category": "Retninger",
+    "type": "Liten"
   },
   {
-    "word": "alcoholometry",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "W",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "alfresco",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "X",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "algolagnic",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Y",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
-    "word": "allured",
-    "category": "Testkategori",
-    "type": "Stor"
+    "word": "Z",
+    "category": "Bokstaver",
+    "type": "Liten"
   },
   {
     "word": "angleworm",
@@ -574,26 +604,6 @@
     "category": "Testkategori",
     "type": "Stor"
   },
-  {
-    "word": "antitragicus",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "apama",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "aphoristic",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "archpriest",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
   {
     "word": "areola",
     "category": "Testkategori",
@@ -609,21 +619,6 @@
     "category": "Testkategori",
     "type": "Stor"
   },
-  {
-    "word": "assubjugated",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "athanasy",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
-  {
-    "word": "atony",
-    "category": "Testkategori",
-    "type": "Stor"
-  },
   {
     "word": "attapulgite",
     "category": "Testkategori",
@@ -3374,9 +3369,14 @@
     "category": "Testkategori",
     "type": "Stor"
   },
+  {
+    "word": "\u00d8st",
+    "category": "Retninger",
+    "type": "Liten"
+  },
   {
     "word": "\u00e6nigmata",
     "category": "Testkategori",
     "type": "Stor"
   }
-]
+]
\ No newline at end of file
diff --git a/test/test_database.py b/test/test_database.py
index 08dc93f079ba024302b4289c6bdff9389451161a..91378d02897dec218179ec99ef0bb523bddeaa9d 100644
--- a/test/test_database.py
+++ b/test/test_database.py
@@ -179,6 +179,48 @@ class DatabaseTest(unittest.TestCase):
         categories_db = self.database.get_categories()
         self.assertEqual(categories_file, categories_db)
 
+    def test_get_random_category_word(self):
+        """Assert function get_random_category_word works as expected."""
+        # Read all category-words
+        stmt = "SELECT Word from CategoryWords"
+        queried = self.database.conn.execute(stmt).fetchall()
+        category_words = [row["Word"] for row in queried]
+
+        # Generate 5 random category-words and assert that they are
+        # from table CategoryWords
+        generated_category_words = []
+        for _ in range(5):
+            category_word = self.database.get_random_category_word()
+            self.assertTrue(category_word in category_words)
+            generated_category_words.append(category_word)
+
+        # Assert that all words are not equal
+        self.assertTrue(len(set(generated_category_words)) > 1)
+
+    def test_get_categories_from_codebook(self):
+        """Assert function get_categories_from_codebook works as expected."""
+        # Get categories in full and small codebook
+        stmt = "SELECT Category FROM Codebook GROUP BY Category"
+        queried = self.database.conn.execute(stmt).fetchall()
+        expected_categories_full = [row["Category"] for row in queried]
+
+        stmt = (
+            "SELECT Category FROM Codebook "
+            "WHERE Type='Liten' GROUP BY Category"
+        )
+        queried = self.database.conn.execute(stmt).fetchall()
+        expected_categories_small = [row["Category"] for row in queried]
+
+        # Get categories in full and small codebook through function
+        actual_categories_full = self.database.get_categories_from_codebook()
+        actual_categories_small = self.database.get_categories_from_codebook(
+            small=True
+        )
+
+        # Assert actual categories matches expected categories
+        self.assertEqual(actual_categories_full, expected_categories_full)
+        self.assertEqual(actual_categories_small, expected_categories_small)
+
     def test_get_codebook(self):
         """Assert function get_codebook returns full codebook."""
         # Load full codebook
@@ -226,6 +268,37 @@ class DatabaseTest(unittest.TestCase):
             self.assertRegex(actual[i]["code"], "[A-Z]{" + str(code_len) + "}")
             self.assertEqual(entry["type"], actual[i]["type"])
 
+    def test_get_codebook_expressions_in_category(self):
+        """Test function get_codebook_expressions_in_category."""
+        # Get a category in full and small codebook
+        category_full = self.database.get_categories_from_codebook()[0]
+        category_small = self.database.get_categories_from_codebook(
+            small=True
+        )[0]
+
+        # Get expressions from category
+        stmt = "SELECT Word FROM Codebook WHERE Category=?"
+        queried = self.database.conn.execute(stmt, (category_full,)).fetchall()
+        expected_expressions_full = [row["Word"] for row in queried]
+
+        stmt += " AND Type='Liten'"
+        queried = self.database.conn.execute(
+            stmt, (category_small,)
+        ).fetchall()
+        expected_expressions_small = [row["Word"] for row in queried]
+
+        # Get expressions from function
+        actual_expr_full = self.database.get_codebook_expressions_in_category(
+            category_full
+        )
+        actual_expr_small = self.database.get_codebook_expressions_in_category(
+            category_small, small=True
+        )
+
+        # Assert actual expressions matches expected expressions
+        self.assertEqual(actual_expr_full, expected_expressions_full)
+        self.assertEqual(actual_expr_small, expected_expressions_small)
+
     def test_update_codebook(self):
         """Test that the codes get updated."""
         # Get entries before and after update
@@ -281,7 +354,7 @@ class DatabaseTest(unittest.TestCase):
             )
 
         else:
-            print("ERROR: Database is not 675 entries long, cant run test")
+            print("ERROR: Database is not 676 entries long, cant run test")
             self.assertTrue(False)  # pylint: disable=W1503
 
     def test_seconds_to_next_update(self):
diff --git a/test/test_module_predefined_codes.py b/test/test_module_predefined_codes.py
new file mode 100644
index 0000000000000000000000000000000000000000..d18fa647fb13d5e94cdbe809ec8ceaccb2dda837
--- /dev/null
+++ b/test/test_module_predefined_codes.py
@@ -0,0 +1,155 @@
+"""Tests for PredefinedCodesModule."""
+import unittest
+import string
+from test.test_database import TESTDBPATH
+from PySide2.QtCore import Qt, QTimer
+from PySide2 import QtGui
+from PySide2.QtTest import QTest
+from PySide2.QtWidgets import QApplication
+from soitool.database import Database
+from soitool.modules.module_predefined_codes import (
+    PredefinedCodesModule,
+    HEADLINE,
+    DEFAULT_COLUMN_HEIGHT,
+)
+
+ALPHABET = string.ascii_uppercase
+
+if isinstance(QtGui.qApp, type(None)):
+    app = QApplication([])
+else:
+    app = QtGui.qApp
+
+
+class TestDefaultPredefinedCodesModule(unittest.TestCase):
+    """TestCase for PredefinedCodesModule."""
+
+    def setUp(self):
+        """Create database and PredefinedCodesModule."""
+        self.database = Database(db_path=TESTDBPATH)
+
+        # Get categories in small codebook
+        self.categories = self.database.get_categories_from_codebook(
+            small=True
+        )
+
+        def press_enter_on_active_dialog():
+            active_widget = app.activeModalWidget()
+            QTest.keyClick(active_widget, Qt.Key_Enter)
+
+        QTimer.singleShot(0, press_enter_on_active_dialog)
+        self.module = PredefinedCodesModule(self.database)
+
+    def test_default_dialog(self):
+        """Test default values in dialog."""
+
+        def test_and_close_dialog():
+            dialog = app.activeModalWidget()
+
+            # Assert prefilled headline is correct
+            self.assertEqual(dialog.edit_headline.text(), HEADLINE)
+
+            # Assert prefilled warning-word is not empty
+            warning_word = dialog.edit_warning_word.text()
+            self.assertTrue(len(warning_word) > 0)
+
+            # Assert prefilled maximum column-height is correct
+            maximum_column_height_value = dialog.edit_column_height.value()
+            self.assertTrue(
+                maximum_column_height_value >= DEFAULT_COLUMN_HEIGHT
+            )
+
+            # Assert categories are correct
+            for i, category in enumerate(self.categories):
+                dialog_category = dialog.list_category_order.item(i).text()
+                self.assertEqual(category, dialog_category)
+
+            # Close dialog
+            dialog.accept()
+
+        # Use shortcut to open dialog
+        QTimer.singleShot(0, test_and_close_dialog)
+        QTest.keyClicks(self.module, "R", Qt.ControlModifier)
+
+    def test_default_module(self):
+        """Test default module."""
+        # Assert headline is correct
+        self.assertEqual(self.module.headline.text(), HEADLINE)
+
+        # Assert warning-word is not empty
+        self.assertTrue(len(self.module.warning_word.text()) > 0)
+
+        # Assert module has one table per category
+        self.assertEqual(len(self.module.tables), len(self.categories))
+
+        # Assert each table has the correct headline and contents
+        for i, table in enumerate(self.module.tables):
+            expected_headline = " " + ALPHABET[i] + " " + self.categories[i]
+            actual_headline = table.item(0, 0).text()
+            self.assertEqual(actual_headline, expected_headline)
+
+            expressions = self.database.get_codebook_expressions_in_category(
+                self.categories[i], small=True
+            )
+            # Assert codes are correct and read expressions in table
+            actual_expressions = []
+            for j in range(1, table.rowCount()):
+                expected_code = ALPHABET[j - 1]
+                actual_code = table.item(j, 0).text()
+                self.assertEqual(actual_code, expected_code)
+
+                actual_expressions.append(table.item(j, 1).text())
+            # Assert expressions are correct
+            for actual_expression in actual_expressions:
+                self.assertTrue(actual_expression in expressions)
+
+
+class TestDefaultPredefinedCodesModuleFromData(unittest.TestCase):
+    """TestCase for initializing PredefinedCodesModule from data."""
+
+    def test_create_from_data(self):
+        """Test creating PredefinedCodesModule from data."""
+        test_data = {
+            "headline": "HeadlineText",
+            "warning_word": "WarningWordText",
+            "maximum_column_height": 500,
+            "categories": ["CategoryOne", "CategoryTwo"],
+            "tables": [
+                {
+                    "table_headline": "TableHeadlineOne",
+                    "expressions": ["expressionOne", "expressionTwo"],
+                },
+                {
+                    "table_headline": "TableHeadlineTwo",
+                    "expressions": ["expressionOne", "expressionTwo"],
+                },
+            ],
+        }
+        # Create PredefinedCodesModule from data
+        module = PredefinedCodesModule(Database(db_path=TESTDBPATH), test_data)
+
+        # Assert headline is correct
+        self.assertEqual(module.headline.text(), "HeadlineText")
+
+        # Assert warning-word is correct
+        self.assertEqual(module.warning_word.text(), "WarningWordText")
+
+        # Assert module has two tables
+        self.assertEqual(len(module.tables), 2)
+
+        # Assert each table has the correct headline and expressions
+        table_one = module.tables[0]
+        table_two = module.tables[1]
+
+        # Assert correct headlines
+        self.assertEqual(table_one.item(0, 0).text(), "TableHeadlineOne")
+        self.assertEqual(table_two.item(0, 0).text(), "TableHeadlineTwo")
+
+        # Assert correct codes and expressions
+        self.assertEqual(table_one.item(1, 1).text(), "expressionOne")
+        self.assertEqual(table_one.item(2, 1).text(), "expressionTwo")
+        self.assertEqual(table_two.item(1, 1).text(), "expressionOne")
+        self.assertEqual(table_two.item(2, 1).text(), "expressionTwo")
+        for i in range(1, 3):
+            self.assertEqual(table_one.item(i, 0).text(), ALPHABET[i - 1])
+            self.assertEqual(table_two.item(i, 0).text(), ALPHABET[i - 1])
diff --git a/test/test_resolution_smoke_test.py b/test/test_resolution_smoke_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bc6fcbd0662307bc26cbfb840408c2339ced370
--- /dev/null
+++ b/test/test_resolution_smoke_test.py
@@ -0,0 +1,152 @@
+"""Test module that looks for differences in SOI modules across resolutions.
+
+This was written as a regression test for #125. Ideally it should also test the
+header of the SOI, but this is much more complicated to test for..
+
+## Important
+
+This test can not test across different resolutions by itself, this is up to
+the user. Our pipeline will test across a couple of different resolutions.
+"""
+import unittest
+from PySide2.QtWidgets import QApplication
+from PySide2 import QtGui
+from PySide2.QtCore import QTimer, Qt, QSysInfo
+from PySide2.QtTest import QTest
+from PySide2.QtGui import QGuiApplication
+from soitool.soi import SOI
+from soitool.modules.module_authentication_board import (
+    AuthenticationBoardModule,
+)
+from soitool.modules.module_subtractorcodes import SubtractorcodesModule
+from soitool.modules.module_predefined_codes import PredefinedCodesModule
+from soitool.new_module_dialog import MODULE_CHOICES
+from soitool.soi_workspace_widget import DATABASE_MODULES
+from soitool.database import Database
+
+# The error being ignored here is pylint telling us that 'test' is a standard
+# module, so the import should be placed further up. In our case we have an
+# actual custom module called 'test', so pylint is confused.
+# pylint: disable=C0411
+from test.test_database import TESTDBPATH
+
+
+if isinstance(QtGui.qApp, type(None)):
+    app = QApplication([])
+else:
+    app = QtGui.qApp
+
+# Modules with a popup as part of their __init__
+POPUP_MODULES = [
+    AuthenticationBoardModule,
+    SubtractorcodesModule,
+    PredefinedCodesModule,
+]
+
+
+def screen_information():
+    """Get string with information about the screen.
+
+    Returns
+    -------
+    str
+        String with screen information.
+    """
+    screen_string = ""
+    screen = QGuiApplication.primaryScreen()
+    screen_string += "screen.size() -> " + str(screen.size()) + "\n"
+    screen_string += (
+        "screen.physicalDotsPerInchX() -> "
+        + str(screen.physicalDotsPerInchX())
+        + "\n"
+    )
+    screen_string += (
+        "screen.physicalDotsPerInchY() -> "
+        + str(screen.physicalDotsPerInchY())
+        + "\n"
+    )
+    screen_string += (
+        "screen.logicalDotsPerInchX() -> "
+        + str(screen.logicalDotsPerInchX())
+        + "\n"
+    )
+    screen_string += (
+        "screen.logicalDotsPerInchY() -> "
+        + str(screen.logicalDotsPerInchY())
+        + "\n"
+    )
+    screen_string += (
+        "screen.devicePixelRatio() -> " + str(screen.devicePixelRatio()) + "\n"
+    )
+    return screen_string
+
+
+class TestModulesAcrossResolutions(unittest.TestCase):
+    """TestCase for modules across resolutions."""
+
+    @unittest.skipUnless(
+        QSysInfo.productType() == "windows",
+        "Test currently only able to target Windows",
+    )
+    def test_add_all_modules(self):
+        """Add all modules, reorganize, and assert result.
+
+        Expected result was gotten by simply running reorganize and noting down
+        the results. We should get the same result across different
+        resolutions.
+
+        Modules added to SOI must be the same every time the program is ran.
+
+        Test only works on Windows, as the expected result is based on the
+        Windows look-and-feel.
+
+        NOTE: This test needs to be updated when a new module is added,
+        deleted, or changed. It will fail until it is updated.
+        """
+        expected_result = [
+            {"x": 0, "y": 0, "page": 1, "name": "PredefinedCodesModule"},
+            {"x": 654, "y": 0, "page": 1, "name": "AuthenticationBoardModule"},
+            {"x": 1061.5, "y": 0, "page": 1, "name": "SubtractorcodesModule"},
+            {"x": 1287.0, "y": 0, "page": 1, "name": "FreeTextModule"},
+            {"x": 1387.0, "y": 0, "page": 1, "name": "TableModule"},
+        ]
+
+        # For use with modules that require a database
+        database = Database(db_path=TESTDBPATH)
+
+        def press_enter():
+            active_widget = app.activeModalWidget()
+
+            # AuthenticationBoardModule needs special treatment because the
+            # title can contain random info
+            if isinstance(active_widget, AuthenticationBoardModule):
+                # triple click to select existing text for overwrite
+                QTest.mouseDClick(active_widget.edit_headline, Qt.LeftButton)
+                QTest.mouseClick(active_widget.edit_headline, Qt.LeftButton)
+                # need to overwrite text because title otherwise contains
+                # unpredictable text
+                QTest.keyClicks(active_widget.edit_headline, "TestTitle")
+            QTest.keyClick(active_widget, Qt.Key_Enter)
+
+        soi = SOI()
+
+        for module in MODULE_CHOICES:
+            # If we're adding one of the modules with a popup as part of it's
+            # constructor we need to singleShot pressing enter to close it
+            if module in POPUP_MODULES:
+                QTimer.singleShot(0, press_enter)
+
+            if module in DATABASE_MODULES:
+                soi.add_module(module.__name__, module(database=database))
+            else:
+                soi.add_module(module.__name__, module())
+
+        soi.reorganize()
+
+        self.assertEqual(
+            expected_result,
+            [module["meta"] for module in soi.modules],
+            "All modules added to SOI and calling reorganize should result in "
+            "expected 'meta' information. Screen is: \n"
+            + screen_information(),
+        )
diff --git a/test/test_serialize_export_import.py b/test/test_serialize_export_import.py
index 143c31635110ed0a69270a28e3124aba99960d80..c55f3b565db59ed040833ef8377c42a300d8b237 100644
--- a/test/test_serialize_export_import.py
+++ b/test/test_serialize_export_import.py
@@ -4,9 +4,11 @@ import unittest
 import json
 from pathlib import Path
 from datetime import datetime
+from test.test_database import TESTDBPATH
 from PySide2.QtWidgets import QApplication
 from soitool.soi import SOI
 from soitool.modules.module_table import TableModule
+from soitool.database import Database
 from soitool.serialize_export_import_soi import (
     serialize_soi,
     export_soi,
@@ -116,6 +118,7 @@ class SerializeTest(unittest.TestCase):
         self.file_path_compressed = os.path.join(
             SOITOOL_ROOT_PATH, file_name_compressed
         )
+        self.database = Database(db_path=TESTDBPATH)
 
     def test_serialize_soi(self):
         """Serialize SOI and check its validity against schema."""
@@ -149,7 +152,7 @@ class SerializeTest(unittest.TestCase):
 
         # Assert import throws error
         with self.assertRaises(ValueError) as error:
-            import_soi(invalid_soi_file_path)
+            import_soi(invalid_soi_file_path, self.database)
         self.assertEqual(
             str(error.exception),
             "Serialized SOI does not have correct format.",
@@ -160,8 +163,10 @@ class SerializeTest(unittest.TestCase):
     def test_import_soi(self):
         """Import serialized SOI's, check content and delete SOI-files."""
         # Import uncompressed and compressed SOI's
-        soi_uncompressed = import_soi(self.file_path_uncompressed)
-        soi_compressed = import_soi(self.file_path_compressed)
+        soi_uncompressed = import_soi(
+            self.file_path_uncompressed, self.database
+        )
+        soi_compressed = import_soi(self.file_path_compressed, self.database)
 
         # Assert SOI content is correct
         self.check_soi_content(soi_uncompressed)