We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Heartbeat has a datetime warning
From the worker logs:
`inventree-worker    | 00:51:15 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
inventree-worker    | 00:51:15 [Q] INFO Process-1:3 processing [may-cat-eleven-beer]
inventree-worker    | Could not perform heartbeat task - App registry not ready
inventree-worker    | /usr/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1416: RuntimeWarning: DateTimeField Task.started received a naive datetime (2021-08-10 00:21:15.816018) while time zone support is active.
inventree-worker    |   warnings.warn("DateTimeField %s received a naive datetime (%s)"
`

The heartbeat task references a non timezoned date which django complains about as USE_TZ is True in the settings.py


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `InvenTree/InvenTree/tasks.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3 
4 import re
5 import json
6 import requests
7 import logging
8 
9 from datetime import datetime, timedelta
10 
11 from django.core.exceptions import AppRegistryNotReady
12 from django.db.utils import OperationalError, ProgrammingError
13 
14 
15 logger = logging.getLogger("inventree")
16 
17 
18 def schedule_task(taskname, **kwargs):
19     """
20     Create a scheduled task.
21     If the task has already been scheduled, ignore!
22     """
23 
24     # If unspecified, repeat indefinitely
25     repeats = kwargs.pop('repeats', -1)
26     kwargs['repeats'] = repeats
27 
28     try:
29         from django_q.models import Schedule
30     except (AppRegistryNotReady):
31         logger.info("Could not start background tasks - App registry not ready")
32         return
33 
34     try:
35         # If this task is already scheduled, don't schedule it again
36         # Instead, update the scheduling parameters
37         if Schedule.objects.filter(func=taskname).exists():
38             logger.info(f"Scheduled task '{taskname}' already exists - updating!")
39 
40             Schedule.objects.filter(func=taskname).update(**kwargs)
41         else:
42             logger.info(f"Creating scheduled task '{taskname}'")
43 
44             Schedule.objects.create(
45                 name=taskname,
46                 func=taskname,
47                 **kwargs
48             )
49     except (OperationalError, ProgrammingError):
50         # Required if the DB is not ready yet
51         pass
52 
53 
54 def offload_task(taskname, force_sync=False, *args, **kwargs):
55     """
56         Create an AsyncTask if workers are running.
57         This is different to a 'scheduled' task,
58         in that it only runs once!
59 
60         If workers are not running or force_sync flag
61         is set then the task is ran synchronously.
62     """
63 
64     try:
65         from django_q.tasks import AsyncTask
66     except (AppRegistryNotReady):
67         logger.warning("Could not offload task - app registry not ready")
68         return
69     import importlib
70     from InvenTree.status import is_worker_running
71 
72     if is_worker_running() and not force_sync:
73         # Running as asynchronous task
74         try:
75             task = AsyncTask(taskname, *args, **kwargs)
76             task.run()
77         except ImportError:
78             logger.warning(f"WARNING: '{taskname}' not started - Function not found")
79     else:
80         # Split path
81         try:
82             app, mod, func = taskname.split('.')
83             app_mod = app + '.' + mod
84         except ValueError:
85             logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
86             return
87 
88         # Import module from app
89         try:
90             _mod = importlib.import_module(app_mod)
91         except ModuleNotFoundError:
92             logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
93             return
94 
95         # Retrieve function
96         try:
97             _func = getattr(_mod, func)
98         except AttributeError:
99             # getattr does not work for local import
100             _func = None
101 
102         try:
103             if not _func:
104                 _func = eval(func)
105         except NameError:
106             logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
107             return
108         
109         # Workers are not running: run it as synchronous task
110         _func()
111 
112 
113 def heartbeat():
114     """
115     Simple task which runs at 5 minute intervals,
116     so we can determine that the background worker
117     is actually running.
118 
119     (There is probably a less "hacky" way of achieving this)?
120     """
121 
122     try:
123         from django_q.models import Success
124         logger.info("Could not perform heartbeat task - App registry not ready")
125     except AppRegistryNotReady:
126         return
127 
128     threshold = datetime.now() - timedelta(minutes=30)
129 
130     # Delete heartbeat results more than half an hour old,
131     # otherwise they just create extra noise
132     heartbeats = Success.objects.filter(
133         func='InvenTree.tasks.heartbeat',
134         started__lte=threshold
135     )
136 
137     heartbeats.delete()
138 
139 
140 def delete_successful_tasks():
141     """
142     Delete successful task logs
143     which are more than a month old.
144     """
145 
146     try:
147         from django_q.models import Success
148     except AppRegistryNotReady:
149         logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
150         return
151 
152     threshold = datetime.now() - timedelta(days=30)
153 
154     results = Success.objects.filter(
155         started__lte=threshold
156     )
157 
158     results.delete()
159 
160 
161 def check_for_updates():
162     """
163     Check if there is an update for InvenTree
164     """
165 
166     try:
167         import common.models
168     except AppRegistryNotReady:
169         # Apps not yet loaded!
170         logger.info("Could not perform 'check_for_updates' - App registry not ready")
171         return
172 
173     response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
174 
175     if not response.status_code == 200:
176         raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
177 
178     data = json.loads(response.text)
179 
180     tag = data.get('tag_name', None)
181 
182     if not tag:
183         raise ValueError("'tag_name' missing from GitHub response")
184 
185     match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
186 
187     if not len(match.groups()) == 3:
188         logger.warning(f"Version '{tag}' did not match expected pattern")
189         return
190 
191     latest_version = [int(x) for x in match.groups()]
192 
193     if not len(latest_version) == 3:
194         raise ValueError(f"Version '{tag}' is not correct format")
195 
196     logger.info(f"Latest InvenTree version: '{tag}'")
197 
198     # Save the version to the database
199     common.models.InvenTreeSetting.set_setting(
200         'INVENTREE_LATEST_VERSION',
201         tag,
202         None
203     )
204 
205 
206 def update_exchange_rates():
207     """
208     Update currency exchange rates
209     """
210 
211     try:
212         from InvenTree.exchange import InvenTreeExchange
213         from djmoney.contrib.exchange.models import ExchangeBackend, Rate
214         from common.settings import currency_code_default, currency_codes
215     except AppRegistryNotReady:
216         # Apps not yet loaded!
217         logger.info("Could not perform 'update_exchange_rates' - App registry not ready")
218         return
219     except:
220         # Other error?
221         return
222 
223     # Test to see if the database is ready yet
224     try:
225         backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
226     except ExchangeBackend.DoesNotExist:
227         pass
228     except:
229         # Some other error
230         print("Database not ready")
231         return
232 
233     backend = InvenTreeExchange()
234     print(f"Updating exchange rates from {backend.url}")
235 
236     base = currency_code_default()
237 
238     print(f"Using base currency '{base}'")
239 
240     backend.update_rates(base_currency=base)
241 
242     # Remove any exchange rates which are not in the provided currencies
243     Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete()
244 
245 
246 def send_email(subject, body, recipients, from_email=None):
247     """
248     Send an email with the specified subject and body,
249     to the specified recipients list.
250     """
251 
252     if type(recipients) == str:
253         recipients = [recipients]
254 
255     offload_task(
256         'django.core.mail.send_mail',
257         subject, body,
258         from_email,
259         recipients,
260     )
261
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -6,7 +6,8 @@
 import requests
 import logging
 
-from datetime import datetime, timedelta
+from datetime import timedelta
+from django.utils import timezone
 
 from django.core.exceptions import AppRegistryNotReady
 from django.db.utils import OperationalError, ProgrammingError
@@ -125,7 +126,7 @@
     except AppRegistryNotReady:
         return
 
-    threshold = datetime.now() - timedelta(minutes=30)
+    threshold = timezone.now() - timedelta(minutes=30)
 
     # Delete heartbeat results more than half an hour old,
     # otherwise they just create extra noise
@@ -149,7 +150,7 @@
         logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
         return
 
-    threshold = datetime.now() - timedelta(days=30)
+    threshold = timezone.now() - timedelta(days=30)
 
     results = Success.objects.filter(
         started__lte=threshold

{"golden_diff": "diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py\n--- a/InvenTree/InvenTree/tasks.py\n+++ b/InvenTree/InvenTree/tasks.py\n@@ -6,7 +6,8 @@\n import requests\n import logging\n \n-from datetime import datetime, timedelta\n+from datetime import timedelta\n+from django.utils import timezone\n \n from django.core.exceptions import AppRegistryNotReady\n from django.db.utils import OperationalError, ProgrammingError\n@@ -125,7 +126,7 @@\n     except AppRegistryNotReady:\n         return\n \n-    threshold = datetime.now() - timedelta(minutes=30)\n+    threshold = timezone.now() - timedelta(minutes=30)\n \n     # Delete heartbeat results more than half an hour old,\n     # otherwise they just create extra noise\n@@ -149,7 +150,7 @@\n         logger.info(\"Could not perform 'delete_successful_tasks' - App registry not ready\")\n         return\n \n-    threshold = datetime.now() - timedelta(days=30)\n+    threshold = timezone.now() - timedelta(days=30)\n \n     results = Success.objects.filter(\n         started__lte=threshold\n", "issue": "Heartbeat has a datetime warning\nFrom the worker logs:\r\n`inventree-worker    | 00:51:15 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]\r\ninventree-worker    | 00:51:15 [Q] INFO Process-1:3 processing [may-cat-eleven-beer]\r\ninventree-worker    | Could not perform heartbeat task - App registry not ready\r\ninventree-worker    | /usr/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1416: RuntimeWarning: DateTimeField Task.started received a naive datetime (2021-08-10 00:21:15.816018) while time zone support is active.\r\ninventree-worker    |   warnings.warn(\"DateTimeField %s received a naive datetime (%s)\"\r\n`\r\n\r\nThe heartbeat task references a non timezoned date which django complains about as USE_TZ is True in the settings.py\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nimport re\nimport json\nimport requests\nimport logging\n\nfrom datetime import datetime, timedelta\n\nfrom django.core.exceptions import AppRegistryNotReady\nfrom django.db.utils import OperationalError, ProgrammingError\n\n\nlogger = logging.getLogger(\"inventree\")\n\n\ndef schedule_task(taskname, **kwargs):\n    \"\"\"\n    Create a scheduled task.\n    If the task has already been scheduled, ignore!\n    \"\"\"\n\n    # If unspecified, repeat indefinitely\n    repeats = kwargs.pop('repeats', -1)\n    kwargs['repeats'] = repeats\n\n    try:\n        from django_q.models import Schedule\n    except (AppRegistryNotReady):\n        logger.info(\"Could not start background tasks - App registry not ready\")\n        return\n\n    try:\n        # If this task is already scheduled, don't schedule it again\n        # Instead, update the scheduling parameters\n        if Schedule.objects.filter(func=taskname).exists():\n            logger.info(f\"Scheduled task '{taskname}' already exists - updating!\")\n\n            Schedule.objects.filter(func=taskname).update(**kwargs)\n        else:\n            logger.info(f\"Creating scheduled task '{taskname}'\")\n\n            Schedule.objects.create(\n                name=taskname,\n                func=taskname,\n                **kwargs\n            )\n    except (OperationalError, ProgrammingError):\n        # Required if the DB is not ready yet\n        pass\n\n\ndef offload_task(taskname, force_sync=False, *args, **kwargs):\n    \"\"\"\n        Create an AsyncTask if workers are running.\n        This is different to a 'scheduled' task,\n        in that it only runs once!\n\n        If workers are not running or force_sync flag\n        is set then the task is ran synchronously.\n    \"\"\"\n\n    try:\n        from django_q.tasks import AsyncTask\n    except (AppRegistryNotReady):\n        logger.warning(\"Could not offload task - app registry not ready\")\n        return\n    import importlib\n    from InvenTree.status import is_worker_running\n\n    if is_worker_running() and not force_sync:\n        # Running as asynchronous task\n        try:\n            task = AsyncTask(taskname, *args, **kwargs)\n            task.run()\n        except ImportError:\n            logger.warning(f\"WARNING: '{taskname}' not started - Function not found\")\n    else:\n        # Split path\n        try:\n            app, mod, func = taskname.split('.')\n            app_mod = app + '.' + mod\n        except ValueError:\n            logger.warning(f\"WARNING: '{taskname}' not started - Malformed function path\")\n            return\n\n        # Import module from app\n        try:\n            _mod = importlib.import_module(app_mod)\n        except ModuleNotFoundError:\n            logger.warning(f\"WARNING: '{taskname}' not started - No module named '{app_mod}'\")\n            return\n\n        # Retrieve function\n        try:\n            _func = getattr(_mod, func)\n        except AttributeError:\n            # getattr does not work for local import\n            _func = None\n\n        try:\n            if not _func:\n                _func = eval(func)\n        except NameError:\n            logger.warning(f\"WARNING: '{taskname}' not started - No function named '{func}'\")\n            return\n        \n        # Workers are not running: run it as synchronous task\n        _func()\n\n\ndef heartbeat():\n    \"\"\"\n    Simple task which runs at 5 minute intervals,\n    so we can determine that the background worker\n    is actually running.\n\n    (There is probably a less \"hacky\" way of achieving this)?\n    \"\"\"\n\n    try:\n        from django_q.models import Success\n        logger.info(\"Could not perform heartbeat task - App registry not ready\")\n    except AppRegistryNotReady:\n        return\n\n    threshold = datetime.now() - timedelta(minutes=30)\n\n    # Delete heartbeat results more than half an hour old,\n    # otherwise they just create extra noise\n    heartbeats = Success.objects.filter(\n        func='InvenTree.tasks.heartbeat',\n        started__lte=threshold\n    )\n\n    heartbeats.delete()\n\n\ndef delete_successful_tasks():\n    \"\"\"\n    Delete successful task logs\n    which are more than a month old.\n    \"\"\"\n\n    try:\n        from django_q.models import Success\n    except AppRegistryNotReady:\n        logger.info(\"Could not perform 'delete_successful_tasks' - App registry not ready\")\n        return\n\n    threshold = datetime.now() - timedelta(days=30)\n\n    results = Success.objects.filter(\n        started__lte=threshold\n    )\n\n    results.delete()\n\n\ndef check_for_updates():\n    \"\"\"\n    Check if there is an update for InvenTree\n    \"\"\"\n\n    try:\n        import common.models\n    except AppRegistryNotReady:\n        # Apps not yet loaded!\n        logger.info(\"Could not perform 'check_for_updates' - App registry not ready\")\n        return\n\n    response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')\n\n    if not response.status_code == 200:\n        raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')\n\n    data = json.loads(response.text)\n\n    tag = data.get('tag_name', None)\n\n    if not tag:\n        raise ValueError(\"'tag_name' missing from GitHub response\")\n\n    match = re.match(r\"^.*(\\d+)\\.(\\d+)\\.(\\d+).*$\", tag)\n\n    if not len(match.groups()) == 3:\n        logger.warning(f\"Version '{tag}' did not match expected pattern\")\n        return\n\n    latest_version = [int(x) for x in match.groups()]\n\n    if not len(latest_version) == 3:\n        raise ValueError(f\"Version '{tag}' is not correct format\")\n\n    logger.info(f\"Latest InvenTree version: '{tag}'\")\n\n    # Save the version to the database\n    common.models.InvenTreeSetting.set_setting(\n        'INVENTREE_LATEST_VERSION',\n        tag,\n        None\n    )\n\n\ndef update_exchange_rates():\n    \"\"\"\n    Update currency exchange rates\n    \"\"\"\n\n    try:\n        from InvenTree.exchange import InvenTreeExchange\n        from djmoney.contrib.exchange.models import ExchangeBackend, Rate\n        from common.settings import currency_code_default, currency_codes\n    except AppRegistryNotReady:\n        # Apps not yet loaded!\n        logger.info(\"Could not perform 'update_exchange_rates' - App registry not ready\")\n        return\n    except:\n        # Other error?\n        return\n\n    # Test to see if the database is ready yet\n    try:\n        backend = ExchangeBackend.objects.get(name='InvenTreeExchange')\n    except ExchangeBackend.DoesNotExist:\n        pass\n    except:\n        # Some other error\n        print(\"Database not ready\")\n        return\n\n    backend = InvenTreeExchange()\n    print(f\"Updating exchange rates from {backend.url}\")\n\n    base = currency_code_default()\n\n    print(f\"Using base currency '{base}'\")\n\n    backend.update_rates(base_currency=base)\n\n    # Remove any exchange rates which are not in the provided currencies\n    Rate.objects.filter(backend=\"InvenTreeExchange\").exclude(currency__in=currency_codes()).delete()\n\n\ndef send_email(subject, body, recipients, from_email=None):\n    \"\"\"\n    Send an email with the specified subject and body,\n    to the specified recipients list.\n    \"\"\"\n\n    if type(recipients) == str:\n        recipients = [recipients]\n\n    offload_task(\n        'django.core.mail.send_mail',\n        subject, body,\n        from_email,\n        recipients,\n    )\n", "path": "InvenTree/InvenTree/tasks.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nimport re\nimport json\nimport requests\nimport logging\n\nfrom datetime import timedelta\nfrom django.utils import timezone\n\nfrom django.core.exceptions import AppRegistryNotReady\nfrom django.db.utils import OperationalError, ProgrammingError\n\n\nlogger = logging.getLogger(\"inventree\")\n\n\ndef schedule_task(taskname, **kwargs):\n    \"\"\"\n    Create a scheduled task.\n    If the task has already been scheduled, ignore!\n    \"\"\"\n\n    # If unspecified, repeat indefinitely\n    repeats = kwargs.pop('repeats', -1)\n    kwargs['repeats'] = repeats\n\n    try:\n        from django_q.models import Schedule\n    except (AppRegistryNotReady):\n        logger.info(\"Could not start background tasks - App registry not ready\")\n        return\n\n    try:\n        # If this task is already scheduled, don't schedule it again\n        # Instead, update the scheduling parameters\n        if Schedule.objects.filter(func=taskname).exists():\n            logger.info(f\"Scheduled task '{taskname}' already exists - updating!\")\n\n            Schedule.objects.filter(func=taskname).update(**kwargs)\n        else:\n            logger.info(f\"Creating scheduled task '{taskname}'\")\n\n            Schedule.objects.create(\n                name=taskname,\n                func=taskname,\n                **kwargs\n            )\n    except (OperationalError, ProgrammingError):\n        # Required if the DB is not ready yet\n        pass\n\n\ndef offload_task(taskname, force_sync=False, *args, **kwargs):\n    \"\"\"\n        Create an AsyncTask if workers are running.\n        This is different to a 'scheduled' task,\n        in that it only runs once!\n\n        If workers are not running or force_sync flag\n        is set then the task is ran synchronously.\n    \"\"\"\n\n    try:\n        from django_q.tasks import AsyncTask\n    except (AppRegistryNotReady):\n        logger.warning(\"Could not offload task - app registry not ready\")\n        return\n    import importlib\n    from InvenTree.status import is_worker_running\n\n    if is_worker_running() and not force_sync:\n        # Running as asynchronous task\n        try:\n            task = AsyncTask(taskname, *args, **kwargs)\n            task.run()\n        except ImportError:\n            logger.warning(f\"WARNING: '{taskname}' not started - Function not found\")\n    else:\n        # Split path\n        try:\n            app, mod, func = taskname.split('.')\n            app_mod = app + '.' + mod\n        except ValueError:\n            logger.warning(f\"WARNING: '{taskname}' not started - Malformed function path\")\n            return\n\n        # Import module from app\n        try:\n            _mod = importlib.import_module(app_mod)\n        except ModuleNotFoundError:\n            logger.warning(f\"WARNING: '{taskname}' not started - No module named '{app_mod}'\")\n            return\n\n        # Retrieve function\n        try:\n            _func = getattr(_mod, func)\n        except AttributeError:\n            # getattr does not work for local import\n            _func = None\n\n        try:\n            if not _func:\n                _func = eval(func)\n        except NameError:\n            logger.warning(f\"WARNING: '{taskname}' not started - No function named '{func}'\")\n            return\n        \n        # Workers are not running: run it as synchronous task\n        _func()\n\n\ndef heartbeat():\n    \"\"\"\n    Simple task which runs at 5 minute intervals,\n    so we can determine that the background worker\n    is actually running.\n\n    (There is probably a less \"hacky\" way of achieving this)?\n    \"\"\"\n\n    try:\n        from django_q.models import Success\n        logger.info(\"Could not perform heartbeat task - App registry not ready\")\n    except AppRegistryNotReady:\n        return\n\n    threshold = timezone.now() - timedelta(minutes=30)\n\n    # Delete heartbeat results more than half an hour old,\n    # otherwise they just create extra noise\n    heartbeats = Success.objects.filter(\n        func='InvenTree.tasks.heartbeat',\n        started__lte=threshold\n    )\n\n    heartbeats.delete()\n\n\ndef delete_successful_tasks():\n    \"\"\"\n    Delete successful task logs\n    which are more than a month old.\n    \"\"\"\n\n    try:\n        from django_q.models import Success\n    except AppRegistryNotReady:\n        logger.info(\"Could not perform 'delete_successful_tasks' - App registry not ready\")\n        return\n\n    threshold = timezone.now() - timedelta(days=30)\n\n    results = Success.objects.filter(\n        started__lte=threshold\n    )\n\n    results.delete()\n\n\ndef check_for_updates():\n    \"\"\"\n    Check if there is an update for InvenTree\n    \"\"\"\n\n    try:\n        import common.models\n    except AppRegistryNotReady:\n        # Apps not yet loaded!\n        logger.info(\"Could not perform 'check_for_updates' - App registry not ready\")\n        return\n\n    response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')\n\n    if not response.status_code == 200:\n        raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')\n\n    data = json.loads(response.text)\n\n    tag = data.get('tag_name', None)\n\n    if not tag:\n        raise ValueError(\"'tag_name' missing from GitHub response\")\n\n    match = re.match(r\"^.*(\\d+)\\.(\\d+)\\.(\\d+).*$\", tag)\n\n    if not len(match.groups()) == 3:\n        logger.warning(f\"Version '{tag}' did not match expected pattern\")\n        return\n\n    latest_version = [int(x) for x in match.groups()]\n\n    if not len(latest_version) == 3:\n        raise ValueError(f\"Version '{tag}' is not correct format\")\n\n    logger.info(f\"Latest InvenTree version: '{tag}'\")\n\n    # Save the version to the database\n    common.models.InvenTreeSetting.set_setting(\n        'INVENTREE_LATEST_VERSION',\n        tag,\n        None\n    )\n\n\ndef update_exchange_rates():\n    \"\"\"\n    Update currency exchange rates\n    \"\"\"\n\n    try:\n        from InvenTree.exchange import InvenTreeExchange\n        from djmoney.contrib.exchange.models import ExchangeBackend, Rate\n        from common.settings import currency_code_default, currency_codes\n    except AppRegistryNotReady:\n        # Apps not yet loaded!\n        logger.info(\"Could not perform 'update_exchange_rates' - App registry not ready\")\n        return\n    except:\n        # Other error?\n        return\n\n    # Test to see if the database is ready yet\n    try:\n        backend = ExchangeBackend.objects.get(name='InvenTreeExchange')\n    except ExchangeBackend.DoesNotExist:\n        pass\n    except:\n        # Some other error\n        print(\"Database not ready\")\n        return\n\n    backend = InvenTreeExchange()\n    print(f\"Updating exchange rates from {backend.url}\")\n\n    base = currency_code_default()\n\n    print(f\"Using base currency '{base}'\")\n\n    backend.update_rates(base_currency=base)\n\n    # Remove any exchange rates which are not in the provided currencies\n    Rate.objects.filter(backend=\"InvenTreeExchange\").exclude(currency__in=currency_codes()).delete()\n\n\ndef send_email(subject, body, recipients, from_email=None):\n    \"\"\"\n    Send an email with the specified subject and body,\n    to the specified recipients list.\n    \"\"\"\n\n    if type(recipients) == str:\n        recipients = [recipients]\n\n    offload_task(\n        'django.core.mail.send_mail',\n        subject, body,\n        from_email,\n        recipients,\n    )\n", "path": "InvenTree/InvenTree/tasks.py"}]}