From 7bec3ff5ddf81230d92cc5353b12c877b2fd18d7 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 6 Mar 2021 20:58:57 +1100
Subject: [PATCH 001/111] django-q

---
 InvenTree/InvenTree/settings.py | 13 +++++++++++++
 requirements.txt                |  1 +
 2 files changed, 14 insertions(+)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 1a298240bc..0826c7d042 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -211,6 +211,7 @@ INSTALLED_APPS = [
     'djmoney',                              # django-money integration
     'djmoney.contrib.exchange',             # django-money exchange rates
     'error_report',                         # Error reporting in the admin interface
+    'django_q',
 ]
 
 MIDDLEWARE = CONFIG.get('middleware', [
@@ -285,6 +286,18 @@ REST_FRAMEWORK = {
 
 WSGI_APPLICATION = 'InvenTree.wsgi.application'
 
+# django-q configuration
+Q_CLUSTER = {
+    'name': 'InvenTree',
+    'workers': 4,
+    'timeout': 90,
+    'retry': 120,
+    'queue_limit': 50,
+    'bulk': 10,
+    'orm': 'default',
+    'sync': True,
+}
+
 # Markdownx configuration
 # Ref: https://neutronx.github.io/django-markdownx/customization/
 MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
diff --git a/requirements.txt b/requirements.txt
index 7bbc14bd54..2c8bd9f3e7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -30,5 +30,6 @@ django-error-report==0.2.0      # Error report viewer for the admin interface
 django-test-migrations==1.1.0   # Unit testing for database migrations 
 python-barcode[images]==0.13.1  # Barcode generator
 qrcode[pil]==6.1                # QR code generator
+django-q==1.3.4                 # Background task scheduling
 
 inventree                       # Install the latest version of the InvenTree API python library

From 45b3c68930842b662c9e40958016e0c09b18567e Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 6 Mar 2021 21:41:19 +1100
Subject: [PATCH 002/111] New status info

---
 InvenTree/InvenTree/context.py | 14 ++++++++++++--
 InvenTree/InvenTree/status.py  | 31 +++++++++++++++++++------------
 InvenTree/templates/stats.html | 15 ++++++++++++++-
 3 files changed, 45 insertions(+), 15 deletions(-)

diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py
index 9fee5deaab..1d07511fa9 100644
--- a/InvenTree/InvenTree/context.py
+++ b/InvenTree/InvenTree/context.py
@@ -30,10 +30,20 @@ def health_status(request):
 
     request._inventree_health_status = True
 
-    return {
-        "system_healthy": InvenTree.status.check_system_health(),
+    status = {
+        'django_q_running': InvenTree.status.is_q_cluster_running(),
     }
 
+    all_healthy = True
+
+    for k in status.keys():
+        if status[k] is not True:
+            all_healthy = False
+
+    status['system_healthy'] = all_healthy
+
+    return status
+
 
 def status_codes(request):
     """
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index ec2422a254..e02e476a51 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -1,15 +1,32 @@
 """
 Provides system status functionality checks.
 """
+# -*- coding: utf-8 -*-
 
-from django.utils.translation import ugettext as _
+from __future__ import unicode_literals
 
 import logging
 
+from django.utils.translation import ugettext as _
+
+from django_q.monitor import Stat
 
 logger = logging.getLogger(__name__)
 
 
+def is_q_cluster_running(**kwargs):
+    """
+    Return True if at least one cluster worker is running
+    """
+
+    clusters = Stat.get_all()
+
+    for cluster in clusters:
+        print("Cluster:", cluster)
+
+    return len(clusters) > 0
+
+
 def check_system_health(**kwargs):
     """
     Check that the InvenTree system is running OK.
@@ -19,7 +36,7 @@ def check_system_health(**kwargs):
 
     result = True
 
-    if not check_celery_worker(**kwargs):
+    if not is_q_cluster_running(**kwargs):
         result = False
         logger.warning(_("Celery worker check failed"))
 
@@ -27,13 +44,3 @@ def check_system_health(**kwargs):
         logger.warning(_("InvenTree system health checks failed"))
 
     return result
-
-
-def check_celery_worker(**kwargs):
-    """
-    Check that a celery worker is running.
-    """
-
-    # TODO - Checks that the configured celery worker thing is running
-
-    return True
diff --git a/InvenTree/templates/stats.html b/InvenTree/templates/stats.html
index 7b8a9bb93a..30d3f3d881 100644
--- a/InvenTree/templates/stats.html
+++ b/InvenTree/templates/stats.html
@@ -13,8 +13,9 @@
         <td>{% trans "Instance Name" %}</td>
         <td>{% inventree_instance_name %}</td>
     </tr>
+    {% if user.is_staff %}
     <tr>
-        <td><span class='fas fa-exclamation-triangle'></span></td>
+        <td><span class='fas fa-server'></span></td>
         <td>{% trans "Server status" %}</td>
         <td>
             {% if system_healthy %}
@@ -24,6 +25,18 @@
             {% endif %}
         </td>
     </tr>
+    <tr>
+        <td><span class='fas fa-tasks'></span></td>
+        <td>{% trans "Background Worker" %}</td>
+        <td>
+            {% if django_q_running %}
+            <span class='label label-green'>{% trans "Operational" %}</span>
+            {% else %}
+            <span class='label label-red'>{% trans "Not running" %}</span>
+            {% endif %}
+        </td>
+    </tr>
+    {% endif %}
 
     {% if not system_healthy %}
     {% for issue in system_issues %}

From 660fed9196df26ae8f96d35c490608b50cfcda3d Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 10 Mar 2021 14:03:09 +1100
Subject: [PATCH 003/111] Remove unused code from settings.py

---
 InvenTree/InvenTree/settings.py | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 0826c7d042..0a72dce33e 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -414,11 +414,6 @@ CACHES = {
     'default': {
         'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
     },
-    'qr-code': {
-        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
-        'LOCATION': 'qr-code-cache',
-        'TIMEOUT': 3600
-    }
 }
 
 # Password validation

From 5b68d82fa316486ec820de30ba2ce0cb2506ef54 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 10 Mar 2021 14:03:19 +1100
Subject: [PATCH 004/111] Skeleton for background tasks

---
 InvenTree/InvenTree/tasks.py | 39 ++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)
 create mode 100644 InvenTree/InvenTree/tasks.py

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
new file mode 100644
index 0000000000..0ebfc5a2fd
--- /dev/null
+++ b/InvenTree/InvenTree/tasks.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import json
+import requests
+import logging
+
+from datetime import timedelta
+
+
+logger = logging.getLogger(__name__)
+
+
+def delete_successful_tasks():
+    """
+    Delete successful task logs
+    which are more than a week old.
+    """
+
+    pass
+
+def check_for_updates():
+    """
+    Check if there is an update for InvenTree
+    """
+
+    response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
+
+    if not response.status_code == 200:
+        logger.warning(f'Unexpected status code from GitHub API: {response.status_code}')
+        return
+
+    data = json.loads(response.text)
+
+    # TODO 
+
+
+def test(x):
+    print(f"Running at task! {x}")
\ No newline at end of file

From 1532a0c3a18e2e32fc5491dde5b9fe11cb6d2f53 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 11 Mar 2021 17:18:57 +1100
Subject: [PATCH 005/111] Add InvenTree/apps.py

---
 InvenTree/InvenTree/apps.py     | 12 ++++++++++++
 InvenTree/InvenTree/settings.py |  1 +
 2 files changed, 13 insertions(+)
 create mode 100644 InvenTree/InvenTree/apps.py

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
new file mode 100644
index 0000000000..a13c5b8d31
--- /dev/null
+++ b/InvenTree/InvenTree/apps.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from django.apps import AppConfig
+
+
+class InvenTreeConfig(AppConfig):
+    name = 'InvenTree'
+
+    def ready(self):
+
+        print("Starting background tasks")
+        pass
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 0a72dce33e..323eade258 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -188,6 +188,7 @@ INSTALLED_APPS = [
     'build.apps.BuildConfig',
     'common.apps.CommonConfig',
     'company.apps.CompanyConfig',
+    'InvenTree.apps.InvenTreeConfig',
     'label.apps.LabelConfig',
     'order.apps.OrderConfig',
     'part.apps.PartConfig',

From 3cf5aec289fdac940a74ca6278f2503837fe2b6f Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 11 Mar 2021 19:21:28 +1100
Subject: [PATCH 006/111] Refactor

---
 InvenTree/InvenTree/apps.py     | 28 ++++++++++++++++++++++++++--
 InvenTree/InvenTree/settings.py |  2 +-
 InvenTree/InvenTree/tasks.py    | 29 +++++++++++++++++++++++++++++
 3 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index a13c5b8d31..0b5283706c 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -1,6 +1,12 @@
 # -*- coding: utf-8 -*-
 
 from django.apps import AppConfig
+import logging
+
+import InvenTree.tasks
+
+
+logger = logging.getLogger(__name__)
 
 
 class InvenTreeConfig(AppConfig):
@@ -8,5 +14,23 @@ class InvenTreeConfig(AppConfig):
 
     def ready(self):
 
-        print("Starting background tasks")
-        pass
+        self.start_background_tasks()
+
+    def start_background_tasks(self):
+
+        try:
+            from django_q.models import Schedule
+        except (AppRegistryNotReady):
+            return
+
+        logger.info("Starting background tasks...")
+
+        InvenTree.tasks.schedule_task(
+            'InvenTree.tasks.delete_successful_tasks',
+            schedule_type=Schedule.WEEKLY,
+        )
+
+        InvenTree.tasks.schedule_task(
+            'InvenTree.tasks.check_for_updates',
+            schedule_type=Schedule.DAILY
+        )
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 323eade258..2c96fa8706 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -188,13 +188,13 @@ INSTALLED_APPS = [
     'build.apps.BuildConfig',
     'common.apps.CommonConfig',
     'company.apps.CompanyConfig',
-    'InvenTree.apps.InvenTreeConfig',
     'label.apps.LabelConfig',
     'order.apps.OrderConfig',
     'part.apps.PartConfig',
     'report.apps.ReportConfig',
     'stock.apps.StockConfig',
     'users.apps.UsersConfig',
+    'InvenTree.apps.InvenTreeConfig',       # InvenTree app runs last
 
     # Third part add-ons
     'django_filters',                       # Extended filter functionality
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 0ebfc5a2fd..e34c7e8c21 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -7,10 +7,35 @@ import logging
 
 from datetime import timedelta
 
+from django.core.exceptions import AppRegistryNotReady
+
 
 logger = logging.getLogger(__name__)
 
 
+def schedule_task(taskname, **kwargs):
+    """
+    Create a scheduled task.
+    If the task has already been scheduled, ignore!
+    """
+
+    try:
+        from django_q.models import Schedule
+    except (AppRegistryNotReady):
+        logger.warning("Could not start background tasks - App registry not ready")
+        return
+
+    if Schedule.objects.filter(func=taskname).exists():
+        logger.info(f"Scheduled task '{taskname}' already exists. (Skipping)")
+    else:
+        logger.info(f"Creating scheduled task '{taskname}'")
+
+        Schedule.objects.create(
+            func=taskname,
+            **kwargs
+        )
+
+        
 def delete_successful_tasks():
     """
     Delete successful task logs
@@ -32,8 +57,12 @@ def check_for_updates():
 
     data = json.loads(response.text)
 
+    print("Response:")
+    print(data)
     # TODO 
 
+    return data
+
 
 def test(x):
     print(f"Running at task! {x}")
\ No newline at end of file

From 18defcff160acca611518b95f9dd444bc0c98a26 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 11 Mar 2021 19:56:22 +1100
Subject: [PATCH 007/111] Read version number from GitHub

---
 InvenTree/InvenTree/tasks.py   | 42 ++++++++++++++++++++++++++++------
 InvenTree/InvenTree/tests.py   | 13 +++++++++++
 InvenTree/InvenTree/version.py | 23 +++++++++++++++++++
 InvenTree/common/models.py     |  2 +-
 4 files changed, 72 insertions(+), 8 deletions(-)

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index e34c7e8c21..61b6885b03 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+import re
 import json
 import requests
 import logging
@@ -35,7 +36,7 @@ def schedule_task(taskname, **kwargs):
             **kwargs
         )
 
-        
+
 def delete_successful_tasks():
     """
     Delete successful task logs
@@ -49,6 +50,12 @@ def check_for_updates():
     Check if there is an update for InvenTree
     """
 
+    try:
+        import common.models
+        import InvenTree.version
+    except AppRegistryNotReady:
+        return
+
     response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
 
     if not response.status_code == 200:
@@ -57,12 +64,33 @@ def check_for_updates():
 
     data = json.loads(response.text)
 
-    print("Response:")
-    print(data)
-    # TODO 
+    tag = data.get('tag_name', None)
 
-    return data
+    if not tag:
+        logger.warning(f"'tag_name' missing from GitHub response")
+        return
 
+    match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
 
-def test(x):
-    print(f"Running at task! {x}")
\ No newline at end of file
+    if not len(match.groups()) == 3:
+        logger.warning(f"Version '{tag}' did not match expected pattern")
+        return
+
+    try:
+        latest_version = [int(x) for x in match.groups()]
+    except (ValueError):
+        logger.warning(f"Version '{tag}' not integer format")
+        return
+
+    if not len(latest_version) == 3:
+        logger.warning(f"Version '{tag}' is not correct format")
+        return
+
+    logger.info(f"Latest InvenTree version: '{tag}'")
+
+    # Save the version to the database
+    common.models.InvenTreeSetting.set_setting(
+        'INVENTREE_LATEST_VERSION',
+        tag,
+        None
+    )
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 96f32e7f57..1cb382e338 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
 
 from .validators import validate_overage, validate_part_name
 from . import helpers
+from . import version
 
 from mptt.exceptions import InvalidMove
 
@@ -269,3 +270,15 @@ class TestSerialNumberExtraction(TestCase):
 
         with self.assertRaises(ValidationError):
             e("10, a, 7-70j", 4)
+
+
+class TestVersionNumber(TestCase):
+
+    def test_tuple(self):
+
+        v = version.inventreeVersionTuple()
+        self.assertEqual(len(v), 3)
+
+        s = '.'.join(v)
+
+        self.assertTrue(s in version.inventreeVersion())
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 4d3d546789..6aa5c2616c 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -4,9 +4,11 @@ Provides information on the current InvenTree version
 
 import subprocess
 import django
+import re
 
 import common.models
 
+
 INVENTREE_SW_VERSION = "0.1.8 pre"
 
 # Increment this number whenever there is a significant change to the API that any clients need to know about
@@ -23,6 +25,27 @@ def inventreeVersion():
     return INVENTREE_SW_VERSION
 
 
+def inventreeVersionTuple():
+    """ Return the InvenTree version string as (maj, min, sub) tuple """
+
+    match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", INVENTREE_SW_VERSION)
+
+    return [int(g) for g in match.groups()]
+
+
+def versionTupleToInt(version):
+    """
+    Convert a version tuple (x, y, z) to an integer.
+    This simple integer can then be used for direct version comparison
+    """
+
+    n = version[0] * 1000 * 1000
+    n += version[1] * 1000
+    n += version[2]
+
+    return n
+    
+
 def inventreeApiVersion():
     return INVENTREE_API_VERSION
 
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 06c06bde05..23e0773605 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -486,7 +486,7 @@ class InvenTreeSetting(models.Model):
             create: If True, create a new setting if the specified key does not exist.
         """
 
-        if not user.is_staff:
+        if user is not None and not user.is_staff:
             return
 
         try:

From 4925f24ca9983e7f06e2a7bcdd465382b2be6698 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 11 Mar 2021 20:07:59 +1100
Subject: [PATCH 008/111] Add "up to date" info to the "about" window

---
 InvenTree/InvenTree/context.py  |  2 ++
 InvenTree/InvenTree/version.py  | 32 +++++++++++++++++++++++++++++---
 InvenTree/templates/about.html  | 19 +++++++++++++++----
 InvenTree/templates/navbar.html | 17 +++++++++++++----
 4 files changed, 59 insertions(+), 11 deletions(-)

diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py
index 1d07511fa9..43e8b904b9 100644
--- a/InvenTree/InvenTree/context.py
+++ b/InvenTree/InvenTree/context.py
@@ -42,6 +42,8 @@ def health_status(request):
 
     status['system_healthy'] = all_healthy
 
+    status['up_to_date'] = InvenTree.version.isInvenTreeUpToDate()
+
     return status
 
 
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 6aa5c2616c..bd11c50882 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -25,10 +25,13 @@ def inventreeVersion():
     return INVENTREE_SW_VERSION
 
 
-def inventreeVersionTuple():
+def inventreeVersionTuple(version=None):
     """ Return the InvenTree version string as (maj, min, sub) tuple """
 
-    match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", INVENTREE_SW_VERSION)
+    if version is None:
+        version = INVENTREE_SW_VERSION
+
+    match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", str(version))
 
     return [int(g) for g in match.groups()]
 
@@ -44,7 +47,30 @@ def versionTupleToInt(version):
     n += version[2]
 
     return n
-    
+
+
+def isInvenTreeUpToDate():
+    """
+    Test if the InvenTree instance is "up to date" with the latest version.
+
+    A background task periodically queries GitHub for latest version,
+    and stores it to the database as INVENTREE_LATEST_VERSION
+    """
+
+    latest = common.models.InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION', None)
+
+    # No record for "latest" version - we must assume we are up to date!
+    if not latest:
+        return True
+
+    # Extract "tuple" version
+    version = inventreeVersionTuple(latest)
+    version_int = versionTupleToInt(version)
+
+    inventree_int = versionTupleToInt(inventreeVersionTuple())
+
+    return inventree_int >= version_int
+
 
 def inventreeApiVersion():
     return INVENTREE_API_VERSION
diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html
index cedfb40ca1..30f9bd19d7 100644
--- a/InvenTree/templates/about.html
+++ b/InvenTree/templates/about.html
@@ -19,19 +19,30 @@
                             <col width='25'>
                             <tr>
                                 <td><span class='fas fa-hashtag'></span></td>
-                                <td>{% trans "InvenTree Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td>
+                                <td>{% trans "InvenTree Version" %}</td>
+                                <td>
+                                    <a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>
+                                    {% if up_to_date %}
+                                    <span class='label label-green float-right'>{% trans "Up to Date" %}</span>
+                                    {% else %}
+                                    <span class='label label-red float-right'>{% trans "Update Available" %}</span>
+                                    {% endif %}
+                                </td>
                             </tr>
                             <tr>
                                 <td><span class='fas fa-hashtag'></span></td>
-                                <td>{% trans "Django Version" %}</td><td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
+                                <td>{% trans "Django Version" %}</td>
+                                <td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
                             </tr>
                             <tr>
                                 <td><span class='fas fa-code-branch'></span></td>
-                                <td>{% trans "Commit Hash" %}</td><td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td>
+                                <td>{% trans "Commit Hash" %}</td>
+                                <td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td>
                             </tr>
                             <tr>
                                 <td><span class='fas fa-calendar-alt'></span></td>
-                                <td>{% trans "Commit Date" %}</td><td>{% inventree_commit_date %}</td>
+                                <td>{% trans "Commit Date" %}</td>
+                                <td>{% inventree_commit_date %}</td>
                             </tr>
                             <tr>
                                 <td><span class='fas fa-book'></span></td>
diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html
index 6e3fe024ca..c52e87d753 100644
--- a/InvenTree/templates/navbar.html
+++ b/InvenTree/templates/navbar.html
@@ -59,8 +59,8 @@
           {% endif %}
           <li class='dropdown'>
             <a class='dropdown-toggle' data-toggle='dropdown' href="#">
-              {% if not system_healthy %}
-              <span title='{% trans "InvenTree server issues detected" %}' class='fas fa-exclamation-triangle icon-red'></span>
+              {% if not system_healthy or not up_to_date %}
+              <span class='fas fa-exclamation-triangle icon-red'></span>
               {% endif %}
               <span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
             <ul class='dropdown-menu'>
@@ -78,11 +78,20 @@
                   {% if system_healthy %}
                   <span class='fas fa-server'>
                   {% else %}
-                  <span class='fas fa-exclamation-triangle icon-red'>
+                  <span class='fas fa-server icon-red'>
                   {% endif %}
                   </span> {% trans "System Information" %}
                 </a></li>
-                <li id='launch-about'><a href='#'><span class="fas fa-info-circle"></span> {% trans "About InvenTree" %}</a></li>
+                <li id='launch-about'>
+                  <a href='#'>
+                    {% if up_to_date %}
+                    <span class="fas fa-info-circle">
+                    {% else %}
+                    <span class='fas fa-info-circle icon-red'>
+                    {% endif %}
+                    </span> {% trans "About InvenTree" %}
+                  </a>
+                </li>
               </ul>
             </li> 
       </ul>

From bfb0cb3b4754109657fb29c353d450f4de94f5dd Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 15:27:28 +1100
Subject: [PATCH 009/111] Add a "heartbeat" task which runs every 5 minutes

- Allows us to track if the worker is running
- Due to Stat.get_all() not always working
---
 InvenTree/InvenTree/apps.py     | 10 +++++++++-
 InvenTree/InvenTree/status.py   | 23 ++++++++++++++++++++---
 InvenTree/InvenTree/tasks.py    | 17 +++++++++++++----
 InvenTree/templates/navbar.html |  4 +++-
 4 files changed, 45 insertions(+), 9 deletions(-)

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 0b5283706c..902ef2c648 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -1,8 +1,10 @@
 # -*- coding: utf-8 -*-
 
-from django.apps import AppConfig
 import logging
 
+from django.apps import AppConfig
+from django.core.exceptions import AppRegistryNotReady
+
 import InvenTree.tasks
 
 
@@ -34,3 +36,9 @@ class InvenTreeConfig(AppConfig):
             'InvenTree.tasks.check_for_updates',
             schedule_type=Schedule.DAILY
         )
+
+        InvenTree.tasks.schedule_task(
+            'InvenTree.tasks.heartbeat',
+            schedule_type=Schedule.MINUTES,
+            minutes=5
+        )
\ No newline at end of file
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index e02e476a51..1f6b01053c 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -6,9 +6,11 @@ Provides system status functionality checks.
 from __future__ import unicode_literals
 
 import logging
+from datetime import datetime, timedelta
 
 from django.utils.translation import ugettext as _
 
+from django_q.models import Success
 from django_q.monitor import Stat
 
 logger = logging.getLogger(__name__)
@@ -21,10 +23,25 @@ def is_q_cluster_running(**kwargs):
 
     clusters = Stat.get_all()
 
-    for cluster in clusters:
-        print("Cluster:", cluster)
+    if len(clusters) > 0:
+        return True
 
-    return len(clusters) > 0
+    """
+    Sometimes Stat.get_all() returns [].
+    In this case we have the 'heartbeat' task running every five minutes.
+    Check to see if we have a result within the last ten minutes
+    """
+
+    now = datetime.now()
+    past = now - timedelta(minutes=10)
+
+    results = Success.objects.filter(
+        func='InvenTree.tasks.heartbeat',
+        started__gte=past
+    )
+
+    # If any results are returned, then the background worker is running!
+    return results.exists()
 
 
 def check_system_health(**kwargs):
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 61b6885b03..985b9e24da 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -6,8 +6,6 @@ import json
 import requests
 import logging
 
-from datetime import timedelta
-
 from django.core.exceptions import AppRegistryNotReady
 
 
@@ -37,6 +35,17 @@ def schedule_task(taskname, **kwargs):
         )
 
 
+def heartbeat():
+    """
+    Simple task which runs at 5 minute intervals,
+    so we can determine that the background worker
+    is actually running.
+
+    (There is probably a less "hacky" way of achieving this)
+    """
+    pass
+
+
 def delete_successful_tasks():
     """
     Delete successful task logs
@@ -45,6 +54,7 @@ def delete_successful_tasks():
 
     pass
 
+
 def check_for_updates():
     """
     Check if there is an update for InvenTree
@@ -52,7 +62,6 @@ def check_for_updates():
 
     try:
         import common.models
-        import InvenTree.version
     except AppRegistryNotReady:
         return
 
@@ -67,7 +76,7 @@ def check_for_updates():
     tag = data.get('tag_name', None)
 
     if not tag:
-        logger.warning(f"'tag_name' missing from GitHub response")
+        logger.warning("'tag_name' missing from GitHub response")
         return
 
     match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html
index c52e87d753..acd71f0cd8 100644
--- a/InvenTree/templates/navbar.html
+++ b/InvenTree/templates/navbar.html
@@ -59,8 +59,10 @@
           {% endif %}
           <li class='dropdown'>
             <a class='dropdown-toggle' data-toggle='dropdown' href="#">
-              {% if not system_healthy or not up_to_date %}
+              {% if not system_healthy %}
               <span class='fas fa-exclamation-triangle icon-red'></span>
+              {% elif not up_to_date %}
+              <span class='fas fa-info-circle icon-green'></span>
               {% endif %}
               <span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
             <ul class='dropdown-menu'>

From 5b8eb1c53089cd45d91725a937b386ee93a478ee Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 15:27:53 +1100
Subject: [PATCH 010/111] Newline

---
 InvenTree/InvenTree/apps.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 902ef2c648..1022a75301 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -41,4 +41,4 @@ class InvenTreeConfig(AppConfig):
             'InvenTree.tasks.heartbeat',
             schedule_type=Schedule.MINUTES,
             minutes=5
-        )
\ No newline at end of file
+        )

From 006dd10a7958de7e565c766458d8dc60fbc615e5 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 15:35:33 +1100
Subject: [PATCH 011/111] Delete successful tasks more than a month old

---
 InvenTree/InvenTree/tasks.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 985b9e24da..def9abc35d 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -6,6 +6,7 @@ import json
 import requests
 import logging
 
+from datetime import datetime, timedelta
 from django.core.exceptions import AppRegistryNotReady
 
 
@@ -49,10 +50,16 @@ def heartbeat():
 def delete_successful_tasks():
     """
     Delete successful task logs
-    which are more than a week old.
+    which are more than a month old.
     """
 
-    pass
+    threshold = datetime.now() - timedelta(days=30)
+
+    results = Success.objects.filter(
+        started__lte=threshold
+    )
+
+    results.delete()
 
 
 def check_for_updates():

From ef4dbda22304e6a2261abc960d4d5d449df3353d Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 15:35:55 +1100
Subject: [PATCH 012/111] Catch errors if the DB is not up

---
 InvenTree/InvenTree/tasks.py | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index def9abc35d..3dcfb66422 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -7,7 +7,9 @@ import requests
 import logging
 
 from datetime import datetime, timedelta
+
 from django.core.exceptions import AppRegistryNotReady
+from django.db.utils import OperationalError, ProgrammingError
 
 
 logger = logging.getLogger(__name__)
@@ -25,15 +27,19 @@ def schedule_task(taskname, **kwargs):
         logger.warning("Could not start background tasks - App registry not ready")
         return
 
-    if Schedule.objects.filter(func=taskname).exists():
-        logger.info(f"Scheduled task '{taskname}' already exists. (Skipping)")
-    else:
-        logger.info(f"Creating scheduled task '{taskname}'")
+    try:
+        if Schedule.objects.filter(func=taskname).exists():
+            logger.info(f"Scheduled task '{taskname}' already exists. (Skipping)")
+        else:
+            logger.info(f"Creating scheduled task '{taskname}'")
 
-        Schedule.objects.create(
-            func=taskname,
-            **kwargs
-        )
+            Schedule.objects.create(
+                func=taskname,
+                **kwargs
+            )
+    except (OperationalError, ProgrammingError):
+        # Required if the DB is not ready yet
+        pass
 
 
 def heartbeat():

From 9d404afec0be035e8166fa1ee70934b27080028b Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 16:00:25 +1100
Subject: [PATCH 013/111] Add 'ignore' rules for the django-q tables

---
 InvenTree/users/models.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 5c95abfe46..57821bb22f 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -132,6 +132,13 @@ class RuleSet(models.Model):
         'error_report_error',
         'exchange_rate',
         'exchange_exchangebackend',
+
+        # Django-q
+        'django_q_ormq',
+        'django_q_failure',
+        'django_q_task',
+        'django_q_schedule',
+        'django_q_success',
     ]
 
     RULE_OPTIONS = [

From 18b559fee7cb4fcae802c8f1a76521f635fd2f02 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 16:28:54 +1100
Subject: [PATCH 014/111] Fix for unit test

---
 InvenTree/InvenTree/tests.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 1cb382e338..5fadb7b68d 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -279,6 +279,6 @@ class TestVersionNumber(TestCase):
         v = version.inventreeVersionTuple()
         self.assertEqual(len(v), 3)
 
-        s = '.'.join(v)
+        s = '.'.join([str(i) for i in v])
 
         self.assertTrue(s in version.inventreeVersion())

From 700effcee794be4c46fd6c14b633bc1142ed6a51 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 12 Mar 2021 16:57:27 +1100
Subject: [PATCH 015/111] Remove celery reference

---
 InvenTree/InvenTree/status.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index 1f6b01053c..f7c955c394 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -55,7 +55,7 @@ def check_system_health(**kwargs):
 
     if not is_q_cluster_running(**kwargs):
         result = False
-        logger.warning(_("Celery worker check failed"))
+        logger.warning(_("Background worker check failed"))
 
     if not result:
         logger.warning(_("InvenTree system health checks failed"))

From de85d614512ce596568bf17caa855b3f71f86419 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 15 Mar 2021 08:31:19 +1100
Subject: [PATCH 016/111] Directly compare version tuples, rather than
 converting to primitive

---
 InvenTree/InvenTree/version.py | 23 ++++-------------------
 1 file changed, 4 insertions(+), 19 deletions(-)

diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index bd11c50882..05946059fc 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -36,19 +36,6 @@ def inventreeVersionTuple(version=None):
     return [int(g) for g in match.groups()]
 
 
-def versionTupleToInt(version):
-    """
-    Convert a version tuple (x, y, z) to an integer.
-    This simple integer can then be used for direct version comparison
-    """
-
-    n = version[0] * 1000 * 1000
-    n += version[1] * 1000
-    n += version[2]
-
-    return n
-
-
 def isInvenTreeUpToDate():
     """
     Test if the InvenTree instance is "up to date" with the latest version.
@@ -63,13 +50,11 @@ def isInvenTreeUpToDate():
     if not latest:
         return True
 
-    # Extract "tuple" version
-    version = inventreeVersionTuple(latest)
-    version_int = versionTupleToInt(version)
+    # Extract "tuple" version (Python can directly compare version tuples)
+    latest_version = inventreeVersionTuple(latest)
+    inventree_version = inventreeVersionTuple()
 
-    inventree_int = versionTupleToInt(inventreeVersionTuple())
-
-    return inventree_int >= version_int
+    return inventree_version >= latest_version
 
 
 def inventreeApiVersion():

From f6dd710d6eec11fc81384134703e715ca56643d9 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 15 Mar 2021 08:35:06 +1100
Subject: [PATCH 017/111] Automatically delete old heartbeat messages

---
 InvenTree/InvenTree/tasks.py | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 3dcfb66422..ca84f93ee7 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -8,6 +8,7 @@ import logging
 
 from datetime import datetime, timedelta
 
+from django_q.models import Success
 from django.core.exceptions import AppRegistryNotReady
 from django.db.utils import OperationalError, ProgrammingError
 
@@ -48,9 +49,19 @@ def heartbeat():
     so we can determine that the background worker
     is actually running.
 
-    (There is probably a less "hacky" way of achieving this)
+    (There is probably a less "hacky" way of achieving this)?
     """
-    pass
+
+    threshold = datetime.now() - timedelta(minutes=30)
+
+    # Delete heartbeat results more than half an hour old,
+    # otherwise they just create extra noise
+    heartbeats = Success.objects.filter(
+        func='InvenTree.tasks.heartbeat',
+        started__lte=threshold
+    )
+
+    heartbeats.delete()
 
 
 def delete_successful_tasks():

From 6ea846ce45cbe114e2cb6b9e30ba9e35c7529864 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 15 Mar 2021 08:36:27 +1100
Subject: [PATCH 018/111] Add a #TODO

---
 InvenTree/InvenTree/status.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index f7c955c394..8c65bdc302 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -24,6 +24,7 @@ def is_q_cluster_running(**kwargs):
     clusters = Stat.get_all()
 
     if len(clusters) > 0:
+        # TODO - Introspect on any cluster information
         return True
 
     """

From 24823adc6df036bf7b79be366e505c33e3a4247f Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 15 Mar 2021 08:51:50 +1100
Subject: [PATCH 019/111] Adds unit tests for version number comparison

---
 InvenTree/InvenTree/tasks.py |  7 ++++++-
 InvenTree/InvenTree/tests.py | 18 ++++++++++++++++++
 2 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index ca84f93ee7..96f53e1e4c 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -8,7 +8,6 @@ import logging
 
 from datetime import datetime, timedelta
 
-from django_q.models import Success
 from django.core.exceptions import AppRegistryNotReady
 from django.db.utils import OperationalError, ProgrammingError
 
@@ -52,6 +51,12 @@ def heartbeat():
     (There is probably a less "hacky" way of achieving this)?
     """
 
+    try:
+        from django_q.models import Success
+        logger.warning("Could not perform heartbeat task - App registry not ready")
+    except AppRegistryNotReady:
+        return
+
     threshold = datetime.now() - timedelta(minutes=30)
 
     # Delete heartbeat results more than half an hour old,
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 5fadb7b68d..0da45e9373 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -273,6 +273,9 @@ class TestSerialNumberExtraction(TestCase):
 
 
 class TestVersionNumber(TestCase):
+    """
+    Unit tests for version number functions
+    """ 
 
     def test_tuple(self):
 
@@ -282,3 +285,18 @@ class TestVersionNumber(TestCase):
         s = '.'.join([str(i) for i in v])
 
         self.assertTrue(s in version.inventreeVersion())
+
+    def test_comparison(self):
+        """
+        Test direct comparison of version numbers
+        """
+
+        v_a = version.inventreeVersionTuple('1.2.0')
+        v_b = version.inventreeVersionTuple('1.2.3')
+        v_c = version.inventreeVersionTuple('1.2.4')
+        v_d = version.inventreeVersionTuple('2.0.0')
+
+        self.assertTrue(v_b > v_a)
+        self.assertTrue(v_c > v_b)
+        self.assertTrue(v_d > v_c)
+        self.assertTrue(v_d > v_a)

From c1aed51de1aeb7f3bbb0c9378d22349dfb47a38e Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 15 Mar 2021 09:34:32 +1100
Subject: [PATCH 020/111] Fix import error

---
 InvenTree/InvenTree/settings.py | 9 +++++++--
 InvenTree/InvenTree/tasks.py    | 6 ++++++
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 2c96fa8706..73a38b484d 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -45,6 +45,8 @@ def get_setting(environment_var, backup_val, default_value=None):
 
     return default_value
 
+# Determine if we are running in "test" mode e.g. "manage.py test"
+TESTING = 'test' in sys.argv
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -336,7 +338,7 @@ DATABASES = {}
 When running unit tests, enforce usage of sqlite3 database,
 so that the tests can be run in RAM without any setup requirements
 """
-if 'test' in sys.argv:
+if TESTING:
     logger.info('InvenTree: Running tests - Using sqlite3 memory database')
     DATABASES['default'] = {
         # Ensure sqlite3 backend is being used
@@ -479,7 +481,10 @@ USE_I18N = True
 
 USE_L10N = True
 
-USE_TZ = True
+# Do not use native timezone support in "test" mode
+# It generates a *lot* of cruft in the logs
+if not TESTING:
+    USE_TZ = True
 
 DATE_INPUT_FORMATS = [
     "%Y-%m-%d",
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 96f53e1e4c..4ba34ca7ba 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -75,6 +75,12 @@ def delete_successful_tasks():
     which are more than a month old.
     """
 
+    try:
+        from django_q.models import Success
+        logger.warning("Could not perform 'delete_successful_tasks' - App registry not ready")
+    except AppRegistryNotReady:
+        return
+
     threshold = datetime.now() - timedelta(days=30)
 
     results = Success.objects.filter(

From c6e154f996255e7f9e401e12696fd2072e1905f2 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 15 Mar 2021 10:15:48 +1100
Subject: [PATCH 021/111] PEP style fixes

---
 InvenTree/InvenTree/settings.py | 1 +
 InvenTree/InvenTree/tests.py    | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 73a38b484d..6d7c937d54 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -45,6 +45,7 @@ def get_setting(environment_var, backup_val, default_value=None):
 
     return default_value
 
+
 # Determine if we are running in "test" mode e.g. "manage.py test"
 TESTING = 'test' in sys.argv
 
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 0da45e9373..8465473901 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -275,7 +275,7 @@ class TestSerialNumberExtraction(TestCase):
 class TestVersionNumber(TestCase):
     """
     Unit tests for version number functions
-    """ 
+    """
 
     def test_tuple(self):
 

From 283663633aa3341f8fad44a4a0cfa864a32d8980 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 19 Mar 2021 21:52:36 +1100
Subject: [PATCH 022/111] First pass at a supervisor.conf file

---
 deploy/inventree.conf | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 deploy/inventree.conf

diff --git a/deploy/inventree.conf b/deploy/inventree.conf
new file mode 100644
index 0000000000..5893b19e67
--- /dev/null
+++ b/deploy/inventree.conf
@@ -0,0 +1,31 @@
+; # Supervisor Config File
+; Example configuration file for running InvenTree using supervisor
+; There are two separate processes which must be managed:
+;
+; ## Web Server
+; The InvenTree server must be launched and managed as a process
+; The recommended way to handle the web server is to use gunicorn
+;
+; ## Background Tasks
+; A background task manager processes long-running and periodic tasks
+; InvenTree uses django-q for this purpose
+
+[supervisord]
+environment=INVENTREE_SRC_DIR="/mnt/c/inventree/InvenTree", INVENTREE_BIN_DIR="/mnt/c/inventree/InvenTree"
+
+[program:inventree-server]
+directory=/mnt/c/inventree/InvenTree
+command=/mnt/c/inventree/inventree-env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
+autostart=true
+autorestart=true
+startretries=3
+stderr_logfile=/var/log/inventree/server.err.log
+stdout_logfile=/var/log/inventree/server.out.log
+
+[program:inventree-cluster]
+directory=/mnt/c/inventree/InvenTree
+command=/mnt/c/inventree/inventree-env/bin/python manage.py qcluster
+autostart=true
+autorestart=true
+stderr_logfile=/var/log/inventree/cluster.err.log
+stdout_logfile=/var/log/inventree/cluster.out.log

From b7718d9c6c59361ae7d269c2d5e3061cfc01286f Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 19 Mar 2021 22:08:11 +1100
Subject: [PATCH 023/111] Specify user and logfile

---
 deploy/inventree.conf | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/deploy/inventree.conf b/deploy/inventree.conf
index 5893b19e67..8acaf04cac 100644
--- a/deploy/inventree.conf
+++ b/deploy/inventree.conf
@@ -11,7 +11,8 @@
 ; InvenTree uses django-q for this purpose
 
 [supervisord]
-environment=INVENTREE_SRC_DIR="/mnt/c/inventree/InvenTree", INVENTREE_BIN_DIR="/mnt/c/inventree/InvenTree"
+;environment=INVENTREE_SRC_DIR="/mnt/c/inventree/InvenTree", INVENTREE_BIN_DIR="/mnt/c/inventree/InvenTree"
+logfile=/var/log/inventree/supervisor.log
 
 [program:inventree-server]
 directory=/mnt/c/inventree/InvenTree
@@ -21,6 +22,7 @@ autorestart=true
 startretries=3
 stderr_logfile=/var/log/inventree/server.err.log
 stdout_logfile=/var/log/inventree/server.out.log
+user=inventree
 
 [program:inventree-cluster]
 directory=/mnt/c/inventree/InvenTree
@@ -29,3 +31,4 @@ autostart=true
 autorestart=true
 stderr_logfile=/var/log/inventree/cluster.err.log
 stdout_logfile=/var/log/inventree/cluster.out.log
+user=inventree
\ No newline at end of file

From 8fd666e6629653fe3675708d2cc2e5c122dc3c0d Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 22 Mar 2021 11:20:09 +1100
Subject: [PATCH 024/111] Improvements for "check for updates" task

- Let it throw an error if something fails
- Errors are caught as "unsuccessful tasks"
---
 InvenTree/InvenTree/apps.py  |  2 +-
 InvenTree/InvenTree/tasks.py | 21 ++++++++++-----------
 2 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 1022a75301..6a2e363789 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -29,7 +29,7 @@ class InvenTreeConfig(AppConfig):
 
         InvenTree.tasks.schedule_task(
             'InvenTree.tasks.delete_successful_tasks',
-            schedule_type=Schedule.WEEKLY,
+            schedule_type=Schedule.DAILY,
         )
 
         InvenTree.tasks.schedule_task(
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 4ba34ca7ba..eba145c660 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -21,6 +21,9 @@ def schedule_task(taskname, **kwargs):
     If the task has already been scheduled, ignore!
     """
 
+    # If unspecified, repeat indefinitely
+    repeats = kwargs.pop('repeats', -1)
+
     try:
         from django_q.models import Schedule
     except (AppRegistryNotReady):
@@ -34,7 +37,9 @@ def schedule_task(taskname, **kwargs):
             logger.info(f"Creating scheduled task '{taskname}'")
 
             Schedule.objects.create(
+                name=taskname,
                 func=taskname,
+                repeats=repeats,
                 **kwargs
             )
     except (OperationalError, ProgrammingError):
@@ -98,21 +103,20 @@ def check_for_updates():
     try:
         import common.models
     except AppRegistryNotReady:
+        # Apps not yet loaded!
         return
 
     response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
 
     if not response.status_code == 200:
-        logger.warning(f'Unexpected status code from GitHub API: {response.status_code}')
-        return
+        raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
 
     data = json.loads(response.text)
 
     tag = data.get('tag_name', None)
 
     if not tag:
-        logger.warning("'tag_name' missing from GitHub response")
-        return
+        raise ValueError("'tag_name' missing from GitHub response")
 
     match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
 
@@ -120,15 +124,10 @@ def check_for_updates():
         logger.warning(f"Version '{tag}' did not match expected pattern")
         return
 
-    try:
-        latest_version = [int(x) for x in match.groups()]
-    except (ValueError):
-        logger.warning(f"Version '{tag}' not integer format")
-        return
+    latest_version = [int(x) for x in match.groups()]
 
     if not len(latest_version) == 3:
-        logger.warning(f"Version '{tag}' is not correct format")
-        return
+        raise ValueError(f"Version '{tag}' is not correct format")
 
     logger.info(f"Latest InvenTree version: '{tag}'")
 

From edbbfff1afe5f137ec14a9dd38ac19f8a43c8293 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Tue, 23 Mar 2021 19:58:29 +1100
Subject: [PATCH 025/111] Reduce frequency of heartbeat

---
 InvenTree/InvenTree/apps.py   | 2 +-
 InvenTree/InvenTree/status.py | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 6a2e363789..07f565e012 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -40,5 +40,5 @@ class InvenTreeConfig(AppConfig):
         InvenTree.tasks.schedule_task(
             'InvenTree.tasks.heartbeat',
             schedule_type=Schedule.MINUTES,
-            minutes=5
+            minutes=15
         )
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index 8c65bdc302..841a54f92b 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -29,12 +29,12 @@ def is_q_cluster_running(**kwargs):
 
     """
     Sometimes Stat.get_all() returns [].
-    In this case we have the 'heartbeat' task running every five minutes.
-    Check to see if we have a result within the last ten minutes
+    In this case we have the 'heartbeat' task running every 15 minutes.
+    Check to see if we have a result within the last 20 minutes
     """
 
     now = datetime.now()
-    past = now - timedelta(minutes=10)
+    past = now - timedelta(minutes=20)
 
     results = Success.objects.filter(
         func='InvenTree.tasks.heartbeat',

From e3f49b8996a664d5f1b9012327c6954761a8dc97 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 24 Mar 2021 08:31:53 +1100
Subject: [PATCH 026/111] Install invoke and gunicorn as part of
 requirements.txt

---
 requirements.txt | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/requirements.txt b/requirements.txt
index 2c8bd9f3e7..4749a78a91 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+invoke>=1.4.0                   # Invoke build tool
 wheel>=0.34.2                   # Wheel
 Django==3.0.7                   # Django package
 pillow==7.1.0                   # Image manipulation
@@ -31,5 +32,6 @@ django-test-migrations==1.1.0   # Unit testing for database migrations
 python-barcode[images]==0.13.1  # Barcode generator
 qrcode[pil]==6.1                # QR code generator
 django-q==1.3.4                 # Background task scheduling
+gunicorn>=20.0.4                # Gunicorn web server
 
 inventree                       # Install the latest version of the InvenTree API python library

From ce64feb79dcb084750d53ce936c070a1aa165bb1 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 24 Mar 2021 08:32:00 +1100
Subject: [PATCH 027/111] Update supervisor conf file

---
 deploy/inventree.conf | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/deploy/inventree.conf b/deploy/inventree.conf
index 8acaf04cac..a99bbe58e3 100644
--- a/deploy/inventree.conf
+++ b/deploy/inventree.conf
@@ -11,24 +11,28 @@
 ; InvenTree uses django-q for this purpose
 
 [supervisord]
-;environment=INVENTREE_SRC_DIR="/mnt/c/inventree/InvenTree", INVENTREE_BIN_DIR="/mnt/c/inventree/InvenTree"
-logfile=/var/log/inventree/supervisor.log
+; Change this path if log files are stored elsewhere
+logfile=/home/inventree/log/supervisor.log
 
 [program:inventree-server]
+user=inventree
 directory=/mnt/c/inventree/InvenTree
 command=/mnt/c/inventree/inventree-env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
+startsecs=10
 autostart=true
 autorestart=true
 startretries=3
-stderr_logfile=/var/log/inventree/server.err.log
-stdout_logfile=/var/log/inventree/server.out.log
-user=inventree
+; Change these paths if log files are stored elsewhere
+stderr_logfile=/home/inventree/log/server.err.log
+stdout_logfile=/home/inventree/log/server.out.log
 
 [program:inventree-cluster]
+user=inventree
 directory=/mnt/c/inventree/InvenTree
 command=/mnt/c/inventree/inventree-env/bin/python manage.py qcluster
+startsecs=10
 autostart=true
 autorestart=true
-stderr_logfile=/var/log/inventree/cluster.err.log
-stdout_logfile=/var/log/inventree/cluster.out.log
-user=inventree
\ No newline at end of file
+; Change these paths if log files are stored elsewhere
+stderr_logfile=/home/inventree/log/cluster.err.log
+stdout_logfile=/home/inventree/log/cluster.out.log
\ No newline at end of file

From df0ab2359f8c3911d644cf7aba36f1c0c4400e0c Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 24 Mar 2021 22:24:47 +1100
Subject: [PATCH 028/111] Remove invoke tasks which perform system commands

- tasks.py is now for InvenTree specific tasks only
---
 InvenTree/InvenTree/settings.py |  1 -
 InvenTree/config_template.yaml  | 31 ++++++++++++++++++-------------
 deploy/inventree.conf           |  8 ++++----
 tasks.py                        | 22 ----------------------
 4 files changed, 22 insertions(+), 40 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 6d7c937d54..73a38b484d 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -45,7 +45,6 @@ def get_setting(environment_var, backup_val, default_value=None):
 
     return default_value
 
-
 # Determine if we are running in "test" mode e.g. "manage.py test"
 TESTING = 'test' in sys.argv
 
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 18e3197cca..343ab312e4 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -7,11 +7,9 @@
 #       with the prefix INVENTREE_DB_
 #       e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
 database:
-  # Default configuration - sqlite filesystem database
-  ENGINE: sqlite3
-  NAME: '../inventree_default_db.sqlite3' 
+  # Uncomment (and edit) one of the database configurations below,
+  # or specify database options using environment variables
   
-  # For more complex database installations, further parameters are required
   # Refer to the django documentation for full list of options
   
   # --- Available options: ---
@@ -27,14 +25,22 @@ database:
 
   # --- Example Configuration - sqlite3 ---
   # ENGINE: sqlite3
-  # NAME: '/path/to/database.sqlite3'
+  # NAME: '/home/inventree/database.sqlite3'
 
   # --- Example Configuration - MySQL ---
   #ENGINE: django.db.backends.mysql
   #NAME: inventree
-  #USER: inventree_username
+  #USER: inventree
   #PASSWORD: inventree_password
-  #HOST: '127.0.0.1'
+  #HOST: 'localhost'
+  #PORT: '3306'
+
+  # --- Example Configuration - Postgresql ---
+  #ENGINE: django.db.backends.postgresql
+  #NAME: inventree
+  #USER: inventree
+  #PASSWORD: inventree_password
+  #HOST: 'localhost'
   #PORT: '5432'
 
 # Select default system language (default is 'en-us')
@@ -86,13 +92,12 @@ cors:
   # - https://sub.example.com
 
 # MEDIA_ROOT is the local filesystem location for storing uploaded files
-# By default, it is stored in a directory named 'inventree_media' local to the InvenTree directory
-# This should be changed for a production installation
-media_root: '../inventree_media'
+# By default, it is stored under /home/inventree
+media_root: '/home/inventree/media'
 
 # STATIC_ROOT is the local filesystem location for storing static files
-# By default it is stored in a directory named 'inventree_static' local to the InvenTree directory
-static_root: '../inventree_static'
+# By default, it is stored under /home/inventree
+static_root: '/home/inventree/static'
 
 # Optional URL schemes to allow in URL fields
 # By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
@@ -105,7 +110,7 @@ static_root: '../inventree_static'
 # Backup options
 # Set the backup_dir parameter to store backup files in a specific location
 # If unspecified, the local user's temp directory will be used
-#backup_dir: '/home/inventree/backup/'
+backup_dir: '/home/inventree/backup/'
 
 # Permit custom authentication backends
 #authentication_backends:
diff --git a/deploy/inventree.conf b/deploy/inventree.conf
index a99bbe58e3..782b6ae2f6 100644
--- a/deploy/inventree.conf
+++ b/deploy/inventree.conf
@@ -16,8 +16,8 @@ logfile=/home/inventree/log/supervisor.log
 
 [program:inventree-server]
 user=inventree
-directory=/mnt/c/inventree/InvenTree
-command=/mnt/c/inventree/inventree-env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
+directory=/home/inventree/src/InvenTree
+command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
 startsecs=10
 autostart=true
 autorestart=true
@@ -28,8 +28,8 @@ stdout_logfile=/home/inventree/log/server.out.log
 
 [program:inventree-cluster]
 user=inventree
-directory=/mnt/c/inventree/InvenTree
-command=/mnt/c/inventree/inventree-env/bin/python manage.py qcluster
+directory=/home/inventree/src/InvenTree
+command=/home/inventree/env/bin/python manage.py qcluster
 startsecs=10
 autostart=true
 autorestart=true
diff --git a/tasks.py b/tasks.py
index 579887c809..179d73546a 100644
--- a/tasks.py
+++ b/tasks.py
@@ -231,28 +231,6 @@ def coverage(c):
     # Generate coverage report
     c.run('coverage html')
 
-@task
-def mysql(c):
-    """
-    Install packages required for using InvenTree with a MySQL database.
-    """
-    
-    print('Installing packages required for MySQL')
-
-    c.run('sudo apt-get install mysql-server libmysqlclient-dev')
-    c.run('pip3 install mysqlclient')
-
-@task
-def postgresql(c):
-    """
-    Install packages required for using InvenTree with a PostgreSQL database
-    """
-
-    print("Installing packages required for PostgreSQL")
-
-    c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
-    c.run('pip3 install psycopg2')
-
 @task(help={'filename': "Output filename (default = 'data.json')"})
 def export_records(c, filename='data.json'):
     """

From 3a0c68bf5c0cd84b225fcbe32fd2cba12e5217f0 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 24 Mar 2021 22:42:04 +1100
Subject: [PATCH 029/111] Add invoke task to start background worker

---
 tasks.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/tasks.py b/tasks.py
index 179d73546a..2d7d395d10 100644
--- a/tasks.py
+++ b/tasks.py
@@ -111,6 +111,13 @@ def shell(c):
 
     manage(c, 'shell', pty=True)
 
+@task
+def worker(c):
+    """
+    Run the InvenTree background worker process
+    """
+
+    manage(c, 'qcluster', pty=True)
 
 @task
 def superuser(c):

From 3ddbb6a6cd9e85f5d81f845ce5ef564db5091a7c Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Tue, 30 Mar 2021 20:53:26 +1100
Subject: [PATCH 030/111] Check for empty values

---
 InvenTree/InvenTree/settings.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 73a38b484d..2822202f13 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -362,6 +362,10 @@ else:
     # Extract database configuration from the config.yaml file
     db_config = CONFIG.get('database', {})
 
+    # Default action if db_config not specified in yaml file
+    if not db_config:
+        db_config = {}
+
     # If a particular database option is not specified in the config file,
     # look for it in the environmental variables
     # e.g. INVENTREE_DB_NAME / INVENTREE_DB_USER / etc

From 39b2c5f943cc3de5de03e6ca347411c40a4c1cef Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Tue, 30 Mar 2021 21:18:09 +1100
Subject: [PATCH 031/111] Reintroduce default database config

---
 InvenTree/config_template.yaml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 343ab312e4..c529182bb7 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -10,6 +10,11 @@ database:
   # Uncomment (and edit) one of the database configurations below,
   # or specify database options using environment variables
   
+  # Default installation uses a simple sqlite database
+  # For production, consider changing this!
+  ENGINE: sqlite3
+  NAME: '/home/inventree/database.sqlite3'
+
   # Refer to the django documentation for full list of options
   
   # --- Available options: ---

From e7ed4c4eab1fdaf8585a57fa1271b8be9a7ef604 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Tue, 30 Mar 2021 21:24:06 +1100
Subject: [PATCH 032/111] Travis fixes

---
 .travis.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index 52d0ef1c5c..fdf3be5bc7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,8 @@ addons:
         - sqlite3
 
 before_install:
+    - sudo useradd --create-home inventree
+    - sudo mkdir /home/inventree/media /home/media/static /home/media/backup
     - sudo apt-get update
     - sudo apt-get install gettext
     - sudo apt-get install mysql-server libmysqlclient-dev

From 83f8afe113022b6f7ca6d6f3d0ed0caecdd7ba87 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Tue, 30 Mar 2021 21:33:49 +1100
Subject: [PATCH 033/111] Add github actions

---
 .github/workflows/ci.yaml    | 32 ++++++++++++++++++++++++++++++++
 .github/workflows/style.yaml | 11 +++++++++++
 2 files changed, 43 insertions(+)
 create mode 100644 .github/workflows/ci.yaml
 create mode 100644 .github/workflows/style.yaml

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000000..f935bdda7f
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,32 @@
+name: Django CI
+
+on:
+  push:
+    branches: [ $default-branch ]
+  pull_request:
+    branches: [ $default-branch ]
+
+jobs:
+
+  # Run tests on an SQLite database
+  sqlite:
+
+    runs-on: ubuntu-latest
+
+    strategy:
+      max-parallel: 4
+      matrix:
+        python-version: [3.7, 3.8, 3.9]
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          pip3 install invoke
+          invoke install
+      - name: Run Tests
+        run: invoke coverage
\ No newline at end of file
diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml
new file mode 100644
index 0000000000..90dae47857
--- /dev/null
+++ b/.github/workflows/style.yaml
@@ -0,0 +1,11 @@
+name: Style Checks
+
+on: push
+
+jobs:
+  pep:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
\ No newline at end of file

From 3f257279ee3a53ba7bfa1f27c3daec8842617770 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:31:50 +1100
Subject: [PATCH 034/111] Specify directories for CI

---
 .github/workflows/coverage.yaml   | 2 ++
 .github/workflows/mariadb.yaml    | 2 ++
 .github/workflows/mysql.yaml      | 2 ++
 .github/workflows/postgresql.yaml | 2 ++
 InvenTree/config_template.yaml    | 3 +++
 5 files changed, 11 insertions(+)

diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 85362ae917..2b883490d2 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -16,6 +16,8 @@ jobs:
       INVENTREE_DB_NAME: './test_db.sqlite'
       INVENTREE_DB_ENGINE: django.db.backends.sqlite3
       INVENTREE_DEBUG: info
+      INVENTREE_MEDIA_ROOT: ./media
+      INVENTREE_STATIC_ROOT: ./static
 
     steps:
       - name: Checkout Code
diff --git a/.github/workflows/mariadb.yaml b/.github/workflows/mariadb.yaml
index 2ae02c2bd0..f976cfa088 100644
--- a/.github/workflows/mariadb.yaml
+++ b/.github/workflows/mariadb.yaml
@@ -16,6 +16,8 @@ jobs:
       INVENTREE_DB_HOST: '127.0.0.1'
       INVENTREE_DB_PORT: 3306
       INVENTREE_DEBUG: info
+      INVENTREE_MEDIA_ROOT: ./media
+      INVENTREE_STATIC_ROOT: ./static
 
     services:
       mariadb:
diff --git a/.github/workflows/mysql.yaml b/.github/workflows/mysql.yaml
index 7d3ee8d6ae..70acad66a1 100644
--- a/.github/workflows/mysql.yaml
+++ b/.github/workflows/mysql.yaml
@@ -18,6 +18,8 @@ jobs:
       INVENTREE_DB_HOST: '127.0.0.1'
       INVENTREE_DB_PORT: 3306
       INVENTREE_DEBUG: info
+      INVENTREE_MEDIA_ROOT: ./media
+      INVENTREE_STATIC_ROOT: ./static
 
     services:
       mysql:
diff --git a/.github/workflows/postgresql.yaml b/.github/workflows/postgresql.yaml
index aab05205cc..76981e5a1b 100644
--- a/.github/workflows/postgresql.yaml
+++ b/.github/workflows/postgresql.yaml
@@ -18,6 +18,8 @@ jobs:
       INVENTREE_DB_HOST: '127.0.0.1'
       INVENTREE_DB_PORT: 5432
       INVENTREE_DEBUG: info
+      INVENTREE_MEDIA_ROOT: ./media
+      INVENTREE_STATIC_ROOT: ./static
 
     services:
       postgres:
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index c529182bb7..bab673306f 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -98,10 +98,12 @@ cors:
 
 # MEDIA_ROOT is the local filesystem location for storing uploaded files
 # By default, it is stored under /home/inventree
+# Use environment variable INVENTREE_MEDIA_ROOT
 media_root: '/home/inventree/media'
 
 # STATIC_ROOT is the local filesystem location for storing static files
 # By default, it is stored under /home/inventree
+# Use environment variable INVENTREE_STATIC_ROOT
 static_root: '/home/inventree/static'
 
 # Optional URL schemes to allow in URL fields
@@ -115,6 +117,7 @@ static_root: '/home/inventree/static'
 # Backup options
 # Set the backup_dir parameter to store backup files in a specific location
 # If unspecified, the local user's temp directory will be used
+# Use environment variable INVENTREE_BACKUP_DIR
 backup_dir: '/home/inventree/backup/'
 
 # Permit custom authentication backends

From c0a0ca458873e1bf3aec855b7646b974d675a8d8 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:35:48 +1100
Subject: [PATCH 035/111] PEP fix

---
 InvenTree/InvenTree/settings.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index f49c5d1f75..eee69c0780 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -45,6 +45,7 @@ def get_setting(environment_var, backup_val, default_value=None):
 
     return default_value
 
+
 # Determine if we are running in "test" mode e.g. "manage.py test"
 TESTING = 'test' in sys.argv
 

From ab57fd3b764a3e0411c81919a175daecb6627afc Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:45:42 +1100
Subject: [PATCH 036/111] Build docker image

---
 .github/workflows/docker.yaml | 16 ++++++++++++
 docker/Dockerfile             | 46 +++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+)
 create mode 100644 .github/workflows/docker.yaml
 create mode 100644 docker/Dockerfile

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
new file mode 100644
index 0000000000..448babcc31
--- /dev/null
+++ b/.github/workflows/docker.yaml
@@ -0,0 +1,16 @@
+# Test that the docker file builds correctly
+
+name: Docker
+
+on: ["push", "pull_request"]
+
+jobs:
+  
+  docker:
+    runs-on: ubtun-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Build Docker Image
+        run: docker build . --file docker/Dockerfile --tag inventree-:$(date +%s)
+    
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000000..da0ddebf39
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,46 @@
+FROM python:alpine as production
+
+# Configuration params
+ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
+ARG INVENTREE_VERSION="master"
+ARG INVENTREE_HOME="/home/inventree"
+
+ENV PYTHONUNBUFFERED 1
+
+# InvenTree paths
+ENV INVENTREE_SRC_DIR="$INVENTREE_HOME/src"
+ENV INVENTREE_STATIC_ROOT="$INVENTREE_HOME/static"
+ENV INVENTREE_MEDIA_ROOT="$INVENTREE_HOME/media"
+ENV INVENTREE_LOG_DIR="$INVENTREE_HOME/log"
+ENV INVENTREE_BACKUP_DIR="$INVENTREE_HOME/backup"
+ENV INVENTREE_VENV="$INVENTREE_HOME/env"
+
+RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
+
+# Create user account
+RUN useradd -ms /bin/bash inventree
+USER inventree
+WORKDIR /home/inventree
+
+# Clone source code
+RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
+
+# Install required system packages
+RUN apk add --no-cache postgresql-contrib postgresql-dev libpq-dev
+RUN apk add --no-cache libmysqlclient-dev
+
+# Install required PIP packages
+RUN python -m venv $INVENTREE_VENV && pip install --upgrade pip setuptools wheel
+RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U invoke
+RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
+
+# Install InvenTree packages
+RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U -r $INVENTREE_SRC_DIR/requirements.txt
+
+# Install supervisor
+RUN apt add --no-cache supervisor
+
+# Copy supervisor file
+COPY deploy/inventree.conf /etc/supervisor/conf.d/inventree.conf
+
+RUN sudo service supervisor start

From 1a7b6e26136418cb7bea2289b56c5d902c5cddfd Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:47:41 +1100
Subject: [PATCH 037/111] Fix

---
 .github/workflows/docker.yaml | 2 +-
 docker/Dockerfile             | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 448babcc31..9d08869db3 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -7,7 +7,7 @@ on: ["push", "pull_request"]
 jobs:
   
   docker:
-    runs-on: ubtun-latest
+    runs-on: ubtunu-latest
 
     steps:
       - uses: actions/checkout@v2
diff --git a/docker/Dockerfile b/docker/Dockerfile
index da0ddebf39..d2301e66fb 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -40,6 +40,9 @@ RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U -r $INVENTRE
 # Install supervisor
 RUN apt add --no-cache supervisor
 
+# Create required directories
+RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home/inventree/backup
+
 # Copy supervisor file
 COPY deploy/inventree.conf /etc/supervisor/conf.d/inventree.conf
 

From 6017cad6b3ed6e4595025ed56803f6198778fadf Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:48:58 +1100
Subject: [PATCH 038/111] So apparently I cannot spell...

---
 .github/workflows/docker.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 9d08869db3..270ddc3600 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -7,7 +7,7 @@ on: ["push", "pull_request"]
 jobs:
   
   docker:
-    runs-on: ubtunu-latest
+    runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@v2

From 2746396d11a8fb93cf8bfee5f42de0cb2b396975 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:50:41 +1100
Subject: [PATCH 039/111] Fix tag name

---
 .github/workflows/docker.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 270ddc3600..749616d716 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -12,5 +12,5 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       - name: Build Docker Image
-        run: docker build . --file docker/Dockerfile --tag inventree-:$(date +%s)
+        run: docker build . --file docker/Dockerfile --tag inventree:$(date +%s)
     
\ No newline at end of file

From 58bfc80f793e8bc3ff538cfd0d637a367b38a907 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:54:17 +1100
Subject: [PATCH 040/111] Alpine uses different commands

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index d2301e66fb..de3c79235f 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -18,7 +18,7 @@ ENV INVENTREE_VENV="$INVENTREE_HOME/env"
 RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
 
 # Create user account
-RUN useradd -ms /bin/bash inventree
+RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
 USER inventree
 WORKDIR /home/inventree
 

From 601aff82832b6a20e0d7672da46d17abf22c9617 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:55:44 +1100
Subject: [PATCH 041/111] Install git

---
 docker/Dockerfile | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index de3c79235f..24f99764d6 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -22,6 +22,9 @@ RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
 USER inventree
 WORKDIR /home/inventree
 
+# Install git
+RUN apt add --no-cache git
+
 # Clone source code
 RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 

From 42b400e619464bd22fd7328d422880f527eb6687 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 22:58:32 +1100
Subject: [PATCH 042/111] typo fix

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 24f99764d6..f4ad7efddd 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -23,7 +23,7 @@ USER inventree
 WORKDIR /home/inventree
 
 # Install git
-RUN apt add --no-cache git
+RUN apk add --no-cache git
 
 # Clone source code
 RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}

From 1f881dd041eaf648981a33eeabea9473abb8c6b3 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:00:22 +1100
Subject: [PATCH 043/111] Run as root

---
 docker/Dockerfile | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index f4ad7efddd..a00cf1982a 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -19,7 +19,6 @@ RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
 
 # Create user account
 RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
-USER inventree
 WORKDIR /home/inventree
 
 # Install git

From 61f8b982ce216d512f32dea0b70f67a07dfe6c4a Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:03:13 +1100
Subject: [PATCH 044/111] lib name fix

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index a00cf1982a..c01fca9075 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -28,7 +28,7 @@ RUN apk add --no-cache git
 RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
 # Install required system packages
-RUN apk add --no-cache postgresql-contrib postgresql-dev libpq-dev
+RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
 RUN apk add --no-cache libmysqlclient-dev
 
 # Install required PIP packages

From 251ec7a02f82325b9a30ac7e663874aa0d5d9fc9 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:06:54 +1100
Subject: [PATCH 045/111] Fix lib names

---
 docker/Dockerfile | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index c01fca9075..b049e2f886 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -29,7 +29,8 @@ RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTRE
 
 # Install required system packages
 RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
-RUN apk add --no-cache libmysqlclient-dev
+RUN apk add --no-cache libmysqlclient
+RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
 # Install required PIP packages
 RUN python -m venv $INVENTREE_VENV && pip install --upgrade pip setuptools wheel

From 24d36e0b661ef793f55489e5dbd3b57213fdc303 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:09:24 +1100
Subject: [PATCH 046/111] Getting there...

---
 docker/Dockerfile | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index b049e2f886..01f6b16d99 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -29,7 +29,6 @@ RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTRE
 
 # Install required system packages
 RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
-RUN apk add --no-cache libmysqlclient
 RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
 # Install required PIP packages

From 286cf9b102b51ed4045676f26d3886e608965f23 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:12:27 +1100
Subject: [PATCH 047/111] gcc required

---
 docker/Dockerfile | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 01f6b16d99..567e3390b0 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -28,6 +28,7 @@ RUN apk add --no-cache git
 RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
 # Install required system packages
+RUN apk add --no-cache gcc g++
 RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
 RUN apk add --no-cache mariadb-connector-c mariadb-dev
 

From 8b227ce297ab8d315b91cf0801230d1da7ffa528 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:20:32 +1100
Subject: [PATCH 048/111] More required packages, I guess...

---
 README.md         |  1 +
 docker/Dockerfile | 18 +++++++++++-------
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 63948c35af..ef0556cceb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
 ![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
+![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
 ![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
 ![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
 ![MariaDB](https://github.com/inventree/inventree/actions/workflows/mariadb.yaml/badge.svg)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 567e3390b0..51f147b670 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -21,17 +21,21 @@ RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
 RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
 WORKDIR /home/inventree
 
-# Install git
-RUN apk add --no-cache git
+# Install required system packages
+RUN apk add --no-cache git make bash \
+    gcc libgcc g++ libstdc++ \
+    libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
+    libffi libffi-dev \
+    zlib zlib-dev \
+RUN apk add --no-cache cairo cairo-dev pango pango-dev
+RUN apk add --no-cache fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto
+RUN apk add --no-cache python3
+RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
+RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
 # Clone source code
 RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
-# Install required system packages
-RUN apk add --no-cache gcc g++
-RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
-RUN apk add --no-cache mariadb-connector-c mariadb-dev
-
 # Install required PIP packages
 RUN python -m venv $INVENTREE_VENV && pip install --upgrade pip setuptools wheel
 RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U invoke

From ff6b127f1b52e77c46380cced4ba8919529c19b5 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:22:17 +1100
Subject: [PATCH 049/111] Typo fixin'

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 51f147b670..6f15ddbd2d 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,7 +26,7 @@ RUN apk add --no-cache git make bash \
     gcc libgcc g++ libstdc++ \
     libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
     libffi libffi-dev \
-    zlib zlib-dev \
+    zlib zlib-dev
 RUN apk add --no-cache cairo cairo-dev pango pango-dev
 RUN apk add --no-cache fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto
 RUN apk add --no-cache python3

From 7683cc1aaa4c2d833499792eab3cdba8f6e7d0ce Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:27:01 +1100
Subject: [PATCH 050/111] APK not APT

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 6f15ddbd2d..b5a1826dd2 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -45,7 +45,7 @@ RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U psycopg2 mys
 RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U -r $INVENTREE_SRC_DIR/requirements.txt
 
 # Install supervisor
-RUN apt add --no-cache supervisor
+RUN apk add --no-cache supervisor
 
 # Create required directories
 RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home/inventree/backup

From b9f9b26ca5c2b75ba514037734691a911dfa03a7 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:32:03 +1100
Subject: [PATCH 051/111] Sudo not required, I guess?

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index b5a1826dd2..2b5878f223 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -53,4 +53,4 @@ RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home
 # Copy supervisor file
 COPY deploy/inventree.conf /etc/supervisor/conf.d/inventree.conf
 
-RUN sudo service supervisor start
+RUN service supervisor start

From b9e81c3c0e685890690f6792277e6f65acd111c2 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 31 Mar 2021 23:39:16 +1100
Subject: [PATCH 052/111] Start supervisord

Ref: https://advancedweb.hu/supervisor-with-docker-lessons-learned/
---
 deploy/inventree.conf | 1 +
 docker/Dockerfile     | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/deploy/inventree.conf b/deploy/inventree.conf
index 782b6ae2f6..a55c7163f6 100644
--- a/deploy/inventree.conf
+++ b/deploy/inventree.conf
@@ -13,6 +13,7 @@
 [supervisord]
 ; Change this path if log files are stored elsewhere
 logfile=/home/inventree/log/supervisor.log
+nodaemon=true
 
 [program:inventree-server]
 user=inventree
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 2b5878f223..c0b088d952 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -53,4 +53,4 @@ RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home
 # Copy supervisor file
 COPY deploy/inventree.conf /etc/supervisor/conf.d/inventree.conf
 
-RUN service supervisor start
+CMD ["/usr/bin/supervisord"]

From 38b9655ad9831eec6410afa21200a1427f316068 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 08:43:58 +1100
Subject: [PATCH 053/111] Remove unused workflow

---
 .github/workflows/ci.yaml | 32 --------------------------------
 1 file changed, 32 deletions(-)
 delete mode 100644 .github/workflows/ci.yaml

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index f935bdda7f..0000000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Django CI
-
-on:
-  push:
-    branches: [ $default-branch ]
-  pull_request:
-    branches: [ $default-branch ]
-
-jobs:
-
-  # Run tests on an SQLite database
-  sqlite:
-
-    runs-on: ubuntu-latest
-
-    strategy:
-      max-parallel: 4
-      matrix:
-        python-version: [3.7, 3.8, 3.9]
-
-    steps:
-      - uses: actions/checkout@v2
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          pip3 install invoke
-          invoke install
-      - name: Run Tests
-        run: invoke coverage
\ No newline at end of file

From 76ab38a06b18ef3d05058cfaeb4906c3b17341c4 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 11:35:03 +1100
Subject: [PATCH 054/111] Add docker info

---
 docker/Dockerfile | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index c0b088d952..a0f0595c93 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -15,6 +15,16 @@ ENV INVENTREE_LOG_DIR="$INVENTREE_HOME/log"
 ENV INVENTREE_BACKUP_DIR="$INVENTREE_HOME/backup"
 ENV INVENTREE_VENV="$INVENTREE_HOME/env"
 
+LABEL org.label-schema.schema-version="1.0" \
+      org.label-schema.build-date=$DATE \
+      org.label-schema.vendor="inventree" \
+      org.label-schema.name="inventree/inventree" \
+      org.label-schema.url="https://hub.docker.com/r/inventree/inventree-docker" \
+      org.label-schema.version=$INVENTREE_VERSION \
+      org.label-schema.vcs-url=$INVENTREE_REPO \
+      org.label-schema.vcs-branch=$BRANCH \
+      org.label-schema.vcs-ref=$COMMIT
+
 RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
 
 # Create user account

From 08a1a6cf43761500a4d79dbb940edacdc9b4e831 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 20:14:17 +1100
Subject: [PATCH 055/111] Add configuration options for the Dockerfile

---
 docker/Dockerfile | 51 +++++++++++++++++++++++++++++++----------------
 1 file changed, 34 insertions(+), 17 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index a0f0595c93..ffba72f4c6 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -4,26 +4,43 @@ FROM python:alpine as production
 ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
 ARG INVENTREE_VERSION="master"
 ARG INVENTREE_HOME="/home/inventree"
+ARG INVENTREE_PORT="80"
+
+# Database configuration options
+ARG INVENTREE_DB_ENGINE="sqlite"
+ARG INVENTREE_DB_NAME="inventree_db.sqlite3"
+ARG INVENTREE_DB_HOST="127.0.0.1"
+ARG INVENTREE_DB_PORT=""
+ARG INVENTREE_DB_USER=""
+ARG INVENTREE_DB_PASSWORD=""
 
 ENV PYTHONUNBUFFERED 1
 
 # InvenTree paths
-ENV INVENTREE_SRC_DIR="$INVENTREE_HOME/src"
-ENV INVENTREE_STATIC_ROOT="$INVENTREE_HOME/static"
-ENV INVENTREE_MEDIA_ROOT="$INVENTREE_HOME/media"
-ENV INVENTREE_LOG_DIR="$INVENTREE_HOME/log"
-ENV INVENTREE_BACKUP_DIR="$INVENTREE_HOME/backup"
-ENV INVENTREE_VENV="$INVENTREE_HOME/env"
+ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
+ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
+ENV INVENTREE_MEDIA_ROOT="${INVENTREE_HOME}/media"
+ENV INVENTREE_LOG_DIR="${INVENTREE_HOME}/log"
+ENV INVENTREE_BACKUP_DIR="${INVENTREE_HOME}/backup"
+ENV INVENTREE_VENV="${INVENTREE_HOME}/env"
+
+# Pass DB configuration through as environment variables
+ENV INVENTREE_DB_ENGINE="${INVENTREE_DB_ENGINE}"
+ENV INVENTREE_DB_NAME="${INVENTREE_DB_NAME}"
+ENV INVENTREE_DB_HOST="${INVENTREE_DB_HOST}"
+ENV INVENTREE_DB_PORT="${INVENTREE_DB_PORT}"
+ENV INVENTREE_DB_USER="${INVENTREE_DB_USER}"
+ENV INVENTREE_DB_PASSWORD="${INVENTREE_DB_PASSWORD}"
 
 LABEL org.label-schema.schema-version="1.0" \
-      org.label-schema.build-date=$DATE \
+      org.label-schema.build-date=${DATE} \
       org.label-schema.vendor="inventree" \
       org.label-schema.name="inventree/inventree" \
       org.label-schema.url="https://hub.docker.com/r/inventree/inventree-docker" \
-      org.label-schema.version=$INVENTREE_VERSION \
-      org.label-schema.vcs-url=$INVENTREE_REPO \
-      org.label-schema.vcs-branch=$BRANCH \
-      org.label-schema.vcs-ref=$COMMIT
+      org.label-schema.version=${INVENTREE_VERSION} \
+      org.label-schema.vcs-url=${INVENTREE_REPO} \
+      org.label-schema.vcs-branch=${BRANCH} \
+      org.label-schema.vcs-ref=${COMMIT}
 
 RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
 
@@ -44,15 +61,15 @@ RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
 RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
 # Clone source code
-RUN git clone --branch $INVENTREE_VERSION --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
+RUN git clone --branch ${INVENTREE_VERSION} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
 # Install required PIP packages
-RUN python -m venv $INVENTREE_VENV && pip install --upgrade pip setuptools wheel
-RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U invoke
-RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
+RUN python -m venv ${INVENTREE_VENV} && pip install --upgrade pip setuptools wheel
+RUN python -m venv ${INVENTREE_VENV} && pip install --no-cache-dir -U invoke
+RUN python -m venv ${INVENTREE_VENV} && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
 
 # Install InvenTree packages
-RUN python -m venv $INVENTREE_VENV && pip install --no-cache-dir -U -r $INVENTREE_SRC_DIR/requirements.txt
+RUN python -m venv ${INVENTREE_VENV} && pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
 
 # Install supervisor
 RUN apk add --no-cache supervisor
@@ -61,6 +78,6 @@ RUN apk add --no-cache supervisor
 RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home/inventree/backup
 
 # Copy supervisor file
-COPY deploy/inventree.conf /etc/supervisor/conf.d/inventree.conf
+COPY docker/inventree.conf /etc/supervisor/conf.d/inventree.conf
 
 CMD ["/usr/bin/supervisord"]

From d446f8ddd121f0fa0d6431b33beeab9be1917df8 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 20:14:31 +1100
Subject: [PATCH 056/111] Add supervisor conf file specific to docker

---
 deploy/inventree.conf |  1 -
 docker/inventree.conf | 41 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 41 insertions(+), 1 deletion(-)
 create mode 100644 docker/inventree.conf

diff --git a/deploy/inventree.conf b/deploy/inventree.conf
index a55c7163f6..782b6ae2f6 100644
--- a/deploy/inventree.conf
+++ b/deploy/inventree.conf
@@ -13,7 +13,6 @@
 [supervisord]
 ; Change this path if log files are stored elsewhere
 logfile=/home/inventree/log/supervisor.log
-nodaemon=true
 
 [program:inventree-server]
 user=inventree
diff --git a/docker/inventree.conf b/docker/inventree.conf
new file mode 100644
index 0000000000..dabf9ddafb
--- /dev/null
+++ b/docker/inventree.conf
@@ -0,0 +1,41 @@
+; # Supervisor Config File (for docker build)
+;
+; This config file is specific to the InvenTree docker build!
+;
+; There are two separate processes which must be managed:
+;
+; ## Web Server
+; The InvenTree server must be launched and managed as a process
+; The recommended way to handle the web server is to use gunicorn
+;
+; ## Background Tasks
+; A background task manager processes long-running and periodic tasks
+; InvenTree uses django-q for this purpose
+
+[supervisord]
+; Change this path if log files are stored elsewhere
+logfile=/home/inventree/log/supervisor.log
+nodaemon=true
+
+[program:inventree-server]
+user=inventree
+directory=/home/inventree/src/InvenTree
+command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:%(INVENTREE_PORT)s
+startsecs=10
+autostart=true
+autorestart=true
+startretries=3
+; Change these paths if log files are stored elsewhere
+stderr_logfile=/home/inventree/log/server.err.log
+stdout_logfile=/home/inventree/log/server.out.log
+
+[program:inventree-cluster]
+user=inventree
+directory=/home/inventree/src/InvenTree
+command=/home/inventree/env/bin/python manage.py qcluster
+startsecs=10
+autostart=true
+autorestart=true
+; Change these paths if log files are stored elsewhere
+stderr_logfile=/home/inventree/log/cluster.err.log
+stdout_logfile=/home/inventree/log/cluster.out.log
\ No newline at end of file

From 839c29117dc516e2a4b76cc194d2da8c3fae3e31 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 20:30:51 +1100
Subject: [PATCH 057/111] Dockerfile updates

- Pipe supervisor logs to stdout (so they are passed to the docker instance)
- Fix supervisor service
- Expose home dir and port as env vars
---
 docker/Dockerfile                          |  8 ++++++--
 docker/{inventree.conf => supervisor.conf} | 16 ++++++++--------
 2 files changed, 14 insertions(+), 10 deletions(-)
 rename docker/{inventree.conf => supervisor.conf} (69%)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index ffba72f4c6..04d85184bd 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -16,6 +16,10 @@ ARG INVENTREE_DB_PASSWORD=""
 
 ENV PYTHONUNBUFFERED 1
 
+# InvenTree key settings
+ENV INVENTREE_HOME="${INVENTREE_HOME}"
+ENV INVENTREE_PORT="${INVENTREE_PORT}"
+
 # InvenTree paths
 ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
 ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
@@ -78,6 +82,6 @@ RUN apk add --no-cache supervisor
 RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home/inventree/backup
 
 # Copy supervisor file
-COPY docker/inventree.conf /etc/supervisor/conf.d/inventree.conf
+COPY docker/supervisor.conf /etc/supervisord.conf
 
-CMD ["/usr/bin/supervisord"]
+CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
diff --git a/docker/inventree.conf b/docker/supervisor.conf
similarity index 69%
rename from docker/inventree.conf
rename to docker/supervisor.conf
index dabf9ddafb..72609e5bac 100644
--- a/docker/inventree.conf
+++ b/docker/supervisor.conf
@@ -13,8 +13,6 @@
 ; InvenTree uses django-q for this purpose
 
 [supervisord]
-; Change this path if log files are stored elsewhere
-logfile=/home/inventree/log/supervisor.log
 nodaemon=true
 
 [program:inventree-server]
@@ -25,9 +23,10 @@ startsecs=10
 autostart=true
 autorestart=true
 startretries=3
-; Change these paths if log files are stored elsewhere
-stderr_logfile=/home/inventree/log/server.err.log
-stdout_logfile=/home/inventree/log/server.out.log
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/fd/2
+stderr_logfile_maxbytes=0
 
 [program:inventree-cluster]
 user=inventree
@@ -36,6 +35,7 @@ command=/home/inventree/env/bin/python manage.py qcluster
 startsecs=10
 autostart=true
 autorestart=true
-; Change these paths if log files are stored elsewhere
-stderr_logfile=/home/inventree/log/cluster.err.log
-stdout_logfile=/home/inventree/log/cluster.out.log
\ No newline at end of file
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/fd/2
+stderr_logfile_maxbytes=0
\ No newline at end of file

From 148600a9c4263627ec3eb1c3dbd8c98f1d7de7c3 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 20:38:18 +1100
Subject: [PATCH 058/111] Copy gunicorn.conf.py

---
 docker/Dockerfile       | 3 +++
 docker/gunicorn.conf.py | 6 ++++++
 docker/supervisor.conf  | 2 +-
 3 files changed, 10 insertions(+), 1 deletion(-)
 create mode 100644 docker/gunicorn.conf.py

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 04d85184bd..d3517b1a05 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -84,4 +84,7 @@ RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home
 # Copy supervisor file
 COPY docker/supervisor.conf /etc/supervisord.conf
 
+# Copy gunicorn config file
+COPY docker/gunicorn.conf.py /home/inventree/gunicorn.conf.py
+
 CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py
new file mode 100644
index 0000000000..1071c2d745
--- /dev/null
+++ b/docker/gunicorn.conf.py
@@ -0,0 +1,6 @@
+import multiprocessing
+
+workers = multiprocessing.cpu_count() * 2 + 1
+
+max_requests = 1000
+max_requests_jitter = 50
diff --git a/docker/supervisor.conf b/docker/supervisor.conf
index 72609e5bac..47eeaf6c28 100644
--- a/docker/supervisor.conf
+++ b/docker/supervisor.conf
@@ -18,7 +18,7 @@ nodaemon=true
 [program:inventree-server]
 user=inventree
 directory=/home/inventree/src/InvenTree
-command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:%(INVENTREE_PORT)s
+command=/home/inventree/env/bin/gunicorn -c /home/inventree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:%(ENV_INVENTREE_PORT)s
 startsecs=10
 autostart=true
 autorestart=true

From db858b3cfc9611f12f93dbc0bf71287b2ad99070 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 20:44:13 +1100
Subject: [PATCH 059/111] Install packages inside venv

---
 docker/Dockerfile | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index d3517b1a05..fe1877b99d 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -67,13 +67,18 @@ RUN apk add --no-cache mariadb-connector-c mariadb-dev
 # Clone source code
 RUN git clone --branch ${INVENTREE_VERSION} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
-# Install required PIP packages
-RUN python -m venv ${INVENTREE_VENV} && pip install --upgrade pip setuptools wheel
-RUN python -m venv ${INVENTREE_VENV} && pip install --no-cache-dir -U invoke
-RUN python -m venv ${INVENTREE_VENV} && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
+# Setup Python virtual environment
+RUN apk add python3-venv
+RUN python -m venv ${INVENTREE_VENV}}
+RUN source ${INVENTREE_VENV}/bin/activate
+
+# Install required PIP packages (into the virtual environment!)
+RUN pip install --upgrade pip setuptools wheel
+RUN pip install --no-cache-dir -U invoke
+RUN pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
 
 # Install InvenTree packages
-RUN python -m venv ${INVENTREE_VENV} && pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
+RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
 
 # Install supervisor
 RUN apk add --no-cache supervisor

From 47ba0599eb2467239f6bb95ac050ae9cdbd227ca Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 20:44:27 +1100
Subject: [PATCH 060/111] Reference environment variables in supervisor conf
 file

---
 docker/supervisor.conf | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/docker/supervisor.conf b/docker/supervisor.conf
index 47eeaf6c28..363bbe6b4a 100644
--- a/docker/supervisor.conf
+++ b/docker/supervisor.conf
@@ -17,8 +17,8 @@ nodaemon=true
 
 [program:inventree-server]
 user=inventree
-directory=/home/inventree/src/InvenTree
-command=/home/inventree/env/bin/gunicorn -c /home/inventree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:%(ENV_INVENTREE_PORT)s
+directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
+command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:%(ENV_INVENTREE_PORT)s
 startsecs=10
 autostart=true
 autorestart=true
@@ -30,8 +30,8 @@ stderr_logfile_maxbytes=0
 
 [program:inventree-cluster]
 user=inventree
-directory=/home/inventree/src/InvenTree
-command=/home/inventree/env/bin/python manage.py qcluster
+directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
+command=%(ENV_INVENTREE_VENV)s/bin/python manage.py qcluster
 startsecs=10
 autostart=true
 autorestart=true

From 8e7e36089b4eedf95f5ae3491bac9c5c0428f4cf Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 1 Apr 2021 21:11:59 +1100
Subject: [PATCH 061/111] Fix venv

---
 docker/Dockerfile | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index fe1877b99d..4d59ec4b8e 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,9 +1,10 @@
 FROM python:alpine as production
 
-# Configuration params
+# GitHub source
 ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
 ARG INVENTREE_VERSION="master"
-ARG INVENTREE_HOME="/home/inventree"
+
+# InvenTree server port
 ARG INVENTREE_PORT="80"
 
 # Database configuration options
@@ -17,7 +18,7 @@ ARG INVENTREE_DB_PASSWORD=""
 ENV PYTHONUNBUFFERED 1
 
 # InvenTree key settings
-ENV INVENTREE_HOME="${INVENTREE_HOME}"
+ENV INVENTREE_HOME="/home/inventree"
 ENV INVENTREE_PORT="${INVENTREE_PORT}"
 
 # InvenTree paths
@@ -50,7 +51,7 @@ RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
 
 # Create user account
 RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
-WORKDIR /home/inventree
+WORKDIR ${INVENTREE_HOME}
 
 # Install required system packages
 RUN apk add --no-cache git make bash \
@@ -68,28 +69,27 @@ RUN apk add --no-cache mariadb-connector-c mariadb-dev
 RUN git clone --branch ${INVENTREE_VERSION} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
 # Setup Python virtual environment
-RUN apk add python3-venv
-RUN python -m venv ${INVENTREE_VENV}}
-RUN source ${INVENTREE_VENV}/bin/activate
+RUN python3 -m venv ${INVENTREE_VENV}
 
 # Install required PIP packages (into the virtual environment!)
-RUN pip install --upgrade pip setuptools wheel
-RUN pip install --no-cache-dir -U invoke
-RUN pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
+RUN source ${INVENTREE_VENV}/bin/activate && pip install --upgrade pip setuptools wheel
+RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U invoke
+RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
+RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U gunicorn
 
 # Install InvenTree packages
-RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
+RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
 
 # Install supervisor
 RUN apk add --no-cache supervisor
 
 # Create required directories
-RUN mkdir /home/inventree/media /home/inventree/static /home/inventree/log /home/inventree/backup
+RUN mkdir ${INVENTREE_HOME}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
 
 # Copy supervisor file
 COPY docker/supervisor.conf /etc/supervisord.conf
 
 # Copy gunicorn config file
-COPY docker/gunicorn.conf.py /home/inventree/gunicorn.conf.py
+COPY docker/gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
 
 CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

From be41be3981858e182bb9d7029a09496f305ee94d Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 2 Apr 2021 00:03:56 +1100
Subject: [PATCH 062/111] Add "wait_for_db" management command

---
 .../management/commands/wait_for_db.py        | 31 +++++++++++++++++++
 InvenTree/config_template.yaml                |  5 ---
 docker/Dockerfile                             |  1 +
 3 files changed, 32 insertions(+), 5 deletions(-)
 create mode 100644 InvenTree/InvenTree/management/commands/wait_for_db.py

diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
new file mode 100644
index 0000000000..5d152d26ca
--- /dev/null
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -0,0 +1,31 @@
+"""
+Custom management command, wait for the database to be ready!
+"""
+
+from django.core.management.base import BaseCommand
+
+from django.db import connections
+from django.db.utils import OperationalError
+
+import time
+
+
+class Command(BaseCommand):
+    """
+    django command to pause execution until the database is ready
+    """
+
+    def handle(self, *args, **kwargs):
+        self.stdout.write("Waiting for database...")
+
+        db_conn = None
+
+        while not db_conn:
+            try:
+                # get the database with keyword 'default' from settings.py
+                db_conn = connections['default']
+                # prints success messge in green
+                self.stdout.write(self.style.SUCCESS('InvenTree database connected'))
+            except OperationalError:
+                self.stdout.write("Database unavailable, waiting 5 seconds ...")
+                time.sleep(5)
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index bab673306f..7ce62e887d 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -9,11 +9,6 @@
 database:
   # Uncomment (and edit) one of the database configurations below,
   # or specify database options using environment variables
-  
-  # Default installation uses a simple sqlite database
-  # For production, consider changing this!
-  ENGINE: sqlite3
-  NAME: '/home/inventree/database.sqlite3'
 
   # Refer to the django documentation for full list of options
   
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 4d59ec4b8e..c49c6e53f0 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -23,6 +23,7 @@ ENV INVENTREE_PORT="${INVENTREE_PORT}"
 
 # InvenTree paths
 ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
+ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
 ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
 ENV INVENTREE_MEDIA_ROOT="${INVENTREE_HOME}/media"
 ENV INVENTREE_LOG_DIR="${INVENTREE_HOME}/log"

From 8d3b9e2ca4fd6137660a04e70a9804bb5d48adbf Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 2 Apr 2021 00:06:17 +1100
Subject: [PATCH 063/111] Updates to settings.py

- Create secret_key.txt if it does not exist
- Copy default settings file if it does not exist
---
 InvenTree/InvenTree/settings.py | 31 ++++++++++++++++++++++++++++---
 tasks.py                        | 32 +++++++-------------------------
 2 files changed, 35 insertions(+), 28 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index eee69c0780..3f705ae70c 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -13,6 +13,9 @@ database setup in this file.
 
 import logging
 import os
+import random
+import string
+import shutil
 import sys
 import tempfile
 from datetime import datetime
@@ -55,8 +58,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
 
 if not os.path.exists(cfg_filename):
-    print("Error: config.yaml not found")
-    sys.exit(-1)
+    print("InvenTree configuration file 'config.yaml' not found - creating default file")
+
+    cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
+    shutil.copyfile(cfg_template, cfg_filename)
 
 with open(cfg_filename, 'r') as cfg:
     CONFIG = yaml.safe_load(cfg)
@@ -99,6 +104,17 @@ LOGGING = {
 # Get a logger instance for this setup file
 logger = logging.getLogger(__name__)
 
+"""
+Specify a secret key to be used by django.
+
+Following options are tested, in descending order of preference:
+
+A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data
+B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file
+C) Look for default key file "secret_key.txt"
+d) Create "secret_key.txt" if it does not exist
+"""
+
 if os.getenv("INVENTREE_SECRET_KEY"):
     # Secret key passed in directly
     SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip()
@@ -111,11 +127,20 @@ else:
         if os.path.isfile(key_file):
             logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY_FILE")
         else:
-            logger.error(f"Secret key file {key_file} not found")
+            logger.error(f"Secret key file '{key_file}'' not found")
             exit(-1)
     else:
         # default secret key location
         key_file = os.path.join(BASE_DIR, "secret_key.txt")
+
+        if not os.path.exists(key_file):
+            logger.info("Creating key file 'secret_key.txt'")
+            # Create a random key file
+            with open(key_file, 'w') as f:
+                options = string.digits + string.ascii_letters + string.punctuation
+                key = ''.join([random.choice(options) for i in range(50)])
+                f.write(key)
+
         logger.info(f"SECRET_KEY loaded from {key_file}")
     try:
         SECRET_KEY = open(key_file, "r").read().strip()
diff --git a/tasks.py b/tasks.py
index 2d7d395d10..83a99949f3 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,11 +3,10 @@
 from invoke import task
 from shutil import copyfile
 
-import random
-import string
 import os
 import sys
 
+
 def apps():
     """
     Returns a list of installed apps
@@ -27,6 +26,7 @@ def apps():
         'users',
     ]
 
+
 def localDir():
     """
     Returns the directory of *THIS* file.
@@ -35,6 +35,7 @@ def localDir():
     """
     return os.path.dirname(os.path.abspath(__file__))
 
+
 def managePyDir():
     """
     Returns the directory of the manage.py file
@@ -42,6 +43,7 @@ def managePyDir():
 
     return os.path.join(localDir(), 'InvenTree')
 
+
 def managePyPath():
     """
     Return the path of the manage.py file
@@ -49,6 +51,7 @@ def managePyPath():
 
     return os.path.join(managePyDir(), 'manage.py')
 
+
 def manage(c, cmd, pty=False):
     """
     Runs a given command against django's "manage.py" script.
@@ -63,32 +66,11 @@ def manage(c, cmd, pty=False):
         cmd=cmd
     ), pty=pty)
 
-@task(help={'length': 'Length of secret key (default=50)'})
-def key(c, length=50, force=False):
-    """
-    Generates a SECRET_KEY file which InvenTree uses for generating security hashes
-    """
 
-    SECRET_KEY_FILE = os.path.join(localDir(), 'InvenTree', 'secret_key.txt')
-
-    # If a SECRET_KEY file does not exist, generate a new one!
-    if force or not os.path.exists(SECRET_KEY_FILE):
-        print("Generating SECRET_KEY file - " + SECRET_KEY_FILE)
-        with open(SECRET_KEY_FILE, 'w') as key_file:
-            options = string.digits + string.ascii_letters + string.punctuation
-
-            key = ''.join([random.choice(options) for i in range(length)])
-
-            key_file.write(key)
-
-    else:
-        print("SECRET_KEY file already exists - skipping")
-
-
-@task(post=[key])
+@task
 def install(c):
     """
-    Installs required python packages, and runs initial setup functions.
+    Installs required python packages
     """
 
     # Install required Python packages with PIP

From 2436b1f2c985a36abf72dde35bd365790ed184db Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 2 Apr 2021 00:40:47 +1100
Subject: [PATCH 064/111] Entrypoint script - start.sh

---
 InvenTree/config_template.yaml |  4 ++--
 docker/Dockerfile              | 18 ++++++++++++++---
 docker/start.sh                | 35 ++++++++++++++++++++++++++++++++++
 tasks.py                       |  2 +-
 4 files changed, 53 insertions(+), 6 deletions(-)
 create mode 100644 docker/start.sh

diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 7ce62e887d..fb30f2d339 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -28,7 +28,7 @@ database:
   # NAME: '/home/inventree/database.sqlite3'
 
   # --- Example Configuration - MySQL ---
-  #ENGINE: django.db.backends.mysql
+  #ENGINE: mysql
   #NAME: inventree
   #USER: inventree
   #PASSWORD: inventree_password
@@ -36,7 +36,7 @@ database:
   #PORT: '3306'
 
   # --- Example Configuration - Postgresql ---
-  #ENGINE: django.db.backends.postgresql
+  #ENGINE: postgresql
   #NAME: inventree
   #USER: inventree
   #PASSWORD: inventree_password
diff --git a/docker/Dockerfile b/docker/Dockerfile
index c49c6e53f0..a9eaef612b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -3,12 +3,13 @@ FROM python:alpine as production
 # GitHub source
 ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
 ARG INVENTREE_VERSION="master"
+ARG INVENTREE_CONFIG_FILE="InvenTree/config_template.yaml"
 
 # InvenTree server port
 ARG INVENTREE_PORT="80"
 
 # Database configuration options
-ARG INVENTREE_DB_ENGINE="sqlite"
+ARG INVENTREE_DB_ENGINE="sqlite3"
 ARG INVENTREE_DB_NAME="inventree_db.sqlite3"
 ARG INVENTREE_DB_HOST="127.0.0.1"
 ARG INVENTREE_DB_PORT=""
@@ -21,6 +22,8 @@ ENV PYTHONUNBUFFERED 1
 ENV INVENTREE_HOME="/home/inventree"
 ENV INVENTREE_PORT="${INVENTREE_PORT}"
 
+ENV INVENTREE_LOG_LEVEL="INFO"
+
 # InvenTree paths
 ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
 ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
@@ -42,7 +45,7 @@ LABEL org.label-schema.schema-version="1.0" \
       org.label-schema.build-date=${DATE} \
       org.label-schema.vendor="inventree" \
       org.label-schema.name="inventree/inventree" \
-      org.label-schema.url="https://hub.docker.com/r/inventree/inventree-docker" \
+      org.label-schema.url="https://hub.docker.com/r/inventree/inventree" \
       org.label-schema.version=${INVENTREE_VERSION} \
       org.label-schema.vcs-url=${INVENTREE_REPO} \
       org.label-schema.vcs-branch=${BRANCH} \
@@ -93,4 +96,13 @@ COPY docker/supervisor.conf /etc/supervisord.conf
 # Copy gunicorn config file
 COPY docker/gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
 
-CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
+# Copy default InvenTree config file
+COPY ${INVENTREE_CONFIG_FILE} ${INVENTREE_SRC_DIR}/InvenTree/config.yaml
+
+# Copy startup script
+COPY docker/start.sh ${INVENTREE_HOME}/start.sh
+
+RUN chmod 755 ${INVENTREE_HOME}/start.sh
+
+# Let us begin
+CMD "./start.sh"
diff --git a/docker/start.sh b/docker/start.sh
new file mode 100644
index 0000000000..3e1287527b
--- /dev/null
+++ b/docker/start.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# Check that the database engine is specified
+if [ -z "$INVENTREE_DB_ENGINE" ]; then
+    echo "INVENTREE_DB_ENGINE not configured"
+    exit 1
+fi
+
+# Check that the base dir is set
+if [ -z "$INVENTREE_HOME" ]; then
+    echo "INVENTREE_HOME not configured"
+    exit 1
+fi
+
+# Activate virtual environment
+source $INVENTREE_VENV/bin/activate
+
+# Wait for the database to be ready
+cd $INVENTREE_MNG_DIR
+python manage.py wait_for_db
+
+sleep 10
+
+echo "Running InvenTree database migrations and collecting static files..."
+
+# We assume at this stage that the database is up and running
+# Ensure that the database schema are up to date
+python manage.py check || exit 1
+python manage.py migrate --noinput || exit 1
+python manage.py migrate --run-syncdb || exit 1
+python manage.py collectstatic --noinput || exit 1
+python manage.py clearsessions || exit 1
+
+# Now we can launch the server and background worker
+/usr/bin/supervisord -c /etc/supervisord.conf
diff --git a/tasks.py b/tasks.py
index 83a99949f3..c6d7dd0173 100644
--- a/tasks.py
+++ b/tasks.py
@@ -143,7 +143,7 @@ def static(c):
     as per Django requirements.
     """
 
-    manage(c, "collectstatic")
+    manage(c, "collectstatic --no-input")
 
 
 @task(pre=[install, migrate, static])

From 00c4519d2863a6d801ad12ad17de549b605d36c7 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 2 Apr 2021 00:54:29 +1100
Subject: [PATCH 065/111] Simplify dockerfile

---
 docker/Dockerfile      | 12 ------------
 docker/supervisor.conf |  2 +-
 2 files changed, 1 insertion(+), 13 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index a9eaef612b..99131700ee 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -5,22 +5,10 @@ ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
 ARG INVENTREE_VERSION="master"
 ARG INVENTREE_CONFIG_FILE="InvenTree/config_template.yaml"
 
-# InvenTree server port
-ARG INVENTREE_PORT="80"
-
-# Database configuration options
-ARG INVENTREE_DB_ENGINE="sqlite3"
-ARG INVENTREE_DB_NAME="inventree_db.sqlite3"
-ARG INVENTREE_DB_HOST="127.0.0.1"
-ARG INVENTREE_DB_PORT=""
-ARG INVENTREE_DB_USER=""
-ARG INVENTREE_DB_PASSWORD=""
-
 ENV PYTHONUNBUFFERED 1
 
 # InvenTree key settings
 ENV INVENTREE_HOME="/home/inventree"
-ENV INVENTREE_PORT="${INVENTREE_PORT}"
 
 ENV INVENTREE_LOG_LEVEL="INFO"
 
diff --git a/docker/supervisor.conf b/docker/supervisor.conf
index 363bbe6b4a..554200837d 100644
--- a/docker/supervisor.conf
+++ b/docker/supervisor.conf
@@ -18,7 +18,7 @@ nodaemon=true
 [program:inventree-server]
 user=inventree
 directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
-command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:%(ENV_INVENTREE_PORT)s
+command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:80
 startsecs=10
 autostart=true
 autorestart=true

From d91531720ba5e503efbc7242aff2b74e81937633 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 7 Apr 2021 22:17:24 +1000
Subject: [PATCH 066/111] Unit testing for task scheduling

---
 InvenTree/InvenTree/tasks.py      | 10 ++++---
 InvenTree/InvenTree/test_tasks.py | 43 +++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 3 deletions(-)
 create mode 100644 InvenTree/InvenTree/test_tasks.py

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index eba145c660..c12d0ec4df 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -23,6 +23,7 @@ def schedule_task(taskname, **kwargs):
 
     # If unspecified, repeat indefinitely
     repeats = kwargs.pop('repeats', -1)
+    kwargs['repeats'] = repeats
 
     try:
         from django_q.models import Schedule
@@ -31,15 +32,18 @@ def schedule_task(taskname, **kwargs):
         return
 
     try:
+        # If this task is already scheduled, don't schedule it again
+        # Instead, update the scheduling parameters
         if Schedule.objects.filter(func=taskname).exists():
-            logger.info(f"Scheduled task '{taskname}' already exists. (Skipping)")
+            logger.info(f"Scheduled task '{taskname}' already exists - updating!")
+
+            Schedule.objects.filter(func=taskname).update(**kwargs)
         else:
             logger.info(f"Creating scheduled task '{taskname}'")
 
             Schedule.objects.create(
                 name=taskname,
                 func=taskname,
-                repeats=repeats,
                 **kwargs
             )
     except (OperationalError, ProgrammingError):
@@ -82,8 +86,8 @@ def delete_successful_tasks():
 
     try:
         from django_q.models import Success
-        logger.warning("Could not perform 'delete_successful_tasks' - App registry not ready")
     except AppRegistryNotReady:
+        logger.warning("Could not perform 'delete_successful_tasks' - App registry not ready")
         return
 
     threshold = datetime.now() - timedelta(days=30)
diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py
new file mode 100644
index 0000000000..02e8d14e5e
--- /dev/null
+++ b/InvenTree/InvenTree/test_tasks.py
@@ -0,0 +1,43 @@
+"""
+Unit tests for task management
+"""
+
+from django.test import TestCase
+from django_q.models import Schedule
+
+import InvenTree.tasks
+
+
+class ScheduledTaskTests(TestCase):
+    """
+    Unit tests for scheduled tasks
+    """
+
+    def get_tasks(self, name):
+
+        return Schedule.objects.filter(func=name)
+
+    def test_add_task(self):
+        """
+        Ensure that duplicate tasks cannot be added.
+        """
+
+        task = 'InvenTree.tasks.heartbeat'
+
+        self.assertEqual(self.get_tasks(task).count(), 0)
+
+        InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=10)
+
+        self.assertEqual(self.get_tasks(task).count(), 1)
+
+        t = Schedule.objects.get(func=task)
+
+        self.assertEqual(t.minutes, 10)
+
+        # Attempt to schedule the same task again
+        InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
+        self.assertEqual(self.get_tasks(task).count(), 1)
+        
+        # But the 'minutes' should have been updated
+        t = Schedule.objects.get(func=task)
+        self.assertEqual(t.minutes, 5)

