Repository: usnistgov/NEMO (language: python)

Issue:
Superusers receive task/problem reports and updates
Can the option for superusers to receive task/problem reports be added?  I understand this may not be desirable in every context, so a global boolean to control this behavior might be reasonable.  Thanks for considering this.

Or if this feature already exists, how do I enable it?  Unless I misunderstood, your 8/8 email suggested that superusers should receive alerts and maintenance updates.


Discussion / hints:

Fix patch:
diff --git a/NEMO/actions.py b/NEMO/actions.py
index 6fcd2eb39..da66ab1ca 100644
--- a/NEMO/actions.py
+++ b/NEMO/actions.py
@@ -3,7 +3,7 @@
 from django.urls import reverse
 from django.utils.safestring import mark_safe
 
-from NEMO.models import Area, Interlock, InterlockCard, Tool, User
+from NEMO.models import Area, Configuration, Interlock, InterlockCard, Tool, User
 from NEMO.typing import QuerySetType
 from NEMO.views.access_requests import access_csv_export
 from NEMO.views.adjustment_requests import adjustments_csv_export
@@ -137,3 +137,36 @@ def adjustment_requests_export_csv(modeladmin, request, queryset):
 @admin.action(description="Export selected access requests in CSV")
 def access_requests_export_csv(modeladmin, request, queryset):
     return access_csv_export(queryset.all())
+
+
+@admin.action(description="Duplicate selected configuration")
+def duplicate_configuration(model_admin, request, queryset: QuerySetType[Configuration]):
+    for configuration in queryset:
+        original_name = configuration.name
+        new_name = "Copy of " + configuration.name
+        try:
+            if Configuration.objects.filter(name=new_name).exists():
+                messages.error(
+                    request,
+                    mark_safe(
+                        f'There is already a copy of {original_name} as <a href="{reverse("admin:NEMO_configuration_change", args=[configuration.id])}">{new_name}</a>. Change the copy\'s name and try again'
+                    ),
+                )
+                continue
+            else:
+                old_maintainers = configuration.maintainers.all()
+                configuration.pk = None
+                configuration.name = new_name
+                configuration.save()
+                for maintainer in old_maintainers:
+                    configuration.maintainers.add(maintainer)
+                messages.success(
+                    request,
+                    mark_safe(
+                        f'A duplicate of {original_name} has been made as <a href="{reverse("admin:NEMO_configuration_change", args=[configuration.id])}">{configuration.name}</a>'
+                    ),
+                )
+        except Exception as error:
+            messages.error(
+                request, f"{original_name} could not be duplicated because of the following error: {str(error)}"
+            )
diff --git a/NEMO/admin.py b/NEMO/admin.py
index 9d2ad7f43..eb9b58ade 100644
--- a/NEMO/admin.py
+++ b/NEMO/admin.py
@@ -19,6 +19,7 @@
     adjustment_requests_export_csv,
     create_next_interlock,
     disable_selected_cards,
+    duplicate_configuration,
     duplicate_tool_configuration,
     enable_selected_cards,
     lock_selected_interlocks,
@@ -27,7 +28,7 @@
     unlock_selected_interlocks,
 )
 from NEMO.forms import BuddyRequestForm, RecurringConsumableChargeForm, UserPreferencesForm
-from NEMO.mixins import ObjPermissionAdminMixin, ModelAdminRedirectMixin
+from NEMO.mixins import ModelAdminRedirectMixin, ObjPermissionAdminMixin
 from NEMO.models import (
     Account,
     AccountType,
@@ -46,6 +47,7 @@
     Comment,
     Configuration,
     ConfigurationHistory,
+    ConfigurationOption,
     Consumable,
     ConsumableCategory,
     ConsumableWithdraw,
@@ -460,6 +462,7 @@ class ConfigurationAdmin(admin.ModelAdmin):
         "exclude_from_configuration_agenda",
     )
     filter_horizontal = ("maintainers",)
+    actions = [duplicate_configuration]
 
 
 @register(ConfigurationHistory)
@@ -559,6 +562,11 @@ def save_model(self, request, obj, form, change):
             obj.manager_set.set(form.cleaned_data["principal_investigators"])
 
 
+class ConfigurationOptionInline(admin.TabularInline):
+    model = ConfigurationOption
+    extra = 0
+
+
 @register(Reservation)
 class ReservationAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
     list_display = (
@@ -584,6 +592,7 @@ class ReservationAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.M
         ("user", admin.RelatedOnlyFieldListFilter),
     )
     date_hierarchy = "start"
+    inlines = [ConfigurationOptionInline]
 
 
 class ReservationQuestionsForm(forms.ModelForm):
