Compare commits

...

29 Commits

Author SHA1 Message Date
Oliver
ca1fbf9ff0 Prevent name check on null attachment file (#3819)
(cherry picked from commit c4ed1e23a01f278d696c2853337bdde0a682c6c5)
(cherry picked from commit 8c2d89de20)
2022-10-20 21:53:25 +11:00
Oliver
531c39725c Fix for allowing plugins without explicit metadata (#3769) 2022-10-12 00:16:12 +11:00
Oliver
cfabf67b67 Settings panel bug fix (#3761) 2022-10-09 09:08:00 +11:00
Oliver
0dc6f79647 Allow auto-loading of plugins in certain conditions (#3763)
Ref: 52af196694
(cherry picked from commit 27937eeddd)
2022-10-09 09:07:11 +11:00
Oliver
5c1b4a5ab1 Bug fix for stock adjustment actions (#3735) (#3736)
* Check for empty strings as well as null values

* JS linting

(cherry picked from commit 1f2859d8c9)
2022-09-30 16:00:03 +10:00
Oliver
39f7fbb332 Add typecasting to certain settings (#3726) (#3728)
* [FR] Add typecasting to certain settings
Fixes #3725

Add typecasting

* Add types to:
- INVENTREE_CACHE_PORT
- INVENTREE_EMAIL_PORT
- INVENTREE_LOGIN_CONFIRM_DAYS
- INVENTREE_LOGIN_ATTEMPTS

* cast DB_PORT to int

* Add logging statements

(cherry picked from commit dce10072ef)

Co-authored-by: Matthias Mair <code@mjmair.com>
2022-09-28 09:39:07 +10:00
Oliver
30d5f93821 Bump version number to 0.8.4 (#3713) 2022-09-24 13:01:38 +10:00
Matthias Mair
f9d2b149c6 Add sanitation for SVG file uploads 2022-09-23 17:05:02 +10:00
Oliver
c4f1d8b345 Bug fix for path string generation (#3696) 2022-09-20 19:39:39 +10:00
Oliver
c4c3cdcb31 Adds callback for clipboard button (#3678) (#3679)
(cherry picked from commit 7645492cc2)
2022-09-14 17:50:15 +10:00
Oliver
66d6896925 Fix purchase order report template (#3674) (#3677)
(cherry picked from commit 6e36ae5f74)
2022-09-14 16:48:05 +10:00
Oliver
6c8226cee6 Fix report permissions (#3642) (#3643)
- Add report model permissions to correct role groups

(cherry picked from commit d7a8d6dd5e)
2022-09-05 14:55:32 +10:00
Oliver
14143c1681 Fix sales order table refresh (#3627) (#3628)
- Refreshing the SalesOrder table would actually reload the PurchaseOrder table

(cherry picked from commit 23edd79431)
2022-09-01 15:04:26 +10:00
Oliver
c7955c267c Bump version number to 0.8.3 (#3614) 2022-08-28 08:55:44 +10:00
Oliver
62f44d06bf Add missing 'remove stock' action (#3610) (#3611)
* Add missing 'remove stock' action

* Add some unit tests for the stock item view

(cherry picked from commit 993f36c98f)
2022-08-25 21:31:23 +10:00
Oliver
ee5238b13e StockItem page template fix (#3601) (#3604)
- Allow damaged / quarantined items to be adjusted
- Fix template rendering logic

(cherry picked from commit 6adfc91c5c)
2022-08-24 17:17:46 +10:00
Oliver
2db9b28063 Bug fix for loading asset files in reports (#3596) (#3598)
- Pathlib does not play well with SafeString
- Enforce string type' when loading an asset file
- Add unit tests

(cherry picked from commit 12509203d6)
2022-08-24 12:01:37 +10:00
Oliver
30beb12c81 Fix dimensions for label templates (#3578) (#3580)
- Disable localization in certain areas
- Different localization settings could mess with label generation

(cherry picked from commit c8de2efd9d)
2022-08-19 12:35:24 +10:00
Oliver
644bcb263f fix: invalid chas in cache key (#3574) (#3577)
(cherry picked from commit efafa3960b)

Co-authored-by: wolflu05 <76838159+wolflu05@users.noreply.github.com>
2022-08-19 11:35:49 +10:00
Oliver
2ae1d1c663 fix typo in variable name (#3541) (#3543)
this fixes broken e-mail from configuration.

(cherry picked from commit d102a87eff)

Co-authored-by: Jacob Siverskog <jacob@teenage.engineering>
2022-08-16 13:08:33 +10:00
Oliver
951225b420 Fix bug in exporting records (#3545) (#3552)
Introduced in #3392

(cherry picked from commit 858d48afe7)

Co-authored-by: miggland <miggland@users.noreply.github.com>
2022-08-16 13:08:11 +10:00
Oliver
836ec3289d Fix weasyprint version (#3539) (#3540)
* Pin weasyprint version

- Revert to 54.3
- Fixes https://github.com/inventree/InvenTree/issues/3528

* Simplify label printing for multiple pages

* Simplify PDF generation for multiple report outputs

* Add content wrapper div for base label template

- Allows more extensibility

(cherry picked from commit 87e7112326)
2022-08-15 12:22:10 +10:00
Oliver
3482b81e8a Bump version to 0.8.2 (#3504) 2022-08-09 13:38:33 +10:00
Oliver
fb97385b23 load admin first (#3484) (#3492)
(cherry picked from commit 9182f62bc4)
2022-08-08 07:47:27 +10:00
Oliver
276ad572c5 Url field fix (#3488) (#3491)
* Updates for automated metadata extraction

* Update link field for StockItem model

- Increase max_length to 200 characters
- Custom migration
- Updates for InvenTreeUrlField model

* Adding unit tests

* Bug fix for metadata.py

(cherry picked from commit 63d221854b)
2022-08-08 07:47:17 +10:00
Oliver
1e7f4dcb4b Improved loading for custom logo (#3489) (#3490)
- First check the 'static' directory
- Second check the 'media' directory (backwards compatibility)
- Third use the default logo

(cherry picked from commit 83b471b4f7)
2022-08-07 23:16:57 +10:00
Oliver
72bec42c0f Depenency updates (#3472) (#3481)
* update requirements

* automate everything

(cherry picked from commit 12a321ed4f)

Co-authored-by: Matthias Mair <code@mjmair.com>
2022-08-06 10:02:44 +10:00
Oliver
8e5202b39a Improvements to version check CI script (#3455) (#3457)
* Improvements to version check CI script

* Fix typo

* Allow duplicate version tags for 'stable' branch

(cherry picked from commit 08d93e0727)
2022-08-02 13:34:08 +10:00
Oliver
63664714ca Return early if themes dir does not exist (#3453)
* Return early if themes dir does not exist

(cherry picked from commit 3e1cdcdb07)

* Bump version number to 0.8.1
2022-08-02 10:38:36 +10:00
44 changed files with 592 additions and 226 deletions

22
.github/workflows/update.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Update dependency files regularly
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup
run: pip install -r requirements-dev.txt
- name: Update requirements.txt
run: pip-compile --output-file=requirements.txt requirements.in -U
- name: Update requirements-dev.txt
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "[Bot] Updated dependency"
branch: dep-update

View File

@@ -58,7 +58,7 @@ def load_config_data() -> map:
return data
def get_setting(env_var=None, config_key=None, default_value=None):
def get_setting(env_var=None, config_key=None, default_value=None, typecast=None):
"""Helper function for retrieving a configuration setting value.
- First preference is to look for the environment variable
@@ -69,15 +69,24 @@ def get_setting(env_var=None, config_key=None, default_value=None):
env_var: Name of the environment variable e.g. 'INVENTREE_STATIC_ROOT'
config_key: Key to lookup in the configuration file
default_value: Value to return if first two options are not provided
typecast: Function to use for typecasting the value
"""
def try_typecasting(value):
"""Attempt to typecast the value"""
if typecast is not None:
# Try to typecast the value
try:
return typecast(value)
except Exception as error:
logger.error(f"Failed to typecast '{env_var}' with value '{value}' to type '{typecast}' with error {error}")
return value
# First, try to load from the environment variables
if env_var is not None:
val = os.getenv(env_var, None)
if val is not None:
return val
return try_typecasting(val)
# Next, try to load from configuration file
if config_key is not None:
@@ -96,10 +105,10 @@ def get_setting(env_var=None, config_key=None, default_value=None):
cfg_data = cfg_data[key]
if result is not None:
return result
return try_typecasting(result)
# Finally, return the default value
return default_value
return try_typecasting(default_value)
def get_boolean_setting(env_var=None, config_key=None, default_value=False):

View File

@@ -6,7 +6,6 @@ from decimal import Decimal
from django import forms
from django.core import validators
from django.db import models as models
from django.forms.fields import URLField as FormURLField
from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField
@@ -23,26 +22,28 @@ class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs):
"""Update schemes."""
# Enforce 'max length' parameter in form validation
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
self.validators[-1].schemes = allowable_url_schemes()
class InvenTreeURLFormField(FormURLField):
"""Custom URL form field with custom scheme validators."""
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
class InvenTreeURLField(models.URLField):
"""Custom URL field which has custom scheme validators."""
validators = [validators.URLValidator(schemes=allowable_url_schemes())]
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
def formfield(self, **kwargs):
"""Return a Field instance for this field."""
return super().formfield(**{
'form_class': InvenTreeURLFormField
})
def __init__(self, **kwargs):
"""Initialization method for InvenTreeURLField"""
# Max length for InvenTreeURLField defaults to 200
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
def money_kwargs():

View File

@@ -12,6 +12,7 @@ from wsgiref.util import FileWrapper
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage
from django.core.validators import URLValidator
@@ -64,28 +65,10 @@ def constructPathString(path, max_chars=250):
pathstring = '/'.join(path)
idx = 0
# Replace middle elements to limit the pathstring
if len(pathstring) > max_chars:
mid = len(path) // 2
path_l = path[0:mid]
path_r = path[mid:]
# Ensure the pathstring length is limited
while len(pathstring) > max_chars:
# Remove an element from the list
if idx % 2 == 0:
path_l = path_l[:-1]
else:
path_r = path_r[1:]
subpath = path_l + ['...'] + path_r
pathstring = '/'.join(subpath)
idx += 1
n = int(max_chars / 2 - 2)
pathstring = pathstring[:n] + "..." + pathstring[-n:]
return pathstring
@@ -241,17 +224,27 @@ def getLogoImage(as_file=False, custom=True):
"""Return the path to the logo-file."""
if custom and settings.CUSTOM_LOGO:
if as_file:
return f"file://{default_storage.path(settings.CUSTOM_LOGO)}"
else:
return default_storage.url(settings.CUSTOM_LOGO)
static_storage = StaticFilesStorage()
else:
if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}"
if static_storage.exists(settings.CUSTOM_LOGO):
storage = static_storage
elif default_storage.exists(settings.CUSTOM_LOGO):
storage = default_storage
else:
return getStaticUrl('img/inventree.png')
storage = None
if storage is not None:
if as_file:
return f"file://{storage.path(settings.CUSTOM_LOGO)}"
else:
return storage.url(settings.CUSTOM_LOGO)
# If we have got to this point, return the default logo
if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}"
else:
return getStaticUrl('img/inventree.png')
def TestIfImageURL(url):

View File

@@ -116,6 +116,12 @@ class InvenTreeMetadata(SimpleMetadata):
model_class = None
# Attributes to copy extra attributes from the model to the field (if they don't exist)
extra_attributes = [
'help_text',
'max_length',
]
try:
model_class = serializer.Meta.model
@@ -148,10 +154,7 @@ class InvenTreeMetadata(SimpleMetadata):
elif name in model_default_values:
serializer_info[name]['default'] = model_default_values[name]
# Attributes to copy from the model to the field (if they don't exist)
attributes = ['help_text']
for attr in attributes:
for attr in extra_attributes:
if attr not in serializer_info[name]:
if hasattr(field, attr):
@@ -172,8 +175,9 @@ class InvenTreeMetadata(SimpleMetadata):
# This is used to automatically filter AJAX requests
serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to()
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
serializer_info[name]['help_text'] = relation.model_field.help_text
for attr in extra_attributes:
if attr not in serializer_info[name] and hasattr(relation.model_field, attr):
serializer_info[name][attr] = getattr(relation.model_field, attr)
if name in model_default_values:
serializer_info[name]['default'] = model_default_values[name]

View File

@@ -4,6 +4,7 @@ import logging
import os
import re
from datetime import datetime
from io import BytesIO
from django.conf import settings
from django.contrib.auth import get_user_model
@@ -24,6 +25,7 @@ import InvenTree.format
import InvenTree.helpers
from common.models import InvenTreeSetting
from InvenTree.fields import InvenTreeURLField
from InvenTree.sanitizer import sanitize_svg
logger = logging.getLogger('inventree')
@@ -383,8 +385,16 @@ class InvenTreeAttachment(models.Model):
'link': _('Missing external link'),
})
if self.attachment and self.attachment.name.lower().endswith('.svg'):
self.attachment.file.file = self.clean_svg(self.attachment)
super().save(*args, **kwargs)
def clean_svg(self, field):
"""Sanitize SVG file before saving."""
cleaned = sanitize_svg(field.file.read())
return BytesIO(bytes(cleaned, 'utf8'))
def __str__(self):
"""Human name for attachment."""
if self.attachment is not None:
@@ -516,8 +526,18 @@ class InvenTreeTree(MPTTModel):
)
if pathstring != self.pathstring:
if 'force_insert' in kwargs:
del kwargs['force_insert']
kwargs['force_update'] = True
self.pathstring = pathstring
super().save(force_update=True)
super().save(*args, **kwargs)
# Ensure that the pathstring changes are propagated down the tree also
for child in self.get_children():
child.save(*args, **kwargs)
class Meta:
"""Metaclass defines extra model properties."""

View File

@@ -13,7 +13,7 @@ def isImportingData():
return 'loaddata' in sys.argv
def canAppAccessDatabase(allow_test=False):
def canAppAccessDatabase(allow_test: bool = False, allow_plugins: bool = False):
"""Returns True if the apps.py file can access database records.
There are some circumstances where we don't want the ready function in apps.py
@@ -25,8 +25,6 @@ def canAppAccessDatabase(allow_test=False):
'flush',
'loaddata',
'dumpdata',
'makemigrations',
'migrate',
'check',
'shell',
'createsuperuser',
@@ -43,6 +41,12 @@ def canAppAccessDatabase(allow_test=False):
# Override for testing mode?
excluded_commands.append('test')
if not allow_plugins:
excluded_commands.extend([
'makemigrations',
'migrate',
])
for cmd in excluded_commands:
if cmd in sys.argv:
return False

View File

@@ -0,0 +1,67 @@
"""Functions to sanitize user input files."""
from bleach import clean
from bleach.css_sanitizer import CSSSanitizer
ALLOWED_ELEMENTS_SVG = [
'a', 'animate', 'animateColor', 'animateMotion',
'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'font-face',
'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern',
'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph',
'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect',
'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'
]
ALLOWED_ATTRIBUTES_SVG = [
'accent-height', 'accumulate', 'additive', 'alphabetic',
'arabic-form', 'ascent', 'attributeName', 'attributeType',
'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height',
'class', 'color', 'color-rendering', 'content', 'cx', 'cy', 'd', 'dx',
'dy', 'descent', 'display', 'dur', 'end', 'fill', 'fill-opacity',
'fill-rule', 'font-family', 'font-size', 'font-stretch', 'font-style',
'font-variant', 'font-weight', 'from', 'fx', 'fy', 'g1', 'g2',
'glyph-name', 'gradientUnits', 'hanging', 'height', 'horiz-adv-x',
'horiz-origin-x', 'id', 'ideographic', 'k', 'keyPoints',
'keySplines', 'keyTimes', 'lang', 'marker-end', 'marker-mid',
'marker-start', 'markerHeight', 'markerUnits', 'markerWidth',
'mathematical', 'max', 'min', 'name', 'offset', 'opacity', 'orient',
'origin', 'overline-position', 'overline-thickness', 'panose-1',
'path', 'pathLength', 'points', 'preserveAspectRatio', 'r', 'refX',
'refY', 'repeatCount', 'repeatDur', 'requiredExtensions',
'requiredFeatures', 'restart', 'rotate', 'rx', 'ry', 'slope',
'stemh', 'stemv', 'stop-color', 'stop-opacity',
'strikethrough-position', 'strikethrough-thickness', 'stroke',
'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity',
'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to',
'transform', 'type', 'u1', 'u2', 'underline-position',
'underline-thickness', 'unicode', 'unicode-range', 'units-per-em',
'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x',
'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole',
'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title',
'xlink:type', 'xml:base', 'xml:lang', 'xml:space', 'xmlns',
'xmlns:xlink', 'y', 'y1', 'y2', 'zoomAndPan', 'style'
]
def sanitize_svg(file_data: str, strip: bool = True, elements: str = ALLOWED_ELEMENTS_SVG, attributes: str = ALLOWED_ATTRIBUTES_SVG) -> str:
"""Sanatize a SVG file.
Args:
file_data (str): SVG as string.
strip (bool, optional): Should invalid elements get removed. Defaults to True.
elements (str, optional): Allowed elements. Defaults to ALLOWED_ELEMENTS_SVG.
attributes (str, optional): Allowed attributes. Defaults to ALLOWED_ATTRIBUTES_SVG.
Returns:
str: Sanitzied SVG file.
"""
cleaned = clean(
file_data,
tags=elements,
attributes=attributes,
strip=strip,
strip_comments=strip,
css_sanitizer=CSSSanitizer()
)
return cleaned

View File

@@ -16,6 +16,7 @@ import sys
from pathlib import Path
import django.conf.locale
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.storage import default_storage
from django.http import Http404
from django.utils.translation import gettext_lazy as _
@@ -135,6 +136,8 @@ MEDIA_URL = '/media/'
# Application definition
INSTALLED_APPS = [
# Admin site integration
'django.contrib.admin',
# InvenTree apps
'build.apps.BuildConfig',
@@ -150,7 +153,6 @@ INSTALLED_APPS = [
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'user_sessions', # db user sessions
@@ -346,6 +348,12 @@ for key in db_keys:
env_var = os.environ.get(env_key, None)
if env_var:
# Make use PORT is int
if key == 'PORT':
try:
env_var = int(env_var)
except ValueError:
logger.error(f"Invalid number for {env_key}: {env_var}")
# Override configuration value
db_config[key] = env_var
@@ -503,7 +511,7 @@ DATABASES = {
# Cache configuration
cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None)
cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379')
cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379', typecast=int)
if cache_host: # pragma: no cover
# We are going to rely upon a possibly non-localhost for our cache,
@@ -669,14 +677,14 @@ EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
# Email configuration options
EMAIL_BACKEND = get_setting('INVENTREE_EMAIL_BACKEND', 'email.backend', 'django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = get_setting('INVENTREE_EMAIL_HOST', 'email.host', '')
EMAIL_PORT = int(get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25))
EMAIL_PORT = get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25, typecast=int)
EMAIL_HOST_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '')
EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ')
EMAIL_USE_TLS = get_boolean_setting('INVENTREE_EMAIL_TLS', 'email.tls', False)
EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
DEFUALT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
EMAIL_USE_LOCALTIME = False
EMAIL_TIMEOUT = 60
@@ -718,8 +726,8 @@ SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', [])
SOCIALACCOUNT_STORE_TOKENS = True
# settings for allauth
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3)
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5)
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3, typecast=int)
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5, typecast=int)
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_PREVENT_ENUMERATION = True
@@ -820,10 +828,22 @@ CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None)
# check that the logo-file exsists in media
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO): # pragma: no cover
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage")
CUSTOM_LOGO = False
"""
Check for the existence of a 'custom logo' file:
- Check the 'static' directory
- Check the 'media' directory (legacy)
"""
if CUSTOM_LOGO:
static_storage = StaticFilesStorage()
if static_storage.exists(CUSTOM_LOGO):
logger.info(f"Loading custom logo from static directory: {CUSTOM_LOGO}")
elif default_storage.exists(CUSTOM_LOGO):
logger.info(f"Loading custom logo from media directory: {CUSTOM_LOGO}")
else:
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the static or media directories")
CUSTOM_LOGO = False
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")

View File

@@ -140,6 +140,8 @@ function inventreeDocReady() {
// start watcher
startNotificationWatcher();
attachClipboard('.clip-btn');
// always refresh when the focus returns
$(document).focus(function(){
startNotificationWatcher();

View File

@@ -13,7 +13,7 @@ import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.8.0"
INVENTREE_SW_VERSION = "0.8.4"
def inventreeInstanceName():

View File

@@ -132,7 +132,7 @@ class BaseInvenTreeSetting(models.Model):
for k, v in kwargs.items():
key += f"_{k}:{v}"
return key
return key.replace(" ", "")
@classmethod
def allValues(cls, user=None, exclude_hidden=False):
@@ -1775,12 +1775,13 @@ class ColorTheme(models.Model):
@classmethod
def get_color_themes_choices(cls):
"""Get all color themes from static folder."""
if settings.TESTING and not os.path.exists(settings.STATIC_COLOR_THEMES_DIR):
if not os.path.exists(settings.STATIC_COLOR_THEMES_DIR):
logger.error('Theme directory does not exsist')
return []
# Get files list from css/color-themes/ folder
files_list = []
for file in os.listdir(settings.STATIC_COLOR_THEMES_DIR):
files_list.append(os.path.splitext(file))

View File

@@ -158,16 +158,12 @@ class LabelPrintMixin:
pages = []
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
pdf = outputs[0].get_document().copy(pages).write_pdf()
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user)

View File

@@ -1,10 +1,13 @@
{% load l10n %}
{% load report %}
{% load barcode %}
<head>
<style>
@page {
{% localize off %}
size: {{ width }}mm {{ height }}mm;
{% endlocalize %}
{% block margin %}
margin: 0mm;
{% endblock %}
@@ -15,6 +18,8 @@
margin: 0mm;
color: #000;
background-color: #FFF;
page-break-before: always;
page-break-after: always;
}
img {
@@ -22,14 +27,23 @@
image-rendering: pixelated;
}
.content {
width: 100%;
break-after: always;
position: relative;
}
{% block style %}
/* User-defined styles can go here */
{% endblock %}
</style>
</head>
<body>
{% block content %}
<!-- Label data rendered here! -->
{% endblock %}
<div class='content'>
{% block content %}
<!-- Label data rendered here! -->
{% endblock %}
</div>
</body>

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,15 +9,19 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
.part {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
{% localize off %}
left: {{ height }}mm;
{% endlocalize %}
top: 2mm;
}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,15 +9,19 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
.part {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
{% localize off %}
left: {{ height }}mm;
{% endlocalize %}
top: 2mm;
}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,8 +9,10 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,8 +9,10 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends "label/label_base.html" %}
{% load l10n %}
{% load barcode %}
{% block style %}
@@ -8,15 +9,19 @@
position: fixed;
left: 0mm;
top: 0mm;
{% localize off %}
height: {{ height }}mm;
width: {{ height }}mm;
{% endlocalize %}
}
.loc {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
{% localize off %}
left: {{ height }}mm;
{% endlocalize %}
top: 2mm;
}

View File

@@ -118,7 +118,7 @@ class CategoryTest(TestCase):
self.assertTrue(len(child.path), 26)
self.assertEqual(
child.pathstring,
"Cat/AAAAAAAAAA/BBBBBBBBBB/CCCCCCCCCC/DDDDDDDDDD/EEEEEEEEEE/FFFFFFFFFF/GGGGGGGGGG/HHHHHHHHHH/IIIIIIIIII/JJJJJJJJJJ/.../OOOOOOOOOO/PPPPPPPPPP/QQQQQQQQQQ/RRRRRRRRRR/SSSSSSSSSS/TTTTTTTTTT/UUUUUUUUUU/VVVVVVVVVV/WWWWWWWWWW/XXXXXXXXXX/YYYYYYYYYY/ZZZZZZZZZZ"
"Cat/AAAAAAAAAA/BBBBBBBBBB/CCCCCCCCCC/DDDDDDDDDD/EEEEEEEEEE/FFFFFFFFFF/GGGGGGGGGG/HHHHHHHHHH/IIIIIIIIII/JJJJJJJJJJ/KKKKKKKKK...OO/PPPPPPPPPP/QQQQQQQQQQ/RRRRRRRRRR/SSSSSSSSSS/TTTTTTTTTT/UUUUUUUUUU/VVVVVVVVVV/WWWWWWWWWW/XXXXXXXXXX/YYYYYYYYYY/ZZZZZZZZZZ"
)
self.assertTrue(len(child.pathstring) <= 250)

View File

@@ -27,7 +27,7 @@ class PluginAppConfig(AppConfig):
def ready(self):
"""The ready method is extended to initialize plugins."""
if settings.PLUGINS_ENABLED:
if not canAppAccessDatabase(allow_test=True):
if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
logger.info("Skipping plugin loading sequence") # pragma: no cover
else:
logger.info('Loading InvenTree plugins')

View File

@@ -64,6 +64,7 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st
"""Handles an error and casts it as an IntegrationPluginError."""
package_path = traceback.extract_tb(error.__traceback__)[-1].filename
install_path = sysconfig.get_paths()["purelib"]
try:
package_name = pathlib.Path(package_path).relative_to(install_path).parts[0]
except ValueError:
@@ -88,9 +89,10 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st
log_error({package_name: str(error)}, **log_kwargs)
if do_raise:
# do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample
# do a straight raise if we are playing with environment variables at execution time, ignore the broken sample
if settings.TESTING_ENV and package_name != 'integration.broken_sample' and isinstance(error, IntegrityError):
raise error # pragma: no cover
raise IntegrationPluginError(package_name, str(error))
# endregion

View File

@@ -6,7 +6,7 @@ import os
import pathlib
import warnings
from datetime import datetime
from importlib.metadata import metadata
from importlib.metadata import PackageNotFoundError, metadata
from django.conf import settings
from django.db.utils import OperationalError, ProgrammingError
@@ -294,7 +294,18 @@ class InvenTreePlugin(MixinBase, MetaBase):
@classmethod
def _get_package_metadata(cls):
"""Get package metadata for plugin."""
meta = metadata(cls.__name__)
# Try simple metadata lookup
try:
meta = metadata(cls.__name__)
# Simple lookup did not work - get data from module
except PackageNotFoundError:
try:
meta = metadata(cls.__module__.split('.')[0])
except PackageNotFoundError:
# Not much information we can extract at this point
return {}
return {
'author': meta['Author-email'],

View File

@@ -233,17 +233,12 @@ class ReportPrintMixin:
pages = []
try:
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
pdf = outputs[0].get_document().copy(pages).write_pdf()
except TemplateDoesNotExist as e:

View File

@@ -74,7 +74,7 @@ table td.expand {
<div class='header-right'>
<h3>{% trans "Purchase Order" %} {{ prefix }}{{ reference }}</h3>
{% if supplier %}{{ supplier.name }}{% endif %}{% else %}{% trans "Supplier was deleted" %}{% endif %}
{% if supplier %}{{ supplier.name }}{% else %}{% trans "Supplier was deleted" %}{% endif %}
</div>
{% endblock %}

View File

@@ -5,7 +5,7 @@ import os
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
from django.utils.safestring import SafeString, mark_safe
import InvenTree.helpers
from common.models import InvenTreeSetting
@@ -28,11 +28,15 @@ def asset(filename):
Raises:
FileNotFoundError if file does not exist
"""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
filename = '' + filename
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
# Test if the file actually exists
full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename)
full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename).resolve()
if not full_path.exists() or not full_path.is_file():
raise FileNotFoundError(f"Asset file '{filename}' does not exist")
@@ -55,6 +59,10 @@ def uploaded_image(filename, replace_missing=True, replacement_file='blank_image
A fully qualified path to the image
"""
if type(filename) is SafeString:
# Prepend an empty string to enforce 'stringiness'
filename = '' + filename
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')

View File

@@ -9,6 +9,7 @@ from django.core.cache import cache
from django.http.response import StreamingHttpResponse
from django.test import TestCase
from django.urls import reverse
from django.utils.safestring import SafeString
from PIL import Image
@@ -49,6 +50,10 @@ class ReportTagTest(TestCase):
asset = report_tags.asset('test.txt')
self.assertEqual(asset, '/media/report/assets/test.txt')
# Ensure that a 'safe string' also works
asset = report_tags.asset(SafeString('test.txt'))
self.assertEqual(asset, '/media/report/assets/test.txt')
self.debug_mode(False)
asset = report_tags.asset('test.txt')
self.assertEqual(asset, f'file://{asset_dir}/test.txt')
@@ -87,10 +92,17 @@ class ReportTagTest(TestCase):
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, '/media/part/images/test.jpg')
# Ensure that a 'safe string' also works
img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
self.assertEqual(img, '/media/part/images/test.jpg')
self.debug_mode(False)
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')
img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')
def test_part_image(self):
"""Unit tests for the 'part_image' tag"""

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.15 on 2022-08-07 02:38
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0081_auto_20220801_0044'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=200, verbose_name='External Link'),
),
]

View File

@@ -647,7 +647,7 @@ class StockItem(MetadataMixin, MPTTModel):
link = InvenTreeURLField(
verbose_name=_('External Link'),
max_length=125, blank=True,
blank=True, max_length=200,
help_text=_("Link to external URL")
)

View File

@@ -75,24 +75,20 @@
<div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.can_adjust_location %}
{% if not item.serialized %}
{% if item.in_stock %}
<li><a class='dropdown-item' href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
{% endif %}
{% if not item.customer %}
<li><a class='dropdown-item' href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
{% endif %}
{% if item.in_stock %}
<li><a class='dropdown-item' href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
{% endif %}
{% if item.in_stock and item.part.trackable %}
{% if item.part.trackable %}
<li><a class='dropdown-item' href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
{% endif %}
{% endif %}
{% if item.in_stock and item.can_adjust_location %}
<li><a class='dropdown-item' href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
{% endif %}
{% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
{% if item.part.salable and not item.customer %}
<li><a class='dropdown-item' href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
{% endif %}
{% if item.customer %}
@@ -100,13 +96,12 @@
{% endif %}
{% if item.belongs_to %}
<li><a class='dropdown-item' href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a></li>
{% else %}
{% endif %}
{% if item.part.get_used_in %}
<!--
<li><a class='dropdown-item' href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
-->
{% endif %}
{% endif %}
</ul>
</div>
{% endif %}
@@ -618,37 +613,7 @@ $("#stock-convert").click(function() {
});
{% endif %}
{% if item.in_stock %}
$("#stock-assign-to-customer").click(function() {
inventreeGet('{% url "api-stock-detail" item.pk %}', {}, {
success: function(response) {
assignStockToCustomer(
[response],
{
success: function() {
location.reload();
},
}
);
}
});
});
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-count").click(function() {
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('take');
});
{% else %}
{% if item.customer %}
$("#stock-return-from-customer").click(function() {
constructForm('{% url "api-stock-item-return" item.pk %}', {
@@ -666,6 +631,37 @@ $("#stock-return-from-customer").click(function() {
});
});
{% else %}
$("#stock-assign-to-customer").click(function() {
inventreeGet('{% url "api-stock-detail" item.pk %}', {}, {
success: function(response) {
assignStockToCustomer(
[response],
{
success: function() {
location.reload();
},
}
);
}
});
});
{% endif %}
{% if item.can_adjust_location %}
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-count").click(function() {
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('take');
});
{% endif %}

View File

@@ -31,6 +31,57 @@ class StockListTest(StockViewTestCase):
self.assertEqual(response.status_code, 200)
class StockDetailTest(StockViewTestCase):
"""Unit test for the 'stock detail' page"""
def test_basic_info(self):
"""Test that basic stock item info is rendered"""
url = reverse('stock-item-detail', kwargs={'pk': 1})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
html = str(response.content)
# Part name
self.assertIn('Stock Item: M2x4 LPHS', html)
# Quantity
self.assertIn('<h5>Available Quantity</h5>', html)
self.assertIn('<h5>4000 </h5>', html)
# Batch code
self.assertIn('Batch', html)
self.assertIn('<td>B123</td>', html)
# Actions to check
actions = [
"id=\\\'stock-count\\\' title=\\\'Count stock\\\'",
"id=\\\'stock-add\\\' title=\\\'Add stock\\\'",
"id=\\\'stock-remove\\\' title=\\\'Remove stock\\\'",
"id=\\\'stock-move\\\' title=\\\'Transfer stock\\\'",
"id=\\\'stock-duplicate\\\'",
"id=\\\'stock-edit\\\'",
"id=\\\'stock-delete\\\'",
]
# Initially we should not have any of the required permissions
for act in actions:
self.assertNotIn(act, html)
# Give the user all the permissions
self.assignRole('stock.add')
self.assignRole('stock.change')
self.assignRole('stock.delete')
response = self.client.get(url)
html = str(response.content)
for act in actions:
self.assertIn(act, html)
class StockOwnershipTest(StockViewTestCase):
"""Tests for stock ownership views."""

View File

@@ -44,6 +44,45 @@ class StockTest(InvenTreeTestCase):
Part.objects.rebuild()
StockItem.objects.rebuild()
def test_link(self):
"""Test the link URL field validation"""
item = StockItem.objects.get(pk=1)
# Check that invalid URLs fail
for bad_url in [
'test.com',
'httpx://abc.xyz',
'https:google.com',
]:
with self.assertRaises(ValidationError):
item.link = bad_url
item.save()
item.full_clean()
# Check that valid URLs pass
for good_url in [
'https://test.com',
'https://digikey.com/datasheets?file=1010101010101.bin',
'ftp://download.com:8080/file.aspx',
]:
item.link = good_url
item.save()
item.full_clean()
# A long URL should fail
long_url = 'https://website.co.uk?query=' + 'a' * 173
with self.assertRaises(ValidationError):
item.link = long_url
item.full_clean()
# Shorten by a single character, will pass
long_url = long_url[:-1]
item.link = long_url
item.save()
def test_expiry(self):
"""Test expiry date functionality for StockItem model."""
today = datetime.datetime.now().date()

View File

@@ -7,7 +7,7 @@
{% block heading %}
{% blocktrans with name=plugin.human_name %}Plugin details for {{name}}{% endblocktrans %}
{% trans "Plugin" %}: <em>{{ plugin.human_name }}</em>
{% endblock %}
{% block content %}

View File

@@ -23,9 +23,7 @@
</td>
<td>
{% if setting.is_bool %}
<div class='form-check form-switch'>
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %} {% if plugin %}plugin='{{ plugin.slug }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}{% if notification_setting %}notification='{{request.user.id}}'{% endif %}>
</div>
{% include "InvenTree/settings/setting_boolean.html" %}
{% else %}
<div id='setting-{{ setting.pk }}'>
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
@@ -41,7 +39,18 @@
</span>
{{ setting.units }}
<div class='btn-group float-right'>
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.slug }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
<button
class='btn btn-outline-secondary btn-small btn-edit-setting'
title='{% trans "Edit setting" %}'
pk='{{ setting.pk }}'
setting='{{ setting.key.upper }}'
{% if plugin %}plugin='{{ plugin.slug }}'{% endif %}
{% if user_setting or notification_setting %}user='{{request.user.id}}'{% endif %}
{% if notification_setting %}
notification=true
method='{{ setting.method }}'
{% endif %}
>
<span class='fas fa-edit icon-green'></span>
</button>
</div>

View File

@@ -0,0 +1,18 @@
<div class='form-check form-switch'>
<input
class='form-check-input boolean-setting'
fieldname='{{ setting.key.upper }}'
pk='{{ setting.pk }}'
setting='{{ setting.key.upper }}'
id='setting-value-{{setting.pk }}-{{ setting.typ }}'
type='checkbox'
{% if setting.as_bool %}checked=''{% endif %}
{{ reference }}
{% if plugin %}plugin='{{ plugin.slug }}'{% endif %}
{% if user_setting or notification_setting %}user='{{ request.user.pk }}'{% endif %}
{% if notification_setting %}
notification=true
method='{{ setting.method }}'
{% endif %}
>
</div>

View File

@@ -78,12 +78,12 @@ $('table').find('.boolean-setting').change(function() {
// Global setting by default
var url = `/api/settings/global/${setting}/`;
if (plugin) {
if (notification) {
url = `/api/settings/notification/${pk}/`;
} else if (plugin) {
url = `/api/plugin/settings/${plugin}/${setting}/`;
} else if (user) {
url = `/api/settings/user/${setting}/`;
} else if (notification) {
url = `/api/settings/notification/${pk}/`;
}
inventreePut(

View File

@@ -2465,7 +2465,7 @@ function loadSalesOrderTable(table, options) {
return `<div id='purchase-order-calendar'></div>`;
},
onRefresh: function() {
loadPurchaseOrderTable(table, options);
loadSalesOrderTable(table, options);
},
onLoadSuccess: function() {

View File

@@ -967,7 +967,7 @@ function adjustStock(action, items, options={}) {
var item = items[idx];
if ((item.serial != null) && !allowSerializedStock) {
if ((item.serial != null) && (item.serial != '') && !allowSerializedStock) {
continue;
}

View File

@@ -127,14 +127,15 @@ class RuleSet(models.Model):
],
'purchase_order': [
'company_company',
'company_manufacturerpart',
'company_manufacturerpartparameter',
'company_supplierpart',
'company_supplierpricebreak',
'order_purchaseorder',
'order_purchaseorderattachment',
'order_purchaseorderlineitem',
'order_purchaseorderextraline',
'company_supplierpart',
'company_manufacturerpart',
'company_manufacturerpartparameter',
'report_purchaseorderreport',
],
'sales_order': [
'company_company',
@@ -144,6 +145,7 @@ class RuleSet(models.Model):
'order_salesorderlineitem',
'order_salesorderextraline',
'order_salesordershipment',
'report_salesorderreport',
]
}

View File

@@ -87,8 +87,16 @@ if __name__ == '__main__':
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
GITHUB_REF = os.environ['GITHUB_REF']
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
# Print out version information, makes debugging actions *much* easier!
print(f"GITHUB_REF: {GITHUB_REF}")
print(f"GITHUB_REF_NAME: {GITHUB_REF_NAME}")
print(f"GITHUB_REF_TYPE: {GITHUB_REF_TYPE}")
print(f"GITHUB_BASE_REF: {GITHUB_BASE_REF}")
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
version = None
@@ -109,8 +117,19 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'")
# Check version number and look for existing versions
# Note that on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
# If a release is found which matches the current tag, throw an error
allow_duplicate = False
# Note: on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
if GITHUB_REF_TYPE == 'tag':
allow_duplicate = True
# Note: on a push to 'stable' branch we also allow duplicates
if GITHUB_BASE_REF == 'stable':
allow_duplicate = True
highest_release = check_version_number(version, allow_duplicate=allow_duplicate)
# Determine which docker tag we are going to use
docker_tags = None

View File

@@ -10,6 +10,10 @@ asgiref==3.5.2 \
# via
# -c requirements.txt
# django
build==0.8.0 \
--hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
--hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
# via pip-tools
certifi==2022.6.15 \
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
@@ -20,9 +24,9 @@ cfgv==3.3.1 \
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
# via pre-commit
charset-normalizer==2.0.12 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df
charset-normalizer==2.1.0 \
--hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \
--hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413
# via
# -c requirements.txt
# requests
@@ -90,13 +94,13 @@ coveralls==2.1.2 \
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
# via -r requirements-dev.in
distlib==0.3.4 \
--hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \
--hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579
distlib==0.3.5 \
--hash=sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe \
--hash=sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c
# via virtualenv
django==3.2.14 \
--hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \
--hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56
django==3.2.15 \
--hash=sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713 \
--hash=sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b
# via
# -c requirements.txt
# django-debug-toolbar
@@ -115,9 +119,9 @@ filelock==3.7.1 \
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
# via virtualenv
flake8==4.0.1 \
--hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \
--hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d
flake8==5.0.4 \
--hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \
--hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248
# via
# -r requirements-dev.in
# flake8-docstrings
@@ -126,9 +130,9 @@ flake8-docstrings==1.6.0 \
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
# via -r requirements-dev.in
identify==2.5.1 \
--hash=sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa \
--hash=sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82
identify==2.5.3 \
--hash=sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893 \
--hash=sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44
# via pre-commit
idna==3.3 \
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
@@ -140,46 +144,54 @@ isort==5.10.1 \
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
# via -r requirements-dev.in
mccabe==0.6.1 \
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
mccabe==0.7.0 \
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via flake8
nodeenv==1.7.0 \
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
# via pre-commit
pep517==0.12.0 \
--hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \
--hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161
# via pip-tools
pep8-naming==0.13.0 \
--hash=sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0 \
--hash=sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5
packaging==21.3 \
--hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
--hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
# via build
pep517==0.13.0 \
--hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
--hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
# via build
pep8-naming==0.13.1 \
--hash=sha256:3af77cdaa9c7965f7c85a56cd579354553c9bbd3fdf3078a776f12db54dd6944 \
--hash=sha256:f7867c1a464fe769be4f972ef7b79d6df1d9aff1b1f04ecf738d471963d3ab9c
# via -r requirements-dev.in
pip-tools==6.6.2 \
--hash=sha256:6b486548e5a139e30e4c4a225b3b7c2d46942a9f6d1a91143c21b1de4d02fd9b \
--hash=sha256:f638503a9f77d98d9a7d72584b1508d3f82ed019b8fab24f4e5ad078c1b8c95e
pip-tools==6.8.0 \
--hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \
--hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8
# via -r requirements-dev.in
platformdirs==2.5.2 \
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
# via virtualenv
pre-commit==2.19.0 \
--hash=sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10 \
--hash=sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615
pre-commit==2.20.0 \
--hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \
--hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959
# via -r requirements-dev.in
pycodestyle==2.8.0 \
--hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \
--hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f
pycodestyle==2.9.1 \
--hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \
--hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b
# via flake8
pydocstyle==6.1.1 \
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
# via flake8-docstrings
pyflakes==2.4.0 \
--hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \
--hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e
pyflakes==2.5.0 \
--hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \
--hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3
# via flake8
pyparsing==3.0.9 \
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
# via packaging
pytz==2022.1 \
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
@@ -223,18 +235,12 @@ pyyaml==6.0 \
# via
# -c requirements.txt
# pre-commit
requests==2.28.0 \
--hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \
--hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b
requests==2.28.1 \
--hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
--hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
# via
# -c requirements.txt
# coveralls
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via
# -c requirements.txt
# virtualenv
snowballstemmer==2.2.0 \
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
@@ -253,20 +259,22 @@ toml==0.10.2 \
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pep517
typing-extensions==4.2.0 \
--hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \
--hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376
# via
# build
# pep517
typing-extensions==4.3.0 \
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
--hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
# via django-test-migrations
urllib3==1.26.9 \
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
urllib3==1.26.11 \
--hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \
--hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a
# via
# -c requirements.txt
# requests
virtualenv==20.15.0 \
--hash=sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc \
--hash=sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336
virtualenv==20.16.3 \
--hash=sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1 \
--hash=sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9
# via pre-commit
wheel==0.37.1 \
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \

View File

@@ -33,6 +33,7 @@ rapidfuzz==0.7.6 # Fuzzy string matching
sentry-sdk # Error reporting (optional)
setuptools # Standard depenedency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
weasyprint==54.3 # PDF generation
# Fixed sub-dependencies
py-moneyed<2.0 # For django-money # FIXED 2022-06-18 as we need `moneyed.localization`

View File

@@ -10,7 +10,7 @@ asgiref==3.5.2
# via django
babel==2.10.3
# via py-moneyed
bleach[css]==5.0.0
bleach[css]==5.0.1
# via django-markdownify
blessed==1.19.1
# via django-q
@@ -20,11 +20,11 @@ certifi==2022.6.15
# via
# requests
# sentry-sdk
cffi==1.15.0
cffi==1.15.1
# via
# cryptography
# weasyprint
charset-normalizer==2.0.12
charset-normalizer==2.1.0
# via requests
coreapi==2.3.3
# via -r requirements.in
@@ -42,7 +42,7 @@ defusedxml==0.7.1
# python3-openid
diff-match-patch==20200713
# via django-import-export
django==3.2.14
django==3.2.15
# via
# -r requirements.in
# django-allauth
@@ -121,7 +121,7 @@ djangorestframework==3.13.1
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
fonttools[woff]==4.33.3
fonttools[woff]==4.34.4
# via weasyprint
gunicorn==20.1.0
# via -r requirements.in
@@ -135,7 +135,7 @@ itypes==1.2.0
# via coreapi
jinja2==3.1.2
# via coreschema
markdown==3.3.7
markdown==3.4.1
# via django-markdownify
markuppy==1.14
# via tablib
@@ -149,7 +149,7 @@ openpyxl==3.0.10
# via tablib
pdf2image==1.16.0
# via -r requirements.in
pillow==9.1.1
pillow==9.2.0
# via
# -r requirements.in
# django-stdimage
@@ -194,14 +194,14 @@ redis==3.5.3
# via
# django-q
# django-redis
requests==2.28.0
requests==2.28.1
# via
# coreapi
# django-allauth
# requests-oauthlib
requests-oauthlib==1.3.1
# via django-allauth
sentry-sdk==1.6.0
sentry-sdk==1.9.0
# via -r requirements.in
six==1.16.0
# via
@@ -224,14 +224,16 @@ tinycss2==1.1.1
# weasyprint
uritemplate==4.1.1
# via coreapi
urllib3==1.26.9
urllib3==1.26.11
# via
# requests
# sentry-sdk
wcwidth==0.2.5
# via blessed
weasyprint==55.0
# via django-weasyprint
weasyprint==54.3
# via
# -r requirements.in
# django-weasyprint
webencodings==0.5.1
# via
# bleach
@@ -242,7 +244,7 @@ xlrd==2.0.1
# via tablib
xlwt==1.3.0
# via tablib
zipp==3.8.0
zipp==3.8.1
# via importlib-metadata
zopfli==0.2.1
# via fonttools

View File

@@ -264,7 +264,7 @@ def export_records(c, filename='data.json', overwrite=False, include_permissions
print(f"Exporting database records to file '{filename}'")
if filename.exists() and overwrite is False:
if Path(filename).is_file() and overwrite is False:
response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ")
response = str(response).strip().lower()