From 4a3ca4638c6ec1e4da9359c5a6a316073ff64e09 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 7 Apr 2021 22:27:55 +1000
Subject: [PATCH 067/111] Dockerfile updates

---
 InvenTree/InvenTree/management/commands/wait_for_db.py | 2 +-
 docker/Dockerfile                                      | 2 +-
 docker/start.sh                                        | 4 +++-
 docker/{supervisor.conf => supervisord.conf}           | 3 ++-
 4 files changed, 7 insertions(+), 4 deletions(-)
 rename docker/{supervisor.conf => supervisord.conf} (88%)

diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
index 5d152d26ca..9019a168ef 100644
--- a/InvenTree/InvenTree/management/commands/wait_for_db.py
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -27,5 +27,5 @@ class Command(BaseCommand):
                 # prints success messge in green
                 self.stdout.write(self.style.SUCCESS('InvenTree database connected'))
             except OperationalError:
-                self.stdout.write("Database unavailable, waiting 5 seconds ...")
+                self.stdout.write(self.style.ERROR("Database unavailable, waiting 5 seconds ..."))
                 time.sleep(5)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 99131700ee..3eb62af93b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -79,7 +79,7 @@ RUN apk add --no-cache supervisor
 RUN mkdir ${INVENTREE_HOME}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
 
 # Copy supervisor file
