Repository: inventree/InvenTree

Issue:
[BUG] :checkered_flag: Plugin scheduled tasks: Schedule not updated
### Please verify that this bug has NOT been raised before.

- [X] I checked and didn't find similar issue

### Describe the bug*

The scheduling time of a task is not updated after change in the source code. 

### Steps to Reproduce

I enabled the scheduled tasks plugin from samples 
I did some slight modification to the code:
```python

def print_hello():
    now = datetime.now()
    print(now, ": Hello")  # pragma: no cover


def print_world():
    now = datetime.now()
    print(now,": World")  # pragma: no cover

class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):

    NAME = "ScheduledTasksPlugin"
    SLUG = "schedule"
    TITLE = "Scheduled Tasks"

    SCHEDULED_TASKS = {
          'hello': {
            'func': 'plugin.samples.integration.scheduled_task.print_hello',
            'schedule': 'I',
            'minutes': 5,
        },
        'world': {
            'func': 'plugin.samples.integration.scheduled_task.print_world',
            'schedule': 'H',
        },
    }
```
Here is what I get from the log after sorting a bit.

2023-01-20 22:50 : World
2023-01-20 23:51 : World
2023-01-21 00:50 : World
2023-01-21 01:50 : World
2023-01-21 02:50 : World
2023-01-21 04:51 : World
2023-01-21 05:50 : World
2023-01-21 06:51 : World

2023-01-20 22:50 : Hello
2023-01-20 23:36 : Hello
2023-01-21 00:20 : Hello
2023-01-21 01:05 : Hello
2023-01-21 01:50 : Hello
2023-01-21 02:36 : Hello
2023-01-21 03:20 : Hello

So Hello is executed every 45 minutes instead of five. I took a look at the database and found the 45:

