diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index 04b9c14bed..cd15800e55 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -385,7 +385,7 @@ class InvenTreeAttachment(models.Model):
                 'link': _('Missing external link'),
             })
 
-        if self.attachment.name.lower().endswith('.svg'):
+        if self.attachment and self.attachment.name.lower().endswith('.svg'):
             self.attachment.file.file = self.clean_svg(self.attachment)
 
         super().save(*args, **kwargs)
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 1c856bebba..b1c90008ec 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -226,22 +226,51 @@ def heartbeat():
 
 @scheduled_task(ScheduledTask.DAILY)
 def delete_successful_tasks():
-    """Delete successful task logs which are more than a month old."""
+    """Delete successful task logs which are older than a specified period"""
     try:
         from django_q.models import Success
+
+        from common.models import InvenTreeSetting
+
+        days = InvenTreeSetting.get_setting('INVENTREE_DELETE_TASKS_DAYS', 30)
+        threshold = timezone.now() - timedelta(days=days)
+
+        # Delete successful tasks
+        results = Success.objects.filter(
+            started__lte=threshold
+        )
+
+        if results.count() > 0:
+            logger.info(f"Deleting {results.count()} successful task records")
+            results.delete()
+
     except AppRegistryNotReady:  # pragma: no cover
         logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
-        return
 
-    threshold = timezone.now() - timedelta(days=30)
 
-    results = Success.objects.filter(
-        started__lte=threshold
-    )
+@scheduled_task(ScheduledTask.DAILY)
+def delete_failed_tasks():
+    """Delete failed task logs which are older than a specified period"""
 
-    if results.count() > 0:
-        logger.info(f"Deleting {results.count()} successful task records")
-        results.delete()
+    try:
+        from django_q.models import Failure
+
+        from common.models import InvenTreeSetting
+
+        days = InvenTreeSetting.get_setting('INVENTREE_DELETE_TASKS_DAYS', 30)
+        threshold = timezone.now() - timedelta(days=days)
+
+        # Delete failed tasks
+        results = Failure.objects.filter(
+            started__lte=threshold
+        )
+
+        if results.count() > 0:
+            logger.info(f"Deleting {results.count()} failed task records")
+            results.delete()
+
+    except AppRegistryNotReady:  # pragma: no cover
+        logger.info("Could not perform 'delete_failed_tasks' - App registry not ready")
 
 
 @scheduled_task(ScheduledTask.DAILY)
@@ -250,8 +279,10 @@ def delete_old_error_logs():
     try:
         from error_report.models import Error
 
-        # Delete any error logs more than 30 days old
-        threshold = timezone.now() - timedelta(days=30)
+        from common.models import InvenTreeSetting
+
+        days = InvenTreeSetting.get_setting('INVENTREE_DELETE_ERRORS_DAYS', 30)
+        threshold = timezone.now() - timedelta(days=days)
 
         errors = Error.objects.filter(
             when__lte=threshold,
@@ -264,7 +295,37 @@ def delete_old_error_logs():
     except AppRegistryNotReady:  # pragma: no cover
         # Apps not yet loaded
         logger.info("Could not perform 'delete_old_error_logs' - App registry not ready")
-        return
+
+
+@scheduled_task(ScheduledTask.DAILY)
+def delete_old_notifications():
+    """Delete old notification logs"""
+
+    try:
+        from common.models import (InvenTreeSetting, NotificationEntry,
+                                   NotificationMessage)
+
+        days = InvenTreeSetting.get_setting('INVENTREE_DELETE_NOTIFICATIONS_DAYS', 30)
+        threshold = timezone.now() - timedelta(days=days)
+
+        items = NotificationEntry.objects.filter(
+            updated__lte=threshold
+        )
+
+        if items.count() > 0:
+            logger.info(f"Deleted {items.count()} old notification entries")
+            items.delete()
+
+        items = NotificationMessage.objects.filter(
+            creation__lte=threshold
+        )
+
+        if items.count() > 0:
+            logger.info(f"Deleted {items.count()} old notification messages")
+            items.delete()
+
+    except AppRegistryNotReady:
+        logger.info("Could not perform 'delete_old_notifications' - App registry not ready")
 
 
 @scheduled_task(ScheduledTask.DAILY)
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 582503ae6f..25589c4887 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -893,6 +893,39 @@ class InvenTreeSetting(BaseInvenTreeSetting):
             'default': True,
         },
 