-COPY docker/supervisor.conf /etc/supervisord.conf
+COPY docker/supervisord.conf ${INVENTREE_HOME}/supervisord.conf
 
 # Copy gunicorn config file
 COPY docker/gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
diff --git a/docker/start.sh b/docker/start.sh
index 3e1287527b..21334d812a 100644
--- a/docker/start.sh
+++ b/docker/start.sh
@@ -15,6 +15,8 @@ fi
 # Activate virtual environment
 source $INVENTREE_VENV/bin/activate
 
+sleep 5
+
 # Wait for the database to be ready
 cd $INVENTREE_MNG_DIR
 python manage.py wait_for_db
@@ -32,4 +34,4 @@ python manage.py collectstatic --noinput || exit 1
 python manage.py clearsessions || exit 1
 
 # Now we can launch the server and background worker
-/usr/bin/supervisord -c /etc/supervisord.conf
+/usr/bin/supervisord -c $INVENTREE_HOME/supervisord.conf
diff --git a/docker/supervisor.conf b/docker/supervisord.conf
similarity index 88%
rename from docker/supervisor.conf
rename to docker/supervisord.conf
index 554200837d..e0eaad4cfb 100644
--- a/docker/supervisor.conf
+++ b/docker/supervisord.conf
@@ -13,6 +13,7 @@
 ; InvenTree uses django-q for this purpose
 
 [supervisord]