![schedule](https://user-images.githubusercontent.com/10836099/213859688-73452d44-bedc-4049-81d2-f9df3bce615a.png)

The problem seems to be in ../plugin/base/integration/mixins.py
```python
  def register_tasks(self):
        """Register the tasks with the database."""
        try:
            from django_q.models import Schedule

            for key, task in self.scheduled_tasks.items():

                task_name = self.get_task_name(key)

                if Schedule.objects.filter(name=task_name).exists():
                    # Scheduled task already exists - continue!
                    continue  # pragma: no cover

                logger.info(f"Adding scheduled task '{task_name}'")

                func_name = task['func'].strip()

```
As I do not see the log message I think create is not executed because it already exists. Or do I miss something?

Michael

### Expected behavior

I would expect the scheduling times to change when I edit the source of the plugin.

### Deployment Method

- [ ] Docker
- [X] Bare metal

### Version Information

InvenTree Version 	[0.10.0 dev](https://github.com/inventree/InvenTree/releases)
Development Version
	Commit Hash	4bfc1f742
	Commit Date	2023-01-11

### Relevant log output

```shell
please see above
```



Discussion:
Dang, this is probably related to https://github.com/inventree/InvenTree/issues/2726, I will take a look.
Thanks for the report @SergeoLacruz 

Resolving patch:
diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py
--- a/InvenTree/plugin/base/integration/mixins.py
+++ b/InvenTree/plugin/base/integration/mixins.py
@@ -153,43 +153,34 @@ def register_tasks(self):
             for key, task in self.scheduled_tasks.items():
 
                 task_name = self.get_task_name(key)
-
-                if Schedule.objects.filter(name=task_name).exists():
-                    # Scheduled task already exists - continue!
-                    continue  # pragma: no cover
-
-                logger.info(f"Adding scheduled task '{task_name}'")
-
+                obj = {
+                    'name': task_name,
+                    'schedule_type': task['schedule'],
+                    'minutes': task.get('minutes', None),
+                    'repeats': task.get('repeats', -1),
+                }
                 func_name = task['func'].strip()
 
                 if '.' in func_name:
                     """Dotted notation indicates that we wish to run a globally defined function, from a specified Python module."""
-
-                    Schedule.objects.create(
-                        name=task_name,
-                        func=func_name,
-                        schedule_type=task['schedule'],
-                        minutes=task.get('minutes', None),
-                        repeats=task.get('repeats', -1),
-                    )
-
+                    obj['func'] = func_name
                 else:
-                    """
-                    Non-dotted notation indicates that we wish to call a 'member function' of the calling plugin.
-
-                    This is managed by the plugin registry itself.
-                    """
-
+                    """Non-dotted notation indicates that we wish to call a 'member function' of the calling plugin. This is managed by the plugin registry itself."""
                     slug = self.plugin_slug()
+                    obj['func'] = registry.call_plugin_function
+                    obj['args'] = f"'{slug}', '{func_name}'"
 
-                    Schedule.objects.create(
-                        name=task_name,
-                        func=registry.call_plugin_function,
-                        args=f"'{slug}', '{func_name}'",
-                        schedule_type=task['schedule'],
-                        minutes=task.get('minutes', None),
-                        repeats=task.get('repeats', -1),
-                    )
+                if Schedule.objects.filter(name=task_name).exists():
+                    # Scheduled task already exists - update it!
+                    logger.info(f"Updating scheduled task '{task_name}'")
+                    instance = Schedule.objects.get(name=task_name)
+                    for item in obj:
+                        setattr(instance, item, obj[item])
+                    instance.save()
+                else:
+                    logger.info(f"Adding scheduled task '{task_name}'")
+                    # Create a new scheduled task
+                    Schedule.objects.create(**obj)
 
         except (ProgrammingError, OperationalError):  # pragma: no cover
             # Database might not yet be ready
diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
--- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
+++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
@@ -55,7 +55,7 @@ def format_matched_response(self, label, model, instance):
             url = instance.get_absolute_url()
             data['web_url'] = url
         else:
-            url = None
+            url = None  # pragma: no cover
 
         response = {
             label: data


Test patch:
diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py
--- a/InvenTree/plugin/base/integration/test_mixins.py
+++ b/InvenTree/plugin/base/integration/test_mixins.py
@@ -271,6 +271,11 @@ def test_api_call(self):
         self.assertTrue(result)
         self.assertEqual(result['name'], 'morpheus')
 
+        # api_call with endpoint with leading slash
+        result = self.mixin.api_call('/orgs/inventree', simple_response=False)
+        self.assertTrue(result)
+        self.assertEqual(result.reason, 'OK')
+
         # api_call with filter
         result = self.mixin.api_call('repos/inventree/InvenTree/stargazers', url_args={'page': '2'})
         self.assertTrue(result)
diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py
--- a/InvenTree/plugin/samples/integration/test_scheduled_task.py
+++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py
@@ -31,6 +31,19 @@ def test_function(self):
         scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.")
         self.assertEqual(len(scheduled_plugin_tasks), 3)
 
+        # test updating the schedule
+        hello_schedule = Schedule.objects.get(name='plugin.schedule.hello')
+        self.assertEqual(hello_schedule.minutes, 45)
+        # change the schedule and reregister
+        plg.scheduled_tasks['hello']['minutes'] = 15
+        plg.register_tasks()
+
+        # Check that the schedule was updated
+        hello_schedule = Schedule.objects.get(name='plugin.schedule.hello')
+        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.")
+        self.assertEqual(hello_schedule.minutes, 15)
+        self.assertEqual(len(scheduled_plugin_tasks), 3)
+
         # delete middle task
         # this is to check the system also deals with disappearing tasks
         scheduled_plugin_tasks[1].delete()
diff --git a/InvenTree/plugin/samples/locate/test_locate_sample.py b/InvenTree/plugin/samples/locate/test_locate_sample.py
--- a/InvenTree/plugin/samples/locate/test_locate_sample.py
+++ b/InvenTree/plugin/samples/locate/test_locate_sample.py
@@ -51,9 +51,14 @@ def test_run_locator(self):
 
     def test_mixin(self):
         """Test that MixinNotImplementedError is raised."""
+        # Test location locator
         with self.assertRaises(MixinNotImplementedError):
             class Wrong(LocateMixin, InvenTreePlugin):
                 pass
 
             plugin = Wrong()
             plugin.locate_stock_location(1)
+
+        # Test item locator
+        with self.assertRaises(MixinNotImplementedError):
+            plugin.locate_stock_item(1)