Merge branch 'inventree:master' into matmair/issue2279

This commit is contained in:
Matthias Mair 2022-01-07 00:08:17 +01:00 committed by GitHub
commit 978018e284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 9747 additions and 9267 deletions

17
.github/workflows/welcome.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# welcome new contributers
name: Welcome
on:
pull_request:
types: [opened]
issues:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: 'Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.\nIf you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).'
pr-message: 'This is your first PR, welcome!\nPlease check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.\nMake sure to document why this PR is needed and to link connected issues so we can review it faster.'

1
.gitignore vendored
View File

@ -49,6 +49,7 @@ static_i18n
# Local config file
config.yaml
plugins.txt
# Default data file
data.json

View File

@ -0,0 +1,90 @@
"""
Helper functions for loading InvenTree configuration options
"""
import os
import shutil
import logging
logger = logging.getLogger('inventree')
def get_base_dir():
""" Returns the base (top-level) InvenTree directory """
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_config_file():
"""
Returns the path of the InvenTree configuration file.
Note: It will be created it if does not already exist!
"""
base_dir = get_base_dir()
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
if cfg_filename:
cfg_filename = cfg_filename.strip()
cfg_filename = os.path.abspath(cfg_filename)
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")
cfg_template = os.path.join(base_dir, "config_template.yaml")
shutil.copyfile(cfg_template, cfg_filename)
print(f"Created config file {cfg_filename}")
return cfg_filename
def get_plugin_file():
"""
Returns the path of the InvenTree plugins specification file.
Note: It will be created if it does not already exist!
"""
# Check if the plugin.txt file (specifying required plugins) is specified
PLUGIN_FILE = os.getenv('INVENTREE_PLUGIN_FILE')
if not PLUGIN_FILE:
# If not specified, look in the same directory as the configuration file
config_dir = os.path.dirname(get_config_file())
PLUGIN_FILE = os.path.join(config_dir, 'plugins.txt')
if not os.path.exists(PLUGIN_FILE):
logger.warning("Plugin configuration file does not exist")
logger.info(f"Creating plugin file at '{PLUGIN_FILE}'")
# If opening the file fails (no write permission, for example), then this will throw an error
with open(PLUGIN_FILE, 'w') as plugin_file:
plugin_file.write("# InvenTree Plugins (uses PIP framework to install)\n\n")
return PLUGIN_FILE
def get_setting(environment_var, backup_val, default_value=None):
"""
Helper function for retrieving a configuration setting value
- First preference is to look for the environment variable
- Second preference is to look for the value of the settings file
- Third preference is the default value
"""
val = os.getenv(environment_var)
if val is not None:
return val
if backup_val is not None:
return backup_val
return default_value

View File

@ -404,21 +404,28 @@ def DownloadFile(data, filename, content_type='application/text', inline=False):
return response
def extract_serial_numbers(serials, expected_quantity):
def extract_serial_numbers(serials, expected_quantity, next_number: int):
""" Attempt to extract serial numbers from an input string.
- Serial numbers must be integer values
- Serial numbers must be positive
- Serial numbers can be split by whitespace / newline / commma chars
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
- Serial numbers can be defined as ~ for getting the next available serial number
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
Args:
serials: input string with patterns
expected_quantity: The number of (unique) serial numbers we expect
next_number(int): the next possible serial number
"""
serials = serials.strip()
# fill in the next serial number into the serial
if '~' in serials:
serials = serials.replace('~', str(next_number))
groups = re.split("[\s,]+", serials)
numbers = []
@ -493,11 +500,20 @@ def extract_serial_numbers(serials, expected_quantity):
errors.append(_("Invalid group: {g}").format(g=group))
continue
# Group should be a number
elif group:
# try conversion
try:
number = int(group)
except:
# seem like it is not a number
raise ValidationError(_(f"Invalid group {group}"))
number_add(number)
# No valid input group detected
else:
if group in numbers:
errors.append(_("Duplicate serial: {g}".format(g=group)))
else:
numbers.append(group)
raise ValidationError(_(f"Invalid/no group {group}"))
if len(errors) > 0:
raise ValidationError(errors)

View File