+user=inventree
 nodaemon=true
 
 [program:inventree-server]
@@ -31,7 +32,7 @@ stderr_logfile_maxbytes=0
 [program:inventree-cluster]
 user=inventree
 directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
-command=%(ENV_INVENTREE_VENV)s/bin/python manage.py qcluster
+command=%(ENV_INVENTREE_VENV)s/bin/python DJANGO_SETTINGS_FILE=InvenTree.settings manage.py qcluster
 startsecs=10
 autostart=true
 autorestart=true

From d4d9263131b139e5aa63cfe2d0d2c748f1312f70 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 7 Apr 2021 23:46:03 +1000
Subject: [PATCH 068/111] Add option to specify config file via environment
 variable

---
 InvenTree/InvenTree/settings.py | 16 +++++++++++++++-
 docker/Dockerfile               | 12 ++++--------
 docker/start.sh                 | 10 +++-------
 docker/supervisord.conf         |  2 +-
 4 files changed, 23 insertions(+), 17 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 3f705ae70c..be9a4b729f 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -55,7 +55,21 @@ TESTING = 'test' in sys.argv
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
-cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
+# Specify where the "config file" is located.
+# By default, this is 'config.yaml'
+
+cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
+
+if cfg_filename:
+    cfg_filename = cfg_filename.strip()
+    cfg_filename = os.path.abspath(cfg_filename)
+
+    if not os.path.exists(cfg_filename):
+        print(f"InvenTree configuration file '{cfg_filename}' does not exist!")
+        sys.exit(1)
+else:
+    # Config file is *not* specified - use the default
+    cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
 
 if not os.path.exists(cfg_filename):
     print("InvenTree configuration file 'config.yaml' not found - creating default file")
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 3eb62af93b..073698adcd 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -3,7 +3,6 @@ FROM python:alpine as production
 # GitHub source
 ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
 ARG INVENTREE_VERSION="master"
