We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
"Install Plugin" via GUI fails to add plugin to plugins.txt correctly for first plugin
### Please verify that this bug has NOT been raised before.

- [X] I checked and didn't find a similar issue

### Describe the bug*

When trying to install a plugin via the GUI function "Install Plugin" without any prior installed plugins, the plugin is appended to the first line of the plugins.txt.

As the first line is already populated with `# InvenTree Plugins (uses PIP framework to install)` the result is e.g. `# InvenTree Plugins (uses PIP framework to install)inventree-zebra-plugin@git+https://github.com/SergeoLacruz/inventree-zebra-plugin  # Installed 2023-04-28 06:55:40.146423+00:00 by xyz` thus ignoring the plugin in a docker env restart because of the `#`.

Subsequently installed plugins are correctly placed in an empty line of the plugins.txt.

### Steps to Reproduce

1. Have a fresh install without installed plugins.
2. Go to "Settings" > "Plugin Settings" > "Plugins" > "Install Plugin"
3. Fill "Package Name" with `inventree-zebra-plugin`
4. Fill "Source URL" with `git+https://github.com/SergeoLacruz/inventree-zebra-plugin`
5. "Submit"
6. Check plugins.txt file

### Expected behaviour

The installed plugin should be added below the comment such that it actually will be parsed by the plugin management.

### Deployment Method

- [X] Docker
- [ ] Bare metal

### Version Information

# Version Information:
InvenTree-Version: 0.11.0
Django Version: 3.2.18
Commit Hash: 593a716
Commit Date: 2023-04-18
Database: postgresql
Debug-Mode: False
Deployed using Docker: True
Active plugins: [{'name': 'InvenTreeBarcode', 'slug': 'inventreebarcode', 'version': '2.0.0'}, {'name': 'InvenTreeCoreNotificationsPlugin', 'slug': 'inventreecorenotificationsplugin', 'version': '1.0.0'}]


### Relevant log output

_No response_