@ -17,7 +17,6 @@ import os
import random
import socket
import string
import shutil
import sys
from datetime import datetime
@ -28,30 +27,12 @@ from django.utils.translation import gettext_lazy as _
from django.contrib.messages import constants as messages
import django.conf.locale
from .config import get_base_dir, get_config_file, get_plugin_file, get_setting
def _is_true(x):
# Shortcut function to determine if a value "looks" like a boolean
return str(x).lower() in ['1', 'y', 'yes', 't', 'true']
def get_setting(environment_var, backup_val, default_value=None):
"""
Helper function for retrieving a configuration setting value
- First preference is to look for the environment variable
- Second preference is to look for the value of the settings file
- Third preference is the default value
"""
val = os.getenv(environment_var)
if val is not None:
return val
if backup_val is not None:
return backup_val
return default_value
return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true']
# Determine if we are running in "test" mode e.g. "manage.py test"
@ -61,27 +42,9 @@ TESTING = 'test' in sys.argv
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = get_base_dir()
# 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)
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")
cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
shutil.copyfile(cfg_template, cfg_filename)
print(f"Created config file {cfg_filename}")
cfg_filename = get_config_file()
with open(cfg_filename, 'r') as cfg:
CONFIG = yaml.safe_load(cfg)
@ -89,6 +52,8 @@ with open(cfg_filename, 'r') as cfg:
# We will place any config files in the same directory as the config file
config_dir = os.path.dirname(cfg_filename)
PLUGIN_FILE = get_plugin_file()
# Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = _is_true(get_setting(
@ -908,8 +873,7 @@ MARKDOWNIFY_BLEACH = False
# Maintenance mode
MAINTENANCE_MODE_RETRY_AFTER = 60
# Plugins
# Plugin Directories (local plugins will be loaded from these directories)
PLUGIN_DIRS = ['plugin.builtin', ]
if not TESTING:

View File

@ -237,25 +237,41 @@ class TestSerialNumberExtraction(TestCase):
e = helpers.extract_serial_numbers
sn = e("1-5", 5)
self.assertEqual(len(sn), 5)
sn = e("1-5", 5, 1)
self.assertEqual(len(sn), 5, 1)
for i in range(1, 6):
self.assertIn(i, sn)
sn = e("1, 2, 3, 4, 5", 5)
sn = e("1, 2, 3, 4, 5", 5, 1)
self.assertEqual(len(sn), 5)
sn = e("1-5, 10-15", 11)
sn = e("1-5, 10-15", 11, 1)
self.assertIn(3, sn)
self.assertIn(13, sn)
sn = e("1+", 10)
sn = e("1+", 10, 1)
self.assertEqual(len(sn), 10)
self.assertEqual(sn, [_ for _ in range(1, 11)])
sn = e("4, 1+2", 4)
sn = e("4, 1+2", 4, 1)
self.assertEqual(len(sn), 4)
self.assertEqual(sn, ["4", 1, 2, 3])
self.assertEqual(sn, [4, 1, 2, 3])
sn = e("~", 1, 1)
self.assertEqual(len(sn), 1)
self.assertEqual(sn, [1])
sn = e("~", 1, 3)
self.assertEqual(len(sn), 1)
self.assertEqual(sn, [3])
sn = e("~+", 2, 5)
self.assertEqual(len(sn), 2)
self.assertEqual(sn, [5, 6])
sn = e("~+3", 4, 5)
self.assertEqual(len(sn), 4)
self.assertEqual(sn, [5, 6, 7, 8])
def test_failures(self):
@ -263,26 +279,45 @@ class TestSerialNumberExtraction(TestCase):
# Test duplicates
with self.assertRaises(ValidationError):
e("1,2,3,3,3", 5)
e("1,2,3,3,3", 5, 1)
# Test invalid length
with self.assertRaises(ValidationError):
e("1,2,3", 5)
e("1,2,3", 5, 1)
# Test empty string
with self.assertRaises(ValidationError):
e(", , ,", 0)
e(", , ,", 0, 1)
# Test incorrect sign in group
with self.assertRaises(ValidationError):
e("10-2", 8)
e("10-2", 8, 1)
# Test invalid group
with self.assertRaises(ValidationError):
e("1-5-10", 10)
e("1-5-10", 10, 1)
with self.assertRaises(ValidationError):
e("10, a, 7-70j", 4)
e("10, a, 7-70j", 4, 1)
def test_combinations(self):
e = helpers.extract_serial_numbers
sn = e("1 3-5 9+2", 7, 1)
self.assertEqual(len(sn), 7)
self.assertEqual(sn, [1, 3, 4, 5, 9, 10, 11])
sn = e("1,3-5,9+2", 7, 1)
self.assertEqual(len(sn), 7)
self.assertEqual(sn, [1, 3, 4, 5, 9, 10, 11])
sn = e("~+2", 3, 14)
self.assertEqual(len(sn), 3)
self.assertEqual(sn, [14, 15, 16])
sn = e("~+", 2, 14)
self.assertEqual(len(sn), 2)
self.assertEqual(sn, [14, 15])
class TestVersionNumber(TestCase):