-ARG INVENTREE_CONFIG_FILE="InvenTree/config_template.yaml"
 
 ENV PYTHONUNBUFFERED 1
 
@@ -79,18 +78,15 @@ RUN apk add --no-cache supervisor
 RUN mkdir ${INVENTREE_HOME}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
 
 # Copy supervisor file
-COPY docker/supervisord.conf ${INVENTREE_HOME}/supervisord.conf
+COPY supervisord.conf ${INVENTREE_HOME}/supervisord.conf
 
 # Copy gunicorn config file
-COPY docker/gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
-
-# Copy default InvenTree config file
-COPY ${INVENTREE_CONFIG_FILE} ${INVENTREE_SRC_DIR}/InvenTree/config.yaml
+COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
 
 # Copy startup script
-COPY docker/start.sh ${INVENTREE_HOME}/start.sh
+COPY start.sh ${INVENTREE_HOME}/start.sh
 
 RUN chmod 755 ${INVENTREE_HOME}/start.sh
 
 # Let us begin
-CMD "./start.sh"
+CMD ["bash", "./start.sh"]
diff --git a/docker/start.sh b/docker/start.sh
index 21334d812a..f17150d0ca 100644
--- a/docker/start.sh
+++ b/docker/start.sh
@@ -1,17 +1,13 @@
 #!/bin/sh
 
+echo "Starting InvenTree server..."
+
 # Check that the database engine is specified
 if [ -z "$INVENTREE_DB_ENGINE" ]; then
     echo "INVENTREE_DB_ENGINE not configured"
     exit 1
 fi
 
-# Check that the base dir is set
-if [ -z "$INVENTREE_HOME" ]; then
-    echo "INVENTREE_HOME not configured"
-    exit 1
-fi
-
 # Activate virtual environment
 source $INVENTREE_VENV/bin/activate
 
@@ -34,4 +30,4 @@ python manage.py collectstatic --noinput || exit 1
 python manage.py clearsessions || exit 1
 
 # Now we can launch the server and background worker
-/usr/bin/supervisord -c $INVENTREE_HOME/supervisord.conf
+/usr/bin/supervisord -c $INVENTREE_HOME/supervisord.conf
\ No newline at end of file
diff --git a/docker/supervisord.conf b/docker/supervisord.conf
index e0eaad4cfb..8dc8e77fc1 100644
--- a/docker/supervisord.conf
+++ b/docker/supervisord.conf
@@ -19,7 +19,7 @@ nodaemon=true
 [program:inventree-server]
 user=inventree
 directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
-command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:80
+command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8080
 startsecs=10
 autostart=true
 autorestart=true

From 14aead038e2ec49d71689511a40112c9e9805e30 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 7 Apr 2021 23:46:30 +1000
Subject: [PATCH 069/111] Adds docker_compose file

---
 docker/docker-compose.yml | 49 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)
 create mode 100644 docker/docker-compose.yml

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000000..abf57c24a7
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,49 @@
+version: "3.8"
+
+# Docker compose recipe for InvenTree
+# - Runs PostgreSQL as the database backend
+# - Serves web data using Gunicorn
+
+services:
+    # Use PostgreSQL as the database backend
+    # Note: this can be changed to a different backend,
+    #       just make sure that you change the INVENTREE_DB_xxx vars below
+    db:
+        image: postgres
+        container_name: db
+        ports:
+            - 5432/tcp
+        environment:
+            - PGDATA=/var/lib/postgresql/data/pgdb
+            - POSTGRES_USER=pguser
+            - POSTGRES_PASSWORD=pgpassword
+        volumes:
+            - database_data:/var/lib/postgresql/data/
+        restart: unless-stopped
+
+    inventree:
+        build: .
+        image: inventree/inventree:latest
+        container_name: inventree
+        ports:
+            - 8080:8080
+        depends_on:
+            - db
+        volumes:
+            - static_volume:/home/inventree/static
+            - media_volume:/home/inventree/media
+            - backup_volume:/home/inventree/backup
+        environment:
+            - INVENTREE_DB_ENGINE=postgresql
+            - INVENTREE_DB_NAME=inventree
+            - INVENTREE_DB_USER=pguser
+            - INVENTREE_DB_PASSWORD=pgpassword
+            - INVENTREE_DB_PORT=5432
+            - INVENTREE_DB_HOST=db
+        restart: unless-stopped
+
+volumes:
+    database_data:
+    static_volume:
+    media_volume:
+    backup_volume:

From ed304f571a78a839018154b120a7081ab3b92fa9 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 8 Apr 2021 00:05:37 +1000
Subject: [PATCH 070/111] Better configuration of github repo

---
 .github/workflows/docker.yaml                          |  2 +-
 InvenTree/InvenTree/management/commands/wait_for_db.py |  4 +++-
 docker/Dockerfile                                      | 10 +++++++---
 3 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 749616d716..34ca585c98 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -12,5 +12,5 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       - name: Build Docker Image
-        run: docker build . --file docker/Dockerfile --tag inventree:$(date +%s)
+        run: cd docker && docker build . --tag inventree:$(date +%s)
     
\ No newline at end of file
diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
index 9019a168ef..198c5dcf73 100644
--- a/InvenTree/InvenTree/management/commands/wait_for_db.py
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -7,6 +7,8 @@ from django.core.management.base import BaseCommand
 from django.db import connections
 from django.db.utils import OperationalError
 
+import psycopg2
+
 import time
 
 
@@ -26,6 +28,6 @@ class Command(BaseCommand):
                 db_conn = connections['default']
                 # prints success messge in green
                 self.stdout.write(self.style.SUCCESS('InvenTree database connected'))
-            except OperationalError:
+            except (OperationalError, psycopg2.OperationalError):
                 self.stdout.write(self.style.ERROR("Database unavailable, waiting 5 seconds ..."))
                 time.sleep(5)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 073698adcd..a86fd09e45 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,14 +1,18 @@
 FROM python:alpine as production
 
 # GitHub source
-ARG INVENTREE_REPO="https://github.com/inventree/InvenTree.git"
-ARG INVENTREE_VERSION="master"
+ARG repository="https://github.com/inventree/InvenTree.git"
+ARG branch="master"
 
 ENV PYTHONUNBUFFERED 1
 
 # InvenTree key settings
 ENV INVENTREE_HOME="/home/inventree"
 
+# GitHub settings
+ENV INVENTREE_REPO="${repository}"
+ENV INVENTREE_BRANCH="${branch}"
+
 ENV INVENTREE_LOG_LEVEL="INFO"
 
 # InvenTree paths
@@ -57,7 +61,7 @@ RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
 RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
 # Clone source code
-RUN git clone --branch ${INVENTREE_VERSION} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
+RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
 # Setup Python virtual environment
 RUN python3 -m venv ${INVENTREE_VENV}

From 71cac6e2697930904f834f33de06abd23015cd7b Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 8 Apr 2021 00:09:51 +1000
Subject: [PATCH 071/111] Simplify waiting for db

---
 InvenTree/InvenTree/management/commands/wait_for_db.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
index 198c5dcf73..8709f5315e 100644
--- a/InvenTree/InvenTree/management/commands/wait_for_db.py
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand
 from django.db import connections
 from django.db.utils import OperationalError
 
-import psycopg2
+from part.models import Part
 
 import time
 
@@ -26,8 +26,12 @@ class Command(BaseCommand):
             try:
                 # get the database with keyword 'default' from settings.py
                 db_conn = connections['default']
+
+                # Try to read some data from the database
+                Part.objects.count()
+
                 # prints success messge in green
                 self.stdout.write(self.style.SUCCESS('InvenTree database connected'))
-            except (OperationalError, psycopg2.OperationalError):
+            except:
                 self.stdout.write(self.style.ERROR("Database unavailable, waiting 5 seconds ..."))
                 time.sleep(5)

From 3926276fd189410db823ee0d97eaf21c25f9d8e9 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 8 Apr 2021 00:37:34 +1000
Subject: [PATCH 072/111] Greatly simplified "wait_for_db" command

---
 .../management/commands/wait_for_db.py        | 35 +++++++++++--------
 1 file changed, 20 insertions(+), 15 deletions(-)

diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
index 8709f5315e..dc510a5fd2 100644
--- a/InvenTree/InvenTree/management/commands/wait_for_db.py
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -4,10 +4,8 @@ Custom management command, wait for the database to be ready!
 
 from django.core.management.base import BaseCommand
 
-from django.db import connections
-from django.db.utils import OperationalError
-
-from part.models import Part
+from django.db import connection
+from django.db.utils import OperationalError, ImproperlyConfigured
 
 import time
 
