Compare commits

...

29 Commits
1.1.1 ... 0.8.4

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 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. """Helper function for retrieving a configuration setting value.
- First preference is to look for the environment variable - 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' env_var: Name of the environment variable e.g. 'INVENTREE_STATIC_ROOT'
config_key: Key to lookup in the configuration file config_key: Key to lookup in the configuration file
default_value: Value to return if first two options are not provided 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 # First, try to load from the environment variables
if env_var is not None: if env_var is not None:
val = os.getenv(env_var, None) val = os.getenv(env_var, None)
if val is not None: if val is not None:
return val return try_typecasting(val)
# Next, try to load from configuration file # Next, try to load from configuration file
if config_key is not None: 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] cfg_data = cfg_data[key]
if result is not None: if result is not None:
return result return try_typecasting(result)
# Finally, return the default value # 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): 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 import forms
from django.core import validators from django.core import validators
from django.db import models as models from django.db import models as models
from django.forms.fields import URLField as FormURLField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField from djmoney.forms.fields import MoneyField
@@ -23,26 +22,28 @@ class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators.""" """Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Update schemes.""" """Update schemes."""
# Enforce 'max length' parameter in form validation
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs) super().__init__(**kwargs)
self.validators[-1].schemes = allowable_url_schemes() 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): class InvenTreeURLField(models.URLField):
"""Custom URL field which has custom scheme validators.""" """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): def __init__(self, **kwargs):
"""Return a Field instance for this field.""" """Initialization method for InvenTreeURLField"""
return super().formfield(**{
'form_class': InvenTreeURLFormField # Max length for InvenTreeURLField defaults to 200
}) if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
def money_kwargs(): def money_kwargs():

View File

@@ -12,6 +12,7 @@ from wsgiref.util import FileWrapper
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.validators import URLValidator from django.core.validators import URLValidator
@@ -64,28 +65,10 @@ def constructPathString(path, max_chars=250):
pathstring = '/'.join(path) pathstring = '/'.join(path)
idx = 0
# Replace middle elements to limit the pathstring # Replace middle elements to limit the pathstring
if len(pathstring) > max_chars: if len(pathstring) > max_chars:
mid = len(path) // 2 n = int(max_chars / 2 - 2)
path_l = path[0:mid] pathstring = pathstring[:n] + "..." + pathstring[-n:]
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
return pathstring return pathstring
@@ -241,17 +224,27 @@ def getLogoImage(as_file=False, custom=True):
"""Return the path to the logo-file.""" """Return the path to the logo-file."""
if custom and settings.CUSTOM_LOGO: if custom and settings.CUSTOM_LOGO:
if as_file: static_storage = StaticFilesStorage()
return f"file://{default_storage.path(settings.CUSTOM_LOGO)}"
else:
return default_storage.url(settings.CUSTOM_LOGO)
else: if static_storage.exists(settings.CUSTOM_LOGO):
if as_file: storage = static_storage
path = settings.STATIC_ROOT.joinpath('img/inventree.png') elif default_storage.exists(settings.CUSTOM_LOGO):
return f"file://{path}" storage = default_storage
else: 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): def TestIfImageURL(url):

View File

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

View File

@@ -4,6 +4,7 @@ import logging
import os import os
import re import re
from datetime import datetime from datetime import datetime
from io import BytesIO
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@@ -24,6 +25,7 @@ import InvenTree.format
import InvenTree.helpers import InvenTree.helpers
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
from InvenTree.sanitizer import sanitize_svg
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@@ -383,8 +385,16 @@ class InvenTreeAttachment(models.Model):
'link': _('Missing external link'), '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) 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): def __str__(self):
"""Human name for attachment.""" """Human name for attachment."""
if self.attachment is not None: if self.attachment is not None:
@@ -516,8 +526,18 @@ class InvenTreeTree(MPTTModel):
) )
if pathstring != self.pathstring: if pathstring != self.pathstring:
if 'force_insert' in kwargs:
del kwargs['force_insert']
kwargs['force_update'] = True
self.pathstring = pathstring 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: class Meta:
"""Metaclass defines extra model properties.""" """Metaclass defines extra model properties."""

View File

@@ -13,7 +13,7 @@ def isImportingData():
return 'loaddata' in sys.argv 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. """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 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', 'flush',
'loaddata', 'loaddata',
'dumpdata', 'dumpdata',
'makemigrations',
'migrate',
'check', 'check',
'shell', 'shell',
'createsuperuser', 'createsuperuser',
@@ -43,6 +41,12 @@ def canAppAccessDatabase(allow_test=False):
# Override for testing mode? # Override for testing mode?
excluded_commands.append('test') excluded_commands.append('test')
if not allow_plugins:
excluded_commands.extend([
'makemigrations',
'migrate',
])
for cmd in excluded_commands: for cmd in excluded_commands:
if cmd in sys.argv: if cmd in sys.argv:
return False 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 from pathlib import Path
import django.conf.locale import django.conf.locale
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -135,6 +136,8 @@ MEDIA_URL = '/media/'
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
# Admin site integration
'django.contrib.admin',
# InvenTree apps # InvenTree apps
'build.apps.BuildConfig', 'build.apps.BuildConfig',
@@ -150,7 +153,6 @@ INSTALLED_APPS = [
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last 'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules # Core django modules
'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'user_sessions', # db user sessions 'user_sessions', # db user sessions
@@ -346,6 +348,12 @@ for key in db_keys:
env_var = os.environ.get(env_key, None) env_var = os.environ.get(env_key, None)
if env_var: 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 # Override configuration value
db_config[key] = env_var db_config[key] = env_var
@@ -503,7 +511,7 @@ DATABASES = {
# Cache configuration # Cache configuration
cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None) 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 if cache_host: # pragma: no cover
# We are going to rely upon a possibly non-localhost for our cache, # 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 configuration options
EMAIL_BACKEND = get_setting('INVENTREE_EMAIL_BACKEND', 'email.backend', 'django.core.mail.backends.smtp.EmailBackend') 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_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_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '') EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '')
EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ') 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_TLS = get_boolean_setting('INVENTREE_EMAIL_TLS', 'email.tls', False)
EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', 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_USE_LOCALTIME = False
EMAIL_TIMEOUT = 60 EMAIL_TIMEOUT = 60
@@ -718,8 +726,8 @@ SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', [])
SOCIALACCOUNT_STORE_TOKENS = True SOCIALACCOUNT_STORE_TOKENS = True
# settings for allauth # settings for allauth
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3) 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) ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5, typecast=int)
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_PREVENT_ENUMERATION = 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) 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 Check for the existence of a 'custom logo' file:
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage") - Check the 'static' directory
CUSTOM_LOGO = False - 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: if DEBUG:
logger.info("InvenTree running with DEBUG enabled") logger.info("InvenTree running with DEBUG enabled")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -118,7 +118,7 @@ class CategoryTest(TestCase):
self.assertTrue(len(child.path), 26) self.assertTrue(len(child.path), 26)
self.assertEqual( self.assertEqual(
child.pathstring, 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) self.assertTrue(len(child.pathstring) <= 250)

View File

@@ -27,7 +27,7 @@ class PluginAppConfig(AppConfig):
def ready(self): def ready(self):
"""The ready method is extended to initialize plugins.""" """The ready method is extended to initialize plugins."""
if settings.PLUGINS_ENABLED: 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 logger.info("Skipping plugin loading sequence") # pragma: no cover
else: else:
logger.info('Loading InvenTree plugins') 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.""" """Handles an error and casts it as an IntegrationPluginError."""
package_path = traceback.extract_tb(error.__traceback__)[-1].filename package_path = traceback.extract_tb(error.__traceback__)[-1].filename
install_path = sysconfig.get_paths()["purelib"] install_path = sysconfig.get_paths()["purelib"]
try: try:
package_name = pathlib.Path(package_path).relative_to(install_path).parts[0] package_name = pathlib.Path(package_path).relative_to(install_path).parts[0]
except ValueError: 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) log_error({package_name: str(error)}, **log_kwargs)
if do_raise: 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): if settings.TESTING_ENV and package_name != 'integration.broken_sample' and isinstance(error, IntegrityError):
raise error # pragma: no cover raise error # pragma: no cover
raise IntegrationPluginError(package_name, str(error)) raise IntegrationPluginError(package_name, str(error))
# endregion # endregion

View File

@@ -6,7 +6,7 @@ import os
import pathlib import pathlib
import warnings import warnings
from datetime import datetime from datetime import datetime
from importlib.metadata import metadata from importlib.metadata import PackageNotFoundError, metadata
from django.conf import settings from django.conf import settings
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
@@ -294,7 +294,18 @@ class InvenTreePlugin(MixinBase, MetaBase):
@classmethod @classmethod
def _get_package_metadata(cls): def _get_package_metadata(cls):
"""Get package metadata for plugin.""" """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 { return {
'author': meta['Author-email'], 'author': meta['Author-email'],

View File

@@ -233,17 +233,12 @@ class ReportPrintMixin:
pages = [] pages = []
try: try:
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
if len(outputs) > 1: pdf = outputs[0].get_document().copy(pages).write_pdf()
# 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()
except TemplateDoesNotExist as e: except TemplateDoesNotExist as e:

View File

@@ -74,7 +74,7 @@ table td.expand {
<div class='header-right'> <div class='header-right'>
<h3>{% trans "Purchase Order" %} {{ prefix }}{{ reference }}</h3> <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> </div>
{% endblock %} {% endblock %}

View File

@@ -5,7 +5,7 @@ import os
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import SafeString, mark_safe
import InvenTree.helpers import InvenTree.helpers
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
@@ -28,11 +28,15 @@ def asset(filename):
Raises: Raises:
FileNotFoundError if file does not exist 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 # If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
# Test if the file actually exists # 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(): if not full_path.exists() or not full_path.is_file():
raise FileNotFoundError(f"Asset file '{filename}' does not exist") 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 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 # If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') 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.http.response import StreamingHttpResponse
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import SafeString
from PIL import Image from PIL import Image
@@ -49,6 +50,10 @@ class ReportTagTest(TestCase):
asset = report_tags.asset('test.txt') asset = report_tags.asset('test.txt')
self.assertEqual(asset, '/media/report/assets/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) self.debug_mode(False)
asset = report_tags.asset('test.txt') asset = report_tags.asset('test.txt')
self.assertEqual(asset, f'file://{asset_dir}/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') img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, '/media/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) self.debug_mode(False)
img = report_tags.uploaded_image('part/images/test.jpg') img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, f'file://{img_path.joinpath("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): def test_part_image(self):
"""Unit tests for the 'part_image' tag""" """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( link = InvenTreeURLField(
verbose_name=_('External Link'), verbose_name=_('External Link'),
max_length=125, blank=True, blank=True, max_length=200,
help_text=_("Link to external URL") help_text=_("Link to external URL")
) )

View File

@@ -75,24 +75,20 @@
<div class='btn-group'> <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> <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'> <ul class='dropdown-menu' role='menu'>
{% if item.can_adjust_location %}
{% if not item.serialized %} {% 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> <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 %} {% 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> <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> <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 %} {% 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> <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 %}
{% 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> <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 %} {% 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> <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 %} {% endif %}
{% if item.customer %} {% if item.customer %}
@@ -100,13 +96,12 @@
{% endif %} {% endif %}
{% if item.belongs_to %} {% 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> <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 %} {% 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> <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 %}
{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@@ -618,37 +613,7 @@ $("#stock-convert").click(function() {
}); });
{% endif %} {% endif %}
{% if item.in_stock %} {% if item.customer %}
$("#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 %}
$("#stock-return-from-customer").click(function() { $("#stock-return-from-customer").click(function() {
constructForm('{% url "api-stock-item-return" item.pk %}', { 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 %} {% endif %}

View File

@@ -31,6 +31,57 @@ class StockListTest(StockViewTestCase):
self.assertEqual(response.status_code, 200) 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): class StockOwnershipTest(StockViewTestCase):
"""Tests for stock ownership views.""" """Tests for stock ownership views."""

View File

@@ -44,6 +44,45 @@ class StockTest(InvenTreeTestCase):
Part.objects.rebuild() Part.objects.rebuild()
StockItem.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): def test_expiry(self):
"""Test expiry date functionality for StockItem model.""" """Test expiry date functionality for StockItem model."""
today = datetime.datetime.now().date() today = datetime.datetime.now().date()

View File

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

View File

@@ -23,9 +23,7 @@
</td> </td>
<td> <td>
{% if setting.is_bool %} {% if setting.is_bool %}
<div class='form-check form-switch'> {% include "InvenTree/settings/setting_boolean.html" %}
<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>
{% else %} {% else %}
<div id='setting-{{ setting.pk }}'> <div id='setting-{{ setting.pk }}'>
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'> <span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
@@ -41,7 +39,18 @@
</span> </span>
{{ setting.units }} {{ setting.units }}
<div class='btn-group float-right'> <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> <span class='fas fa-edit icon-green'></span>
</button> </button>
</div> </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 // Global setting by default
var url = `/api/settings/global/${setting}/`; var url = `/api/settings/global/${setting}/`;
if (plugin) { if (notification) {
url = `/api/settings/notification/${pk}/`;
} else if (plugin) {
url = `/api/plugin/settings/${plugin}/${setting}/`; url = `/api/plugin/settings/${plugin}/${setting}/`;
} else if (user) { } else if (user) {
url = `/api/settings/user/${setting}/`; url = `/api/settings/user/${setting}/`;
} else if (notification) {
url = `/api/settings/notification/${pk}/`;
} }
inventreePut( inventreePut(

View File

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

View File

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

View File

@@ -127,14 +127,15 @@ class RuleSet(models.Model):
], ],
'purchase_order': [ 'purchase_order': [
'company_company', 'company_company',
'company_manufacturerpart',
'company_manufacturerpartparameter',
'company_supplierpart',
'company_supplierpricebreak', 'company_supplierpricebreak',
'order_purchaseorder', 'order_purchaseorder',
'order_purchaseorderattachment', 'order_purchaseorderattachment',
'order_purchaseorderlineitem', 'order_purchaseorderlineitem',
'order_purchaseorderextraline', 'order_purchaseorderextraline',
'company_supplierpart', 'report_purchaseorderreport',
'company_manufacturerpart',
'company_manufacturerpartparameter',
], ],
'sales_order': [ 'sales_order': [
'company_company', 'company_company',
@@ -144,6 +145,7 @@ class RuleSet(models.Model):
'order_salesorderlineitem', 'order_salesorderlineitem',
'order_salesorderextraline', 'order_salesorderextraline',
'order_salesordershipment', '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 may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
GITHUB_REF = os.environ['GITHUB_REF'] GITHUB_REF = os.environ['GITHUB_REF']
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF'] 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_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
version = None version = None
@@ -109,8 +117,19 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'") print(f"InvenTree Version: '{version}'")
# Check version number and look for existing versions # 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 # If a release is found which matches the current tag, throw an error
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
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 # Determine which docker tag we are going to use
docker_tags = None docker_tags = None

View File

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

View File

@@ -33,6 +33,7 @@ rapidfuzz==0.7.6 # Fuzzy string matching
sentry-sdk # Error reporting (optional) sentry-sdk # Error reporting (optional)
setuptools # Standard depenedency setuptools # Standard depenedency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
weasyprint==54.3 # PDF generation
# Fixed sub-dependencies # Fixed sub-dependencies
py-moneyed<2.0 # For django-money # FIXED 2022-06-18 as we need `moneyed.localization` 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 # via django
babel==2.10.3 babel==2.10.3
# via py-moneyed # via py-moneyed
bleach[css]==5.0.0 bleach[css]==5.0.1
# via django-markdownify # via django-markdownify
blessed==1.19.1 blessed==1.19.1
# via django-q # via django-q
@@ -20,11 +20,11 @@ certifi==2022.6.15
# via # via
# requests # requests
# sentry-sdk # sentry-sdk
cffi==1.15.0 cffi==1.15.1
# via # via
# cryptography # cryptography
# weasyprint # weasyprint
charset-normalizer==2.0.12 charset-normalizer==2.1.0
# via requests # via requests
coreapi==2.3.3 coreapi==2.3.3
# via -r requirements.in # via -r requirements.in
@@ -42,7 +42,7 @@ defusedxml==0.7.1
# python3-openid # python3-openid
diff-match-patch==20200713 diff-match-patch==20200713
# via django-import-export # via django-import-export
django==3.2.14 django==3.2.15
# via # via
# -r requirements.in # -r requirements.in
# django-allauth # django-allauth
@@ -121,7 +121,7 @@ djangorestframework==3.13.1
# via -r requirements.in # via -r requirements.in
et-xmlfile==1.1.0 et-xmlfile==1.1.0
# via openpyxl # via openpyxl
fonttools[woff]==4.33.3 fonttools[woff]==4.34.4
# via weasyprint # via weasyprint
gunicorn==20.1.0 gunicorn==20.1.0
# via -r requirements.in # via -r requirements.in
@@ -135,7 +135,7 @@ itypes==1.2.0
# via coreapi # via coreapi
jinja2==3.1.2 jinja2==3.1.2
# via coreschema # via coreschema
markdown==3.3.7 markdown==3.4.1
# via django-markdownify # via django-markdownify
markuppy==1.14 markuppy==1.14
# via tablib # via tablib
@@ -149,7 +149,7 @@ openpyxl==3.0.10
# via tablib # via tablib
pdf2image==1.16.0 pdf2image==1.16.0
# via -r requirements.in # via -r requirements.in
pillow==9.1.1 pillow==9.2.0
# via # via
# -r requirements.in # -r requirements.in
# django-stdimage # django-stdimage
@@ -194,14 +194,14 @@ redis==3.5.3
# via # via
# django-q # django-q
# django-redis # django-redis
requests==2.28.0 requests==2.28.1
# via # via
# coreapi # coreapi
# django-allauth # django-allauth
# requests-oauthlib # requests-oauthlib
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
# via django-allauth # via django-allauth
sentry-sdk==1.6.0 sentry-sdk==1.9.0
# via -r requirements.in # via -r requirements.in
six==1.16.0 six==1.16.0
# via # via
@@ -224,14 +224,16 @@ tinycss2==1.1.1
# weasyprint # weasyprint
uritemplate==4.1.1 uritemplate==4.1.1
# via coreapi # via coreapi
urllib3==1.26.9 urllib3==1.26.11
# via # via
# requests # requests
# sentry-sdk # sentry-sdk
wcwidth==0.2.5 wcwidth==0.2.5
# via blessed # via blessed
weasyprint==55.0 weasyprint==54.3
# via django-weasyprint # via
# -r requirements.in
# django-weasyprint
webencodings==0.5.1 webencodings==0.5.1
# via # via
# bleach # bleach
@@ -242,7 +244,7 @@ xlrd==2.0.1
# via tablib # via tablib
xlwt==1.3.0 xlwt==1.3.0
# via tablib # via tablib
zipp==3.8.0 zipp==3.8.1
# via importlib-metadata # via importlib-metadata
zopfli==0.2.1 zopfli==0.2.1
# via fonttools # 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}'") 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 = input("Warning: file already exists. Do you want to overwrite? [y/N]: ")
response = str(response).strip().lower() response = str(response).strip().lower()