View File

@ -109,7 +109,7 @@ class BuildOutputCreate(AjaxUpdateView):
# Check that the serial numbers are valid
if serials:
try:
extracted = extract_serial_numbers(serials, quantity)
extracted = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
if extracted:
# Check for conflicting serial numbers
@ -143,7 +143,7 @@ class BuildOutputCreate(AjaxUpdateView):
serials = data.get('serial_numbers', None)
if serials:
serial_numbers = extract_serial_numbers(serials, quantity)
serial_numbers = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
else:
serial_numbers = None

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -861,7 +861,7 @@ class SOSerialAllocationSerializer(serializers.Serializer):
part = line_item.part
try:
data['serials'] = extract_serial_numbers(serial_numbers, quantity)
data['serials'] = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
except DjangoValidationError as e:
raise ValidationError({
'serial_numbers': e.messages,

View File

@ -594,6 +594,26 @@ class Part(MPTTModel):
# No serial numbers found
return None
def getLatestSerialNumberInt(self):
"""
Return the "latest" serial number for this Part as a integer.
If it is not an integer the result is 0
"""
latest = self.getLatestSerialNumber()
# No serial number = > 0
if latest is None:
latest = 0
# Attempt to turn into an integer and return
try:
latest = int(latest)
return latest
except:
# not an integer so 0
return 0
def getSerialNumberString(self, quantity=1):
"""
Return a formatted string representing the next available serial numbers,

View File

@ -1,44 +1,45 @@
"""default mixins for IntegrationMixins"""
"""
Plugin mixin classes
"""
from django.conf.urls import url, include
from plugin.urls import PLUGIN_BASE
class GlobalSettingsMixin:
"""Mixin that enables global settings for the plugin"""
class SettingsMixin:
"""
Mixin that enables global settings for the plugin
"""
class MixinMeta:
"""meta options for this mixin"""
MIXIN_NAME = 'Global settings'
MIXIN_NAME = 'Settings'
def __init__(self):
super().__init__()
self.add_mixin('globalsettings', 'has_globalsettings', __class__)
self.globalsettings = self.setup_globalsettings()
def setup_globalsettings(self):
"""
setup global settings for this plugin
"""
return getattr(self, 'GLOBALSETTINGS', None)
self.add_mixin('settings', 'has_globalsettings', __class__)
self.globalsettings = getattr(self, 'SETTINGS', None)
@property
def has_globalsettings(self):
"""
does this plugin use custom global settings
Does this plugin use custom global settings
"""
return bool(self.globalsettings)
@property
def globalsettingspatterns(self):
"""
get patterns for InvenTreeSetting defintion
Get patterns for InvenTreeSetting defintion
"""
if self.has_globalsettings:
return {f'PLUGIN_{self.slug.upper()}_{key}': value for key, value in self.globalsettings.items()}
return None
def _globalsetting_name(self, key):
"""get global name of setting"""
"""
Get global name of setting
"""
return f'PLUGIN_{self.slug.upper()}_{key}'
def get_globalsetting(self, key):
@ -57,9 +58,11 @@ class GlobalSettingsMixin:
class UrlsMixin:
"""Mixin that enables urls for the plugin"""
"""
Mixin that enables custom URLs for the plugin
"""
class MixinMeta:
"""meta options for this mixin"""
MIXIN_NAME = 'URLs'
def __init__(self):
@ -105,7 +108,10 @@ class UrlsMixin:
class NavigationMixin:
"""Mixin that enables adding navigation links with the plugin"""
"""
Mixin that enables custom navigation links with the plugin
"""
NAVIGATION_TAB_NAME = None
NAVIGATION_TAB_ICON = "fas fa-question"
@ -152,7 +158,10 @@ class NavigationMixin:
class AppMixin:
"""Mixin that enables full django app functions for a plugin"""
"""
Mixin that enables full django app functions for a plugin
"""
class MixinMeta:
"""meta options for this mixin"""
MIXIN_NAME = 'App registration'

View File

@ -1,6 +1,9 @@
"""utility class to enable simpler imports"""
from ..builtin.integration.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin
from ..builtin.integration.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin
__all__ = [
'AppMixin', 'GlobalSettingsMixin', 'UrlsMixin', 'NavigationMixin',
'AppMixin',
'NavigationMixin',
'SettingsMixin',
'UrlsMixin',
]

View File

@ -54,7 +54,11 @@ class PluginConfig(models.Model):
# extra attributes from the registry
def mixins(self):
try:
return self.plugin._mixinreg
except (AttributeError, ValueError):
return {}
# functions

View File

@ -251,7 +251,7 @@ class Plugins:
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
logger.info('Registering IntegrationPlugin global settings')
for slug, plugin in plugins:
if plugin.mixin_enabled('globalsettings'):
if plugin.mixin_enabled('settings'):
plugin_setting = plugin.globalsettingspatterns
self.mixins_globalsettings[slug] = plugin_setting

View File

@ -1,15 +1,18 @@
"""sample implementations for IntegrationPlugin"""
"""
Sample implementations for IntegrationPlugin
"""
from plugin import IntegrationPluginBase
from plugin.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin
from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
from django.conf.urls import url, include
class SampleIntegrationPlugin(AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin, IntegrationPluginBase):
class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, IntegrationPluginBase):
"""
An full integration plugin
A full integration plugin example
"""
PLUGIN_NAME = "SampleIntegrationPlugin"

View File

@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model
from datetime import datetime
from plugin import IntegrationPluginBase
from plugin.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin
from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin
from plugin.urls import PLUGIN_BASE
@ -20,19 +20,19 @@ class BaseMixinDefinition:
self.assertEqual(self.mixin.registered_mixins[0]['human_name'], self.MIXIN_HUMAN_NAME)
class GlobalSettingsMixinTest(BaseMixinDefinition, TestCase):
MIXIN_HUMAN_NAME = 'Global settings'
MIXIN_NAME = 'globalsettings'
class SettingsMixinTest(BaseMixinDefinition, TestCase):
MIXIN_HUMAN_NAME = 'Settings'
MIXIN_NAME = 'settings'
MIXIN_ENABLE_CHECK = 'has_globalsettings'
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
def setUp(self):
class SettingsCls(GlobalSettingsMixin, IntegrationPluginBase):
GLOBALSETTINGS = self.TEST_SETTINGS
class SettingsCls(SettingsMixin, IntegrationPluginBase):
SETTINGS = self.TEST_SETTINGS
self.mixin = SettingsCls()
class NoSettingsCls(GlobalSettingsMixin, IntegrationPluginBase):
class NoSettingsCls(SettingsMixin, IntegrationPluginBase):
pass
self.mixin_nothing = NoSettingsCls()

View File

@ -480,18 +480,6 @@ class StockList(generics.ListCreateAPIView):
notes = data.get('notes', '')
serials = None
if serial_numbers:
# If serial numbers are specified, check that they match!
try:
serials = extract_serial_numbers(serial_numbers, data['quantity'])
except DjangoValidationError as e:
raise ValidationError({
'quantity': e.messages,
'serial_numbers': e.messages,
})
with transaction.atomic():
# Create an initial stock item
@ -507,6 +495,19 @@ class StockList(generics.ListCreateAPIView):
if item.part.default_expiry > 0:
item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
# fetch serial numbers
serials = None
if serial_numbers:
# If serial numbers are specified, check that they match!
try:
serials = extract_serial_numbers(serial_numbers, quantity, item.part.getLatestSerialNumberInt())
except DjangoValidationError as e:
raise ValidationError({
'quantity': e.messages,
'serial_numbers': e.messages,
})
# Finally, save the item (with user information)
item.save(user=user)

View File

@ -350,7 +350,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
serial_numbers = data['serial_numbers']
try:
serials = InvenTree.helpers.extract_serial_numbers(serial_numbers, quantity)
serials = InvenTree.helpers.extract_serial_numbers(serial_numbers, quantity, item.part.getLatestSerialNumberInt())
except DjangoValidationError as e:
raise ValidationError({
'serial_numbers': e.messages,
@ -379,6 +379,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
serials = InvenTree.helpers.extract_serial_numbers(
data['serial_numbers'],
data['quantity'],
item.part.getLatestSerialNumberInt()
)
item.serializeStock(

View File

@ -3,6 +3,7 @@
{% load inventree_extras %}
{% load status_codes %}
{% load i18n %}
{% load l10n %}
{% block page_title %}
{% inventree_title %} | {% trans "Stock Item" %} - {{ item }}
@ -429,7 +430,7 @@ $("#stock-serialize").click(function() {
part: {{ item.part.pk }},
reload: true,
data: {
quantity: {{ item.quantity }},
quantity: {{ item.quantity|unlocalize }},
{% if item.location %}
destination: {{ item.location.pk }},
{% elif item.part.default_location %}

View File

@ -1241,7 +1241,7 @@ class StockItemCreate(AjaxCreateView):
if len(sn) > 0:
try:
serials = extract_serial_numbers(sn, quantity)
serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt())
except ValidationError as e:
serials = None
form.add_error('serial_numbers', e.messages)
@ -1283,7 +1283,7 @@ class StockItemCreate(AjaxCreateView):
# Create a single stock item for each provided serial number
if len(sn) > 0:
serials = extract_serial_numbers(sn, quantity)
serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt())
for serial in serials:
item = StockItem(

View File

@ -1,7 +1,10 @@
{% load i18n %}
{% load plugin_extras %}
<h4>{% trans "Settings" %}</h4>
<div class='panel-heading'>
<h4>{% trans "Settings" %}</h4>
</div>
{% plugin_globalsettings plugin_key as plugin_settings %}
<table class='table table-striped table-condensed'>

View File

@ -1,7 +1,9 @@
{% load i18n %}
{% load inventree_extras %}
<h4>{% trans "URLs" %}</h4>
<div class='panel-heading'>
<h4>{% trans "URLs" %}</h4>
</div>
{% define plugin.base_url as base %}
<p>{% blocktrans %}The Base-URL for this plugin is <a href="/{{ base }}" target="_blank"><strong>{{ base }}</strong></a>.{% endblocktrans %}</p>
@ -18,7 +20,7 @@
<tr>
<td>{{key}}</td>
<td>{{entry.1}}</td>
<td><a class="btn btn-primary btn-small" href="/{{ base }}{{entry.1}}" target="_blank">{% trans 'open in new tab' %}</a></td>
<td><a class="btn btn-primary btn-small" href="/{{ base }}{{entry.1}}" target="_blank">{% trans 'Open in new tab' %}</a></td>
</tr>
{% endif %}{% endfor %}
</tbody>

View File

@ -70,7 +70,7 @@
{% if mixin_list %}
{% for mixin in mixin_list %}
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
<span class='badge bg-dark badge-right'>{% blocktrans with name=mixin.human_name %}has {{name}}{% endblocktrans %}</span>
<span class='badge bg-dark badge-right'>{{ mixin.human_name }}</span>
</a>
{% endfor %}
{% endif %}

View File

@ -67,7 +67,10 @@
</div>
{% if plugin.is_package == False %}
<p>{% trans 'The code information is pulled from the latest git commit for this plugin. It might not reflect official version numbers or information but the actual code running.' %}</p>
<div class='alert alert-block alert-info'>
{% trans 'The code information is pulled from the latest git commit for this plugin. It might not reflect official version numbers or information but the actual code running.' %}
</div>
{% endif %}
</div>
<div class="col">
@ -124,8 +127,8 @@
</div>
</div>
{% mixin_enabled plugin 'globalsettings' as globalsettings %}
{% if globalsettings %}
{% mixin_enabled plugin 'settings' as settings %}
{% if settings %}
{% include 'InvenTree/settings/mixins/settings.html' %}
{% endif %}

View File

@ -30,9 +30,11 @@ ENV INVENTREE_MNG_DIR="${INVENTREE_HOME}/InvenTree"
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_PLUGIN_DIR="${INVENTREE_DATA_DIR}/plugins"
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DATA_DIR}/plugins.txt"
# Worker configuration (can be altered by user)
ENV INVENTREE_GUNICORN_WORKERS="4"
@ -59,6 +61,7 @@ RUN apk -U upgrade
# Install required system packages
RUN apk add --no-cache git make bash \
gcc libgcc g++ libstdc++ \
gnupg \
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
libffi libffi-dev \
zlib zlib-dev \
@ -128,8 +131,12 @@ ENV INVENTREE_PY_ENV="${INVENTREE_DEV_DIR}/env"
# Override default path settings
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DEV_DIR}/static"
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DEV_DIR}/media"
ENV INVENTREE_PLUGIN_DIR="${INVENTREE_DEV_DIR}/plugins"
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DEV_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DEV_DIR}/secret_key.txt"
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DEV_DIR}/plugins.txt"
WORKDIR ${INVENTREE_HOME}

View File

@ -71,17 +71,32 @@ def manage(c, cmd, pty=False):
cmd=cmd
), pty=pty)
@task
def plugins(c):
"""
Installs all plugins as specified in 'plugins.txt'
"""
from InvenTree.InvenTree.config import get_plugin_file
plugin_file = get_plugin_file()
print(f"Installing plugin packages from '{plugin_file}'")
# Install the plugins
c.run(f"pip3 install -U -r '{plugin_file}'")
@task(post=[plugins])
def install(c):
"""
Installs required python packages
"""
print("Installing required python packages from 'requirements.txt'")
# Install required Python packages with PIP
c.run('pip3 install -U -r requirements.txt')
@task
def shell(c):
"""