@@ -18,20 +16,27 @@ class Command(BaseCommand):
     """
 
     def handle(self, *args, **kwargs):
+
         self.stdout.write("Waiting for database...")
 
-        db_conn = None
+        connected = False
+
+        while not connected:
+
+            time.sleep(5)
 
-        while not db_conn:
             try:
-                # get the database with keyword 'default' from settings.py
-                db_conn = connections['default']
+                connection.ensure_connection()
 
-                # Try to read some data from the database
-                Part.objects.count()
+                connected = True
 
-                # prints success messge in green
-                self.stdout.write(self.style.SUCCESS('InvenTree database connected'))
-            except:
-                self.stdout.write(self.style.ERROR("Database unavailable, waiting 5 seconds ..."))
-                time.sleep(5)
+            except OperationalError as e:
+                self.stdout.write(f"Could not connect to database: {e}")
+            except ImproperlyConfigured as e:
+                self.stdout.write(f"Improperly configured: {e}")
+            else:
+                if not connection.is_usable():
+                    self.stdout.write("Database configuration is not usable")
+
+            if connected:
+                self.stdout.write("Database connection sucessful!")
\ No newline at end of file

From 3381945e14910a98fc4cbf90f3e55ce82f3b3270 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 8 Apr 2021 17:10:48 +1000
Subject: [PATCH 073/111] Add newline

---
 InvenTree/InvenTree/management/commands/wait_for_db.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
index dc510a5fd2..b9fa4e5025 100644
--- a/InvenTree/InvenTree/management/commands/wait_for_db.py
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -39,4 +39,4 @@ class Command(BaseCommand):
                     self.stdout.write("Database configuration is not usable")
 
             if connected:
-                self.stdout.write("Database connection sucessful!")
\ No newline at end of file
+                self.stdout.write("Database connection sucessful!")

From 47a93bc4cbb63c7f33a8a4dd97d96539ce6f9959 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Thu, 8 Apr 2021 21:01:52 +1000
Subject: [PATCH 074/111] More environment variables for config.yaml

---
 InvenTree/InvenTree/settings.py | 7 +++++--
 InvenTree/config_template.yaml  | 3 +++
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index be9a4b729f..338a2dda9d 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -409,7 +409,7 @@ reqiured_keys = ['ENGINE', 'NAME']
 
 for key in reqiured_keys:
     if key not in db_config:
-        error_msg = f'Missing required database configuration value {key} in config.yaml'
+        error_msg = f'Missing required database configuration value {key}'
         logger.error(error_msg)
 
         print('Error: ' + error_msg)
@@ -503,7 +503,10 @@ LOCALE_PATHS = (
     os.path.join(BASE_DIR, 'locale/'),
 )
 
-TIME_ZONE = CONFIG.get('timezone', 'UTC')
+TIME_ZONE = get_setting(
+    'INVENTREE_TIMEZONE',
+    CONFIG.get('timezone', 'UTC')
+)
 
 USE_I18N = True
 
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index fb30f2d339..9e18adb759 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -49,6 +49,7 @@ language: en-us
 # System time-zone (default is UTC)
 # Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
 # Select an option from the "TZ database name" column
+# Use the environment variable INVENTREE_TIMEZONE
 timezone: UTC
 
 # List of currencies supported by default.
@@ -63,6 +64,7 @@ currencies:
   - USD
 
 # Set debug to False to run in production mode
+# Use the environment variable INVENTREE_DEBUG
 debug: True
 
 # Set debug_toolbar to True to enable a debugging toolbar for InvenTree
@@ -71,6 +73,7 @@ debug: True
 debug_toolbar: False
 
 # Configure the system logging level
+# Use environment variable INVENTREE_LOG_LEVEL
 # Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
 log_level: WARNING
 

From b5a5f5b4094b5eb59c4c63d50a6087110e300806 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 8 Apr 2021 13:42:35 -0400
Subject: [PATCH 075/111] Simplified stock table view in Part and SupplierPart
 detail pages

---
 .../company/templates/company/supplier_part_stock.html   | 2 +-
 InvenTree/part/templates/part/stock.html                 | 2 +-
 InvenTree/templates/js/stock.js                          | 9 +++++++--
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/InvenTree/company/templates/company/supplier_part_stock.html b/InvenTree/company/templates/company/supplier_part_stock.html
index 49a5a809c2..524c508957 100644
--- a/InvenTree/company/templates/company/supplier_part_stock.html
+++ b/InvenTree/company/templates/company/supplier_part_stock.html
@@ -22,7 +22,7 @@
         params: {
             supplier_part: {{ part.id }},
             location_detail: true,
-            part_detail: true,
+            part_detail: false,
         },
         groupByField: 'location',
         buttons: ['#stock-options'],
diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html
index cd4f9ab2fa..2d1385f89c 100644
--- a/InvenTree/part/templates/part/stock.html
+++ b/InvenTree/part/templates/part/stock.html
@@ -40,7 +40,7 @@
         params: {
             part: {{ part.id }},
             location_detail: true,
-            part_detail: true,
+            part_detail: false,
         },
         groupByField: 'location',
         buttons: [
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index 02714810e3..cba3e56b35 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -241,7 +241,7 @@ function loadStockTable(table, options) {
 
     // List of user-params which override the default filters
 
-    options.params['part_detail'] = true;
+    // options.params['part_detail'] = true;
     options.params['location_detail'] = true;
 
     var params = options.params || {};
@@ -524,7 +524,8 @@ function loadStockTable(table, options) {
                 title: '{% trans "Part" %}',
                 sortName: 'part__name',
                 sortable: true,
-                switchable: false,
+                visible: params['part_detail'],
+                switchable: params['part_detail'],
                 formatter: function(value, row, index, field) {
 
                     var url = `/stock/item/${row.pk}/`;
@@ -543,6 +544,8 @@ function loadStockTable(table, options) {
                 title: 'IPN',
                 sortName: 'part__IPN',
                 sortable: true,
+                visible: params['part_detail'],
+                switchable: params['part_detail'],
                 formatter: function(value, row, index, field) {
                     return row.part_detail.IPN;
                 },
@@ -550,6 +553,8 @@ function loadStockTable(table, options) {
             {
                 field: 'part_detail.description',
                 title: '{% trans "Description" %}',
+                visible: params['part_detail'],
+                switchable: params['part_detail'],
                 formatter: function(value, row, index, field) {
                     return row.part_detail.description;
                 }

From 7491cda313cd512f21cd7bb1988ad45a734bec9e Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 8 Apr 2021 14:35:47 -0400
Subject: [PATCH 076/111] Replace normalize with integer wrapper for quantity
 field

---
 InvenTree/part/admin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index 4681404ba1..d08d5f1eaa 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -180,7 +180,7 @@ class BomItemResource(ModelResource):
 
         Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
         """
-        return normalize(item.quantity)
+        return int(item.quantity)
 
     def before_export(self, queryset, *args, **kwargs):
 

From cbddda6640693d8b2a035f8a1e6dadff452ecbe5 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 8 Apr 2021 14:41:06 -0400
Subject: [PATCH 077/111] Remove normalize import

---
 InvenTree/part/admin.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index d08d5f1eaa..eb728c5295 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -16,8 +16,6 @@ from .models import PartCategoryParameterTemplate
 from .models import PartTestTemplate
 from .models import PartSellPriceBreak
 
-from InvenTree.helpers import normalize
-
 from stock.models import StockLocation
 from company.models import SupplierPart
 

From 97e1bc0a67d18aa13e0b556607f067bffa1fc62b Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 8 Apr 2021 21:46:11 -0400
Subject: [PATCH 078/111] Added missing part_detail reference

---
 InvenTree/templates/InvenTree/index.html | 1 +
 InvenTree/templates/js/stock.js          | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html