+        'INVENTREE_DELETE_TASKS_DAYS': {
+            'name': _('Delete Old Tasks'),
+            'description': _('Background task results will be deleted after specified number of days'),
+            'default': 30,
+            'units': 'days',
+            'validator': [
+                int,
+                MinValueValidator(7),
+            ]
+        },
+
+        'INVENTREE_DELETE_ERRORS_DAYS': {
+            'name': _('Delete Error Logs'),
+            'description': _('Error logs will be deleted after specified number of days'),
+            'default': 30,
+            'units': 'days',
+            'validator': [
+                int,
+                MinValueValidator(7)
+            ]
+        },
+
+        'INVENTREE_DELETE_NOTIFICATIONS_DAYS': {
+            'name': _('Delete Noficiations'),
+            'description': _('User notifications will be deleted after specified number of days'),
+            'default': 30,
+            'units': 'days',
+            'validator': [
+                int,
+                MinValueValidator(7),
+            ]
+        },
+
         'BARCODE_ENABLE': {
             'name': _('Barcode Support'),
             'description': _('Enable barcode scanner support'),
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 35179485d0..9555ed5092 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -2350,3 +2350,83 @@ class PartParameterTest(InvenTreeAPITestCase):
         data = response.data
 
         self.assertEqual(data['data'], '15')
+
+
+class PartAttachmentTest(InvenTreeAPITestCase):
+    """Unit tests for the PartAttachment API endpoint"""
+
+    fixtures = [
+        'category',
+        'part',
+        'location',
+    ]
+
+    def test_add_attachment(self):
+        """Test that we can create a new PartAttachment via the API"""
+
+        url = reverse('api-part-attachment-list')
+
+        # Upload without permission
+        response = self.post(
+            url,
+            {},
+            expected_code=403,
+        )
+
+        # Add required permission
+        self.assignRole('part.add')
+
+        # Upload without specifying part (will fail)
+        response = self.post(
+            url,
+            {
+                'comment': 'Hello world',
+            },
+            expected_code=400
+        )
+
+        self.assertIn('This field is required', str(response.data['part']))
+
+        # Upload without file OR link (will fail)
+        response = self.post(
+            url,
+            {
+                'part': 1,
+                'comment': 'Hello world',
+            },
+            expected_code=400
+        )
+
+        self.assertIn('Missing file', str(response.data['attachment']))
+        self.assertIn('Missing external link', str(response.data['link']))
+
+        # Upload an invalid link (will fail)
+        response = self.post(
+            url,
+            {
+                'part': 1,
+                'link': 'not-a-link.py',
+            },
+            expected_code=400
+        )
+
+        self.assertIn('Enter a valid URL', str(response.data['link']))
+
+        link = 'https://www.google.com/test'
+
+        # Upload a valid link (will pass)
+        response = self.post(
+            url,
+            {
+                'part': 1,
+                'link': link,
+                'comment': 'Hello world',
+            },
+            expected_code=201
+        )
+
+        data = response.data
+
+        self.assertEqual(data['part'], 1)
+        self.assertEqual(data['link'], link)
+        self.assertEqual(data['comment'], 'Hello world')
diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html
index 32dee05241..aab649b0bc 100644
--- a/InvenTree/templates/InvenTree/settings/global.html
+++ b/InvenTree/templates/InvenTree/settings/global.html
@@ -24,6 +24,10 @@
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_TREE_DEPTH" icon="fa-sitemap" %}
         {% include "InvenTree/settings/setting.html" with key="INVENTREE_BACKUP_ENABLE" icon="fa-hdd" %}
+        <tr><td colspan='5'></td></tr>
+        {% include "InvenTree/settings/setting.html" with key="INVENTREE_DELETE_TASKS_DAYS" icon="fa-calendar-alt" %}
+        {% include "InvenTree/settings/setting.html" with key="INVENTREE_DELETE_ERRORS_DAYS" icon="fa-calendar-alt" %}
+        {% include "InvenTree/settings/setting.html" with key="INVENTREE_DELETE_NOTIFICATIONS_DAYS" icon="fa-calendar-alt" %}
     </tbody>
 </table>
 
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 5380afae64..1fe0746396 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -1308,15 +1308,20 @@ function loadBomTable(table, options={}) {
 
             var data = table.bootstrapTable('getData');
 
+            var update_required = false;
+
             for (var idx = 0; idx < data.length; idx++) {
-                var row = data[idx];
 
-                if (!row.parentId) {
-                    row.parentId = parent_id;
-
-                    table.bootstrapTable('updateByUniqueId', row.pk, row, true);
+                if (!data[idx].parentId) {
+                    data[idx].parentId = parent_id;
+                    update_required = true;
                 }
             }
+
+            // Re-load the table back data
+            if (update_required) {
+                table.bootstrapTable('load', data);
+            }
         },
         onLoadSuccess: function(data) {
 
diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js
index 349b3028ed..d2edcaf5fe 100644
--- a/InvenTree/templates/js/translated/filters.js
+++ b/InvenTree/templates/js/translated/filters.js
@@ -324,7 +324,7 @@ function setupFilterList(tableKey, table, target, options={}) {
 
     // Callback for reloading the table
     element.find(`#reload-${tableKey}`).click(function() {
-        $(table).bootstrapTable('refresh');
+        reloadTableFilters(table);
     });
 
     // Add a callback for downloading table data
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index fbc07bdd09..e99f53bde8 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -1301,15 +1301,17 @@ function loadParametricPartTable(table, options={}) {
 
             for (var idx = 0; idx < data.length; idx++) {
                 var row = data[idx];
-                var pk = row.pk;
 
                 // Make each parameter accessible, based on the "template" columns
                 row.parameters.forEach(function(parameter) {
                     row[`parameter_${parameter.template}`] = parameter.data;
                 });
 
-                $(table).bootstrapTable('updateByUniqueId', pk, row);
+                data[idx] = row;
             }
+
+            // Update the table
+            $(table).bootstrapTable('load', data);
         }
     });
 }