mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
commit
978018e284
17
.github/workflows/welcome.yml
vendored
Normal file
17
.github/workflows/welcome.yml
vendored
Normal 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
1
.gitignore
vendored
@ -49,6 +49,7 @@ static_i18n
|
|||||||
|
|
||||||
# Local config file
|
# Local config file
|
||||||
config.yaml
|
config.yaml
|
||||||
|
plugins.txt
|
||||||
|
|
||||||
# Default data file
|
# Default data file
|
||||||
data.json
|
data.json
|
||||||
|
90
InvenTree/InvenTree/config.py
Normal file
90
InvenTree/InvenTree/config.py
Normal 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
|
@ -404,21 +404,28 @@ def DownloadFile(data, filename, content_type='application/text', inline=False):
|
|||||||
return response
|
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.
|
""" Attempt to extract serial numbers from an input string.
|
||||||
- Serial numbers must be integer values
|
- Serial numbers must be integer values
|
||||||
- Serial numbers must be positive
|
- Serial numbers must be positive
|
||||||
- Serial numbers can be split by whitespace / newline / commma chars
|
- 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 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>+ for getting all expecteded numbers starting from <start>
|
||||||
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
serials: input string with patterns
|
||||||
expected_quantity: The number of (unique) serial numbers we expect
|
expected_quantity: The number of (unique) serial numbers we expect
|
||||||
|
next_number(int): the next possible serial number
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serials = serials.strip()
|
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)
|
groups = re.split("[\s,]+", serials)
|
||||||
|
|
||||||
numbers = []
|
numbers = []
|
||||||
@ -493,11 +500,20 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
continue
|
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:
|
else:
|
||||||
if group in numbers:
|
raise ValidationError(_(f"Invalid/no group {group}"))
|
||||||
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
|
||||||
else:
|
|
||||||
numbers.append(group)
|
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
@ -17,7 +17,6 @@ import os
|
|||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import string
|
import string
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@ -28,30 +27,12 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
import django.conf.locale
|
import django.conf.locale
|
||||||
|
|
||||||
|
from .config import get_base_dir, get_config_file, get_plugin_file, get_setting
|
||||||
|
|
||||||
|
|
||||||
def _is_true(x):
|
def _is_true(x):
|
||||||
# Shortcut function to determine if a value "looks" like a boolean
|
# Shortcut function to determine if a value "looks" like a boolean
|
||||||
return str(x).lower() in ['1', 'y', 'yes', 't', 'true']
|
return str(x).strip().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
|
|
||||||
|
|
||||||
|
|
||||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
# 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'
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# 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.
|
cfg_filename = get_config_file()
|
||||||
# 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}")
|
|
||||||
|
|
||||||
with open(cfg_filename, 'r') as cfg:
|
with open(cfg_filename, 'r') as cfg:
|
||||||
CONFIG = yaml.safe_load(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
|
# We will place any config files in the same directory as the config file
|
||||||
config_dir = os.path.dirname(cfg_filename)
|
config_dir = os.path.dirname(cfg_filename)
|
||||||
|
|
||||||
|
PLUGIN_FILE = get_plugin_file()
|
||||||
|
|
||||||
# Default action is to run the system in Debug mode
|
# Default action is to run the system in Debug mode
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = _is_true(get_setting(
|
DEBUG = _is_true(get_setting(
|
||||||
@ -908,8 +873,7 @@ MARKDOWNIFY_BLEACH = False
|
|||||||
# Maintenance mode
|
# Maintenance mode
|
||||||
MAINTENANCE_MODE_RETRY_AFTER = 60
|
MAINTENANCE_MODE_RETRY_AFTER = 60
|
||||||
|
|
||||||
|
# Plugin Directories (local plugins will be loaded from these directories)
|
||||||
# Plugins
|
|
||||||
PLUGIN_DIRS = ['plugin.builtin', ]
|
PLUGIN_DIRS = ['plugin.builtin', ]
|
||||||
|
|
||||||
if not TESTING:
|
if not TESTING:
|
||||||
|
@ -237,25 +237,41 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
|
|
||||||
e = helpers.extract_serial_numbers
|
e = helpers.extract_serial_numbers
|
||||||
|
|
||||||
sn = e("1-5", 5)
|
sn = e("1-5", 5, 1)
|
||||||
self.assertEqual(len(sn), 5)
|
self.assertEqual(len(sn), 5, 1)
|
||||||
for i in range(1, 6):
|
for i in range(1, 6):
|
||||||
self.assertIn(i, sn)
|
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)
|
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(3, sn)
|
||||||
self.assertIn(13, sn)
|
self.assertIn(13, sn)
|
||||||
|
|
||||||
sn = e("1+", 10)
|
sn = e("1+", 10, 1)
|
||||||
self.assertEqual(len(sn), 10)
|
self.assertEqual(len(sn), 10)
|
||||||
self.assertEqual(sn, [_ for _ in range(1, 11)])
|
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(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):
|
def test_failures(self):
|
||||||
|
|
||||||
@ -263,26 +279,45 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
|
|
||||||
# Test duplicates
|
# Test duplicates
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("1,2,3,3,3", 5)
|
e("1,2,3,3,3", 5, 1)
|
||||||
|
|
||||||
# Test invalid length
|
# Test invalid length
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("1,2,3", 5)
|
e("1,2,3", 5, 1)
|
||||||
|
|
||||||
# Test empty string
|
# Test empty string
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e(", , ,", 0)
|
e(", , ,", 0, 1)
|
||||||
|
|
||||||
# Test incorrect sign in group
|
# Test incorrect sign in group
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("10-2", 8)
|
e("10-2", 8, 1)
|
||||||
|
|
||||||
# Test invalid group
|
# Test invalid group
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("1-5-10", 10)
|
e("1-5-10", 10, 1)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
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):
|
class TestVersionNumber(TestCase):
|
||||||
|
@ -109,7 +109,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
# Check that the serial numbers are valid
|
# Check that the serial numbers are valid
|
||||||
if serials:
|
if serials:
|
||||||
try:
|
try:
|
||||||
extracted = extract_serial_numbers(serials, quantity)
|
extracted = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
|
||||||
|
|
||||||
if extracted:
|
if extracted:
|
||||||
# Check for conflicting serial numbers
|
# Check for conflicting serial numbers
|
||||||
@ -143,7 +143,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
serials = data.get('serial_numbers', None)
|
serials = data.get('serial_numbers', None)
|
||||||
|
|
||||||
if serials:
|
if serials:
|
||||||
serial_numbers = extract_serial_numbers(serials, quantity)
|
serial_numbers = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
|
||||||
else:
|
else:
|
||||||
serial_numbers = None
|
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
@ -861,7 +861,7 @@ class SOSerialAllocationSerializer(serializers.Serializer):
|
|||||||
part = line_item.part
|
part = line_item.part
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data['serials'] = extract_serial_numbers(serial_numbers, quantity)
|
data['serials'] = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
|
||||||
except DjangoValidationError as e:
|
except DjangoValidationError as e:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'serial_numbers': e.messages,
|
'serial_numbers': e.messages,
|
||||||
|
@ -594,6 +594,26 @@ class Part(MPTTModel):
|
|||||||
# No serial numbers found
|
# No serial numbers found
|
||||||
return None
|
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):
|
def getSerialNumberString(self, quantity=1):
|
||||||
"""
|
"""
|
||||||
Return a formatted string representing the next available serial numbers,
|
Return a formatted string representing the next available serial numbers,
|
||||||
|
@ -1,44 +1,45 @@
|
|||||||
"""default mixins for IntegrationMixins"""
|
"""
|
||||||
|
Plugin mixin classes
|
||||||
|
"""
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from plugin.urls import PLUGIN_BASE
|
from plugin.urls import PLUGIN_BASE
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsMixin:
|
class SettingsMixin:
|
||||||
"""Mixin that enables global settings for the plugin"""
|
"""
|
||||||
|
Mixin that enables global settings for the plugin
|
||||||
|
"""
|
||||||
|
|
||||||
class MixinMeta:
|
class MixinMeta:
|
||||||
"""meta options for this mixin"""
|
MIXIN_NAME = 'Settings'
|
||||||
MIXIN_NAME = 'Global settings'
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin('globalsettings', 'has_globalsettings', __class__)
|
self.add_mixin('settings', 'has_globalsettings', __class__)
|
||||||
self.globalsettings = self.setup_globalsettings()
|
self.globalsettings = getattr(self, 'SETTINGS', None)
|
||||||
|
|
||||||
def setup_globalsettings(self):
|
|
||||||
"""
|
|
||||||
setup global settings for this plugin
|
|
||||||
"""
|
|
||||||
return getattr(self, 'GLOBALSETTINGS', None)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_globalsettings(self):
|
def has_globalsettings(self):
|
||||||
"""
|
"""
|
||||||
does this plugin use custom global settings
|
Does this plugin use custom global settings
|
||||||
"""
|
"""
|
||||||
return bool(self.globalsettings)
|
return bool(self.globalsettings)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def globalsettingspatterns(self):
|
def globalsettingspatterns(self):
|
||||||
"""
|
"""
|
||||||
get patterns for InvenTreeSetting defintion
|
Get patterns for InvenTreeSetting defintion
|
||||||
"""
|
"""
|
||||||
if self.has_globalsettings:
|
if self.has_globalsettings:
|
||||||
return {f'PLUGIN_{self.slug.upper()}_{key}': value for key, value in self.globalsettings.items()}
|
return {f'PLUGIN_{self.slug.upper()}_{key}': value for key, value in self.globalsettings.items()}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _globalsetting_name(self, key):
|
def _globalsetting_name(self, key):
|
||||||
"""get global name of setting"""
|
"""
|
||||||
|
Get global name of setting
|
||||||
|
"""
|
||||||
return f'PLUGIN_{self.slug.upper()}_{key}'
|
return f'PLUGIN_{self.slug.upper()}_{key}'
|
||||||
|
|
||||||
def get_globalsetting(self, key):
|
def get_globalsetting(self, key):
|
||||||
@ -57,9 +58,11 @@ class GlobalSettingsMixin:
|
|||||||
|
|
||||||
|
|
||||||
class UrlsMixin:
|
class UrlsMixin:
|
||||||
"""Mixin that enables urls for the plugin"""
|
"""
|
||||||
|
Mixin that enables custom URLs for the plugin
|
||||||
|
"""
|
||||||
|
|
||||||
class MixinMeta:
|
class MixinMeta:
|
||||||
"""meta options for this mixin"""
|
|
||||||
MIXIN_NAME = 'URLs'
|
MIXIN_NAME = 'URLs'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -105,7 +108,10 @@ class UrlsMixin:
|
|||||||
|
|
||||||
|
|
||||||
class NavigationMixin:
|
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_NAME = None
|
||||||
NAVIGATION_TAB_ICON = "fas fa-question"
|
NAVIGATION_TAB_ICON = "fas fa-question"
|
||||||
|
|
||||||
@ -152,7 +158,10 @@ class NavigationMixin:
|
|||||||
|
|
||||||
|
|
||||||
class AppMixin:
|
class AppMixin:
|
||||||
"""Mixin that enables full django app functions for a plugin"""
|
"""
|
||||||
|
Mixin that enables full django app functions for a plugin
|
||||||
|
"""
|
||||||
|
|
||||||
class MixinMeta:
|
class MixinMeta:
|
||||||
"""meta options for this mixin"""
|
"""meta options for this mixin"""
|
||||||
MIXIN_NAME = 'App registration'
|
MIXIN_NAME = 'App registration'
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
"""utility class to enable simpler imports"""
|
"""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__ = [
|
__all__ = [
|
||||||
'AppMixin', 'GlobalSettingsMixin', 'UrlsMixin', 'NavigationMixin',
|
'AppMixin',
|
||||||
|
'NavigationMixin',
|
||||||
|
'SettingsMixin',
|
||||||
|
'UrlsMixin',
|
||||||
]
|
]
|
||||||
|
@ -54,7 +54,11 @@ class PluginConfig(models.Model):
|
|||||||
|
|
||||||
# extra attributes from the registry
|
# extra attributes from the registry
|
||||||
def mixins(self):
|
def mixins(self):
|
||||||
|
|
||||||
|
try:
|
||||||
return self.plugin._mixinreg
|
return self.plugin._mixinreg
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
# functions
|
# functions
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ class Plugins:
|
|||||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
|
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
|
||||||
logger.info('Registering IntegrationPlugin global settings')
|
logger.info('Registering IntegrationPlugin global settings')
|
||||||
for slug, plugin in plugins:
|
for slug, plugin in plugins:
|
||||||
if plugin.mixin_enabled('globalsettings'):
|
if plugin.mixin_enabled('settings'):
|
||||||
plugin_setting = plugin.globalsettingspatterns
|
plugin_setting = plugin.globalsettingspatterns
|
||||||
self.mixins_globalsettings[slug] = plugin_setting
|
self.mixins_globalsettings[slug] = plugin_setting
|
||||||
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
"""sample implementations for IntegrationPlugin"""
|
"""
|
||||||
|
Sample implementations for IntegrationPlugin
|
||||||
|
"""
|
||||||
|
|
||||||
from plugin import IntegrationPluginBase
|
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.http import HttpResponse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf.urls import url, include
|
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"
|
PLUGIN_NAME = "SampleIntegrationPlugin"
|
||||||
|
@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from plugin import IntegrationPluginBase
|
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
|
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)
|
self.assertEqual(self.mixin.registered_mixins[0]['human_name'], self.MIXIN_HUMAN_NAME)
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsMixinTest(BaseMixinDefinition, TestCase):
|
class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
||||||
MIXIN_HUMAN_NAME = 'Global settings'
|
MIXIN_HUMAN_NAME = 'Settings'
|
||||||
MIXIN_NAME = 'globalsettings'
|
MIXIN_NAME = 'settings'
|
||||||
MIXIN_ENABLE_CHECK = 'has_globalsettings'
|
MIXIN_ENABLE_CHECK = 'has_globalsettings'
|
||||||
|
|
||||||
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class SettingsCls(GlobalSettingsMixin, IntegrationPluginBase):
|
class SettingsCls(SettingsMixin, IntegrationPluginBase):
|
||||||
GLOBALSETTINGS = self.TEST_SETTINGS
|
SETTINGS = self.TEST_SETTINGS
|
||||||
self.mixin = SettingsCls()
|
self.mixin = SettingsCls()
|
||||||
|
|
||||||
class NoSettingsCls(GlobalSettingsMixin, IntegrationPluginBase):
|
class NoSettingsCls(SettingsMixin, IntegrationPluginBase):
|
||||||
pass
|
pass
|
||||||
self.mixin_nothing = NoSettingsCls()
|
self.mixin_nothing = NoSettingsCls()
|
||||||
|
|
||||||
|
@ -480,18 +480,6 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
notes = data.get('notes', '')
|
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():
|
with transaction.atomic():
|
||||||
|
|
||||||
# Create an initial stock item
|
# Create an initial stock item
|
||||||
@ -507,6 +495,19 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if item.part.default_expiry > 0:
|
if item.part.default_expiry > 0:
|
||||||
item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
|
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)
|
# Finally, save the item (with user information)
|
||||||
item.save(user=user)
|
item.save(user=user)
|
||||||
|
|
||||||
|
@ -350,7 +350,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
|||||||
serial_numbers = data['serial_numbers']
|
serial_numbers = data['serial_numbers']
|
||||||
|
|
||||||
try:
|
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:
|
except DjangoValidationError as e:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'serial_numbers': e.messages,
|
'serial_numbers': e.messages,
|
||||||
@ -379,6 +379,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
|||||||
serials = InvenTree.helpers.extract_serial_numbers(
|
serials = InvenTree.helpers.extract_serial_numbers(
|
||||||
data['serial_numbers'],
|
data['serial_numbers'],
|
||||||
data['quantity'],
|
data['quantity'],
|
||||||
|
item.part.getLatestSerialNumberInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
item.serializeStock(
|
item.serializeStock(
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% inventree_title %} | {% trans "Stock Item" %} - {{ item }}
|
{% inventree_title %} | {% trans "Stock Item" %} - {{ item }}
|
||||||
@ -429,7 +430,7 @@ $("#stock-serialize").click(function() {
|
|||||||
part: {{ item.part.pk }},
|
part: {{ item.part.pk }},
|
||||||
reload: true,
|
reload: true,
|
||||||
data: {
|
data: {
|
||||||
quantity: {{ item.quantity }},
|
quantity: {{ item.quantity|unlocalize }},
|
||||||
{% if item.location %}
|
{% if item.location %}
|
||||||
destination: {{ item.location.pk }},
|
destination: {{ item.location.pk }},
|
||||||
{% elif item.part.default_location %}
|
{% elif item.part.default_location %}
|
||||||
|
@ -1241,7 +1241,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
if len(sn) > 0:
|
if len(sn) > 0:
|
||||||
try:
|
try:
|
||||||
serials = extract_serial_numbers(sn, quantity)
|
serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt())
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
serials = None
|
serials = None
|
||||||
form.add_error('serial_numbers', e.messages)
|
form.add_error('serial_numbers', e.messages)
|
||||||
@ -1283,7 +1283,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
# Create a single stock item for each provided serial number
|
# Create a single stock item for each provided serial number
|
||||||
if len(sn) > 0:
|
if len(sn) > 0:
|
||||||
serials = extract_serial_numbers(sn, quantity)
|
serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt())
|
||||||
|
|
||||||
for serial in serials:
|
for serial in serials:
|
||||||
item = StockItem(
|
item = StockItem(
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load plugin_extras %}
|
{% load plugin_extras %}
|
||||||
|
|
||||||
<h4>{% trans "Settings" %}</h4>
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "Settings" %}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% plugin_globalsettings plugin_key as plugin_settings %}
|
{% plugin_globalsettings plugin_key as plugin_settings %}
|
||||||
|
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
<h4>{% trans "URLs" %}</h4>
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "URLs" %}</h4>
|
||||||
|
</div>
|
||||||
{% define plugin.base_url as base %}
|
{% 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>
|
<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>
|
<tr>
|
||||||
<td>{{key}}</td>
|
<td>{{key}}</td>
|
||||||
<td>{{entry.1}}</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>
|
</tr>
|
||||||
{% endif %}{% endfor %}
|
{% endif %}{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
{% if mixin_list %}
|
{% if mixin_list %}
|
||||||
{% for mixin in mixin_list %}
|
{% for mixin in mixin_list %}
|
||||||
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
<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>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -67,7 +67,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if plugin.is_package == False %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@ -124,8 +127,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% mixin_enabled plugin 'globalsettings' as globalsettings %}
|
{% mixin_enabled plugin 'settings' as settings %}
|
||||||
{% if globalsettings %}
|
{% if settings %}
|
||||||
{% include 'InvenTree/settings/mixins/settings.html' %}
|
{% include 'InvenTree/settings/mixins/settings.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -30,9 +30,11 @@ ENV INVENTREE_MNG_DIR="${INVENTREE_HOME}/InvenTree"
|
|||||||
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
||||||
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
|
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
|
||||||
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
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_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
||||||
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
|
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)
|
# Worker configuration (can be altered by user)
|
||||||
ENV INVENTREE_GUNICORN_WORKERS="4"
|
ENV INVENTREE_GUNICORN_WORKERS="4"
|
||||||
@ -59,6 +61,7 @@ RUN apk -U upgrade
|
|||||||
# Install required system packages
|
# Install required system packages
|
||||||
RUN apk add --no-cache git make bash \
|
RUN apk add --no-cache git make bash \
|
||||||
gcc libgcc g++ libstdc++ \
|
gcc libgcc g++ libstdc++ \
|
||||||
|
gnupg \
|
||||||
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
|
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
|
||||||
libffi libffi-dev \
|
libffi libffi-dev \
|
||||||
zlib zlib-dev \
|
zlib zlib-dev \
|
||||||
@ -128,8 +131,12 @@ ENV INVENTREE_PY_ENV="${INVENTREE_DEV_DIR}/env"
|
|||||||
# Override default path settings
|
# Override default path settings
|
||||||
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DEV_DIR}/static"
|
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DEV_DIR}/static"
|
||||||
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DEV_DIR}/media"
|
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_CONFIG_FILE="${INVENTREE_DEV_DIR}/config.yaml"
|
||||||
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DEV_DIR}/secret_key.txt"
|
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DEV_DIR}/secret_key.txt"
|
||||||
|
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DEV_DIR}/plugins.txt"
|
||||||
|
|
||||||
|
|
||||||
WORKDIR ${INVENTREE_HOME}
|
WORKDIR ${INVENTREE_HOME}
|
||||||
|
|
||||||
|
19
tasks.py
19
tasks.py
@ -71,17 +71,32 @@ def manage(c, cmd, pty=False):
|
|||||||
cmd=cmd
|
cmd=cmd
|
||||||
), pty=pty)
|
), pty=pty)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@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):
|
def install(c):
|
||||||
"""
|
"""
|
||||||
Installs required python packages
|
Installs required python packages
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
print("Installing required python packages from 'requirements.txt'")
|
||||||
|
|
||||||
# Install required Python packages with PIP
|
# Install required Python packages with PIP
|
||||||
c.run('pip3 install -U -r requirements.txt')
|
c.run('pip3 install -U -r requirements.txt')
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def shell(c):
|
def shell(c):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user