index b7807840c5..f7154e5fbb 100644
--- a/InvenTree/templates/InvenTree/index.html
+++ b/InvenTree/templates/InvenTree/index.html
@@ -131,6 +131,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa
 
 loadStockTable($('#table-recently-updated-stock'), {
     params: {
+        part_detail: true,
         ordering: "-updated",
         max_results: {% settings_value "STOCK_RECENT_COUNT" %},
     },
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index cba3e56b35..b163bc89f3 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -241,7 +241,6 @@ function loadStockTable(table, options) {
 
     // List of user-params which override the default filters
 
-    // options.params['part_detail'] = true;
     options.params['location_detail'] = true;
 
     var params = options.params || {};

From afddf12339c36b26f7dd3e7c200b9fe1b25f6ae9 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Thu, 8 Apr 2021 22:04:26 -0400
Subject: [PATCH 079/111] Changed int to float

---
 InvenTree/part/admin.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index eb728c5295..af2f615803 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -178,7 +178,7 @@ class BomItemResource(ModelResource):
 
         Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
         """
-        return int(item.quantity)
+        return float(item.quantity)
 
     def before_export(self, queryset, *args, **kwargs):
 

From 6bf4140e5a4f311f4b289521c5c83bf6d99848db Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Fri, 9 Apr 2021 16:55:05 -0400
Subject: [PATCH 080/111] Fixed transfer stock action in template

---
 InvenTree/stock/templates/stock/item_base.html | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index a72b727f69..04518a0c65 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -165,13 +165,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
                 {% if item.in_stock %}
                 <li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
                 {% endif %}
-                {% if item.in_stock and item.can_adjust_location %}
-                <li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
-                {% endif %}
                 {% if item.in_stock and item.part.trackable %}
                 <li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
                 {% endif %}
                 {% endif %}
+                {% if item.in_stock and item.can_adjust_location %}
+                <li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
+                {% endif %}
                 {% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
                 <li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
                 {% endif %}

From 8eb571bddf292d697b5a92672af7087be4550081 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 15:08:10 +1000
Subject: [PATCH 081/111] Update dockerfile

---
 InvenTree/InvenTree/settings.py      |  2 +-
 docker/Dockerfile                    | 29 ++++++++++++----------------
 docker/{start.sh => start_server.sh} |  4 ++--
 docker/start_worker.sh               | 24 +++++++++++++++++++++++
 docker/supervisord.conf              | 11 ++++++-----
 5 files changed, 45 insertions(+), 25 deletions(-)
 rename docker/{start.sh => start_server.sh} (87%)
 create mode 100644 docker/start_worker.sh

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 338a2dda9d..5e259a66e6 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -338,7 +338,7 @@ Q_CLUSTER = {
     'queue_limit': 50,
     'bulk': 10,
     'orm': 'default',
-    'sync': True,
+    'sync': False,
 }
 
 # Markdownx configuration
diff --git a/docker/Dockerfile b/docker/Dockerfile
index a86fd09e45..893f028b9b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -42,8 +42,6 @@ LABEL org.label-schema.schema-version="1.0" \
       org.label-schema.vcs-branch=${BRANCH} \
       org.label-schema.vcs-ref=${COMMIT}
 
-RUN echo "Installing InvenTree '${INVENTREE_VERSION}' from ${INVENTREE_REPO}"
-
 # Create user account
 RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
 WORKDIR ${INVENTREE_HOME}
@@ -60,8 +58,8 @@ RUN apk add --no-cache python3
 RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
 RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
-# Clone source code
-RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
+# Create required directories
+RUN mkdir ${INVENTREE_HOME}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
 
 # Setup Python virtual environment
 RUN python3 -m venv ${INVENTREE_VENV}
@@ -72,25 +70,22 @@ RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U invok
 RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
 RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U gunicorn
 
+# Clone source code
+RUN echo "Downloading InvenTree from ${INVENTREE_REPO}"
+RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
+
 # Install InvenTree packages
 RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
 
-# Install supervisor
-RUN apk add --no-cache supervisor
-
-# Create required directories
-RUN mkdir ${INVENTREE_HOME}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
-
-# Copy supervisor file
-COPY supervisord.conf ${INVENTREE_HOME}/supervisord.conf
-
 # Copy gunicorn config file
 COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
 
-# Copy startup script
-COPY start.sh ${INVENTREE_HOME}/start.sh
+# Copy startup scripts
+COPY start_server.sh ${INVENTREE_HOME}/start_server.sh
+COPY start_worker.sh ${INVENTREE_HOME}/start_worker.sh
 
-RUN chmod 755 ${INVENTREE_HOME}/start.sh
+RUN chmod 755 ${INVENTREE_HOME}/start_server.sh
+RUN chmod 755 ${INVENTREE_HOME}/start_worker.sh
 
 # Let us begin
-CMD ["bash", "./start.sh"]
+CMD ["bash", "./start_server.sh"]
diff --git a/docker/start.sh b/docker/start_server.sh
similarity index 87%
rename from docker/start.sh
rename to docker/start_server.sh
index f17150d0ca..fb84783829 100644
--- a/docker/start.sh
+++ b/docker/start_server.sh
@@ -29,5 +29,5 @@ python manage.py migrate --run-syncdb || exit 1
 python manage.py collectstatic --noinput || exit 1
 python manage.py clearsessions || exit 1
 
-# Now we can launch the server and background worker
-/usr/bin/supervisord -c $INVENTREE_HOME/supervisord.conf
\ No newline at end of file
+# Now we can launch the server
+gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8080
diff --git a/docker/start_worker.sh b/docker/start_worker.sh
new file mode 100644
index 0000000000..9b7f3f408e
--- /dev/null
+++ b/docker/start_worker.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+echo "Starting InvenTree worker..."
+
+# Check that the database engine is specified
+if [ -z "$INVENTREE_DB_ENGINE" ]; then
+    echo "INVENTREE_DB_ENGINE not configured"
+    exit 1
+fi
+
+# Activate virtual environment
+source ./env/bin/activate
+
+sleep 5
+
+# Wait for the database to be ready
+cd src/InvenTree
+
+python manage.py wait_for_db
+
+sleep 10
+
+# Now we can launch the background worker process
+python manage.py qcluster
diff --git a/docker/supervisord.conf b/docker/supervisord.conf
index 8dc8e77fc1..b99c48edcf 100644
--- a/docker/supervisord.conf
+++ b/docker/supervisord.conf
@@ -16,18 +16,20 @@
 user=inventree
 nodaemon=true
 
+[supervisorctl]
+serverurl=unix:///var/run/supervisor.sock
+
 [program:inventree-server]
 user=inventree
 directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
-command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8080
+command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 127.0.0.1:8080
 startsecs=10
 autostart=true
 autorestart=true
 startretries=3
 stdout_logfile=/dev/fd/1
 stdout_logfile_maxbytes=0
-stderr_logfile=/dev/fd/2
-stderr_logfile_maxbytes=0
+redirect_stderr=true
 
 [program:inventree-cluster]
 user=inventree
@@ -38,5 +40,4 @@ autostart=true
 autorestart=true
 stdout_logfile=/dev/fd/1
 stdout_logfile_maxbytes=0
-stderr_logfile=/dev/fd/2
-stderr_logfile_maxbytes=0
\ No newline at end of file
+redirect_stderr=true
\ No newline at end of file

From 1372343bd5fd6580a4aba5055fa9a7795308ace9 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 15:27:50 +1000
Subject: [PATCH 082/111] Updates to docker-compose file

- Note: not ready yet!
---
 docker/docker-compose.yml | 41 ++++++++++++++++++++++++++++++++++-----
 1 file changed, 36 insertions(+), 5 deletions(-)

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index abf57c24a7..ba9d8e1261 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -3,6 +3,8 @@ version: "3.8"
 # Docker compose recipe for InvenTree
 # - Runs PostgreSQL as the database backend
 # - Serves web data using Gunicorn
+# - Runs the background worker process
+# - Runs nginx as a reverse proxy
 
 services:
     # Use PostgreSQL as the database backend
@@ -10,7 +12,7 @@ services:
     #       just make sure that you change the INVENTREE_DB_xxx vars below
     db:
         image: postgres
-        container_name: db
+        container_name: inventree_db
         ports:
             - 5432/tcp
         environment:
@@ -21,12 +23,16 @@ services:
             - database_data:/var/lib/postgresql/data/
         restart: unless-stopped
 
-    inventree:
-        build: .
+    server:
+        build:
+            context: .
+            args:
+                repository: "https://github.com/SchrodingersGat/InvenTree.git"
+                branch: "django-q"
         image: inventree/inventree:latest
-        container_name: inventree
+        container_name: inventree_server
         ports:
-            - 8080:8080
+            - "8080:8080"
         depends_on:
             - db
         volumes:
@@ -42,6 +48,31 @@ services:
             - INVENTREE_DB_HOST=db
         restart: unless-stopped
 
+    worker:
+        build:
+            context: .
+            args:
+                repository: "https://github.com/SchrodingersGat/InvenTree.git"
+                branch: "django-q"
+        entrypoint: ./start_worker.sh
+        image: inventree/worker:latest
+        container_name: inventree_worker
+        depends_on:
+            - db
+            - server
+        volumes:
+            - static_volume:/home/inventree/static
+            - media_volume:/home/inventree/media
+            - backup_volume:/home/inventree/backup
+        environment:
+            - INVENTREE_DB_ENGINE=postgresql
+            - INVENTREE_DB_NAME=inventree
+            - INVENTREE_DB_USER=pguser
+            - INVENTREE_DB_PASSWORD=pgpassword
+            - INVENTREE_DB_PORT=5432
+            - INVENTREE_DB_HOST=db
+        restart: unless-stopped
+
 volumes:
     database_data:
     static_volume:

From e6bd91c9e2fc72b380dd5212990a3796df5b467b Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 15:29:44 +1000
Subject: [PATCH 083/111] Company description is no longer a required field

---
 .../migrations/0033_auto_20210410_1528.py      | 18 ++++++++++++++++++
 InvenTree/company/models.py                    |  7 ++++++-
 2 files changed, 24 insertions(+), 1 deletion(-)
 create mode 100644 InvenTree/company/migrations/0033_auto_20210410_1528.py

diff --git a/InvenTree/company/migrations/0033_auto_20210410_1528.py b/InvenTree/company/migrations/0033_auto_20210410_1528.py
new file mode 100644
index 0000000000..12153a9ef6
--- /dev/null
+++ b/InvenTree/company/migrations/0033_auto_20210410_1528.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2021-04-10 05:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('company', '0032_auto_20210403_1837'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='company',
+            name='description',
+            field=models.CharField(blank=True, help_text='Description of the company', max_length=500, verbose_name='Company description'),
+        ),
+    ]
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index 17091dcbc3..d83748c930 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -95,7 +95,12 @@ class Company(models.Model):
                             help_text=_('Company name'),
                             verbose_name=_('Company name'))
 
-    description = models.CharField(max_length=500, verbose_name=_('Company description'), help_text=_('Description of the company'))
+    description = models.CharField(
+        max_length=500,
+        verbose_name=_('Company description'),
+        help_text=_('Description of the company'),
+        blank=True,
+    )
 
     website = models.URLField(blank=True, verbose_name=_('Website'), help_text=_('Company website URL'))
 

From 9086c8a3bf202be3debbd81f86aa5681bd7bbe29 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 17:36:19 +1000
Subject: [PATCH 084/111] Simplify external directory structure

- All InvenTree data now in a single subdir
- Copy default config file (if it does not exist)
- Config file is accessible from outside world
- Update start_server and start_worker scripts
---
 docker/Dockerfile         | 12 +++++++-----
 docker/docker-compose.yml | 20 ++++++--------------
 docker/start_server.sh    | 28 ++++++++++++++++++++++------
 docker/start_worker.sh    |  6 ------
 4 files changed, 35 insertions(+), 31 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 893f028b9b..89af08f835 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -18,12 +18,14 @@ ENV INVENTREE_LOG_LEVEL="INFO"
 # InvenTree paths
 ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
 ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
-ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
-ENV INVENTREE_MEDIA_ROOT="${INVENTREE_HOME}/media"
-ENV INVENTREE_LOG_DIR="${INVENTREE_HOME}/log"
-ENV INVENTREE_BACKUP_DIR="${INVENTREE_HOME}/backup"
+ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
+ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
+ENV INVENTREE_MEDIA_ROOT="${INVENTREE_MEDIA_DIR}/media"
+ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
 ENV INVENTREE_VENV="${INVENTREE_HOME}/env"
 
+ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
+
 # Pass DB configuration through as environment variables
 ENV INVENTREE_DB_ENGINE="${INVENTREE_DB_ENGINE}"
 ENV INVENTREE_DB_NAME="${INVENTREE_DB_NAME}"
@@ -59,7 +61,7 @@ RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
 RUN apk add --no-cache mariadb-connector-c mariadb-dev
 
 # Create required directories
-RUN mkdir ${INVENTREE_HOME}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
+#RUN mkdir ${INVENTREE_DATA_DIR}}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
 
 # Setup Python virtual environment
 RUN python3 -m venv ${INVENTREE_VENV}
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index ba9d8e1261..d7d2c09cdb 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -20,7 +20,9 @@ services:
             - POSTGRES_USER=pguser
             - POSTGRES_PASSWORD=pgpassword
         volumes:
-            - database_data:/var/lib/postgresql/data/
+            # Map external directory to store database data
+            # Replace /path/to/dir with the required external path
+            - /mnt/c/abcdatabase:/var/lib/postgresql/data/
         restart: unless-stopped
 
     server:
@@ -36,9 +38,9 @@ services:
         depends_on:
             - db
         volumes:
-            - static_volume:/home/inventree/static
-            - media_volume:/home/inventree/media
-            - backup_volume:/home/inventree/backup
+            # Map external directory to store InvenTree data
+            # Replace /path/to/dir with the required external path
+            - /mnt/c/abcde:/home/inventree/data
         environment:
             - INVENTREE_DB_ENGINE=postgresql
             - INVENTREE_DB_NAME=inventree
@@ -60,10 +62,6 @@ services:
         depends_on:
             - db
             - server
-        volumes:
-            - static_volume:/home/inventree/static
-            - media_volume:/home/inventree/media
-            - backup_volume:/home/inventree/backup
         environment:
             - INVENTREE_DB_ENGINE=postgresql
             - INVENTREE_DB_NAME=inventree
@@ -72,9 +70,3 @@ services:
             - INVENTREE_DB_PORT=5432
             - INVENTREE_DB_HOST=db
         restart: unless-stopped
-
-volumes:
-    database_data:
-    static_volume:
-    media_volume:
-    backup_volume:
diff --git a/docker/start_server.sh b/docker/start_server.sh
index fb84783829..e9eaff1dfa 100644
--- a/docker/start_server.sh
+++ b/docker/start_server.sh
@@ -1,17 +1,33 @@
 #!/bin/sh
 
-echo "Starting InvenTree server..."
+# Create required directory structure (if it does not already exist)
+if [[ ! -d "$INVENTREE_STATIC_ROOT" ]]; then
+    echo "Creating directory $INVENTREE_STATIC_ROOT"
+    mkdir $INVENTREE_STATIC_ROOT
+fi
 
-# Check that the database engine is specified
-if [ -z "$INVENTREE_DB_ENGINE" ]; then
-    echo "INVENTREE_DB_ENGINE not configured"
-    exit 1
+if [[ ! -d "$INVENTREE_MEDIA_ROOT" ]]; then
+    echo "Creating directory $INVENTREE_MEDIA_ROOT"
+    mkdir $INVENTREE_MEDIA_ROOT
+fi
+
+if [[ ! -d "$INVENTREE_BACKUP_DIR" ]]; then
+    echo "Creating directory $INVENTREE_BACKUP_DIR"
+    mkdir $INVENTREE_BACKUP_DIR
+fi
+
+# Check if "config.yaml" has been copied into the correct location
+if test -f "$INVENTREE_CONFIG_FILE"; then
+    echo "$INVENTREE_CONFIG_FILE exists - skipping"
+else
+    echo "Copying config file to $INVENTREE_CONFIG_FILE"
+    cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
 fi
 
 # Activate virtual environment
 source $INVENTREE_VENV/bin/activate
 
-sleep 5
+echo "Starting InvenTree server..."
 
 # Wait for the database to be ready
 cd $INVENTREE_MNG_DIR
diff --git a/docker/start_worker.sh b/docker/start_worker.sh
index 9b7f3f408e..a8a4815583 100644
--- a/docker/start_worker.sh
+++ b/docker/start_worker.sh
@@ -2,12 +2,6 @@
 
 echo "Starting InvenTree worker..."
 
-# Check that the database engine is specified
-if [ -z "$INVENTREE_DB_ENGINE" ]; then
-    echo "INVENTREE_DB_ENGINE not configured"
-    exit 1
-fi
-
 # Activate virtual environment
 source ./env/bin/activate
 

From 5e54b0f5cfc9b628451c23d30faaf89abb051eed Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 19:01:02 +1000
Subject: [PATCH 085/111] Auto-generate key file if it does not exist!

---
 InvenTree/InvenTree/settings.py | 24 +++++++++++-------------
 docker/Dockerfile               |  3 ++-
 2 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 5e259a66e6..fe0164af94 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -138,24 +138,22 @@ else:
     key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
 
     if key_file:
-        if os.path.isfile(key_file):
-            logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY_FILE")
-        else:
-            logger.error(f"Secret key file '{key_file}'' not found")
-            exit(-1)
+        key_file = os.path.abspath(key_file)
     else:
         # default secret key location
         key_file = os.path.join(BASE_DIR, "secret_key.txt")
+        key_file = os.path.abspath(key_file)
 
-        if not os.path.exists(key_file):
-            logger.info("Creating key file 'secret_key.txt'")
-            # Create a random key file
-            with open(key_file, 'w') as f:
-                options = string.digits + string.ascii_letters + string.punctuation
-                key = ''.join([random.choice(options) for i in range(50)])
-                f.write(key)
+    if not os.path.exists(key_file):
+        logger.info(f"Generating random key file at '{key_file}'")
+        # Create a random key file
+        with open(key_file, 'w') as f:
+            options = string.digits + string.ascii_letters + string.punctuation
+            key = ''.join([random.choice(options) for i in range(100)])
+            f.write(key)
 
-        logger.info(f"SECRET_KEY loaded from {key_file}")
+    logger.info(f"Loading SECRET_KEY from '{key_file}'")
+    
     try:
         SECRET_KEY = open(key_file, "r").read().strip()
     except Exception:
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 89af08f835..1fd9329d69 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -20,11 +20,12 @@ ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
 ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
 ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
 ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
-ENV INVENTREE_MEDIA_ROOT="${INVENTREE_MEDIA_DIR}/media"
+ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
 ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
 ENV INVENTREE_VENV="${INVENTREE_HOME}/env"
 
 ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
+ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
 
 # Pass DB configuration through as environment variables
 ENV INVENTREE_DB_ENGINE="${INVENTREE_DB_ENGINE}"

From e787c853e5b53c18f1d145b5366b82aea3e6b174 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 20:08:13 +1000
Subject: [PATCH 086/111] Update logger context

---
 InvenTree/InvenTree/api.py         | 2 +-
 InvenTree/InvenTree/apps.py        | 2 +-
 InvenTree/InvenTree/middleware.py  | 2 +-
 InvenTree/InvenTree/settings.py    | 4 ++--
 InvenTree/InvenTree/status.py      | 2 +-
 InvenTree/InvenTree/tasks.py       | 2 +-
 InvenTree/barcodes/barcode.py      | 2 +-
 InvenTree/company/apps.py          | 2 +-
 InvenTree/label/apps.py            | 2 +-
 InvenTree/label/models.py          | 2 +-
 InvenTree/order/views.py           | 2 +-
 InvenTree/part/apps.py             | 2 +-
 InvenTree/part/models.py           | 2 +-
 InvenTree/plugins/action/action.py | 2 +-
 InvenTree/plugins/plugins.py       | 2 +-
 InvenTree/report/apps.py           | 2 +-
 InvenTree/report/models.py         | 2 +-
 InvenTree/users/models.py          | 2 +-
 18 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index 2d04195d42..c7386e8fbc 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -23,7 +23,7 @@ from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceNam
 from plugins import plugins as inventree_plugins
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 logger.info("Loading action plugins...")
diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 07f565e012..03eb2bcb60 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -8,7 +8,7 @@ from django.core.exceptions import AppRegistryNotReady
 import InvenTree.tasks
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class InvenTreeConfig(AppConfig):
diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py
index 2f1cf3a157..a34df4b7bd 100644
--- a/InvenTree/InvenTree/middleware.py
+++ b/InvenTree/InvenTree/middleware.py
@@ -8,7 +8,7 @@ import operator
 
 from rest_framework.authtoken.models import Token
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class AuthRequiredMiddleware(object):
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index fe0164af94..a8a2060451 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -116,7 +116,7 @@ LOGGING = {
 }
 
 # Get a logger instance for this setup file
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 """
 Specify a secret key to be used by django.
@@ -153,7 +153,7 @@ else:
             f.write(key)
 
     logger.info(f"Loading SECRET_KEY from '{key_file}'")
-    
+
     try:
         SECRET_KEY = open(key_file, "r").read().strip()
     except Exception:
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index 92e4737f25..42160927b0 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -11,7 +11,7 @@ from datetime import datetime, timedelta
 from django_q.models import Success
 from django_q.monitor import Stat
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 def is_q_cluster_running(**kwargs):
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index c12d0ec4df..4829514f19 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -12,7 +12,7 @@ from django.core.exceptions import AppRegistryNotReady
 from django.db.utils import OperationalError, ProgrammingError
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 def schedule_task(taskname, **kwargs):
diff --git a/InvenTree/barcodes/barcode.py b/InvenTree/barcodes/barcode.py
index 412065bf75..a00e91d7e4 100644
--- a/InvenTree/barcodes/barcode.py
+++ b/InvenTree/barcodes/barcode.py
@@ -12,7 +12,7 @@ from stock.serializers import StockItemSerializer, LocationSerializer
 from part.serializers import PartSerializer
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger('inventree')
 
 
 def hash_barcode(barcode_data):
diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py
index 3fa3197183..53e884c50f 100644
--- a/InvenTree/company/apps.py
+++ b/InvenTree/company/apps.py
@@ -9,7 +9,7 @@ from django.conf import settings
 
 from PIL import UnidentifiedImageError
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class CompanyConfig(AppConfig):
diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py
index 9f2d3ea9c4..4200b6e8bc 100644
--- a/InvenTree/label/apps.py
+++ b/InvenTree/label/apps.py
@@ -7,7 +7,7 @@ from django.apps import AppConfig
 from django.conf import settings
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 def hashFile(filename):
diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py
index c76b3f80c8..96850f4cb0 100644
--- a/InvenTree/label/models.py
+++ b/InvenTree/label/models.py
@@ -32,7 +32,7 @@ except OSError as err:
     sys.exit(1)
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 def rename_label(instance, filename):
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index 7dc8b4efff..284a24fcf5 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -37,7 +37,7 @@ from InvenTree.views import InvenTreeRoleMixin
 
 from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py
index d08e7680fe..11329abdd9 100644
--- a/InvenTree/part/apps.py
+++ b/InvenTree/part/apps.py
@@ -9,7 +9,7 @@ from django.conf import settings
 
 from PIL import UnidentifiedImageError
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class PartConfig(AppConfig):
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index e10dfba4ba..bd02672b3e 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -52,7 +52,7 @@ import common.models
 import part.settings as part_settings
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class PartCategory(InvenTreeTree):
diff --git a/InvenTree/plugins/action/action.py b/InvenTree/plugins/action/action.py
index 8d70302021..d61838f49b 100644
--- a/InvenTree/plugins/action/action.py
+++ b/InvenTree/plugins/action/action.py
@@ -5,7 +5,7 @@ import logging
 import plugins.plugin as plugin
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class ActionPlugin(plugin.InvenTreePlugin):
diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py
index abb167d173..f6b68112bc 100644
--- a/InvenTree/plugins/plugins.py
+++ b/InvenTree/plugins/plugins.py
@@ -10,7 +10,7 @@ import plugins.action as action
 from plugins.action.action import ActionPlugin
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 def iter_namespace(pkg):
diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py
index 941133e481..77529263f6 100644
--- a/InvenTree/report/apps.py
+++ b/InvenTree/report/apps.py
@@ -6,7 +6,7 @@ from django.apps import AppConfig
 from django.conf import settings
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class ReportConfig(AppConfig):
diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py
index 6f3891d6be..4e218a2c50 100644
--- a/InvenTree/report/models.py
+++ b/InvenTree/report/models.py
@@ -38,7 +38,7 @@ except OSError as err:
     sys.exit(1)
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class ReportFileUpload(FileSystemStorage):
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 07bf2296ba..e454281b37 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -15,7 +15,7 @@ from django.db.models.signals import post_save, post_delete
 import logging
 
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("inventree")
 
 
 class RuleSet(models.Model):

From 178715ce6113b96aee9ec590498736fceee8485e Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 20:57:56 +1000
Subject: [PATCH 087/111] Auto create config file in specified location if it
 does not exist

---
 InvenTree/InvenTree/settings.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index a8a2060451..9a6a4d59ae 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -64,9 +64,6 @@ if cfg_filename:
     cfg_filename = cfg_filename.strip()
     cfg_filename = os.path.abspath(cfg_filename)
 
-    if not os.path.exists(cfg_filename):
-        print(f"InvenTree configuration file '{cfg_filename}' does not exist!")
-        sys.exit(1)
 else:
     # Config file is *not* specified - use the default
     cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
@@ -76,6 +73,7 @@ if not os.path.exists(cfg_filename):
 
     cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
     shutil.copyfile(cfg_template, cfg_filename)
+    print(f"Created config file {cfg_filename}")
 
 with open(cfg_filename, 'r') as cfg:
     CONFIG = yaml.safe_load(cfg)

From 823f84e46a75b178a652b046b56f00b82c80fbe1 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 20:58:51 +1000
Subject: [PATCH 088/111] Simplified volume management in docker-compose

---
 docker/docker-compose.yml | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index d7d2c09cdb..08eb84d45b 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -20,9 +20,7 @@ services:
             - POSTGRES_USER=pguser
             - POSTGRES_PASSWORD=pgpassword
         volumes:
-            # Map external directory to store database data
-            # Replace /path/to/dir with the required external path
-            - /mnt/c/abcdatabase:/var/lib/postgresql/data/
+            - data:/var/lib/postgresql/data/
         restart: unless-stopped
 
     server:
@@ -38,9 +36,7 @@ services:
         depends_on:
             - db
         volumes:
-            # Map external directory to store InvenTree data
-            # Replace /path/to/dir with the required external path
-            - /mnt/c/abcde:/home/inventree/data
+            - data:/home/inventree/data
         environment:
             - INVENTREE_DB_ENGINE=postgresql
             - INVENTREE_DB_NAME=inventree
@@ -62,6 +58,8 @@ services:
         depends_on:
             - db
             - server
+        volumes:
+            - data:/home/inventree/data
         environment:
             - INVENTREE_DB_ENGINE=postgresql
             - INVENTREE_DB_NAME=inventree
@@ -70,3 +68,12 @@ services:
             - INVENTREE_DB_PORT=5432
             - INVENTREE_DB_HOST=db
         restart: unless-stopped
+
+volumes:
+    data:
+        driver: local
+        driver_opts:
+            type: none
+            o: bind
+            # This directory specified where InvenTree data are stored "outside" the docker containers
+            device: c:/abcdef
\ No newline at end of file

From 2f1db486a038e19002ce3e5ed7dc650598226150 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 21:40:27 +1000
Subject: [PATCH 089/111] Do not use python virtual environment inside
 container

---
 docker/Dockerfile         | 16 ++++++----------
 docker/docker-compose.yml |  7 +++++--
 docker/start_server.sh    |  3 ---
 docker/start_worker.sh    |  3 ---
 4 files changed, 11 insertions(+), 18 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 1fd9329d69..eefc700cc7 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -22,7 +22,6 @@ ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
 ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
 ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
 ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
-ENV INVENTREE_VENV="${INVENTREE_HOME}/env"
 
 ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
 ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
@@ -64,21 +63,18 @@ RUN apk add --no-cache mariadb-connector-c mariadb-dev
 # Create required directories
 #RUN mkdir ${INVENTREE_DATA_DIR}}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
 
-# Setup Python virtual environment
-RUN python3 -m venv ${INVENTREE_VENV}
-
-# Install required PIP packages (into the virtual environment!)
-RUN source ${INVENTREE_VENV}/bin/activate && pip install --upgrade pip setuptools wheel
-RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U invoke
-RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
-RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U gunicorn
+# Install required python packages
+RUN pip install --upgrade pip setuptools wheel
+RUN pip install --no-cache-dir -U invoke
+RUN pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
+RUN pip install --no-cache-dir -U gunicorn
 
 # Clone source code
 RUN echo "Downloading InvenTree from ${INVENTREE_REPO}"
 RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
 
 # Install InvenTree packages
-RUN source ${INVENTREE_VENV}/bin/activate && pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
+RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
 
 # Copy gunicorn config file
 COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 08eb84d45b..839128738d 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -7,6 +7,7 @@ version: "3.8"
 # - Runs nginx as a reverse proxy
 
 services:
+    # Database service
     # Use PostgreSQL as the database backend
     # Note: this can be changed to a different backend,
     #       just make sure that you change the INVENTREE_DB_xxx vars below
@@ -23,7 +24,9 @@ services:
             - data:/var/lib/postgresql/data/
         restart: unless-stopped
 
-    server:
+    # InvenTree web server services
+    # Uses gunicorn as the web server
+    inventree:
         build:
             context: .
             args:
@@ -57,7 +60,7 @@ services:
         container_name: inventree_worker
         depends_on:
             - db
-            - server
+            - inventree
         volumes:
             - data:/home/inventree/data
         environment:
diff --git a/docker/start_server.sh b/docker/start_server.sh
index e9eaff1dfa..0436cd532f 100644
--- a/docker/start_server.sh
+++ b/docker/start_server.sh
@@ -24,9 +24,6 @@ else
     cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
 fi
 
-# Activate virtual environment
-source $INVENTREE_VENV/bin/activate
-
 echo "Starting InvenTree server..."
 
 # Wait for the database to be ready
diff --git a/docker/start_worker.sh b/docker/start_worker.sh
index a8a4815583..64c8d75ee4 100644
--- a/docker/start_worker.sh
+++ b/docker/start_worker.sh
@@ -2,9 +2,6 @@
 
 echo "Starting InvenTree worker..."
 
-# Activate virtual environment
-source ./env/bin/activate
-
 sleep 5
 
 # Wait for the database to be ready

From 91b6f98f95484bdce2159a105516937685d72ba2 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:08:36 +1000
Subject: [PATCH 090/111] Update directory structure to match docker config

---
 InvenTree/InvenTree/settings.py | 4 ++--
 InvenTree/config_template.yaml  | 6 +++---
 docker/Dockerfile               | 4 +++-
 3 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 9a6a4d59ae..8b5400e374 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -182,7 +182,7 @@ STATIC_URL = '/static/'
 STATIC_ROOT = os.path.abspath(
     get_setting(
         'INVENTREE_STATIC_ROOT',
-        CONFIG.get('static_root', os.path.join(BASE_DIR, 'static'))
+        CONFIG.get('static_root', '/home/inventree/static')
     )
 )
 
@@ -200,7 +200,7 @@ MEDIA_URL = '/media/'
 MEDIA_ROOT = os.path.abspath(
     get_setting(
         'INVENTREE_MEDIA_ROOT',
-        CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))
+        CONFIG.get('media_root', '/home/inventree/data/media')
     )
 )
 
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 9e18adb759..a64e6d42c0 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -95,9 +95,9 @@ cors:
   # - https://sub.example.com
 
 # MEDIA_ROOT is the local filesystem location for storing uploaded files
-# By default, it is stored under /home/inventree
+# By default, it is stored under /home/inventree/data/media
 # Use environment variable INVENTREE_MEDIA_ROOT
-media_root: '/home/inventree/media'
+media_root: '/home/inventree/data/media'
 
 # STATIC_ROOT is the local filesystem location for storing static files
 # By default, it is stored under /home/inventree
@@ -116,7 +116,7 @@ static_root: '/home/inventree/static'
 # Set the backup_dir parameter to store backup files in a specific location
 # If unspecified, the local user's temp directory will be used
 # Use environment variable INVENTREE_BACKUP_DIR
-backup_dir: '/home/inventree/backup/'
+backup_dir: '/home/inventree/data/backup/'
 
 # Permit custom authentication backends
 #authentication_backends:
diff --git a/docker/Dockerfile b/docker/Dockerfile
index eefc700cc7..5ab396ca44 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -19,7 +19,7 @@ ENV INVENTREE_LOG_LEVEL="INFO"
 ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
 ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
 ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
-ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
+ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
 ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
 ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
 
@@ -48,6 +48,8 @@ LABEL org.label-schema.schema-version="1.0" \
 RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
 WORKDIR ${INVENTREE_HOME}
 
+RUN mkdir ${INVENTREE_STATIC_ROOT}
+
 # Install required system packages
 RUN apk add --no-cache git make bash \
     gcc libgcc g++ libstdc++ \

From 5d9e2735598d2f350ab8d7ef50f094defc7bda46 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:25:07 +1000
Subject: [PATCH 091/111] Adds nxinx service

---
 docker/Dockerfile         |  5 ++++-
 docker/docker-compose.yml | 24 ++++++++++++++++++++++--
 docker/nginx/Dockerfile   | 14 ++++++++++++++
 docker/nginx/nginx.conf   | 21 +++++++++++++++++++++
 4 files changed, 61 insertions(+), 3 deletions(-)
 create mode 100644 docker/nginx/Dockerfile
 create mode 100644 docker/nginx/nginx.conf

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 5ab396ca44..3d89ef9349 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -88,5 +88,8 @@ COPY start_worker.sh ${INVENTREE_HOME}/start_worker.sh
 RUN chmod 755 ${INVENTREE_HOME}/start_server.sh
 RUN chmod 755 ${INVENTREE_HOME}/start_worker.sh
 
+# exec commands should be executed from the "src" directory
+WORKDIR ${INVENTREE_SRC_DIR}
+
 # Let us begin
-CMD ["bash", "./start_server.sh"]
+CMD ["bash", "../start_server.sh"]
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 839128738d..14f6108527 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -34,12 +34,13 @@ services:
                 branch: "django-q"
         image: inventree/inventree:latest
         container_name: inventree_server
-        ports:
-            - "8080:8080"
+        expose:
+            - 8080
         depends_on:
             - db
         volumes:
             - data:/home/inventree/data
+            - static:/home/inventree/static
         environment:
             - INVENTREE_DB_ENGINE=postgresql
             - INVENTREE_DB_NAME=inventree
@@ -49,6 +50,21 @@ services:
             - INVENTREE_DB_HOST=db
         restart: unless-stopped
 
+    # nginx acts as a reverse proxy
+    # static files are served by nginx
+    # web requests are redirected to gunicorn
+    nginx:
+        build:
+            context: nginx
+        container_name: inventree_proxy
+        depends_on:
+            - inventree
+        ports:
+            - 1337:80
+        volumes:
+            - static:/home/inventree/static
+
+    # background worker process handles long-running or periodic tasks
     worker:
         build:
             context: .
@@ -63,6 +79,7 @@ services:
             - inventree
         volumes:
             - data:/home/inventree/data
+            - static:/home/inventree/static
         environment:
             - INVENTREE_DB_ENGINE=postgresql
             - INVENTREE_DB_NAME=inventree
@@ -73,6 +90,9 @@ services:
         restart: unless-stopped
 
 volumes:
+    # Static files, shared between containers
+    static:
+    # Persistent data, stored externally
     data:
         driver: local
         driver_opts:
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
new file mode 100644
index 0000000000..e754597f02
--- /dev/null
+++ b/docker/nginx/Dockerfile
@@ -0,0 +1,14 @@
+FROM nginx:1.19.0-alpine
+
+# Create user account
+RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
+
+ENV HOME=/home/inventree
+WORKDIR $HOME
+
+# Create the "static" volume directory
+RUN mkdir $HOME/static
+
+RUN rm /etc/nginx/conf.d/default.conf
+COPY nginx.conf /etc/nginx/conf.d
+
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
new file mode 100644
index 0000000000..0f25f51674
--- /dev/null
+++ b/docker/nginx/nginx.conf
@@ -0,0 +1,21 @@
+upstream inventree {
+    server inventree:8080;
+}
+
+server {
+
+    listen 80;
+
+    location / {
+        proxy_pass http://inventree;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header Host $host;
+        proxy_redirect off;
+        client_max_body_size 100M;
+    }
+
+    location /static/ {
+        alias /home/inventree/static/;
+    }
+
+}
\ No newline at end of file

From 8f626d305e771e937bd40d1e431f8715669dae44 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:35:10 +1000
Subject: [PATCH 092/111] Fix location of entrypoint scripts

---
 docker/Dockerfile         | 12 +++++++-----
 docker/docker-compose.yml |  5 +++--
 docker/start_worker.sh    |  3 +--
 tasks.py                  |  8 ++++++++
 4 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 3d89ef9349..91870b6551 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -82,14 +82,16 @@ RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
 COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
 
 # Copy startup scripts
-COPY start_server.sh ${INVENTREE_HOME}/start_server.sh
-COPY start_worker.sh ${INVENTREE_HOME}/start_worker.sh
+COPY start_server.sh ${INVENTREE_SRC_DIR}/start_server.sh
+COPY start_worker.sh ${INVENTREE_SRC_DIR}/start_worker.sh
 
-RUN chmod 755 ${INVENTREE_HOME}/start_server.sh
-RUN chmod 755 ${INVENTREE_HOME}/start_worker.sh
+RUN chmod 755 ${INVENTREE_SRC_DIR}/start_server.sh
+RUN chmod 755 ${INVENTREE_SRC_DIR}/start_worker.sh
 
 # exec commands should be executed from the "src" directory
 WORKDIR ${INVENTREE_SRC_DIR}
 
+USER inventree
+
 # Let us begin
-CMD ["bash", "../start_server.sh"]
+CMD ["bash", "./start_server.sh"]
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 14f6108527..265f8b7c1c 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -2,9 +2,9 @@ version: "3.8"
 
 # Docker compose recipe for InvenTree
 # - Runs PostgreSQL as the database backend
-# - Serves web data using Gunicorn
-# - Runs the background worker process
+# - Runs Gunicorn as the web server
 # - Runs nginx as a reverse proxy
+# - Runs the background worker process
 
 services:
     # Database service
@@ -60,6 +60,7 @@ services:
         depends_on:
             - inventree
         ports:
+            # Change "1337" to the port where you want InvenTree web server to be available
             - 1337:80
         volumes:
             - static:/home/inventree/static
diff --git a/docker/start_worker.sh b/docker/start_worker.sh
index 64c8d75ee4..7d0921a7af 100644
--- a/docker/start_worker.sh
+++ b/docker/start_worker.sh
@@ -5,8 +5,7 @@ echo "Starting InvenTree worker..."
 sleep 5
 
 # Wait for the database to be ready
-cd src/InvenTree
-
+cd $INVENTREE_MNG_DIR
 python manage.py wait_for_db
 
 sleep 10
diff --git a/tasks.py b/tasks.py
index c6d7dd0173..5f9cff6c7d 100644
--- a/tasks.py
+++ b/tasks.py
@@ -117,6 +117,14 @@ def check(c):
 
     manage(c, "check")
 
+@task
+def wait(c):
+    """
+    Wait until the database connection is ready
+    """
+
+    manage(c, "wait_for_db")
+
 @task
 def migrate(c):
     """

From 5a168abbfee3db4791f00f943a34e12d3b5dbfeb Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:42:08 +1000
Subject: [PATCH 093/111] Separated docker file into separate directory

---
 docker/docker-compose.yml               | 11 +++++--
 docker/{ => inventree}/Dockerfile       |  0
 docker/{ => inventree}/gunicorn.conf.py |  0
 docker/{ => inventree}/start_server.sh  |  0
 docker/{ => inventree}/start_worker.sh  |  0
 docker/supervisord.conf                 | 43 -------------------------
 6 files changed, 9 insertions(+), 45 deletions(-)
 rename docker/{ => inventree}/Dockerfile (100%)
 rename docker/{ => inventree}/gunicorn.conf.py (100%)
 rename docker/{ => inventree}/start_server.sh (100%)
 rename docker/{ => inventree}/start_worker.sh (100%)
 delete mode 100644 docker/supervisord.conf

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 265f8b7c1c..e3a86b195f 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -6,6 +6,13 @@ version: "3.8"
 # - Runs nginx as a reverse proxy
 # - Runs the background worker process
 
+# ---------------------------------
+# IMPORTANT - READ BEFORE STARTING!
+# ---------------------------------
+# Before running, ensure that you change the "/path/to/data" directory,
+# specified in the "volumes" section at the end of this file.
+# This path determines where the InvenTree data will be stored!
+
 services:
     # Database service
     # Use PostgreSQL as the database backend
@@ -28,7 +35,7 @@ services:
     # Uses gunicorn as the web server
     inventree:
         build:
-            context: .
+            context: inventree
             args:
                 repository: "https://github.com/SchrodingersGat/InvenTree.git"
                 branch: "django-q"
@@ -68,7 +75,7 @@ services:
     # background worker process handles long-running or periodic tasks
     worker:
         build:
-            context: .
+            context: inventree
             args:
                 repository: "https://github.com/SchrodingersGat/InvenTree.git"
                 branch: "django-q"
diff --git a/docker/Dockerfile b/docker/inventree/Dockerfile
similarity index 100%
rename from docker/Dockerfile
rename to docker/inventree/Dockerfile
diff --git a/docker/gunicorn.conf.py b/docker/inventree/gunicorn.conf.py
similarity index 100%
rename from docker/gunicorn.conf.py
rename to docker/inventree/gunicorn.conf.py
diff --git a/docker/start_server.sh b/docker/inventree/start_server.sh
similarity index 100%
rename from docker/start_server.sh
rename to docker/inventree/start_server.sh
diff --git a/docker/start_worker.sh b/docker/inventree/start_worker.sh
similarity index 100%
rename from docker/start_worker.sh
rename to docker/inventree/start_worker.sh
diff --git a/docker/supervisord.conf b/docker/supervisord.conf
deleted file mode 100644
index b99c48edcf..0000000000
--- a/docker/supervisord.conf
+++ /dev/null
@@ -1,43 +0,0 @@
-; # Supervisor Config File (for docker build)
-;
-; This config file is specific to the InvenTree docker build!
-;
-; There are two separate processes which must be managed:
-;
-; ## Web Server
-; The InvenTree server must be launched and managed as a process
-; The recommended way to handle the web server is to use gunicorn
-;
-; ## Background Tasks
-; A background task manager processes long-running and periodic tasks
-; InvenTree uses django-q for this purpose
-
-[supervisord]
-user=inventree
-nodaemon=true
-
-[supervisorctl]
-serverurl=unix:///var/run/supervisor.sock
-
-[program:inventree-server]
-user=inventree
-directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
-command=%(ENV_INVENTREE_VENV)s/bin/gunicorn -c %(ENV_INVENTREE_HOME)s/gunicorn.conf.py InvenTree.wsgi -b 127.0.0.1:8080
-startsecs=10
-autostart=true
-autorestart=true
-startretries=3
-stdout_logfile=/dev/fd/1
-stdout_logfile_maxbytes=0
-redirect_stderr=true
-
-[program:inventree-cluster]
-user=inventree
-directory=%(ENV_INVENTREE_SRC_DIR)s/InvenTree
-command=%(ENV_INVENTREE_VENV)s/bin/python DJANGO_SETTINGS_FILE=InvenTree.settings manage.py qcluster
-startsecs=10
-autostart=true
-autorestart=true
-stdout_logfile=/dev/fd/1
-stdout_logfile_maxbytes=0
-redirect_stderr=true
\ No newline at end of file

From 3da5505b58027ed9caefd6c6b8f83d6ed2920d18 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:44:37 +1000
Subject: [PATCH 094/111] Fix build workflow

---
 .github/workflows/docker.yaml | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 34ca585c98..a5165861f2 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -6,11 +6,19 @@ on: ["push", "pull_request"]
 
 jobs:
   
-  docker:
+  inventree:
     runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@v2
-      - name: Build Docker Image
-        run: cd docker && docker build . --tag inventree:$(date +%s)
-    
\ No newline at end of file
+      - name: Build Server Image
+        run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
+    
+  nginx:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Build nginx Image
+        run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
+        
\ No newline at end of file

From 0e1b647e7bb627a6cdaa5dccad26c4d1b6a01d69 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:47:30 +1000
Subject: [PATCH 095/111] Remove mariadb test (uses the same backend as mysql!)

---
 .github/workflows/mariadb.yaml | 48 ----------------------------------
 README.md                      |  1 -
 2 files changed, 49 deletions(-)
 delete mode 100644 .github/workflows/mariadb.yaml

diff --git a/.github/workflows/mariadb.yaml b/.github/workflows/mariadb.yaml
deleted file mode 100644
index f976cfa088..0000000000
--- a/.github/workflows/mariadb.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: MariaDB
-
-on: ["push", "pull_request"]
-
-jobs:
-
-  test:
-    runs-on: ubuntu-latest
-
-    env:
-      # Database backend configuration
-      INVENTREE_DB_ENGINE: django.db.backends.mysql
-      INVENTREE_DB_NAME: inventree
-      INVENTREE_DB_USER: root
-      INVENTREE_DB_PASSWORD: password
-      INVENTREE_DB_HOST: '127.0.0.1'
-      INVENTREE_DB_PORT: 3306
-      INVENTREE_DEBUG: info
-      INVENTREE_MEDIA_ROOT: ./media
-      INVENTREE_STATIC_ROOT: ./static
-
-    services:
-      mariadb:
-        image: mariadb:latest
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: inventree
-          MYSQL_USER: inventree
-          MYSQL_PASSWORD: password
-          MYSQL_ROOT_PASSWORD: password
-        ports:
-          - 3306:3306
-
-    steps:
-      - name: Checkout Code
-        uses: actions/checkout@v2
-      - name: Setup Python
-        uses: actions/setup-python@v2
-        with:
-          python-version: 3.7
-      - name: Install Dependencies
-        run: |
-          sudo apt-get install mysql-server libmysqlclient-dev
-          pip3 install invoke
-          pip3 install mysqlclient
-          invoke install
-      - name: Run Tests
-        run: invoke test
diff --git a/README.md b/README.md
index ef0556cceb..2a622266d7 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
 ![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
 ![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
 ![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
-![MariaDB](https://github.com/inventree/inventree/actions/workflows/mariadb.yaml/badge.svg)
 ![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
 
 <img src="images/logo/inventree.png" alt="InvenTree" width="128"/>

From c9021fe9919854cad255280ace7cb0bab3922cb5 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 10 Apr 2021 22:48:23 +1000
Subject: [PATCH 096/111] Simplify docker build workflow

---
 .github/workflows/docker.yaml | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index a5165861f2..c9f8a69654 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -6,19 +6,13 @@ on: ["push", "pull_request"]
 
 jobs:
   
-  inventree:
+  docker:
     runs-on: ubuntu-latest
 
     steps:
       - uses: actions/checkout@v2
       - name: Build Server Image
         run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
-    
-  nginx:
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v2
       - name: Build nginx Image
         run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
         
\ No newline at end of file

From 2e8d3b64244ae9b97537a233b081431edb44552c Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 13:22:16 +1000
Subject: [PATCH 097/111] Fix for tasks.py (??)

---
 deploy/{inventree.conf => supervisord.conf} | 6 ++++++
 tasks.py                                    | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)
 rename deploy/{inventree.conf => supervisord.conf} (86%)

diff --git a/deploy/inventree.conf b/deploy/supervisord.conf
similarity index 86%
rename from deploy/inventree.conf
rename to deploy/supervisord.conf
index 782b6ae2f6..d4f6ce4419 100644
--- a/deploy/inventree.conf
+++ b/deploy/supervisord.conf
@@ -13,7 +13,12 @@
 [supervisord]
 ; Change this path if log files are stored elsewhere
 logfile=/home/inventree/log/supervisor.log
+user=inventree
 
+[supervisorctl]
+serverurl=unix:///var/run/supervisor.sock
+
+; InvenTree Web Server Process
 [program:inventree-server]
 user=inventree
 directory=/home/inventree/src/InvenTree
@@ -26,6 +31,7 @@ startretries=3
 stderr_logfile=/home/inventree/log/server.err.log
 stdout_logfile=/home/inventree/log/server.out.log
 
+; InvenTree Background Worker Process
 [program:inventree-cluster]
 user=inventree
 directory=/home/inventree/src/InvenTree
diff --git a/tasks.py b/tasks.py
index 5f9cff6c7d..ebbe199865 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-from invoke import task
+from invoke import ctask as task
 from shutil import copyfile
 
 import os

From 78bcbe271a4be1afb7fa646d1bd75bd57eb61cf1 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 13:45:56 +1000
Subject: [PATCH 098/111] Update supervisor conf file

---
 deploy/supervisord.conf | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/deploy/supervisord.conf b/deploy/supervisord.conf
index d4f6ce4419..88a5af3e5c 100644
--- a/deploy/supervisord.conf
+++ b/deploy/supervisord.conf
@@ -16,7 +16,9 @@ logfile=/home/inventree/log/supervisor.log
 user=inventree
 
 [supervisorctl]
-serverurl=unix:///var/run/supervisor.sock
+
+[inet_http_server]
+port = 127.0.0.1:9001
 
 ; InvenTree Web Server Process
 [program:inventree-server]

From f6f3815f31149ce9a51c2d7533f58e8c1f7182e0 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 13:58:59 +1000
Subject: [PATCH 099/111] Include worker status in main API call

---
 InvenTree/InvenTree/api.py     | 2 ++
 InvenTree/InvenTree/context.py | 2 +-
 InvenTree/InvenTree/status.py  | 6 +++---
 docker/docker-compose.yml      | 3 ++-
 4 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index c7386e8fbc..a006050694 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -19,6 +19,7 @@ from rest_framework.views import APIView
 
 from .views import AjaxView
 from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
+from .status import is_worker_running
 
 from plugins import plugins as inventree_plugins
 
@@ -44,6 +45,7 @@ class InfoView(AjaxView):
             'version': inventreeVersion(),
             'instance': inventreeInstanceName(),
             'apiVersion': inventreeApiVersion(),
+            'worker_running': is_worker_running(),
         }
 
         return JsonResponse(data)
diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py
index 43e8b904b9..3e9af2f751 100644
--- a/InvenTree/InvenTree/context.py
+++ b/InvenTree/InvenTree/context.py
@@ -31,7 +31,7 @@ def health_status(request):
     request._inventree_health_status = True
 
     status = {
-        'django_q_running': InvenTree.status.is_q_cluster_running(),
+        'django_q_running': InvenTree.status.is_worker_running(),
     }
 
     all_healthy = True
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index 42160927b0..88acc69a7a 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -14,9 +14,9 @@ from django_q.monitor import Stat
 logger = logging.getLogger("inventree")
 
 
-def is_q_cluster_running(**kwargs):
+def is_worker_running(**kwargs):
     """
-    Return True if at least one cluster worker is running
+    Return True if the background worker process is oprational
     """
 
     clusters = Stat.get_all()
@@ -52,7 +52,7 @@ def check_system_health(**kwargs):
 
     result = True
 
-    if not is_q_cluster_running(**kwargs):
+    if not is_worker_running(**kwargs):
         result = False
         logger.warning(_("Background worker check failed"))
 
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index e3a86b195f..8ef272ece2 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -107,4 +107,5 @@ volumes:
             type: none
             o: bind
             # This directory specified where InvenTree data are stored "outside" the docker containers
-            device: c:/abcdef
\ No newline at end of file
+            # Change this path to a local system path where you want InvenTree data stored
+            device: /path/to/data

From 44fe5721e0e4fee4eab9ef1b0560ece11236d8c2 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 14:05:55 +1000
Subject: [PATCH 100/111] Disgusting hack for tasks.py

---
 tasks.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/tasks.py b/tasks.py
index ebbe199865..895f3183ce 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,11 +1,14 @@
 # -*- coding: utf-8 -*-
 
-from invoke import ctask as task
 from shutil import copyfile
-
 import os
 import sys
 
+try:
+    from invoke import ctask as task
+except:
+    from invoke import task
+
 
 def apps():
     """

From 5f9236d280faeef96c474a57c3b8930dd2748a93 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 14:46:40 +1000
Subject: [PATCH 101/111] Updates to docker files

---
 docker/docker-compose.yml   | 3 ++-
 docker/inventree/Dockerfile | 2 --
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 8ef272ece2..c802444c79 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -64,6 +64,7 @@ services:
         build:
             context: nginx
         container_name: inventree_proxy
+        image: inventree/nginx:latest
         depends_on:
             - inventree
         ports:
@@ -80,7 +81,7 @@ services:
                 repository: "https://github.com/SchrodingersGat/InvenTree.git"
                 branch: "django-q"
         entrypoint: ./start_worker.sh
-        image: inventree/worker:latest
+        image: inventree/inventree:latest
         container_name: inventree_worker
         depends_on:
             - db
diff --git a/docker/inventree/Dockerfile b/docker/inventree/Dockerfile
index 91870b6551..fcba19e964 100644
--- a/docker/inventree/Dockerfile
+++ b/docker/inventree/Dockerfile
@@ -91,7 +91,5 @@ RUN chmod 755 ${INVENTREE_SRC_DIR}/start_worker.sh
 # exec commands should be executed from the "src" directory
 WORKDIR ${INVENTREE_SRC_DIR}
 
-USER inventree
-
 # Let us begin
 CMD ["bash", "./start_server.sh"]

From 34e95ab70cf533a12892e5f0497f7965decfc5f8 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 14:49:41 +1000
Subject: [PATCH 102/111] Update version.py

---
 InvenTree/InvenTree/version.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index d7bcd4f7ed..1a28900905 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -7,7 +7,7 @@ import django
 
 import common.models
 
-INVENTREE_SW_VERSION = "0.1.8 pre"
+INVENTREE_SW_VERSION = "0.2.0 pre"
 
 # Increment this number whenever there is a significant change to the API that any clients need to know about
 INVENTREE_API_VERSION = 2

From b490c5d03549fb11213ad5bfd0bf269b64b370dd Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:08:13 +1000
Subject: [PATCH 103/111] Add new docker workflow for publising docker images
 on release

---
 .../{docker.yaml => docker_build.yaml}        |  0
 .github/workflows/docker_publish.yaml         | 38 +++++++++++++++++++
 2 files changed, 38 insertions(+)
 rename .github/workflows/{docker.yaml => docker_build.yaml} (100%)
 create mode 100644 .github/workflows/docker_publish.yaml

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker_build.yaml
similarity index 100%
rename from .github/workflows/docker.yaml
rename to .github/workflows/docker_build.yaml
diff --git a/.github/workflows/docker_publish.yaml b/.github/workflows/docker_publish.yaml
new file mode 100644
index 0000000000..3b9ad13a98
--- /dev/null
+++ b/.github/workflows/docker_publish.yaml
@@ -0,0 +1,38 @@
+# Publish docker images to dockerhub
+
+name: Docker Publish
+
+on:
+  release:
+    types: [published]
+
+jobs:
+  server_image:
+    name: Push InvenTree web server image to dockerhub
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repo
+        uses: actions/checkout@v2
+      - name: Push to Docker Hub
+        uses: docker/build-push-action@v1
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_PASSWORD }}
+          repository: inventree/inventree
+          tag_with_ref: true
+          context: docker/inventree
+
+  nginx_image:
+    name: Push InvenTree nginx image to dockerhub
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repo
+        uses: actions/checkout@v2
+      - name: Push to Docker Hub
+        uses: docker/build-push-action@v1
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_PASSWORD }}
+          repository: inventree/nginx
+          tag_with_ref: true
+          context: docker/nginx