diff --git a/NEMO/apps/area_access/templates/area_access/alerts.html b/NEMO/apps/area_access/templates/area_access/alerts.html
new file mode 100644
index 000000000..b9f14bbfc
--- /dev/null
+++ b/NEMO/apps/area_access/templates/area_access/alerts.html
@@ -0,0 +1,32 @@
+{% if alerts or disabled_resources %}
+    <div style="text-align:center">
+        <h2>Alerts and outages</h2>
+        <p>{# For spacing #}&nbsp;</p>
+        {% for a in alerts %}
+            <div class="alert alert-danger">
+                <table style="width:100%">
+                    <tr>
+                        <td style="width:100px"><span class="glyphicon glyphicon-alert danger-highlight" style="font-size:64px"></span></td>
+                        <td>
+                            {% if a.title %}<span style="font-weight:bold">{{ a.title }}</span><br>{% endif %}
+                            {{ a.contents|linebreaks }}
+                        </td>
+                    </tr>
+                </table>
+            </div>
+        {% endfor %}
+        {% for r in disabled_resources %}
+            <div class="alert alert-danger">
+                <table style="width:100%">
+                    <tr>
+                        <td style="width:100px"><span class="glyphicon glyphicon-leaf danger-highlight" style="font-size:64px"></span></td>
+                        <td>
+                            <span style="font-weight:bold">Resource outage: {{ r }}</span><br>
+                            {{ r.restriction_message }}
+                        </td>
+                    </tr>
+                </table>
+            </div>
+        {% endfor %}
+    </div>
+{% endif %}
\ No newline at end of file
diff --git a/NEMO/apps/area_access/urls.py b/NEMO/apps/area_access/urls.py
index 06fde8d3b..99c0c82f9 100644
--- a/NEMO/apps/area_access/urls.py
+++ b/NEMO/apps/area_access/urls.py
@@ -6,6 +6,7 @@
 urlpatterns = [
     # Tablet area access
     path("occupancy/", area_access.occupancy, name="area_access_occupancy"),
+    path("area_access_alerts/", views.get_alerts, name="area_access_alerts"),
     path("welcome_screen/<int:door_id>/", views.welcome_screen, name="welcome_screen"),
     path("farewell_screen/<int:door_id>/", views.farewell_screen, name="farewell_screen"),
     path("login_to_area/<int:door_id>/", views.login_to_area, name="login_to_area"),
diff --git a/NEMO/apps/area_access/views.py b/NEMO/apps/area_access/views.py
index 4573f5033..cd876afce 100644
--- a/NEMO/apps/area_access/views.py
+++ b/NEMO/apps/area_access/views.py
@@ -30,6 +30,8 @@
     Project,
     UsageEvent,
     User,
+    Alert,
+    Resource,
 )
 from NEMO.policy import policy_class as policy
 from NEMO.views.area_access import log_in_user_to_area, log_out_user
@@ -323,3 +325,11 @@ def get_badge_reader(request) -> BadgeReader:
     except BadgeReader.DoesNotExist:
         badge_reader = BadgeReader.default()
     return badge_reader
+
+
+def get_alerts(request):
+    dictionary = {
+        "alerts": Alert.objects.filter(user=None, debut_time__lte=timezone.now(), expired=False, deleted=False),
+        "disabled_resources": Resource.objects.filter(available=False),
+    }
+    return render(request, "area_access/alerts.html", dictionary)
diff --git a/NEMO/apps/contracts/templates/contracts/contracts_and_procurements.html b/NEMO/apps/contracts/templates/contracts/contracts_and_procurements.html
index 0ecce1ef8..cac7f1b0f 100644
--- a/NEMO/apps/contracts/templates/contracts/contracts_and_procurements.html
+++ b/NEMO/apps/contracts/templates/contracts/contracts_and_procurements.html
@@ -62,7 +62,7 @@ <h1 style="margin-top: 0; margin-bottom: 25px">Contracts & procurements</h1>
         {# Set a global click handler to dismiss manual tooltips #}
         $('html').on('click', function(e)
         {
-            {# Only dimiss tooltips when clicking outside of them #}
+            {# Only dismiss tooltips when clicking outside of them #}
             let target_id = $(e.target).parents('.tooltip').attr('id');
             if (typeof $(e.target).data('original-title') == 'undefined')
             {
diff --git a/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html b/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html
index 19f578443..3900f85e1 100644
--- a/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html
+++ b/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html
@@ -26,7 +26,15 @@
                 <strong>This reservation was created on short notice. Tool configuration is not guaranteed upon start time.</strong><br>
             {% endif %}
         {% endif %}
+        {% if reservation.configurationoption_set.exists %}
+            <div style="height:10px">{# Vertical spacer #}</div>
+            Configuration:<br>
+            {% for config_option in reservation.configurationoption_set.all %}
+                {{ config_option.name }}: {% if config_option.get_color %}<span class="configuration-reservation-option" style="background-color: {{ config_option.get_color }}">{{ config_option.current_setting }}</span>{% else %}{{ config_option.current_setting }}{% endif %}<br>
+            {% endfor %}
+        {% endif %}
         {% if reservation.additional_information %}
+            <div style="height:10px">{# Vertical spacer #}</div>
             Additional information:<br>
             {{ reservation.additional_information|linebreaksbr }}
         {% endif %}
diff --git a/NEMO/apps/kiosk/templates/kiosk/kiosk.html b/NEMO/apps/kiosk/templates/kiosk/kiosk.html
index 6ce0af342..b572a2b6a 100644
--- a/NEMO/apps/kiosk/templates/kiosk/kiosk.html
+++ b/NEMO/apps/kiosk/templates/kiosk/kiosk.html
@@ -47,7 +47,14 @@
 			<div id="default_content">
 				<h1>Scan your badge to control tools</h1>
 				<div id="occupancy"></div>
+                <div id="usage_title"></div>
+                <div class="tab-pane{% if tools_exist and tab == 'tools' %} active{% endif %} info-tooltip-container" id="tools">
+                    <div id="usage"></div>
+                </div>
+                <div id="alerts">
+                </div>
 			</div>
+
 			<div id="error" style="display: none"><h1>There was a problem communicating with the web server. Please visit the user office for assistance.</h1></div>
             <div id="interlock_error" style="display: none">
                 <h1 class="text-center">
@@ -90,7 +97,8 @@ <h3 id="interlock_message" class="text-center"></h3>
 		let timeout_handle = null;
 		let countdown_handle = null;
 		let occupancy = new URLSearchParams(window.location.search).get('occupancy');
-
+        let usage = new URLSearchParams(window.location.search).get('usage') !== null;
+        let alerts = new URLSearchParams(window.location.search).get('alerts') !== null;
 		let default_content_element = $("#default_content");
 		let status_element = $("#status");
 		let error_element = $("#error");
@@ -119,6 +127,16 @@ <h3 id="interlock_message" class="text-center"></h3>
 			fetch_occupancy();
 			set_interval_when_visible(document, fetch_occupancy, 30000);
 		}
+        if(usage)
+		{
+			fetch_usage();
+			set_interval_when_visible(document, fetch_usage, 30000);
+		}
+        if(alerts)
+		{
+			fetch_alerts();
+			set_interval_when_visible(document, fetch_alerts, 30000);
+		}
 
 		function load_complete(response, status, xml_http_request)
 		{
@@ -152,6 +170,8 @@ <h3 id="interlock_message" class="text-center"></h3>
 		function revert_to_default_content()
 		{
 			fetch_occupancy();
+            fetch_usage();
+            fetch_alerts();
 			clear_timeout();
 			close_numpads();
 			close_keyboards();
@@ -320,8 +340,34 @@ <h3 id="interlock_message" class="text-center"></h3>
 		}
 		function fetch_occupancy()
 		{
-			ajax_get('{% url 'kiosk_occupancy' %}', {'occupancy': occupancy}, function(response) { $("#occupancy").html(response); });
+            if(occupancy)
+            {
+			    ajax_get('{% url 'kiosk_occupancy' %}', {'occupancy': occupancy}, function(response) { $("#occupancy").html(response); });
+            }
 		}
+
+        function fetch_usage()
+		{
+            if(usage)
+            {
+                $("#usage_title").html("<h2>Tools in use</h2>")
+                ajax_get('{% url 'kiosk_usage' %}', {'interest': 'tools'},
+                    function(response) {
+                        $("#usage").html(response);
+                        $(".in_use").css("display", "table-row");
+                        $(".idle").hide();
+                        $(".glyphicon-remove-circle").hide();
+                    }
+                );
+            }
+        }
+        function fetch_alerts()
+        {
+            if(alerts)
+            {
+                $("#alerts").load("{% url 'area_access_alerts' %}");
+            }
+        }
 		function close_numpads()
 		{
 			$.each($('[data-numpad]'), function (index, item)
diff --git a/NEMO/apps/kiosk/urls.py b/NEMO/apps/kiosk/urls.py
index 580377c21..b464302bb 100644
--- a/NEMO/apps/kiosk/urls.py
+++ b/NEMO/apps/kiosk/urls.py
@@ -1,7 +1,6 @@
 from django.urls import include, path, re_path
-
 from NEMO.apps.kiosk import views
-from NEMO.views import area_access
+from NEMO.views import area_access, status_dashboard
 
 urlpatterns = [
     # Tablet kiosk
@@ -10,6 +9,7 @@
         include(
             [
                 path("occupancy/", area_access.occupancy, name="kiosk_occupancy"),
+                path("kiosk_usage/", status_dashboard.status_dashboard, name="kiosk_usage"),
                 path("enable_tool/", views.enable_tool, name="enable_tool_from_kiosk"),
                 path("disable_tool/", views.disable_tool, name="disable_tool_from_kiosk"),
                 path("reserve_tool/", views.reserve_tool, name="reserve_tool_from_kiosk"),
diff --git a/NEMO/apps/kiosk/views.py b/NEMO/apps/kiosk/views.py
index 78fdd635a..8b89bf99c 100644
--- a/NEMO/apps/kiosk/views.py
+++ b/NEMO/apps/kiosk/views.py
@@ -14,9 +14,9 @@
 from NEMO.utilities import localize, quiet_int
 from NEMO.views.calendar import (
     cancel_the_reservation,
-    extract_configuration,
     extract_reservation_questions,
     render_reservation_questions,
+    set_reservation_configuration,
     shorten_reservation,
 )
 from NEMO.views.customization import ApplicationCustomization
@@ -178,7 +178,7 @@ def reserve_tool(request):
         error_dictionary["message"] = "You must specify a project for your reservation"
         return render(request, "kiosk/error.html", error_dictionary)
 
-    reservation.additional_information, reservation.self_configuration = extract_configuration(request)
+    set_reservation_configuration(reservation, request)
     # Reservation can't be short notice if the user is configuring the tool themselves.
     if reservation.self_configuration:
         reservation.short_notice = False
diff --git a/NEMO/context_processors.py b/NEMO/context_processors.py
index 3f7e91a2a..9889bb9be 100644
--- a/NEMO/context_processors.py
+++ b/NEMO/context_processors.py
@@ -88,6 +88,26 @@ def base_context(request):
         safety_menu_item = SafetyCustomization.get_bool("safety_main_menu")
     except:
         safety_menu_item = True
+    try:
+        calendar_page_title = ApplicationCustomization.get("calendar_page_title")
+    except:
+        calendar_page_title = "Calendar"
+    try:
+        tool_control_page_title = ApplicationCustomization.get("tool_control_page_title")
+    except:
+        tool_control_page_title = "Tool control"
+    try:
+        status_dashboard_page_title = ApplicationCustomization.get("status_dashboard_page_title")
+    except:
+        status_dashboard_page_title = "Status dashboard"
+    try:
+        requests_page_title = ApplicationCustomization.get("requests_page_title")
+    except:
+        requests_page_title = "Requests"
+    try:
+        safety_page_title = ApplicationCustomization.get("safety_page_title")
+    except:
+        safety_page_title = "Safety"
     return {
         "facility_name": facility_name,
         "recurring_charges_name": recurring_charges_name,
@@ -109,4 +129,9 @@ def base_context(request):
         "datetime_input_js_format": datetime_input_js_format,
         "no_header": request.session.get("no_header", False),
         "safety_menu_item": safety_menu_item,
+        "calendar_page_title": calendar_page_title,
+        "tool_control_page_title": tool_control_page_title,
+        "status_dashboard_page_title": status_dashboard_page_title,
+        "requests_page_title": requests_page_title,
+        "safety_page_title": safety_page_title,
     }
diff --git a/NEMO/forms.py b/NEMO/forms.py
index deccf4d81..cb78fa4ca 100644
--- a/NEMO/forms.py
+++ b/NEMO/forms.py
@@ -197,7 +197,7 @@ class Meta:
         model = Comment
         fields = ["tool", "content", "staff_only"]
 
-    expiration = IntegerField(label="Expiration date", min_value=0)
+    expiration = IntegerField(label="Expiration date", min_value=-1)
 
 
 class SafetyIssueCreationForm(ModelForm):
diff --git a/NEMO/migrations/0055_reservationconfigurationoption.py b/NEMO/migrations/0055_reservationconfigurationoption.py
new file mode 100644
index 000000000..e27ae00ff
--- /dev/null
+++ b/NEMO/migrations/0055_reservationconfigurationoption.py
@@ -0,0 +1,177 @@
+# Generated by Django 3.2.22 on 2023-11-28 18:58
+import re
+
+import django.core.validators
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("NEMO", "0054_documents_update_max_length"),
+    ]
+
+    def migrate_configuration_option_forward(apps, schema_editor):
+        ConfigurationHistory = apps.get_model("NEMO", "ConfigurationHistory")
+        ConfigurationOption = apps.get_model("NEMO", "ConfigurationOption")
+        Reservation = apps.get_model("NEMO", "Reservation")
+        for reservation in Reservation.objects.exclude(additional_information="").exclude(
+            additional_information__isnull=True
+        ):
+            configs = extract_patterns_in_additional_information(reservation.additional_information)
+            for config in configs:
+                name, value = config[0].strip(), config[1].strip()
+                ConfigurationOption.objects.create(name=name, reservation=reservation, current_setting=value)
+        for config_history in ConfigurationHistory.objects.all():
+            conf = config_history.configuration
+            config_history.item_name = (
+                f"{conf.configurable_item_name} #{config_history.slot + 1}"
+                if conf.configurable_item_name
+                else conf.name
+            )
+            config_history.save()
+
+    def migrate_configuration_option_reverse(apps, schema_editor):
+        pass
+
+    operations = [
+        migrations.AlterField(
+            model_name="area",
+            name="area_calendar_color",
+            field=models.CharField(
+                default="#88B7CD",
+                help_text="Color for tool reservations in calendar overviews",
+                max_length=9,
+                validators=[
+                    django.core.validators.RegexValidator(
+                        re.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"),
+                        "Enter a valid hex color, eg. #000000",
+                        "invalid",
+                    )
+                ],
+            ),
+        ),
+        migrations.AlterField(
+            model_name="tool",
+            name="_tool_calendar_color",
+            field=models.CharField(
+                db_column="tool_calendar_color",
+                default="#33ad33",
+                help_text="Color for tool reservations in calendar overviews",
+                max_length=9,
+                validators=[
+                    django.core.validators.RegexValidator(
+                        re.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"),
+                        "Enter a valid hex color, eg. #000000",
+                        "invalid",
+                    )
+                ],
+            ),
+        ),
+        migrations.AddField(
+            model_name="configurationhistory",
+            name="item_name",
+            field=models.CharField(max_length=255, null=True),
+        ),
+        migrations.AddField(
+            model_name="configuration",
+            name="calendar_colors",
+            field=models.TextField(
+                blank=True,
+                help_text="Comma separated list of html colors for each available setting. E.g. #ffffff, #eeeeee",
+                null=True,
+                validators=[
+                    django.core.validators.RegexValidator(
+                        re.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})(?:,\\s*#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}))*$"),
+                        code="invalid",
+                        message="Enter a valid hex color list, eg. #000000,#111111",
+                    )
+                ],
+            ),
+        ),
+        migrations.CreateModel(
+            name="ConfigurationOption",
+            fields=[
+                ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+                ("name", models.CharField(max_length=255)),
+                (
+                    "current_setting",
+                    models.CharField(
+                        blank=True,
+                        help_text="The current value for this configuration option",
+                        max_length=255,
+                        null=True,
+                    ),
+                ),
+                (
+                    "available_settings",
+                    models.TextField(
+                        blank=True,
+                        help_text="The available choices to select for this configuration option. Multiple values are separated by commas.",
+                        null=True,
+                    ),
+                ),
+                (
+                    "calendar_colors",
+                    models.TextField(
+                        blank=True,
+                        help_text="Comma separated list of html colors for each available setting. E.g. #ffffff, #eeeeee",
+                        null=True,
+                        validators=[
+                            django.core.validators.RegexValidator(
+                                re.compile(
+                                    "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})(?:,\\s*#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}))*$"
+                                ),
+                                code="invalid",
+                                message="Enter a valid hex color list, eg. #000000,#111111",
+                            )
+                        ],
+                    ),
+                ),
+                (
+                    "absence_string",
+                    models.CharField(
+                        blank=True,
+                        help_text="The text that appears to indicate absence of a choice.",
+                        max_length=100,
+                        null=True,
+                    ),
+                ),
+                (
+                    "configuration",
+                    models.ForeignKey(
+                        blank=True,
+                        help_text="The configuration this option applies to",
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to="NEMO.configuration",
+                    ),
+                ),
+                (
+                    "reservation",
+                    models.ForeignKey(
+                        help_text="The reservation this option is set on",
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="configurationoption_set",
+                        to="NEMO.reservation",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+        ),
+        migrations.RunPython(migrate_configuration_option_forward, migrate_configuration_option_reverse),
+    ]
+
+
+def extract_patterns_in_additional_information(sentence):
+    # Define the pattern using regular expressions
+    # pattern = r"(.*?)\s+(?:#\d+\s+)?needs\s+to\s+be\s+set\s+to\s+(.*?)\."
+    pattern = r"(.*?)\s+needs\s+to\s+be\s+set\s+to\s+(.*?)\."
+
+    # Find all matches in the sentence
+    matches = re.findall(pattern, sentence)
+
+    return matches
diff --git a/NEMO/mixins.py b/NEMO/mixins.py
index dd951e9dd..8316f0c1d 100644
--- a/NEMO/mixins.py
+++ b/NEMO/mixins.py
@@ -10,7 +10,7 @@
 from django.shortcuts import redirect
 from django.utils import timezone
 
-from NEMO.utilities import beginning_of_the_day, format_datetime, get_recurring_rule, RecurrenceFrequency
+from NEMO.utilities import RecurrenceFrequency, beginning_of_the_day, format_datetime, get_recurring_rule
 from NEMO.views.constants import NEXT_PARAMETER_NAME
 
 if TYPE_CHECKING:
@@ -296,3 +296,19 @@ def has_view_permission(self, request, obj=None):
         return request.user.has_perm("%s.%s" % (opts.app_label, codename_view), obj) or request.user.has_perm(
             "%s.%s" % (opts.app_label, codename_change), obj
         )
+
+
+class ConfigurationMixin:
+    def calendar_colors_as_list(self):
+        return [x.strip() for x in self.calendar_colors.split(",")] if self.calendar_colors else []
+
+    def get_available_setting(self, choice):
+        choice = int(choice)
+        available_settings = self.available_settings_as_list()
+        return available_settings[choice]
+
+    def current_settings_as_list(self):
+        return [x.strip() for x in self.current_settings.split(",")]
+
+    def available_settings_as_list(self):
+        return [x.strip() for x in self.available_settings.split(",")] if self.available_settings else []
diff --git a/NEMO/models.py b/NEMO/models.py
index c1d92ebac..d9c52c297 100644
--- a/NEMO/models.py
+++ b/NEMO/models.py
@@ -33,7 +33,7 @@
 from mptt.models import MPTTModel
 
 from NEMO import fields
-from NEMO.mixins import BillableItemMixin, CalendarDisplayMixin, RecurrenceMixin
+from NEMO.mixins import BillableItemMixin, CalendarDisplayMixin, ConfigurationMixin, RecurrenceMixin
 from NEMO.typing import QuerySetType
 from NEMO.utilities import (
     EmailCategory,
@@ -53,6 +53,7 @@
     send_mail,
     supported_embedded_extensions,
 )
+from NEMO.validators import color_hex_list_validator, color_hex_validator
 from NEMO.views.constants import ADDITIONAL_INFORMATION_MAXIMUM_LENGTH, CHAR_FIELD_MAXIMUM_LENGTH
 from NEMO.widgets.configuration_editor import ConfigurationEditor
 
@@ -1096,6 +1097,7 @@ class Tool(SerializationByNameModel):
         max_length=9,
         default="#33ad33",
         help_text="Color for tool reservations in calendar overviews",
+        validators=[color_hex_validator],
     )
     _category = models.CharField(
         db_column="category",
@@ -1896,7 +1898,7 @@ class Meta:
         db_table = "NEMO_user_qualifications"
 
 
-class Configuration(BaseModel):
+class Configuration(BaseModel, ConfigurationMixin):
     name = models.CharField(
         max_length=200,
         help_text="The name of this overall configuration. This text is displayed as a label on the tool control page.",
@@ -1929,6 +1931,15 @@ class Configuration(BaseModel):
         null=True,
         help_text="The available choices to select for this configuration option. Multiple values are separated by commas.",
     )
+    calendar_colors = models.TextField(
+        blank=True,
+        null=True,
+        help_text="Comma separated list of html colors for each available setting. E.g. #ffffff, #eeeeee",
+        validators=[color_hex_list_validator],
+    )
+    absence_string = models.CharField(
+        max_length=100, blank=True, null=True, help_text="The text that appears to indicate absence of a choice."
+    )
     maintainers = models.ManyToManyField(
         User, blank=True, help_text="Select the users that are allowed to change this configuration."
     )
@@ -1940,9 +1951,6 @@ class Configuration(BaseModel):
         default=False,
         help_text="Reservations containing this configuration will be excluded from the Configuration Agenda page.",
     )
-    absence_string = models.CharField(
-        max_length=100, blank=True, null=True, help_text="The text that appears to indicate absence of a choice."
-    )
     enabled = models.BooleanField(
         default=True, help_text="Only active configurations will show up for the selected tool"
     )
@@ -1960,17 +1968,6 @@ def get_current_setting(self, slot):
             )
         return self.current_settings_as_list()[slot]
 
-    def current_settings_as_list(self):
-        return [x.strip() for x in self.current_settings.split(",")]
-
-    def available_settings_as_list(self):
-        return [x.strip() for x in self.available_settings.split(",")]
-
-    def get_available_setting(self, choice):
-        choice = int(choice)
-        available_settings = self.available_settings_as_list()
-        return available_settings[choice]
-
     def replace_current_setting(self, slot, choice):
         slot = int(slot)
         current_settings = self.current_settings_as_list()
@@ -1980,6 +1977,14 @@ def replace_current_setting(self, slot, choice):
     def range_of_configurable_items(self):
         return range(0, len(self.current_settings.split(",")))
 
+    def get_color(self, setting):
+        index = (
+            self.available_settings_as_list().index(setting) if setting in self.available_settings_as_list() else None
+        )
+        if index is not None:
+            color_list = self.calendar_colors_as_list()
+            return color_list[index] if color_list and len(color_list) > index else None
+
     def user_is_maintainer(self, user):
         if user in self.maintainers.all() or user.is_staff:
             return True
@@ -1994,6 +1999,61 @@ def __str__(self):
         return str(self.tool.name) + ": " + str(self.name)
 
 
+class ConfigurationOption(BaseModel, ConfigurationMixin):
+    name = models.CharField(max_length=CHAR_FIELD_MAXIMUM_LENGTH)
+    configuration = models.ForeignKey(
+        Configuration,
+        null=True,
+        blank=True,
+        help_text="The configuration this option applies to",
+        on_delete=models.SET_NULL,
+    )
+    reservation = models.ForeignKey(
+        "Reservation",
+        help_text="The reservation this option is set on",
+        on_delete=models.CASCADE,
+        related_name="configurationoption_set",
+    )
+    current_setting = models.CharField(
+        null=True,
+        blank=True,
+        max_length=CHAR_FIELD_MAXIMUM_LENGTH,
+        help_text="The current value for this configuration option",
+    )
+    available_settings = models.TextField(
+        blank=True,
+        null=True,
+        help_text="The available choices to select for this configuration option. Multiple values are separated by commas.",
+    )
+    calendar_colors = models.TextField(
+        blank=True,
+        null=True,
+        help_text="Comma separated list of html colors for each available setting. E.g. #ffffff, #eeeeee",
+        validators=[color_hex_list_validator],
+    )
+    absence_string = models.CharField(
+        max_length=100, blank=True, null=True, help_text="The text that appears to indicate absence of a choice."
+    )
+
+    def get_color(self):
+        # if the underlying configuration has not changed (same available settings), use color from config
+        same_config = self.configuration and self.configuration.available_settings == self.available_settings
+        if same_config:
+            return self.configuration.get_color(self.current_setting)
+        index = (
+            self.available_settings_as_list().index(self.current_setting)
+            if self.current_setting in self.available_settings_as_list()
+            else None
+        )
+        if index is not None:
+            color_list = self.calendar_colors_as_list()
+            return color_list[index] if color_list and len(color_list) > index else None
+
+    def __str__(self):
+        selected = f", current value: {self.current_setting}" if self.current_setting else ""
+        return f"{self.name}, options: {self.available_settings_as_list()}{selected}"
+
+
 class TrainingSession(BaseModel, BillableItemMixin):
     class Type(object):
         INDIVIDUAL = 0
@@ -2088,7 +2148,10 @@ class Area(MPTTModel):
 
     # Additional information
     area_calendar_color = models.CharField(
-        max_length=9, default="#88B7CD", help_text="Color for tool reservations in calendar overviews"
+        max_length=9,
+        default="#88B7CD",
+        help_text="Color for tool reservations in calendar overviews",
+        validators=[color_hex_validator],
     )
 
     # Area access
@@ -2332,6 +2395,7 @@ class ConfigurationHistory(BaseModel):
     configuration = models.ForeignKey(Configuration, on_delete=models.CASCADE)
     user = models.ForeignKey(User, on_delete=models.CASCADE)
     modification_time = models.DateTimeField(default=timezone.now)
+    item_name = models.CharField(null=True, blank=False, max_length=CHAR_FIELD_MAXIMUM_LENGTH)
     slot = models.PositiveIntegerField()
     setting = models.TextField()
 
@@ -2520,6 +2584,25 @@ def has_not_ended(self):
     def has_not_started(self):
         return False if self.start <= timezone.now() else True
 
+    def get_configuration_options_display(self):
+        result = ""
+        for config_option in self.configurationoption_set.all():
+            result += f"{config_option.name}: {config_option.current_setting}\n"
+        return result
+
+    def get_configuration_options_colors(self):
+        colors = []
+        for config_option in self.configurationoption_set.all():
+            colors.append(config_option.get_color() or "#5561ec")
+        return colors
+
+    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
+        super().save(force_insert, force_update, using, update_fields)
+        deferred_related_models = getattr(self, "_deferred_related_models", None)
+        if deferred_related_models:
+            for deferred_related_model in deferred_related_models:
+                deferred_related_model.save()
+
     def save_and_notify(self):
         self.save()
         from NEMO.views.calendar import (
@@ -2545,6 +2628,15 @@ def copy(self, new_start: datetime = None, new_end: datetime = None):
             new_reservation.end = new_end
         if new_reservation.tool:
             new_reservation.short_notice = new_reservation.tool.determine_insufficient_notice(new_reservation.start)
+        # If we have configuration options, we have to save them later since this copy does not save the reservation
+        # In the reservation save method, it will check for the existence of this
+        if self.configurationoption_set.exists():
+            deferred_related_models = []
+            for config_option in self.configurationoption_set.all():
+                config_option.pk = None
+                config_option.reservation = new_reservation
+                deferred_related_models.append(config_option)
+            new_reservation._deferred_related_models = deferred_related_models
         return new_reservation
 
     class Meta:
@@ -3688,6 +3780,24 @@ def outage_item_filter(self):
         else:
             return {self.outage_item_type.value: self.outage_item}
 
+    def has_not_ended(self):
+        return False if self.end < timezone.now() else True
+
+    def has_not_started(self):
+        return False if self.start <= timezone.now() else True
+
+    def clean(self):
+        if self.start and self.end and self.start >= self.end:
+            raise ValidationError(
+                {
+                    "start": "Outage start time ("
+                    + format_datetime(self.start)
+                    + ") must be before the end time ("
+                    + format_datetime(self.end)
+                    + ")."
+                }
+            )
+
     def __str__(self):
         return str(self.title)
 
diff --git a/NEMO/serializers.py b/NEMO/serializers.py
index 479c22d91..0b4173b6a 100644
--- a/NEMO/serializers.py
+++ b/NEMO/serializers.py
@@ -14,6 +14,8 @@
     AccountType,
     Area,
     AreaAccessRecord,
+    Configuration,
+    ConfigurationOption,
     Consumable,
     ConsumableCategory,
     ConsumableWithdraw,
@@ -174,8 +176,28 @@ class Meta:
         expandable_fields = {"parent_area": "NEMO.serializers.AreaSerializer"}
 
 
+class ConfigurationOptionSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = ConfigurationOption
+        fields = "__all__"
+        expandable_fields = {
+            "reservation": "NEMO.serializers.ReservationSerializer",
+            "configuration": "NEMO.serializers.ConfigurationSerializer",
+        }
+
+
+class ConfigurationSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = Configuration
+        fields = "__all__"
+        expandable_fields = {
+            "tool": "NEMO.serializers.ToolSerializer",
+        }
+
+
 class ReservationSerializer(FlexFieldsSerializerMixin, ModelSerializer):
     question_data = serializers.JSONField(source="question_data_json")
+    configuration_options = ConfigurationOptionSerializer(source="configurationoption_set", many=True, read_only=True)
 
     class Meta:
         model = Reservation
@@ -188,6 +210,7 @@ class Meta:
             "area": "NEMO.serializers.AreaSerializer",
             "project": "NEMO.serializers.ProjectSerializer",
             "descendant": "NEMO.serializers.ReservationSerializer",
+            "configuration_options": ("NEMO.serializers.ConfigurationOptionSerializer", {"many": True}),
         }
 
 
diff --git a/NEMO/static/nemo.css b/NEMO/static/nemo.css
index ff2617f73..576832f49 100644
--- a/NEMO/static/nemo.css
+++ b/NEMO/static/nemo.css
@@ -1184,4 +1184,10 @@ li.area-tree-node a, li.area-tree-node a:hover, li.area-tree-node a:focus
 .consumable-row .tooltip-inner
 {
 	text-align: left!important;
+}
+
+.configuration-reservation-option
+{
+	padding: 2px;
+	color: white;
 }
\ No newline at end of file
diff --git a/NEMO/static/nemo.js b/NEMO/static/nemo.js
index e6011c0b9..620cad16b 100644
--- a/NEMO/static/nemo.js
+++ b/NEMO/static/nemo.js
@@ -824,3 +824,28 @@ function wait_for_element(selector)
 		});
     });
 }
+
+function set_multiple_colors(element, colors) {
+	// Create a vertical gradient with every color given, evenly distributed
+	if (Array.isArray(colors)) {
+		if (colors.length === 1) {
+			element.css({"background-color": colors[0]});
+		} else {
+			const interval = 100 / colors.length;
+			let percentage = interval;
+			let css_background = "linear-gradient(90deg, ";
+			colors.forEach(function (color, index) {
+				css_background += color + " " + percentage + "%";
+				if (index !== colors.length - 1) {
+					css_background += ", ";
+				}
+				if (index !== 0 && index !== colors.length - 1) {
+					percentage += interval
+					css_background += color + " " + percentage + "%, ";
+				}
+			});
+			css_background += ")";
+			element.css({"background-image": css_background});
+		}
+	}
+}
diff --git a/NEMO/templates/accounts_and_projects/transfer_charges.html b/NEMO/templates/accounts_and_projects/transfer_charges.html
new file mode 100644
index 000000000..214cb678a
--- /dev/null
+++ b/NEMO/templates/accounts_and_projects/transfer_charges.html
@@ -0,0 +1,193 @@
+{% extends 'base.html' %}
+{% load custom_tags_and_filters %}
+{% load static %}
+{% block title %}Transfer charges to another project{% endblock %}
+{% block extrahead %}
+    <script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
+    <link rel="stylesheet" type="text/css" href="{% static "datetimepicker/bootstrap-datetimepicker.css" %}"/>
+{% endblock %}
+{% block content %}
+    <h1>Transfer charges to another project</h1>
+    <p>
+        This page allows managers and accounting officers to transfer charges from one project to another.
+    </p>
+    <div style="height: 20px"></div>
+    <form class="form-horizontal" role="form" method="post">
+        {% if form.errors %}
+            <div class="form-group">
+                <div class="col-sm-8 col-lg-6">
+                    <div class="alert alert-danger">
+                        Oops! Something went wrong. Please correct the errors highlighted below:
+                        {% if form.non_field_errors %}
+                        <br><br>
+                        <ul><li>{{ form.non_field_errors }}</li></ul>
+                        {% endif %}
+                    </div>
+                </div>
+                <div class="col-sm-4 col-lg-6"></div>
+            </div>
+        {% endif %}
+        {% csrf_token %}
+        <div class="form-group">
+            <div class="control-label col-sm-1">
+                <label for="start_date">Start</label>
+            </div>
+            <div class="col-sm-3 col-md-3 col-lg-2">
+                <input type="text" id="start_date" name="start" class="form-control text-center"
+                       placeholder="Choose a date" value="{{ form.start.value|input_date_format }}" required>
+            </div>
+            <div class="control-label col-sm-1">
+                <label for="end_date">End</label>
+            </div>
+            <div class="col-sm-3 col-md-3 col-lg-2">
+                <input type="text" id="end_date" name="end" class="form-control text-center" placeholder="Optional"
+                       value="{{ form.end.value|input_date_format }}">
+            </div>
+        </div>
+        {% if form.start.errors or form.end.errors %}
+            <div class="col-md-4 col-lg-3 form-control-static danger-highlight">{{ form.start.errors|striptags }}</div>
+            <div class="col-md-8 col-lg-9 form-control-static danger-highlight">{{ form.end.errors|striptags }}</div>
+        {% endif %}
+        <div class="form-group">
+            <div class="control-label col-sm-1">
+                <label for="project_search">Original</label>
+            </div>
+            <div class="col-sm-7 col-lg-5">
+                <input type="text" id="project_search" name="project_id" class="form-control"
+                       placeholder="Choose a project" required>
+                <input type="button" id="selected_project" class="btn btn-default"
+                       onclick="clear_project_selection('projects')" style="display:none">
+            </div>
+            {% if form.project_id.errors %}
+                <div class="col-sm-offset-1 col-sm-8 col-log-6 form-control-static danger-highlight">{{ form.project_id.errors|striptags }}</div>
+            {% endif %}
+        </div>
+        <div class="form-group">
+            <div class="control-label col-sm-1">
+                <label for="user_search">User</label>
+            </div>
+            <div class="col-sm-7 col-lg-5">
+                <input type="text" id="user_search" name="user_id" class="form-control"
+                       placeholder="[Optional] search for a specific user">
+                <input type="button" id="selected_user" class="btn btn-default" onclick="clear_user_selection()"
+                       style="display:none">
+            </div>
+            {% if form.user_id.errors or form.username.errors %}
+                <div class="col-sm-offset-1 col-sm-7 col-lg-5 form-control-static danger-highlight">
+                    {{ form.user_id.errors|striptags }}{{ form.username.errors|striptags }}</div>
+            {% endif %}
+        </div>
+        <div class="form-group">
+            <div class="col-sm-offset-1 col-sm-11">
+                {% button type="export" submit=True value="Search" icon="glyphicon-search" %}
+            </div>
+        </div>
+        {% if charges %}
+            <div class="form-group">
+                <div class="control-label col-sm-1">
+                    <label for="new_project_search">New</label>
+                </div>
+                <div class="col-sm-7 col-lg-5">
+                    <input type="text" id="new_project_search" name="new_project_id" class="form-control"
+                           placeholder="Choose the new project to transfer charges to">
+                    <input type="button" id="selected_new_project" class="btn btn-default"
+                           onclick="clear_project_selection('new_projects')" style="display:none">
+                </div>
+                {% if form.new_project_id.errors %}
+                    <div class="col-sm-offset-1 col-sm-7 col-log-5 form-control-static danger-highlight">{{ form.new_project_id.errors|striptags }}</div>
+                {% endif %}
+            </div>
+            {% with number_of_charges=charges|length %}
+            <div class="form-group">
+                <div class="col-sm-offset-1 col-sm-11">
+                    {% button type="save" submit=False value="Confirm" name="confirm" onclick="if (this.form.reportValidity()){ if(window.confirm('Are you sure you want to transfer these "|concat:number_of_charges|concat:" charges?')){submit_and_disable(this)}}" %}
+                </div>
+            </div>
+            <div class="danger-highlight" style="margin-top: 25px;">{{ number_of_charges }} charges were found:</div>
+            {% endwith %}
+
+            <table class="table table-striped">
+                <thead>
+                <tr>
+                    <th>Date</th>
+                    <th>Type</th>
+                    <th>Item</th>
+                    <th>Quantity</th>
+                    <th>User</th>
+                    <th>Project</th>
+                    <th class="text-right">{{ "projects_and_accounts"|customization:"project_application_identifier_name" }}</th>
+                </tr>
+                </thead>
+                {% for charge in charges %}
+                    <tr>
+                        <td>{{ charge.end|date:"SHORT_DATETIME_FORMAT" }}</td>
+                        <td>{{ charge.type }}</td>
+                        <td>{{ charge.name }}</td>
+                        <td>{{ charge.quantity }}</td>
+                        <td>{{ charge.user }}</td>
+                        <td>{{ charge.project }}</td>
+                        <td class="text-right">{{ charge.application }}</td>
+                    </tr>
+                {% endfor %}
+            </table>
+        {% endif %}
+        <script>
+            const js_date_format = "{{ date_input_js_format }}";
+
+            let start_date_jq = $('#start_date');
+            let end_date_jq = $('#end_date');
+            start_date_jq.datetimepicker({format: js_date_format});
+            end_date_jq.datetimepicker({format: js_date_format});
+
+            function clear_project_selection(dataset_name) {
+                if (dataset_name === "projects") {
+                    $("#selected_project").hide();
+                    $('#project_search').prop('required', true).typeahead('val', '').show().focus();
+                } else {
+                    $("#selected_new_project").hide();
+                    $('#new_project_search').typeahead('val', '').show().focus();
+                }
+            }
+
+            function select_project(jquery_event, search_selection, dataset_name) {
+                if (dataset_name === "projects") {
+                    $('#project_search').prop('required', false).typeahead('val', search_selection.id).hide();
+                    $("#selected_project").val(search_selection.name).show();
+                } else {
+                    $('#new_project_search').typeahead('val', search_selection.id).hide();
+                    $("#selected_new_project").val(search_selection.name).show();
+                }
+            }
+
+            function clear_user_selection(dataset_name) {
+                $("#selected_user").hide();
+                $('#user_search').typeahead('val', '').show().focus();
+            }
+
+            function select_user(jquery_event, search_selection) {
+                $('#user_search').typeahead('val', search_selection.id).hide();
+                $("#selected_user").val(search_selection.name).show();
+            }
+
+            function on_load() {
+                $("#user_search").autocomplete('users', select_user, '{% url 'user_search' %}', true);
+                $('#project_search').autocomplete('projects', select_project, '{% url "search_project_for_transfer" %}');
+                $('#new_project_search').autocomplete('new_projects', select_project, '{% url "search_project_for_transfer" %}');
+                if ('{{ project|default_if_none:"" }}') select_project(null, {
+                    'name': '{{ project.name }}',
+                    'id': '{{ project.id }}'
+                }, 'projects');
+                if ('{{ new_project|default_if_none:"" }}') select_project(null, {
+                    'name': '{{ new_project.name }}',
+                    'id': '{{ new_project.id }}'
+                }, 'new_projects');
+                if ('{{ customer|default_if_none:"" }}') select_user(null, {
+                    'name': '{{ customer }}',
+                    'id': '{{ customer.id }}'
+                });
+            }
+
+            $(on_load);
+        </script>
+    </form>
+{% endblock %}
\ No newline at end of file
diff --git a/NEMO/templates/base/navbar_base.html b/NEMO/templates/base/navbar_base.html
index c3f578823..d1ba18b07 100644
--- a/NEMO/templates/base/navbar_base.html
+++ b/NEMO/templates/base/navbar_base.html
@@ -13,18 +13,18 @@
 		<div class="navbar-collapse collapse" id="site-navigation">
 			<ul class="nav navbar-nav">
                 {% block navbar_main %}
-				    <li><a href="{% url 'calendar' %}">Calendar</a></li>
-                    {% if tools_exist %}<li><a href="{% url 'tool_control' %}">Tool control</a></li>{% endif %}
+				    <li><a href="{% url 'calendar' %}">{{ calendar_page_title }}</a></li>
+                    {% if tools_exist %}<li><a href="{% url 'tool_control' %}">{{ tool_control_page_title }}</a></li>{% endif %}
                     {% if tools_exist or areas_exist %}
                         {% if areas_exist %}
-                            <li><a href="{% url 'status_dashboard' %}">Status dashboard</a></li>
+                            <li><a href="{% url 'status_dashboard' %}">{{ status_dashboard_page_title }}</a></li>
                         {% else %}
-                            <li><a href="{% url 'status_dashboard_tab' 'tools' %}">Status dashboard</a></li>
+                            <li><a href="{% url 'status_dashboard_tab' 'tools' %}">{{ status_dashboard_page_title }}</a></li>
                         {% endif %}
                     {% endif %}
                     {% if buddy_system_areas_exist or access_user_request_allowed_exist and facility_managers_exist or adjustment_request_allowed and facility_managers_exist %}
                         <li><a href="{% url 'user_requests' %}">
-                            Requests
+                            {{ requests_page_title }}
                             {% if buddy_notification_count or temporary_access_notification_count or adjustment_notification_count %}
                                 <span class="badge active">{{ buddy_notification_count|add:temporary_access_notification_count|add:adjustment_notification_count }}</span>
                             {% endif %}
@@ -32,7 +32,7 @@
                     {% endif %}
                     {% if safety_menu_item %}
                         <li><a href="{% url 'safety' %}">
-                            Safety
+                            {{ safety_page_title }}
                             {% if safety_notification_count %}
                                 <span class="badge active">{{ safety_notification_count }}</span>
                             {% endif %}
@@ -70,6 +70,9 @@
                                     {% navigation_url 'consumables' 'Supplies' user.is_staff user.is_user_office user.is_superuser %}
                                     {% if tools_exist %}{% navigation_url 'training' 'Training' user.is_staff user.is_superuser %}{% endif %}
                                     {% navigation_url 'users' 'Users' %}
+                                    {% if "projects_and_accounts"|customization:"project_allow_transferring_charges" == "enabled" %}
+                                        {% navigation_url 'transfer_charges' 'Transfer charges' user.is_accounting_officer user.is_facility_manager user.is_superuser %}
+                                    {% endif %}
                                 {% elif user.is_tool_superuser or user.is_project_pi %}
                                     {% navigation_url 'projects' 'Projects' user.is_project_pi %}
                                     {% if tools_exist %}{% navigation_url 'training' 'Training' user.is_tool_superuser %}{% endif %}
diff --git a/NEMO/templates/calendar/calendar.html b/NEMO/templates/calendar/calendar.html
index 1ed8c7764..de680db72 100644
--- a/NEMO/templates/calendar/calendar.html
+++ b/NEMO/templates/calendar/calendar.html
@@ -1,6 +1,6 @@
 {% extends 'base.html' %}
 {% load custom_tags_and_filters %}
-{% block title %}Calendar{% endblock %}
+{% block title %}{{ calendar_page_title }}{% endblock %}
 {% block extrahead %}
 	{% load static %}
 	<link rel="stylesheet" type="text/css" href="{% static "fullcalendar/fullcalendar.min.css" %}" />
@@ -18,6 +18,9 @@
 			<li class="dropdown-header" id="event_type_help">Which type of events would you like<br>to view on the calendar?</li>
 			<li data-toggle="tooltip" data-placement="right" title="Displays {% if tools and areas %}tool and area{% elif tools %}tool{% elif areas %}area{% endif %} reservations. While viewing reservations, the 'Personal schedule' link shows your reservations for all {% if tools and areas %}tools and areas{% elif tools %}tools{% elif areas %}areas{% endif %}. Select a {% if tools and areas %}tool or area{% elif tools %}tool{% elif areas %}area{% endif %} to view everyone's reservations for that {% if tools and areas %}tool or area{% elif tools %}tool{% elif areas %}area{% endif %}."><a aria-describedby="event_type_help" href="javascript:void(0)" onclick="change_calendar_event_type(this)">Reservations</a></li>
 			<li data-toggle="tooltip" data-placement="right" title="Displays your {{ facility_name }} usage. This includes{% if areas %} area access,{% endif %}{% if tools %} tool usage,{% endif %} and missed reservations (which are all billable items)."><a aria-describedby="event_type_help" href="javascript:void(0)" onclick="change_calendar_event_type(this)">{{ facility_name }} usage</a></li>
+                <li data-toggle="tooltip" data-placement="right" title="Displays the tool configuration agenda."><a
+                        aria-describedby="event_type_help" href="javascript:void(0)"
+                        onclick="change_calendar_event_type(this)">Configuration agenda</a></li>
 			{% if user.is_staff %}
 				<li data-toggle="tooltip" data-placement="right" title="Displays {{ facility_name }} reservations and usage for a specific user. This includes reservations, area access, tool usage, and missed reservations."><a aria-describedby="event_type_help" href="javascript:void(0)" onclick="change_calendar_event_type(this)">Specific user</a></li>
 			{% endif %}
@@ -37,15 +40,19 @@
 
 	{# The following menu tree code was take from an example at http://www.bootply.com/120625 #}
 	<ul id="extra-links" class="nav nav-list" style="margin-top: 10px; margin-bottom: 8px;">
-		<li><a href="javascript:void(0)" class="selected personal_schedule" data-item-id="personal_schedule" onclick="on_extra_link_click(this)">Personal schedule</a></li>
+        <li><a href="javascript:void(0)" id="extra-links-personal-schedule" class="selected personal_schedule"
+               data-item-id="personal_schedule" onclick="on_extra_link_click(this)">Personal schedule</a></li>
 		{% if calendar_all_tools %}
-		<li><a href="javascript:void(0)" class="all_tools" data-item-id="all_tools" onclick="on_extra_link_click(this)">All tools</a></li>
+            <li><a href="javascript:void(0)" id="extra-links-all-tools" class="all_tools" data-item-id="all_tools"
+                   onclick="on_extra_link_click(this)">All tools</a></li>
 		{% endif %}
 		{% if calendar_all_areas %}
-		<li><a href="javascript:void(0)" class="all_areas" data-item-id="all_areas" onclick="on_extra_link_click(this)">All areas</a></li>
+            <li><a href="javascript:void(0)" id="extra-links-all-areas" class="all_areas" data-item-id="all_areas"
+                   onclick="on_extra_link_click(this)">All areas</a></li>
 		{% endif %}
 		{% if calendar_all_areastools %}
-		<li><a href="javascript:void(0)" class="all_areastools" data-item-id="all_areastools" onclick="on_extra_link_click(this)">All areas and tools</a></li>
+            <li><a href="javascript:void(0)" id="extra-links-all-areastools" class="all_areastools"
+                   data-item-id="all_areastools" onclick="on_extra_link_click(this)">All areas and tools</a></li>
 		{% endif %}
 	</ul>
     <button style="width: 100%" id="clear-selection-btn" class="btn btn-default" onclick="on_clear_selection_click();">
@@ -88,7 +95,7 @@
     let item_event_source_list = [];
     let url = "{% url 'event_feed' %}";
 
-	if(event_type === 'reservations' || event_type === '{{ facility_name|lower }} usage')
+    if (event_type === 'reservations' || event_type === 'configuration agenda' || event_type === '{{ facility_name|lower }} usage')
 	{
 		let item = get_selected_item();
 		let scheduled_outage_button = $(".fc-scheduledOutage-button")[0];
@@ -232,6 +239,9 @@
 		element.attr('title', event.tooltip)
 	else if ('title' in event)
 		element.attr('title', event.title)
+    if ("colors" in event) {
+        set_multiple_colors($(element), event["colors"]);
+    }
 }
 
 function event_after_all_render()
@@ -651,9 +661,13 @@
 	set_event_type(calling_element.text);
 	let expand_collapse = $("#expand-collapse").hide();
 	let item_search = $("#item_search").hide();
-	let item_tree = $(".item_tree, .item_tree_fieldset").hide();
+    let item_tree_tools = $("#item-tree-tool");
+    let item_tree_areas = $("#item-tree-area");
 	let event_type = get_event_type();
-	let extra_links = $("#extra-links");
+    let extra_links_personal_schedule = $("#extra-links-personal-schedule");
+    let extra_links_all_tools = $("#extra-links-all-tools");
+    let extra_links_all_areas = $("#extra-links-all-areas");
+    let extra_links_all_areastools = $("#extra-links-all-areastools");
 	let user_search = $("#user_search").hide();
 	let chosen_user_button = $("#chosen_user_button").show();
 	let proxy_reservation_button = $(".fc-proxyReservation-button")[0];
@@ -662,22 +676,44 @@
 	{
 		user_search.hide();
 		chosen_user_button.hide();
-		extra_links.show();
+        extra_links_personal_schedule.show();
+        extra_links_all_tools.show();
+        extra_links_all_areas.show();
+        extra_links_all_areastools.show();
 		expand_collapse.show();
 		item_search.show();
-		item_tree.show();
+        item_tree_tools.show();
+        item_tree_areas.show();
 		$(proxy_reservation_button).show();
 		$(scheduled_outage_button).show();
 		update_event_sources();
+    } else if (event_type === "configuration agenda") {
+        user_search.hide();
+        chosen_user_button.hide();
+        extra_links_personal_schedule.hide();
+        extra_links_all_tools.show();
+        extra_links_all_areas.hide();
+        extra_links_all_areastools.hide();
+        expand_collapse.show();
+        item_search.show();
+        item_tree_tools.show();
+        item_tree_areas.hide();
+        $(proxy_reservation_button).hide();
+        $(scheduled_outage_button).hide();
+        update_event_sources();
 	}
 	else if(event_type === "{{ facility_name|lower }} usage")
 	{
 		user_search.hide();
 		chosen_user_button.hide();
-		extra_links.show();
+        extra_links_personal_schedule.show();
+        extra_links_all_tools.show();
+        extra_links_all_areas.show();
+        extra_links_all_areastools.show();
 		expand_collapse.show();
 		item_search.show();
-		item_tree.show();
+        item_tree_tools.show();
+        item_tree_areas.show();
 		$(proxy_reservation_button).hide();
 		$(scheduled_outage_button).hide();
 		update_event_sources();
@@ -686,10 +722,14 @@
 	{
 		user_search.show();
 		chosen_user_button.show();
-		extra_links.hide();
+        extra_links_personal_schedule.hide();
+        extra_links_all_tools.hide();
+        extra_links_all_areas.hide();
+        extra_links_all_areastools.hide();
 		expand_collapse.hide();
 		item_search.hide();
-		item_tree.hide();
+        item_tree_tools.hide();
+        item_tree_areas.hide();
 		$(proxy_reservation_button).hide();
 		$(scheduled_outage_button).hide();
 		clear_specific_user();
diff --git a/NEMO/templates/calendar/configuration_event_feed.html b/NEMO/templates/calendar/configuration_event_feed.html
new file mode 100644
index 000000000..0c5acea24
--- /dev/null
+++ b/NEMO/templates/calendar/configuration_event_feed.html
@@ -0,0 +1,23 @@
+[
+{% for x in events %}
+    {
+    {% if all_tools and x.tool %}
+        "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }})\n{{ x.get_configuration_options_display|escapejs }}",
+    {% else %}
+        "title": "{{ x.user|escapejs }}\n{{ x.get_configuration_options_display|escapejs }}",
+    {% endif %}
+    {% if x.creator.id == x.user.id %}
+        "tooltip": "{{ x.reservation_item.name }} reservation for {{ x.user }}",
+    {% else %}
+        "tooltip": "{{ x.reservation_item.name }} reservation for {{ x.user }}, created by {{ x.creator }}",
+    {% endif %}
+    "colors": ["{{ x.get_configuration_options_colors|join:'","' }}"],
+    "id": "Reservation {{ x.id }}",
+    {# The reservation creator or staff may edit the event: #}
+    {% if user.id == x.user.id or user.is_staff %}"editable": true,{% endif %}
+    "start": "{{ x.start|date:"c" }}",
+    "end": "{{ x.get_visual_end|date:"c" }}",
+    "details_url": "{% url 'reservation_details' x.id %}"
+    }{% if not forloop.last %},{% endif %}
+{% endfor %}
+]
\ No newline at end of file
diff --git a/NEMO/templates/calendar/configuration_helper.html b/NEMO/templates/calendar/configuration_helper.html
index 8dd091959..b2b5da718 100644
--- a/NEMO/templates/calendar/configuration_helper.html
+++ b/NEMO/templates/calendar/configuration_helper.html
@@ -19,7 +19,9 @@
 		{% else %}
 			{{ config.name }}:<br>
 			{% for slot in config.range_of_configurable_items %}
-				<label for="configuration_{{ config.id }}__slot_{{ slot }}__display_order_{{ config.display_order }}">{{ config.configurable_item_name }}:</label>
+                <label for="configuration_{{ config.id }}__slot_{{ slot }}__display_order_{{ config.display_order }}">
+                    {% if config.configurable_item_name %}{{ config.configurable_item_name }}{% else %}
+                        {{ config.name }}{% endif %} #{{ slot|add:1 }}:</label>
 				<select class="form-control" name="configuration_{{ config.id }}__slot_{{ slot }}__display_order_{{ config.display_order }}" id="configuration_{{ config.id }}__slot_{{ slot }}__display_order_{{ config.display_order }}">
 					{% if config.absence_string %}
 						<option value="">{{ config.absence_string }}</option>
diff --git a/NEMO/templates/configuration_agenda.html b/NEMO/templates/configuration_agenda.html
index 029664cdd..62536a9e0 100644
--- a/NEMO/templates/configuration_agenda.html
+++ b/NEMO/templates/configuration_agenda.html
@@ -36,22 +36,32 @@ <h1>Configuration agenda
 					{% if t.grouper.id == tool.id %}
 						{% for reservation in t.list %}
 							<tr style="border:2px solid black">
-								<td style="width:50%;border:2px solid black" {% if reservation.short_notice %}class="danger"{% endif %}>
-									<p>
-										{{ reservation.user }}<br>
-										{% if reservation.project %}{{ reservation.project }}<br>{% endif %}
-										{{ reservation.start }}<br>
-										{{ reservation.end }}<br>
-										{{ reservation.additional_information|linebreaksbr }}
-									</p>
-									{% if not reservation.tool.in_use and reservation.project %}
-                                        {% url 'enable_tool' tool_id=reservation.tool.id user_id=reservation.user.id project_id=reservation.project.id staff_charge='false' as enable_tool_for_user_url %}
-                                        {% button type="info" icon="glyphicon-play" value="Enable the tool on behalf of "|concat:reservation.user.first_name onclick="enable_tool('"|concat:enable_tool_for_user_url|concat:"');" %}
-                                        {% if "remote_work"|customization:"remote_work_ask_explicitly" %}
-                                            <div style="margin-top: 5px">
-                                                {% button type="info" icon="glyphicon-play" value="Enable the tool on behalf of "|concat:reservation.user.first_name|concat:" for remote work" onclick="enable_tool('"|concat:enable_tool_for_user_url|concat:"', true);" %}
-                                            </div>
+                                <td style="width:50%;border:2px solid black"
+                                    {% if reservation.short_notice %}class="danger"{% endif %}>
+                                    {{ reservation.user }}<br>
+                                    {% if reservation.project %}{{ reservation.project }}<br>{% endif %}
+                                    {{ reservation.start }}<br>
+                                    {{ reservation.end }}<br>
+                                    {{ reservation.additional_information|linebreaksbr }}
+                                    {% for config_option in reservation.configurationoption_set.all %}
+                                        {% if forloop.first %}
+                                            {% if reservation.additional_information %}
+                                                <br>
+                                            {% endif %}
+                                            <br>
                                         {% endif %}
+                                        <div>{{ config_option.name }} needs to be set to <span {% if config_option.get_color %}class="configuration-reservation-option" style="display: inline-block;background-color: {{ config_option.get_color }}" {% else %}style="color: inherit"{% endif %}>{{ config_option.current_setting }}</span></div>
+                                    {% endfor %}
+									{% if not reservation.tool.in_use and reservation.project %}
+                                        <div style="margin-top: 8px">
+                                            {% url 'enable_tool' tool_id=reservation.tool.id user_id=reservation.user.id project_id=reservation.project.id staff_charge='false' as enable_tool_for_user_url %}
+                                            {% button type="info" icon="glyphicon-play" value="Enable the tool on behalf of "|concat:reservation.user.first_name onclick="enable_tool('"|concat:enable_tool_for_user_url|concat:"');" %}
+                                            {% if "remote_work"|customization:"remote_work_ask_explicitly" %}
+                                                <div style="margin-top: 5px">
+                                                    {% button type="info" icon="glyphicon-play" value="Enable the tool on behalf of "|concat:reservation.user.first_name|concat:" for remote work" onclick="enable_tool('"|concat:enable_tool_for_user_url|concat:"', true);" %}
+                                                </div>
+                                            {% endif %}
+                                        </div>
 									{% endif %}
 								</td>
 								{% if forloop.first %}
@@ -62,27 +72,31 @@ <h1>Configuration agenda
 											{% endif %}
 										{% endfor %}
 										{% if reservation.tool.in_use %}
-											{% if reservation.tool.get_current_usage_event.operator.id == user.id %}
-                                                {% url 'disable_tool' reservation.tool.id as disable_tool_url %}
-                                                {% button type="delete" id="interlock" icon="glyphicon-stop" onclick="disable_tool('"|concat:disable_tool_url|concat:"')" value="Disable tool" %}
-											{% else %}
-                                                {% button type="warn" disabled="disabled" icon="glyphicon-ban-circle" value="Tool in use" %}
-											{% endif %}
+                                            <div style="margin-top: 8px">
+                                                {% if reservation.tool.get_current_usage_event.operator.id == user.id %}
+                                                    {% url 'disable_tool' reservation.tool.id as disable_tool_url %}
+                                                    {% button type="delete" id="interlock" icon="glyphicon-stop" onclick="disable_tool('"|concat:disable_tool_url|concat:"')" value="Disable tool" %}
+                                                {% else %}
+                                                    {% button type="warn" disabled="disabled" icon="glyphicon-ban-circle" value="Tool in use" %}
+                                                {% endif %}
+                                            </div>
 										{% else %}
-											{% if user.active_project_count == 1 %}
-                                                {% url 'enable_tool' tool_id=reservation.tool.id user_id=user.id project_id=user.active_projects.0.id staff_charge='false' as enable_tool_one_project %}
-                                                {% button id="interlock" type="save" onclick="enable_tool('"|concat:enable_tool_one_project|concat:"');" value="Enable the tool for myself" icon="glyphicon-play" %}
-											{% elif user.active_project_count > 1 %}
-												<div class="btn-group">
-													<button id="interlock" type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown"><i class="glyphicon glyphicon-play"></i> Enable the tool for myself <span class="caret"></span></button>
-													<ul class="dropdown-menu">
-														<li class="dropdown-header">Using which project?</li>
-														{% for project in user.active_projects %}
-															<li><a href="javascript:void(0)" onclick="enable_tool('{% url 'enable_tool' tool_id=reservation.tool.id user_id=user.id project_id=project.id staff_charge='false' %}')">{% project_selection_display project %}</a></li>
-														{% endfor %}
-													</ul>
-												</div>
-											{% endif %}
+                                            <div style="margin-top: 8px">
+                                                {% if user.active_project_count == 1 %}
+                                                    {% url 'enable_tool' tool_id=reservation.tool.id user_id=user.id project_id=user.active_projects.0.id staff_charge='false' as enable_tool_one_project %}
+                                                    {% button id="interlock" type="save" onclick="enable_tool('"|concat:enable_tool_one_project|concat:"');" value="Enable the tool for myself" icon="glyphicon-play" %}
+                                                {% elif user.active_project_count > 1 %}
+                                                    <div class="btn-group">
+                                                        <button id="interlock" type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown"><i class="glyphicon glyphicon-play"></i> Enable the tool for myself <span class="caret"></span></button>
+                                                        <ul class="dropdown-menu">
+                                                            <li class="dropdown-header">Using which project?</li>
+                                                            {% for project in user.active_projects %}
+                                                                <li><a href="javascript:void(0)" onclick="enable_tool('{% url 'enable_tool' tool_id=reservation.tool.id user_id=user.id project_id=project.id staff_charge='false' %}')">{% project_selection_display project %}</a></li>
+                                                            {% endfor %}
+                                                        </ul>
+                                                    </div>
+                                                {% endif %}
+                                            </div>
 										{% endif %}
 									</td>
 								{% endif %}
diff --git a/NEMO/templates/consumables/consumables.html b/NEMO/templates/consumables/consumables.html
index d70d5e7e3..efbd8dcee 100644
--- a/NEMO/templates/consumables/consumables.html
+++ b/NEMO/templates/consumables/consumables.html
@@ -46,31 +46,35 @@ <h1>Withdraw consumables</h1>
             <div class="form-group">
                 <div class="col-sm-12">
                     <table class="table">
-                        {% regroup consumables by category as categories %}
-                        {% for category in categories %}
-                            {% if categories|length > 1 or category.grouper %}
-                                <tr><th>{{ category.grouper|default_if_none:"Uncategorized" }}</th></tr>
-                            {% endif %}
-                            {% for item in category.list %}
-                                <tr><td {% if categories|length > 1 or category.grouper %}style="padding-left: 15px;"{% endif %}>
-                                    <div class="consumable-row">
-                                        {% if item.notes %}<span data-toggle="notes-tooltip" title="{{ item.notes|linebreaksbr }}" class="glyphicon glyphicon-info-sign primary-highlight"></span>{% endif %}
-                                        <span class="consumable_trigger" data-item-id="{{ item.id }}">{{ item.name }}{% if rates and rates|get_item:item.name %} ({{ rates|get_item:item.name|safe }}){% endif %}
-                                            {% if not item.reusable %}
-                                                {% if item.quantity > 0 %}
-                                                    <span>({{ item.quantity }} left)</span>
-                                                {% else %}
-                                                    <span class="danger-highlight">(sold-out)</span>
-                                                {% endif %}
-                                            {% endif %}
-                                            {% if item.reusable or item.quantity > 0 %}
-                                                <span class="glyphicon glyphicon-plus-sign success-highlight" style="vertical-align: text-top"></span>
-                                            {% endif %}
-                                        </span>
-                                    </div>
-                                </td></tr>
+                        {% with category_collapse="application"|customization:"consumable_category_collapse" %}
+                            {% regroup consumables by category as categories %}
+                            {% for category in categories %}
+                                {% if categories|length > 1 or category.grouper %}
+                                    <tr onclick="toggle_details('#chevron_{{ category.grouper.id }}')" data-toggle="collapse" data-target="#category_{{ category.grouper.id }}_details"><th id="chevron_{{ category.grouper.id }}"><span class="glyphicon glyphicon-chevron-{% if category_collapse %}right{% else %}down{% endif %} pull-left chevron"></span>{{ category.grouper|default_if_none:"Uncategorized" }}</th></tr>
+                                {% endif %}
+                                <tbody id="category_{{ category.grouper.id }}_details" class="collapse {% if not category_collapse %}in{% endif %}">
+                                    {% for item in category.list %}
+                                        <tr><td {% if categories|length > 1 or category.grouper %}style="padding-left: 45px;"{% endif %}>
+                                            <div class="consumable-row">
+                                                {% if item.notes %}<span data-toggle="notes-tooltip" title="{{ item.notes|linebreaksbr }}" class="glyphicon glyphicon-info-sign primary-highlight"></span>{% endif %}
+                                                <span class="consumable_trigger" data-item-id="{{ item.id }}">{{ item.name }}{% if rates and rates|get_item:item.name %} ({{ rates|get_item:item.name|safe }}){% endif %}
+                                                    {% if not item.reusable %}
+                                                        {% if item.quantity > 0 %}
+                                                            <span>({{ item.quantity }} left)</span>
+                                                        {% else %}
+                                                            <span class="danger-highlight">(sold-out)</span>
+                                                        {% endif %}
+                                                    {% endif %}
+                                                    {% if item.reusable or item.quantity > 0 %}
+                                                        <span class="glyphicon glyphicon-plus-sign success-highlight" style="vertical-align: text-top"></span>
+                                                    {% endif %}
+                                                </span>
+                                            </div>
+                                        </td></tr>
+                                    {% endfor %}
+                                </tbody>
                             {% endfor %}
-                        {% endfor %}
+                        {% endwith %}
                     </table>
                 </div>
             </div>
@@ -137,7 +141,7 @@ <h1>Withdraw consumables</h1>
 		}
         window.addEventListener("load", function ()
         {
-			$('[data-toggle="notes-tooltip"]').tooltip({"html": true});
+			$('[data-toggle="notes-tooltip"]').tooltip({"html": true, container: 'body'});
             $('#customer_search').autocomplete('users', fetch_projects, {{ users|json_search_base }}).focus();
             let withdrawal_order_form = $('#withdrawal_form_order');
             withdrawal_order_form.on('shown.bs.tooltip', function(e)
@@ -146,7 +150,7 @@ <h1>Withdraw consumables</h1>
             });
             $(document).on('click', function(e)
             {
-                {# Dimiss tooltip when clicking on everything outside the tooltip itself #}
+                {# Dismiss tooltip when clicking on everything outside the tooltip itself #}
                 if($(e.target).closest(".tooltip").length === 0 )
                 {
                     $('#withdrawal_form_order .tooltip').remove();
diff --git a/NEMO/templates/customizations/customizations_application.html b/NEMO/templates/customizations/customizations_application.html
index a27e04657..a1b036430 100644
--- a/NEMO/templates/customizations/customizations_application.html
+++ b/NEMO/templates/customizations/customizations_application.html
@@ -51,6 +51,14 @@ <h3 class="customization-section-title">Application settings</h3>
             <div class="col-md-offset-3 col-md-9 help-block light-grey">
                 If enabled, you need to add a {% if "django.contrib.admin"|app_installed %}<a href="{% url 'admin:NEMO_landingpagechoice_changelist' %}">landing page choice</a>{% else %}landing page option{% endif %} with url <b>/consumables/</b> so users can have a way to get to the page.
             </div>
+            <div class="col-md-offset-3 col-md-9">
+                <div class="checkbox">
+                    <label><input type="checkbox" id="consumable_category_collapse" name="consumable_category_collapse" {% if consumable_category_collapse %}checked{% endif %} value="enabled"> Collapse consumable categories by default</label>
+                </div>
+            </div>
+            <div class="col-md-offset-3 col-md-9 help-block light-grey">
+                Check this box to collapse consumable categories by default.
+            </div>
         </div>
 		{% if "NEMO.apps.area_access"|app_installed or "NEMO.apps.kiosk"|app_installed and badge_readers %}
 			<div class="form-group">
@@ -69,6 +77,53 @@ <h3 class="customization-section-title">Application settings</h3>
 			</div>
 		{% endif %}
         <div class="customization-separation" style="margin-bottom: 15px"></div>
+        <h3 class="customization-section-title">Page title settings</h3>
+        <div class="form-group">
+            <label class="control-label col-md-3" for="calendar_page_title">Calendar</label>
+            <div class="col-md-4">
+                <input type="text" id="calendar_page_title" name="calendar_page_title" class="form-control" value="{{ calendar_page_title }}">
+            </div>
+            <div class="col-md-5 help-block light-grey">
+                The title of the "Calendar" page and its corresponding link in the navbar.
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-md-3" for="tool_control_page_title">Tool control</label>
+            <div class="col-md-4">
+                <input type="text" id="tool_control_page_title" name="tool_control_page_title" class="form-control" value="{{ tool_control_page_title }}">
+            </div>
+            <div class="col-md-5 help-block light-grey">
+                The title of the "Tool control" page and its corresponding link in the navbar.
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-md-3" for="status_dashboard_page_title">Status dashboard</label>
+            <div class="col-md-4">
+                <input type="text" id="status_dashboard_page_title" name="status_dashboard_page_title" class="form-control" value="{{ status_dashboard_page_title }}">
+            </div>
+            <div class="col-md-5 help-block light-grey">
+                The title of the "Status dashboard" page and its corresponding link in the navbar.
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-md-3" for="requests_page_title">Requests</label>
+            <div class="col-md-4">
+                <input type="text" id="requests_page_title" name="requests_page_title" class="form-control" value="{{ requests_page_title }}">
+            </div>
+            <div class="col-md-5 help-block light-grey">
+                The title of the "Requests" page and its corresponding link in the navbar.
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-md-3" for="safety_page_title">Safety</label>
+            <div class="col-md-4">
+                <input type="text" id="safety_page_title" name="safety_page_title" class="form-control" value="{{ safety_page_title }}">
+            </div>
+            <div class="col-md-5 help-block light-grey">
+                The title of the "Safety" page and its corresponding link in the navbar.
+            </div>
+        </div>
+        <div class="customization-separation" style="margin-bottom: 15px"></div>
         <div class="text-center">
             {% button type="save" value="Save settings" %}
         </div>
diff --git a/NEMO/templates/customizations/customizations_projects_and_accounts.html b/NEMO/templates/customizations/customizations_projects_and_accounts.html
index 870f35ce7..b433c5d41 100644
--- a/NEMO/templates/customizations/customizations_projects_and_accounts.html
+++ b/NEMO/templates/customizations/customizations_projects_and_accounts.html
@@ -4,14 +4,14 @@ <h3 class="customization-section-title">Projects & accounts</h3>
     <form method="POST" action="{% url 'customize' 'projects_and_accounts' %}" class="form-horizontal">
         {% csrf_token %}
         <div class="form-group" style="margin-bottom: 0">
-            <label class="control-label col-md-3" for="account_list_active_only">Account list</label>
+            <label class="control-label col-md-3" for="account_list_active_only">Projects/accounts list</label>
             <div class="col-md-9">
                 <div class="checkbox">
                     <label><input type="checkbox" id="account_list_active_only" name="account_list_active_only" {% if account_list_active_only %}checked{% endif %} value="enabled"> Hide inactive accounts</label>
                 </div>
             </div>
             <div class="col-md-offset-3 col-md-9 help-block light-grey">
-                Check this box to only show active accounts in the user list.
+                Check this box to only show active accounts in the account list.
             </div>
         </div>
         <div class="form-group" style="margin-bottom: 0">
@@ -21,7 +21,7 @@ <h3 class="customization-section-title">Projects & accounts</h3>
                 </div>
             </div>
             <div class="col-md-offset-3 col-md-9 help-block light-grey">
-                Check this box to only show active projects in the user list.
+                Check this box to only show active projects in the project list.
             </div>
         </div>
         <div class="form-group">
@@ -33,6 +33,21 @@ <h3 class="customization-section-title">Projects & accounts</h3>
             <div class="col-md-offset-3 col-md-9 help-block light-grey">
                 Check this box to collapse account projects by default.
             </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-md-3" for="project_allow_transferring_charges">Transfer charges</label>
+            <div class="col-md-8">
+                <div class="checkbox">
+                    <label><input type="checkbox" id="project_allow_transferring_charges"
+                                  name="project_allow_transferring_charges"
+                                  {% if project_allow_transferring_charges %}checked{% endif %} value="enabled"> Allow
+                        accounting officers and managers to transfer charges to another project</label>
+                </div>
+            </div>
+            <div class="col-md-offset-3 col-md-8 help-block light-grey">
+                Check this box to enable the transfer charges feature (available in the Administration menu or <a
+                    href="{% url 'transfer_charges' %}">here</a>).
+            </div>
         </div>
 		<div class="form-group">
 			<label class="control-label col-md-3" for="project_allow_pi_manage_users">Project PIs</label>
diff --git a/NEMO/templates/customizations/customizations_tool.html b/NEMO/templates/customizations/customizations_tool.html
index bde278f26..d82964aa1 100644
--- a/NEMO/templates/customizations/customizations_tool.html
+++ b/NEMO/templates/customizations/customizations_tool.html
@@ -66,7 +66,7 @@ <h3 class="customization-section-title">Tool problems</h3>
             <label class="control-label col-sm-4 col-md-3">New problems</label>
             <div class="col-sm-8 col-md-9">
                 <div class="checkbox">
-                    <label><input type="checkbox" name="tool_problem_send_to_all_qualified_users" {% if tool_problem_send_to_all_qualified_users %}checked{% endif %} value="enabled">Send new tool problems to all qualified users</label>
+                    <label><input type="checkbox" name="tool_problem_send_to_all_qualified_users" {% if tool_problem_send_to_all_qualified_users %}checked{% endif %} value="enabled">Send new tool problems to all qualified users (but not updates)</label>
                 </div>
             </div>
         </div>
@@ -75,7 +75,8 @@ <h3 class="customization-section-title">Tool problems</h3>
             <div class="col-sm-8 col-md-9">
                 <div class="checkbox">
                     <input type="hidden" name="tool_task_updates_facility_managers" value="off"/>
-                    <label><input type="checkbox" name="tool_task_updates_facility_managers" {% if tool_task_updates_facility_managers != 'off' %}checked{% endif %} value="">Send all tool problem updates to facility managers</label>
+                    <label><input type="checkbox" name="tool_task_updates_facility_managers" {% if tool_task_updates_facility_managers != 'off' %}checked{% endif %} value="">Send all tool problem & updates to facility managers</label><br>
+                    <label><input type="checkbox" name="tool_task_updates_superusers" {% if tool_task_updates_superusers %}checked{% endif %} value="enabled">Send all tool problem & updates to tool superusers</label>
                 </div>
             </div>
         </div>
diff --git a/NEMO/templates/event_details/outage_details.html b/NEMO/templates/event_details/outage_details.html
index 69077716b..d9f14987b 100644
--- a/NEMO/templates/event_details/outage_details.html
+++ b/NEMO/templates/event_details/outage_details.html
@@ -1,5 +1,6 @@
 {% extends popup_view|yesno:"base/popup.html,base.html" %}
 {% load custom_tags_and_filters %}
+{% load tz %}
 
 {% if not popup_view %}
 {% block extrahead %}
@@ -44,50 +45,120 @@
     {% if not popup_view %}
         <h1>Scheduled outage details</h1>
     {% endif %}
-    {% if user.is_staff %}
-        <label for="outage_title" style="margin-bottom: 0">Title: </label>
-        <div id="outage_display_title" style="display: inline">
-            {{ outage.title }}
-            <span class="glyphicon glyphicon-pencil pull-right" title="Change the outage title" onclick="$('#outage_display_title').hide();$('#outage_change_title').css('display', 'inline');"></span>
-        </div>
-        <div id="outage_change_title" style="display: none">
-            <div class="input-group" style="display: inline-flex; vertical-align: middle">
-                <input type="text" id="outage_title" class="form-control" value="{{ outage.title }}">
-                <span class="input-group-btn">
-                    <button type="button" class="btn btn-success" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_title('{% url 'change_outage_title' outage.id %}', $('#outage_title').val()) {% if popup_view %}}){% endif %}">Change title</button>
-                </span>
-            </div>
-        </div>
-        <br>
-    {% else %}
-        Title: {{ outage.title }}<br>
-    {% endif %}
-    Created by: {{ outage.creator }}<br>
-    {% if outage.tool %}Tool: {{ outage.tool }}<br>{% endif %}
-    {% if outage.resource %}Resource: {{ outage.resource }} <span class="light-grey">({{ outage.resource.category }})</span><br>{% endif %}
-    {% if outage.category %}Outage category: {{ outage.category }}<br>{% endif %}
-    Start: {{ outage.start }}<br>
-    End: {{ outage.end }}<br>
-    Identifier: {{ outage.id }}<br>
-    {% if outage.details or user.is_staff %}
+    <table class="table table-striped" style="margin-bottom: 0">
         {% if user.is_staff %}
-            <label for="outage_details" style="margin-bottom: 0">Details: </label><br>
-            <div id="outage_display_details">
-                <span class="glyphicon glyphicon-pencil pull-right" title="Change the outage details" onclick="$('#outage_display_details').hide();$('#outage_change_details').show();auto_size_textarea(document.getElementById('outage_details'))"></span>
-                {{ outage.details|linebreaksbr }}
-            </div>
-            <div id="outage_change_details" style="display: none">
-                <div class="input-group" style="width: 100%;">
-                    <textarea id="outage_details" class="form-control">{{ outage.details }}</textarea>
-                    <span class="input-group-addon btn btn-success" style="color: #fff" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_details('{% url 'change_outage_details' outage.id %}', $('#outage_details').val()) {% if popup_view %}}){% endif %}">Change details</span>
-                </div>
-            </div>
-            <br>
+            <tr>
+                <td><label for="outage_title" style="margin-bottom: 0">Title:</label></td>
+                <td>
+                    <div id="outage_display_title" style="display: inline">
+                        {{ outage.title }}
+                        <span class="glyphicon glyphicon-pencil pull-right" title="Change the outage title" onclick="$('#outage_display_title').hide();$('#outage_change_title').css('display', 'inline');"></span>
+                    </div>
+                    <div id="outage_change_title" style="display: none">
+                        <div class="input-group" style="display: inline-flex; vertical-align: middle">
+                            <input type="text" id="outage_title" class="form-control" value="{{ outage.title }}">
+                            <span class="input-group-btn">
+                                <button type="button" class="btn btn-success" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_title('{% url 'change_outage_title' outage.id %}', $('#outage_title').val()) {% if popup_view %}}){% endif %}">Change title</button>
+                            </span>
+                        </div>
+                    </div>
+                </td>
+            </tr>
         {% else %}
-            Details:<br>
-            {{ outage.details|linebreaksbr }}<br>
+            <tr>
+                <td>Title:</td>
+                <td>{{ outage.title }}</td>
+            </tr>
         {% endif %}
-    {% endif %}
+        <tr>
+            <td>Created by:</td>
+            <td>{{ outage.creator }}</td>
+        </tr>
+        {% if outage.tool %}
+            <tr>
+                <td>Tool:</td>
+                <td>{{ outage.tool }}</td>
+            </tr>
+        {% endif %}
+        {% if outage.resource %}
+            <tr>
+                <td>Resource:</td>
+                <td>{{ outage.resource }} <span class="light-grey">({{ outage.resource.category }})</span></td>
+            </tr>
+        {% endif %}
+        {% if outage.category %}
+            <tr>
+                <td>Outage category:</td>
+                <td>{{ outage.category }}</td>
+            </tr>
+        {% endif %}
+        <tr>
+            {% if user.is_staff and outage.has_not_ended and outage.has_not_started %}
+                <td style="vertical-align: middle"><label for="outage_start" style="margin-bottom: 0">Start:</label></td>
+                <td id="outage_display_start">
+                    {{ outage.start }}
+                    <span class="glyphicon glyphicon-pencil pull-right" title="Change the start date" onclick="$('#outage_display_start').hide();$('#outage_change_start').show();"></span>
+                </td>
+                <td id="outage_change_start" style="display: none">
+                    <div class="input-group">
+                        <input type="text" id="outage_start" class="form-control" value="{{ outage.start|localtime|input_date_format }}">
+                        <span class="input-group-btn">
+                            <button type="button" class="btn btn-success" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_date('{% url 'change_outage_date' %}', {{ outage.id }}, 'new_start', $('#outage_start').val()) {% if popup_view %}}){% endif %}">Change start</button>
+                        </span>
+                    </div>
+                </td>
+            {% else %}
+                <td>Start:</td>
+                <td>{{ outage.start }}</td>
+            {% endif %}
+        </tr>
+        <tr>
+            {% if user.is_staff and outage.has_not_ended and outage.has_not_started %}
+                <td style="vertical-align: middle"><label for="outage_end" style="margin-bottom: 0">End:</label></td>
+                <td id="outage_display_end">
+                    {{ outage.end }}
+                    <span class="glyphicon glyphicon-pencil pull-right" title="Change the end date" onclick="$('#outage_display_end').hide();$('#outage_change_end').show();"></span>
+                </td>
+                <td id="outage_change_end" style="display: none">
+                    <div class="input-group">
+                        <input type="text" id="outage_end" class="form-control" value="{{ outage.end|localtime|input_date_format }}">
+                        <span class="input-group-btn">
+                            <button type="button" class="btn btn-success" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_date('{% url 'change_outage_date' %}', {{ outage.id }}, 'new_end', $('#outage_end').val()) {% if popup_view %}}){% endif %}">Change end</button>
+                        </span>
+                    </div>
+                </td>
+            {% else %}
+                <td>End:</td>
+                <td>{{ outage.end }}</td>
+            {% endif %}
+        </tr>
+        <tr>
+            <td>Identifier:</td>
+            <td>{{ outage.id }}</td>
+        </tr>
+        {% if outage.details or user.is_staff %}
+            {% if user.is_staff %}
+                <tr>
+                    <td><label for="outage_details" style="margin-bottom: 0">Details:</label></td>
+                    <td id="outage_display_details">
+                        <span class="glyphicon glyphicon-pencil pull-right" title="Change the outage details" onclick="$('#outage_display_details').hide();$('#outage_change_details').show();auto_size_textarea(document.getElementById('outage_details'))"></span>
+                        {{ outage.details|linebreaksbr }}
+                    </td>
+                    <td id="outage_change_details" style="display: none">
+                        <div class="input-group" style="width: 100%;">
+                            <textarea id="outage_details" class="form-control" oninput="auto_size_textarea(this);">{{ outage.details }}</textarea>
+                            <span class="input-group-addon btn btn-success" style="color: #fff" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_details('{% url 'change_outage_details' outage.id %}', $('#outage_details').val()) {% if popup_view %}}){% endif %}">Change details</span>
+                        </div>
+                    </td>
+                </tr>
+            {% else %}
+                <tr>
+                    <td>Details:</td>
+                    <td>{{ outage.details|linebreaksbr }}</td>
+                </tr>
+            {% endif %}
+        {% endif %}
+    </table>
 
     {# Allow the user to cancel the outage if they have that privilege. only in popup view #}
     {% if user.is_staff %}
diff --git a/NEMO/templates/event_details/reservation_details.html b/NEMO/templates/event_details/reservation_details.html
index 7c3e625c5..715eae796 100644
--- a/NEMO/templates/event_details/reservation_details.html
+++ b/NEMO/templates/event_details/reservation_details.html
@@ -62,7 +62,7 @@ <h1>{{ reservation.reservation_item }} reservation details</h1>
 		<div class="alert alert-danger">This reservation was automatically removed from the calendar due to {{ reservation.reservation_item_type.value }} inactivity.</div>
 	{% endif %}
 
-    <table class="table table-striped">
+    <table class="table table-striped" style="margin-bottom: 0">
 
 	{% if user.is_staff %}
     <tr>
@@ -208,6 +208,16 @@ <h1>{{ reservation.reservation_item }} reservation details</h1>
             <td>{{ reservation.user }} has opted to perform the tool configuration</td>
         </tr>
     {% endif %}
+        {% if reservation.configurationoption_set.exists %}
+            <tr>
+                <td>Configuration:</td>
+                <td>
+                    {% for config_option in reservation.configurationoption_set.all %}
+                        {{ config_option.name }}: {% if config_option.get_color %}<span class="configuration-reservation-option" style="background-color: {{ config_option.get_color }}">{{ config_option.current_setting }}</span>{% else %}{{ config_option.current_setting }}{% endif %}<br>
+                    {% endfor %}
+                </td>
+            </tr>
+        {% endif %}
     {% if reservation.additional_information %}
         <tr>
             <td>Additional information:</td>
diff --git a/NEMO/templates/mobile/individual_reservation.html b/NEMO/templates/mobile/individual_reservation.html
index d173b2ebc..8b458a6d8 100644
--- a/NEMO/templates/mobile/individual_reservation.html
+++ b/NEMO/templates/mobile/individual_reservation.html
@@ -20,7 +20,15 @@
 				<strong>This reservation was created on short notice. Tool configuration is not guaranteed upon start time.</strong><br>
 			{% endif %}
 		{% endif %}
+        {% if reservation.configurationoption_set.exists %}
+            <div style="height:10px">{# Vertical spacer #}</div>
+            Configuration:<br>
+            {% for config_option in reservation.configurationoption_set.all %}
+                {{ config_option.name }}: {% if config_option.get_color %}<span class="configuration-reservation-option" style="background-color: {{ config_option.get_color }}">{{ config_option.current_setting }}</span>{% else %}{{ config_option.current_setting }}{% endif %}<br>
+            {% endfor %}
+        {% endif %}
 		{% if reservation.additional_information %}
+            <div style="height:10px">{# Vertical spacer #}</div>
 			Additional information:<br>
 			{{ reservation.additional_information|linebreaksbr }}
 		{% endif %}
diff --git a/NEMO/templates/requests/user_requests.html b/NEMO/templates/requests/user_requests.html
index f78445960..9d2744bcf 100644
--- a/NEMO/templates/requests/user_requests.html
+++ b/NEMO/templates/requests/user_requests.html
@@ -1,5 +1,5 @@
 {% extends 'base.html' %}
-{% block title %}Requests{% endblock %}
+{% block title %}{{ requests_page_title }}{% endblock %}
 {% block content %}
     <div class="clearfix" style="margin-top: 0;">
         <ul class="nav nav-pills nav-pills-spacer" id="tabs" style="display: inline-block;float: right; margin-bottom: 15px">
diff --git a/NEMO/templates/resources/scheduled_outage.html b/NEMO/templates/resources/scheduled_outage.html
index 3bc4a4ecb..2486e08ac 100644
--- a/NEMO/templates/resources/scheduled_outage.html
+++ b/NEMO/templates/resources/scheduled_outage.html
@@ -68,7 +68,6 @@ <h4>{% if editing %}Edit this outage{% else %}Create a new {{ resource }} outage
             format: "{{ datetime_input_js_format }}",
 			sideBySide: true
 		};
-		$('#start').datetimepicker(timepicker_properties);
-		$('#end').datetimepicker(timepicker_properties);
+        set_start_end_datetime_pickers($('#start'), $('#end'), timepicker_properties);
 	</script>
 {% endblock %}
\ No newline at end of file
diff --git a/NEMO/templates/safety/safety.html b/NEMO/templates/safety/safety.html
index 1dba3df54..2dc3f3081 100644
--- a/NEMO/templates/safety/safety.html
+++ b/NEMO/templates/safety/safety.html
@@ -1,7 +1,7 @@
 {% extends 'safety/safety_base.html' %}
 {% load custom_tags_and_filters %}
-{% block title %}Safety{% endblock %}
-{% block safety_title %}Safety{% endblock %}
+{% block title %}{{ safety_page_title }}{% endblock %}
+{% block safety_title %}{{ safety_page_title }}{% endblock %}
 {% block tab_content %}
     {% if safety_categories or safety_items %}
         <div class="row" style="margin-bottom: 25px">
diff --git a/NEMO/templates/status_dashboard/status_dashboard.html b/NEMO/templates/status_dashboard/status_dashboard.html
index 75bf75fde..c392d5d7c 100644
--- a/NEMO/templates/status_dashboard/status_dashboard.html
+++ b/NEMO/templates/status_dashboard/status_dashboard.html
@@ -1,5 +1,5 @@
 {% extends 'base.html' %}
-{% block title %}Status dashboard{% endblock %}
+{% block title %}{{ status_dashboard_page_title }}{% endblock %}
 {% load static %}
 {% block extrahead %}
     {# We need this for staff absence form popup #}
@@ -8,7 +8,7 @@
 {% endblock %}
 {% block content %}
 
-<h1 class="pull-left" style="margin-right:20px; margin-top:0; margin-bottom:0">Status dashboard</h1>
+<h1 class="pull-left" style="margin-right:20px; margin-top:0; margin-bottom:0">{{ status_dashboard_page_title }}</h1>
 <ul class="nav nav-pills" id="tabs" {% if no_header %}style="display:none"{% endif %}>
     {% if areas_exist %}<li {% if tab == 'occupancy' %}class="active"{% endif %}><a href="#occupancy">Area occupancy</a></li>{% endif %}
     {% if tools_exist %}<li {% if tab == 'tools' %}class="active"{% endif %}><a href="#tools">Tool status & usage</a></li>{% endif %}
@@ -64,7 +64,7 @@ <h1 class="pull-left" style="margin-right:20px; margin-top:0; margin-bottom:0">S
         {# Set a global click handler to dismiss manual tooltips #}
         $('html').on('click', function(e)
         {
-            {# Only dimiss tooltips when clicking outside of them #}
+            {# Only dismiss tooltips when clicking outside of them #}
             let target_id = $(e.target).parents('.tooltip').attr('id');
             if (typeof $(e.target).data('original-title') == 'undefined')
             {
diff --git a/NEMO/templates/status_dashboard/tools.html b/NEMO/templates/status_dashboard/tools.html
index a1b9a442a..3ce9ca054 100644
--- a/NEMO/templates/status_dashboard/tools.html
+++ b/NEMO/templates/status_dashboard/tools.html
@@ -1,3 +1,10 @@
+<p>
+    Staff members are highlighted in <span class="bg-success">&nbsp;green&nbsp;</span><br>
+    Service personnel are highlighted in <span class="bg-warning">&nbsp;orange&nbsp;</span><br>
+    {% if reservations_can_expire %}
+    Users with expired reservations are highlighted in <span class="bg-danger">&nbsp;red&nbsp;</span>
+    {% endif %}
+</p>
 <table class="table table-bordered table-condensed">
 	<thead>
 	<tr>
diff --git a/NEMO/templates/tool_control/config_history.html b/NEMO/templates/tool_control/config_history.html
new file mode 100644
index 000000000..d2c5610a0
--- /dev/null
+++ b/NEMO/templates/tool_control/config_history.html
@@ -0,0 +1,16 @@
+{% for config in configs %}
+    <div class="media" style="margin-top: 15px">
+        <a onclick="toggle_details(this)" data-toggle="collapse" data-target="#config_{{ forloop.counter }}_details"
+           class="pointer">
+            <span class="glyphicon pull-left chevron glyphicon-chevron-right"></span>
+        </a>
+        <div class="media-body">
+            <div class="media-heading">On <i
+                    style="font-size: small">{{ config.modification_time|default:'Current' }}</i> {{ config.user }}
+                changed the {{ config.name }} configuration
+            </div>
+            <div id="config_{{ forloop.counter }}_details" class="media-middle collapse"
+                 style="margin-top: 10px">{{ config.html }}</div>
+        </div>
+    </div>
+{% endfor %}
\ No newline at end of file
diff --git a/NEMO/templates/tool_control/tool_control.html b/NEMO/templates/tool_control/tool_control.html
index 2eaf0989e..32613ff92 100644
--- a/NEMO/templates/tool_control/tool_control.html
+++ b/NEMO/templates/tool_control/tool_control.html
@@ -1,6 +1,6 @@
 {% extends 'base.html' %}
 {% load custom_tags_and_filters %}
-{% block title %}Tool control{% endblock %}
+{% block title %}{{ tool_control_page_title }}{% endblock %}
 {% block extrahead %}
 	{% load static %}
 	<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
@@ -318,6 +318,11 @@
 			}
 		}
 
+        function load_config_history(tool_id) {
+            let failure_dialog = ajax_complete_callback("Unable to find the configuration history", "There was a problem looking up the configuration history.");
+            $("#config").load("{% url 'tool_config_history' 999 %}".replace('999', tool_id), undefined, failure_dialog);
+        }
+
 		function refresh_sidebar_tool_icons()
 		{
 		    $.getScript('{% url 'refresh_sidebar_icons' item_type='tool' %}')
diff --git a/NEMO/templates/tool_control/tool_status.html b/NEMO/templates/tool_control/tool_status.html
index 087b21eb7..a32ff7783 100644
--- a/NEMO/templates/tool_control/tool_status.html
+++ b/NEMO/templates/tool_control/tool_status.html
@@ -6,7 +6,10 @@ <h1>{{ tool.name_or_child_in_use_name }}</h1>
             <ul class="nav nav-pills" id="tabs" style="margin-bottom:10px">
                 <li style="width:48%; text-align:center" class="active"><a href="#summary" style="padding: 10px 5px">Summary</a></li>
                 <li style="width:48%; text-align:center"><a href="#details" style="padding: 10px 5px">Details</a></li>
-                {% if configs %}<li style="width:48%; text-align:center"><a href="#config" style="padding: 10px 5px">Config History</a></li>{% endif %}
+                {% if tool.configuration_set.exists %}
+                    <li style="width:48%; text-align:center"><a href="#config" style="padding: 10px 5px"
+                                                                onclick="load_config_history('{{ tool.id }}');">Config
+                        History</a></li>{% endif %}
                 {% if tool.post_usage_questions and not hide_data_history or tool.post_usage_questions and user.is_any_part_of_staff %}<li style="width: 48%; text-align: center"><a href="#usage_data" style="padding: 10px 5px;" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a></li>{% endif %}
                 <li style="width:48%; text-align:center"><a href="#problem" style="padding: 10px 5px">Report a problem</a></li>
                 <li style="width:48%; text-align:center"><a href="#comment" style="padding: 10px 5px">Post a comment</a></li>
@@ -16,7 +19,9 @@ <h1 class="pull-left" style="margin-right:20px; margin-top:0; margin-bottom:10px
             <ul class="nav nav-pills" id="tabs">
                 <li class="active"><a href="#summary">Summary</a></li>
                 <li><a href="#details">Details</a></li>
-                {% if configs %}<li><a href="#config">Config History</a></li>{% endif %}
+                {% if tool.configuration_set.exists %}
+                    <li><a href="#config" onclick="load_config_history('{{ tool.id }}');">Config History</a>
+                    </li>{% endif %}
                 {% if tool.post_usage_questions and not hide_data_history or tool.post_usage_questions and user.is_any_part_of_staff %}<li><a href="#usage_data" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a></li>{% endif %}
                 <li><a href="#problem">Report a problem</a></li>
                 <li><a href="#comment">Post a comment</a></li>
@@ -542,17 +547,7 @@ <h4 class="media-heading">Task &amp; comment history for this tool</h4>
 	</div>
 
 	<div class="tab-pane" id="config">
-		{% for config in configs %}
-			<div class="media" style="margin-top: 15px">
-				<a onclick="toggle_details(this)" data-toggle="collapse" data-target="#config_{{ forloop.counter }}_details" class="pointer">
-					<span class="glyphicon pull-left chevron glyphicon-chevron-right"></span>
-				</a>
-				<div class="media-body">
-					<div class="media-heading">On <i style="font-size: small">{{ config.modification_time|default:'Current' }}</i> {{ config.user }} changed the configuration</div>
-					<div id="config_{{ forloop.counter }}_details" class="media-middle collapse" style="margin-top: 10px">{{ config.html }}</div>
-				</div>
-			</div>
-		{% endfor %}
+        {# the data to be loaded here is in tool_control/config_history.html #}
 	</div>
 
 	<div class="tab-pane" id="usage_data">
@@ -636,13 +631,15 @@ <h4 class="media-heading">Task &amp; comment history for this tool</h4>
 			<div class="form-group">
 				<label class="form-inline">
 					How many days would you like the comment to be visible?
-					<select name="expiration" class="form-control" style="width:150px">
+					<select name="expiration" class="form-control" style="width:250px" required>
+						<option value="" disabled selected></option>
 						<option value="1">1 day</option>
 						<option value="7">7 days</option>
-						<option value="30" selected>30 days</option>
+						<option value="30">30 days</option>
 						<option value="60">60 days</option>
 						<option value="90">90 days</option>
-						<option value="0">Indefinitely</option>
+						<option value="-1">Indefinitely</option>
+						{% if user.is_any_part_of_staff %}<option value="0">Not visible</option>{% endif %}
 					</select>
 				</label>
 			</div>
diff --git a/NEMO/urls.py b/NEMO/urls.py
index 7e763be3a..26dc1d573 100644
--- a/NEMO/urls.py
+++ b/NEMO/urls.py
@@ -77,6 +77,7 @@ def sort_urls(url_path):
 router.register(r"area_access_records", api.AreaAccessRecordViewSet)
 router.register(r"areas", api.AreaViewSet)
 router.register(r"billing", api.BillingViewSet, basename="billing")
+router.register(r"configurations", api.ConfigurationViewSet)
 router.register(r"consumable_categories", api.ConsumableCategoryViewSet)
 router.register(r"consumable_withdrawals", api.ConsumableWithdrawViewSet)
 router.register(r"consumables", api.ConsumableViewSet)
@@ -85,6 +86,7 @@ def sort_urls(url_path):
 router.register(r"projects", api.ProjectViewSet)
 router.register(r"qualifications", api.QualificationViewSet)
 router.register(r"reservations", api.ReservationViewSet)
+router.register(r"reservation_configuration_options", api.ConfigurationOptionViewSet)
 router.register(r"resources", api.ResourceViewSet)
 router.register(r"scheduled_outages", api.ScheduledOutageViewSet)
 router.register(r"staff_charges", api.StaffChargeViewSet)
@@ -156,6 +158,7 @@ def sort_urls(url_path):
         name="enable_tool",
     ),
     path("disable_tool/<int:tool_id>/", tool_control.disable_tool, name="disable_tool"),
+    path("tool_config_history/<int:tool_id>/", tool_control.tool_config_history, name="tool_config_history"),
     path("usage_data_history/<int:tool_id>/", tool_control.usage_data_history, name="usage_data_history"),
     path("past_comments_and_tasks/", tool_control.past_comments_and_tasks, name="past_comments_and_tasks"),
     path(
@@ -237,6 +240,7 @@ def sort_urls(url_path):
     path("change_outage_title/<int:outage_id>/", calendar.change_outage_title, name="change_outage_title"),
     path("change_outage_details/<int:outage_id>/", calendar.change_outage_details, name="change_outage_details"),
     path("change_reservation_date/", calendar.change_reservation_date, name="change_reservation_date"),
+    path("change_outage_date/", calendar.change_outage_date, name="change_outage_date"),
     path(
         "change_reservation_project/<int:reservation_id>/",
         calendar.change_reservation_project,
@@ -579,6 +583,12 @@ def sort_urls(url_path):
             accounts_and_projects.add_document_to_project,
             name="add_document_to_project",
         ),
+        path("projects/transfer_charges/", accounts_and_projects.transfer_charges, name="transfer_charges"),
+        path(
+            "projects/search_project_for_transfer/",
+            accounts_and_projects.search_project_for_transfer,
+            name="search_project_for_transfer",
+        ),
         # Account, project, and user history
         re_path(r"^history/(?P<item_type>account|project|user)/(?P<item_id>\d+)/$", history.history, name="history"),
         # Site customization:
diff --git a/NEMO/utilities.py b/NEMO/utilities.py
index 6c746c5ef..b8e71749e 100644
--- a/NEMO/utilities.py
+++ b/NEMO/utilities.py
@@ -246,9 +246,9 @@ def month_list(since=datetime(year=2013, month=11, day=1)):
     return result
 
 
-def get_month_timeframe(date=None):
-    if date:
-        start = parse(date)
+def get_month_timeframe(date_str: str = None):
+    if date_str:
+        start = parse(date_str)
     else:
         start = timezone.now()
     first_of_the_month = localize(datetime(start.year, start.month, 1))
diff --git a/NEMO/validators.py b/NEMO/validators.py
new file mode 100644
index 000000000..28296cac2
--- /dev/null
+++ b/NEMO/validators.py
@@ -0,0 +1,20 @@
+import re
+
+from django.core.validators import RegexValidator
+from django.utils.translation import gettext_lazy as _
+
+COLOR_HEX_RE = "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})"
+
+
+color_hex_validator = RegexValidator(
+    re.compile(r"^" + COLOR_HEX_RE + "$"),
+    _("Enter a valid hex color, eg. #000000"),
+    "invalid",
+)
+
+
+color_hex_list_validator = RegexValidator(
+    re.compile(r"^" + COLOR_HEX_RE + "(?:,\s*" + COLOR_HEX_RE + ")*$"),
+    message=_("Enter a valid hex color list, eg. #000000,#111111"),
+    code="invalid",
+)
diff --git a/NEMO/views/accounts_and_projects.py b/NEMO/views/accounts_and_projects.py
index 890f75402..d183b04ea 100644
--- a/NEMO/views/accounts_and_projects.py
+++ b/NEMO/views/accounts_and_projects.py
@@ -1,15 +1,61 @@
+from datetime import datetime
+from typing import List
+
+from django import forms
+from django.contrib import messages
 from django.contrib.auth.decorators import login_required
-from django.http import HttpResponseBadRequest, HttpResponseForbidden
+from django.core.exceptions import ValidationError
+from django.db import transaction
+from django.http import HttpResponseBadRequest, HttpResponseForbidden, QueryDict
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.translation import gettext_lazy as _
 from django.views.decorators.http import require_GET, require_POST, require_http_methods
 
-from NEMO.decorators import accounting_or_user_office_or_manager_required
+from NEMO.decorators import accounting_or_manager_required, accounting_or_user_office_or_manager_required
 from NEMO.forms import AccountForm, ProjectForm
-from NEMO.models import Account, AccountType, ActivityHistory, MembershipHistory, Project, ProjectDocuments, User
+from NEMO.models import (
+    Account,
+    AccountType,
+    ActivityHistory,
+    AreaAccessRecord,
+    ConsumableWithdraw,
+    MembershipHistory,
+    Project,
+    ProjectDocuments,
+    Reservation,
+    StaffCharge,
+    TrainingSession,
+    UsageEvent,
+    User,
+)
+from NEMO.utilities import date_input_format, queryset_search_filter
+from NEMO.views.api_billing import BillableItem, BillingFilterForm, get_billing_charges
 from NEMO.views.customization import ProjectsAccountsCustomization
 from NEMO.views.pagination import SortedPaginator
 
 
+class ProjectTransferForm(BillingFilterForm):
+    end = forms.DateField(required=False)
+    project_id = forms.IntegerField(required=True)
+    new_project_id = forms.IntegerField(required=False)
+    user_id = forms.IntegerField(required=False)
+
+    def clean(self):
+        errors = {}
+        project_id = self.cleaned_data.get("project_id")
+        new_project_id = self.cleaned_data.get("new_project_id")
+        start = self.cleaned_data.get("start")
+        end = self.cleaned_data.get("end")
+        if project_id and project_id == new_project_id:
+            errors["project_id"] = _("The projects have to be different")
+            errors["new_project_id"] = _("The projects have to be different")
+        if end and end < start:
+            errors["end"] = _("The end date must be on or after the start date")
+        if errors:
+            raise ValidationError(errors)
+        return self.cleaned_data
+
+
 @accounting_or_user_office_or_manager_required
 @require_GET
 def accounts_and_projects(request):
@@ -208,3 +254,75 @@ def is_user_allowed(user: User, project):
     else:
         project_manager = user in project.manager_set.all()
         return is_active and (project_manager or accounting_or_user_office_or_manager)
+
+
+@accounting_or_manager_required
+@require_http_methods(["GET", "POST"])
+def transfer_charges(request):
+    if not ProjectsAccountsCustomization.get_bool("project_allow_transferring_charges"):
+        return redirect("landing")
+    form = ProjectTransferForm(request.POST or None)
+    project = Project.objects.filter(pk=form.data.get("project_id")).first()
+    new_project_id = form.data.get("new_project_id")
+    new_project = Project.objects.filter(pk=new_project_id).first() if new_project_id else None
+    user_id = form.data.get("user_id")
+    customer = User.objects.filter(id=user_id).first() if user_id else None
+    dictionary = {"form": form, "project": project, "new_project": new_project, "customer": customer}
+
+    if request.method == "POST":
+        if form.is_valid():
+            charges = get_charges_for_project_and_user(request.POST, customer.username if customer else None)
+            confirm = "confirm" in request.POST
+            if confirm:
+                if not new_project_id:
+                    dictionary["charges"] = charges
+                    form.add_error("new_project_id", _("This field is required"))
+                else:
+                    do_transfer_charges(charges, new_project.id)
+                    messages.success(request, f"{len(charges)} charges were transfered from {project} to {new_project}")
+            else:
+                dictionary["charges"] = charges
+    return render(request, "accounts_and_projects/transfer_charges.html", dictionary)
+
+
+def get_charges_for_project_and_user(params: QueryDict, username: str = None) -> List[BillableItem]:
+    dictionary = params.copy()
+    dictionary["username"] = username
+    if not dictionary.get("end"):
+        dictionary["end"] = datetime.now().strftime(date_input_format)
+    return get_billing_charges(dictionary)
+
+
+@transaction.atomic
+def do_transfer_charges(charges: List[BillableItem], new_project_id: int):
+    usage_event_ids = []
+    area_access_record_ids = []
+    consumable_withdrawal_ids = []
+    missed_reservation_ids = []
+    staff_charge_ids = []
+    training_session_ids = []
+    for charge in charges:
+        if charge.type == "tool_usage":
+            usage_event_ids.append(charge.item_id)
+        elif charge.type == "area_access":
+            area_access_record_ids.append(charge.item_id)
+        elif charge.type == "consumable":
+            consumable_withdrawal_ids.append(charge.item_id)
+        elif charge.type == "missed_reservation":
+            missed_reservation_ids.append(charge.item_id)
+        elif charge.type == "staff_charge":
+            staff_charge_ids.append(charge.item_id)
+        elif charge.type == "training_session":
+            training_session_ids.append(charge.item_id)
+    UsageEvent.objects.filter(id__in=usage_event_ids).update(project_id=new_project_id)
+    AreaAccessRecord.objects.filter(id__in=area_access_record_ids).update(project_id=new_project_id)
+    ConsumableWithdraw.objects.filter(id__in=consumable_withdrawal_ids).update(project_id=new_project_id)
+    Reservation.objects.filter(id__in=missed_reservation_ids).update(project_id=new_project_id)
+    StaffCharge.objects.filter(id__in=staff_charge_ids).update(project_id=new_project_id)
+    TrainingSession.objects.filter(id__in=training_session_ids).update(project_id=new_project_id)
+
+
+@accounting_or_manager_required
+@require_GET
+def search_project_for_transfer(request):
+    return queryset_search_filter(Project.objects.all(), ["name", "application_identifier"], request)
diff --git a/NEMO/views/api.py b/NEMO/views/api.py
index 462ad70b5..3d3400386 100644
--- a/NEMO/views/api.py
+++ b/NEMO/views/api.py
@@ -1,5 +1,3 @@
-from typing import List
-
 from django.contrib.auth.models import Group, Permission
 from django.contrib.contenttypes.models import ContentType
 from django.db import transaction
@@ -16,6 +14,8 @@
     AccountType,
     Area,
     AreaAccessRecord,
+    Configuration,
+    ConfigurationOption,
     Consumable,
     ConsumableCategory,
     ConsumableWithdraw,
@@ -39,6 +39,8 @@
     AreaAccessRecordSerializer,
     AreaSerializer,
     BillableItemSerializer,
+    ConfigurationOptionSerializer,
+    ConfigurationSerializer,
     ConsumableCategorySerializer,
     ConsumableSerializer,
     ConsumableWithdrawSerializer,
@@ -60,14 +62,8 @@
 )
 from NEMO.utilities import export_format_datetime
 from NEMO.views.api_billing import (
-    BillableItem,
     BillingFilterForm,
-    get_area_access_for_billing,
-    get_consumables_for_billing,
-    get_missed_reservations_for_billing,
-    get_staff_charges_for_billing,
-    get_training_sessions_for_billing,
-    get_usage_events_for_billing,
+    get_billing_charges,
 )
 
 
@@ -171,6 +167,7 @@ class ProjectViewSet(ModelViewSet):
         "application_identifier": ["exact"],
         "active": ["exact"],
         "account_id": ["exact", "in"],
+        "account": ["exact", "in"],
     }
 
 
@@ -247,6 +244,38 @@ class ResourceViewSet(ModelViewSet):
     }
 
 
+class ConfigurationViewSet(ModelViewSet):
+    filename = "configurations"
+    queryset = Configuration.objects.all()
+    serializer_class = ConfigurationSerializer
+    filterset_fields = {
+        "id": ["exact", "in"],
+        "name": ["exact", "iexact", "in"],
+        "tool_id": ["exact", "in", "isnull"],
+        "tool": ["exact", "in", "isnull"],
+        "advance_notice_limit": ["exact", "in", "gte", "gt", "lte", "lt"],
+        "display_order": ["exact", "in", "gte", "gt", "lte", "lt"],
+        "maintainers": ["exact", "in", "isnull"],
+        "qualified_users_are_maintainers": ["exact"],
+        "exclude_from_configuration_agenda": ["exact"],
+        "enabled": ["exact"],
+    }
+
+
+class ConfigurationOptionViewSet(ModelViewSet):
+    filename = "reservation_configuration_options"
+    queryset = ConfigurationOption.objects.all()
+    serializer_class = ConfigurationOptionSerializer
+    filterset_fields = {
+        "id": ["exact", "in"],
+        "name": ["exact", "iexact", "in"],
+        "reservation_id": ["exact", "in", "isnull"],
+        "reservation": ["exact", "in", "isnull"],
+        "configuration_id": ["exact", "in", "isnull"],
+        "configuration": ["exact", "in", "isnull"],
+    }
+
+
 class ReservationViewSet(ModelViewSet):
     filename = "reservations"
     queryset = Reservation.objects.all()
@@ -256,10 +285,15 @@ class ReservationViewSet(ModelViewSet):
         "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
         "project_id": ["exact", "in"],
+        "project": ["exact", "in"],
         "user_id": ["exact", "in"],
+        "user": ["exact", "in"],
         "creator_id": ["exact", "in"],
+        "creator": ["exact", "in"],
         "tool_id": ["exact", "in", "isnull"],
+        "tool": ["exact", "in", "isnull"],
         "area_id": ["exact", "in", "isnull"],
+        "area": ["exact", "in", "isnull"],
         "cancelled": ["exact"],
         "missed": ["exact"],
         "validated": ["exact"],
@@ -277,9 +311,13 @@ class UsageEventViewSet(ModelViewSet):
         "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
         "project_id": ["exact", "in"],
+        "project": ["exact", "in"],
         "user_id": ["exact", "in"],
+        "user": ["exact", "in"],
         "operator_id": ["exact", "in"],
+        "operator": ["exact", "in"],
         "tool_id": ["exact", "in"],
+        "tool": ["exact", "in"],
         "validated": ["exact"],
         "validated_by": ["exact", "in", "isnull"],
     }
@@ -294,9 +332,13 @@ class AreaAccessRecordViewSet(ModelViewSet):
         "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
         "project_id": ["exact", "in"],
+        "project": ["exact", "in"],
         "customer_id": ["exact", "in"],
+        "customer": ["exact", "in"],
         "area_id": ["exact", "in"],
+        "area": ["exact", "in"],
         "staff_charge_id": ["exact", "isnull", "in"],
+        "staff_charge": ["exact", "isnull", "in"],
         "validated": ["exact"],
         "validated_by": ["exact", "in", "isnull"],
     }
@@ -310,9 +352,11 @@ class TaskViewSet(ModelViewSet):
         "id": ["exact", "in"],
         "urgency": ["exact", "gte", "gt", "lte", "lt"],
         "tool_id": ["exact", "in"],
+        "tool": ["exact", "in"],
         "force_shutdown": ["exact"],
         "safety_hazard": ["exact"],
         "creator_id": ["exact", "in"],
+        "creator": ["exact", "in"],
         "creation_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "estimated_resolution_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "problem_category": ["exact", "in"],
@@ -320,6 +364,7 @@ class TaskViewSet(ModelViewSet):
         "resolved": ["exact"],
         "resolution_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "resolver_id": ["exact", "in"],
+        "resolver": ["exact", "in"],
         "resolution_category": ["exact", "in"],
     }
 
@@ -333,10 +378,14 @@ class ScheduledOutageViewSet(ModelViewSet):
         "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
         "creator_id": ["exact", "in"],
+        "creator": ["exact", "in"],
         "category": ["exact", "in"],
         "tool_id": ["exact", "in", "isnull"],
+        "tool": ["exact", "in", "isnull"],
         "area_id": ["exact", "in", "isnull"],
+        "area": ["exact", "in", "isnull"],
         "resource_id": ["exact", "in", "isnull"],
+        "resource": ["exact", "in", "isnull"],
     }
 
 
@@ -347,8 +396,11 @@ class StaffChargeViewSet(ModelViewSet):
     filterset_fields = {
         "id": ["exact", "in"],
         "staff_member_id": ["exact", "in"],
+        "staff_member": ["exact", "in"],
         "customer_id": ["exact", "in"],
+        "customer": ["exact", "in"],
         "project_id": ["exact", "in"],
+        "project": ["exact", "in"],
         "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
         "validated": ["exact"],
@@ -364,9 +416,13 @@ class TrainingSessionViewSet(ModelViewSet):
     filterset_fields = {
         "id": ["exact", "in"],
         "trainer_id": ["exact", "in"],
+        "trainer": ["exact", "in"],
         "trainee_id": ["exact", "in"],
+        "trainee": ["exact", "in"],
         "tool_id": ["exact", "in"],
+        "tool": ["exact", "in"],
         "project_id": ["exact", "in"],
+        "project": ["exact", "in"],
         "duration": ["exact", "gte", "lte", "gt", "lt"],
         "type": ["exact", "in"],
         "date": ["month", "year", "day", "gte", "gt", "lte", "lt"],
@@ -390,6 +446,7 @@ class ConsumableViewSet(ModelViewSet):
     filterset_fields = {
         "id": ["exact", "in"],
         "category_id": ["exact", "in"],
+        "category": ["exact", "in"],
         "quantity": ["exact", "gte", "lte", "gt", "lt"],
         "reminder_threshold": ["exact", "gte", "lte", "gt", "lt"],
         "visible": ["exact"],
@@ -405,9 +462,13 @@ class ConsumableWithdrawViewSet(ModelViewSet):
     filterset_fields = {
         "id": ["exact", "in"],
         "customer_id": ["exact", "in"],
+        "customer": ["exact", "in"],
         "merchant_id": ["exact", "in"],
+        "merchant": ["exact", "in"],
         "consumable_id": ["exact", "in"],
+        "consumable": ["exact", "in"],
         "project_id": ["exact", "in"],
+        "project": ["exact", "in"],
         "quantity": ["exact", "gte", "lte", "gt", "lt"],
         "date": ["month", "year", "day", "gte", "gt", "lte", "lt"],
         "validated": ["exact"],
@@ -447,6 +508,7 @@ class PermissionViewSet(XLSXFileMixin, viewsets.ReadOnlyModelViewSet):
         "name": ["exact", "iexact", "in"],
         "codename": ["exact", "iexact", "in"],
         "content_type_id": ["exact", "in"],
+        "content_type": ["exact", "in"],
     }
 
     def get_filename(self, *args, **kwargs):
@@ -469,18 +531,7 @@ def check_permissions(self, request):
             self.permission_denied(request)
 
     def get_queryset(self):
-        billing_form = BillingFilterForm(self.request.GET)
-        billing_form.full_clean()
-        data: List[BillableItem] = []
-        data.extend(get_usage_events_for_billing(billing_form))
-        data.extend(get_area_access_for_billing(billing_form))
-        data.extend(get_consumables_for_billing(billing_form))
-        data.extend(get_missed_reservations_for_billing(billing_form))
-        data.extend(get_staff_charges_for_billing(billing_form))
-        data.extend(get_training_sessions_for_billing(billing_form))
-
-        data.sort(key=lambda x: x.start, reverse=True)
-        return data
+        return get_billing_charges(self.request.GET)
 
     def get_filename(self, *args, **kwargs):
         return f"billing-{export_format_datetime()}.xlsx"
diff --git a/NEMO/views/api_billing.py b/NEMO/views/api_billing.py
index e7bd291a5..25dfd5664 100644
--- a/NEMO/views/api_billing.py
+++ b/NEMO/views/api_billing.py
@@ -1,6 +1,6 @@
 from datetime import datetime, timedelta
 from decimal import Decimal
-from typing import List, Optional
+from typing import Dict, List, Optional
 
 from django import forms
 
@@ -58,6 +58,7 @@ def __init__(self, item_type: str, project: Project, user: User):
         self.type: Optional[str] = item_type
         self.name: Optional[str] = None
         self.details: Optional[str] = ""
+        self.item_id: Optional[int] = None
         if project:
             self.account: Optional[str] = project.account.name
             self.account_id: Optional[int] = project.account.id
@@ -73,6 +74,21 @@ def __init__(self, item_type: str, project: Project, user: User):
         self.quantity: Optional[Decimal] = None
 
 
+def get_billing_charges(request_params: Dict) -> List[BillableItem]:
+    billing_form = BillingFilterForm(request_params)
+    billing_form.full_clean()
+    data: List[BillableItem] = []
+    data.extend(get_usage_events_for_billing(billing_form))
+    data.extend(get_area_access_for_billing(billing_form))
+    data.extend(get_consumables_for_billing(billing_form))
+    data.extend(get_missed_reservations_for_billing(billing_form))
+    data.extend(get_staff_charges_for_billing(billing_form))
+    data.extend(get_training_sessions_for_billing(billing_form))
+
+    data.sort(key=lambda x: x.start, reverse=True)
+    return data
+
+
 def get_usage_events_for_billing(billing_form: BillingFilterForm) -> List[BillableItem]:
     queryset = UsageEvent.objects.filter()
     start, end = billing_form.get_start_date(), billing_form.get_end_date()
diff --git a/NEMO/views/calendar.py b/NEMO/views/calendar.py
index 760aa4cf4..a9ef91e3d 100644
--- a/NEMO/views/calendar.py
+++ b/NEMO/views/calendar.py
@@ -1,9 +1,9 @@
+import re
 from datetime import datetime, timedelta
 from http import HTTPStatus
 from json import dumps, loads
 from logging import getLogger
-from re import match
-from typing import List, Optional, Union
+from typing import List, Optional, Tuple, Union
 
 from django.contrib.auth.decorators import login_required
 from django.db.models import Q
@@ -21,6 +21,7 @@
     Area,
     AreaAccessRecord,
     Configuration,
+    ConfigurationOption,
     Project,
     Reservation,
     ReservationItemType,
@@ -158,6 +159,8 @@ def event_feed(request):
     elif event_type == "specific user" and request.user.is_staff:
         user = get_object_or_404(User, id=request.GET.get("user"))
         return specific_user_feed(request, user, start, end)
+    elif event_type == "configuration agenda":
+        return configuration_agenda_event_feed(request, start, end)
     else:
         return HttpResponseBadRequest("Invalid event type or operation not authorized.")
 
@@ -340,6 +343,38 @@ def specific_user_feed(request, user, start, end):
     return render(request, "calendar/specific_user_feed.html", dictionary)
 
 
+def configuration_agenda_event_feed(request, start, end):
+    events = Reservation.objects.filter(
+        cancelled=False, missed=False, shortened=False, configurationoption_set__isnull=False
+    ).distinct()
+    # Exclude events for which the following is true:
+    # The event starts and ends before the time-window, and...
+    # The event starts and ends after the time-window.
+    events = events.exclude(start__lt=start, end__lt=start)
+    events = events.exclude(start__gt=end, end__gt=end)
+    all_tools = request.GET.get("all_tools")
+
+    # Filter events that only have to do with the relevant tool/area.
+    item_type = request.GET.get("item_type")
+    if all_tools:
+        events = events.filter(area=None)
+    if item_type:
+        item_type = ReservationItemType(item_type)
+        item_id = request.GET.get("item_id")
+        if item_id and not all_tools:
+            events = events.filter(**{f"{item_type.value}__id": item_id})
+
+    # TODO: Filter events that only have to do with the current user's primary, backup and superuser tools.
+    personal_schedule = request.GET.get("personal_schedule")
+
+    dictionary = {
+        "events": events,
+        "personal_schedule": personal_schedule,
+        "all_tools": all_tools,
+    }
+    return render(request, "calendar/configuration_event_feed.html", dictionary)
+
+
 @login_required
 @require_POST
 def create_reservation(request):
@@ -465,7 +500,7 @@ def create_item_reservation(request, current_user, start, end, item_type: Reserv
 
         # If a reservation is requested and configuration information is present also...
         elif item.is_configurable() and configured:
-            new_reservation.additional_information, new_reservation.self_configuration = extract_configuration(request)
+            set_reservation_configuration(new_reservation, request)
             # Reservation can't be short notice if the user is configuring the tool themselves.
             if new_reservation.self_configuration:
                 new_reservation.short_notice = False
@@ -525,42 +560,45 @@ def reservation_success(request, reservation: Reservation):
         return HttpResponse()
 
 
-def extract_configuration(request):
-    cleaned_configuration = []
+def set_reservation_configuration(reservation: Reservation, request):
+    configuration_options = []
     for key, value in request.POST.items():
-        entry = parse_configuration_entry(key, value)
+        entry = parse_configuration_entry(reservation, key, value)
         if entry:
-            cleaned_configuration.append(entry)
-    # Sort by configuration display priority and join the results:
-    result = ""
-    for config in sorted(cleaned_configuration):
-        result += config[1] + "\n"
+            configuration_options.append(entry)
+    # Sort by configuration display priority and add config options to the list to save later:
+    if configuration_options:
+        reservation._deferred_related_models = []
+        for config in sorted(configuration_options, key=lambda x: x[0]):
+            reservation._deferred_related_models.append(config[1])
     if "additional_information" in request.POST:
-        result += request.POST["additional_information"][:ADDITIONAL_INFORMATION_MAXIMUM_LENGTH].strip()
-    self_configuration = True if request.POST.get("self_configuration") == "on" else False
-    return result, self_configuration
+        reservation.additional_information = request.POST["additional_information"][
+            :ADDITIONAL_INFORMATION_MAXIMUM_LENGTH
+        ].strip()
+    reservation.self_configuration = True if request.POST.get("self_configuration") == "on" else False
 
 
-def parse_configuration_entry(key, value):
-    if value == "" or not match("^configuration_[0-9]+__slot_[0-9]+__display_order_[0-9]+$", key):
+def parse_configuration_entry(reservation: Reservation, key, value) -> Optional[Tuple[int, ConfigurationOption]]:
+    if value == "" or not re.match("^configuration_[0-9]+__slot_[0-9]+__display_order_[0-9]+$", key):
         return None
     config_id, slot, display_order = [int(s) for s in key.split("_") if s.isdigit()]
     configuration = Configuration.objects.get(pk=config_id)
     if not configuration.enabled:
         return None
-    available_setting = configuration.get_available_setting(value)
+    setting = configuration.get_available_setting(value)
+
+    option_value = ConfigurationOption()
+    option_value.current_setting = setting
+    option_value.available_settings = configuration.available_settings
+    option_value.calendar_colors = configuration.calendar_colors
+    option_value.absence_string = configuration.absence_string
+    option_value.reservation = reservation
+    option_value.configuration = configuration
     if len(configuration.current_settings_as_list()) == 1:
-        return display_order, configuration.name + " needs to be set to " + available_setting + "."
+        option_value.name = configuration.configurable_item_name or configuration.name
     else:
-        return (
-            display_order,
-            configuration.configurable_item_name
-            + " #"
-            + str(slot + 1)
-            + " needs to be set to "
-            + available_setting
-            + ".",
-        )
+        option_value.name = f"{configuration.configurable_item_name or configuration.name} #{str(slot + 1)}"
+    return display_order, option_value
 
 
 @staff_member_required
@@ -746,7 +784,8 @@ def modify_outage(request, start_delta, end_delta):
         return HttpResponseNotFound("The outage that you wish to modify doesn't exist!")
     if start_delta:
         outage.start += start_delta
-    outage.end += end_delta
+    if end_delta:
+        outage.end += end_delta
     policy_problem = policy.check_to_create_outage(outage)
     if policy_problem:
         return HttpResponseBadRequest(policy_problem)
@@ -827,7 +866,7 @@ def change_reservation_date(request):
     new_start = request.POST.get("new_start", None)
     if new_start:
         try:
-            new_start = make_aware(datetime.strptime(new_start, datetime_input_format))
+            new_start = make_aware(datetime.strptime(new_start, datetime_input_format), is_dst=False)
             if new_start.time().minute not in [0, 15, 30, 45]:
                 return HttpResponseBadRequest("Reservation time only works with 15 min increments")
         except ValueError:
@@ -836,7 +875,7 @@ def change_reservation_date(request):
     new_end = request.POST.get("new_end", None)
     if new_end:
         try:
-            new_end = make_aware(datetime.strptime(new_end, datetime_input_format))
+            new_end = make_aware(datetime.strptime(new_end, datetime_input_format), is_dst=False)
             if new_end.time().minute not in [0, 15, 30, 45]:
                 return HttpResponseBadRequest("Reservation time only works with 15 min increments")
         except ValueError:
@@ -848,6 +887,36 @@ def change_reservation_date(request):
         return HttpResponseBadRequest("Invalid delta")
 
 
+@staff_member_required
+@require_POST
+def change_outage_date(request):
+    """Change an outage's start or end date."""
+    outage = get_object_or_404(ScheduledOutage, id=request.POST["id"])
+    start_delta, end_delta = None, None
+    new_start = request.POST.get("new_start", None)
+    if new_start:
+        try:
+            new_start = make_aware(datetime.strptime(new_start, datetime_input_format), is_dst=False)
+            if new_start.time().minute not in [0, 15, 30, 45]:
+                return HttpResponseBadRequest("Outage time only works with 15 min increments")
+        except ValueError:
+            return HttpResponseBadRequest("Invalid date format for start date")
+        start_delta = new_start - outage.start
+    new_end = request.POST.get("new_end", None)
+    if new_end:
+        try:
+            new_end = make_aware(datetime.strptime(new_end, datetime_input_format), is_dst=False)
+            if new_end.time().minute not in [0, 15, 30, 45]:
+                return HttpResponseBadRequest("Outage time only works with 15 min increments")
+        except ValueError:
+            return HttpResponseBadRequest("Invalid date format for end date")
+        end_delta = (new_end - outage.end) if new_end else None
+    if start_delta or end_delta:
+        return modify_outage(request, start_delta, end_delta)
+    else:
+        return HttpResponseBadRequest("Invalid delta")
+
+
 @login_required
 @require_POST
 def change_reservation_project(request, reservation_id):
diff --git a/NEMO/views/configuration_agenda.py b/NEMO/views/configuration_agenda.py
index fe9c8d741..0f8990d9b 100644
--- a/NEMO/views/configuration_agenda.py
+++ b/NEMO/views/configuration_agenda.py
@@ -45,7 +45,7 @@ def configuration_agenda(request, time_period="today"):
             missed=False,
             shortened=False,
         )
-        .exclude(additional_information="")
+        .exclude(configurationoption_set__isnull=True)
         .order_by("start")
     )
     tools = Tool.objects.filter(id__in=reservations.values_list("tool", flat=True))
diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py
index 71f469d1d..b17cc93a7 100644
--- a/NEMO/views/customization.py
+++ b/NEMO/views/customization.py
@@ -139,7 +139,7 @@ def get_list_int(cls, name: str, raise_exception=True) -> List[int]:
         for item in cls.get_list(name, raise_exception):
             if item:
                 integer = quiet_int(item.strip(), None)
-                if integer:
+                if integer is not None:
                     result.append(integer)
         return result
 
@@ -167,7 +167,13 @@ class ApplicationCustomization(CustomizationBase):
         "area_logout_already_logged_in": "",
         "default_badge_reader_id": "",
         "consumable_user_self_checkout": "",
+        "consumable_category_collapse": "",
         "area_in_usage_reminders": "enabled",
+        "calendar_page_title": "Calendar",
+        "tool_control_page_title": "Tool control",
+        "status_dashboard_page_title": "Status dashboard",
+        "requests_page_title": "Requests",
+        "safety_page_title": "Safety",
     }
 
     def context(self) -> Dict:
@@ -191,6 +197,7 @@ class ProjectsAccountsCustomization(CustomizationBase):
         "project_list_active_only": "",
         "account_list_collapse": "",
         "project_allow_pi_manage_users": "",
+        "project_allow_transferring_charges": "",
     }
 
     def validate(self, name, value):
@@ -399,6 +406,7 @@ class ToolCustomization(CustomizationBase):
         "tool_phone_number_required": "enabled",
         "tool_location_required": "enabled",
         "tool_task_updates_facility_managers": "enabled",
+        "tool_task_updates_superusers": "",
         "tool_control_hide_data_history_users": "",
         "tool_control_configuration_setting_template": "{{ current_setting }}",
         "tool_control_broadcast_upcoming_reservation": "",
diff --git a/NEMO/views/maintenance.py b/NEMO/views/maintenance.py
index d7fd33502..54dd18786 100644
--- a/NEMO/views/maintenance.py
+++ b/NEMO/views/maintenance.py
@@ -18,7 +18,7 @@ def maintenance(request, sort_by=""):
         # Limit tools to preferences + tools user is the owner of + tools user is a backup owner of.
         limit_tools = set(user.get_preferences().tool_task_notifications.all())
         limit_tools.update(user.primary_tool_owner.all())
-        limit_tools.update(user.superuser_for_tools.all())
+        limit_tools.update(user.backup_for_tools.all())
         pending_tasks = pending_tasks.filter(tool__in=limit_tools)
     if sort_by in ["urgency", "force_shutdown", "tool", "problem_category", "last_updated", "creation_time"]:
         if sort_by == "last_updated":
diff --git a/NEMO/views/mobile.py b/NEMO/views/mobile.py
index 0a48560eb..5041711ee 100644
--- a/NEMO/views/mobile.py
+++ b/NEMO/views/mobile.py
@@ -14,9 +14,9 @@
 from NEMO.policy import policy_class as policy
 from NEMO.utilities import beginning_of_the_day, end_of_the_day, localize
 from NEMO.views.calendar import (
-    extract_configuration,
     extract_reservation_questions,
     render_reservation_questions,
+    set_reservation_configuration,
 )
 from NEMO.views.customization import CalendarCustomization
 
@@ -139,7 +139,7 @@ def make_reservation(request):
         if not request.user.is_staff:
             return render(request, "mobile/error.html", {"message": "You must specify a project for your reservation"})
 
-    reservation.additional_information, reservation.self_configuration = extract_configuration(request)
+    set_reservation_configuration(reservation, request)
     # Reservation can't be short notice if the user is configuring the tool themselves.
     if reservation.self_configuration:
         reservation.short_notice = False
diff --git a/NEMO/views/tasks.py b/NEMO/views/tasks.py
index d282836b6..353b9a71b 100644
--- a/NEMO/views/tasks.py
+++ b/NEMO/views/tasks.py
@@ -391,6 +391,8 @@ def get_task_email_recipients(task: Task) -> List[str]:
     recipient_users: Set[User] = {task.tool.primary_owner}
     # Add backup owners
     recipient_users.update(task.tool.backup_owners.all())
+    if ToolCustomization.get_bool("tool_task_updates_superusers"):
+        recipient_users.update(task.tool.superusers.all())
     # Add facility managers and take into account their preferences
     if ToolCustomization.get_bool("tool_task_updates_facility_managers"):
         recipient_users.update(
diff --git a/NEMO/views/tool_control.py b/NEMO/views/tool_control.py
index cc8863dc3..b2b5bc52a 100644
--- a/NEMO/views/tool_control.py
+++ b/NEMO/views/tool_control.py
@@ -104,7 +104,6 @@ def tool_status(request, tool_id):
         "mobile": request.device == "mobile",
         "task_statuses": TaskStatus.objects.all(),
         "post_usage_questions": DynamicForm(tool.post_usage_questions).render("tool_usage_group_question", tool_id),
-        "configs": get_tool_full_config_history(tool),
         "show_broadcast_upcoming_reservation": user_is_staff
         or (user_is_qualified and tool_control_broadcast_upcoming_reservation_enabled),
     }
@@ -138,25 +137,28 @@ def use_tool_for_other(request):
     return render(request, "tool_control/use_tool_for_other.html", dictionary)
 
 
-def get_tool_full_config_history(tool: Tool):
+@login_required
+@require_GET
+def tool_config_history(request, tool_id):
     # tool config by user and tool and time
     configs = []
-    config_history = ConfigurationHistory.objects.filter(configuration__tool_id=tool.id).order_by("-modification_time")[
+    config_history = ConfigurationHistory.objects.filter(configuration__tool_id=tool_id).order_by("-modification_time")[
         :20
     ]
-    configurations = tool.current_ordered_configurations()
-    for c in config_history:
-        for co in configurations:
-            if co == c.configuration:
-                current_settings = co.current_settings_as_list()
-                current_settings[c.slot] = c.setting
-                co.current_settings = ", ".join(current_settings)
-        config_input = {"configurations": configurations, "render_as_form": False}
+    for history in config_history:
         configuration = ConfigurationEditor()
+        conf = history.configuration
+        conf.name = history.item_name
+        conf.current_settings = history.setting
         configs.append(
-            {"modification_time": c.modification_time, "user": c.user, "html": configuration.render(None, config_input)}
+            {
+                "modification_time": history.modification_time,
+                "configuration": history.configuration,
+                "user": history.user,
+                "html": mark_safe(configuration._render_for_one(conf, render_as_form=False)),
+            }
         )
-    return configs
+    return render(request, "tool_control/config_history.html", {"configs": configs})
 
 
 @login_required
@@ -276,6 +278,7 @@ def tool_configuration(request):
     configuration.save()
     history = ConfigurationHistory()
     history.configuration = configuration
+    history.item_name = configuration.configurable_item_name or configuration.name
     history.slot = slot
     history.user = request.user
     history.setting = configuration.get_current_setting(slot)
@@ -294,7 +297,7 @@ def create_comment(request):
     comment.author = request.user
     comment.expiration_date = (
         None
-        if form.cleaned_data["expiration"] == 0
+        if form.cleaned_data["expiration"] == -1
         else timezone.now() + timedelta(days=form.cleaned_data["expiration"])
     )
     comment.save()
@@ -462,6 +465,7 @@ def disable_tool(request, tool_id):
 @login_required
 @require_GET
 def past_comments_and_tasks(request):
+    user: User = request.user
     start, end = extract_optional_beginning_and_end_times(request.GET)
     search = request.GET.get("search")
     if not start and not end and not search:
@@ -469,7 +473,9 @@ def past_comments_and_tasks(request):
     tool_id = request.GET.get("tool_id")
     try:
         tasks = Task.objects.filter(tool_id=tool_id)
-        comments = Comment.objects.filter(tool_id=tool_id, staff_only=False)
+        comments = Comment.objects.filter(tool_id=tool_id)
+        if not user.is_staff:
+            comments = comments.filter(staff_only=False)
         if start:
             tasks = tasks.filter(creation_time__gt=start)
             comments = comments.filter(creation_date__gt=start)
@@ -492,8 +498,12 @@ def past_comments_and_tasks(request):
 @login_required
 @require_GET
 def ten_most_recent_past_comments_and_tasks(request, tool_id):
+    user: User = request.user
     tasks = Task.objects.filter(tool_id=tool_id).order_by("-creation_time")[:10]
-    comments = Comment.objects.filter(tool_id=tool_id, staff_only=False).order_by("-creation_date")[:10]
+    comments = Comment.objects.filter(tool_id=tool_id).order_by("-creation_date")
+    if not user.is_staff:
+        comments = comments.filter(staff_only=False)
+    comments = comments[:10]
     past = list(chain(tasks, comments))
     past.sort(key=lambda x: getattr(x, "creation_time", None) or getattr(x, "creation_date", None))
     past.reverse()
diff --git a/NEMO/widgets/configuration_editor.py b/NEMO/widgets/configuration_editor.py
index 73d3138ab..e8e2592f6 100644
--- a/NEMO/widgets/configuration_editor.py
+++ b/NEMO/widgets/configuration_editor.py
@@ -18,12 +18,12 @@ def render(self, name, value, attrs=None, **kwargs):
             if render_as_form is None:
                 render_as_form = not config.tool.in_use() and config.user_is_maintainer(value["user"])
             if len(current_settings) == 1:
-                result += self.__render_for_one(config, render_as_form)
+                result += self._render_for_one(config, render_as_form)
             else:
-                result += self.__render_for_multiple(config, render_as_form)
+                result += self._render_for_multiple(config, render_as_form)
         return mark_safe(result)
 
-    def __render_for_one(self, config, render_as_form=None):
+    def _render_for_one(self, config, render_as_form=None):
         current_setting = config.current_settings_as_list()[0]
         result = "<p><label class='form-inline'>" + escape(config.name) + ": "
         if render_as_form:
@@ -45,14 +45,14 @@ def __render_for_one(self, config, render_as_form=None):
         result += "</label></p>"
         return result
 
-    def __render_for_multiple(self, config, render_as_form=None):
+    def _render_for_multiple(self, config, render_as_form=None):
         result = "<p>" + escape(config.name) + ":<ul>"
         for setting_index, current_setting in enumerate(config.current_settings_as_list()):
             result += "<li>"
             if render_as_form:
                 result += (
                     "<label class='form-inline'>"
-                    + escape(config.configurable_item_name)
+                    + escape(config.configurable_item_name or config.name)
                     + " #"
                     + str(setting_index + 1)
                     + ": "
@@ -74,7 +74,7 @@ def __render_for_multiple(self, config, render_as_form=None):
                 result += "</select></label>"
             else:
                 result += (
-                    config.configurable_item_name
+                    (config.configurable_item_name or config.name)
                     + " #"
                     + str(setting_index + 1)
                     + ": "
diff --git a/NEMO/widgets/item_tree.py b/NEMO/widgets/item_tree.py
index 83a8499da..f1e6a63da 100644
--- a/NEMO/widgets/item_tree.py
+++ b/NEMO/widgets/item_tree.py
@@ -131,7 +131,7 @@ def render(self, legend=False, category_items_lookup=None):
         """
         item_type = f"'{self.item_type.value}'"
         result = (
-            f'<fieldset class="item_tree_fieldset"><legend align="center" onclick="toggle_item_categories({item_type})">{self.item_type.value.capitalize()}s</legend>'
+            f'<fieldset id="item-tree-{self.item_type.value}" class="item_tree_fieldset"><legend align="center" onclick="toggle_item_categories({item_type})">{self.item_type.value.capitalize()}s</legend>'
             if legend
             else ""
         )
diff --git a/resources/fixtures/splash_pad.json b/resources/fixtures/splash_pad.json
index d4d5ca12c..5f1324c18 100644
--- a/resources/fixtures/splash_pad.json
+++ b/resources/fixtures/splash_pad.json
@@ -1011,6 +1011,13 @@
       "value": "enabled"
     }
   },
+  {
+    "model": "NEMO.customization",
+    "pk": "project_allow_transferring_charges",
+    "fields": {
+      "value": "enabled"
+    }
+  },
   {
     "model": "NEMO.customization",
     "pk": "recurring_charges_category",
@@ -9160,6 +9167,7 @@
       "prompt": "What blade type do you need to use?",
       "current_settings": "SiC Blade",
       "available_settings": "Metal Blade, Resin Blade, SiC Blade",
+      "calendar_colors": "#670862, #075e21, #878014",
       "qualified_users_are_maintainers": false,
       "exclude_from_configuration_agenda": false,
       "absence_string": null,
@@ -9177,6 +9185,28 @@
       ]
     }
   },
+  {
+    "model": "NEMO.configuration",
+    "pk": 2,
+    "fields": {
+      "name": "Wafer",
+      "tool": [
+        "CMP"
+      ],
+      "configurable_item_name": "Wafer chuck",
+      "advance_notice_limit": 18,
+      "display_order": 0,
+      "prompt": "Please choose a wafer size you will need for your reservation.",
+      "current_settings": "4\"",
+      "available_settings": "3\", 4\"",
+      "calendar_colors": "#0c0b55, #530202",
+      "qualified_users_are_maintainers": false,
+      "exclude_from_configuration_agenda": false,
+      "absence_string": null,
+      "enabled": true,
+      "maintainers": []
+    }
+  },
   {
     "model": "NEMO.project",
     "fields": {
diff --git a/setup.py b/setup.py
index 682563557..d0d52e688 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 setup(
     name="NEMO",
-    version="5.0.0",
+    version="5.1.0",
     python_requires=">=3.8, <4",
     packages=find_namespace_packages(exclude=["NEMO.tests", "NEMO.tests.*"]),
     include_package_data=True,
@@ -28,16 +28,16 @@
         "Programming Language :: Python :: 3.12",
     ],
     install_requires=[
-        "cryptography==41.0.4",
+        "cryptography==41.0.7",
         "Django==3.2.22",
         "django-auditlog==2.3.0",
-        "django-filter==23.3",
+        "django-filter==23.4",
         "django-mptt==0.14.0",
         "djangorestframework==3.14.0",
         "drf-excel==2.4.0",
         "drf-flex-fields==1.0.2",
         "ldap3==2.9.1",
-        "Pillow==10.0.1",
+        "Pillow==10.1.0",
         "pymodbus==3.3.2",
         "python-dateutil==2.8.2",
         "pytz==2023.3",


Test patch:
diff --git a/NEMO/tests/test_urls.py b/NEMO/tests/test_urls.py
index 6efb3a028..2166c97e7 100644
--- a/NEMO/tests/test_urls.py
+++ b/NEMO/tests/test_urls.py
@@ -111,6 +111,18 @@
             "new_end": end_one_day.replace(minute=30).strftime(settings.DATETIME_INPUT_FORMATS[0]),
         },
     },
+    "change_outage_date": {
+        "login_id": 1,
+        "post": {
+            "id": 1,
+            "new_start": (start + datetime.timedelta(days=1, hours=12))
+            .replace(minute=15)
+            .strftime(settings.DATETIME_INPUT_FORMATS[0]),
+            "new_end": (end_one_day + datetime.timedelta(days=1))
+            .replace(minute=30)
+            .strftime(settings.DATETIME_INPUT_FORMATS[0]),
+        },
+    },
     "compose_email": {"login_id": 1},
 }