--- 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/plugin/serializers.py`
Content:
```
1 """JSON serializers for plugin app."""
2 
3 import subprocess
4 
5 from django.conf import settings
6 from django.core.exceptions import ValidationError
7 from django.utils import timezone
8 from django.utils.translation import gettext_lazy as _
9 
10 from rest_framework import serializers
11 
12 from common.serializers import GenericReferencedSettingSerializer
13 from InvenTree.tasks import check_for_migrations, offload_task
14 from plugin.models import NotificationUserSetting, PluginConfig, PluginSetting
15 
16 
17 class MetadataSerializer(serializers.ModelSerializer):
18     """Serializer class for model metadata API access."""
19 
20     metadata = serializers.JSONField(required=True)
21 
22     class Meta:
23         """Metaclass options."""
24 
25         fields = [
26             'metadata',
27         ]
28 
29     def __init__(self, model_type, *args, **kwargs):
30         """Initialize the metadata serializer with information on the model type"""
31         self.Meta.model = model_type
32         super().__init__(*args, **kwargs)
33 
34     def update(self, instance, data):
35         """Perform update on the metadata field:
36 
37         - If this is a partial (PATCH) update, try to 'merge' data in
38         - Else, if it is a PUT update, overwrite any existing metadata
39         """
40         if self.partial:
41             # Default behaviour is to "merge" new data in
42             metadata = instance.metadata.copy() if instance.metadata else {}
43             metadata.update(data['metadata'])
44             data['metadata'] = metadata
45 
46         return super().update(instance, data)
47 
48 
49 class PluginConfigSerializer(serializers.ModelSerializer):
50     """Serializer for a PluginConfig."""
51 
52     class Meta:
53         """Meta for serializer."""
54         model = PluginConfig
55         fields = [
56             'key',
57             'name',
58             'active',
59             'meta',
60             'mixins',
61         ]
62 
63     meta = serializers.DictField(read_only=True)
64     mixins = serializers.DictField(read_only=True)
65 
66 
67 class PluginConfigInstallSerializer(serializers.Serializer):
68     """Serializer for installing a new plugin."""
69 
70     class Meta:
71         """Meta for serializer."""
72         fields = [
73             'url',
74             'packagename',
75             'confirm',
76         ]
77 
78     url = serializers.CharField(
79         required=False,
80         allow_blank=True,
81         label=_('Source URL'),
82         help_text=_('Source for the package - this can be a custom registry or a VCS path')
83     )
84     packagename = serializers.CharField(
85         required=False,
86         allow_blank=True,
87         label=_('Package Name'),
88         help_text=_('Name for the Plugin Package - can also contain a version indicator'),
89     )
90     confirm = serializers.BooleanField(
91         label=_('Confirm plugin installation'),
92         help_text=_('This will install this plugin now into the current instance. The instance will go into maintenance.')
93     )
94 
95     def validate(self, data):
96         """Validate inputs.
97 
98         Make sure both confirm and url are provided.
99         """
100         super().validate(data)
101 
102         # check the base requirements are met
103         if not data.get('confirm'):
104             raise ValidationError({'confirm': _('Installation not confirmed')})
105         if (not data.get('url')) and (not data.get('packagename')):
106             msg = _('Either packagename of URL must be provided')
107             raise ValidationError({'url': msg, 'packagename': msg})
108 
109         return data
110 
111     def save(self):
112         """Install a plugin from a package registry and set operational results as instance data."""
113         data = self.validated_data
114 
115         packagename = data.get('packagename', '')
116         url = data.get('url', '')
117 
118         # build up the command
119         install_name = []
120 
121         if url:
122             # use custom registration / VCS
123             if True in [identifier in url for identifier in ['git+https', 'hg+https', 'svn+svn', ]]:
124                 # using a VCS provider
125                 if packagename:
126                     install_name.append(f'{packagename}@{url}')
127                 else:
128                     install_name.append(url)
129             else:  # pragma: no cover
130                 # using a custom package repositories
131                 # This is only for pypa compliant directory services (all current are tested above)
132                 # and not covered by tests.
133                 install_name.append('-i')
134                 install_name.append(url)
135                 install_name.append(packagename)
136 
137         elif packagename:
138             # use pypi
139             install_name.append(packagename)
140 
141         command = 'python -m pip install'.split()
142         command.extend(install_name)
143         ret = {'command': ' '.join(command)}
144         success = False
145         # execute pypi
146         try:
147             result = subprocess.check_output(command, cwd=settings.BASE_DIR.parent)
148             ret['result'] = str(result, 'utf-8')
149             ret['success'] = True
150             success = True
151         except subprocess.CalledProcessError as error:  # pragma: no cover
152             ret['result'] = str(error.output, 'utf-8')
153             ret['error'] = True
154 
155         # save plugin to plugin_file if installed successfull
156         if success:
157             with open(settings.PLUGIN_FILE, "a") as plugin_file:
158                 plugin_file.write(f'{" ".join(install_name)}  # Installed {timezone.now()} by {str(self.context["request"].user)}\n')
159 
160         # Check for migrations
161         offload_task(check_for_migrations, worker=True)
162 
163         return ret
164 
165 
166 class PluginConfigEmptySerializer(serializers.Serializer):
167     """Serializer for a PluginConfig."""
168 
169 
170 class PluginSettingSerializer(GenericReferencedSettingSerializer):
171     """Serializer for the PluginSetting model."""
172 
173     MODEL = PluginSetting
174     EXTRA_FIELDS = [
175         'plugin',
176     ]
177 
178     plugin = serializers.CharField(source='plugin.key', read_only=True)
179 
180 
181 class NotificationUserSettingSerializer(GenericReferencedSettingSerializer):
182     """Serializer for the PluginSetting model."""
183 
184     MODEL = NotificationUserSetting
185     EXTRA_FIELDS = ['method', ]
186 
187     method = serializers.CharField(read_only=True)
188
```
--- 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/plugin/serializers.py b/InvenTree/plugin/serializers.py
--- a/InvenTree/plugin/serializers.py
+++ b/InvenTree/plugin/serializers.py
@@ -154,7 +154,13 @@
 
         # save plugin to plugin_file if installed successfull
         if success:
+            # Read content of plugin file
+            plg_lines = open(settings.PLUGIN_FILE).readlines()
             with open(settings.PLUGIN_FILE, "a") as plugin_file:
+                # Check if last line has a newline
+                if plg_lines[-1][-1:] != '\n':
+                    plugin_file.write('\n')
+                # Write new plugin to file
                 plugin_file.write(f'{" ".join(install_name)}  # Installed {timezone.now()} by {str(self.context["request"].user)}\n')
 
         # Check for migrations

{"golden_diff": "diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py\n--- a/InvenTree/plugin/serializers.py\n+++ b/InvenTree/plugin/serializers.py\n@@ -154,7 +154,13 @@\n \n         # save plugin to plugin_file if installed successfull\n         if success:\n+            # Read content of plugin file\n+            plg_lines = open(settings.PLUGIN_FILE).readlines()\n             with open(settings.PLUGIN_FILE, \"a\") as plugin_file:\n+                # Check if last line has a newline\n+                if plg_lines[-1][-1:] != '\\n':\n+                    plugin_file.write('\\n')\n+                # Write new plugin to file\n                 plugin_file.write(f'{\" \".join(install_name)}  # Installed {timezone.now()} by {str(self.context[\"request\"].user)}\\n')\n \n         # Check for migrations\n", "issue": "\"Install Plugin\" via GUI fails to add plugin to plugins.txt correctly for first plugin\n### Please verify that this bug has NOT been raised before.\n\n- [X] I checked and didn't find a similar issue\n\n### Describe the bug*\n\nWhen trying to install a plugin via the GUI function \"Install Plugin\" without any prior installed plugins, the plugin is appended to the first line of the plugins.txt.\r\n\r\nAs the first line is already populated with `# InvenTree Plugins (uses PIP framework to install)` the result is e.g. `# InvenTree Plugins (uses PIP framework to install)inventree-zebra-plugin@git+https://github.com/SergeoLacruz/inventree-zebra-plugin  # Installed 2023-04-28 06:55:40.146423+00:00 by xyz` thus ignoring the plugin in a docker env restart because of the `#`.\r\n\r\nSubsequently installed plugins are correctly placed in an empty line of the plugins.txt.\n\n### Steps to Reproduce\n\n1. Have a fresh install without installed plugins.\r\n2. Go to \"Settings\" > \"Plugin Settings\" > \"Plugins\" > \"Install Plugin\"\r\n3. Fill \"Package Name\" with `inventree-zebra-plugin`\r\n4. Fill \"Source URL\" with `git+https://github.com/SergeoLacruz/inventree-zebra-plugin`\r\n5. \"Submit\"\r\n6. Check plugins.txt file\n\n### Expected behaviour\n\nThe installed plugin should be added below the comment such that it actually will be parsed by the plugin management.\n\n### Deployment Method\n\n- [X] Docker\n- [ ] Bare metal\n\n### Version Information\n\n# Version Information:\r\nInvenTree-Version: 0.11.0\r\nDjango Version: 3.2.18\r\nCommit Hash: 593a716\r\nCommit Date: 2023-04-18\r\nDatabase: postgresql\r\nDebug-Mode: False\r\nDeployed using Docker: True\r\nActive plugins: [{'name': 'InvenTreeBarcode', 'slug': 'inventreebarcode', 'version': '2.0.0'}, {'name': 'InvenTreeCoreNotificationsPlugin', 'slug': 'inventreecorenotificationsplugin', 'version': '1.0.0'}]\r\n\n\n### Relevant log output\n\n_No response_\n", "before_files": [{"content": "\"\"\"JSON serializers for plugin app.\"\"\"\n\nimport subprocess\n\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom rest_framework import serializers\n\nfrom common.serializers import GenericReferencedSettingSerializer\nfrom InvenTree.tasks import check_for_migrations, offload_task\nfrom plugin.models import NotificationUserSetting, PluginConfig, PluginSetting\n\n\nclass MetadataSerializer(serializers.ModelSerializer):\n    \"\"\"Serializer class for model metadata API access.\"\"\"\n\n    metadata = serializers.JSONField(required=True)\n\n    class Meta:\n        \"\"\"Metaclass options.\"\"\"\n\n        fields = [\n            'metadata',\n        ]\n\n    def __init__(self, model_type, *args, **kwargs):\n        \"\"\"Initialize the metadata serializer with information on the model type\"\"\"\n        self.Meta.model = model_type\n        super().__init__(*args, **kwargs)\n\n    def update(self, instance, data):\n        \"\"\"Perform update on the metadata field:\n\n        - If this is a partial (PATCH) update, try to 'merge' data in\n        - Else, if it is a PUT update, overwrite any existing metadata\n        \"\"\"\n        if self.partial:\n            # Default behaviour is to \"merge\" new data in\n            metadata = instance.metadata.copy() if instance.metadata else {}\n            metadata.update(data['metadata'])\n            data['metadata'] = metadata\n\n        return super().update(instance, data)\n\n\nclass PluginConfigSerializer(serializers.ModelSerializer):\n    \"\"\"Serializer for a PluginConfig.\"\"\"\n\n    class Meta:\n        \"\"\"Meta for serializer.\"\"\"\n        model = PluginConfig\n        fields = [\n            'key',\n            'name',\n            'active',\n            'meta',\n            'mixins',\n        ]\n\n    meta = serializers.DictField(read_only=True)\n    mixins = serializers.DictField(read_only=True)\n\n\nclass PluginConfigInstallSerializer(serializers.Serializer):\n    \"\"\"Serializer for installing a new plugin.\"\"\"\n\n    class Meta:\n        \"\"\"Meta for serializer.\"\"\"\n        fields = [\n            'url',\n            'packagename',\n            'confirm',\n        ]\n\n    url = serializers.CharField(\n        required=False,\n        allow_blank=True,\n        label=_('Source URL'),\n        help_text=_('Source for the package - this can be a custom registry or a VCS path')\n    )\n    packagename = serializers.CharField(\n        required=False,\n        allow_blank=True,\n        label=_('Package Name'),\n        help_text=_('Name for the Plugin Package - can also contain a version indicator'),\n    )\n    confirm = serializers.BooleanField(\n        label=_('Confirm plugin installation'),\n        help_text=_('This will install this plugin now into the current instance. The instance will go into maintenance.')\n    )\n\n    def validate(self, data):\n        \"\"\"Validate inputs.\n\n        Make sure both confirm and url are provided.\n        \"\"\"\n        super().validate(data)\n\n        # check the base requirements are met\n        if not data.get('confirm'):\n            raise ValidationError({'confirm': _('Installation not confirmed')})\n        if (not data.get('url')) and (not data.get('packagename')):\n            msg = _('Either packagename of URL must be provided')\n            raise ValidationError({'url': msg, 'packagename': msg})\n\n        return data\n\n    def save(self):\n        \"\"\"Install a plugin from a package registry and set operational results as instance data.\"\"\"\n        data = self.validated_data\n\n        packagename = data.get('packagename', '')\n        url = data.get('url', '')\n\n        # build up the command\n        install_name = []\n\n        if url:\n            # use custom registration / VCS\n            if True in [identifier in url for identifier in ['git+https', 'hg+https', 'svn+svn', ]]:\n                # using a VCS provider\n                if packagename:\n                    install_name.append(f'{packagename}@{url}')\n                else:\n                    install_name.append(url)\n            else:  # pragma: no cover\n                # using a custom package repositories\n                # This is only for pypa compliant directory services (all current are tested above)\n                # and not covered by tests.\n                install_name.append('-i')\n                install_name.append(url)\n                install_name.append(packagename)\n\n        elif packagename:\n            # use pypi\n            install_name.append(packagename)\n\n        command = 'python -m pip install'.split()\n        command.extend(install_name)\n        ret = {'command': ' '.join(command)}\n        success = False\n        # execute pypi\n        try:\n            result = subprocess.check_output(command, cwd=settings.BASE_DIR.parent)\n            ret['result'] = str(result, 'utf-8')\n            ret['success'] = True\n            success = True\n        except subprocess.CalledProcessError as error:  # pragma: no cover\n            ret['result'] = str(error.output, 'utf-8')\n            ret['error'] = True\n\n        # save plugin to plugin_file if installed successfull\n        if success:\n            with open(settings.PLUGIN_FILE, \"a\") as plugin_file:\n                plugin_file.write(f'{\" \".join(install_name)}  # Installed {timezone.now()} by {str(self.context[\"request\"].user)}\\n')\n\n        # Check for migrations\n        offload_task(check_for_migrations, worker=True)\n\n        return ret\n\n\nclass PluginConfigEmptySerializer(serializers.Serializer):\n    \"\"\"Serializer for a PluginConfig.\"\"\"\n\n\nclass PluginSettingSerializer(GenericReferencedSettingSerializer):\n    \"\"\"Serializer for the PluginSetting model.\"\"\"\n\n    MODEL = PluginSetting\n    EXTRA_FIELDS = [\n        'plugin',\n    ]\n\n    plugin = serializers.CharField(source='plugin.key', read_only=True)\n\n\nclass NotificationUserSettingSerializer(GenericReferencedSettingSerializer):\n    \"\"\"Serializer for the PluginSetting model.\"\"\"\n\n    MODEL = NotificationUserSetting\n    EXTRA_FIELDS = ['method', ]\n\n    method = serializers.CharField(read_only=True)\n", "path": "InvenTree/plugin/serializers.py"}], "after_files": [{"content": "\"\"\"JSON serializers for plugin app.\"\"\"\n\nimport subprocess\n\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom rest_framework import serializers\n\nfrom common.serializers import GenericReferencedSettingSerializer\nfrom InvenTree.tasks import check_for_migrations, offload_task\nfrom plugin.models import NotificationUserSetting, PluginConfig, PluginSetting\n\n\nclass MetadataSerializer(serializers.ModelSerializer):\n    \"\"\"Serializer class for model metadata API access.\"\"\"\n\n    metadata = serializers.JSONField(required=True)\n\n    class Meta:\n        \"\"\"Metaclass options.\"\"\"\n\n        fields = [\n            'metadata',\n        ]\n\n    def __init__(self, model_type, *args, **kwargs):\n        \"\"\"Initialize the metadata serializer with information on the model type\"\"\"\n        self.Meta.model = model_type\n        super().__init__(*args, **kwargs)\n\n    def update(self, instance, data):\n        \"\"\"Perform update on the metadata field:\n\n        - If this is a partial (PATCH) update, try to 'merge' data in\n        - Else, if it is a PUT update, overwrite any existing metadata\n        \"\"\"\n        if self.partial:\n            # Default behaviour is to \"merge\" new data in\n            metadata = instance.metadata.copy() if instance.metadata else {}\n            metadata.update(data['metadata'])\n            data['metadata'] = metadata\n\n        return super().update(instance, data)\n\n\nclass PluginConfigSerializer(serializers.ModelSerializer):\n    \"\"\"Serializer for a PluginConfig.\"\"\"\n\n    class Meta:\n        \"\"\"Meta for serializer.\"\"\"\n        model = PluginConfig\n        fields = [\n            'key',\n            'name',\n            'active',\n            'meta',\n            'mixins',\n        ]\n\n    meta = serializers.DictField(read_only=True)\n    mixins = serializers.DictField(read_only=True)\n\n\nclass PluginConfigInstallSerializer(serializers.Serializer):\n    \"\"\"Serializer for installing a new plugin.\"\"\"\n\n    class Meta:\n        \"\"\"Meta for serializer.\"\"\"\n        fields = [\n            'url',\n            'packagename',\n            'confirm',\n        ]\n\n    url = serializers.CharField(\n        required=False,\n        allow_blank=True,\n        label=_('Source URL'),\n        help_text=_('Source for the package - this can be a custom registry or a VCS path')\n    )\n    packagename = serializers.CharField(\n        required=False,\n        allow_blank=True,\n        label=_('Package Name'),\n        help_text=_('Name for the Plugin Package - can also contain a version indicator'),\n    )\n    confirm = serializers.BooleanField(\n        label=_('Confirm plugin installation'),\n        help_text=_('This will install this plugin now into the current instance. The instance will go into maintenance.')\n    )\n\n    def validate(self, data):\n        \"\"\"Validate inputs.\n\n        Make sure both confirm and url are provided.\n        \"\"\"\n        super().validate(data)\n\n        # check the base requirements are met\n        if not data.get('confirm'):\n            raise ValidationError({'confirm': _('Installation not confirmed')})\n        if (not data.get('url')) and (not data.get('packagename')):\n            msg = _('Either packagename of URL must be provided')\n            raise ValidationError({'url': msg, 'packagename': msg})\n\n        return data\n\n    def save(self):\n        \"\"\"Install a plugin from a package registry and set operational results as instance data.\"\"\"\n        data = self.validated_data\n\n        packagename = data.get('packagename', '')\n        url = data.get('url', '')\n\n        # build up the command\n        install_name = []\n\n        if url:\n            # use custom registration / VCS\n            if True in [identifier in url for identifier in ['git+https', 'hg+https', 'svn+svn', ]]:\n                # using a VCS provider\n                if packagename:\n                    install_name.append(f'{packagename}@{url}')\n                else:\n                    install_name.append(url)\n            else:  # pragma: no cover\n                # using a custom package repositories\n                # This is only for pypa compliant directory services (all current are tested above)\n                # and not covered by tests.\n                install_name.append('-i')\n                install_name.append(url)\n                install_name.append(packagename)\n\n        elif packagename:\n            # use pypi\n            install_name.append(packagename)\n\n        command = 'python -m pip install'.split()\n        command.extend(install_name)\n        ret = {'command': ' '.join(command)}\n        success = False\n        # execute pypi\n        try:\n            result = subprocess.check_output(command, cwd=settings.BASE_DIR.parent)\n            ret['result'] = str(result, 'utf-8')\n            ret['success'] = True\n            success = True\n        except subprocess.CalledProcessError as error:  # pragma: no cover\n            ret['result'] = str(error.output, 'utf-8')\n            ret['error'] = True\n\n        # save plugin to plugin_file if installed successfull\n        if success:\n            # Read content of plugin file\n            plg_lines = open(settings.PLUGIN_FILE).readlines()\n            with open(settings.PLUGIN_FILE, \"a\") as plugin_file:\n                # Check if last line has a newline\n                if plg_lines[-1][-1:] != '\\n':\n                    plugin_file.write('\\n')\n                # Write new plugin to file\n                plugin_file.write(f'{\" \".join(install_name)}  # Installed {timezone.now()} by {str(self.context[\"request\"].user)}\\n')\n\n        # Check for migrations\n        offload_task(check_for_migrations, worker=True)\n\n        return ret\n\n\nclass PluginConfigEmptySerializer(serializers.Serializer):\n    \"\"\"Serializer for a PluginConfig.\"\"\"\n\n\nclass PluginSettingSerializer(GenericReferencedSettingSerializer):\n    \"\"\"Serializer for the PluginSetting model.\"\"\"\n\n    MODEL = PluginSetting\n    EXTRA_FIELDS = [\n        'plugin',\n    ]\n\n    plugin = serializers.CharField(source='plugin.key', read_only=True)\n\n\nclass NotificationUserSettingSerializer(GenericReferencedSettingSerializer):\n    \"\"\"Serializer for the PluginSetting model.\"\"\"\n\n    MODEL = NotificationUserSetting\n    EXTRA_FIELDS = ['method', ]\n\n    method = serializers.CharField(read_only=True)\n", "path": "InvenTree/plugin/serializers.py"}]}