From 8f07efa4e3de7657e90f599959ad64b231f65e87 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:15:11 +1000
Subject: [PATCH 104/111] Add dockerhub badge

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index 2a622266d7..28169a7f90 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/inventree/inventree)
 [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
 ![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
 ![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)

From c2f85b0447d61e82da79e545c9e8a3b81b5c51f7 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:25:32 +1000
Subject: [PATCH 105/111] docker-compose tweaks

---
 docker/docker-compose.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index a01f152b07..90b5fa2668 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -19,8 +19,8 @@ services:
     # Note: this can be changed to a different backend,
     #       just make sure that you change the INVENTREE_DB_xxx vars below
     db:
+        container_name: db
         image: postgres
-        container_name: inventree_db
         ports:
             - 5432/tcp
         environment:
@@ -34,8 +34,8 @@ services:
     # InvenTree web server services
     # Uses gunicorn as the web server
     inventree:
+        container_name: server
         image: inventree/inventree:latest
-        container_name: inventree_server
         expose:
             - 8080
         depends_on:
@@ -56,7 +56,7 @@ services:
     # static files are served by nginx
     # web requests are redirected to gunicorn
     nginx:
-        container_name: inventree_proxy
+        container_name: nginx
         image: inventree/nginx:latest
         depends_on:
             - inventree
@@ -68,9 +68,9 @@ services:
 
     # background worker process handles long-running or periodic tasks
     worker:
-        entrypoint: ./start_worker.sh
+        container_name: worker
         image: inventree/inventree:latest
-        container_name: inventree_worker
+        entrypoint: ./start_worker.sh
         depends_on:
             - db
             - inventree

From 4cf0339393baf0c330ef120efe235b9be84b216f Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:38:52 +1000
Subject: [PATCH 106/111] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 28169a7f90..0a8cfa1749 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 [![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/inventree/inventree)
 [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
 ![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
-![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
+![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker_build.yaml/badge.svg)
 ![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
 ![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
 ![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)

From 7c9ad3f4062234763cb8c5fb043b36e24c64f44f Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:39:16 +1000
Subject: [PATCH 107/111] Update version.py

---
 InvenTree/InvenTree/version.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 7bfb752b63..177db4d93c 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -8,7 +8,7 @@ import re
 
 import common.models
 
-INVENTREE_SW_VERSION = "0.2.0 pre"
+INVENTREE_SW_VERSION = "0.2.0"
 
 # Increment this number whenever there is a significant change to the API that any clients need to know about
 INVENTREE_API_VERSION = 2

From effd5472600ca3876ed1dbbf1eff668f8e226bc1 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:39:53 +1000
Subject: [PATCH 108/111] Update version.py

---
 InvenTree/InvenTree/version.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 177db4d93c..b79323e1e7 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -8,7 +8,7 @@ import re
 
 import common.models
 
-INVENTREE_SW_VERSION = "0.2.0"
+INVENTREE_SW_VERSION = "0.2.1 pre"
 
 # Increment this number whenever there is a significant change to the API that any clients need to know about
 INVENTREE_API_VERSION = 2

From 029808a986abb161d7b38d2cccb751c1d6d5e32c Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 15:45:17 +1000
Subject: [PATCH 109/111] Fix workflow for publishing docker files

---
 .github/workflows/docker_publish.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/docker_publish.yaml b/.github/workflows/docker_publish.yaml
index 3b9ad13a98..6870754ad3 100644
--- a/.github/workflows/docker_publish.yaml
+++ b/.github/workflows/docker_publish.yaml
@@ -20,7 +20,7 @@ jobs:
           password: ${{ secrets.DOCKER_PASSWORD }}
           repository: inventree/inventree
           tag_with_ref: true
-          context: docker/inventree
+          dockerfile: docker/inventree/Dockerfile
 
   nginx_image:
     name: Push InvenTree nginx image to dockerhub
@@ -35,4 +35,4 @@ jobs:
           password: ${{ secrets.DOCKER_PASSWORD }}
           repository: inventree/nginx
           tag_with_ref: true
-          context: docker/nginx
+          dockerfile: docker/nginx/Dockerfile

From 85c9bc1b81a0dd749d0cf36dbad7238147410543 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 18:56:35 +1000
Subject: [PATCH 110/111] Adds detail endpoint for PartParameter model

---
 InvenTree/part/api.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index a245f9d67c..54222b7e67 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -735,6 +735,15 @@ class PartParameterList(generics.ListCreateAPIView):
     ]
 
 
+class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
+    """
+    API endpoint for detail view of a single PartParameter object
+    """
+
+    queryset = PartParameter.objects.all()
+    serializer_class = part_serializers.PartParameterSerializer
+
+
 class BomList(generics.ListCreateAPIView):
     """ API endpoint for accessing a list of BomItem objects.
 
@@ -942,6 +951,8 @@ part_api_urls = [
     # Base URL for PartParameter API endpoints
     url(r'^parameter/', include([
         url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
+
+        url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
         url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
     ])),
 

From 8a06eaa40d97293c56d1f1bcf45f129dd66d95b9 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sun, 11 Apr 2021 19:28:39 +1000
Subject: [PATCH 111/111] Unit testing

---
 InvenTree/part/fixtures/params.yaml |  21 ++++++
 InvenTree/part/test_api.py          | 103 ++++++++++++++++++++++++++++
 2 files changed, 124 insertions(+)

diff --git a/InvenTree/part/fixtures/params.yaml b/InvenTree/part/fixtures/params.yaml
index e65c7335cc..3056be473b 100644
--- a/InvenTree/part/fixtures/params.yaml
+++ b/InvenTree/part/fixtures/params.yaml
@@ -33,6 +33,27 @@
     template: 1
     data: 12
 
+- model: part.PartParameter
+  pk: 3
+  fields:
+    part: 3
+    template: 1
+    data: 12
+
+- model: part.PartParameter
+  pk: 4
+  fields:
+    part: 3
+    template: 2
+    data: 12
+
+- model: part.PartParameter
+  pk: 5
+  fields:
+    part: 3
+    template: 3
+    data: 12
+
 # Add some template parameters to categories (requires category.yaml)
 - model: part.PartCategoryParameterTemplate
   pk: 1
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index faadf26c15..ed88e1dd55 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -325,3 +325,106 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
 
         self.assertEqual(data['in_stock'], 1100)
         self.assertEqual(data['stock_item_count'], 105)
+
+
+class PartParameterTest(InvenTreeAPITestCase):
+    """
+    Tests for the ParParameter API
+    """
+
+    superuser = True
+
+    fixtures = [
+        'category',
+        'part',
+        'location',
+        'params',
+    ]
+
+    def setUp(self):
+
+        super().setUp()
+
+    def test_list_params(self):
+        """
+        Test for listing part parameters
+        """
+
+        url = reverse('api-part-param-list')
+
+        response = self.client.get(url, format='json')
+
+        self.assertEqual(len(response.data), 5)
+
+        # Filter by part
+        response = self.client.get(
+            url,
+            {
+                'part': 3,
+            },
+            format='json'
+        )
+
+        self.assertEqual(len(response.data), 3)
+
+        # Filter by template
+        response = self.client.get(
+            url,
+            {
+                'template': 1,
+            },
+            format='json',
+        )
+
+        self.assertEqual(len(response.data), 3)
+
+    def test_create_param(self):
+        """
+        Test that we can create a param via the API
+        """
+
+        url = reverse('api-part-param-list')
+
+        response = self.client.post(
+            url,
+            {
+                'part': '2',
+                'template': '3',
+                'data': 70
+            }
+        )
+
+        self.assertEqual(response.status_code, 201)
+
+        response = self.client.get(url, format='json')
+
+        self.assertEqual(len(response.data), 6)
+
+    def test_param_detail(self):
+        """
+        Tests for the PartParameter detail endpoint
+        """
+
+        url = reverse('api-part-param-detail', kwargs={'pk': 5})
+
+        response = self.client.get(url)
+
+        self.assertEqual(response.status_code, 200)
+
+        data = response.data
+
+        self.assertEqual(data['pk'], 5)
+        self.assertEqual(data['part'], 3)
+        self.assertEqual(data['data'], '12')
+
+        # PATCH data back in
+        response = self.client.patch(url, {'data': '15'}, format='json')
+
+        self.assertEqual(response.status_code, 200)
+        
+        # Check that the data changed!
+        response = self.client.get(url, format='json')
+
+        data = response.data
+
+        self.assertEqual(data['data'], '15')