switched to single quotes everywhere

This commit is contained in:
Matthias Mair 2024-01-07 21:03:14 +01:00
parent a92442e60e
commit f83fedbbb8
No known key found for this signature in database
GPG Key ID: A593429DDA23B66A
181 changed files with 2080 additions and 2080 deletions

View File

@ -32,12 +32,12 @@ class InvenTreeResource(ModelResource):
"""Override the default import_data_inner function to provide better error handling""" """Override the default import_data_inner function to provide better error handling"""
if len(dataset) > self.MAX_IMPORT_ROWS: if len(dataset) > self.MAX_IMPORT_ROWS:
raise ImportExportError( raise ImportExportError(
f"Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})" f'Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})'
) )
if len(dataset.headers) > self.MAX_IMPORT_COLS: if len(dataset.headers) > self.MAX_IMPORT_COLS:
raise ImportExportError( raise ImportExportError(
f"Dataset contains too many columns (max {self.MAX_IMPORT_COLS})" f'Dataset contains too many columns (max {self.MAX_IMPORT_COLS})'
) )
return super().import_data_inner( return super().import_data_inner(

View File

@ -232,19 +232,19 @@ class BulkDeleteMixin:
if not items and not filters: if not items and not filters:
raise ValidationError({ raise ValidationError({
"non_field_errors": [ 'non_field_errors': [
"List of items or filters must be provided for bulk deletion" 'List of items or filters must be provided for bulk deletion'
] ]
}) })
if items and type(items) is not list: if items and type(items) is not list:
raise ValidationError({ raise ValidationError({
"items": ["'items' must be supplied as a list object"] 'items': ["'items' must be supplied as a list object"]
}) })
if filters and type(filters) is not dict: if filters and type(filters) is not dict:
raise ValidationError({ raise ValidationError({
"filters": ["'filters' must be supplied as a dict object"] 'filters': ["'filters' must be supplied as a dict object"]
}) })
# Keep track of how many items we deleted # Keep track of how many items we deleted
@ -266,7 +266,7 @@ class BulkDeleteMixin:
n_deleted = queryset.count() n_deleted = queryset.count()
queryset.delete() queryset.delete()
return Response({'success': f"Deleted {n_deleted} items"}, status=204) return Response({'success': f'Deleted {n_deleted} items'}, status=204)
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI): class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
@ -308,7 +308,7 @@ class APIDownloadMixin:
def download_queryset(self, queryset, export_format): def download_queryset(self, queryset, export_format):
"""This function must be implemented to provide a downloadFile request.""" """This function must be implemented to provide a downloadFile request."""
raise NotImplementedError("download_queryset method not implemented!") raise NotImplementedError('download_queryset method not implemented!')
class AttachmentMixin: class AttachmentMixin:

View File

@ -21,7 +21,7 @@ from InvenTree.ready import (
isPluginRegistryLoaded, isPluginRegistryLoaded,
) )
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
class InvenTreeConfig(AppConfig): class InvenTreeConfig(AppConfig):
@ -82,11 +82,11 @@ class InvenTreeConfig(AppConfig):
try: try:
Schedule.objects.filter(func__in=obsolete).delete() Schedule.objects.filter(func__in=obsolete).delete()
except Exception: except Exception:
logger.exception("Failed to remove obsolete tasks - database not ready") logger.exception('Failed to remove obsolete tasks - database not ready')
def start_background_tasks(self): def start_background_tasks(self):
"""Start all background tests for InvenTree.""" """Start all background tests for InvenTree."""
logger.info("Starting background tasks...") logger.info('Starting background tasks...')
from django_q.models import Schedule from django_q.models import Schedule
@ -130,17 +130,17 @@ class InvenTreeConfig(AppConfig):
if len(tasks_to_create) > 0: if len(tasks_to_create) > 0:
Schedule.objects.bulk_create(tasks_to_create) Schedule.objects.bulk_create(tasks_to_create)
logger.info("Created %s new scheduled tasks", len(tasks_to_create)) logger.info('Created %s new scheduled tasks', len(tasks_to_create))
if len(tasks_to_update) > 0: if len(tasks_to_update) > 0:
Schedule.objects.bulk_update(tasks_to_update, ['schedule_type', 'minutes']) Schedule.objects.bulk_update(tasks_to_update, ['schedule_type', 'minutes'])
logger.info("Updated %s existing scheduled tasks", len(tasks_to_update)) logger.info('Updated %s existing scheduled tasks', len(tasks_to_update))
# Put at least one task onto the background worker stack, # Put at least one task onto the background worker stack,
# which will be processed as soon as the worker comes online # which will be processed as soon as the worker comes online
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat, force_async=True) InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat, force_async=True)
logger.info("Started %s scheduled background tasks...", len(tasks)) logger.info('Started %s scheduled background tasks...', len(tasks))
def collect_tasks(self): def collect_tasks(self):
"""Collect all background tasks.""" """Collect all background tasks."""
@ -152,7 +152,7 @@ class InvenTreeConfig(AppConfig):
try: try:
import_module(f'{app.module.__package__}.tasks') import_module(f'{app.module.__package__}.tasks')
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logger.exception("Error loading tasks for %s: %s", app_name, e) logger.exception('Error loading tasks for %s: %s', app_name, e)
def update_exchange_rates(self): # pragma: no cover def update_exchange_rates(self): # pragma: no cover
"""Update exchange rates each time the server is started. """Update exchange rates each time the server is started.
@ -183,20 +183,20 @@ class InvenTreeConfig(AppConfig):
if last_update is None: if last_update is None:
# Never been updated # Never been updated
logger.info("Exchange backend has never been updated") logger.info('Exchange backend has never been updated')
update = True update = True
# Backend currency has changed? # Backend currency has changed?
if base_currency != backend.base_currency: if base_currency != backend.base_currency:
logger.info( logger.info(
"Base currency changed from %s to %s", 'Base currency changed from %s to %s',
backend.base_currency, backend.base_currency,
base_currency, base_currency,
) )
update = True update = True
except ExchangeBackend.DoesNotExist: except ExchangeBackend.DoesNotExist:
logger.info("Exchange backend not found - updating") logger.info('Exchange backend not found - updating')
update = True update = True
except Exception: except Exception:
@ -207,9 +207,9 @@ class InvenTreeConfig(AppConfig):
try: try:
update_exchange_rates() update_exchange_rates()
except OperationalError: except OperationalError:
logger.warning("Could not update exchange rates - database not ready") logger.warning('Could not update exchange rates - database not ready')
except Exception as e: except Exception as e:
logger.exception("Error updating exchange rates: %s (%s)", e, type(e)) logger.exception('Error updating exchange rates: %s (%s)', e, type(e))
def add_user_on_startup(self): def add_user_on_startup(self):
"""Add a user on startup.""" """Add a user on startup."""
@ -222,7 +222,7 @@ class InvenTreeConfig(AppConfig):
add_email = get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email') add_email = get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email')
add_password = get_setting('INVENTREE_ADMIN_PASSWORD', 'admin_password') add_password = get_setting('INVENTREE_ADMIN_PASSWORD', 'admin_password')
add_password_file = get_setting( add_password_file = get_setting(
"INVENTREE_ADMIN_PASSWORD_FILE", "admin_password_file", None 'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None
) )
# check if all values are present # check if all values are present
@ -260,7 +260,7 @@ class InvenTreeConfig(AppConfig):
try: try:
with transaction.atomic(): with transaction.atomic():
if user.objects.filter(username=add_user).exists(): if user.objects.filter(username=add_user).exists():
logger.info("User %s already exists - skipping creation", add_user) logger.info('User %s already exists - skipping creation', add_user)
else: else:
new_user = user.objects.create_superuser( new_user = user.objects.create_superuser(
add_user, add_email, add_password add_user, add_email, add_password
@ -272,12 +272,12 @@ class InvenTreeConfig(AppConfig):
def add_user_from_file(self): def add_user_from_file(self):
"""Add the superuser from a file.""" """Add the superuser from a file."""
# stop if checks were already created # stop if checks were already created
if hasattr(settings, "USER_ADDED_FILE") and settings.USER_ADDED_FILE: if hasattr(settings, 'USER_ADDED_FILE') and settings.USER_ADDED_FILE:
return return
# get values # get values
add_password_file = get_setting( add_password_file = get_setting(
"INVENTREE_ADMIN_PASSWORD_FILE", "admin_password_file", None 'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None
) )
# no variable set -> do not try anything # no variable set -> do not try anything
@ -296,7 +296,7 @@ class InvenTreeConfig(AppConfig):
self._create_admin_user( self._create_admin_user(
get_setting('INVENTREE_ADMIN_USER', 'admin_user', 'admin'), get_setting('INVENTREE_ADMIN_USER', 'admin_user', 'admin'),
get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email', ''), get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email', ''),
add_password_file.read_text(encoding="utf-8"), add_password_file.read_text(encoding='utf-8'),
) )
# do not try again # do not try again

View File

@ -63,9 +63,9 @@ class RenderJavascriptFiles(InvenTreeTestCase): # pragma: no cover
"""Look for all javascript files.""" """Look for all javascript files."""
n = 0 n = 0
print("Rendering javascript files...") print('Rendering javascript files...')
n += self.download_files('translated', '/js/i18n') n += self.download_files('translated', '/js/i18n')
n += self.download_files('dynamic', '/js/dynamic') n += self.download_files('dynamic', '/js/dynamic')
print(f"Rendered {n} javascript files.") print(f'Rendered {n} javascript files.')

View File

@ -99,9 +99,9 @@ def get_config_file(create=True) -> Path:
) )
ensure_dir(cfg_filename.parent) ensure_dir(cfg_filename.parent)
cfg_template = base_dir.joinpath("config_template.yaml") cfg_template = base_dir.joinpath('config_template.yaml')
shutil.copyfile(cfg_template, cfg_filename) shutil.copyfile(cfg_template, cfg_filename)
print(f"Created config file {cfg_filename}") print(f'Created config file {cfg_filename}')
return cfg_filename return cfg_filename
@ -293,14 +293,14 @@ def get_plugin_file():
if not plugin_file.exists(): if not plugin_file.exists():
logger.warning( logger.warning(
"Plugin configuration file does not exist - creating default file" 'Plugin configuration file does not exist - creating default file'
) )
logger.info("Creating plugin file at '%s'", plugin_file) logger.info("Creating plugin file at '%s'", plugin_file)
ensure_dir(plugin_file.parent) ensure_dir(plugin_file.parent)
# If opening the file fails (no write permission, for example), then this will throw an error # If opening the file fails (no write permission, for example), then this will throw an error
plugin_file.write_text( plugin_file.write_text(
"# InvenTree Plugins (uses PIP framework to install)\n\n" '# InvenTree Plugins (uses PIP framework to install)\n\n'
) )
return plugin_file return plugin_file
@ -323,7 +323,7 @@ def get_secret_key():
""" """
# Look for environment variable # Look for environment variable
if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'): if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'):
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover logger.info('SECRET_KEY loaded by INVENTREE_SECRET_KEY') # pragma: no cover
return secret_key return secret_key
# Look for secret key file # Look for secret key file
@ -331,7 +331,7 @@ def get_secret_key():
secret_key_file = Path(secret_key_file).resolve() secret_key_file = Path(secret_key_file).resolve()
else: else:
# Default location for secret key file # Default location for secret key file
secret_key_file = get_base_dir().joinpath("secret_key.txt").resolve() secret_key_file = get_base_dir().joinpath('secret_key.txt').resolve()
if not secret_key_file.exists(): if not secret_key_file.exists():
logger.info("Generating random key file at '%s'", secret_key_file) logger.info("Generating random key file at '%s'", secret_key_file)
@ -367,9 +367,9 @@ def get_custom_file(
static_storage = StaticFilesStorage() static_storage = StaticFilesStorage()
if static_storage.exists(value): if static_storage.exists(value):
logger.info("Loading %s from %s directory: %s", log_ref, 'static', value) logger.info('Loading %s from %s directory: %s', log_ref, 'static', value)
elif lookup_media and default_storage.exists(value): elif lookup_media and default_storage.exists(value):
logger.info("Loading %s from %s directory: %s", log_ref, 'media', value) logger.info('Loading %s from %s directory: %s', log_ref, 'media', value)
else: else:
add_dir_str = ' or media' if lookup_media else '' add_dir_str = ' or media' if lookup_media else ''
logger.warning( logger.warning(

View File

@ -127,7 +127,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
if unit: if unit:
raise ValidationError(_(f'Could not convert {original} to {unit}')) raise ValidationError(_(f'Could not convert {original} to {unit}'))
else: else:
raise ValidationError(_("Invalid quantity supplied")) raise ValidationError(_('Invalid quantity supplied'))
# Calculate the "magnitude" of the value, as a float # Calculate the "magnitude" of the value, as a float
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues # If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues

View File

@ -30,22 +30,22 @@ def is_email_configured():
# Display warning unless in test mode # Display warning unless in test mode
if not testing: # pragma: no cover if not testing: # pragma: no cover
logger.debug("EMAIL_HOST is not configured") logger.debug('EMAIL_HOST is not configured')
# Display warning unless in test mode # Display warning unless in test mode
if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover
logger.debug("EMAIL_HOST_USER is not configured") logger.debug('EMAIL_HOST_USER is not configured')
# Display warning unless in test mode # Display warning unless in test mode
if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover
logger.debug("EMAIL_HOST_PASSWORD is not configured") logger.debug('EMAIL_HOST_PASSWORD is not configured')
# Email sender must be configured # Email sender must be configured
if not settings.DEFAULT_FROM_EMAIL: if not settings.DEFAULT_FROM_EMAIL:
configured = False configured = False
if not testing: # pragma: no cover if not testing: # pragma: no cover
logger.debug("DEFAULT_FROM_EMAIL is not configured") logger.debug('DEFAULT_FROM_EMAIL is not configured')
return configured return configured
@ -75,7 +75,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
if settings.TESTING: if settings.TESTING:
from_email = 'from@test.com' from_email = 'from@test.com'
else: else:
logger.error("send_email failed: DEFAULT_FROM_EMAIL not specified") logger.error('send_email failed: DEFAULT_FROM_EMAIL not specified')
return return
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(

View File

@ -86,7 +86,7 @@ def exception_handler(exc, context):
# If in DEBUG mode, provide error information in the response # If in DEBUG mode, provide error information in the response
error_detail = str(exc) error_detail = str(exc)
else: else:
error_detail = _("Error details can be found in the admin panel") error_detail = _('Error details can be found in the admin panel')
response_data = { response_data = {
'error': type(exc).__name__, 'error': type(exc).__name__,

View File

@ -18,7 +18,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
Uses the plugin system to actually fetch the rates from an external API. Uses the plugin system to actually fetch the rates from an external API.
""" """
name = "InvenTreeExchange" name = 'InvenTreeExchange'
def get_rates(self, **kwargs) -> None: def get_rates(self, **kwargs) -> None:
"""Set the requested currency codes and get rates.""" """Set the requested currency codes and get rates."""
@ -55,19 +55,19 @@ class InvenTreeExchange(SimpleExchangeBackend):
try: try:
rates = plugin.update_exchange_rates(base_currency, symbols) rates = plugin.update_exchange_rates(base_currency, symbols)
except Exception as exc: except Exception as exc:
logger.exception("Exchange rate update failed: %s", exc) logger.exception('Exchange rate update failed: %s', exc)
return {} return {}
if not rates: if not rates:
logger.warning( logger.warning(
"Exchange rate update failed - no data returned from plugin %s", slug 'Exchange rate update failed - no data returned from plugin %s', slug
) )
return {} return {}
# Update exchange rates based on returned data # Update exchange rates based on returned data
if type(rates) is not dict: if type(rates) is not dict:
logger.warning( logger.warning(
"Invalid exchange rate data returned from plugin %s (type %s)", 'Invalid exchange rate data returned from plugin %s (type %s)',
slug, slug,
type(rates), type(rates),
) )
@ -82,7 +82,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
def update_rates(self, base_currency=None, **kwargs): def update_rates(self, base_currency=None, **kwargs):
"""Call to update all exchange rates""" """Call to update all exchange rates"""
backend, _ = ExchangeBackend.objects.update_or_create( backend, _ = ExchangeBackend.objects.update_or_create(
name=self.name, defaults={"base_currency": base_currency} name=self.name, defaults={'base_currency': base_currency}
) )
if base_currency is None: if base_currency is None:
@ -91,7 +91,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
symbols = currency_codes() symbols = currency_codes()
logger.info( logger.info(
"Updating exchange rates for %s (%s currencies)", 'Updating exchange rates for %s (%s currencies)',
base_currency, base_currency,
len(symbols), len(symbols),
) )
@ -110,7 +110,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
]) ])
else: else:
logger.info( logger.info(
"No exchange rates returned from backend - currencies not updated" 'No exchange rates returned from backend - currencies not updated'
) )
logger.info("Updated exchange rates for %s", base_currency) logger.info('Updated exchange rates for %s', base_currency)

View File

@ -76,7 +76,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
if whole: if whole:
# Wrap the search term to enable word-boundary matching # Wrap the search term to enable word-boundary matching
term = r"\y" + term + r"\y" term = r'\y' + term + r'\y'
terms.append(term) terms.append(term)

View File

@ -64,7 +64,7 @@ def construct_format_regex(fmt_string: str) -> str:
Raises: Raises:
ValueError: Format string is invalid ValueError: Format string is invalid
""" """
pattern = "^" pattern = '^'
for group in string.Formatter().parse(fmt_string): for group in string.Formatter().parse(fmt_string):
prefix = group[0] # Prefix (literal text appearing before this group) prefix = group[0] # Prefix (literal text appearing before this group)
@ -87,7 +87,7 @@ def construct_format_regex(fmt_string: str) -> str:
':', ':',
';', ';',
'|', '|',
'\'', "'",
'"', '"',
] ]
@ -115,9 +115,9 @@ def construct_format_regex(fmt_string: str) -> str:
# TODO: Introspect required width # TODO: Introspect required width
w = '+' w = '+'
pattern += f"(?P<{name}>{chr}{w})" pattern += f'(?P<{name}>{chr}{w})'
pattern += "$" pattern += '$'
return pattern return pattern
@ -172,7 +172,7 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
if not result: if not result:
raise ValueError( raise ValueError(
_("Provided value does not match required pattern: ") + fmt_string _('Provided value does not match required pattern: ') + fmt_string
) )
# And return the value we are interested in # And return the value we are interested in
@ -198,7 +198,7 @@ def format_money(money: Money, decimal_places: int = None, format: str = None) -
if format: if format:
pattern = parse_pattern(format) pattern = parse_pattern(format)
else: else:
pattern = locale.currency_formats["standard"] pattern = locale.currency_formats['standard']
if decimal_places is not None: if decimal_places is not None:
pattern.frac_prec = (decimal_places, decimal_places) pattern.frac_prec = (decimal_places, decimal_places)

View File

@ -140,7 +140,7 @@ class SetPasswordForm(HelperForm):
) )
old_password = forms.CharField( old_password = forms.CharField(
label=_("Old password"), label=_('Old password'),
strip=False, strip=False,
required=False, required=False,
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -178,23 +178,23 @@ class CustomSignupForm(SignupForm):
# check for two mail fields # check for two mail fields
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'): if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
self.fields["email2"] = forms.EmailField( self.fields['email2'] = forms.EmailField(
label=_("Email (again)"), label=_('Email (again)'),
widget=forms.TextInput( widget=forms.TextInput(
attrs={ attrs={
"type": "email", 'type': 'email',
"placeholder": _("Email address confirmation"), 'placeholder': _('Email address confirmation'),
} }
), ),
) )
# check for two password fields # check for two password fields
if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'): if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'):
self.fields.pop("password2") self.fields.pop('password2')
# reorder fields # reorder fields
set_form_field_order( set_form_field_order(
self, ["username", "email", "email2", "password1", "password2"] self, ['username', 'email', 'email2', 'password1', 'password2']
) )
def clean(self): def clean(self):
@ -203,10 +203,10 @@ class CustomSignupForm(SignupForm):
# check for two mail fields # check for two mail fields
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'): if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
email = cleaned_data.get("email") email = cleaned_data.get('email')
email2 = cleaned_data.get("email2") email2 = cleaned_data.get('email2')
if (email and email2) and email != email2: if (email and email2) and email != email2:
self.add_error("email2", _("You must type the same email each time.")) self.add_error('email2', _('You must type the same email each time.'))
return cleaned_data return cleaned_data
@ -221,7 +221,7 @@ def registration_enabled():
return True return True
else: else:
logger.error( logger.error(
"Registration cannot be enabled, because EMAIL_HOST is not configured." 'Registration cannot be enabled, because EMAIL_HOST is not configured.'
) )
return False return False
@ -292,7 +292,7 @@ class CustomUrlMixin:
def get_email_confirmation_url(self, request, emailconfirmation): def get_email_confirmation_url(self, request, emailconfirmation):
"""Custom email confirmation (activation) url.""" """Custom email confirmation (activation) url."""
url = reverse("account_confirm_email", args=[emailconfirmation.key]) url = reverse('account_confirm_email', args=[emailconfirmation.key])
return Site.objects.get_current().domain + url return Site.objects.get_current().domain + url

View File

@ -36,7 +36,7 @@ def generateTestKey(test_name):
Tests must be named such that they will have unique keys. Tests must be named such that they will have unique keys.
""" """
key = test_name.strip().lower() key = test_name.strip().lower()
key = key.replace(" ", "") key = key.replace(' ', '')
# Remove any characters that cannot be used to represent a variable # Remove any characters that cannot be used to represent a variable
key = re.sub(r'[^a-zA-Z0-9]', '', key) key = re.sub(r'[^a-zA-Z0-9]', '', key)
@ -56,7 +56,7 @@ def constructPathString(path, max_chars=250):
# Replace middle elements to limit the pathstring # Replace middle elements to limit the pathstring
if len(pathstring) > max_chars: if len(pathstring) > max_chars:
n = int(max_chars / 2 - 2) n = int(max_chars / 2 - 2)
pathstring = pathstring[:n] + "..." + pathstring[-n:] pathstring = pathstring[:n] + '...' + pathstring[-n:]
return pathstring return pathstring
@ -82,12 +82,12 @@ def TestIfImage(img):
def getBlankImage(): def getBlankImage():
"""Return the qualified path for the 'blank image' placeholder.""" """Return the qualified path for the 'blank image' placeholder."""
return getStaticUrl("img/blank_image.png") return getStaticUrl('img/blank_image.png')
def getBlankThumbnail(): def getBlankThumbnail():
"""Return the qualified path for the 'blank image' thumbnail placeholder.""" """Return the qualified path for the 'blank image' thumbnail placeholder."""
return getStaticUrl("img/blank_image.thumbnail.png") return getStaticUrl('img/blank_image.thumbnail.png')
def getLogoImage(as_file=False, custom=True): def getLogoImage(as_file=False, custom=True):
@ -105,13 +105,13 @@ def getLogoImage(as_file=False, custom=True):
if storage is not None: if storage is not None:
if as_file: if as_file:
return f"file://{storage.path(settings.CUSTOM_LOGO)}" return f'file://{storage.path(settings.CUSTOM_LOGO)}'
return storage.url(settings.CUSTOM_LOGO) return storage.url(settings.CUSTOM_LOGO)
# If we have got to this point, return the default logo # If we have got to this point, return the default logo
if as_file: if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png') path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}" return f'file://{path}'
return getStaticUrl('img/inventree.png') return getStaticUrl('img/inventree.png')
@ -124,7 +124,7 @@ def getSplashScreen(custom=True):
return static_storage.url(settings.CUSTOM_SPLASH) return static_storage.url(settings.CUSTOM_SPLASH)
# No custom splash screen # No custom splash screen
return static_storage.url("img/inventree_splash.jpg") return static_storage.url('img/inventree_splash.jpg')
def TestIfImageURL(url): def TestIfImageURL(url):
@ -234,7 +234,7 @@ def increment(value):
# Provide a default value if provided with a null input # Provide a default value if provided with a null input
return '1' return '1'
pattern = r"(.*?)(\d+)?$" pattern = r'(.*?)(\d+)?$'
result = re.search(pattern, value) result = re.search(pattern, value)
@ -293,7 +293,7 @@ def decimal2string(d):
if '.' not in s: if '.' not in s:
return s return s
return s.rstrip("0").rstrip(".") return s.rstrip('0').rstrip('.')
def decimal2money(d, currency=None): def decimal2money(d, currency=None):
@ -395,7 +395,7 @@ def DownloadFile(
length = len(bytes(data, response.charset)) length = len(bytes(data, response.charset))
response['Content-Length'] = length response['Content-Length'] = length
disposition = "inline" if inline else "attachment" disposition = 'inline' if inline else 'attachment'
response['Content-Disposition'] = f'{disposition}; filename={filename}' response['Content-Disposition'] = f'{disposition}; filename={filename}'
@ -455,7 +455,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
try: try:
expected_quantity = int(expected_quantity) expected_quantity = int(expected_quantity)
except ValueError: except ValueError:
raise ValidationError([_("Invalid quantity provided")]) raise ValidationError([_('Invalid quantity provided')])
if input_string: if input_string:
input_string = str(input_string).strip() input_string = str(input_string).strip()
@ -463,7 +463,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
input_string = '' input_string = ''
if len(input_string) == 0: if len(input_string) == 0:
raise ValidationError([_("Empty serial number string")]) raise ValidationError([_('Empty serial number string')])
next_value = increment_serial_number(starting_value) next_value = increment_serial_number(starting_value)
@ -473,7 +473,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
next_value = increment_serial_number(next_value) next_value = increment_serial_number(next_value)
# Split input string by whitespace or comma (,) characters # Split input string by whitespace or comma (,) characters
groups = re.split(r"[\s,]+", input_string) groups = re.split(r'[\s,]+', input_string)
serials = [] serials = []
errors = [] errors = []
@ -493,7 +493,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
return return
if serial in serials: if serial in serials:
add_error(_("Duplicate serial") + f": {serial}") add_error(_('Duplicate serial') + f': {serial}')
else: else:
serials.append(serial) serials.append(serial)
@ -525,7 +525,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
if a == b: if a == b:
# Invalid group # Invalid group
add_error(_(f"Invalid group range: {group}")) add_error(_(f'Invalid group range: {group}'))
continue continue
group_items = [] group_items = []
@ -556,7 +556,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
if len(group_items) > remaining: if len(group_items) > remaining:
add_error( add_error(
_( _(
f"Group range {group} exceeds allowed quantity ({expected_quantity})" f'Group range {group} exceeds allowed quantity ({expected_quantity})'
) )
) )
elif ( elif (
@ -568,7 +568,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
for item in group_items: for item in group_items:
add_serial(item) add_serial(item)
else: else:
add_error(_(f"Invalid group range: {group}")) add_error(_(f'Invalid group range: {group}'))
else: else:
# In the case of a different number of hyphens, simply add the entire group # In the case of a different number of hyphens, simply add the entire group
@ -586,14 +586,14 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
sequence_count = max(0, expected_quantity - len(serials)) sequence_count = max(0, expected_quantity - len(serials))
if len(items) > 2 or len(items) == 0: if len(items) > 2 or len(items) == 0:
add_error(_(f"Invalid group sequence: {group}")) add_error(_(f'Invalid group sequence: {group}'))
continue continue
elif len(items) == 2: elif len(items) == 2:
try: try:
if items[1]: if items[1]:
sequence_count = int(items[1]) + 1 sequence_count = int(items[1]) + 1
except ValueError: except ValueError:
add_error(_(f"Invalid group sequence: {group}")) add_error(_(f'Invalid group sequence: {group}'))
continue continue
value = items[0] value = items[0]
@ -612,7 +612,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
for item in sequence_items: for item in sequence_items:
add_serial(item) add_serial(item)
else: else:
add_error(_(f"Invalid group sequence: {group}")) add_error(_(f'Invalid group sequence: {group}'))
else: else:
# At this point, we assume that the 'group' is just a single serial value # At this point, we assume that the 'group' is just a single serial value
@ -622,12 +622,12 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
raise ValidationError(errors) raise ValidationError(errors)
if len(serials) == 0: if len(serials) == 0:
raise ValidationError([_("No serial numbers found")]) raise ValidationError([_('No serial numbers found')])
if len(errors) == 0 and len(serials) != expected_quantity: if len(errors) == 0 and len(serials) != expected_quantity:
raise ValidationError([ raise ValidationError([
_( _(
f"Number of unique serial numbers ({len(serials)}) must match quantity ({expected_quantity})" f'Number of unique serial numbers ({len(serials)}) must match quantity ({expected_quantity})'
) )
]) ])
@ -666,7 +666,7 @@ def validateFilterString(value, model=None):
pair = group.split('=') pair = group.split('=')
if len(pair) != 2: if len(pair) != 2:
raise ValidationError(f"Invalid group: {group}") raise ValidationError(f'Invalid group: {group}')
k, v = pair k, v = pair
@ -674,7 +674,7 @@ def validateFilterString(value, model=None):
v = v.strip() v = v.strip()
if not k or not v: if not k or not v:
raise ValidationError(f"Invalid group: {group}") raise ValidationError(f'Invalid group: {group}')
results[k] = v results[k] = v
@ -745,7 +745,7 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
if len(cleaned) != len(value) and raise_error: if len(cleaned) != len(value) and raise_error:
field = field_name or 'non_field_errors' field = field_name or 'non_field_errors'
raise ValidationError({field: [_("Remove HTML tags from this value")]}) raise ValidationError({field: [_('Remove HTML tags from this value')]})
return cleaned return cleaned

View File

@ -120,7 +120,7 @@ def download_image_from_url(remote_url, timeout=2.5):
'INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT' 'INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT'
) )
if user_agent: if user_agent:
headers = {"User-Agent": user_agent} headers = {'User-Agent': user_agent}
else: else:
headers = None headers = None
@ -135,28 +135,28 @@ def download_image_from_url(remote_url, timeout=2.5):
# Throw an error if anything goes wrong # Throw an error if anything goes wrong
response.raise_for_status() response.raise_for_status()
except requests.exceptions.ConnectionError as exc: except requests.exceptions.ConnectionError as exc:
raise Exception(_("Connection error") + f": {str(exc)}") raise Exception(_('Connection error') + f': {str(exc)}')
except requests.exceptions.Timeout as exc: except requests.exceptions.Timeout as exc:
raise exc raise exc
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
raise requests.exceptions.HTTPError( raise requests.exceptions.HTTPError(
_("Server responded with invalid status code") + f": {response.status_code}" _('Server responded with invalid status code') + f': {response.status_code}'
) )
except Exception as exc: except Exception as exc:
raise Exception(_("Exception occurred") + f": {str(exc)}") raise Exception(_('Exception occurred') + f': {str(exc)}')
if response.status_code != 200: if response.status_code != 200:
raise Exception( raise Exception(
_("Server responded with invalid status code") + f": {response.status_code}" _('Server responded with invalid status code') + f': {response.status_code}'
) )
try: try:
content_length = int(response.headers.get('Content-Length', 0)) content_length = int(response.headers.get('Content-Length', 0))
except ValueError: except ValueError:
raise ValueError(_("Server responded with invalid Content-Length value")) raise ValueError(_('Server responded with invalid Content-Length value'))
if content_length > max_size: if content_length > max_size:
raise ValueError(_("Image size is too large")) raise ValueError(_('Image size is too large'))
# Download the file, ensuring we do not exceed the reported size # Download the file, ensuring we do not exceed the reported size
file = io.BytesIO() file = io.BytesIO()
@ -168,12 +168,12 @@ def download_image_from_url(remote_url, timeout=2.5):
dl_size += len(chunk) dl_size += len(chunk)
if dl_size > max_size: if dl_size > max_size:
raise ValueError(_("Image download exceeded maximum size")) raise ValueError(_('Image download exceeded maximum size'))
file.write(chunk) file.write(chunk)
if dl_size == 0: if dl_size == 0:
raise ValueError(_("Remote server returned empty response")) raise ValueError(_('Remote server returned empty response'))
# Now, attempt to convert the downloaded data to a valid image file # Now, attempt to convert the downloaded data to a valid image file
# img.verify() will throw an exception if the image is not valid # img.verify() will throw an exception if the image is not valid
@ -181,7 +181,7 @@ def download_image_from_url(remote_url, timeout=2.5):
img = Image.open(file).convert() img = Image.open(file).convert()
img.verify() img.verify()
except Exception: except Exception:
raise TypeError(_("Supplied URL is not a valid image file")) raise TypeError(_('Supplied URL is not a valid image file'))
return img return img

View File

@ -18,13 +18,13 @@ def send_simple_login_email(user, link):
"""Send an email with the login link to this user.""" """Send an email with the login link to this user."""
site = Site.objects.get_current() site = Site.objects.get_current()
context = {"username": user.username, "site_name": site.name, "link": link} context = {'username': user.username, 'site_name': site.name, 'link': link}
email_plaintext_message = render_to_string( email_plaintext_message = render_to_string(
"InvenTree/user_simple_login.txt", context 'InvenTree/user_simple_login.txt', context
) )
send_mail( send_mail(
_(f"[{site.name}] Log in to the app"), _(f'[{site.name}] Log in to the app'),
email_plaintext_message, email_plaintext_message,
settings.DEFAULT_FROM_EMAIL, settings.DEFAULT_FROM_EMAIL,
[user.email], [user.email],
@ -34,7 +34,7 @@ def send_simple_login_email(user, link):
class GetSimpleLoginSerializer(serializers.Serializer): class GetSimpleLoginSerializer(serializers.Serializer):
"""Serializer for the simple login view.""" """Serializer for the simple login view."""
email = serializers.CharField(label=_("Email")) email = serializers.CharField(label=_('Email'))
class GetSimpleLoginView(APIView): class GetSimpleLoginView(APIView):
@ -47,14 +47,14 @@ class GetSimpleLoginView(APIView):
"""Get the token for the current user or fail.""" """Get the token for the current user or fail."""
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
self.email_submitted(email=serializer.data["email"]) self.email_submitted(email=serializer.data['email'])
return Response({"status": "ok"}) return Response({'status': 'ok'})
def email_submitted(self, email): def email_submitted(self, email):
"""Notify user about link.""" """Notify user about link."""
user = self.get_user(email) user = self.get_user(email)
if user is None: if user is None:
print("user not found:", email) print('user not found:', email)
return return
link = self.create_link(user) link = self.create_link(user)
send_simple_login_email(user, link) send_simple_login_email(user, link)
@ -68,7 +68,7 @@ class GetSimpleLoginView(APIView):
def create_link(self, user): def create_link(self, user):
"""Create a login link for this user.""" """Create a login link for this user."""
link = reverse("sesame-login") link = reverse('sesame-login')
link = self.request.build_absolute_uri(link) link = self.request.build_absolute_uri(link)
link += sesame.utils.get_query_string(user) link += sesame.utils.get_query_string(user)
return link return link

View File

@ -12,7 +12,7 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
"""Cleanup old (undefined) settings in the database.""" """Cleanup old (undefined) settings in the database."""
logger.info("Collecting settings") logger.info('Collecting settings')
from common.models import InvenTreeSetting, InvenTreeUserSetting from common.models import InvenTreeSetting, InvenTreeUserSetting
# general settings # general settings
@ -35,4 +35,4 @@ class Command(BaseCommand):
setting.delete() setting.delete()
logger.info("deleted user setting '%s'", setting.key) logger.info("deleted user setting '%s'", setting.key)
logger.info("checked all settings") logger.info('checked all settings')

View File

@ -58,10 +58,10 @@ class Command(BaseCommand):
for file in os.listdir(SOURCE_DIR): for file in os.listdir(SOURCE_DIR):
path = os.path.join(SOURCE_DIR, file) path = os.path.join(SOURCE_DIR, file)
if os.path.exists(path) and os.path.isfile(path): if os.path.exists(path) and os.path.isfile(path):
print(f"render {file}") print(f'render {file}')
render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx) render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
else: else:
raise NotImplementedError( raise NotImplementedError(
'Using multi-level directories is not implemented at this point' 'Using multi-level directories is not implemented at this point'
) # TODO multilevel dir if needed ) # TODO multilevel dir if needed
print(f"rendered all files in {SOURCE_DIR}") print(f'rendered all files in {SOURCE_DIR}')

View File

@ -13,50 +13,50 @@ class Command(BaseCommand):
"""Rebuild all database models which leverage the MPTT structure.""" """Rebuild all database models which leverage the MPTT structure."""
# Part model # Part model
try: try:
print("Rebuilding Part objects") print('Rebuilding Part objects')
from part.models import Part from part.models import Part
Part.objects.rebuild() Part.objects.rebuild()
except Exception: except Exception:
print("Error rebuilding Part objects") print('Error rebuilding Part objects')
# Part category # Part category
try: try:
print("Rebuilding PartCategory objects") print('Rebuilding PartCategory objects')
from part.models import PartCategory from part.models import PartCategory
PartCategory.objects.rebuild() PartCategory.objects.rebuild()
except Exception: except Exception:
print("Error rebuilding PartCategory objects") print('Error rebuilding PartCategory objects')
# StockItem model # StockItem model
try: try:
print("Rebuilding StockItem objects") print('Rebuilding StockItem objects')
from stock.models import StockItem from stock.models import StockItem
StockItem.objects.rebuild() StockItem.objects.rebuild()
except Exception: except Exception:
print("Error rebuilding StockItem objects") print('Error rebuilding StockItem objects')
# StockLocation model # StockLocation model
try: try:
print("Rebuilding StockLocation objects") print('Rebuilding StockLocation objects')
from stock.models import StockLocation from stock.models import StockLocation
StockLocation.objects.rebuild() StockLocation.objects.rebuild()
except Exception: except Exception:
print("Error rebuilding StockLocation objects") print('Error rebuilding StockLocation objects')
# Build model # Build model
try: try:
print("Rebuilding Build objects") print('Rebuilding Build objects')
from build.models import Build from build.models import Build
Build.objects.rebuild() Build.objects.rebuild()
except Exception: except Exception:
print("Error rebuilding Build objects") print('Error rebuilding Build objects')

View File

@ -37,20 +37,20 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
"""Rebuild all thumbnail images.""" """Rebuild all thumbnail images."""
logger.info("Rebuilding Part thumbnails") logger.info('Rebuilding Part thumbnails')
for part in Part.objects.exclude(image=None): for part in Part.objects.exclude(image=None):
try: try:
self.rebuild_thumbnail(part) self.rebuild_thumbnail(part)
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError):
logger.exception("ERROR: Database read error.") logger.exception('ERROR: Database read error.')
break break
logger.info("Rebuilding Company thumbnails") logger.info('Rebuilding Company thumbnails')
for company in Company.objects.exclude(image=None): for company in Company.objects.exclude(image=None):
try: try:
self.rebuild_thumbnail(company) self.rebuild_thumbnail(company)
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError):
logger.exception("ERROR: abase read error.") logger.exception('ERROR: abase read error.')
break break

View File

@ -12,7 +12,7 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
"""Wait till the database is ready.""" """Wait till the database is ready."""
self.stdout.write("Waiting for database...") self.stdout.write('Waiting for database...')
connected = False connected = False
@ -25,12 +25,12 @@ class Command(BaseCommand):
connected = True connected = True
except OperationalError as e: except OperationalError as e:
self.stdout.write(f"Could not connect to database: {e}") self.stdout.write(f'Could not connect to database: {e}')
except ImproperlyConfigured as e: except ImproperlyConfigured as e:
self.stdout.write(f"Improperly configured: {e}") self.stdout.write(f'Improperly configured: {e}')
else: else:
if not connection.is_usable(): if not connection.is_usable():
self.stdout.write("Database configuration is not usable") self.stdout.write('Database configuration is not usable')
if connected: if connected:
self.stdout.write("Database connection successful!") self.stdout.write('Database connection successful!')

View File

@ -69,7 +69,7 @@ class InvenTreeMetadata(SimpleMetadata):
metadata['model'] = tbl_label metadata['model'] = tbl_label
table = f"{app_label}_{tbl_label}" table = f'{app_label}_{tbl_label}'
actions = metadata.get('actions', None) actions = metadata.get('actions', None)
@ -87,7 +87,7 @@ class InvenTreeMetadata(SimpleMetadata):
} }
# let the view define a custom rolemap # let the view define a custom rolemap
if hasattr(view, "rolemap"): if hasattr(view, 'rolemap'):
rolemap.update(view.rolemap) rolemap.update(view.rolemap)
# Remove any HTTP methods that the user does not have permission for # Remove any HTTP methods that the user does not have permission for
@ -264,7 +264,7 @@ class InvenTreeMetadata(SimpleMetadata):
model = field.queryset.model model = field.queryset.model
else: else:
logger.debug( logger.debug(
"Could not extract model for:", field_info.get('label'), '->', field 'Could not extract model for:', field_info.get('label'), '->', field
) )
model = None model = None
@ -286,4 +286,4 @@ class InvenTreeMetadata(SimpleMetadata):
return field_info return field_info
InvenTreeMetadata.label_lookup[DependentField] = "dependent field" InvenTreeMetadata.label_lookup[DependentField] = 'dependent field'

View File

@ -15,7 +15,7 @@ from error_report.middleware import ExceptionProcessor
from InvenTree.urls import frontendpatterns from InvenTree.urls import frontendpatterns
from users.models import ApiToken from users.models import ApiToken
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
class AuthRequiredMiddleware(object): class AuthRequiredMiddleware(object):
@ -91,7 +91,7 @@ class AuthRequiredMiddleware(object):
authorized = True authorized = True
except ApiToken.DoesNotExist: except ApiToken.DoesNotExist:
logger.warning("Access denied for unknown token %s", token_key) logger.warning('Access denied for unknown token %s', token_key)
# No authorization was found for the request # No authorization was found for the request
if not authorized: if not authorized:

View File

@ -303,7 +303,7 @@ class ReferenceIndexingMixin(models.Model):
if recent: if recent:
reference = recent.reference reference = recent.reference
else: else:
reference = "" reference = ''
return reference return reference
@ -316,20 +316,20 @@ class ReferenceIndexingMixin(models.Model):
info = InvenTree.format.parse_format_string(pattern) info = InvenTree.format.parse_format_string(pattern)
except Exception as exc: except Exception as exc:
raise ValidationError({ raise ValidationError({
"value": _("Improperly formatted pattern") + ": " + str(exc) 'value': _('Improperly formatted pattern') + ': ' + str(exc)
}) })
# Check that only 'allowed' keys are provided # Check that only 'allowed' keys are provided
for key in info.keys(): for key in info.keys():
if key not in ctx.keys(): if key not in ctx.keys():
raise ValidationError({ raise ValidationError({
"value": _("Unknown format key specified") + f": '{key}'" 'value': _('Unknown format key specified') + f": '{key}'"
}) })
# Check that the 'ref' variable is specified # Check that the 'ref' variable is specified
if 'ref' not in info.keys(): if 'ref' not in info.keys():
raise ValidationError({ raise ValidationError({
'value': _("Missing required format key") + ": 'ref'" 'value': _('Missing required format key') + ": 'ref'"
}) })
@classmethod @classmethod
@ -340,7 +340,7 @@ class ReferenceIndexingMixin(models.Model):
value = str(value).strip() value = str(value).strip()
if len(value) == 0: if len(value) == 0:
raise ValidationError(_("Reference field cannot be empty")) raise ValidationError(_('Reference field cannot be empty'))
# An 'empty' pattern means no further validation is required # An 'empty' pattern means no further validation is required
if not pattern: if not pattern:
@ -348,7 +348,7 @@ class ReferenceIndexingMixin(models.Model):
if not InvenTree.format.validate_string(value, pattern): if not InvenTree.format.validate_string(value, pattern):
raise ValidationError( raise ValidationError(
_("Reference must match required pattern") + ": " + pattern _('Reference must match required pattern') + ': ' + pattern
) )
# Check that the reference field can be rebuild # Check that the reference field can be rebuild
@ -380,7 +380,7 @@ class ReferenceIndexingMixin(models.Model):
if validate: if validate:
if reference_int > models.BigIntegerField.MAX_BIGINT: if reference_int > models.BigIntegerField.MAX_BIGINT:
raise ValidationError({"reference": _("Reference number is too large")}) raise ValidationError({'reference': _('Reference number is too large')})
return reference_int return reference_int
@ -399,7 +399,7 @@ def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
return 0 return 0
# Look at the start of the string - can it be "integerized"? # Look at the start of the string - can it be "integerized"?
result = re.match(r"^(\d+)", reference) result = re.match(r'^(\d+)', reference)
if result and len(result.groups()) == 1: if result and len(result.groups()) == 1:
ref = result.groups()[0] ref = result.groups()[0]
@ -455,7 +455,7 @@ class InvenTreeAttachment(models.Model):
Note: Re-implement this for each subclass of InvenTreeAttachment Note: Re-implement this for each subclass of InvenTreeAttachment
""" """
return "attachments" return 'attachments'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Provide better validation error.""" """Provide better validation error."""
@ -547,7 +547,7 @@ class InvenTreeAttachment(models.Model):
logger.error( logger.error(
"Attempted to rename attachment outside valid directory: '%s'", new_file "Attempted to rename attachment outside valid directory: '%s'", new_file
) )
raise ValidationError(_("Invalid attachment directory")) raise ValidationError(_('Invalid attachment directory'))
# Ignore further checks if the filename is not actually being renamed # Ignore further checks if the filename is not actually being renamed
if new_file == old_file: if new_file == old_file:
@ -556,23 +556,23 @@ class InvenTreeAttachment(models.Model):
forbidden = [ forbidden = [
"'", "'",
'"', '"',
"#", '#',
"@", '@',
"!", '!',
"&", '&',
"^", '^',
"<", '<',
">", '>',
":", ':',
";", ';',
"/", '/',
"\\", '\\',
"|", '|',
"?", '?',
"*", '*',
"%", '%',
"~", '~',
"`", '`',
] ]
for c in forbidden: for c in forbidden:
@ -580,7 +580,7 @@ class InvenTreeAttachment(models.Model):
raise ValidationError(_(f"Filename contains illegal character '{c}'")) raise ValidationError(_(f"Filename contains illegal character '{c}'"))
if len(fn.split('.')) < 2: if len(fn.split('.')) < 2:
raise ValidationError(_("Filename missing extension")) raise ValidationError(_('Filename missing extension'))
if not old_file.exists(): if not old_file.exists():
logger.error( logger.error(
@ -589,14 +589,14 @@ class InvenTreeAttachment(models.Model):
return return
if new_file.exists(): if new_file.exists():
raise ValidationError(_("Attachment with this filename already exists")) raise ValidationError(_('Attachment with this filename already exists'))
try: try:
os.rename(old_file, new_file) os.rename(old_file, new_file)
self.attachment.name = os.path.join(self.getSubdir(), fn) self.attachment.name = os.path.join(self.getSubdir(), fn)
self.save() self.save()
except Exception: except Exception:
raise ValidationError(_("Error renaming file")) raise ValidationError(_('Error renaming file'))
def fully_qualified_url(self): def fully_qualified_url(self):
"""Return a 'fully qualified' URL for this attachment. """Return a 'fully qualified' URL for this attachment.
@ -656,7 +656,7 @@ class InvenTreeTree(MPTTModel):
except self.__class__.DoesNotExist: except self.__class__.DoesNotExist:
# If the object no longer exists, raise a ValidationError # If the object no longer exists, raise a ValidationError
raise ValidationError( raise ValidationError(
"Object %s of type %s no longer exists", str(self), str(self.__class__) 'Object %s of type %s no longer exists', str(self), str(self.__class__)
) )
# Cache node ID values for lower nodes, before we delete this one # Cache node ID values for lower nodes, before we delete this one
@ -791,7 +791,7 @@ class InvenTreeTree(MPTTModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
except InvalidMove: except InvalidMove:
# Provide better error for parent selection # Provide better error for parent selection
raise ValidationError({'parent': _("Invalid choice")}) raise ValidationError({'parent': _('Invalid choice')})
# Re-calculate the 'pathstring' field # Re-calculate the 'pathstring' field
pathstring = self.construct_pathstring() pathstring = self.construct_pathstring()
@ -821,14 +821,14 @@ class InvenTreeTree(MPTTModel):
self.__class__.objects.bulk_update(nodes_to_update, ['pathstring']) self.__class__.objects.bulk_update(nodes_to_update, ['pathstring'])
name = models.CharField( name = models.CharField(
blank=False, max_length=100, verbose_name=_("Name"), help_text=_("Name") blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Name')
) )
description = models.CharField( description = models.CharField(
blank=True, blank=True,
max_length=250, max_length=250,
verbose_name=_("Description"), verbose_name=_('Description'),
help_text=_("Description (optional)"), help_text=_('Description (optional)'),
) )
# When a category is deleted, graft the children onto its parent # When a category is deleted, graft the children onto its parent
@ -837,7 +837,7 @@ class InvenTreeTree(MPTTModel):
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,
blank=True, blank=True,
null=True, null=True,
verbose_name=_("parent"), verbose_name=_('parent'),
related_name='children', related_name='children',
) )
@ -854,7 +854,7 @@ class InvenTreeTree(MPTTModel):
The default implementation returns an empty list The default implementation returns an empty list
""" """
raise NotImplementedError(f"items() method not implemented for {type(self)}") raise NotImplementedError(f'items() method not implemented for {type(self)}')
def getUniqueParents(self): def getUniqueParents(self):
"""Return a flat set of all parent items that exist above this node. """Return a flat set of all parent items that exist above this node.
@ -929,7 +929,7 @@ class InvenTreeTree(MPTTModel):
def __str__(self): def __str__(self):
"""String representation of a category is the full path to that category.""" """String representation of a category is the full path to that category."""
return f"{self.pathstring} - {self.description}" return f'{self.pathstring} - {self.description}'
class InvenTreeNotesMixin(models.Model): class InvenTreeNotesMixin(models.Model):
@ -1008,7 +1008,7 @@ class InvenTreeBarcodeMixin(models.Model):
if hasattr(self, 'get_api_url'): if hasattr(self, 'get_api_url'):
api_url = self.get_api_url() api_url = self.get_api_url()
data['api_url'] = f"{api_url}{self.pk}/" data['api_url'] = f'{api_url}{self.pk}/'
if hasattr(self, 'get_absolute_url'): if hasattr(self, 'get_absolute_url'):
data['web_url'] = self.get_absolute_url() data['web_url'] = self.get_absolute_url()
@ -1040,7 +1040,7 @@ class InvenTreeBarcodeMixin(models.Model):
# Check for existing item # Check for existing item
if self.__class__.lookup_barcode(barcode_hash) is not None: if self.__class__.lookup_barcode(barcode_hash) is not None:
if raise_error: if raise_error:
raise ValidationError(_("Existing barcode found")) raise ValidationError(_('Existing barcode found'))
else: else:
return False return False

View File

@ -18,7 +18,7 @@ def get_model_for_view(view, raise_error=True):
if hasattr(view, 'get_serializer_class'): if hasattr(view, 'get_serializer_class'):
return view.get_serializr_class().Meta.model return view.get_serializr_class().Meta.model
raise AttributeError(f"Serializer class not specified for {view.__class__}") raise AttributeError(f'Serializer class not specified for {view.__class__}')
class RolePermission(permissions.BasePermission): class RolePermission(permissions.BasePermission):
@ -62,7 +62,7 @@ class RolePermission(permissions.BasePermission):
} }
# let the view define a custom rolemap # let the view define a custom rolemap
if hasattr(view, "rolemap"): if hasattr(view, 'rolemap'):
rolemap.update(view.rolemap) rolemap.update(view.rolemap)
permission = rolemap[request.method] permission = rolemap[request.method]
@ -78,7 +78,7 @@ class RolePermission(permissions.BasePermission):
app_label = model._meta.app_label app_label = model._meta.app_label
model_name = model._meta.model_name model_name = model._meta.model_name
table = f"{app_label}_{model_name}" table = f'{app_label}_{model_name}'
except AttributeError: except AttributeError:
# We will assume that if the serializer class does *not* have a Meta, # We will assume that if the serializer class does *not* have a Meta,
# then we don't need a permission # then we don't need a permission

View File

@ -25,8 +25,8 @@ def isInMainThread():
- The RUN_MAIN env is set in that case. However if --noreload is applied, this variable - The RUN_MAIN env is set in that case. However if --noreload is applied, this variable
is not set because there are no different threads. is not set because there are no different threads.
""" """
if "runserver" in sys.argv and "--noreload" not in sys.argv: if 'runserver' in sys.argv and '--noreload' not in sys.argv:
return os.environ.get('RUN_MAIN', None) == "true" return os.environ.get('RUN_MAIN', None) == 'true'
return True return True

View File

@ -37,7 +37,7 @@ def sentry_ignore_errors():
def init_sentry(dsn, sample_rate, tags): def init_sentry(dsn, sample_rate, tags):
"""Initialize sentry.io error reporting""" """Initialize sentry.io error reporting"""
logger.info("Initializing sentry.io integration") logger.info('Initializing sentry.io integration')
sentry_sdk.init( sentry_sdk.init(
dsn=dsn, dsn=dsn,
@ -65,9 +65,9 @@ def report_exception(exc):
"""Report an exception to sentry.io""" """Report an exception to sentry.io"""
if settings.SENTRY_ENABLED and settings.SENTRY_DSN: if settings.SENTRY_ENABLED and settings.SENTRY_DSN:
if not any(isinstance(exc, e) for e in sentry_ignore_errors()): if not any(isinstance(exc, e) for e in sentry_ignore_errors()):
logger.info("Reporting exception to sentry.io: %s", exc) logger.info('Reporting exception to sentry.io: %s', exc)
try: try:
sentry_sdk.capture_exception(exc) sentry_sdk.capture_exception(exc)
except Exception: except Exception:
logger.warning("Failed to report exception to sentry.io") logger.warning('Failed to report exception to sentry.io')

View File

@ -37,9 +37,9 @@ class InvenTreeMoneySerializer(MoneyField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Override default values.""" """Override default values."""
kwargs["max_digits"] = kwargs.get("max_digits", 19) kwargs['max_digits'] = kwargs.get('max_digits', 19)
self.decimal_places = kwargs["decimal_places"] = kwargs.get("decimal_places", 6) self.decimal_places = kwargs['decimal_places'] = kwargs.get('decimal_places', 6)
kwargs["required"] = kwargs.get("required", False) kwargs['required'] = kwargs.get('required', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -57,7 +57,7 @@ class InvenTreeMoneySerializer(MoneyField):
amount = Decimal(amount) amount = Decimal(amount)
amount = round(amount, self.decimal_places) amount = round(amount, self.decimal_places)
except Exception: except Exception:
raise ValidationError({self.field_name: [_("Must be a valid number")]}) raise ValidationError({self.field_name: [_('Must be a valid number')]})
currency = data.get( currency = data.get(
get_currency_field_name(self.field_name), self.default_currency get_currency_field_name(self.field_name), self.default_currency
@ -134,7 +134,7 @@ class DependentField(serializers.Field):
def get_child(self, raise_exception=False): def get_child(self, raise_exception=False):
"""This method tries to extract the child based on the provided data in the request by the client.""" """This method tries to extract the child based on the provided data in the request by the client."""
data = deepcopy(self.context["request"].data) data = deepcopy(self.context['request'].data)
def visit_parent(node): def visit_parent(node):
"""Recursively extract the data for the parent field/serializer in reverse.""" """Recursively extract the data for the parent field/serializer in reverse."""
@ -144,7 +144,7 @@ class DependentField(serializers.Field):
visit_parent(node.parent) visit_parent(node.parent)
# only do for composite fields and stop right before the current field # only do for composite fields and stop right before the current field
if hasattr(node, "child") and node is not self and isinstance(data, dict): if hasattr(node, 'child') and node is not self and isinstance(data, dict):
data = data.get(node.field_name, None) data = data.get(node.field_name, None)
visit_parent(self) visit_parent(self)
@ -424,7 +424,7 @@ class ExendedUserSerializer(UserSerializer):
pass pass
else: else:
raise PermissionDenied( raise PermissionDenied(
_("You do not have permission to change this user role.") _('You do not have permission to change this user role.')
) )
return super().validate(attrs) return super().validate(attrs)
@ -436,7 +436,7 @@ class UserCreateSerializer(ExendedUserSerializer):
"""Expanded valiadation for auth.""" """Expanded valiadation for auth."""
# Check that the user trying to create a new user is a superuser # Check that the user trying to create a new user is a superuser
if not self.context['request'].user.is_superuser: if not self.context['request'].user.is_superuser:
raise serializers.ValidationError(_("Only superusers can create new users")) raise serializers.ValidationError(_('Only superusers can create new users'))
# Generate a random password # Generate a random password
password = User.objects.make_random_password(length=14) password = User.objects.make_random_password(length=14)
@ -453,9 +453,9 @@ class UserCreateSerializer(ExendedUserSerializer):
current_site = Site.objects.get_current() current_site = Site.objects.get_current()
domain = current_site.domain domain = current_site.domain
instance.email_user( instance.email_user(
subject=_(f"Welcome to {current_site.name}"), subject=_(f'Welcome to {current_site.name}'),
message=_( message=_(
f"Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain})." f'Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain}).'
), ),
) )
return instance return instance
@ -551,7 +551,7 @@ class InvenTreeDecimalField(serializers.FloatField):
try: try:
return Decimal(str(data)) return Decimal(str(data))
except Exception: except Exception:
raise serializers.ValidationError(_("Invalid value")) raise serializers.ValidationError(_('Invalid value'))
class DataFileUploadSerializer(serializers.Serializer): class DataFileUploadSerializer(serializers.Serializer):
@ -571,8 +571,8 @@ class DataFileUploadSerializer(serializers.Serializer):
fields = ['data_file'] fields = ['data_file']
data_file = serializers.FileField( data_file = serializers.FileField(
label=_("Data File"), label=_('Data File'),
help_text=_("Select data file for upload"), help_text=_('Select data file for upload'),
required=True, required=True,
allow_empty_file=False, allow_empty_file=False,
) )
@ -589,13 +589,13 @@ class DataFileUploadSerializer(serializers.Serializer):
accepted_file_types = ['xls', 'xlsx', 'csv', 'tsv', 'xml'] accepted_file_types = ['xls', 'xlsx', 'csv', 'tsv', 'xml']
if ext not in accepted_file_types: if ext not in accepted_file_types:
raise serializers.ValidationError(_("Unsupported file type")) raise serializers.ValidationError(_('Unsupported file type'))
# Impose a 50MB limit on uploaded BOM files # Impose a 50MB limit on uploaded BOM files
max_upload_file_size = 50 * 1024 * 1024 max_upload_file_size = 50 * 1024 * 1024
if data_file.size > max_upload_file_size: if data_file.size > max_upload_file_size:
raise serializers.ValidationError(_("File is too large")) raise serializers.ValidationError(_('File is too large'))
# Read file data into memory (bytes object) # Read file data into memory (bytes object)
try: try:
@ -616,10 +616,10 @@ class DataFileUploadSerializer(serializers.Serializer):
raise serializers.ValidationError(str(e)) raise serializers.ValidationError(str(e))
if len(self.dataset.headers) == 0: if len(self.dataset.headers) == 0:
raise serializers.ValidationError(_("No columns found in file")) raise serializers.ValidationError(_('No columns found in file'))
if len(self.dataset) == 0: if len(self.dataset) == 0:
raise serializers.ValidationError(_("No data rows found in file")) raise serializers.ValidationError(_('No data rows found in file'))
return data_file return data_file
@ -732,10 +732,10 @@ class DataFileExtractSerializer(serializers.Serializer):
self.rows = data.get('rows', []) self.rows = data.get('rows', [])
if len(self.rows) == 0: if len(self.rows) == 0:
raise serializers.ValidationError(_("No data rows provided")) raise serializers.ValidationError(_('No data rows provided'))
if len(self.columns) == 0: if len(self.columns) == 0:
raise serializers.ValidationError(_("No data columns supplied")) raise serializers.ValidationError(_('No data columns supplied'))
self.validate_extracted_columns() self.validate_extracted_columns()
@ -758,7 +758,7 @@ class DataFileExtractSerializer(serializers.Serializer):
processed_row = self.process_row(self.row_to_dict(row)) processed_row = self.process_row(self.row_to_dict(row))
if processed_row: if processed_row:
rows.append({"original": row, "data": processed_row}) rows.append({'original': row, 'data': processed_row})
return {'fields': model_fields, 'columns': self.columns, 'rows': rows} return {'fields': model_fields, 'columns': self.columns, 'rows': rows}
@ -834,8 +834,8 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
required=False, required=False,
allow_blank=False, allow_blank=False,
write_only=True, write_only=True,
label=_("Remote Image"), label=_('Remote Image'),
help_text=_("URL of remote image file"), help_text=_('URL of remote image file'),
) )
def validate_remote_image(self, url): def validate_remote_image(self, url):
@ -851,7 +851,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
'INVENTREE_DOWNLOAD_FROM_URL' 'INVENTREE_DOWNLOAD_FROM_URL'
): ):
raise ValidationError( raise ValidationError(
_("Downloading images from remote URL is not enabled") _('Downloading images from remote URL is not enabled')
) )
try: try:

View File

@ -52,7 +52,7 @@ if TESTING:
site_packages = '/usr/local/lib/python3.9/site-packages' site_packages = '/usr/local/lib/python3.9/site-packages'
if site_packages not in sys.path: if site_packages not in sys.path:
print("Adding missing site-packages path:", site_packages) print('Adding missing site-packages path:', site_packages)
sys.path.append(site_packages) sys.path.append(site_packages)
# Are environment variables manipulated by tests? Needs to be set by testing code # Are environment variables manipulated by tests? Needs to be set by testing code
@ -87,7 +87,7 @@ ENABLE_PLATFORM_FRONTEND = get_boolean_setting(
# Configure logging settings # Configure logging settings
log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING') log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
logging.basicConfig(level=log_level, format="%(asctime)s %(levelname)s %(message)s") logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s %(message)s')
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
log_level = 'WARNING' # pragma: no cover log_level = 'WARNING' # pragma: no cover
@ -109,7 +109,7 @@ if get_setting('INVENTREE_DB_LOGGING', 'db_logging', False):
LOGGING['loggers'] = {'django.db.backends': {'level': log_level or 'DEBUG'}} LOGGING['loggers'] = {'django.db.backends': {'level': log_level or 'DEBUG'}}
# Get a logger instance for this setup file # Get a logger instance for this setup file
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
# Load SECRET_KEY # Load SECRET_KEY
SECRET_KEY = config.get_secret_key() SECRET_KEY = config.get_secret_key()
@ -122,7 +122,7 @@ MEDIA_ROOT = config.get_media_dir()
# List of allowed hosts (default = allow all) # List of allowed hosts (default = allow all)
ALLOWED_HOSTS = get_setting( ALLOWED_HOSTS = get_setting(
"INVENTREE_ALLOWED_HOSTS", 'INVENTREE_ALLOWED_HOSTS',
config_key='allowed_hosts', config_key='allowed_hosts',
default_value=['*'], default_value=['*'],
typecast=list, typecast=list,
@ -135,11 +135,11 @@ CORS_URLS_REGEX = r'^/(api|media|static)/.*$'
# Extract CORS options from configuration file # Extract CORS options from configuration file
CORS_ORIGIN_ALLOW_ALL = get_boolean_setting( CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
"INVENTREE_CORS_ORIGIN_ALLOW_ALL", config_key='cors.allow_all', default_value=False 'INVENTREE_CORS_ORIGIN_ALLOW_ALL', config_key='cors.allow_all', default_value=False
) )
CORS_ORIGIN_WHITELIST = get_setting( CORS_ORIGIN_WHITELIST = get_setting(
"INVENTREE_CORS_ORIGIN_WHITELIST", 'INVENTREE_CORS_ORIGIN_WHITELIST',
config_key='cors.whitelist', config_key='cors.whitelist',
default_value=[], default_value=[],
typecast=list, typecast=list,
@ -279,43 +279,43 @@ AUTHENTICATION_BACKENDS = CONFIG.get(
'django.contrib.auth.backends.RemoteUserBackend', # proxy login 'django.contrib.auth.backends.RemoteUserBackend', # proxy login
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers 'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
"sesame.backends.ModelBackend", # Magic link login django-sesame 'sesame.backends.ModelBackend', # Magic link login django-sesame
], ],
) )
# LDAP support # LDAP support
LDAP_AUTH = get_boolean_setting("INVENTREE_LDAP_ENABLED", "ldap.enabled", False) LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False)
if LDAP_AUTH: if LDAP_AUTH:
import ldap import ldap
from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPSearch from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPSearch
AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend") AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
# debug mode to troubleshoot configuration # debug mode to troubleshoot configuration
LDAP_DEBUG = get_boolean_setting("INVENTREE_LDAP_DEBUG", "ldap.debug", False) LDAP_DEBUG = get_boolean_setting('INVENTREE_LDAP_DEBUG', 'ldap.debug', False)
if LDAP_DEBUG: if LDAP_DEBUG:
if "loggers" not in LOGGING: if 'loggers' not in LOGGING:
LOGGING["loggers"] = {} LOGGING['loggers'] = {}
LOGGING["loggers"]["django_auth_ldap"] = { LOGGING['loggers']['django_auth_ldap'] = {
"level": "DEBUG", 'level': 'DEBUG',
"handlers": ["console"], 'handlers': ['console'],
} }
# get global options from dict and use ldap.OPT_* as keys and values # get global options from dict and use ldap.OPT_* as keys and values
global_options_dict = get_setting( global_options_dict = get_setting(
"INVENTREE_LDAP_GLOBAL_OPTIONS", "ldap.global_options", {}, dict 'INVENTREE_LDAP_GLOBAL_OPTIONS', 'ldap.global_options', {}, dict
) )
global_options = {} global_options = {}
for k, v in global_options_dict.items(): for k, v in global_options_dict.items():
# keys are always ldap.OPT_* constants # keys are always ldap.OPT_* constants
k_attr = getattr(ldap, k, None) k_attr = getattr(ldap, k, None)
if not k.startswith("OPT_") or k_attr is None: if not k.startswith('OPT_') or k_attr is None:
print(f"[LDAP] ldap.global_options, key '{k}' not found, skipping...") print(f"[LDAP] ldap.global_options, key '{k}' not found, skipping...")
continue continue
# values can also be other strings, e.g. paths # values can also be other strings, e.g. paths
v_attr = v v_attr = v
if v.startswith("OPT_"): if v.startswith('OPT_'):
v_attr = getattr(ldap, v, None) v_attr = getattr(ldap, v, None)
if v_attr is None: if v_attr is None:
@ -325,55 +325,55 @@ if LDAP_AUTH:
global_options[k_attr] = v_attr global_options[k_attr] = v_attr
AUTH_LDAP_GLOBAL_OPTIONS = global_options AUTH_LDAP_GLOBAL_OPTIONS = global_options
if LDAP_DEBUG: if LDAP_DEBUG:
print("[LDAP] ldap.global_options =", global_options) print('[LDAP] ldap.global_options =', global_options)
AUTH_LDAP_SERVER_URI = get_setting("INVENTREE_LDAP_SERVER_URI", "ldap.server_uri") AUTH_LDAP_SERVER_URI = get_setting('INVENTREE_LDAP_SERVER_URI', 'ldap.server_uri')
AUTH_LDAP_START_TLS = get_boolean_setting( AUTH_LDAP_START_TLS = get_boolean_setting(
"INVENTREE_LDAP_START_TLS", "ldap.start_tls", False 'INVENTREE_LDAP_START_TLS', 'ldap.start_tls', False
) )
AUTH_LDAP_BIND_DN = get_setting("INVENTREE_LDAP_BIND_DN", "ldap.bind_dn") AUTH_LDAP_BIND_DN = get_setting('INVENTREE_LDAP_BIND_DN', 'ldap.bind_dn')
AUTH_LDAP_BIND_PASSWORD = get_setting( AUTH_LDAP_BIND_PASSWORD = get_setting(
"INVENTREE_LDAP_BIND_PASSWORD", "ldap.bind_password" 'INVENTREE_LDAP_BIND_PASSWORD', 'ldap.bind_password'
) )
AUTH_LDAP_USER_SEARCH = LDAPSearch( AUTH_LDAP_USER_SEARCH = LDAPSearch(
get_setting("INVENTREE_LDAP_SEARCH_BASE_DN", "ldap.search_base_dn"), get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'),
ldap.SCOPE_SUBTREE, ldap.SCOPE_SUBTREE,
str( str(
get_setting( get_setting(
"INVENTREE_LDAP_SEARCH_FILTER_STR", 'INVENTREE_LDAP_SEARCH_FILTER_STR',
"ldap.search_filter_str", 'ldap.search_filter_str',
"(uid= %(user)s)", '(uid= %(user)s)',
) )
), ),
) )
AUTH_LDAP_USER_DN_TEMPLATE = get_setting( AUTH_LDAP_USER_DN_TEMPLATE = get_setting(
"INVENTREE_LDAP_USER_DN_TEMPLATE", "ldap.user_dn_template" 'INVENTREE_LDAP_USER_DN_TEMPLATE', 'ldap.user_dn_template'
) )
AUTH_LDAP_USER_ATTR_MAP = get_setting( AUTH_LDAP_USER_ATTR_MAP = get_setting(
"INVENTREE_LDAP_USER_ATTR_MAP", 'INVENTREE_LDAP_USER_ATTR_MAP',
"ldap.user_attr_map", 'ldap.user_attr_map',
{'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'}, {'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'},
dict, dict,
) )
AUTH_LDAP_ALWAYS_UPDATE_USER = get_boolean_setting( AUTH_LDAP_ALWAYS_UPDATE_USER = get_boolean_setting(
"INVENTREE_LDAP_ALWAYS_UPDATE_USER", "ldap.always_update_user", True 'INVENTREE_LDAP_ALWAYS_UPDATE_USER', 'ldap.always_update_user', True
) )
AUTH_LDAP_CACHE_TIMEOUT = get_setting( AUTH_LDAP_CACHE_TIMEOUT = get_setting(
"INVENTREE_LDAP_CACHE_TIMEOUT", "ldap.cache_timeout", 3600, int 'INVENTREE_LDAP_CACHE_TIMEOUT', 'ldap.cache_timeout', 3600, int
) )
AUTH_LDAP_GROUP_SEARCH = LDAPSearch( AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
get_setting("INVENTREE_LDAP_GROUP_SEARCH", "ldap.group_search"), get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'),
ldap.SCOPE_SUBTREE, ldap.SCOPE_SUBTREE,
"(objectClass=groupOfUniqueNames)", '(objectClass=groupOfUniqueNames)',
) )
AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType(name_attr="cn") AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType(name_attr='cn')
AUTH_LDAP_REQUIRE_GROUP = get_setting( AUTH_LDAP_REQUIRE_GROUP = get_setting(
"INVENTREE_LDAP_REQUIRE_GROUP", "ldap.require_group" 'INVENTREE_LDAP_REQUIRE_GROUP', 'ldap.require_group'
) )
AUTH_LDAP_DENY_GROUP = get_setting("INVENTREE_LDAP_DENY_GROUP", "ldap.deny_group") AUTH_LDAP_DENY_GROUP = get_setting('INVENTREE_LDAP_DENY_GROUP', 'ldap.deny_group')
AUTH_LDAP_USER_FLAGS_BY_GROUP = get_setting( AUTH_LDAP_USER_FLAGS_BY_GROUP = get_setting(
"INVENTREE_LDAP_USER_FLAGS_BY_GROUP", "ldap.user_flags_by_group", {}, dict 'INVENTREE_LDAP_USER_FLAGS_BY_GROUP', 'ldap.user_flags_by_group', {}, dict
) )
AUTH_LDAP_FIND_GROUP_PERMS = True AUTH_LDAP_FIND_GROUP_PERMS = True
@ -383,7 +383,7 @@ DEBUG_TOOLBAR_ENABLED = DEBUG and get_setting(
# If the debug toolbar is enabled, add the modules # If the debug toolbar is enabled, add the modules
if DEBUG_TOOLBAR_ENABLED: # pragma: no cover if DEBUG_TOOLBAR_ENABLED: # pragma: no cover
logger.info("Running with DEBUG_TOOLBAR enabled") logger.info('Running with DEBUG_TOOLBAR enabled')
INSTALLED_APPS.append('debug_toolbar') INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
@ -401,9 +401,9 @@ DOCKER = get_boolean_setting('INVENTREE_DOCKER', default_value=False)
if DOCKER: # pragma: no cover if DOCKER: # pragma: no cover
# Internal IP addresses are different when running under docker # Internal IP addresses are different when running under docker
hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname()) hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [ INTERNAL_IPS = [ip[: ip.rfind('.')] + '.1' for ip in ips] + [
"127.0.0.1", '127.0.0.1',
"10.0.2.2", '10.0.2.2',
] ]
# Allow secure http developer server in debug mode # Allow secure http developer server in debug mode
@ -521,7 +521,7 @@ Configure the database backend based on the user-specified values.
- The following code lets the user "mix and match" database configuration - The following code lets the user "mix and match" database configuration
""" """
logger.debug("Configuring database backend:") logger.debug('Configuring database backend:')
# Extract database configuration from the config.yaml file # Extract database configuration from the config.yaml file
db_config = CONFIG.get('database', {}) db_config = CONFIG.get('database', {})
@ -535,7 +535,7 @@ db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
for key in db_keys: for key in db_keys:
# First, check the environment variables # First, check the environment variables
env_key = f"INVENTREE_DB_{key}" env_key = f'INVENTREE_DB_{key}'
env_var = os.environ.get(env_key, None) env_var = os.environ.get(env_key, None)
if env_var: if env_var:
@ -544,7 +544,7 @@ for key in db_keys:
try: try:
env_var = int(env_var) env_var = int(env_var)
except ValueError: except ValueError:
logger.exception("Invalid number for %s: %s", env_key, env_var) logger.exception('Invalid number for %s: %s', env_key, env_var)
# Override configuration value # Override configuration value
db_config[key] = env_var db_config[key] = env_var
@ -585,9 +585,9 @@ if 'sqlite' in db_engine:
db_name = str(Path(db_name).resolve()) db_name = str(Path(db_name).resolve())
db_config['NAME'] = db_name db_config['NAME'] = db_name
logger.info("DB_ENGINE: %s", db_engine) logger.info('DB_ENGINE: %s', db_engine)
logger.info("DB_NAME: %s", db_name) logger.info('DB_NAME: %s', db_name)
logger.info("DB_HOST: %s", db_host) logger.info('DB_HOST: %s', db_host)
""" """
In addition to base-level database configuration, we may wish to specify specific options to the database backend In addition to base-level database configuration, we may wish to specify specific options to the database backend
@ -600,21 +600,21 @@ Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS
# connecting to the database server (such as a replica failover) don't sit and # connecting to the database server (such as a replica failover) don't sit and
# wait for possibly an hour or more, just tell the client something went wrong # wait for possibly an hour or more, just tell the client something went wrong
# and let the client retry when they want to. # and let the client retry when they want to.
db_options = db_config.get("OPTIONS", db_config.get("options", {})) db_options = db_config.get('OPTIONS', db_config.get('options', {}))
# Specific options for postgres backend # Specific options for postgres backend
if "postgres" in db_engine: # pragma: no cover if 'postgres' in db_engine: # pragma: no cover
from psycopg2.extensions import ( from psycopg2.extensions import (
ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_READ_COMMITTED,
ISOLATION_LEVEL_SERIALIZABLE, ISOLATION_LEVEL_SERIALIZABLE,
) )
# Connection timeout # Connection timeout
if "connect_timeout" not in db_options: if 'connect_timeout' not in db_options:
# The DB server is in the same data center, it should not take very # The DB server is in the same data center, it should not take very
# long to connect to the database server # long to connect to the database server
# # seconds, 2 is minimum allowed by libpq # # seconds, 2 is minimum allowed by libpq
db_options["connect_timeout"] = int( db_options['connect_timeout'] = int(
get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2) get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
) )
@ -624,36 +624,36 @@ if "postgres" in db_engine: # pragma: no cover
# issue to resolve itself. It it that doesn't happen whatever happened # issue to resolve itself. It it that doesn't happen whatever happened
# is probably fatal and no amount of waiting is going to fix it. # is probably fatal and no amount of waiting is going to fix it.
# # 0 - TCP Keepalives disabled; 1 - enabled # # 0 - TCP Keepalives disabled; 1 - enabled
if "keepalives" not in db_options: if 'keepalives' not in db_options:
db_options["keepalives"] = int( db_options['keepalives'] = int(
get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1) get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1)
) )
# Seconds after connection is idle to send keep alive # Seconds after connection is idle to send keep alive
if "keepalives_idle" not in db_options: if 'keepalives_idle' not in db_options:
db_options["keepalives_idle"] = int( db_options['keepalives_idle'] = int(
get_setting( get_setting(
'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1 'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1
) )
) )
# Seconds after missing ACK to send another keep alive # Seconds after missing ACK to send another keep alive
if "keepalives_interval" not in db_options: if 'keepalives_interval' not in db_options:
db_options["keepalives_interval"] = int( db_options['keepalives_interval'] = int(
get_setting( get_setting(
"INVENTREE_DB_TCP_KEEPALIVES_INTERVAL", 'INVENTREE_DB_TCP_KEEPALIVES_INTERVAL',
"database.tcp_keepalives_internal", 'database.tcp_keepalives_internal',
"1", '1',
) )
) )
# Number of missing ACKs before we close the connection # Number of missing ACKs before we close the connection
if "keepalives_count" not in db_options: if 'keepalives_count' not in db_options:
db_options["keepalives_count"] = int( db_options['keepalives_count'] = int(
get_setting( get_setting(
"INVENTREE_DB_TCP_KEEPALIVES_COUNT", 'INVENTREE_DB_TCP_KEEPALIVES_COUNT',
"database.tcp_keepalives_count", 'database.tcp_keepalives_count',
"5", '5',
) )
) )
@ -668,18 +668,18 @@ if "postgres" in db_engine: # pragma: no cover
# protect against simultaneous changes. # protect against simultaneous changes.
# https://www.postgresql.org/docs/devel/transaction-iso.html # https://www.postgresql.org/docs/devel/transaction-iso.html
# https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level # https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level
if "isolation_level" not in db_options: if 'isolation_level' not in db_options:
serializable = get_boolean_setting( serializable = get_boolean_setting(
'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False 'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
) )
db_options["isolation_level"] = ( db_options['isolation_level'] = (
ISOLATION_LEVEL_SERIALIZABLE ISOLATION_LEVEL_SERIALIZABLE
if serializable if serializable
else ISOLATION_LEVEL_READ_COMMITTED else ISOLATION_LEVEL_READ_COMMITTED
) )
# Specific options for MySql / MariaDB backend # Specific options for MySql / MariaDB backend
elif "mysql" in db_engine: # pragma: no cover elif 'mysql' in db_engine: # pragma: no cover
# TODO TCP time outs and keepalives # TODO TCP time outs and keepalives
# MariaDB's default isolation level is Repeatable Read which is # MariaDB's default isolation level is Repeatable Read which is
@ -688,16 +688,16 @@ elif "mysql" in db_engine: # pragma: no cover
# protect against siumltaneous changes. # protect against siumltaneous changes.
# https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level # https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level
# https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level # https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level
if "isolation_level" not in db_options: if 'isolation_level' not in db_options:
serializable = get_boolean_setting( serializable = get_boolean_setting(
'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False 'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
) )
db_options["isolation_level"] = ( db_options['isolation_level'] = (
"serializable" if serializable else "read committed" 'serializable' if serializable else 'read committed'
) )
# Specific options for sqlite backend # Specific options for sqlite backend
elif "sqlite" in db_engine: elif 'sqlite' in db_engine:
# TODO: Verify timeouts are not an issue because no network is involved for SQLite # TODO: Verify timeouts are not an issue because no network is involved for SQLite
# SQLite's default isolation level is Serializable due to SQLite's # SQLite's default isolation level is Serializable due to SQLite's
@ -756,30 +756,30 @@ if cache_host: # pragma: no cover
# so don't wait too long for the cache as nothing in the cache should be # so don't wait too long for the cache as nothing in the cache should be
# irreplaceable. # irreplaceable.
_cache_options = { _cache_options = {
"CLIENT_CLASS": "django_redis.client.DefaultClient", 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")), 'SOCKET_CONNECT_TIMEOUT': int(os.getenv('CACHE_CONNECT_TIMEOUT', '2')),
"SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")), 'SOCKET_TIMEOUT': int(os.getenv('CACHE_SOCKET_TIMEOUT', '2')),
"CONNECTION_POOL_KWARGS": { 'CONNECTION_POOL_KWARGS': {
"socket_keepalive": config.is_true(os.getenv("CACHE_TCP_KEEPALIVE", "1")), 'socket_keepalive': config.is_true(os.getenv('CACHE_TCP_KEEPALIVE', '1')),
"socket_keepalive_options": { 'socket_keepalive_options': {
socket.TCP_KEEPCNT: int(os.getenv("CACHE_KEEPALIVES_COUNT", "5")), socket.TCP_KEEPCNT: int(os.getenv('CACHE_KEEPALIVES_COUNT', '5')),
socket.TCP_KEEPIDLE: int(os.getenv("CACHE_KEEPALIVES_IDLE", "1")), socket.TCP_KEEPIDLE: int(os.getenv('CACHE_KEEPALIVES_IDLE', '1')),
socket.TCP_KEEPINTVL: int(os.getenv("CACHE_KEEPALIVES_INTERVAL", "1")), socket.TCP_KEEPINTVL: int(os.getenv('CACHE_KEEPALIVES_INTERVAL', '1')),
socket.TCP_USER_TIMEOUT: int( socket.TCP_USER_TIMEOUT: int(
os.getenv("CACHE_TCP_USER_TIMEOUT", "1000") os.getenv('CACHE_TCP_USER_TIMEOUT', '1000')
), ),
}, },
}, },
} }
CACHES = { CACHES = {
"default": { 'default': {
"BACKEND": "django_redis.cache.RedisCache", 'BACKEND': 'django_redis.cache.RedisCache',
"LOCATION": f"redis://{cache_host}:{cache_port}/0", 'LOCATION': f'redis://{cache_host}:{cache_port}/0',
"OPTIONS": _cache_options, 'OPTIONS': _cache_options,
} }
} }
else: else:
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
_q_worker_timeout = int( _q_worker_timeout = int(
get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90) get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90)
@ -813,7 +813,7 @@ if SENTRY_ENABLED and SENTRY_DSN:
if cache_host: # pragma: no cover if cache_host: # pragma: no cover
# If using external redis cache, make the cache the broker for Django Q # If using external redis cache, make the cache the broker for Django Q
# as well # as well
Q_CLUSTER["django_redis"] = "worker" Q_CLUSTER['django_redis'] = 'worker'
# database user sessions # database user sessions
SESSION_ENGINE = 'user_sessions.backends.db' SESSION_ENGINE = 'user_sessions.backends.db'
@ -840,7 +840,7 @@ AUTH_PASSWORD_VALIDATORS = [
EXTRA_URL_SCHEMES = get_setting('INVENTREE_EXTRA_URL_SCHEMES', 'extra_url_schemes', []) EXTRA_URL_SCHEMES = get_setting('INVENTREE_EXTRA_URL_SCHEMES', 'extra_url_schemes', [])
if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover
logger.warning("extra_url_schemes not correctly formatted") logger.warning('extra_url_schemes not correctly formatted')
EXTRA_URL_SCHEMES = [] EXTRA_URL_SCHEMES = []
# Internationalization # Internationalization
@ -912,7 +912,7 @@ CURRENCIES = get_setting(
# Ensure that at least one currency value is available # Ensure that at least one currency value is available
if len(CURRENCIES) == 0: # pragma: no cover if len(CURRENCIES) == 0: # pragma: no cover
logger.warning("No currencies selected: Defaulting to USD") logger.warning('No currencies selected: Defaulting to USD')
CURRENCIES = ['USD'] CURRENCIES = ['USD']
# Maximum number of decimal places for currency rendering # Maximum number of decimal places for currency rendering
@ -965,7 +965,7 @@ USE_L10N = True
if not TESTING: if not TESTING:
USE_TZ = True # pragma: no cover USE_TZ = True # pragma: no cover
DATE_INPUT_FORMATS = ["%Y-%m-%d"] DATE_INPUT_FORMATS = ['%Y-%m-%d']
# crispy forms use the bootstrap templates # crispy forms use the bootstrap templates
CRISPY_TEMPLATE_PACK = 'bootstrap4' CRISPY_TEMPLATE_PACK = 'bootstrap4'
@ -1090,7 +1090,7 @@ PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None) SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
if SITE_URL: if SITE_URL:
logger.info("Site URL: %s", SITE_URL) logger.info('Site URL: %s', SITE_URL)
# Check that the site URL is valid # Check that the site URL is valid
validator = URLValidator() validator = URLValidator()
@ -1111,7 +1111,7 @@ FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform') FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
if DEBUG: if DEBUG:
logger.info("InvenTree running with DEBUG enabled") logger.info('InvenTree running with DEBUG enabled')
logger.info("MEDIA_ROOT: '%s'", MEDIA_ROOT) logger.info("MEDIA_ROOT: '%s'", MEDIA_ROOT)
logger.info("STATIC_ROOT: '%s'", STATIC_ROOT) logger.info("STATIC_ROOT: '%s'", STATIC_ROOT)
@ -1131,12 +1131,12 @@ FLAGS = {
CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict) CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict)
if CUSTOM_FLAGS: if CUSTOM_FLAGS:
if not isinstance(CUSTOM_FLAGS, dict): if not isinstance(CUSTOM_FLAGS, dict):
logger.error("Invalid custom flags, must be valid dict: %s", str(CUSTOM_FLAGS)) logger.error('Invalid custom flags, must be valid dict: %s', str(CUSTOM_FLAGS))
else: else:
logger.info("Custom flags: %s", str(CUSTOM_FLAGS)) logger.info('Custom flags: %s', str(CUSTOM_FLAGS))
FLAGS.update(CUSTOM_FLAGS) FLAGS.update(CUSTOM_FLAGS)
# Magic login django-sesame # Magic login django-sesame
SESAME_MAX_AGE = 300 SESAME_MAX_AGE = 300
# LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/" # LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/"
LOGIN_REDIRECT_URL = "/index/" LOGIN_REDIRECT_URL = '/index/'

View File

@ -74,9 +74,9 @@ provider_urlpatterns = []
for name, provider in providers.registry.provider_map.items(): for name, provider in providers.registry.provider_map.items():
try: try:
prov_mod = import_module(provider.get_package() + ".views") prov_mod = import_module(provider.get_package() + '.views')
except ImportError: except ImportError:
logger.exception("Could not import authentication provider %s", name) logger.exception('Could not import authentication provider %s', name)
continue continue
# Try to extract the adapter class # Try to extract the adapter class

View File

@ -48,7 +48,7 @@ def check_provider(provider, raise_error=False):
if allauth.app_settings.SITES_ENABLED: if allauth.app_settings.SITES_ENABLED:
# At least one matching site must be specified # At least one matching site must be specified
if not app.sites.exists(): if not app.sites.exists():
logger.error("SocialApp %s has no sites configured", app) logger.error('SocialApp %s has no sites configured', app)
return False return False
# At this point, we assume that the provider is correctly configured # At this point, we assume that the provider is correctly configured

View File

@ -13,7 +13,7 @@ from django_q.status import Stat
import InvenTree.email import InvenTree.email
import InvenTree.ready import InvenTree.ready
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
def is_worker_running(**kwargs): def is_worker_running(**kwargs):
@ -63,13 +63,13 @@ def check_system_health(**kwargs):
if not is_worker_running(**kwargs): # pragma: no cover if not is_worker_running(**kwargs): # pragma: no cover
result = False result = False
logger.warning(_("Background worker check failed")) logger.warning(_('Background worker check failed'))
if not InvenTree.email.is_email_configured(): # pragma: no cover if not InvenTree.email.is_email_configured(): # pragma: no cover
result = False result = False
logger.warning(_("Email backend not configured")) logger.warning(_('Email backend not configured'))
if not result: # pragma: no cover if not result: # pragma: no cover
logger.warning(_("InvenTree system health checks failed")) logger.warning(_('InvenTree system health checks failed'))
return result return result

View File

@ -9,12 +9,12 @@ class PurchaseOrderStatus(StatusCode):
"""Defines a set of status codes for a PurchaseOrder.""" """Defines a set of status codes for a PurchaseOrder."""
# Order status codes # Order status codes
PENDING = 10, _("Pending"), 'secondary' # Order is pending (not yet placed) PENDING = 10, _('Pending'), 'secondary' # Order is pending (not yet placed)
PLACED = 20, _("Placed"), 'primary' # Order has been placed with supplier PLACED = 20, _('Placed'), 'primary' # Order has been placed with supplier
COMPLETE = 30, _("Complete"), 'success' # Order has been completed COMPLETE = 30, _('Complete'), 'success' # Order has been completed
CANCELLED = 40, _("Cancelled"), 'danger' # Order was cancelled CANCELLED = 40, _('Cancelled'), 'danger' # Order was cancelled
LOST = 50, _("Lost"), 'warning' # Order was lost LOST = 50, _('Lost'), 'warning' # Order was lost
RETURNED = 60, _("Returned"), 'warning' # Order was returned RETURNED = 60, _('Returned'), 'warning' # Order was returned
class PurchaseOrderStatusGroups: class PurchaseOrderStatusGroups:
@ -34,16 +34,16 @@ class PurchaseOrderStatusGroups:
class SalesOrderStatus(StatusCode): class SalesOrderStatus(StatusCode):
"""Defines a set of status codes for a SalesOrder.""" """Defines a set of status codes for a SalesOrder."""
PENDING = 10, _("Pending"), 'secondary' # Order is pending PENDING = 10, _('Pending'), 'secondary' # Order is pending
IN_PROGRESS = ( IN_PROGRESS = (
15, 15,
_("In Progress"), _('In Progress'),
'primary', 'primary',
) # Order has been issued, and is in progress ) # Order has been issued, and is in progress
SHIPPED = 20, _("Shipped"), 'success' # Order has been shipped to customer SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer
CANCELLED = 40, _("Cancelled"), 'danger' # Order has been cancelled CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled
LOST = 50, _("Lost"), 'warning' # Order was lost LOST = 50, _('Lost'), 'warning' # Order was lost
RETURNED = 60, _("Returned"), 'warning' # Order was returned RETURNED = 60, _('Returned'), 'warning' # Order was returned
class SalesOrderStatusGroups: class SalesOrderStatusGroups:
@ -59,18 +59,18 @@ class SalesOrderStatusGroups:
class StockStatus(StatusCode): class StockStatus(StatusCode):
"""Status codes for Stock.""" """Status codes for Stock."""
OK = 10, _("OK"), 'success' # Item is OK OK = 10, _('OK'), 'success' # Item is OK
ATTENTION = 50, _("Attention needed"), 'warning' # Item requires attention ATTENTION = 50, _('Attention needed'), 'warning' # Item requires attention
DAMAGED = 55, _("Damaged"), 'warning' # Item is damaged DAMAGED = 55, _('Damaged'), 'warning' # Item is damaged
DESTROYED = 60, _("Destroyed"), 'danger' # Item is destroyed DESTROYED = 60, _('Destroyed'), 'danger' # Item is destroyed
REJECTED = 65, _("Rejected"), 'danger' # Item is rejected REJECTED = 65, _('Rejected'), 'danger' # Item is rejected
LOST = 70, _("Lost"), 'dark' # Item has been lost LOST = 70, _('Lost'), 'dark' # Item has been lost
QUARANTINED = ( QUARANTINED = (
75, 75,
_("Quarantined"), _('Quarantined'),
'info', 'info',
) # Item has been quarantined and is unavailable ) # Item has been quarantined and is unavailable
RETURNED = 85, _("Returned"), 'warning' # Item has been returned from a customer RETURNED = 85, _('Returned'), 'warning' # Item has been returned from a customer
class StockStatusGroups: class StockStatusGroups:
@ -129,7 +129,7 @@ class StockHistoryCode(StatusCode):
BUILD_CONSUMED = 57, _('Consumed by build order') BUILD_CONSUMED = 57, _('Consumed by build order')
# Sales order codes # Sales order codes
SHIPPED_AGAINST_SALES_ORDER = 60, _("Shipped against Sales Order") SHIPPED_AGAINST_SALES_ORDER = 60, _('Shipped against Sales Order')
# Purchase order codes # Purchase order codes
RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order') RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
@ -145,10 +145,10 @@ class StockHistoryCode(StatusCode):
class BuildStatus(StatusCode): class BuildStatus(StatusCode):
"""Build status codes.""" """Build status codes."""
PENDING = 10, _("Pending"), 'secondary' # Build is pending / active PENDING = 10, _('Pending'), 'secondary' # Build is pending / active
PRODUCTION = 20, _("Production"), 'primary' # BuildOrder is in production PRODUCTION = 20, _('Production'), 'primary' # BuildOrder is in production
CANCELLED = 30, _("Cancelled"), 'danger' # Build was cancelled CANCELLED = 30, _('Cancelled'), 'danger' # Build was cancelled
COMPLETE = 40, _("Complete"), 'success' # Build is complete COMPLETE = 40, _('Complete'), 'success' # Build is complete
class BuildStatusGroups: class BuildStatusGroups:
@ -161,13 +161,13 @@ class ReturnOrderStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrder""" """Defines a set of status codes for a ReturnOrder"""
# Order is pending, waiting for receipt of items # Order is pending, waiting for receipt of items
PENDING = 10, _("Pending"), 'secondary' PENDING = 10, _('Pending'), 'secondary'
# Items have been received, and are being inspected # Items have been received, and are being inspected
IN_PROGRESS = 20, _("In Progress"), 'primary' IN_PROGRESS = 20, _('In Progress'), 'primary'
COMPLETE = 30, _("Complete"), 'success' COMPLETE = 30, _('Complete'), 'success'
CANCELLED = 40, _("Cancelled"), 'danger' CANCELLED = 40, _('Cancelled'), 'danger'
class ReturnOrderStatusGroups: class ReturnOrderStatusGroups:
@ -179,19 +179,19 @@ class ReturnOrderStatusGroups:
class ReturnOrderLineStatus(StatusCode): class ReturnOrderLineStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrderLineItem""" """Defines a set of status codes for a ReturnOrderLineItem"""
PENDING = 10, _("Pending"), 'secondary' PENDING = 10, _('Pending'), 'secondary'
# Item is to be returned to customer, no other action # Item is to be returned to customer, no other action
RETURN = 20, _("Return"), 'success' RETURN = 20, _('Return'), 'success'
# Item is to be repaired, and returned to customer # Item is to be repaired, and returned to customer
REPAIR = 30, _("Repair"), 'primary' REPAIR = 30, _('Repair'), 'primary'
# Item is to be replaced (new item shipped) # Item is to be replaced (new item shipped)
REPLACE = 40, _("Replace"), 'warning' REPLACE = 40, _('Replace'), 'warning'
# Item is to be refunded (cannot be repaired) # Item is to be refunded (cannot be repaired)
REFUND = 50, _("Refund"), 'info' REFUND = 50, _('Refund'), 'info'
# Item is rejected # Item is rejected
REJECT = 60, _("Reject"), 'danger' REJECT = 60, _('Reject'), 'danger'

View File

@ -31,7 +31,7 @@ from plugin import registry
from .version import isInvenTreeUpToDate from .version import isInvenTreeUpToDate
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
def schedule_task(taskname, **kwargs): def schedule_task(taskname, **kwargs):
@ -46,7 +46,7 @@ def schedule_task(taskname, **kwargs):
try: try:
from django_q.models import Schedule from django_q.models import Schedule
except AppRegistryNotReady: # pragma: no cover except AppRegistryNotReady: # pragma: no cover
logger.info("Could not start background tasks - App registry not ready") logger.info('Could not start background tasks - App registry not ready')
return return
try: try:
@ -278,13 +278,13 @@ class ScheduledTask:
interval: str interval: str
minutes: int = None minutes: int = None
MINUTES = "I" MINUTES = 'I'
HOURLY = "H" HOURLY = 'H'
DAILY = "D" DAILY = 'D'
WEEKLY = "W" WEEKLY = 'W'
MONTHLY = "M" MONTHLY = 'M'
QUARTERLY = "Q" QUARTERLY = 'Q'
YEARLY = "Y" YEARLY = 'Y'
TYPE = [MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] TYPE = [MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]
@ -349,7 +349,7 @@ def heartbeat():
try: try:
from django_q.models import Success from django_q.models import Success
except AppRegistryNotReady: # pragma: no cover except AppRegistryNotReady: # pragma: no cover
logger.info("Could not perform heartbeat task - App registry not ready") logger.info('Could not perform heartbeat task - App registry not ready')
return return
threshold = timezone.now() - timedelta(minutes=30) threshold = timezone.now() - timedelta(minutes=30)
@ -378,7 +378,7 @@ def delete_successful_tasks():
results = Success.objects.filter(started__lte=threshold) results = Success.objects.filter(started__lte=threshold)
if results.count() > 0: if results.count() > 0:
logger.info("Deleting %s successful task records", results.count()) logger.info('Deleting %s successful task records', results.count())
results.delete() results.delete()
except AppRegistryNotReady: # pragma: no cover except AppRegistryNotReady: # pragma: no cover
@ -402,7 +402,7 @@ def delete_failed_tasks():
results = Failure.objects.filter(started__lte=threshold) results = Failure.objects.filter(started__lte=threshold)
if results.count() > 0: if results.count() > 0:
logger.info("Deleting %s failed task records", results.count()) logger.info('Deleting %s failed task records', results.count())
results.delete() results.delete()
except AppRegistryNotReady: # pragma: no cover except AppRegistryNotReady: # pragma: no cover
@ -423,7 +423,7 @@ def delete_old_error_logs():
errors = Error.objects.filter(when__lte=threshold) errors = Error.objects.filter(when__lte=threshold)
if errors.count() > 0: if errors.count() > 0:
logger.info("Deleting %s old error logs", errors.count()) logger.info('Deleting %s old error logs', errors.count())
errors.delete() errors.delete()
except AppRegistryNotReady: # pragma: no cover except AppRegistryNotReady: # pragma: no cover
@ -449,13 +449,13 @@ def delete_old_notifications():
items = NotificationEntry.objects.filter(updated__lte=threshold) items = NotificationEntry.objects.filter(updated__lte=threshold)
if items.count() > 0: if items.count() > 0:
logger.info("Deleted %s old notification entries", items.count()) logger.info('Deleted %s old notification entries', items.count())
items.delete() items.delete()
items = NotificationMessage.objects.filter(creation__lte=threshold) items = NotificationMessage.objects.filter(creation__lte=threshold)
if items.count() > 0: if items.count() > 0:
logger.info("Deleted %s old notification messages", items.count()) logger.info('Deleted %s old notification messages', items.count())
items.delete() items.delete()
except AppRegistryNotReady: except AppRegistryNotReady:
@ -485,7 +485,7 @@ def check_for_updates():
if not check_daily_holdoff('check_for_updates', interval): if not check_daily_holdoff('check_for_updates', interval):
return return
logger.info("Checking for InvenTree software updates") logger.info('Checking for InvenTree software updates')
headers = {} headers = {}
@ -494,7 +494,7 @@ def check_for_updates():
token = os.getenv('GITHUB_TOKEN', None) token = os.getenv('GITHUB_TOKEN', None)
if token: if token:
headers['Authorization'] = f"Bearer {token}" headers['Authorization'] = f'Bearer {token}'
response = requests.get( response = requests.get(
'https://api.github.com/repos/inventree/inventree/releases/latest', 'https://api.github.com/repos/inventree/inventree/releases/latest',
@ -513,7 +513,7 @@ def check_for_updates():
if not tag: if not tag:
raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag) match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag)
if len(match.groups()) != 3: # pragma: no cover if len(match.groups()) != 3: # pragma: no cover
logger.warning("Version '%s' did not match expected pattern", tag) logger.warning("Version '%s' did not match expected pattern", tag)
@ -534,15 +534,15 @@ def check_for_updates():
# Send notification if there is a new version # Send notification if there is a new version
if not isInvenTreeUpToDate(): if not isInvenTreeUpToDate():
logger.warning("InvenTree is not up-to-date, sending notification") logger.warning('InvenTree is not up-to-date, sending notification')
plg = registry.get_plugin('InvenTreeCoreNotificationsPlugin') plg = registry.get_plugin('InvenTreeCoreNotificationsPlugin')
if not plg: if not plg:
logger.warning("Cannot send notification - plugin not found") logger.warning('Cannot send notification - plugin not found')
return return
plg = plg.plugin_config() plg = plg.plugin_config()
if not plg: if not plg:
logger.warning("Cannot send notification - plugin config not found") logger.warning('Cannot send notification - plugin config not found')
return return
# Send notification # Send notification
trigger_superuser_notification( trigger_superuser_notification(
@ -579,7 +579,7 @@ def update_exchange_rates(force: bool = False):
) )
if not check_daily_holdoff('update_exchange_rates', interval): if not check_daily_holdoff('update_exchange_rates', interval):
logger.info("Skipping exchange rate update (interval not reached)") logger.info('Skipping exchange rate update (interval not reached)')
return return
backend = InvenTreeExchange() backend = InvenTreeExchange()
@ -590,7 +590,7 @@ def update_exchange_rates(force: bool = False):
backend.update_rates(base_currency=base) backend.update_rates(base_currency=base)
# Remove any exchange rates which are not in the provided currencies # Remove any exchange rates which are not in the provided currencies
Rate.objects.filter(backend="InvenTreeExchange").exclude( Rate.objects.filter(backend='InvenTreeExchange').exclude(
currency__in=currency_codes() currency__in=currency_codes()
).delete() ).delete()
@ -598,9 +598,9 @@ def update_exchange_rates(force: bool = False):
record_task_success('update_exchange_rates') record_task_success('update_exchange_rates')
except (AppRegistryNotReady, OperationalError, ProgrammingError): except (AppRegistryNotReady, OperationalError, ProgrammingError):
logger.warning("Could not update exchange rates - database not ready") logger.warning('Could not update exchange rates - database not ready')
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logger.exception("Error updating exchange rates: %s", str(type(e))) logger.exception('Error updating exchange rates: %s', str(type(e)))
@scheduled_task(ScheduledTask.DAILY) @scheduled_task(ScheduledTask.DAILY)
@ -620,11 +620,11 @@ def run_backup():
if not check_daily_holdoff('run_backup', interval): if not check_daily_holdoff('run_backup', interval):
return return
logger.info("Performing automated database backup task") logger.info('Performing automated database backup task')
call_command("dbbackup", noinput=True, clean=True, compress=True, interactive=False) call_command('dbbackup', noinput=True, clean=True, compress=True, interactive=False)
call_command( call_command(
"mediabackup", noinput=True, clean=True, compress=True, interactive=False 'mediabackup', noinput=True, clean=True, compress=True, interactive=False
) )
# Record that this task was successful # Record that this task was successful
@ -653,7 +653,7 @@ def check_for_migrations():
logger.info('There are %s pending migrations', n) logger.info('There are %s pending migrations', n)
InvenTreeSetting.set_setting('_PENDING_MIGRATIONS', n, None) InvenTreeSetting.set_setting('_PENDING_MIGRATIONS', n, None)
logger.info("Checking for pending database migrations") logger.info('Checking for pending database migrations')
# Force plugin registry reload # Force plugin registry reload
registry.check_reload() registry.check_reload()
@ -671,12 +671,12 @@ def check_for_migrations():
# Test if auto-updates are enabled # Test if auto-updates are enabled
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'): if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
logger.info("Auto-update is disabled - skipping migrations") logger.info('Auto-update is disabled - skipping migrations')
return return
# Log open migrations # Log open migrations
for migration in plan: for migration in plan:
logger.info("- %s", str(migration[0])) logger.info('- %s', str(migration[0]))
# Set the application to maintenance mode - no access from now on. # Set the application to maintenance mode - no access from now on.
set_maintenance_mode(True) set_maintenance_mode(True)
@ -694,13 +694,13 @@ def check_for_migrations():
else: else:
set_pending_migrations(0) set_pending_migrations(0)
logger.info("Completed %s migrations", n) logger.info('Completed %s migrations', n)
# Make sure we are out of maintenance mode # Make sure we are out of maintenance mode
if get_maintenance_mode(): if get_maintenance_mode():
logger.warning("Maintenance mode was not disabled - forcing it now") logger.warning('Maintenance mode was not disabled - forcing it now')
set_maintenance_mode(False) set_maintenance_mode(False)
logger.info("Manually released maintenance mode") logger.info('Manually released maintenance mode')
# We should be current now - triggering full reload to make sure all models # We should be current now - triggering full reload to make sure all models
# are loaded fully in their new state. # are loaded fully in their new state.

View File

@ -69,11 +69,11 @@ class APITests(InvenTreeAPITestCase):
"""Helper function to use basic auth.""" """Helper function to use basic auth."""
# Use basic authentication # Use basic authentication
authstring = bytes("{u}:{p}".format(u=self.username, p=self.password), "ascii") authstring = bytes('{u}:{p}'.format(u=self.username, p=self.password), 'ascii')
# Use "basic" auth by default # Use "basic" auth by default
auth = b64encode(authstring).decode("ascii") auth = b64encode(authstring).decode('ascii')
self.client.credentials(HTTP_AUTHORIZATION="Basic {auth}".format(auth=auth)) self.client.credentials(HTTP_AUTHORIZATION='Basic {auth}'.format(auth=auth))
def tokenAuth(self): def tokenAuth(self):
"""Helper function to use token auth.""" """Helper function to use token auth."""
@ -274,7 +274,7 @@ class BulkDeleteTests(InvenTreeAPITestCase):
) )
# DELETE with invalid 'items' # DELETE with invalid 'items'
response = self.delete(url, {'items': {"hello": "world"}}, expected_code=400) response = self.delete(url, {'items': {'hello': 'world'}}, expected_code=400)
self.assertIn("'items' must be supplied as a list object", str(response.data)) self.assertIn("'items' must be supplied as a list object", str(response.data))

View File

@ -67,7 +67,7 @@ class URLTest(TestCase):
"""Search for all instances of {% url %} in supplied template file.""" """Search for all instances of {% url %} in supplied template file."""
urls = [] urls = []
pattern = "{% url ['\"]([^'\"]+)['\"]([^%]*)%}" pattern = '{% url [\'"]([^\'"]+)[\'"]([^%]*)%}'
with open(input_file, 'r') as f: with open(input_file, 'r') as f:
data = f.read() data = f.read()
@ -91,16 +91,16 @@ class URLTest(TestCase):
pk = None pk = None
# TODO: Handle reverse lookup of admin URLs! # TODO: Handle reverse lookup of admin URLs!
if url.startswith("admin:"): if url.startswith('admin:'):
return return
# TODO can this be more elegant? # TODO can this be more elegant?
if url.startswith("account_"): if url.startswith('account_'):
return return
if pk: if pk:
# We will assume that there is at least one item in the database # We will assume that there is at least one item in the database
reverse(url, kwargs={"pk": 1}) reverse(url, kwargs={'pk': 1})
else: else:
reverse(url) reverse(url)
@ -113,14 +113,14 @@ class URLTest(TestCase):
def test_html_templates(self): def test_html_templates(self):
"""Test all HTML templates for broken url tags.""" """Test all HTML templates for broken url tags."""
template_files = self.find_files("*.html") template_files = self.find_files('*.html')
for f in template_files: for f in template_files:
self.check_file(f) self.check_file(f)
def test_js_templates(self): def test_js_templates(self):
"""Test all JS templates for broken url tags.""" """Test all JS templates for broken url tags."""
template_files = self.find_files("*.js") template_files = self.find_files('*.js')
for f in template_files: for f in template_files:
self.check_file(f) self.check_file(f)

View File

@ -23,13 +23,13 @@ class ViewTests(InvenTreeTestCase):
def test_index_redirect(self): def test_index_redirect(self):
"""Top-level URL should redirect to "index" page.""" """Top-level URL should redirect to "index" page."""
response = self.client.get("/") response = self.client.get('/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def get_index_page(self): def get_index_page(self):
"""Retrieve the index page (used for subsequent unit tests)""" """Retrieve the index page (used for subsequent unit tests)"""
response = self.client.get("/index/") response = self.client.get('/index/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -68,8 +68,8 @@ class ViewTests(InvenTreeTestCase):
# Default user has staff access, so all panels will be present # Default user has staff access, so all panels will be present
for panel in user_panels + staff_panels + plugin_panels: for panel in user_panels + staff_panels + plugin_panels:
self.assertIn(f"select-{panel}", content) self.assertIn(f'select-{panel}', content)
self.assertIn(f"panel-{panel}", content) self.assertIn(f'panel-{panel}', content)
# Now create a user who does not have staff access # Now create a user who does not have staff access
pleb_user = get_user_model().objects.create_user( pleb_user = get_user_model().objects.create_user(
@ -93,24 +93,24 @@ class ViewTests(InvenTreeTestCase):
# Normal user still has access to user-specific panels # Normal user still has access to user-specific panels
for panel in user_panels: for panel in user_panels:
self.assertIn(f"select-{panel}", content) self.assertIn(f'select-{panel}', content)
self.assertIn(f"panel-{panel}", content) self.assertIn(f'panel-{panel}', content)
# Normal user does NOT have access to global or plugin settings # Normal user does NOT have access to global or plugin settings
for panel in staff_panels + plugin_panels: for panel in staff_panels + plugin_panels:
self.assertNotIn(f"select-{panel}", content) self.assertNotIn(f'select-{panel}', content)
self.assertNotIn(f"panel-{panel}", content) self.assertNotIn(f'panel-{panel}', content)
def test_url_login(self): def test_url_login(self):
"""Test logging in via arguments""" """Test logging in via arguments"""
# Log out # Log out
self.client.logout() self.client.logout()
response = self.client.get("/index/") response = self.client.get('/index/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# Try login with url # Try login with url
response = self.client.get( response = self.client.get(
f"/accounts/login/?next=/&login={self.username}&password={self.password}" f'/accounts/login/?next=/&login={self.username}&password={self.password}'
) )
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')

View File

@ -45,12 +45,12 @@ class ConversionTest(TestCase):
def test_prefixes(self): def test_prefixes(self):
"""Test inputs where prefixes are used""" """Test inputs where prefixes are used"""
tests = { tests = {
"3": 3, '3': 3,
"3m": 3, '3m': 3,
"3mm": 0.003, '3mm': 0.003,
"3k": 3000, '3k': 3000,
"3u": 0.000003, '3u': 0.000003,
"3 inch": 0.0762, '3 inch': 0.0762,
} }
for val, expected in tests.items(): for val, expected in tests.items():
@ -60,13 +60,13 @@ class ConversionTest(TestCase):
def test_base_units(self): def test_base_units(self):
"""Test conversion to specified base units""" """Test conversion to specified base units"""
tests = { tests = {
"3": 3, '3': 3,
"3 dozen": 36, '3 dozen': 36,
"50 dozen kW": 600000, '50 dozen kW': 600000,
"1 / 10": 0.1, '1 / 10': 0.1,
"1/2 kW": 500, '1/2 kW': 500,
"1/2 dozen kW": 6000, '1/2 dozen kW': 6000,
"0.005 MW": 5000, '0.005 MW': 5000,
} }
for val, expected in tests.items(): for val, expected in tests.items():
@ -173,24 +173,24 @@ class ValidatorTest(TestCase):
def test_overage(self): def test_overage(self):
"""Test overage validator.""" """Test overage validator."""
validate_overage("100%") validate_overage('100%')
validate_overage("10") validate_overage('10')
validate_overage("45.2 %") validate_overage('45.2 %')
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
validate_overage("-1") validate_overage('-1')
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
validate_overage("-2.04 %") validate_overage('-2.04 %')
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
validate_overage("105%") validate_overage('105%')
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
validate_overage("xxx %") validate_overage('xxx %')
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
validate_overage("aaaa") validate_overage('aaaa')
def test_url_validation(self): def test_url_validation(self):
"""Test for AllowedURLValidator""" """Test for AllowedURLValidator"""
@ -230,7 +230,7 @@ class FormatTest(TestCase):
def test_parse(self): def test_parse(self):
"""Tests for the 'parse_format_string' function""" """Tests for the 'parse_format_string' function"""
# Extract data from a valid format string # Extract data from a valid format string
fmt = "PO-{abc:02f}-{ref:04d}-{date}-???" fmt = 'PO-{abc:02f}-{ref:04d}-{date}-???'
info = InvenTree.format.parse_format_string(fmt) info = InvenTree.format.parse_format_string(fmt)
@ -246,10 +246,10 @@ class FormatTest(TestCase):
def test_create_regex(self): def test_create_regex(self):
"""Test function for creating a regex from a format string""" """Test function for creating a regex from a format string"""
tests = { tests = {
"PO-123-{ref:04f}": r"^PO\-123\-(?P<ref>.+)$", 'PO-123-{ref:04f}': r'^PO\-123\-(?P<ref>.+)$',
"{PO}-???-{ref}-{date}-22": r"^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$", '{PO}-???-{ref}-{date}-22': r'^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$',
"ABC-123-###-{ref}": r"^ABC\-123\-\d\d\d\-(?P<ref>.+)$", 'ABC-123-###-{ref}': r'^ABC\-123\-\d\d\d\-(?P<ref>.+)$',
"ABC-123": r"^ABC\-123$", 'ABC-123': r'^ABC\-123$',
} }
for fmt, reg in tests.items(): for fmt, reg in tests.items():
@ -259,28 +259,28 @@ class FormatTest(TestCase):
"""Test that string validation works as expected""" """Test that string validation works as expected"""
# These tests should pass # These tests should pass
for value, pattern in { for value, pattern in {
"ABC-hello-123": "???-{q}-###", 'ABC-hello-123': '???-{q}-###',
"BO-1234": "BO-{ref}", 'BO-1234': 'BO-{ref}',
"111.222.fred.china": "???.###.{name}.{place}", '111.222.fred.china': '???.###.{name}.{place}',
"PO-1234": "PO-{ref:04d}", 'PO-1234': 'PO-{ref:04d}',
}.items(): }.items():
self.assertTrue(InvenTree.format.validate_string(value, pattern)) self.assertTrue(InvenTree.format.validate_string(value, pattern))
# These tests should fail # These tests should fail
for value, pattern in { for value, pattern in {
"ABC-hello-123": "###-{q}-???", 'ABC-hello-123': '###-{q}-???',
"BO-1234": "BO.{ref}", 'BO-1234': 'BO.{ref}',
"BO-####": "BO-{pattern}-{next}", 'BO-####': 'BO-{pattern}-{next}',
"BO-123d": "BO-{ref:04d}", 'BO-123d': 'BO-{ref:04d}',
}.items(): }.items():
self.assertFalse(InvenTree.format.validate_string(value, pattern)) self.assertFalse(InvenTree.format.validate_string(value, pattern))
def test_extract_value(self): def test_extract_value(self):
"""Test that we can extract named values based on a format string""" """Test that we can extract named values based on a format string"""
# Simple tests based on a straight-forward format string # Simple tests based on a straight-forward format string
fmt = "PO-###-{ref:04d}" fmt = 'PO-###-{ref:04d}'
tests = {"123": "PO-123-123", "456": "PO-123-456", "789": "PO-123-789"} tests = {'123': 'PO-123-123', '456': 'PO-123-456', '789': 'PO-123-789'}
for k, v in tests.items(): for k, v in tests.items():
self.assertEqual(InvenTree.format.extract_named_group('ref', v, fmt), k) self.assertEqual(InvenTree.format.extract_named_group('ref', v, fmt), k)
@ -293,8 +293,8 @@ class FormatTest(TestCase):
InvenTree.format.extract_named_group('ref', v, fmt) InvenTree.format.extract_named_group('ref', v, fmt)
# More complex tests # More complex tests
fmt = "PO-{date}-{test}-???-{ref}-###" fmt = 'PO-{date}-{test}-???-{ref}-###'
val = "PO-2022-02-01-hello-ABC-12345-222" val = 'PO-2022-02-01-hello-ABC-12345-222'
data = {'date': '2022-02-01', 'test': 'hello', 'ref': '12345'} data = {'date': '2022-02-01', 'test': 'hello', 'ref': '12345'}
@ -305,42 +305,42 @@ class FormatTest(TestCase):
# Raises a ValueError as the format string is bad # Raises a ValueError as the format string is bad
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
InvenTree.format.extract_named_group("test", "PO-1234-5", "PO-{test}-{") InvenTree.format.extract_named_group('test', 'PO-1234-5', 'PO-{test}-{')
# Raises a NameError as the named group does not exist in the format string # Raises a NameError as the named group does not exist in the format string
with self.assertRaises(NameError): with self.assertRaises(NameError):
InvenTree.format.extract_named_group("missing", "PO-12345", "PO-{test}") InvenTree.format.extract_named_group('missing', 'PO-12345', 'PO-{test}')
# Raises a ValueError as the value does not match the format string # Raises a ValueError as the value does not match the format string
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
InvenTree.format.extract_named_group("test", "PO-1234", "PO-{test}-1234") InvenTree.format.extract_named_group('test', 'PO-1234', 'PO-{test}-1234')
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
InvenTree.format.extract_named_group("test", "PO-ABC-xyz", "PO-###-{test}") InvenTree.format.extract_named_group('test', 'PO-ABC-xyz', 'PO-###-{test}')
def test_currency_formatting(self): def test_currency_formatting(self):
"""Test that currency formatting works correctly for multiple currencies""" """Test that currency formatting works correctly for multiple currencies"""
test_data = ( test_data = (
(Money(3651.285718, "USD"), 4, "$3,651.2857"), # noqa: E201,E202 (Money(3651.285718, 'USD'), 4, '$3,651.2857'), # noqa: E201,E202
(Money(487587.849178, "CAD"), 5, "CA$487,587.84918"), # noqa: E201,E202 (Money(487587.849178, 'CAD'), 5, 'CA$487,587.84918'), # noqa: E201,E202
(Money(0.348102, "EUR"), 1, "€0.3"), # noqa: E201,E202 (Money(0.348102, 'EUR'), 1, '€0.3'), # noqa: E201,E202
(Money(0.916530, "GBP"), 1, "£0.9"), # noqa: E201,E202 (Money(0.916530, 'GBP'), 1, '£0.9'), # noqa: E201,E202
(Money(61.031024, "JPY"), 3, "¥61.031"), # noqa: E201,E202 (Money(61.031024, 'JPY'), 3, '¥61.031'), # noqa: E201,E202
(Money(49609.694602, "JPY"), 1, "¥49,609.7"), # noqa: E201,E202 (Money(49609.694602, 'JPY'), 1, '¥49,609.7'), # noqa: E201,E202
(Money(155565.264777, "AUD"), 2, "A$155,565.26"), # noqa: E201,E202 (Money(155565.264777, 'AUD'), 2, 'A$155,565.26'), # noqa: E201,E202
(Money(0.820437, "CNY"), 4, "CN¥0.8204"), # noqa: E201,E202 (Money(0.820437, 'CNY'), 4, 'CN¥0.8204'), # noqa: E201,E202
(Money(7587.849178, "EUR"), 0, "€7,588"), # noqa: E201,E202 (Money(7587.849178, 'EUR'), 0, '€7,588'), # noqa: E201,E202
(Money(0.348102, "GBP"), 3, "£0.348"), # noqa: E201,E202 (Money(0.348102, 'GBP'), 3, '£0.348'), # noqa: E201,E202
(Money(0.652923, "CHF"), 0, "CHF1"), # noqa: E201,E202 (Money(0.652923, 'CHF'), 0, 'CHF1'), # noqa: E201,E202
(Money(0.820437, "CNY"), 1, "CN¥0.8"), # noqa: E201,E202 (Money(0.820437, 'CNY'), 1, 'CN¥0.8'), # noqa: E201,E202
(Money(98789.5295680, "CHF"), 0, "CHF98,790"), # noqa: E201,E202 (Money(98789.5295680, 'CHF'), 0, 'CHF98,790'), # noqa: E201,E202
(Money(0.585787, "USD"), 1, "$0.6"), # noqa: E201,E202 (Money(0.585787, 'USD'), 1, '$0.6'), # noqa: E201,E202
(Money(0.690541, "CAD"), 3, "CA$0.691"), # noqa: E201,E202 (Money(0.690541, 'CAD'), 3, 'CA$0.691'), # noqa: E201,E202
(Money(427.814104, "AUD"), 5, "A$427.81410"), # noqa: E201,E202 (Money(427.814104, 'AUD'), 5, 'A$427.81410'), # noqa: E201,E202
) )
with self.settings(LANGUAGE_CODE="en-us"): with self.settings(LANGUAGE_CODE='en-us'):
for value, decimal_places, expected_result in test_data: for value, decimal_places, expected_result in test_data:
result = InvenTree.format.format_money( result = InvenTree.format.format_money(
value, decimal_places=decimal_places value, decimal_places=decimal_places
@ -353,22 +353,22 @@ class TestHelpers(TestCase):
def test_absolute_url(self): def test_absolute_url(self):
"""Test helper function for generating an absolute URL""" """Test helper function for generating an absolute URL"""
base = "https://demo.inventree.org:12345" base = 'https://demo.inventree.org:12345'
InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None) InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
tests = { tests = {
"": base, '': base,
"api/": base + "/api/", 'api/': base + '/api/',
"/api/": base + "/api/", '/api/': base + '/api/',
"api": base + "/api", 'api': base + '/api',
"media/label/output/": base + "/media/label/output/", 'media/label/output/': base + '/media/label/output/',
"static/logo.png": base + "/static/logo.png", 'static/logo.png': base + '/static/logo.png',
"https://www.google.com": "https://www.google.com", 'https://www.google.com': 'https://www.google.com',
"https://demo.inventree.org:12345/out.html": "https://demo.inventree.org:12345/out.html", 'https://demo.inventree.org:12345/out.html': 'https://demo.inventree.org:12345/out.html',
"https://demo.inventree.org/test.html": "https://demo.inventree.org/test.html", 'https://demo.inventree.org/test.html': 'https://demo.inventree.org/test.html',
"http://www.cwi.nl:80/%7Eguido/Python.html": "http://www.cwi.nl:80/%7Eguido/Python.html", 'http://www.cwi.nl:80/%7Eguido/Python.html': 'http://www.cwi.nl:80/%7Eguido/Python.html',
"test.org": base + "/test.org", 'test.org': base + '/test.org',
} }
for url, expected in tests.items(): for url, expected in tests.items():
@ -442,7 +442,7 @@ class TestHelpers(TestCase):
def test_download_image(self): def test_download_image(self):
"""Test function for downloading image from remote URL""" """Test function for downloading image from remote URL"""
# Run check with a sequence of bad URLs # Run check with a sequence of bad URLs
for url in ["blog", "htp://test.com/?", "google", "\\invalid-url"]: for url in ['blog', 'htp://test.com/?', 'google', '\\invalid-url']:
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
InvenTree.helpers_model.download_image_from_url(url) InvenTree.helpers_model.download_image_from_url(url)
@ -467,7 +467,7 @@ class TestHelpers(TestCase):
# Re-throw this error # Re-throw this error
raise exc raise exc
else: else:
print("Unexpected error:", type(exc), exc) print('Unexpected error:', type(exc), exc)
tries += 1 tries += 1
time.sleep(10 * tries) time.sleep(10 * tries)
@ -480,7 +480,7 @@ class TestHelpers(TestCase):
# TODO: Re-implement this test when we are happier with the external service # TODO: Re-implement this test when we are happier with the external service
# dl_helper("https://httpstat.us/200?sleep=5000", requests.exceptions.ReadTimeout, timeout=1) # dl_helper("https://httpstat.us/200?sleep=5000", requests.exceptions.ReadTimeout, timeout=1)
large_img = "https://github.com/inventree/InvenTree/raw/master/InvenTree/InvenTree/static/img/paper_splash_large.jpg" large_img = 'https://github.com/inventree/InvenTree/raw/master/InvenTree/InvenTree/static/img/paper_splash_large.jpg'
InvenTreeSetting.set_setting( InvenTreeSetting.set_setting(
'INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 1, change_user=None 'INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 1, change_user=None
@ -527,14 +527,14 @@ class TestIncrement(TestCase):
def tests(self): def tests(self):
"""Test 'intelligent' incrementing function.""" """Test 'intelligent' incrementing function."""
tests = [ tests = [
("", '1'), ('', '1'),
(1, "2"), (1, '2'),
("001", "002"), ('001', '002'),
("1001", "1002"), ('1001', '1002'),
("ABC123", "ABC124"), ('ABC123', 'ABC124'),
("XYZ0", "XYZ1"), ('XYZ0', 'XYZ1'),
("123Q", "123Q"), ('123Q', '123Q'),
("QQQ", "QQQ"), ('QQQ', 'QQQ'),
] ]
for test in tests: for test in tests:
@ -550,7 +550,7 @@ class TestMakeBarcode(TestCase):
def test_barcode_extended(self): def test_barcode_extended(self):
"""Test creation of barcode with extended data.""" """Test creation of barcode with extended data."""
bc = helpers.MakeBarcode( bc = helpers.MakeBarcode(
"part", 3, {"id": 3, "url": "www.google.com"}, brief=False 'part', 3, {'id': 3, 'url': 'www.google.com'}, brief=False
) )
self.assertIn('part', bc) self.assertIn('part', bc)
@ -564,7 +564,7 @@ class TestMakeBarcode(TestCase):
def test_barcode_brief(self): def test_barcode_brief(self):
"""Test creation of simple barcode.""" """Test creation of simple barcode."""
bc = helpers.MakeBarcode("stockitem", 7) bc = helpers.MakeBarcode('stockitem', 7)
data = json.loads(bc) data = json.loads(bc)
self.assertEqual(len(data), 1) self.assertEqual(len(data), 1)
@ -576,8 +576,8 @@ class TestDownloadFile(TestCase):
def test_download(self): def test_download(self):
"""Tests for DownloadFile.""" """Tests for DownloadFile."""
helpers.DownloadFile("hello world", "out.txt") helpers.DownloadFile('hello world', 'out.txt')
helpers.DownloadFile(bytes(b"hello world"), "out.bin") helpers.DownloadFile(bytes(b'hello world'), 'out.bin')
class TestMPTT(TestCase): class TestMPTT(TestCase):
@ -636,62 +636,62 @@ class TestSerialNumberExtraction(TestCase):
e = helpers.extract_serial_numbers e = helpers.extract_serial_numbers
# Test a range of numbers # Test a range of numbers
sn = e("1-5", 5, 1) sn = e('1-5', 5, 1)
self.assertEqual(len(sn), 5) self.assertEqual(len(sn), 5)
for i in range(1, 6): for i in range(1, 6):
self.assertIn(str(i), sn) self.assertIn(str(i), sn)
sn = e("11-30", 20, 1) sn = e('11-30', 20, 1)
self.assertEqual(len(sn), 20) self.assertEqual(len(sn), 20)
sn = e("1, 2, 3, 4, 5", 5, 1) sn = e('1, 2, 3, 4, 5', 5, 1)
self.assertEqual(len(sn), 5) self.assertEqual(len(sn), 5)
# Test partially specifying serials # Test partially specifying serials
sn = e("1, 2, 4+", 5, 1) sn = e('1, 2, 4+', 5, 1)
self.assertEqual(len(sn), 5) self.assertEqual(len(sn), 5)
self.assertEqual(sn, ['1', '2', '4', '5', '6']) self.assertEqual(sn, ['1', '2', '4', '5', '6'])
# Test groups are not interpolated if enough serials are supplied # Test groups are not interpolated if enough serials are supplied
sn = e("1, 2, 3, AF5-69H, 5", 5, 1) sn = e('1, 2, 3, AF5-69H, 5', 5, 1)
self.assertEqual(len(sn), 5) self.assertEqual(len(sn), 5)
self.assertEqual(sn, ['1', '2', '3', 'AF5-69H', '5']) self.assertEqual(sn, ['1', '2', '3', 'AF5-69H', '5'])
# Test groups are not interpolated with more than one hyphen in a word # Test groups are not interpolated with more than one hyphen in a word
sn = e("1, 2, TG-4SR-92, 4+", 5, 1) sn = e('1, 2, TG-4SR-92, 4+', 5, 1)
self.assertEqual(len(sn), 5) self.assertEqual(len(sn), 5)
self.assertEqual(sn, ['1', '2', "TG-4SR-92", '4', '5']) self.assertEqual(sn, ['1', '2', 'TG-4SR-92', '4', '5'])
# Test multiple placeholders # Test multiple placeholders
sn = e("1 2 ~ ~ ~", 5, 2) sn = e('1 2 ~ ~ ~', 5, 2)
self.assertEqual(len(sn), 5) self.assertEqual(len(sn), 5)
self.assertEqual(sn, ['1', '2', '3', '4', '5']) self.assertEqual(sn, ['1', '2', '3', '4', '5'])
sn = e("1-5, 10-15", 11, 1) sn = e('1-5, 10-15', 11, 1)
self.assertIn('3', sn) self.assertIn('3', sn)
self.assertIn('13', sn) self.assertIn('13', sn)
sn = e("1+", 10, 1) sn = e('1+', 10, 1)
self.assertEqual(len(sn), 10) self.assertEqual(len(sn), 10)
self.assertEqual(sn, [str(_) for _ in range(1, 11)]) self.assertEqual(sn, [str(_) for _ in range(1, 11)])
sn = e("4, 1+2", 4, 1) sn = e('4, 1+2', 4, 1)
self.assertEqual(len(sn), 4) self.assertEqual(len(sn), 4)
self.assertEqual(sn, ['4', '1', '2', '3']) self.assertEqual(sn, ['4', '1', '2', '3'])
sn = e("~", 1, 1) sn = e('~', 1, 1)
self.assertEqual(len(sn), 1) self.assertEqual(len(sn), 1)
self.assertEqual(sn, ['2']) self.assertEqual(sn, ['2'])
sn = e("~", 1, 3) sn = e('~', 1, 3)
self.assertEqual(len(sn), 1) self.assertEqual(len(sn), 1)
self.assertEqual(sn, ['4']) self.assertEqual(sn, ['4'])
sn = e("~+", 2, 4) sn = e('~+', 2, 4)
self.assertEqual(len(sn), 2) self.assertEqual(len(sn), 2)
self.assertEqual(sn, ['5', '6']) self.assertEqual(sn, ['5', '6'])
sn = e("~+3", 4, 4) sn = e('~+3', 4, 4)
self.assertEqual(len(sn), 4) self.assertEqual(len(sn), 4)
self.assertEqual(sn, ['5', '6', '7', '8']) self.assertEqual(sn, ['5', '6', '7', '8'])
@ -701,70 +701,70 @@ class TestSerialNumberExtraction(TestCase):
# Test duplicates # Test duplicates
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("1,2,3,3,3", 5, 1) e('1,2,3,3,3', 5, 1)
# Test invalid length # Test invalid length
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("1,2,3", 5, 1) e('1,2,3', 5, 1)
# Test empty string # Test empty string
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e(", , ,", 0, 1) e(', , ,', 0, 1)
# Test incorrect sign in group # Test incorrect sign in group
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("10-2", 8, 1) e('10-2', 8, 1)
# Test invalid group # Test invalid group
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("1-5-10", 10, 1) e('1-5-10', 10, 1)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("10, a, 7-70j", 4, 1) e('10, a, 7-70j', 4, 1)
# Test groups are not interpolated with word characters # Test groups are not interpolated with word characters
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
e("1, 2, 3, E-5", 5, 1) e('1, 2, 3, E-5', 5, 1)
# Extract a range of values with a smaller range # Extract a range of values with a smaller range
with self.assertRaises(ValidationError) as exc: with self.assertRaises(ValidationError) as exc:
e("11-50", 10, 1) e('11-50', 10, 1)
self.assertIn('Range quantity exceeds 10', str(exc)) self.assertIn('Range quantity exceeds 10', str(exc))
# Test groups are not interpolated with alpha characters # Test groups are not interpolated with alpha characters
with self.assertRaises(ValidationError) as exc: with self.assertRaises(ValidationError) as exc:
e("1, A-2, 3+", 5, 1) e('1, A-2, 3+', 5, 1)
self.assertIn('Invalid group range: A-2', str(exc)) self.assertIn('Invalid group range: A-2', str(exc))
def test_combinations(self): def test_combinations(self):
"""Test complex serial number combinations.""" """Test complex serial number combinations."""
e = helpers.extract_serial_numbers e = helpers.extract_serial_numbers
sn = e("1 3-5 9+2", 7, 1) sn = e('1 3-5 9+2', 7, 1)
self.assertEqual(len(sn), 7) self.assertEqual(len(sn), 7)
self.assertEqual(sn, ['1', '3', '4', '5', '9', '10', '11']) self.assertEqual(sn, ['1', '3', '4', '5', '9', '10', '11'])
sn = e("1,3-5,9+2", 7, 1) sn = e('1,3-5,9+2', 7, 1)
self.assertEqual(len(sn), 7) self.assertEqual(len(sn), 7)
self.assertEqual(sn, ['1', '3', '4', '5', '9', '10', '11']) self.assertEqual(sn, ['1', '3', '4', '5', '9', '10', '11'])
sn = e("~+2", 3, 13) sn = e('~+2', 3, 13)
self.assertEqual(len(sn), 3) self.assertEqual(len(sn), 3)
self.assertEqual(sn, ['14', '15', '16']) self.assertEqual(sn, ['14', '15', '16'])
sn = e("~+", 2, 13) sn = e('~+', 2, 13)
self.assertEqual(len(sn), 2) self.assertEqual(len(sn), 2)
self.assertEqual(sn, ['14', '15']) self.assertEqual(sn, ['14', '15'])
# Test multiple increment groups # Test multiple increment groups
sn = e("~+4, 20+4, 30+4", 15, 10) sn = e('~+4, 20+4, 30+4', 15, 10)
self.assertEqual(len(sn), 15) self.assertEqual(len(sn), 15)
for v in [14, 24, 34]: for v in [14, 24, 34]:
self.assertIn(str(v), sn) self.assertIn(str(v), sn)
# Test multiple range groups # Test multiple range groups
sn = e("11-20, 41-50, 91-100", 30, 1) sn = e('11-20, 41-50, 91-100', 30, 1)
self.assertEqual(len(sn), 30) self.assertEqual(len(sn), 30)
for v in range(11, 21): for v in range(11, 21):
@ -859,7 +859,7 @@ class CurrencyTests(TestCase):
break break
else: # pragma: no cover else: # pragma: no cover
print("Exchange rate update failed - retrying") print('Exchange rate update failed - retrying')
print(f'Expected {currency_codes()}, got {[a.currency for a in rates]}') print(f'Expected {currency_codes()}, got {[a.currency for a in rates]}')
time.sleep(1) time.sleep(1)
@ -1030,7 +1030,7 @@ class TestSettings(InvenTreeTestCase):
# test typecasting to dict - valid JSON string should be mapped to corresponding dict # test typecasting to dict - valid JSON string should be mapped to corresponding dict
with self.in_env_context({TEST_ENV_NAME: '{"a": 1}'}): with self.in_env_context({TEST_ENV_NAME: '{"a": 1}'}):
self.assertEqual( self.assertEqual(
config.get_setting(TEST_ENV_NAME, None, typecast=dict), {"a": 1} config.get_setting(TEST_ENV_NAME, None, typecast=dict), {'a': 1}
) )
# test typecasting to dict - invalid JSON string should be mapped to empty dict # test typecasting to dict - invalid JSON string should be mapped to empty dict
@ -1047,8 +1047,8 @@ class TestInstanceName(InvenTreeTestCase):
self.assertEqual(version.inventreeInstanceTitle(), 'InvenTree') self.assertEqual(version.inventreeInstanceTitle(), 'InvenTree')
# set up required setting # set up required setting
InvenTreeSetting.set_setting("INVENTREE_INSTANCE_TITLE", True, self.user) InvenTreeSetting.set_setting('INVENTREE_INSTANCE_TITLE', True, self.user)
InvenTreeSetting.set_setting("INVENTREE_INSTANCE", "Testing title", self.user) InvenTreeSetting.set_setting('INVENTREE_INSTANCE', 'Testing title', self.user)
self.assertEqual(version.inventreeInstanceTitle(), 'Testing title') self.assertEqual(version.inventreeInstanceTitle(), 'Testing title')
@ -1060,7 +1060,7 @@ class TestInstanceName(InvenTreeTestCase):
"""Test instance url settings.""" """Test instance url settings."""
# Set up required setting # Set up required setting
InvenTreeSetting.set_setting( InvenTreeSetting.set_setting(
"INVENTREE_BASE_URL", "http://127.1.2.3", self.user 'INVENTREE_BASE_URL', 'http://127.1.2.3', self.user
) )
# The site should also be changed # The site should also be changed
@ -1107,7 +1107,7 @@ class TestOffloadTask(InvenTreeTestCase):
offload_task('dummy_task.numbers', 1, 1, 1, force_sync=True) offload_task('dummy_task.numbers', 1, 1, 1, force_sync=True)
) )
self.assertIn("Malformed function path", str(log.output)) self.assertIn('Malformed function path', str(log.output))
# Offload dummy task with a Part instance # Offload dummy task with a Part instance
# This should succeed, ensuring that the Part instance is correctly pickled # This should succeed, ensuring that the Part instance is correctly pickled

View File

@ -39,7 +39,7 @@ def getMigrationFileNames(app):
files = local_dir.joinpath('..', app, 'migrations').iterdir() files = local_dir.joinpath('..', app, 'migrations').iterdir()
# Regex pattern for migration files # Regex pattern for migration files
regex = re.compile(r"^[\d]+_.*\.py$") regex = re.compile(r'^[\d]+_.*\.py$')
migration_files = [] migration_files = []
@ -241,14 +241,14 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
yield # your test will be run here yield # your test will be run here
if verbose: if verbose:
msg = "\r\n%s" % json.dumps(context.captured_queries, indent=4) msg = '\r\n%s' % json.dumps(context.captured_queries, indent=4)
else: else:
msg = None msg = None
n = len(context.captured_queries) n = len(context.captured_queries)
if debug: if debug:
print(f"Expected less than {value} queries, got {n} queries") print(f'Expected less than {value} queries, got {n} queries')
self.assertLess(n, value, msg=msg) self.assertLess(n, value, msg=msg)
@ -357,7 +357,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
# Check that the response is of the correct type # Check that the response is of the correct type
if not isinstance(response, StreamingHttpResponse): if not isinstance(response, StreamingHttpResponse):
raise ValueError( raise ValueError(
"Response is not a StreamingHttpResponse object as expected" 'Response is not a StreamingHttpResponse object as expected'
) )
# Extract filename # Extract filename

View File

@ -67,7 +67,7 @@ from .views import (
auth_request, auth_request,
) )
admin.site.site_header = "InvenTree Admin" admin.site.site_header = 'InvenTree Admin'
apipatterns = [ apipatterns = [
@ -96,7 +96,7 @@ apipatterns = [
), ),
# InvenTree information endpoints # InvenTree information endpoints
path( path(
"version-text", VersionTextView.as_view(), name="api-version-text" 'version-text', VersionTextView.as_view(), name='api-version-text'
), # version text ), # version text
path('version/', VersionView.as_view(), name='api-version'), # version info path('version/', VersionView.as_view(), name='api-version'), # version info
path('', InfoView.as_view(), name='api-inventree-info'), # server info path('', InfoView.as_view(), name='api-inventree-info'), # server info
@ -153,11 +153,11 @@ apipatterns = [
), ),
# Magic login URLs # Magic login URLs
path( path(
"email/generate/", 'email/generate/',
csrf_exempt(GetSimpleLoginView().as_view()), csrf_exempt(GetSimpleLoginView().as_view()),
name="sesame-generate", name='sesame-generate',
), ),
path("email/login/", LoginView.as_view(), name="sesame-login"), path('email/login/', LoginView.as_view(), name='sesame-login'),
# Unknown endpoint # Unknown endpoint
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'), re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
] ]
@ -403,12 +403,12 @@ classic_frontendpatterns = [
name='socialaccount_connections', name='socialaccount_connections',
), ),
re_path( re_path(
r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", r'^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$',
CustomPasswordResetFromKeyView.as_view(), CustomPasswordResetFromKeyView.as_view(),
name="account_reset_password_from_key", name='account_reset_password_from_key',
), ),
# Override login page # Override login page
re_path("accounts/login/", CustomLoginView.as_view(), name="account_login"), re_path('accounts/login/', CustomLoginView.as_view(), name='account_login'),
re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
] ]

View File

@ -119,7 +119,7 @@ def validate_overage(value):
i = Decimal(value) i = Decimal(value)
if i < 0: if i < 0:
raise ValidationError(_("Overage value must not be negative")) raise ValidationError(_('Overage value must not be negative'))
# Looks like a number # Looks like a number
return True return True
@ -135,15 +135,15 @@ def validate_overage(value):
f = float(v) f = float(v)
if f < 0: if f < 0:
raise ValidationError(_("Overage value must not be negative")) raise ValidationError(_('Overage value must not be negative'))
elif f > 100: elif f > 100:
raise ValidationError(_("Overage must not exceed 100%")) raise ValidationError(_('Overage must not exceed 100%'))
return True return True
except ValueError: except ValueError:
pass pass
raise ValidationError(_("Invalid value for overage")) raise ValidationError(_('Invalid value for overage'))
def validate_part_name_format(value): def validate_part_name_format(value):

View File

@ -19,7 +19,7 @@ from dulwich.repo import NotGitRepository, Repo
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version # InvenTree software version
INVENTREE_SW_VERSION = "0.14.0 dev" INVENTREE_SW_VERSION = '0.14.0 dev'
# Discover git # Discover git
try: try:
@ -32,8 +32,8 @@ except (NotGitRepository, FileNotFoundError):
def checkMinPythonVersion(): def checkMinPythonVersion():
"""Check that the Python version is at least 3.9""" """Check that the Python version is at least 3.9"""
version = sys.version.split(" ")[0] version = sys.version.split(' ')[0]
docs = "https://docs.inventree.org/en/stable/start/intro/#python-requirements" docs = 'https://docs.inventree.org/en/stable/start/intro/#python-requirements'
msg = f""" msg = f"""
InvenTree requires Python 3.9 or above - you are running version {version}. InvenTree requires Python 3.9 or above - you are running version {version}.
@ -47,22 +47,22 @@ def checkMinPythonVersion():
if sys.version_info.major == 3 and sys.version_info.minor < 9: if sys.version_info.major == 3 and sys.version_info.minor < 9:
raise RuntimeError(msg) raise RuntimeError(msg)
print(f"Python version {version} - {sys.executable}") print(f'Python version {version} - {sys.executable}')
def inventreeInstanceName(): def inventreeInstanceName():
"""Returns the InstanceName settings for the current database.""" """Returns the InstanceName settings for the current database."""
import common.models import common.models
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") return common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE', '')
def inventreeInstanceTitle(): def inventreeInstanceTitle():
"""Returns the InstanceTitle for the current database.""" """Returns the InstanceTitle for the current database."""
import common.models import common.models
if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False): if common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE_TITLE', False):
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") return common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE', '')
return 'InvenTree' return 'InvenTree'
@ -76,7 +76,7 @@ def inventreeVersionTuple(version=None):
if version is None: if version is None:
version = INVENTREE_SW_VERSION version = INVENTREE_SW_VERSION
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", str(version)) match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', str(version))
return [int(g) for g in match.groups()] return [int(g) for g in match.groups()]
@ -93,14 +93,14 @@ def inventreeDocsVersion():
Release -> "major.minor.sub" e.g. "0.5.2" Release -> "major.minor.sub" e.g. "0.5.2"
""" """
if isInvenTreeDevelopmentVersion(): if isInvenTreeDevelopmentVersion():
return "latest" return 'latest'
return INVENTREE_SW_VERSION # pragma: no cover return INVENTREE_SW_VERSION # pragma: no cover
def inventreeDocUrl(): def inventreeDocUrl():
"""Return URL for InvenTree documentation site.""" """Return URL for InvenTree documentation site."""
tag = inventreeDocsVersion() tag = inventreeDocsVersion()
return f"https://docs.inventree.org/en/{tag}" return f'https://docs.inventree.org/en/{tag}'
def inventreeAppUrl(): def inventreeAppUrl():
@ -110,12 +110,12 @@ def inventreeAppUrl():
def inventreeCreditsUrl(): def inventreeCreditsUrl():
"""Return URL for InvenTree credits site.""" """Return URL for InvenTree credits site."""
return "https://docs.inventree.org/en/latest/credits/" return 'https://docs.inventree.org/en/latest/credits/'
def inventreeGithubUrl(): def inventreeGithubUrl():
"""Return URL for InvenTree github site.""" """Return URL for InvenTree github site."""
return "https://github.com/InvenTree/InvenTree/" return 'https://github.com/InvenTree/InvenTree/'
def isInvenTreeUpToDate(): def isInvenTreeUpToDate():
@ -147,26 +147,26 @@ def inventreeApiVersion():
def parse_version_text(): def parse_version_text():
"""Parse the version text to structured data.""" """Parse the version text to structured data."""
patched_data = INVENTREE_API_TEXT.split("\n\n") patched_data = INVENTREE_API_TEXT.split('\n\n')
# Remove first newline on latest version # Remove first newline on latest version
patched_data[0] = patched_data[0].replace("\n", "", 1) patched_data[0] = patched_data[0].replace('\n', '', 1)
version_data = {} version_data = {}
for version in patched_data: for version in patched_data:
data = version.split("\n") data = version.split('\n')
version_split = data[0].split(' -> ') version_split = data[0].split(' -> ')
version_detail = ( version_detail = (
version_split[1].split(':', 1) if len(version_split) > 1 else [''] version_split[1].split(':', 1) if len(version_split) > 1 else ['']
) )
new_data = { new_data = {
"version": version_split[0].strip(), 'version': version_split[0].strip(),
"date": version_detail[0].strip(), 'date': version_detail[0].strip(),
"gh": version_detail[1].strip() if len(version_detail) > 1 else None, 'gh': version_detail[1].strip() if len(version_detail) > 1 else None,
"text": data[1:], 'text': data[1:],
"latest": False, 'latest': False,
} }
version_data[new_data["version"]] = new_data version_data[new_data['version']] = new_data
return version_data return version_data
@ -188,7 +188,7 @@ def inventreeApiText(versions: int = 10, start_version: int = 0):
start_version = INVENTREE_API_VERSION - versions start_version = INVENTREE_API_VERSION - versions
return { return {
f"v{a}": version_data.get(f"v{a}", None) f'v{a}': version_data.get(f'v{a}', None)
for a in range(start_version, start_version + versions) for a in range(start_version, start_version + versions)
} }

View File

@ -135,13 +135,13 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
app_label = model._meta.app_label app_label = model._meta.app_label
model_name = model._meta.model_name model_name = model._meta.model_name
table = f"{app_label}_{model_name}" table = f'{app_label}_{model_name}'
permission = self.get_permission_class() permission = self.get_permission_class()
if not permission: if not permission:
raise AttributeError( raise AttributeError(
f"permission_class not defined for {type(self).__name__}" f'permission_class not defined for {type(self).__name__}'
) )
# Check if the user has the required permission # Check if the user has the required permission
@ -396,8 +396,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
class EditUserView(AjaxUpdateView): class EditUserView(AjaxUpdateView):
"""View for editing user information.""" """View for editing user information."""
ajax_template_name = "modal_form.html" ajax_template_name = 'modal_form.html'
ajax_form_title = _("Edit User Information") ajax_form_title = _('Edit User Information')
form_class = EditUserForm form_class = EditUserForm
def get_object(self): def get_object(self):
@ -408,8 +408,8 @@ class EditUserView(AjaxUpdateView):
class SetPasswordView(AjaxUpdateView): class SetPasswordView(AjaxUpdateView):
"""View for setting user password.""" """View for setting user password."""
ajax_template_name = "InvenTree/password.html" ajax_template_name = 'InvenTree/password.html'
ajax_form_title = _("Set Password") ajax_form_title = _('Set Password')
form_class = SetPasswordForm form_class = SetPasswordForm
def get_object(self): def get_object(self):
@ -491,14 +491,14 @@ class SearchView(TemplateView):
class DynamicJsView(TemplateView): class DynamicJsView(TemplateView):
"""View for returning javacsript files, which instead of being served dynamically, are passed through the django translation engine!""" """View for returning javacsript files, which instead of being served dynamically, are passed through the django translation engine!"""
template_name = "" template_name = ''
content_type = 'text/javascript' content_type = 'text/javascript'
class SettingsView(TemplateView): class SettingsView(TemplateView):
"""View for configuring User settings.""" """View for configuring User settings."""
template_name = "InvenTree/settings/settings.html" template_name = 'InvenTree/settings/settings.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add data for template.""" """Add data for template."""
@ -506,12 +506,12 @@ class SettingsView(TemplateView):
ctx['settings'] = common_models.InvenTreeSetting.objects.all().order_by('key') ctx['settings'] = common_models.InvenTreeSetting.objects.all().order_by('key')
ctx["base_currency"] = common_settings.currency_code_default() ctx['base_currency'] = common_settings.currency_code_default()
ctx["currencies"] = common_settings.currency_codes ctx['currencies'] = common_settings.currency_codes
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange") ctx['rates'] = Rate.objects.filter(backend='InvenTreeExchange')
ctx["categories"] = PartCategory.objects.all().order_by( ctx['categories'] = PartCategory.objects.all().order_by(
'tree_id', 'lft', 'name' 'tree_id', 'lft', 'name'
) )
@ -520,16 +520,16 @@ class SettingsView(TemplateView):
backend = ExchangeBackend.objects.filter(name='InvenTreeExchange') backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')
if backend.exists(): if backend.exists():
backend = backend.first() backend = backend.first()
ctx["rates_updated"] = backend.last_update ctx['rates_updated'] = backend.last_update
except Exception: except Exception:
ctx["rates_updated"] = None ctx['rates_updated'] = None
# Forms and context for allauth # Forms and context for allauth
ctx['add_email_form'] = AddEmailForm ctx['add_email_form'] = AddEmailForm
ctx["can_add_email"] = EmailAddress.objects.can_add_email(self.request.user) ctx['can_add_email'] = EmailAddress.objects.can_add_email(self.request.user)
# Form and context for allauth social-accounts # Form and context for allauth social-accounts
ctx["request"] = self.request ctx['request'] = self.request
ctx['social_form'] = DisconnectForm(request=self.request) ctx['social_form'] = DisconnectForm(request=self.request)
# user db sessions # user db sessions
@ -552,19 +552,19 @@ class AllauthOverrides(LoginRequiredMixin):
class CustomEmailView(AllauthOverrides, EmailView): class CustomEmailView(AllauthOverrides, EmailView):
"""Override of allauths EmailView to always show the settings but leave the functions allow.""" """Override of allauths EmailView to always show the settings but leave the functions allow."""
success_url = reverse_lazy("settings") success_url = reverse_lazy('settings')
class CustomConnectionsView(AllauthOverrides, ConnectionsView): class CustomConnectionsView(AllauthOverrides, ConnectionsView):
"""Override of allauths ConnectionsView to always show the settings but leave the functions allow.""" """Override of allauths ConnectionsView to always show the settings but leave the functions allow."""
success_url = reverse_lazy("settings") success_url = reverse_lazy('settings')
class CustomPasswordResetFromKeyView(PasswordResetFromKeyView): class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
"""Override of allauths PasswordResetFromKeyView to always show the settings but leave the functions allow.""" """Override of allauths PasswordResetFromKeyView to always show the settings but leave the functions allow."""
success_url = reverse_lazy("account_login") success_url = reverse_lazy('account_login')
class UserSessionOverride: class UserSessionOverride:
@ -646,18 +646,18 @@ class AppearanceSelectView(RedirectView):
class DatabaseStatsView(AjaxView): class DatabaseStatsView(AjaxView):
"""View for displaying database statistics.""" """View for displaying database statistics."""
ajax_template_name = "stats.html" ajax_template_name = 'stats.html'
ajax_form_title = _("System Information") ajax_form_title = _('System Information')
class AboutView(AjaxView): class AboutView(AjaxView):
"""A view for displaying InvenTree version information""" """A view for displaying InvenTree version information"""
ajax_template_name = "about.html" ajax_template_name = 'about.html'
ajax_form_title = _("About InvenTree") ajax_form_title = _('About InvenTree')
class NotificationsView(TemplateView): class NotificationsView(TemplateView):
"""View for showing notifications.""" """View for showing notifications."""
template_name = "InvenTree/notifications/notifications.html" template_name = 'InvenTree/notifications/notifications.html'

View File

@ -11,7 +11,7 @@ import os # pragma: no cover
from django.core.wsgi import get_wsgi_application # pragma: no cover from django.core.wsgi import get_wsgi_application # pragma: no cover
os.environ.setdefault( os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "InvenTree.settings" 'DJANGO_SETTINGS_MODULE', 'InvenTree.settings'
) # pragma: no cover ) # pragma: no cover
application = get_wsgi_application() # pragma: no cover application = get_wsgi_application() # pragma: no cover

View File

@ -181,7 +181,7 @@ class SettingsList(ListAPI):
class GlobalSettingsList(SettingsList): class GlobalSettingsList(SettingsList):
"""API endpoint for accessing a list of global settings objects.""" """API endpoint for accessing a list of global settings objects."""
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_") queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer serializer_class = common.serializers.GlobalSettingsSerializer
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
@ -214,7 +214,7 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
""" """
lookup_field = 'key' lookup_field = 'key'
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_") queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer serializer_class = common.serializers.GlobalSettingsSerializer
def get_object(self): def get_object(self):

View File

@ -29,7 +29,7 @@ class CommonConfig(AppConfig):
if common.models.InvenTreeSetting.get_setting( if common.models.InvenTreeSetting.get_setting(
'SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False 'SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False
): ):
logger.info("Clearing SERVER_RESTART_REQUIRED flag") logger.info('Clearing SERVER_RESTART_REQUIRED flag')
if not isImportingData(): if not isImportingData():
common.models.InvenTreeSetting.set_setting( common.models.InvenTreeSetting.set_setting(

View File

@ -220,7 +220,7 @@ class BaseInvenTreeSetting(models.Model):
If a particular setting is not present, create it with the default value If a particular setting is not present, create it with the default value
""" """
cache_key = f"BUILD_DEFAULT_VALUES:{str(cls.__name__)}" cache_key = f'BUILD_DEFAULT_VALUES:{str(cls.__name__)}'
if InvenTree.helpers.str2bool(cache.get(cache_key, False)): if InvenTree.helpers.str2bool(cache.get(cache_key, False)):
# Already built default values # Already built default values
@ -234,7 +234,7 @@ class BaseInvenTreeSetting(models.Model):
if len(missing_keys) > 0: if len(missing_keys) > 0:
logger.info( logger.info(
"Building %s default values for %s", len(missing_keys), str(cls) 'Building %s default values for %s', len(missing_keys), str(cls)
) )
cls.objects.bulk_create([ cls.objects.bulk_create([
cls(key=key, value=cls.get_setting_default(key), **kwargs) cls(key=key, value=cls.get_setting_default(key), **kwargs)
@ -243,7 +243,7 @@ class BaseInvenTreeSetting(models.Model):
]) ])
except Exception as exc: except Exception as exc:
logger.exception( logger.exception(
"Failed to build default values for %s (%s)", str(cls), str(type(exc)) 'Failed to build default values for %s (%s)', str(cls), str(type(exc))
) )
pass pass
@ -299,12 +299,12 @@ class BaseInvenTreeSetting(models.Model):
- The unique KEY string - The unique KEY string
- Any key:value kwargs associated with the particular setting type (e.g. user-id) - Any key:value kwargs associated with the particular setting type (e.g. user-id)
""" """
key = f"{str(cls.__name__)}:{setting_key}" key = f'{str(cls.__name__)}:{setting_key}'
for k, v in kwargs.items(): for k, v in kwargs.items():
key += f"_{k}:{v}" key += f'_{k}:{v}'
return key.replace(" ", "") return key.replace(' ', '')
@classmethod @classmethod
def get_filters(cls, **kwargs): def get_filters(cls, **kwargs):
@ -366,14 +366,14 @@ class BaseInvenTreeSetting(models.Model):
) )
# remove any hidden settings # remove any hidden settings
if exclude_hidden and setting.get("hidden", False): if exclude_hidden and setting.get('hidden', False):
del settings[key.upper()] del settings[key.upper()]
# format settings values and remove protected # format settings values and remove protected
for key, setting in settings.items(): for key, setting in settings.items():
validator = cls.get_setting_validator(key, **filters) validator = cls.get_setting_validator(key, **filters)
if cls.is_protected(key, **filters) and setting.value != "": if cls.is_protected(key, **filters) and setting.value != '':
setting.value = '***' setting.value = '***'
elif cls.validator_is_bool(validator): elif cls.validator_is_bool(validator):
setting.value = InvenTree.helpers.str2bool(setting.value) setting.value = InvenTree.helpers.str2bool(setting.value)
@ -438,7 +438,7 @@ class BaseInvenTreeSetting(models.Model):
if setting.required: if setting.required:
value = setting.value or cls.get_setting_default(setting.key, **kwargs) value = setting.value or cls.get_setting_default(setting.key, **kwargs)
if value == "": if value == '':
missing_settings.append(setting.key.upper()) missing_settings.append(setting.key.upper())
return len(missing_settings) == 0, missing_settings return len(missing_settings) == 0, missing_settings
@ -766,7 +766,7 @@ class BaseInvenTreeSetting(models.Model):
options = self.valid_options() options = self.valid_options()
if options and self.value not in options: if options and self.value not in options:
raise ValidationError(_("Chosen value is not a valid option")) raise ValidationError(_('Chosen value is not a valid option'))
def run_validator(self, validator): def run_validator(self, validator):
"""Run a validator against the 'value' field for this InvenTreeSetting object.""" """Run a validator against the 'value' field for this InvenTreeSetting object."""
@ -1049,7 +1049,7 @@ class BaseInvenTreeSetting(models.Model):
"""Check if this setting value is required.""" """Check if this setting value is required."""
setting = cls.get_setting_definition(key, **cls.get_filters(**kwargs)) setting = cls.get_setting_definition(key, **cls.get_filters(**kwargs))
return setting.get("required", False) return setting.get('required', False)
@property @property
def required(self): def required(self):
@ -1131,8 +1131,8 @@ class InvenTreeSetting(BaseInvenTreeSetting):
class Meta: class Meta:
"""Meta options for InvenTreeSetting.""" """Meta options for InvenTreeSetting."""
verbose_name = "InvenTree Setting" verbose_name = 'InvenTree Setting'
verbose_name_plural = "InvenTree Settings" verbose_name_plural = 'InvenTree Settings'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""When saving a global setting, check to see if it requires a server restart. """When saving a global setting, check to see if it requires a server restart.
@ -1454,7 +1454,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'name': _('Part Name Display Format'), 'name': _('Part Name Display Format'),
'description': _('Format to display the part name'), 'description': _('Format to display the part name'),
'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}" 'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}"
"{{ part.revision if part.revision }}", '{{ part.revision if part.revision }}',
'validator': InvenTree.validators.validate_part_name_format, 'validator': InvenTree.validators.validate_part_name_format,
}, },
'PART_CATEGORY_DEFAULT_ICON': { 'PART_CATEGORY_DEFAULT_ICON': {
@ -1860,7 +1860,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'after_save': reload_plugin_registry, 'after_save': reload_plugin_registry,
}, },
"PROJECT_CODES_ENABLED": { 'PROJECT_CODES_ENABLED': {
'name': _('Enable project codes'), 'name': _('Enable project codes'),
'description': _('Enable project codes for tracking projects'), 'description': _('Enable project codes for tracking projects'),
'default': False, 'default': False,
@ -1946,8 +1946,8 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
class Meta: class Meta:
"""Meta options for InvenTreeUserSetting.""" """Meta options for InvenTreeUserSetting."""
verbose_name = "InvenTree User Setting" verbose_name = 'InvenTree User Setting'
verbose_name_plural = "InvenTree User Settings" verbose_name_plural = 'InvenTree User Settings'
constraints = [ constraints = [
models.UniqueConstraint(fields=['key', 'user'], name='unique key and user') models.UniqueConstraint(fields=['key', 'user'], name='unique key and user')
] ]
@ -2069,7 +2069,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
"LABEL_INLINE": { 'LABEL_INLINE': {
'name': _('Inline label display'), 'name': _('Inline label display'),
'description': _( 'description': _(
'Display PDF labels in the browser, instead of downloading as a file' 'Display PDF labels in the browser, instead of downloading as a file'
@ -2077,7 +2077,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
"LABEL_DEFAULT_PRINTER": { 'LABEL_DEFAULT_PRINTER': {
'name': _('Default label printer'), 'name': _('Default label printer'),
'description': _( 'description': _(
'Configure which label printer should be selected by default' 'Configure which label printer should be selected by default'
@ -2085,7 +2085,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': '', 'default': '',
'choices': label_printer_options, 'choices': label_printer_options,
}, },
"REPORT_INLINE": { 'REPORT_INLINE': {
'name': _('Inline report display'), 'name': _('Inline report display'),
'description': _( 'description': _(
'Display PDF reports in the browser, instead of downloading as a file' 'Display PDF reports in the browser, instead of downloading as a file'
@ -2112,7 +2112,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
}, },
'SEARCH_HIDE_INACTIVE_PARTS': { 'SEARCH_HIDE_INACTIVE_PARTS': {
'name': _("Hide Inactive Parts"), 'name': _('Hide Inactive Parts'),
'description': _('Excluded inactive parts from search preview window'), 'description': _('Excluded inactive parts from search preview window'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
@ -2360,7 +2360,7 @@ class PriceBreak(MetaMixin):
converted = convert_money(self.price, currency_code) converted = convert_money(self.price, currency_code)
except MissingRate: except MissingRate:
logger.warning( logger.warning(
"No currency conversion rate available for %s -> %s", 'No currency conversion rate available for %s -> %s',
self.price_currency, self.price_currency,
currency_code, currency_code,
) )
@ -2510,11 +2510,11 @@ class WebhookEndpoint(models.Model):
""" """
# Token # Token
TOKEN_NAME = "Token" TOKEN_NAME = 'Token'
VERIFICATION_METHOD = VerificationMethod.NONE VERIFICATION_METHOD = VerificationMethod.NONE
MESSAGE_OK = "Message was received." MESSAGE_OK = 'Message was received.'
MESSAGE_TOKEN_ERROR = "Incorrect token in header." MESSAGE_TOKEN_ERROR = 'Incorrect token in header.'
endpoint_id = models.CharField( endpoint_id = models.CharField(
max_length=255, max_length=255,
@ -2589,7 +2589,7 @@ class WebhookEndpoint(models.Model):
This can be overridden to create your own token validation method. This can be overridden to create your own token validation method.
""" """
token = headers.get(self.TOKEN_NAME, "") token = headers.get(self.TOKEN_NAME, '')
# no token # no token
if self.verify == VerificationMethod.NONE: if self.verify == VerificationMethod.NONE:

View File

@ -311,23 +311,23 @@ class InvenTreeNotificationBodies:
""" """
NewOrder = NotificationBody( NewOrder = NotificationBody(
name=_("New {verbose_name}"), name=_('New {verbose_name}'),
slug='{app_label}.new_{model_name}', slug='{app_label}.new_{model_name}',
message=_("A new order has been created and assigned to you"), message=_('A new order has been created and assigned to you'),
template='email/new_order_assigned.html', template='email/new_order_assigned.html',
) )
"""Send when a new order (build, sale or purchase) was created.""" """Send when a new order (build, sale or purchase) was created."""
OrderCanceled = NotificationBody( OrderCanceled = NotificationBody(
name=_("{verbose_name} canceled"), name=_('{verbose_name} canceled'),
slug='{app_label}.canceled_{model_name}', slug='{app_label}.canceled_{model_name}',
message=_("A order that is assigned to you was canceled"), message=_('A order that is assigned to you was canceled'),
template='email/canceled_order_assigned.html', template='email/canceled_order_assigned.html',
) )
"""Send when a order (sale, return or purchase) was canceled.""" """Send when a order (sale, return or purchase) was canceled."""
ItemsReceived = NotificationBody( ItemsReceived = NotificationBody(
name=_("Items Received"), name=_('Items Received'),
slug='purchase_order.items_received', slug='purchase_order.items_received',
message=_('Items have been received against a purchase order'), message=_('Items have been received against a purchase order'),
template='email/purchase_order_received.html', template='email/purchase_order_received.html',
@ -414,7 +414,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
# Unhandled type # Unhandled type
else: else:
logger.error( logger.error(
"Unknown target passed to trigger_notification method: %s", target 'Unknown target passed to trigger_notification method: %s', target
) )
if target_users: if target_users:
@ -515,4 +515,4 @@ def deliver_notification(
str(obj), str(obj),
) )
if not success: if not success:
logger.info("There were some problems") logger.info('There were some problems')

View File

@ -51,7 +51,7 @@ def update_news_feed():
try: try:
d = feedparser.parse(settings.INVENTREE_NEWS_URL) d = feedparser.parse(settings.INVENTREE_NEWS_URL)
except Exception as entry: # pragma: no cover except Exception as entry: # pragma: no cover
logger.warning("update_news_feed: Error parsing the newsfeed", entry) logger.warning('update_news_feed: Error parsing the newsfeed', entry)
return return
# Get a reference list # Get a reference list
@ -97,7 +97,7 @@ def delete_old_notes_images():
# Remove any notes which point to non-existent image files # Remove any notes which point to non-existent image files
for note in NotesImage.objects.all(): for note in NotesImage.objects.all():
if not os.path.exists(note.image.path): if not os.path.exists(note.image.path):
logger.info("Deleting note %s - image file does not exist", note.image.path) logger.info('Deleting note %s - image file does not exist', note.image.path)
note.delete() note.delete()
note_classes = getModelsWithMixin(InvenTreeNotesMixin) note_classes = getModelsWithMixin(InvenTreeNotesMixin)
@ -116,7 +116,7 @@ def delete_old_notes_images():
break break
if not found: if not found:
logger.info("Deleting note %s - image file not linked to a note", img) logger.info('Deleting note %s - image file not linked to a note', img)
note.delete() note.delete()
# Finally, remove any images in the notes dir which are not linked to a note # Finally, remove any images in the notes dir which are not linked to a note
@ -139,5 +139,5 @@ def delete_old_notes_images():
break break
if not found: if not found:
logger.info("Deleting note %s - image file not linked to a note", image) logger.info('Deleting note %s - image file not linked to a note', image)
os.remove(os.path.join(notes_dir, image)) os.remove(os.path.join(notes_dir, image))

View File

@ -136,19 +136,19 @@ class SettingsTest(InvenTreeTestCase):
def test_all_settings(self): def test_all_settings(self):
"""Make sure that the all_settings function returns correctly""" """Make sure that the all_settings function returns correctly"""
result = InvenTreeSetting.all_settings() result = InvenTreeSetting.all_settings()
self.assertIn("INVENTREE_INSTANCE", result) self.assertIn('INVENTREE_INSTANCE', result)
self.assertIsInstance(result['INVENTREE_INSTANCE'], InvenTreeSetting) self.assertIsInstance(result['INVENTREE_INSTANCE'], InvenTreeSetting)
@mock.patch("common.models.InvenTreeSetting.get_setting_definition") @mock.patch('common.models.InvenTreeSetting.get_setting_definition')
def test_check_all_settings(self, get_setting_definition): def test_check_all_settings(self, get_setting_definition):
"""Make sure that the check_all_settings function returns correctly""" """Make sure that the check_all_settings function returns correctly"""
# define partial schema # define partial schema
settings_definition = { settings_definition = {
"AB": { # key that's has not already been accessed 'AB': { # key that's has not already been accessed
"required": True 'required': True
}, },
"CD": {"required": True, "protected": True}, 'CD': {'required': True, 'protected': True},
"EF": {}, 'EF': {},
} }
def mocked(key, **kwargs): def mocked(key, **kwargs):
@ -160,28 +160,28 @@ class SettingsTest(InvenTreeTestCase):
InvenTreeSetting.check_all_settings( InvenTreeSetting.check_all_settings(
settings_definition=settings_definition settings_definition=settings_definition
), ),
(False, ["AB", "CD"]), (False, ['AB', 'CD']),
) )
InvenTreeSetting.set_setting('AB', "hello", self.user) InvenTreeSetting.set_setting('AB', 'hello', self.user)
InvenTreeSetting.set_setting('CD', "world", self.user) InvenTreeSetting.set_setting('CD', 'world', self.user)
self.assertEqual(InvenTreeSetting.check_all_settings(), (True, [])) self.assertEqual(InvenTreeSetting.check_all_settings(), (True, []))
@mock.patch("common.models.InvenTreeSetting.get_setting_definition") @mock.patch('common.models.InvenTreeSetting.get_setting_definition')
def test_settings_validator(self, get_setting_definition): def test_settings_validator(self, get_setting_definition):
"""Make sure that the validator function gets called on set setting.""" """Make sure that the validator function gets called on set setting."""
def validator(x): def validator(x):
if x == "hello": if x == 'hello':
return x return x
raise ValidationError(f"{x} is not valid") raise ValidationError(f'{x} is not valid')
mock_validator = mock.Mock(side_effect=validator) mock_validator = mock.Mock(side_effect=validator)
# define partial schema # define partial schema
settings_definition = { settings_definition = {
"AB": { # key that's has not already been accessed 'AB': { # key that's has not already been accessed
"validator": mock_validator 'validator': mock_validator
} }
} }
@ -190,12 +190,12 @@ class SettingsTest(InvenTreeTestCase):
get_setting_definition.side_effect = mocked get_setting_definition.side_effect = mocked
InvenTreeSetting.set_setting("AB", "hello", self.user) InvenTreeSetting.set_setting('AB', 'hello', self.user)
mock_validator.assert_called_with("hello") mock_validator.assert_called_with('hello')
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
InvenTreeSetting.set_setting("AB", "world", self.user) InvenTreeSetting.set_setting('AB', 'world', self.user)
mock_validator.assert_called_with("world") mock_validator.assert_called_with('world')
def run_settings_check(self, key, setting): def run_settings_check(self, key, setting):
"""Test that all settings are valid. """Test that all settings are valid.
@ -322,7 +322,7 @@ class SettingsTest(InvenTreeTestCase):
# Generate a number of new users # Generate a number of new users
for idx in range(5): for idx in range(5):
get_user_model().objects.create( get_user_model().objects.create(
username=f"User_{idx}", password="hunter42", email="email@dot.com" username=f'User_{idx}', password='hunter42', email='email@dot.com'
) )
key = 'SEARCH_PREVIEW_RESULTS' key = 'SEARCH_PREVIEW_RESULTS'
@ -333,7 +333,7 @@ class SettingsTest(InvenTreeTestCase):
cache_key = setting.cache_key cache_key = setting.cache_key
self.assertEqual( self.assertEqual(
cache_key, cache_key,
f"InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}", f'InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}',
) )
InvenTreeUserSetting.set_setting(key, user.pk, None, user=user) InvenTreeUserSetting.set_setting(key, user.pk, None, user=user)
self.assertIsNotNone(cache.get(cache_key)) self.assertIsNotNone(cache.get(cache_key))
@ -399,7 +399,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
def test_api_detail(self): def test_api_detail(self):
"""Test that we can access the detail view for a setting based on the <key>.""" """Test that we can access the detail view for a setting based on the <key>."""
# These keys are invalid, and should return 404 # These keys are invalid, and should return 404
for key in ["apple", "carrot", "dog"]: for key in ['apple', 'carrot', 'dog']:
response = self.get( response = self.get(
reverse('api-global-setting-detail', kwargs={'key': key}), reverse('api-global-setting-detail', kwargs={'key': key}),
expected_code=404, expected_code=404,
@ -770,7 +770,7 @@ class WebhookMessageTests(TestCase):
""" """
response = self.client.post( response = self.client.post(
self.url, self.url,
data={"this": "is a message"}, data={'this': 'is a message'},
content_type=CONTENT_TYPE_JSON, content_type=CONTENT_TYPE_JSON,
**{'HTTP_TOKEN': str(self.endpoint_def.token)}, **{'HTTP_TOKEN': str(self.endpoint_def.token)},
) )
@ -778,7 +778,7 @@ class WebhookMessageTests(TestCase):
assert response.status_code == HTTPStatus.OK assert response.status_code == HTTPStatus.OK
assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
message = WebhookMessage.objects.get() message = WebhookMessage.objects.get()
assert message.body == {"this": "is a message"} assert message.body == {'this': 'is a message'}
class NotificationTest(InvenTreeAPITestCase): class NotificationTest(InvenTreeAPITestCase):
@ -1033,7 +1033,7 @@ class CurrencyAPITests(InvenTreeAPITestCase):
# Delay and try again # Delay and try again
time.sleep(10) time.sleep(10)
raise TimeoutError("Could not refresh currency exchange data after 5 attempts") raise TimeoutError('Could not refresh currency exchange data after 5 attempts')
class NotesImageTest(InvenTreeAPITestCase): class NotesImageTest(InvenTreeAPITestCase):
@ -1048,28 +1048,28 @@ class NotesImageTest(InvenTreeAPITestCase):
reverse('api-notes-image-list'), reverse('api-notes-image-list'),
data={ data={
'image': SimpleUploadedFile( 'image': SimpleUploadedFile(
'test.txt', b"this is not an image file", content_type='text/plain' 'test.txt', b'this is not an image file', content_type='text/plain'
) )
}, },
format='multipart', format='multipart',
expected_code=400, expected_code=400,
) )
self.assertIn("Upload a valid image", str(response.data['image'])) self.assertIn('Upload a valid image', str(response.data['image']))
# Test upload of an invalid image file # Test upload of an invalid image file
response = self.post( response = self.post(
reverse('api-notes-image-list'), reverse('api-notes-image-list'),
data={ data={
'image': SimpleUploadedFile( 'image': SimpleUploadedFile(
'test.png', b"this is not an image file", content_type='image/png' 'test.png', b'this is not an image file', content_type='image/png'
) )
}, },
format='multipart', format='multipart',
expected_code=400, expected_code=400,
) )
self.assertIn("Upload a valid image", str(response.data['image'])) self.assertIn('Upload a valid image', str(response.data['image']))
# Check that no extra database entries have been created # Check that no extra database entries have been created
self.assertEqual(NotesImage.objects.count(), n) self.assertEqual(NotesImage.objects.count(), n)

View File

@ -81,7 +81,7 @@ class FileManagementFormView(MultiStepFormView):
('fields', forms.MatchFieldForm), ('fields', forms.MatchFieldForm),
('items', forms.MatchItemForm), ('items', forms.MatchItemForm),
] ]
form_steps_description = [_("Upload File"), _("Match Fields"), _("Match Items")] form_steps_description = [_('Upload File'), _('Match Fields'), _('Match Items')]
media_folder = 'file_upload/' media_folder = 'file_upload/'
extra_context_data = {} extra_context_data = {}

View File

@ -96,7 +96,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
constraints = [ constraints = [
UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair') UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair')
] ]
verbose_name_plural = "Companies" verbose_name_plural = 'Companies'
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
@ -215,7 +215,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
def __str__(self): def __str__(self):
"""Get string representation of a Company.""" """Get string representation of a Company."""
return f"{self.name} - {self.description}" return f'{self.name} - {self.description}'
def get_absolute_url(self): def get_absolute_url(self):
"""Get the web URL for the detail view for this Company.""" """Get the web URL for the detail view for this Company."""
@ -318,7 +318,7 @@ class Address(models.Model):
class Meta: class Meta:
"""Metaclass defines extra model options""" """Metaclass defines extra model options"""
verbose_name_plural = "Addresses" verbose_name_plural = 'Addresses'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Custom init function""" """Custom init function"""
@ -340,7 +340,7 @@ class Address(models.Model):
if len(line) > 0: if len(line) > 0:
populated_lines.append(line) populated_lines.append(line)
return ", ".join(populated_lines) return ', '.join(populated_lines)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Run checks when saving an address: """Run checks when saving an address:
@ -564,7 +564,7 @@ class ManufacturerPartAttachment(InvenTreeAttachment):
def getSubdir(self): def getSubdir(self):
"""Return the subdirectory where attachment files for the ManufacturerPart model are located""" """Return the subdirectory where attachment files for the ManufacturerPart model are located"""
return os.path.join("manufacturer_part_files", str(self.manufacturer_part.id)) return os.path.join('manufacturer_part_files', str(self.manufacturer_part.id))
manufacturer_part = models.ForeignKey( manufacturer_part = models.ForeignKey(
ManufacturerPart, ManufacturerPart,
@ -711,14 +711,14 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
): ):
raise ValidationError({ raise ValidationError({
'pack_quantity': _( 'pack_quantity': _(
"Pack units must be compatible with the base part units" 'Pack units must be compatible with the base part units'
) )
}) })
# Native value must be greater than zero # Native value must be greater than zero
if float(native_value.magnitude) <= 0: if float(native_value.magnitude) <= 0:
raise ValidationError({ raise ValidationError({
'pack_quantity': _("Pack units must be greater than zero") 'pack_quantity': _('Pack units must be greater than zero')
}) })
# Update native pack units value # Update native pack units value
@ -732,7 +732,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
if self.manufacturer_part.part != self.part: if self.manufacturer_part.part != self.part:
raise ValidationError({ raise ValidationError({
'manufacturer_part': _( 'manufacturer_part': _(
"Linked manufacturer part must reference the same base part" 'Linked manufacturer part must reference the same base part'
) )
}) })
@ -787,7 +787,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
SKU = models.CharField( SKU = models.CharField(
max_length=100, max_length=100,
verbose_name=__("SKU = Stock Keeping Unit (supplier part number)", 'SKU'), verbose_name=__('SKU = Stock Keeping Unit (supplier part number)', 'SKU'),
help_text=_('Supplier stock keeping unit'), help_text=_('Supplier stock keeping unit'),
) )
@ -1007,7 +1007,7 @@ class SupplierPriceBreak(common.models.PriceBreak):
class Meta: class Meta:
"""Metaclass defines extra model options""" """Metaclass defines extra model options"""
unique_together = ("part", "quantity") unique_together = ('part', 'quantity')
# This model was moved from the 'Part' app # This model was moved from the 'Part' app
db_table = 'part_supplierpricebreak' db_table = 'part_supplierpricebreak'

View File

@ -168,7 +168,7 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
remote_img.save(buffer, format=fmt) remote_img.save(buffer, format=fmt)
# Construct a simplified name for the image # Construct a simplified name for the image
filename = f"company_{company.pk}_image.{fmt.lower()}" filename = f'company_{company.pk}_image.{fmt.lower()}'
company.image.save(filename, ContentFile(buffer.getvalue())) company.image.save(filename, ContentFile(buffer.getvalue()))

View File

@ -107,8 +107,8 @@ class CompanyTest(InvenTreeAPITestCase):
response = self.post( response = self.post(
url, url,
{ {
'name': "Another Company", 'name': 'Another Company',
'description': "Also created via the API!", 'description': 'Also created via the API!',
'currency': 'AUD', 'currency': 'AUD',
'is_supplier': False, 'is_supplier': False,
'is_manufacturer': True, 'is_manufacturer': True,
@ -125,7 +125,7 @@ class CompanyTest(InvenTreeAPITestCase):
# Attempt to create with invalid currency # Attempt to create with invalid currency
response = self.post( response = self.post(
url, url,
{'name': "A name", 'description': 'A description', 'currency': 'POQD'}, {'name': 'A name', 'description': 'A description', 'currency': 'POQD'},
expected_code=400, expected_code=400,
) )
@ -144,7 +144,7 @@ class ContactTest(InvenTreeAPITestCase):
# Create some companies # Create some companies
companies = [ companies = [
Company(name=f"Company {idx}", description="Some company") Company(name=f'Company {idx}', description='Some company')
for idx in range(3) for idx in range(3)
] ]
@ -155,7 +155,7 @@ class ContactTest(InvenTreeAPITestCase):
# Create some contacts # Create some contacts
for cmp in Company.objects.all(): for cmp in Company.objects.all():
contacts += [ contacts += [
Contact(company=cmp, name=f"My name {idx}") for idx in range(3) Contact(company=cmp, name=f'My name {idx}') for idx in range(3)
] ]
Contact.objects.bulk_create(contacts) Contact.objects.bulk_create(contacts)
@ -251,7 +251,7 @@ class AddressTest(InvenTreeAPITestCase):
cls.num_addr = 3 cls.num_addr = 3
# Create some companies # Create some companies
companies = [ companies = [
Company(name=f"Company {idx}", description="Some company") Company(name=f'Company {idx}', description='Some company')
for idx in range(cls.num_companies) for idx in range(cls.num_companies)
] ]
@ -262,7 +262,7 @@ class AddressTest(InvenTreeAPITestCase):
# Create some contacts # Create some contacts
for cmp in Company.objects.all(): for cmp in Company.objects.all():
addresses += [ addresses += [
Address(company=cmp, title=f"Address no. {idx}") Address(company=cmp, title=f'Address no. {idx}')
for idx in range(cls.num_addr) for idx in range(cls.num_addr)
] ]

View File

@ -228,8 +228,8 @@ class TestCurrencyMigration(MigratorTestCase):
Part = self.old_state.apps.get_model('part', 'part') Part = self.old_state.apps.get_model('part', 'part')
part = Part.objects.create( part = Part.objects.create(
name="PART", name='PART',
description="A purchaseable part", description='A purchaseable part',
purchaseable=True, purchaseable=True,
level=0, level=0,
tree_id=0, tree_id=0,
@ -309,7 +309,7 @@ class TestAddressMigration(MigratorTestCase):
a2 = Address.objects.filter(company=c2.pk).first() a2 = Address.objects.filter(company=c2.pk).first()
self.assertEqual(a1.line1, self.short_l1) self.assertEqual(a1.line1, self.short_l1)
self.assertEqual(a1.line2, "") self.assertEqual(a1.line2, '')
self.assertEqual(a2.line1, self.long_l1) self.assertEqual(a2.line1, self.long_l1)
self.assertEqual(a2.line2, self.l2) self.assertEqual(a2.line2, self.l2)
self.assertEqual(c1.address, '') self.assertEqual(c1.address, '')
@ -329,8 +329,8 @@ class TestSupplierPartQuantity(MigratorTestCase):
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart') SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
self.part = Part.objects.create( self.part = Part.objects.create(
name="PART", name='PART',
description="A purchaseable part", description='A purchaseable part',
purchaseable=True, purchaseable=True,
level=0, level=0,
tree_id=0, tree_id=0,

View File

@ -103,9 +103,9 @@ class CompanySimpleTest(TestCase):
"""Unit tests for supplier part pricing""" """Unit tests for supplier part pricing"""
m2x4 = Part.objects.get(name='M2x4 LPHS') m2x4 = Part.objects.get(name='M2x4 LPHS')
self.assertEqual(m2x4.get_price_info(5.5), "38.5 - 41.25") self.assertEqual(m2x4.get_price_info(5.5), '38.5 - 41.25')
self.assertEqual(m2x4.get_price_info(10), "70 - 75") self.assertEqual(m2x4.get_price_info(10), '70 - 75')
self.assertEqual(m2x4.get_price_info(100), "125 - 350") self.assertEqual(m2x4.get_price_info(100), '125 - 350')
pmin, pmax = m2x4.get_price_range(5) pmin, pmax = m2x4.get_price_range(5)
self.assertEqual(pmin, 35) self.assertEqual(pmin, 35)
@ -222,13 +222,13 @@ class AddressTest(TestCase):
def test_model_str(self): def test_model_str(self):
"""Test value of __str__""" """Test value of __str__"""
t = "Test address" t = 'Test address'
l1 = "Busy street 56" l1 = 'Busy street 56'
l2 = "Red building" l2 = 'Red building'
pcd = "12345" pcd = '12345'
pct = "City" pct = 'City'
pv = "Province" pv = 'Province'
cn = "COUNTRY" cn = 'COUNTRY'
addr = Address.objects.create( addr = Address.objects.create(
company=self.c, company=self.c,
title=t, title=t,

View File

@ -39,10 +39,10 @@ class StatusView(APIView):
status_class = self.get_status_model() status_class = self.get_status_model()
if not inspect.isclass(status_class): if not inspect.isclass(status_class):
raise NotImplementedError("`status_class` not a class") raise NotImplementedError('`status_class` not a class')
if not issubclass(status_class, StatusCode): if not issubclass(status_class, StatusCode):
raise NotImplementedError("`status_class` not a valid StatusCode class") raise NotImplementedError('`status_class` not a valid StatusCode class')
data = {'class': status_class.__name__, 'values': status_class.dict()} data = {'class': status_class.__name__, 'values': status_class.dict()}

View File

@ -14,9 +14,9 @@ from .states import StatusCode
class GeneralStatus(StatusCode): class GeneralStatus(StatusCode):
"""Defines a set of status codes for tests.""" """Defines a set of status codes for tests."""
PENDING = 10, _("Pending"), 'secondary' PENDING = 10, _('Pending'), 'secondary'
PLACED = 20, _("Placed"), 'primary' PLACED = 20, _('Placed'), 'primary'
COMPLETE = 30, _("Complete"), 'success' COMPLETE = 30, _('Complete'), 'success'
ABC = None # This should be ignored ABC = None # This should be ignored
_DEF = None # This should be ignored _DEF = None # This should be ignored
jkl = None # This should be ignored jkl = None # This should be ignored
@ -183,11 +183,11 @@ class GeneralStateTest(InvenTreeTestCase):
# Invalid call - not a class # Invalid call - not a class
with self.assertRaises(NotImplementedError) as e: with self.assertRaises(NotImplementedError) as e:
resp = view(rqst, **{StatusView.MODEL_REF: 'invalid'}) resp = view(rqst, **{StatusView.MODEL_REF: 'invalid'})
self.assertEqual(str(e.exception), "`status_class` not a class") self.assertEqual(str(e.exception), '`status_class` not a class')
# Invalid call - not the right class # Invalid call - not the right class
with self.assertRaises(NotImplementedError) as e: with self.assertRaises(NotImplementedError) as e:
resp = view(rqst, **{StatusView.MODEL_REF: object}) resp = view(rqst, **{StatusView.MODEL_REF: object})
self.assertEqual( self.assertEqual(
str(e.exception), "`status_class` not a valid StatusCode class" str(e.exception), '`status_class` not a valid StatusCode class'
) )

View File

@ -2,7 +2,7 @@
import multiprocessing import multiprocessing
bind = "0.0.0.0:8000" bind = '0.0.0.0:8000'
workers = multiprocessing.cpu_count() * 2 + 1 workers = multiprocessing.cpu_count() * 2 + 1

View File

@ -130,12 +130,12 @@ class LabelListView(LabelFilterMixin, ListCreateAPI):
class LabelPrintMixin(LabelFilterMixin): class LabelPrintMixin(LabelFilterMixin):
"""Mixin for printing labels.""" """Mixin for printing labels."""
rolemap = {"GET": "view", "POST": "view"} rolemap = {'GET': 'view', 'POST': 'view'}
def check_permissions(self, request): def check_permissions(self, request):
"""Override request method to GET so that also non superusers can print using a post request.""" """Override request method to GET so that also non superusers can print using a post request."""
if request.method == "POST": if request.method == 'POST':
request = clone_request(request, "GET") request = clone_request(request, 'GET')
return super().check_permissions(request) return super().check_permissions(request)
@method_decorator(never_cache) @method_decorator(never_cache)
@ -199,7 +199,7 @@ class LabelPrintMixin(LabelFilterMixin):
if not plugin.is_active(): if not plugin.is_active():
raise ValidationError(f"Plugin '{plugin_key}' is not enabled") raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
if not plugin.mixin_enabled("labels"): if not plugin.mixin_enabled('labels'):
raise ValidationError( raise ValidationError(
f"Plugin '{plugin_key}' is not a label printing plugin" f"Plugin '{plugin_key}' is not a label printing plugin"
) )

View File

@ -19,7 +19,7 @@ from InvenTree.ready import (
isPluginRegistryLoaded, isPluginRegistryLoaded,
) )
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
def hashFile(filename): def hashFile(filename):

View File

@ -25,12 +25,12 @@ from plugin.registry import registry
try: try:
from django_weasyprint import WeasyTemplateResponseMixin from django_weasyprint import WeasyTemplateResponseMixin
except OSError as err: # pragma: no cover except OSError as err: # pragma: no cover
print(f"OSError: {err}") print(f'OSError: {err}')
print("You may require some further system packages to be installed.") print('You may require some further system packages to be installed.')
sys.exit(1) sys.exit(1)
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
def rename_label(instance, filename): def rename_label(instance, filename):
@ -97,7 +97,7 @@ class LabelTemplate(MetadataMixin, models.Model):
abstract = True abstract = True
# Each class of label files will be stored in a separate subdirectory # Each class of label files will be stored in a separate subdirectory
SUBDIR = "label" SUBDIR = 'label'
# Object we will be printing against (will be filled out later) # Object we will be printing against (will be filled out later)
object_to_print = None object_to_print = None
@ -109,7 +109,7 @@ class LabelTemplate(MetadataMixin, models.Model):
def __str__(self): def __str__(self):
"""Format a string representation of a label instance""" """Format a string representation of a label instance"""
return f"{self.name} - {self.description}" return f'{self.name} - {self.description}'
name = models.CharField( name = models.CharField(
blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Label name') blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Label name')
@ -154,7 +154,7 @@ class LabelTemplate(MetadataMixin, models.Model):
) )
filename_pattern = models.CharField( filename_pattern = models.CharField(
default="label.pdf", default='label.pdf',
verbose_name=_('Filename Pattern'), verbose_name=_('Filename Pattern'),
help_text=_('Pattern for generating label filenames'), help_text=_('Pattern for generating label filenames'),
max_length=100, max_length=100,
@ -265,7 +265,7 @@ class LabelTemplate(MetadataMixin, models.Model):
wp = WeasyprintLabelMixin( wp = WeasyprintLabelMixin(
request, request,
self.template_name, self.template_name,
base_url=request.build_absolute_uri("/"), base_url=request.build_absolute_uri('/'),
presentational_hints=True, presentational_hints=True,
filename=self.generate_filename(request), filename=self.generate_filename(request),
**kwargs, **kwargs,
@ -304,7 +304,7 @@ class StockItemLabel(LabelTemplate):
"""Return the API URL associated with the StockItemLabel model""" """Return the API URL associated with the StockItemLabel model"""
return reverse('api-stockitem-label-list') # pragma: no cover return reverse('api-stockitem-label-list') # pragma: no cover
SUBDIR = "stockitem" SUBDIR = 'stockitem'
filters = models.CharField( filters = models.CharField(
blank=True, blank=True,
@ -343,7 +343,7 @@ class StockLocationLabel(LabelTemplate):
"""Return the API URL associated with the StockLocationLabel model""" """Return the API URL associated with the StockLocationLabel model"""
return reverse('api-stocklocation-label-list') # pragma: no cover return reverse('api-stocklocation-label-list') # pragma: no cover
SUBDIR = "stocklocation" SUBDIR = 'stocklocation'
filters = models.CharField( filters = models.CharField(
blank=True, blank=True,

View File

@ -55,13 +55,13 @@ class LabelTest(InvenTreeAPITestCase):
def test_filters(self): def test_filters(self):
"""Test the label filters.""" """Test the label filters."""
filter_string = "part__pk=10" filter_string = 'part__pk=10'
filters = validateFilterString(filter_string, model=StockItem) filters = validateFilterString(filter_string, model=StockItem)
self.assertEqual(type(filters), dict) self.assertEqual(type(filters), dict)
bad_filter_string = "part_pk=10" bad_filter_string = 'part_pk=10'
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
validateFilterString(bad_filter_string, model=StockItem) validateFilterString(bad_filter_string, model=StockItem)
@ -107,7 +107,7 @@ class LabelTest(InvenTreeAPITestCase):
buffer = io.StringIO() buffer = io.StringIO()
buffer.write(label_data) buffer.write(label_data)
template = ContentFile(buffer.getvalue(), "label.html") template = ContentFile(buffer.getvalue(), 'label.html')
# Construct a label template # Construct a label template
label = PartLabel.objects.create( label = PartLabel.objects.create(
@ -140,7 +140,7 @@ class LabelTest(InvenTreeAPITestCase):
content = f.read() content = f.read()
# Test that each element has been rendered correctly # Test that each element has been rendered correctly
self.assertIn(f"part: {part_pk} - {part_name}", content) self.assertIn(f'part: {part_pk} - {part_name}', content)
self.assertIn(f'data: {{"part": {part_pk}}}', content) self.assertIn(f'data: {{"part": {part_pk}}}', content)
self.assertIn(f'http://testserver/part/{part_pk}/', content) self.assertIn(f'http://testserver/part/{part_pk}/', content)

View File

@ -7,17 +7,17 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "InvenTree.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: # pragma: no cover except ImportError as exc: # pragma: no cover
raise ImportError( raise ImportError(
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you " 'available on your PYTHONPATH environment variable? Did you '
"forget to activate a virtual environment?" 'forget to activate a virtual environment?'
) from exc ) from exc
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == '__main__':
main() main()

View File

@ -86,7 +86,7 @@ class OrderFilter(rest_filters.FilterSet):
"""Base class for custom API filters for the OrderList endpoint.""" """Base class for custom API filters for the OrderList endpoint."""
# Filter against order status # Filter against order status
status = rest_filters.NumberFilter(label="Order Status", method='filter_status') status = rest_filters.NumberFilter(label='Order Status', method='filter_status')
def filter_status(self, queryset, name, value): def filter_status(self, queryset, name, value):
"""Filter by integer status code""" """Filter by integer status code"""
@ -94,7 +94,7 @@ class OrderFilter(rest_filters.FilterSet):
# Exact match for reference # Exact match for reference
reference = rest_filters.CharFilter( reference = rest_filters.CharFilter(
label='Filter by exact reference', field_name='reference', lookup_expr="iexact" label='Filter by exact reference', field_name='reference', lookup_expr='iexact'
) )
assigned_to_me = rest_filters.BooleanFilter( assigned_to_me = rest_filters.BooleanFilter(
@ -155,7 +155,7 @@ class LineItemFilter(rest_filters.FilterSet):
) )
has_pricing = rest_filters.BooleanFilter( has_pricing = rest_filters.BooleanFilter(
label="Has Pricing", method='filter_has_pricing' label='Has Pricing', method='filter_has_pricing'
) )
def filter_has_pricing(self, queryset, name, value): def filter_has_pricing(self, queryset, name, value):
@ -271,7 +271,7 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_PurchaseOrders.{export_format}" filename = f'InvenTree_PurchaseOrders.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -518,7 +518,7 @@ class PurchaseOrderLineItemList(
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_PurchaseOrderItems.{export_format}" filename = f'InvenTree_PurchaseOrderItems.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -567,7 +567,7 @@ class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""Download this queryset as a file""" """Download this queryset as a file"""
dataset = PurchaseOrderExtraLineResource().export(queryset=queryset) dataset = PurchaseOrderExtraLineResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_ExtraPurchaseOrderLines.{export_format}" filename = f'InvenTree_ExtraPurchaseOrderLines.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -665,7 +665,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_SalesOrders.{export_format}" filename = f'InvenTree_SalesOrders.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -809,7 +809,7 @@ class SalesOrderLineItemList(SalesOrderLineItemMixin, APIDownloadMixin, ListCrea
dataset = SalesOrderLineItemResource().export(queryset=queryset) dataset = SalesOrderLineItemResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_SalesOrderItems.{export_format}" filename = f'InvenTree_SalesOrderItems.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -836,7 +836,7 @@ class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""Download this queryset as a file""" """Download this queryset as a file"""
dataset = SalesOrderExtraLineResource().export(queryset=queryset) dataset = SalesOrderExtraLineResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_ExtraSalesOrderLines.{export_format}" filename = f'InvenTree_ExtraSalesOrderLines.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -1127,7 +1127,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
"""Download this queryset as a file""" """Download this queryset as a file"""
dataset = ReturnOrderResource().export(queryset=queryset) dataset = ReturnOrderResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_ReturnOrders.{export_format}" filename = f'InvenTree_ReturnOrders.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -1274,7 +1274,7 @@ class ReturnOrderLineItemList(
def download_queryset(self, queryset, export_format): def download_queryset(self, queryset, export_format):
"""Download the requested queryset as a file""" """Download the requested queryset as a file"""
raise NotImplementedError( raise NotImplementedError(
"download_queryset not yet implemented for this endpoint" 'download_queryset not yet implemented for this endpoint'
) )
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
@ -1303,7 +1303,7 @@ class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
def download_queryset(self, queryset, export_format): def download_queryset(self, queryset, export_format):
"""Download this queryset as a file""" """Download this queryset as a file"""
raise NotImplementedError("download_queryset not yet implemented") raise NotImplementedError('download_queryset not yet implemented')
class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI): class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
@ -1339,9 +1339,9 @@ class OrderCalendarExport(ICalFeed):
instance_url = get_base_url() instance_url = get_base_url()
instance_url = instance_url.replace("http://", "").replace("https://", "") instance_url = instance_url.replace('http://', '').replace('https://', '')
timezone = settings.TIME_ZONE timezone = settings.TIME_ZONE
file_name = "calendar.ics" file_name = 'calendar.ics'
def __call__(self, request, *args, **kwargs): def __call__(self, request, *args, **kwargs):
"""Overload call in order to check for authentication. """Overload call in order to check for authentication.
@ -1367,8 +1367,8 @@ class OrderCalendarExport(ICalFeed):
if len(auth) == 2: if len(auth) == 2:
# NOTE: We are only support basic authentication for now. # NOTE: We are only support basic authentication for now.
# #
if auth[0].lower() == "basic": if auth[0].lower() == 'basic':
uname, passwd = base64.b64decode(auth[1]).decode("ascii").split(':') uname, passwd = base64.b64decode(auth[1]).decode('ascii').split(':')
user = authenticate(username=uname, password=passwd) user = authenticate(username=uname, password=passwd)
if user is not None: if user is not None:
if user.is_active: if user.is_active:
@ -1383,7 +1383,7 @@ class OrderCalendarExport(ICalFeed):
# Still nothing - return Unauth. header with info on how to authenticate # Still nothing - return Unauth. header with info on how to authenticate
# Information is needed by client, eg Thunderbird # Information is needed by client, eg Thunderbird
response = JsonResponse({ response = JsonResponse({
"detail": "Authentication credentials were not provided." 'detail': 'Authentication credentials were not provided.'
}) })
response['WWW-Authenticate'] = 'Basic realm="api"' response['WWW-Authenticate'] = 'Basic realm="api"'
response.status_code = 401 response.status_code = 401
@ -1402,11 +1402,11 @@ class OrderCalendarExport(ICalFeed):
def title(self, obj): def title(self, obj):
"""Return calendar title.""" """Return calendar title."""
if obj["ordertype"] == 'purchase-order': if obj['ordertype'] == 'purchase-order':
ordertype_title = _('Purchase Order') ordertype_title = _('Purchase Order')
elif obj["ordertype"] == 'sales-order': elif obj['ordertype'] == 'sales-order':
ordertype_title = _('Sales Order') ordertype_title = _('Sales Order')
elif obj["ordertype"] == 'return-order': elif obj['ordertype'] == 'return-order':
ordertype_title = _('Return Order') ordertype_title = _('Return Order')
else: else:
ordertype_title = _('Unknown') ordertype_title = _('Unknown')
@ -1433,7 +1433,7 @@ class OrderCalendarExport(ICalFeed):
).filter(status__lt=PurchaseOrderStatus.COMPLETE.value) ).filter(status__lt=PurchaseOrderStatus.COMPLETE.value)
else: else:
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False) outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
elif obj["ordertype"] == 'sales-order': elif obj['ordertype'] == 'sales-order':
if obj['include_completed'] is False: if obj['include_completed'] is False:
# Do not include completed (=shipped) orders from list in this case # Do not include completed (=shipped) orders from list in this case
# Shipped status = 20 # Shipped status = 20
@ -1442,7 +1442,7 @@ class OrderCalendarExport(ICalFeed):
).filter(status__lt=SalesOrderStatus.SHIPPED.value) ).filter(status__lt=SalesOrderStatus.SHIPPED.value)
else: else:
outlist = models.SalesOrder.objects.filter(target_date__isnull=False) outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
elif obj["ordertype"] == 'return-order': elif obj['ordertype'] == 'return-order':
if obj['include_completed'] is False: if obj['include_completed'] is False:
# Do not include completed orders from list in this case # Do not include completed orders from list in this case
# Complete status = 30 # Complete status = 30
@ -1458,11 +1458,11 @@ class OrderCalendarExport(ICalFeed):
def item_title(self, item): def item_title(self, item):
"""Set the event title to the order reference""" """Set the event title to the order reference"""
return f"{item.reference}" return f'{item.reference}'
def item_description(self, item): def item_description(self, item):
"""Set the event description""" """Set the event description"""
return f"Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}" return f'Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}'
def item_start_datetime(self, item): def item_start_datetime(self, item):
"""Set event start to target date. Goal is all-day event.""" """Set event start to target date. Goal is all-day event."""

View File

@ -224,7 +224,7 @@ class Order(
if self.company and self.contact: if self.company and self.contact:
if self.contact.company != self.company: if self.contact.company != self.company:
raise ValidationError({ raise ValidationError({
"contact": _("Contact does not match selected company") 'contact': _('Contact does not match selected company')
}) })
@classmethod @classmethod
@ -327,7 +327,7 @@ class Order(
@classmethod @classmethod
def get_status_class(cls): def get_status_class(cls):
"""Return the enumeration class which represents the 'status' field for this model""" """Return the enumeration class which represents the 'status' field for this model"""
raise NotImplementedError(f"get_status_class() not implemented for {__class__}") raise NotImplementedError(f'get_status_class() not implemented for {__class__}')
class PurchaseOrder(TotalPriceMixin, Order): class PurchaseOrder(TotalPriceMixin, Order):
@ -454,7 +454,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
max_length=64, max_length=64,
blank=True, blank=True,
verbose_name=_('Supplier Reference'), verbose_name=_('Supplier Reference'),
help_text=_("Supplier order reference code"), help_text=_('Supplier order reference code'),
) )
received_by = models.ForeignKey( received_by = models.ForeignKey(
@ -514,14 +514,14 @@ class PurchaseOrder(TotalPriceMixin, Order):
quantity = int(quantity) quantity = int(quantity)
if quantity <= 0: if quantity <= 0:
raise ValidationError({ raise ValidationError({
'quantity': _("Quantity must be greater than zero") 'quantity': _('Quantity must be greater than zero')
}) })
except ValueError: except ValueError:
raise ValidationError({'quantity': _("Invalid quantity provided")}) raise ValidationError({'quantity': _('Invalid quantity provided')})
if supplier_part.supplier != self.supplier: if supplier_part.supplier != self.supplier:
raise ValidationError({ raise ValidationError({
'supplier': _("Part supplier must match PO supplier") 'supplier': _('Part supplier must match PO supplier')
}) })
if group: if group:
@ -715,11 +715,11 @@ class PurchaseOrder(TotalPriceMixin, Order):
try: try:
if quantity < 0: if quantity < 0:
raise ValidationError({ raise ValidationError({
"quantity": _("Quantity must be a positive number") 'quantity': _('Quantity must be a positive number')
}) })
quantity = InvenTree.helpers.clean_decimal(quantity) quantity = InvenTree.helpers.clean_decimal(quantity)
except TypeError: except TypeError:
raise ValidationError({"quantity": _("Invalid quantity provided")}) raise ValidationError({'quantity': _('Invalid quantity provided')})
# Create a new stock item # Create a new stock item
if line.part and quantity > 0: if line.part and quantity > 0:
@ -882,7 +882,7 @@ class SalesOrder(TotalPriceMixin, Order):
limit_choices_to={'is_customer': True}, limit_choices_to={'is_customer': True},
related_name='return_orders', related_name='return_orders',
verbose_name=_('Customer'), verbose_name=_('Customer'),
help_text=_("Company to which the items are being sold"), help_text=_('Company to which the items are being sold'),
) )
@property @property
@ -906,7 +906,7 @@ class SalesOrder(TotalPriceMixin, Order):
max_length=64, max_length=64,
blank=True, blank=True,
verbose_name=_('Customer Reference '), verbose_name=_('Customer Reference '),
help_text=_("Customer order reference code"), help_text=_('Customer order reference code'),
) )
shipment_date = models.DateField( shipment_date = models.DateField(
@ -979,12 +979,12 @@ class SalesOrder(TotalPriceMixin, Order):
elif self.pending_shipment_count > 0: elif self.pending_shipment_count > 0:
raise ValidationError( raise ValidationError(
_("Order cannot be completed as there are incomplete shipments") _('Order cannot be completed as there are incomplete shipments')
) )
elif not allow_incomplete_lines and self.pending_line_count > 0: elif not allow_incomplete_lines and self.pending_line_count > 0:
raise ValidationError( raise ValidationError(
_("Order cannot be completed as there are incomplete line items") _('Order cannot be completed as there are incomplete line items')
) )
except ValidationError as e: except ValidationError as e:
@ -1174,10 +1174,10 @@ class PurchaseOrderAttachment(InvenTreeAttachment):
def getSubdir(self): def getSubdir(self):
"""Return the directory path where PurchaseOrderAttachment files are located""" """Return the directory path where PurchaseOrderAttachment files are located"""
return os.path.join("po_files", str(self.order.id)) return os.path.join('po_files', str(self.order.id))
order = models.ForeignKey( order = models.ForeignKey(
PurchaseOrder, on_delete=models.CASCADE, related_name="attachments" PurchaseOrder, on_delete=models.CASCADE, related_name='attachments'
) )
@ -1191,7 +1191,7 @@ class SalesOrderAttachment(InvenTreeAttachment):
def getSubdir(self): def getSubdir(self):
"""Return the directory path where SalesOrderAttachment files are located""" """Return the directory path where SalesOrderAttachment files are located"""
return os.path.join("so_files", str(self.order.id)) return os.path.join('so_files', str(self.order.id))
order = models.ForeignKey( order = models.ForeignKey(
SalesOrder, on_delete=models.CASCADE, related_name='attachments' SalesOrder, on_delete=models.CASCADE, related_name='attachments'
@ -1342,7 +1342,7 @@ class PurchaseOrderLineItem(OrderLineItem):
def __str__(self): def __str__(self):
"""Render a string representation of a PurchaseOrderLineItem instance""" """Render a string representation of a PurchaseOrderLineItem instance"""
return "{n} x {part} from {supplier} (for {po})".format( return '{n} x {part} from {supplier} (for {po})'.format(
n=decimal2string(self.quantity), n=decimal2string(self.quantity),
part=self.part.SKU if self.part else 'unknown part', part=self.part.SKU if self.part else 'unknown part',
supplier=self.order.supplier.name if self.order.supplier else _('deleted'), supplier=self.order.supplier.name if self.order.supplier else _('deleted'),
@ -1373,7 +1373,7 @@ class PurchaseOrderLineItem(OrderLineItem):
null=True, null=True,
related_name='purchase_order_line_items', related_name='purchase_order_line_items',
verbose_name=_('Part'), verbose_name=_('Part'),
help_text=_("Supplier part"), help_text=_('Supplier part'),
) )
received = models.DecimalField( received = models.DecimalField(
@ -1483,12 +1483,12 @@ class SalesOrderLineItem(OrderLineItem):
if self.part: if self.part:
if self.part.virtual: if self.part.virtual:
raise ValidationError({ raise ValidationError({
'part': _("Virtual part cannot be assigned to a sales order") 'part': _('Virtual part cannot be assigned to a sales order')
}) })
if not self.part.salable: if not self.part.salable:
raise ValidationError({ raise ValidationError({
'part': _("Only salable parts can be assigned to a sales order") 'part': _('Only salable parts can be assigned to a sales order')
}) })
order = models.ForeignKey( order = models.ForeignKey(
@ -1668,10 +1668,10 @@ class SalesOrderShipment(InvenTreeNotesMixin, MetadataMixin, models.Model):
try: try:
if self.shipment_date: if self.shipment_date:
# Shipment has already been sent! # Shipment has already been sent!
raise ValidationError(_("Shipment has already been sent")) raise ValidationError(_('Shipment has already been sent'))
if self.allocations.count() == 0: if self.allocations.count() == 0:
raise ValidationError(_("Shipment has no allocated stock items")) raise ValidationError(_('Shipment has no allocated stock items'))
except ValidationError as e: except ValidationError as e:
if raise_error: if raise_error:
@ -1807,7 +1807,7 @@ class SalesOrderAllocation(models.Model):
# Ensure that we do not 'over allocate' a stock item # Ensure that we do not 'over allocate' a stock item
build_allocation_count = self.item.build_allocation_count() build_allocation_count = self.item.build_allocation_count()
sales_allocation_count = self.item.sales_order_allocation_count( sales_allocation_count = self.item.sales_order_allocation_count(
exclude_allocations={"pk": self.pk} exclude_allocations={'pk': self.pk}
) )
total_allocation = ( total_allocation = (
@ -1954,7 +1954,7 @@ class ReturnOrder(TotalPriceMixin, Order):
limit_choices_to={'is_customer': True}, limit_choices_to={'is_customer': True},
related_name='sales_orders', related_name='sales_orders',
verbose_name=_('Customer'), verbose_name=_('Customer'),
help_text=_("Company from which items are being returned"), help_text=_('Company from which items are being returned'),
) )
@property @property
@ -1973,7 +1973,7 @@ class ReturnOrder(TotalPriceMixin, Order):
max_length=64, max_length=64,
blank=True, blank=True,
verbose_name=_('Customer Reference '), verbose_name=_('Customer Reference '),
help_text=_("Customer order reference code"), help_text=_('Customer order reference code'),
) )
issue_date = models.DateField( issue_date = models.DateField(
@ -2078,7 +2078,7 @@ class ReturnOrder(TotalPriceMixin, Order):
""" """
# Prevent an item from being "received" multiple times # Prevent an item from being "received" multiple times
if line.received_date is not None: if line.received_date is not None:
logger.warning("receive_line_item called with item already returned") logger.warning('receive_line_item called with item already returned')
return return
stock_item = line.item stock_item = line.item
@ -2144,7 +2144,7 @@ class ReturnOrderLineItem(OrderLineItem):
if self.item and not self.item.serialized: if self.item and not self.item.serialized:
raise ValidationError({ raise ValidationError({
'item': _("Only serialized items can be assigned to a Return Order") 'item': _('Only serialized items can be assigned to a Return Order')
}) })
order = models.ForeignKey( order = models.ForeignKey(

View File

@ -261,7 +261,7 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
order = self.context['order'] order = self.context['order']
if not order.can_cancel: if not order.can_cancel:
raise ValidationError(_("Order cannot be cancelled")) raise ValidationError(_('Order cannot be cancelled'))
order.cancel_order() order.cancel_order()
@ -286,7 +286,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
order = self.context['order'] order = self.context['order']
if not value and not order.is_complete: if not value and not order.is_complete:
raise ValidationError(_("Order has incomplete line items")) raise ValidationError(_('Order has incomplete line items'))
return value return value
@ -390,7 +390,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
def validate_quantity(self, quantity): def validate_quantity(self, quantity):
"""Validation for the 'quantity' field""" """Validation for the 'quantity' field"""
if quantity <= 0: if quantity <= 0:
raise ValidationError(_("Quantity must be greater than zero")) raise ValidationError(_('Quantity must be greater than zero'))
return quantity return quantity
@ -517,7 +517,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
def validate_quantity(self, quantity): def validate_quantity(self, quantity):
"""Validation for the 'quantity' field""" """Validation for the 'quantity' field"""
if quantity <= 0: if quantity <= 0:
raise ValidationError(_("Quantity must be greater than zero")) raise ValidationError(_('Quantity must be greater than zero'))
return quantity return quantity
@ -647,7 +647,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
if not item['location']: if not item['location']:
raise ValidationError({ raise ValidationError({
'location': _("Destination location must be specified") 'location': _('Destination location must be specified')
}) })
# Ensure barcodes are unique # Ensure barcodes are unique
@ -1075,7 +1075,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
shipment = self.context.get('shipment', None) shipment = self.context.get('shipment', None)
if not shipment: if not shipment:
raise ValidationError(_("No shipment details provided")) raise ValidationError(_('No shipment details provided'))
shipment.check_can_complete(raise_error=True) shipment.check_can_complete(raise_error=True)
@ -1135,7 +1135,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
# Ensure that the line item points to the correct order # Ensure that the line item points to the correct order
if line_item.order != order: if line_item.order != order:
raise ValidationError(_("Line item is not associated with this order")) raise ValidationError(_('Line item is not associated with this order'))
return line_item return line_item
@ -1154,7 +1154,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
def validate_quantity(self, quantity): def validate_quantity(self, quantity):
"""Custom validation for the 'quantity' field""" """Custom validation for the 'quantity' field"""
if quantity <= 0: if quantity <= 0:
raise ValidationError(_("Quantity must be positive")) raise ValidationError(_('Quantity must be positive'))
return quantity return quantity
@ -1171,13 +1171,13 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
if stock_item.serialized and quantity != 1: if stock_item.serialized and quantity != 1:
raise ValidationError({ raise ValidationError({
'quantity': _("Quantity must be 1 for serialized stock item") 'quantity': _('Quantity must be 1 for serialized stock item')
}) })
q = normalize(stock_item.unallocated_quantity()) q = normalize(stock_item.unallocated_quantity())
if quantity > q: if quantity > q:
raise ValidationError({'quantity': _(f"Available quantity ({q}) exceeded")}) raise ValidationError({'quantity': _(f'Available quantity ({q}) exceeded')})
return data return data
@ -1197,7 +1197,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
order = self.context['order'] order = self.context['order']
if not value and not order.is_completed(): if not value and not order.is_completed():
raise ValidationError(_("Order has incomplete line items")) raise ValidationError(_('Order has incomplete line items'))
return value return value
@ -1274,7 +1274,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
# Ensure that the line item points to the correct order # Ensure that the line item points to the correct order
if line_item.order != order: if line_item.order != order:
raise ValidationError(_("Line item is not associated with this order")) raise ValidationError(_('Line item is not associated with this order'))
return line_item return line_item
@ -1283,8 +1283,8 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
) )
serial_numbers = serializers.CharField( serial_numbers = serializers.CharField(
label=_("Serial Numbers"), label=_('Serial Numbers'),
help_text=_("Enter serial numbers to allocate"), help_text=_('Enter serial numbers to allocate'),
required=True, required=True,
allow_blank=False, allow_blank=False,
) )
@ -1306,10 +1306,10 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
order = self.context['order'] order = self.context['order']
if shipment.shipment_date is not None: if shipment.shipment_date is not None:
raise ValidationError(_("Shipment has already been shipped")) raise ValidationError(_('Shipment has already been shipped'))
if shipment.order != order: if shipment.order != order:
raise ValidationError(_("Shipment is not associated with this order")) raise ValidationError(_('Shipment is not associated with this order'))
return shipment return shipment
@ -1356,16 +1356,16 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
serials_allocated.append(str(serial)) serials_allocated.append(str(serial))
if len(serials_not_exist) > 0: if len(serials_not_exist) > 0:
error_msg = _("No match found for the following serial numbers") error_msg = _('No match found for the following serial numbers')
error_msg += ": " error_msg += ': '
error_msg += ",".join(serials_not_exist) error_msg += ','.join(serials_not_exist)
raise ValidationError({'serial_numbers': error_msg}) raise ValidationError({'serial_numbers': error_msg})
if len(serials_allocated) > 0: if len(serials_allocated) > 0:
error_msg = _("The following serial numbers are already allocated") error_msg = _('The following serial numbers are already allocated')
error_msg += ": " error_msg += ': '
error_msg += ",".join(serials_allocated) error_msg += ','.join(serials_allocated)
raise ValidationError({'serial_numbers': error_msg}) raise ValidationError({'serial_numbers': error_msg})
@ -1412,10 +1412,10 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
order = self.context['order'] order = self.context['order']
if shipment.shipment_date is not None: if shipment.shipment_date is not None:
raise ValidationError(_("Shipment has already been shipped")) raise ValidationError(_('Shipment has already been shipped'))
if shipment.order != order: if shipment.order != order:
raise ValidationError(_("Shipment is not associated with this order")) raise ValidationError(_('Shipment is not associated with this order'))
return shipment return shipment
@ -1596,10 +1596,10 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
def validate_line_item(self, item): def validate_line_item(self, item):
"""Validation for a single line item""" """Validation for a single line item"""
if item.order != self.context['order']: if item.order != self.context['order']:
raise ValidationError(_("Line item does not match return order")) raise ValidationError(_('Line item does not match return order'))
if item.received: if item.received:
raise ValidationError(_("Line item has already been received")) raise ValidationError(_('Line item has already been received'))
return item return item
@ -1628,7 +1628,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
order = self.context['order'] order = self.context['order']
if order.status != ReturnOrderStatus.IN_PROGRESS: if order.status != ReturnOrderStatus.IN_PROGRESS:
raise ValidationError( raise ValidationError(
_("Items can only be received against orders which are in progress") _('Items can only be received against orders which are in progress')
) )
data = super().validate(data) data = super().validate(data)
@ -1636,7 +1636,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
items = data.get('items', []) items = data.get('items', [])
if len(items) == 0: if len(items) == 0:
raise ValidationError(_("Line items must be provided")) raise ValidationError(_('Line items must be provided'))
return data return data

View File

@ -76,7 +76,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
context = { context = {
'order': so, 'order': so,
'name': name, 'name': name,
'message': _(f"Sales order {so} is now overdue"), 'message': _(f'Sales order {so} is now overdue'),
'link': InvenTree.helpers_model.construct_absolute_url(so.get_absolute_url()), 'link': InvenTree.helpers_model.construct_absolute_url(so.get_absolute_url()),
'template': {'html': 'email/overdue_sales_order.html', 'subject': name}, 'template': {'html': 'email/overdue_sales_order.html', 'subject': name},
} }

View File

@ -237,7 +237,7 @@ class PurchaseOrderTest(OrderTest):
self.assignRole('purchase_order.add') self.assignRole('purchase_order.add')
url = reverse('api-po-list') url = reverse('api-po-list')
huge_number = "PO-92233720368547758089999999999999999" huge_number = 'PO-92233720368547758089999999999999999'
response = self.post( response = self.post(
url, url,
@ -333,7 +333,7 @@ class PurchaseOrderTest(OrderTest):
response = self.delete(url, expected_code=403) response = self.delete(url, expected_code=403)
# Now, add the "delete" permission! # Now, add the "delete" permission!
self.assignRole("purchase_order.delete") self.assignRole('purchase_order.delete')
response = self.delete(url, expected_code=204) response = self.delete(url, expected_code=204)
@ -589,7 +589,7 @@ class PurchaseOrderTest(OrderTest):
resp_dict = response.json() resp_dict = response.json()
self.assertEqual( self.assertEqual(
resp_dict['detail'], "Authentication credentials were not provided." resp_dict['detail'], 'Authentication credentials were not provided.'
) )
def test_po_calendar_auth(self): def test_po_calendar_auth(self):
@ -731,7 +731,7 @@ class PurchaseOrderReceiveTest(OrderTest):
def test_no_items(self): def test_no_items(self):
"""Test with an empty list of items.""" """Test with an empty list of items."""
data = self.post( data = self.post(
self.url, {"items": [], "location": None}, expected_code=400 self.url, {'items': [], 'location': None}, expected_code=400
).data ).data
self.assertIn('Line items must be provided', str(data)) self.assertIn('Line items must be provided', str(data))
@ -743,14 +743,14 @@ class PurchaseOrderReceiveTest(OrderTest):
"""Test than errors are returned as expected for invalid data.""" """Test than errors are returned as expected for invalid data."""
data = self.post( data = self.post(
self.url, self.url,
{"items": [{"line_item": 12345, "location": 12345}]}, {'items': [{'line_item': 12345, 'location': 12345}]},
expected_code=400, expected_code=400,
).data ).data
items = data['items'][0] items = data['items'][0]
self.assertIn('Invalid pk "12345"', str(items['line_item'])) self.assertIn('Invalid pk "12345"', str(items['line_item']))
self.assertIn("object does not exist", str(items['location'])) self.assertIn('object does not exist', str(items['location']))
# No new stock items have been created # No new stock items have been created
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
@ -760,8 +760,8 @@ class PurchaseOrderReceiveTest(OrderTest):
data = self.post( data = self.post(
self.url, self.url,
{ {
"items": [ 'items': [
{"line_item": 22, "location": 1, "status": 99999, "quantity": 5} {'line_item': 22, 'location': 1, 'status': 99999, 'quantity': 5}
] ]
}, },
expected_code=400, expected_code=400,
@ -1330,7 +1330,7 @@ class SalesOrderTest(OrderTest):
{'export': fmt}, {'export': fmt},
decode=True if fmt == 'csv' else False, decode=True if fmt == 'csv' else False,
expected_code=200, expected_code=200,
expected_fn=f"InvenTree_SalesOrders.{fmt}", expected_fn=f'InvenTree_SalesOrders.{fmt}',
) )
@ -1357,7 +1357,7 @@ class SalesOrderLineItemTest(OrderTest):
order=so, order=so,
part=part, part=part,
quantity=(idx + 1) * 5, quantity=(idx + 1) * 5,
reference=f"Order {so.reference} - line {idx}", reference=f'Order {so.reference} - line {idx}',
) )
) )
@ -1376,7 +1376,7 @@ class SalesOrderLineItemTest(OrderTest):
self.assertEqual(len(response.data), n) self.assertEqual(len(response.data), n)
# List *all* lines, but paginate # List *all* lines, but paginate
response = self.get(self.url, {"limit": 5}, expected_code=200) response = self.get(self.url, {'limit': 5}, expected_code=200)
self.assertEqual(response.data['count'], n) self.assertEqual(response.data['count'], n)
self.assertEqual(len(response.data['results']), 5) self.assertEqual(len(response.data['results']), 5)
@ -1530,9 +1530,9 @@ class SalesOrderAllocateTest(OrderTest):
data = { data = {
'items': [ 'items': [
{ {
"line_item": line.pk, 'line_item': line.pk,
"stock_item": part.stock_items.last().pk, 'stock_item': part.stock_items.last().pk,
"quantity": 0, 'quantity': 0,
} }
] ]
} }
@ -1576,16 +1576,16 @@ class SalesOrderAllocateTest(OrderTest):
# First, check that there are no line items allocated against this SalesOrder # First, check that there are no line items allocated against this SalesOrder
self.assertEqual(self.order.stock_allocations.count(), 0) self.assertEqual(self.order.stock_allocations.count(), 0)
data = {"items": [], "shipment": self.shipment.pk} data = {'items': [], 'shipment': self.shipment.pk}
for line in self.order.lines.all(): for line in self.order.lines.all():
stock_item = line.part.stock_items.last() stock_item = line.part.stock_items.last()
# Fully-allocate each line # Fully-allocate each line
data['items'].append({ data['items'].append({
"line_item": line.pk, 'line_item': line.pk,
"stock_item": stock_item.pk, 'stock_item': stock_item.pk,
"quantity": 5, 'quantity': 5,
}) })
self.post(self.url, data, expected_code=201) self.post(self.url, data, expected_code=201)
@ -1603,7 +1603,7 @@ class SalesOrderAllocateTest(OrderTest):
# First, check that there are no line items allocated against this SalesOrder # First, check that there are no line items allocated against this SalesOrder
self.assertEqual(self.order.stock_allocations.count(), 0) self.assertEqual(self.order.stock_allocations.count(), 0)
data = {"items": [], "shipment": self.shipment.pk} data = {'items': [], 'shipment': self.shipment.pk}
def check_template(line_item): def check_template(line_item):
return line_item.part.is_template return line_item.part.is_template
@ -1619,9 +1619,9 @@ class SalesOrderAllocateTest(OrderTest):
# Fully-allocate each line # Fully-allocate each line
data['items'].append({ data['items'].append({
"line_item": line.pk, 'line_item': line.pk,
"stock_item": stock_item.pk, 'stock_item': stock_item.pk,
"quantity": 5, 'quantity': 5,
}) })
self.post(self.url, data, expected_code=201) self.post(self.url, data, expected_code=201)
@ -1719,8 +1719,8 @@ class SalesOrderAllocateTest(OrderTest):
url, url,
{ {
'order': order.pk, 'order': order.pk,
'reference': f"SH{idx + 1}", 'reference': f'SH{idx + 1}',
'tracking_number': f"TRK_{order.pk}_{idx}", 'tracking_number': f'TRK_{order.pk}_{idx}',
}, },
expected_code=201, expected_code=201,
) )
@ -1932,7 +1932,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
# Issue the order (via the API) # Issue the order (via the API)
self.assertIsNone(rma.issue_date) self.assertIsNone(rma.issue_date)
self.post( self.post(
reverse("api-return-order-issue", kwargs={"pk": rma.pk}), expected_code=201 reverse('api-return-order-issue', kwargs={'pk': rma.pk}), expected_code=201
) )
rma.refresh_from_db() rma.refresh_from_db()

View File

@ -30,8 +30,8 @@ class TestRefIntMigrations(MigratorTestCase):
for ii in range(10): for ii in range(10):
order = PurchaseOrder.objects.create( order = PurchaseOrder.objects.create(
supplier=supplier, supplier=supplier,
reference=f"{ii}-abcde", reference=f'{ii}-abcde',
description="Just a test order", description='Just a test order',
) )
# Initially, the 'reference_int' field is unavailable # Initially, the 'reference_int' field is unavailable
@ -40,8 +40,8 @@ class TestRefIntMigrations(MigratorTestCase):
sales_order = SalesOrder.objects.create( sales_order = SalesOrder.objects.create(
customer=supplier, customer=supplier,
reference=f"{ii}-xyz", reference=f'{ii}-xyz',
description="A test sales order", description='A test sales order',
) )
# Initially, the 'reference_int' field is unavailable # Initially, the 'reference_int' field is unavailable
@ -67,8 +67,8 @@ class TestRefIntMigrations(MigratorTestCase):
SalesOrder = self.new_state.apps.get_model('order', 'salesorder') SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
for ii in range(10): for ii in range(10):
po = PurchaseOrder.objects.get(reference=f"{ii}-abcde") po = PurchaseOrder.objects.get(reference=f'{ii}-abcde')
so = SalesOrder.objects.get(reference=f"{ii}-xyz") so = SalesOrder.objects.get(reference=f'{ii}-xyz')
# The integer reference field must have been correctly updated # The integer reference field must have been correctly updated
self.assertEqual(po.reference_int, ii) self.assertEqual(po.reference_int, ii)
@ -166,8 +166,8 @@ class TestAdditionalLineMigration(MigratorTestCase):
for ii in range(10): for ii in range(10):
order = PurchaseOrder.objects.create( order = PurchaseOrder.objects.create(
supplier=supplier, supplier=supplier,
reference=f"{ii}-abcde", reference=f'{ii}-abcde',
description="Just a test order", description='Just a test order',
) )
order.lines.create(part=supplierpart, quantity=12, received=1) order.lines.create(part=supplierpart, quantity=12, received=1)
order.lines.create(quantity=12, received=1) order.lines.create(quantity=12, received=1)
@ -188,7 +188,7 @@ class TestAdditionalLineMigration(MigratorTestCase):
"""Test that the the PO lines where converted correctly.""" """Test that the the PO lines where converted correctly."""
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder') PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
for ii in range(10): for ii in range(10):
po = PurchaseOrder.objects.get(reference=f"{ii}-abcde") po = PurchaseOrder.objects.get(reference=f'{ii}-abcde')
self.assertEqual(po.extra_lines.count(), 1) self.assertEqual(po.extra_lines.count(), 1)
self.assertEqual(po.lines.count(), 1) self.assertEqual(po.lines.count(), 1)

View File

@ -33,7 +33,7 @@ class SalesOrderTest(TestCase):
"""Initial setup for this set of unit tests""" """Initial setup for this set of unit tests"""
# Create a Company to ship the goods to # Create a Company to ship the goods to
cls.customer = Company.objects.create( cls.customer = Company.objects.create(
name="ABC Co", description="My customer", is_customer=True name='ABC Co', description='My customer', is_customer=True
) )
# Create a Part to ship # Create a Part to ship
@ -72,7 +72,7 @@ class SalesOrderTest(TestCase):
# Create an extra line # Create an extra line
cls.extraline = SalesOrderExtraLine.objects.create( cls.extraline = SalesOrderExtraLine.objects.create(
quantity=1, order=cls.order, reference="Extra line" quantity=1, order=cls.order, reference='Extra line'
) )
def test_so_reference(self): def test_so_reference(self):

View File

@ -46,7 +46,7 @@ class OrderTest(TestCase):
self.assertEqual(order.reference, f'PO-{pk:04d}') self.assertEqual(order.reference, f'PO-{pk:04d}')
line = PurchaseOrderLineItem.objects.get(pk=1) line = PurchaseOrderLineItem.objects.get(pk=1)
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO-0001 - ACME)") self.assertEqual(str(line), '100 x ACME0001 from ACME (for PO-0001 - ACME)')
def test_rebuild_reference(self): def test_rebuild_reference(self):
"""Test that the reference_int field is correctly updated when the model is saved""" """Test that the reference_int field is correctly updated when the model is saved"""
@ -236,7 +236,7 @@ class OrderTest(TestCase):
# Create a new PurchaseOrder # Create a new PurchaseOrder
po = PurchaseOrder.objects.create( po = PurchaseOrder.objects.create(
supplier=sup, reference=f"PO-{n + 1}", description='Some PO' supplier=sup, reference=f'PO-{n + 1}', description='Some PO'
) )
# Add line items # Add line items

View File

@ -32,7 +32,7 @@ from .models import (
SalesOrderLineItem, SalesOrderLineItem,
) )
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView): class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
@ -115,9 +115,9 @@ class PurchaseOrderUpload(FileManagementFormView):
'order/order_wizard/match_parts.html', 'order/order_wizard/match_parts.html',
] ]
form_steps_description = [ form_steps_description = [
_("Upload File"), _('Upload File'),
_("Match Fields"), _('Match Fields'),
_("Match Supplier Parts"), _('Match Supplier Parts'),
] ]
form_field_map = { form_field_map = {
'item_select': 'part', 'item_select': 'part',
@ -294,7 +294,7 @@ class SalesOrderExport(AjaxView):
export_format = request.GET.get('format', 'csv') export_format = request.GET.get('format', 'csv')
filename = f"{str(order)} - {order.customer.name}.{export_format}" filename = f'{str(order)} - {order.customer.name}.{export_format}'
dataset = SalesOrderLineItemResource().export(queryset=order.lines.all()) dataset = SalesOrderLineItemResource().export(queryset=order.lines.all())

View File

@ -114,7 +114,7 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
"""Download the filtered queryset as a data file""" """Download the filtered queryset as a data file"""
dataset = PartCategoryResource().export(queryset=queryset) dataset = PartCategoryResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_Categories.{export_format}" filename = f'InvenTree_Categories.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -682,23 +682,23 @@ class PartRequirements(RetrieveAPI):
part = self.get_object() part = self.get_object()
data = { data = {
"available_stock": part.available_stock, 'available_stock': part.available_stock,
"on_order": part.on_order, 'on_order': part.on_order,
"required_build_order_quantity": part.required_build_order_quantity(), 'required_build_order_quantity': part.required_build_order_quantity(),
"allocated_build_order_quantity": part.build_order_allocation_count(), 'allocated_build_order_quantity': part.build_order_allocation_count(),
"required_sales_order_quantity": part.required_sales_order_quantity(), 'required_sales_order_quantity': part.required_sales_order_quantity(),
"allocated_sales_order_quantity": part.sales_order_allocation_count( 'allocated_sales_order_quantity': part.sales_order_allocation_count(
pending=True pending=True
), ),
} }
data["allocated"] = ( data['allocated'] = (
data["allocated_build_order_quantity"] data['allocated_build_order_quantity']
+ data["allocated_sales_order_quantity"] + data['allocated_sales_order_quantity']
) )
data["required"] = ( data['required'] = (
data["required_build_order_quantity"] data['required_build_order_quantity']
+ data["required_sales_order_quantity"] + data['required_sales_order_quantity']
) )
return Response(data) return Response(data)
@ -850,7 +850,7 @@ class PartFilter(rest_filters.FilterSet):
IPN = rest_filters.CharFilter( IPN = rest_filters.CharFilter(
label='Filter by exact IPN (internal part number)', label='Filter by exact IPN (internal part number)',
field_name='IPN', field_name='IPN',
lookup_expr="iexact", lookup_expr='iexact',
) )
# Regex match for IPN # Regex match for IPN
@ -895,7 +895,7 @@ class PartFilter(rest_filters.FilterSet):
return queryset.filter(Q(unallocated_stock__lte=0)) return queryset.filter(Q(unallocated_stock__lte=0))
convert_from = rest_filters.ModelChoiceFilter( convert_from = rest_filters.ModelChoiceFilter(
label="Can convert from", label='Can convert from',
queryset=Part.objects.all(), queryset=Part.objects.all(),
method='filter_convert_from', method='filter_convert_from',
) )
@ -909,7 +909,7 @@ class PartFilter(rest_filters.FilterSet):
return queryset return queryset
exclude_tree = rest_filters.ModelChoiceFilter( exclude_tree = rest_filters.ModelChoiceFilter(
label="Exclude Part tree", label='Exclude Part tree',
queryset=Part.objects.all(), queryset=Part.objects.all(),
method='filter_exclude_tree', method='filter_exclude_tree',
) )
@ -947,7 +947,7 @@ class PartFilter(rest_filters.FilterSet):
return queryset.filter(id__in=[p.pk for p in bom_parts]) return queryset.filter(id__in=[p.pk for p in bom_parts])
has_pricing = rest_filters.BooleanFilter( has_pricing = rest_filters.BooleanFilter(
label="Has Pricing", method="filter_has_pricing" label='Has Pricing', method='filter_has_pricing'
) )
def filter_has_pricing(self, queryset, name, value): def filter_has_pricing(self, queryset, name, value):
@ -961,7 +961,7 @@ class PartFilter(rest_filters.FilterSet):
return queryset.filter(q_a | q_b).distinct() return queryset.filter(q_a | q_b).distinct()
stocktake = rest_filters.BooleanFilter( stocktake = rest_filters.BooleanFilter(
label="Has stocktake", method='filter_has_stocktake' label='Has stocktake', method='filter_has_stocktake'
) )
def filter_has_stocktake(self, queryset, name, value): def filter_has_stocktake(self, queryset, name, value):
@ -997,7 +997,7 @@ class PartFilter(rest_filters.FilterSet):
return queryset.exclude(Q(in_stock=0) & ~Q(stock_item_count=0)) return queryset.exclude(Q(in_stock=0) & ~Q(stock_item_count=0))
default_location = rest_filters.ModelChoiceFilter( default_location = rest_filters.ModelChoiceFilter(
label="Default Location", queryset=StockLocation.objects.all() label='Default Location', queryset=StockLocation.objects.all()
) )
is_template = rest_filters.BooleanFilter() is_template = rest_filters.BooleanFilter()
@ -1095,7 +1095,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
dataset = PartResource().export(queryset=queryset) dataset = PartResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_Parts.{export_format}" filename = f'InvenTree_Parts.{export_format}'
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
@ -1668,7 +1668,7 @@ class BomFilter(rest_filters.FilterSet):
) )
available_stock = rest_filters.BooleanFilter( available_stock = rest_filters.BooleanFilter(
label="Has available stock", method="filter_available_stock" label='Has available stock', method='filter_available_stock'
) )
def filter_available_stock(self, queryset, name, value): def filter_available_stock(self, queryset, name, value):
@ -1677,7 +1677,7 @@ class BomFilter(rest_filters.FilterSet):
return queryset.filter(available_stock__gt=0) return queryset.filter(available_stock__gt=0)
return queryset.filter(available_stock=0) return queryset.filter(available_stock=0)
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order") on_order = rest_filters.BooleanFilter(label='On order', method='filter_on_order')
def filter_on_order(self, queryset, name, value): def filter_on_order(self, queryset, name, value):
"""Filter the queryset based on whether each line item has any stock on order""" """Filter the queryset based on whether each line item has any stock on order"""
@ -1686,7 +1686,7 @@ class BomFilter(rest_filters.FilterSet):
return queryset.filter(on_order=0) return queryset.filter(on_order=0)
has_pricing = rest_filters.BooleanFilter( has_pricing = rest_filters.BooleanFilter(
label="Has Pricing", method="filter_has_pricing" label='Has Pricing', method='filter_has_pricing'
) )
def filter_has_pricing(self, queryset, name, value): def filter_has_pricing(self, queryset, name, value):

View File

@ -12,7 +12,7 @@ from InvenTree.ready import (
isPluginRegistryLoaded, isPluginRegistryLoaded,
) )
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
class PartConfig(AppConfig): class PartConfig(AppConfig):
@ -67,11 +67,11 @@ class PartConfig(AppConfig):
if items.count() > 0: if items.count() > 0:
# Find any pricing objects which have the 'scheduled_for_update' flag set # Find any pricing objects which have the 'scheduled_for_update' flag set
logger.info( logger.info(
"Resetting update flags for %s pricing objects...", items.count() 'Resetting update flags for %s pricing objects...', items.count()
) )
for pricing in items: for pricing in items:
pricing.scheduled_for_update = False pricing.scheduled_for_update = False
pricing.save() pricing.save()
except Exception: except Exception:
logger.exception("Failed to reset pricing flags - database not ready") logger.exception('Failed to reset pricing flags - database not ready')

View File

@ -269,14 +269,14 @@ def ExportBom(
# Generate column names for this supplier # Generate column names for this supplier
k_sup = ( k_sup = (
str(_("Supplier")) str(_('Supplier'))
+ "_" + '_'
+ str(mp_idx) + str(mp_idx)
+ "_" + '_'
+ str(sp_idx) + str(sp_idx)
) )
k_sku = ( k_sku = (
str(_("SKU")) + "_" + str(mp_idx) + "_" + str(sp_idx) str(_('SKU')) + '_' + str(mp_idx) + '_' + str(sp_idx)
) )
try: try:
@ -307,8 +307,8 @@ def ExportBom(
supplier_sku = sp_part.SKU supplier_sku = sp_part.SKU
# Generate column names for this supplier # Generate column names for this supplier
k_sup = str(_("Supplier")) + "_" + str(sp_idx) k_sup = str(_('Supplier')) + '_' + str(sp_idx)
k_sku = str(_("SKU")) + "_" + str(sp_idx) k_sku = str(_('SKU')) + '_' + str(sp_idx)
try: try:
manufacturer_cols[k_sup].update({bom_idx: supplier_name}) manufacturer_cols[k_sup].update({bom_idx: supplier_name})
@ -322,6 +322,6 @@ def ExportBom(
data = dataset.export(fmt) data = dataset.export(fmt)
filename = f"{part.full_name}_BOM.{fmt}" filename = f'{part.full_name}_BOM.{fmt}'
return DownloadFile(data, filename) return DownloadFile(data, filename)

View File

@ -69,7 +69,7 @@ def render_part_full_name(part) -> str:
return template.render(part=part) return template.render(part=part)
except Exception as e: except Exception as e:
logger.warning( logger.warning(
"exception while trying to create full name for part %s: %s", 'exception while trying to create full name for part %s: %s',
part.name, part.name,
e, e,
) )
@ -80,7 +80,7 @@ def render_part_full_name(part) -> str:
# Subdirectory for storing part images # Subdirectory for storing part images
PART_IMAGE_DIR = "part_images" PART_IMAGE_DIR = 'part_images'
def get_part_image_directory() -> str: def get_part_image_directory() -> str:

View File

@ -67,7 +67,7 @@ from InvenTree.status_codes import (
from order import models as OrderModels from order import models as OrderModels
from stock import models as StockModels from stock import models as StockModels
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
class PartCategory(MetadataMixin, InvenTreeTree): class PartCategory(MetadataMixin, InvenTreeTree):
@ -85,8 +85,8 @@ class PartCategory(MetadataMixin, InvenTreeTree):
class Meta: class Meta:
"""Metaclass defines extra model properties""" """Metaclass defines extra model properties"""
verbose_name = _("Part Category") verbose_name = _('Part Category')
verbose_name_plural = _("Part Categories") verbose_name_plural = _('Part Categories')
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""Custom model deletion routine, which updates any child categories or parts. """Custom model deletion routine, which updates any child categories or parts.
@ -101,7 +101,7 @@ class PartCategory(MetadataMixin, InvenTreeTree):
default_location = TreeForeignKey( default_location = TreeForeignKey(
'stock.StockLocation', 'stock.StockLocation',
related_name="default_categories", related_name='default_categories',
null=True, null=True,
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -129,8 +129,8 @@ class PartCategory(MetadataMixin, InvenTreeTree):
icon = models.CharField( icon = models.CharField(
blank=True, blank=True,
max_length=100, max_length=100,
verbose_name=_("Icon"), verbose_name=_('Icon'),
help_text=_("Icon (optional)"), help_text=_('Icon (optional)'),
) )
@staticmethod @staticmethod
@ -150,8 +150,8 @@ class PartCategory(MetadataMixin, InvenTreeTree):
if self.pk and self.structural and self.partcount(False, False) > 0: if self.pk and self.structural and self.partcount(False, False) > 0:
raise ValidationError( raise ValidationError(
_( _(
"You cannot make this part category structural because some parts " 'You cannot make this part category structural because some parts '
"are already assigned to it!" 'are already assigned to it!'
) )
) )
super().clean() super().clean()
@ -387,8 +387,8 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
class Meta: class Meta:
"""Metaclass defines extra model properties""" """Metaclass defines extra model properties"""
verbose_name = _("Part") verbose_name = _('Part')
verbose_name_plural = _("Parts") verbose_name_plural = _('Parts')
ordering = ['name'] ordering = ['name']
constraints = [ constraints = [
UniqueConstraint(fields=['name', 'IPN', 'revision'], name='unique_part') UniqueConstraint(fields=['name', 'IPN', 'revision'], name='unique_part')
@ -482,7 +482,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
def __str__(self): def __str__(self):
"""Return a string representation of the Part (for use in the admin interface)""" """Return a string representation of the Part (for use in the admin interface)"""
return f"{self.full_name} - {self.description}" return f'{self.full_name} - {self.description}'
def get_parts_in_bom(self, **kwargs): def get_parts_in_bom(self, **kwargs):
"""Return a list of all parts in the BOM for this part. """Return a list of all parts in the BOM for this part.
@ -686,8 +686,8 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
if stock.exists(): if stock.exists():
if raise_error: if raise_error:
raise ValidationError( raise ValidationError(
_("Stock item with this serial number already exists") _('Stock item with this serial number already exists')
+ ": " + ': '
+ serial + serial
) )
else: else:
@ -800,7 +800,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
.exists() .exists()
): ):
raise ValidationError( raise ValidationError(
_("Part with this Name, IPN and Revision already exists.") _('Part with this Name, IPN and Revision already exists.')
) )
def clean(self): def clean(self):
@ -815,7 +815,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
""" """
if self.category is not None and self.category.structural: if self.category is not None and self.category.structural:
raise ValidationError({ raise ValidationError({
'category': _("Parts cannot be assigned to structural part categories!") 'category': _('Parts cannot be assigned to structural part categories!')
}) })
super().clean() super().clean()
@ -989,7 +989,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
units = models.CharField( units = models.CharField(
max_length=20, max_length=20,
default="", default='',
blank=True, blank=True,
null=True, null=True,
verbose_name=_('Units'), verbose_name=_('Units'),
@ -1024,7 +1024,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
salable = models.BooleanField( salable = models.BooleanField(
default=part_settings.part_salable_default, default=part_settings.part_salable_default,
verbose_name=_('Salable'), verbose_name=_('Salable'),
help_text=_("Can this part be sold to customers?"), help_text=_('Can this part be sold to customers?'),
) )
active = models.BooleanField( active = models.BooleanField(
@ -1823,7 +1823,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
min_price = normalize(min_price) min_price = normalize(min_price)
max_price = normalize(max_price) max_price = normalize(max_price)
return f"{min_price} - {max_price}" return f'{min_price} - {max_price}'
def get_supplier_price_range(self, quantity=1): def get_supplier_price_range(self, quantity=1):
"""Return the supplier price range of this part: """Return the supplier price range of this part:
@ -1872,7 +1872,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
for item in self.get_bom_items().select_related('sub_part'): for item in self.get_bom_items().select_related('sub_part'):
if item.sub_part.pk == self.pk: if item.sub_part.pk == self.pk:
logger.warning("WARNING: BomItem ID %s contains itself in BOM", item.pk) logger.warning('WARNING: BomItem ID %s contains itself in BOM', item.pk)
continue continue
q = decimal.Decimal(quantity) q = decimal.Decimal(quantity)
@ -2402,7 +2402,7 @@ class PartPricing(common.models.MetaMixin):
result = convert_money(money, target_currency) result = convert_money(money, target_currency)
except MissingRate: except MissingRate:
logger.warning( logger.warning(
"No currency conversion rate available for %s -> %s", 'No currency conversion rate available for %s -> %s',
money.currency, money.currency,
target_currency, target_currency,
) )
@ -2432,7 +2432,7 @@ class PartPricing(common.models.MetaMixin):
or not Part.objects.filter(pk=self.part.pk).exists() or not Part.objects.filter(pk=self.part.pk).exists()
): ):
logger.warning( logger.warning(
"Referenced part instance does not exist - skipping pricing update." 'Referenced part instance does not exist - skipping pricing update.'
) )
return return
@ -2458,13 +2458,13 @@ class PartPricing(common.models.MetaMixin):
if self.scheduled_for_update: if self.scheduled_for_update:
# Ignore if the pricing is already scheduled to be updated # Ignore if the pricing is already scheduled to be updated
logger.debug("Pricing for %s already scheduled for update - skipping", p) logger.debug('Pricing for %s already scheduled for update - skipping', p)
return return
if counter > 25: if counter > 25:
# Prevent infinite recursion / stack depth issues # Prevent infinite recursion / stack depth issues
logger.debug( logger.debug(
counter, f"Skipping pricing update for {p} - maximum depth exceeded" counter, f'Skipping pricing update for {p} - maximum depth exceeded'
) )
return return
@ -3260,7 +3260,7 @@ class PartAttachment(InvenTreeAttachment):
def getSubdir(self): def getSubdir(self):
"""Returns the media subdirectory where part attachments are stored""" """Returns the media subdirectory where part attachments are stored"""
return os.path.join("part_files", str(self.part.id)) return os.path.join('part_files', str(self.part.id))
part = models.ForeignKey( part = models.ForeignKey(
Part, Part,
@ -3423,7 +3423,7 @@ class PartTestTemplate(MetadataMixin, models.Model):
for test in tests: for test in tests:
if test.key == key: if test.key == key:
raise ValidationError({ raise ValidationError({
'test_name': _("Test with this name already exists for this part") 'test_name': _('Test with this name already exists for this part')
}) })
super().validate_unique(exclude) super().validate_unique(exclude)
@ -3444,35 +3444,35 @@ class PartTestTemplate(MetadataMixin, models.Model):
test_name = models.CharField( test_name = models.CharField(
blank=False, blank=False,
max_length=100, max_length=100,
verbose_name=_("Test Name"), verbose_name=_('Test Name'),
help_text=_("Enter a name for the test"), help_text=_('Enter a name for the test'),
) )
description = models.CharField( description = models.CharField(
blank=False, blank=False,
null=True, null=True,
max_length=100, max_length=100,
verbose_name=_("Test Description"), verbose_name=_('Test Description'),
help_text=_("Enter description for this test"), help_text=_('Enter description for this test'),
) )
required = models.BooleanField( required = models.BooleanField(
default=True, default=True,
verbose_name=_("Required"), verbose_name=_('Required'),
help_text=_("Is this test required to pass?"), help_text=_('Is this test required to pass?'),
) )
requires_value = models.BooleanField( requires_value = models.BooleanField(
default=False, default=False,
verbose_name=_("Requires Value"), verbose_name=_('Requires Value'),
help_text=_("Does this test require a value when adding a test result?"), help_text=_('Does this test require a value when adding a test result?'),
) )
requires_attachment = models.BooleanField( requires_attachment = models.BooleanField(
default=False, default=False,
verbose_name=_("Requires Attachment"), verbose_name=_('Requires Attachment'),
help_text=_( help_text=_(
"Does this test require a file attachment when adding a test result?" 'Does this test require a file attachment when adding a test result?'
), ),
) )
@ -3503,7 +3503,7 @@ class PartParameterTemplate(MetadataMixin, models.Model):
"""Return a string representation of a PartParameterTemplate instance""" """Return a string representation of a PartParameterTemplate instance"""
s = str(self.name) s = str(self.name)
if self.units: if self.units:
s += f" ({self.units})" s += f' ({self.units})'
return s return s
def clean(self): def clean(self):
@ -3557,8 +3557,8 @@ class PartParameterTemplate(MetadataMixin, models.Model):
).exclude(pk=self.pk) ).exclude(pk=self.pk)
if others.exists(): if others.exists():
msg = _("Parameter template name must be unique") msg = _('Parameter template name must be unique')
raise ValidationError({"name": msg}) raise ValidationError({'name': msg})
except PartParameterTemplate.DoesNotExist: except PartParameterTemplate.DoesNotExist:
pass pass
@ -3644,7 +3644,7 @@ class PartParameter(MetadataMixin, models.Model):
def __str__(self): def __str__(self):
"""String representation of a PartParameter (used in the admin interface)""" """String representation of a PartParameter (used in the admin interface)"""
return f"{self.part.full_name} : {self.template.name} = {self.data} ({self.template.units})" return f'{self.part.full_name} : {self.template.name} = {self.data} ({self.template.units})'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Custom save method for the PartParameter model.""" """Custom save method for the PartParameter model."""
@ -3856,11 +3856,11 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
class Meta: class Meta:
"""Metaclass providing extra model definition""" """Metaclass providing extra model definition"""
verbose_name = _("BOM Item") verbose_name = _('BOM Item')
def __str__(self): def __str__(self):
"""Return a string representation of this BomItem instance""" """Return a string representation of this BomItem instance"""
return f"{decimal2string(self.quantity)} x {self.sub_part.full_name} to make {self.part.full_name}" return f'{decimal2string(self.quantity)} x {self.sub_part.full_name} to make {self.part.full_name}'
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
@ -3968,13 +3968,13 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
optional = models.BooleanField( optional = models.BooleanField(
default=False, default=False,
verbose_name=_('Optional'), verbose_name=_('Optional'),
help_text=_("This BOM item is optional"), help_text=_('This BOM item is optional'),
) )
consumable = models.BooleanField( consumable = models.BooleanField(
default=False, default=False,
verbose_name=_('Consumable'), verbose_name=_('Consumable'),
help_text=_("This BOM item is consumable (it is not tracked in build orders)"), help_text=_('This BOM item is consumable (it is not tracked in build orders)'),
) )
overage = models.CharField( overage = models.CharField(
@ -4106,8 +4106,8 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
if self.sub_part.trackable: if self.sub_part.trackable:
if self.quantity != int(self.quantity): if self.quantity != int(self.quantity):
raise ValidationError({ raise ValidationError({
"quantity": _( 'quantity': _(
"Quantity must be integer value for trackable parts" 'Quantity must be integer value for trackable parts'
) )
}) })
@ -4204,7 +4204,7 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
pmin = decimal2money(pmin) pmin = decimal2money(pmin)
pmax = decimal2money(pmax) pmax = decimal2money(pmax)
return f"{pmin} to {pmax}" return f'{pmin} to {pmax}'
@receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines') @receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines')
@ -4259,7 +4259,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
class Meta: class Meta:
"""Metaclass providing extra model definition""" """Metaclass providing extra model definition"""
verbose_name = _("BOM Item Substitute") verbose_name = _('BOM Item Substitute')
# Prevent duplication of substitute parts # Prevent duplication of substitute parts
unique_together = ('part', 'bom_item') unique_together = ('part', 'bom_item')
@ -4280,7 +4280,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
if self.part == self.bom_item.sub_part: if self.part == self.bom_item.sub_part:
raise ValidationError({ raise ValidationError({
"part": _("Substitute part cannot be the same as the master part") 'part': _('Substitute part cannot be the same as the master part')
}) })
@staticmethod @staticmethod
@ -4345,9 +4345,9 @@ class PartRelated(MetadataMixin, models.Model):
if self.part_1 == self.part_2: if self.part_1 == self.part_2:
raise ValidationError( raise ValidationError(
_("Part relationship cannot be created between a part and itself") _('Part relationship cannot be created between a part and itself')
) )
# Check for inverse relationship # Check for inverse relationship
if PartRelated.objects.filter(part_1=self.part_2, part_2=self.part_1).exists(): if PartRelated.objects.filter(part_1=self.part_2, part_2=self.part_1).exists():
raise ValidationError(_("Duplicate relationship already exists")) raise ValidationError(_('Duplicate relationship already exists'))

View File

@ -55,7 +55,7 @@ from .models import (
PartTestTemplate, PartTestTemplate,
) )
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer): class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
@ -220,7 +220,7 @@ class PartThumbSerializerUpdate(InvenTree.serializers.InvenTreeModelSerializer):
"""Check that file is an image.""" """Check that file is an image."""
validate = imghdr.what(value) validate = imghdr.what(value)
if not validate: if not validate:
raise serializers.ValidationError("File is not an image") raise serializers.ValidationError('File is not an image')
return value return value
image = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True) image = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True)
@ -346,7 +346,7 @@ class PartSetCategorySerializer(serializers.Serializer):
def validate_parts(self, parts): def validate_parts(self, parts):
"""Validate the selected parts""" """Validate the selected parts"""
if len(parts) == 0: if len(parts) == 0:
raise serializers.ValidationError(_("No parts selected")) raise serializers.ValidationError(_('No parts selected'))
return parts return parts
@ -881,7 +881,7 @@ class PartSerializer(
) )
except IntegrityError: except IntegrityError:
logger.exception( logger.exception(
"Could not create new PartParameter for part %s", instance 'Could not create new PartParameter for part %s', instance
) )
# Create initial stock entry # Create initial stock entry
@ -945,7 +945,7 @@ class PartSerializer(
remote_img.save(buffer, format=fmt) remote_img.save(buffer, format=fmt)
# Construct a simplified name for the image # Construct a simplified name for the image
filename = f"part_{part.pk}_image.{fmt.lower()}" filename = f'part_{part.pk}_image.{fmt.lower()}'
part.image.save(filename, ContentFile(buffer.getvalue())) part.image.save(filename, ContentFile(buffer.getvalue()))
@ -1071,12 +1071,12 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
# Stocktake functionality must be enabled # Stocktake functionality must be enabled
if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False): if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False):
raise serializers.ValidationError( raise serializers.ValidationError(
_("Stocktake functionality is not enabled") _('Stocktake functionality is not enabled')
) )
# Check that background worker is running # Check that background worker is running
if not InvenTree.status.is_worker_running(): if not InvenTree.status.is_worker_running():
raise serializers.ValidationError(_("Background worker check failed")) raise serializers.ValidationError(_('Background worker check failed'))
return data return data
@ -1381,7 +1381,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
def validate_quantity(self, quantity): def validate_quantity(self, quantity):
"""Perform validation for the BomItem quantity field""" """Perform validation for the BomItem quantity field"""
if quantity <= 0: if quantity <= 0:
raise serializers.ValidationError(_("Quantity must be greater than zero")) raise serializers.ValidationError(_('Quantity must be greater than zero'))
return quantity return quantity
@ -1680,7 +1680,7 @@ class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer
if not any(col in self.columns for col in part_columns): if not any(col in self.columns for col in part_columns):
# At least one part column is required! # At least one part column is required!
raise serializers.ValidationError(_("No part column specified")) raise serializers.ValidationError(_('No part column specified'))
@staticmethod @staticmethod
def process_row(row): def process_row(row):
@ -1768,7 +1768,7 @@ class BomImportSubmitSerializer(serializers.Serializer):
items = data['items'] items = data['items']
if len(items) == 0: if len(items) == 0:
raise serializers.ValidationError(_("At least one BOM item is required")) raise serializers.ValidationError(_('At least one BOM item is required'))
data = super().validate(data) data = super().validate(data)
@ -1798,7 +1798,7 @@ class BomImportSubmitSerializer(serializers.Serializer):
bom_items.append(BomItem(**item)) bom_items.append(BomItem(**item))
if len(bom_items) > 0: if len(bom_items) > 0:
logger.info("Importing %s BOM items", len(bom_items)) logger.info('Importing %s BOM items', len(bom_items))
BomItem.objects.bulk_create(bom_items) BomItem.objects.bulk_create(bom_items)
except Exception as e: except Exception as e:

View File

@ -62,7 +62,7 @@ def perform_stocktake(
if not pricing.is_valid: if not pricing.is_valid:
# If pricing is not valid, let's update # If pricing is not valid, let's update
logger.info("Pricing not valid for %s - updating", target) logger.info('Pricing not valid for %s - updating', target)
pricing.update_pricing(cascade=False) pricing.update_pricing(cascade=False)
pricing.refresh_from_db() pricing.refresh_from_db()
@ -204,10 +204,10 @@ def generate_stocktake_report(**kwargs):
n_parts = parts.count() n_parts = parts.count()
if n_parts == 0: if n_parts == 0:
logger.info("No parts selected for stocktake report - exiting") logger.info('No parts selected for stocktake report - exiting')
return return
logger.info("Generating new stocktake report for %s parts", n_parts) logger.info('Generating new stocktake report for %s parts', n_parts)
base_currency = common.settings.currency_code_default() base_currency = common.settings.currency_code_default()
@ -266,7 +266,7 @@ def generate_stocktake_report(**kwargs):
buffer.write(dataset.export('csv')) buffer.write(dataset.export('csv'))
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
filename = f"InvenTree_Stocktake_{today}.csv" filename = f'InvenTree_Stocktake_{today}.csv'
report_file = ContentFile(buffer.getvalue(), name=filename) report_file = ContentFile(buffer.getvalue(), name=filename)
if generate_report: if generate_report:
@ -295,7 +295,7 @@ def generate_stocktake_report(**kwargs):
t_stocktake = time.time() - t_start t_stocktake = time.time() - t_start
logger.info( logger.info(
"Generated stocktake report for %s parts in %ss", 'Generated stocktake report for %s parts in %ss',
total_parts, total_parts,
round(t_stocktake, 2), round(t_stocktake, 2),
) )

View File

@ -24,7 +24,7 @@ from InvenTree.tasks import (
scheduled_task, scheduled_task,
) )
logger = logging.getLogger("inventree") logger = logging.getLogger('inventree')
def notify_low_stock(part: part.models.Part): def notify_low_stock(part: part.models.Part):
@ -33,7 +33,7 @@ def notify_low_stock(part: part.models.Part):
- Triggered when the available stock for a given part falls be low the configured threhsold - Triggered when the available stock for a given part falls be low the configured threhsold
- A notification is delivered to any users who are 'subscribed' to this part - A notification is delivered to any users who are 'subscribed' to this part
""" """
name = _("Low stock notification") name = _('Low stock notification')
message = _( message = _(
f'The available stock for {part.name} has fallen below the configured minimum level' f'The available stock for {part.name} has fallen below the configured minimum level'
) )
@ -70,7 +70,7 @@ def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0):
pricing: The target PartPricing instance to be updated pricing: The target PartPricing instance to be updated
counter: How many times this function has been called in sequence counter: How many times this function has been called in sequence
""" """
logger.info("Updating part pricing for %s", pricing.part) logger.info('Updating part pricing for %s', pricing.part)
pricing.update_pricing(counter=counter) pricing.update_pricing(counter=counter)
@ -90,7 +90,7 @@ def check_missing_pricing(limit=250):
results = part.models.PartPricing.objects.filter(updated=None)[:limit] results = part.models.PartPricing.objects.filter(updated=None)[:limit]
if results.count() > 0: if results.count() > 0:
logger.info("Found %s parts with empty pricing", results.count()) logger.info('Found %s parts with empty pricing', results.count())
for pp in results: for pp in results:
pp.schedule_for_update() pp.schedule_for_update()
@ -102,7 +102,7 @@ def check_missing_pricing(limit=250):
results = part.models.PartPricing.objects.filter(updated__lte=stale_date)[:limit] results = part.models.PartPricing.objects.filter(updated__lte=stale_date)[:limit]
if results.count() > 0: if results.count() > 0:
logger.info("Found %s stale pricing entries", results.count()) logger.info('Found %s stale pricing entries', results.count())
for pp in results: for pp in results:
pp.schedule_for_update() pp.schedule_for_update()
@ -112,7 +112,7 @@ def check_missing_pricing(limit=250):
results = part.models.PartPricing.objects.exclude(currency=currency) results = part.models.PartPricing.objects.exclude(currency=currency)
if results.count() > 0: if results.count() > 0:
logger.info("Found %s pricing entries in the wrong currency", results.count()) logger.info('Found %s pricing entries in the wrong currency', results.count())
for pp in results: for pp in results:
pp.schedule_for_update() pp.schedule_for_update()
@ -121,7 +121,7 @@ def check_missing_pricing(limit=250):
results = part.models.Part.objects.filter(pricing_data=None)[:limit] results = part.models.Part.objects.filter(pricing_data=None)[:limit]
if results.count() > 0: if results.count() > 0:
logger.info("Found %s parts without pricing", results.count()) logger.info('Found %s parts without pricing', results.count())
for p in results: for p in results:
pricing = p.pricing pricing = p.pricing
@ -151,14 +151,14 @@ def scheduled_stocktake_reports():
old_reports = part.models.PartStocktakeReport.objects.filter(date__lt=threshold) old_reports = part.models.PartStocktakeReport.objects.filter(date__lt=threshold)
if old_reports.count() > 0: if old_reports.count() > 0:
logger.info("Deleting %s stale stocktake reports", old_reports.count()) logger.info('Deleting %s stale stocktake reports', old_reports.count())
old_reports.delete() old_reports.delete()
# Next, check if stocktake functionality is enabled # Next, check if stocktake functionality is enabled
if not common.models.InvenTreeSetting.get_setting( if not common.models.InvenTreeSetting.get_setting(
'STOCKTAKE_ENABLE', False, cache=False 'STOCKTAKE_ENABLE', False, cache=False
): ):
logger.info("Stocktake functionality is not enabled - exiting") logger.info('Stocktake functionality is not enabled - exiting')
return return
report_n_days = int( report_n_days = int(
@ -168,11 +168,11 @@ def scheduled_stocktake_reports():
) )
if report_n_days < 1: if report_n_days < 1:
logger.info("Stocktake auto reports are disabled, exiting") logger.info('Stocktake auto reports are disabled, exiting')
return return
if not check_daily_holdoff('STOCKTAKE_RECENT_REPORT', report_n_days): if not check_daily_holdoff('STOCKTAKE_RECENT_REPORT', report_n_days):
logger.info("Stocktake report was recently generated - exiting") logger.info('Stocktake report was recently generated - exiting')
return return
# Let's start a new stocktake report for all parts # Let's start a new stocktake report for all parts

View File

@ -42,15 +42,15 @@ class CustomTranslateNode(TranslateNode):
result = result.replace(c, '') result = result.replace(c, '')
# Escape any quotes contained in the string # Escape any quotes contained in the string
result = result.replace("'", r"\'") result = result.replace("'", r'\'')
result = result.replace('"', r'\"') result = result.replace('"', r'\"')
# Return the 'clean' resulting string # Return the 'clean' resulting string
return result return result
@register.tag("translate") @register.tag('translate')
@register.tag("trans") @register.tag('trans')
def do_translate(parser, token): def do_translate(parser, token):
"""Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py """Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py
@ -66,7 +66,7 @@ def do_translate(parser, token):
asvar = None asvar = None
message_context = None message_context = None
seen = set() seen = set()
invalid_context = {"as", "noop"} invalid_context = {'as', 'noop'}
while remaining: while remaining:
option = remaining.pop(0) option = remaining.pop(0)
@ -74,9 +74,9 @@ def do_translate(parser, token):
raise TemplateSyntaxError( raise TemplateSyntaxError(
"The '%s' option was specified more than once." % option "The '%s' option was specified more than once." % option
) )
elif option == "noop": elif option == 'noop':
noop = True noop = True
elif option == "context": elif option == 'context':
try: try:
value = remaining.pop(0) value = remaining.pop(0)
except IndexError: except IndexError:
@ -87,10 +87,10 @@ def do_translate(parser, token):
if value in invalid_context: if value in invalid_context:
raise TemplateSyntaxError( raise TemplateSyntaxError(
"Invalid argument '%s' provided to the '%s' tag for the context " "Invalid argument '%s' provided to the '%s' tag for the context "
"option" % (value, bits[0]) 'option' % (value, bits[0])
) )
message_context = parser.compile_filter(value) message_context = parser.compile_filter(value)
elif option == "as": elif option == 'as':
try: try:
value = remaining.pop(0) value = remaining.pop(0)
except IndexError: except IndexError:
@ -110,26 +110,26 @@ def do_translate(parser, token):
# Re-register tags which we have not explicitly overridden # Re-register tags which we have not explicitly overridden
register.tag("blocktrans", django.templatetags.i18n.do_block_translate) register.tag('blocktrans', django.templatetags.i18n.do_block_translate)
register.tag("blocktranslate", django.templatetags.i18n.do_block_translate) register.tag('blocktranslate', django.templatetags.i18n.do_block_translate)
register.tag("language", django.templatetags.i18n.language) register.tag('language', django.templatetags.i18n.language)
register.tag( register.tag(
"get_available_languages", django.templatetags.i18n.do_get_available_languages 'get_available_languages', django.templatetags.i18n.do_get_available_languages
) )
register.tag("get_language_info", django.templatetags.i18n.do_get_language_info) register.tag('get_language_info', django.templatetags.i18n.do_get_language_info)
register.tag( register.tag(
"get_language_info_list", django.templatetags.i18n.do_get_language_info_list 'get_language_info_list', django.templatetags.i18n.do_get_language_info_list
) )
register.tag("get_current_language", django.templatetags.i18n.do_get_current_language) register.tag('get_current_language', django.templatetags.i18n.do_get_current_language)
register.tag( register.tag(
"get_current_language_bidi", django.templatetags.i18n.do_get_current_language_bidi 'get_current_language_bidi', django.templatetags.i18n.do_get_current_language_bidi
) )
register.filter("language_name", django.templatetags.i18n.language_name) register.filter('language_name', django.templatetags.i18n.language_name)
register.filter( register.filter(
"language_name_translated", django.templatetags.i18n.language_name_translated 'language_name_translated', django.templatetags.i18n.language_name_translated
) )
register.filter("language_name_local", django.templatetags.i18n.language_name_local) register.filter('language_name_local', django.templatetags.i18n.language_name_local)
register.filter("language_bidi", django.templatetags.i18n.language_bidi) register.filter('language_bidi', django.templatetags.i18n.language_bidi)

View File

@ -65,7 +65,7 @@ def render_date(context, date_object):
try: try:
date_object = date.fromisoformat(date_object) date_object = date.fromisoformat(date_object)
except ValueError: except ValueError:
logger.warning("Tried to convert invalid date string: %s", date_object) logger.warning('Tried to convert invalid date string: %s', date_object)
return None return None
# We may have already pre-cached the date format by calling this already! # We may have already pre-cached the date format by calling this already!
@ -220,7 +220,7 @@ def python_version(*args, **kwargs):
def inventree_version(shortstring=False, *args, **kwargs): def inventree_version(shortstring=False, *args, **kwargs):
"""Return InvenTree version string.""" """Return InvenTree version string."""
if shortstring: if shortstring:
return _(f"{version.inventreeInstanceTitle()} v{version.inventreeVersion()}") return _(f'{version.inventreeInstanceTitle()} v{version.inventreeVersion()}')
return version.inventreeVersion() return version.inventreeVersion()
@ -645,18 +645,18 @@ def admin_url(user, table, pk):
from django.urls import reverse from django.urls import reverse
if not djangosettings.INVENTREE_ADMIN_ENABLED: if not djangosettings.INVENTREE_ADMIN_ENABLED:
return "" return ''
if not user.is_staff: if not user.is_staff:
return "" return ''
# Check the user has the correct permission # Check the user has the correct permission
perm_string = f"{app}.change_{model}" perm_string = f'{app}.change_{model}'
if not user.has_perm(perm_string): if not user.has_perm(perm_string):
return '' return ''
# Fallback URL # Fallback URL
url = reverse(f"admin:{app}_{model}_changelist") url = reverse(f'admin:{app}_{model}_changelist')
if pk: if pk:
try: try:

View File

@ -178,14 +178,14 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
# Create child categories # Create child categories
for ii in range(10): for ii in range(10):
child = PartCategory.objects.create( child = PartCategory.objects.create(
name=f"Child cat {ii}", description="A child category", parent=cat name=f'Child cat {ii}', description='A child category', parent=cat
) )
# Create parts in this category # Create parts in this category
for jj in range(10): for jj in range(10):
Part.objects.create( Part.objects.create(
name=f"Part xyz {jj}_{ii}", name=f'Part xyz {jj}_{ii}',
description="A test part with a description", description='A test part with a description',
category=child, category=child,
) )
@ -351,8 +351,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
for jj in range(3): for jj in range(3):
parts.append( parts.append(
Part.objects.create( Part.objects.create(
name=f"Part xyz {i}_{jj}", name=f'Part xyz {i}_{jj}',
description="Child part of the deleted category", description='Child part of the deleted category',
category=cat_to_delete, category=cat_to_delete,
) )
) )
@ -362,8 +362,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
# Create child categories under the category to be deleted # Create child categories under the category to be deleted
for ii in range(3): for ii in range(3):
child = PartCategory.objects.create( child = PartCategory.objects.create(
name=f"Child parent_cat {i}_{ii}", name=f'Child parent_cat {i}_{ii}',
description="A child category of the deleted category", description='A child category of the deleted category',
parent=cat_to_delete, parent=cat_to_delete,
) )
child_categories.append(child) child_categories.append(child)
@ -372,8 +372,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
for jj in range(3): for jj in range(3):
child_categories_parts.append( child_categories_parts.append(
Part.objects.create( Part.objects.create(
name=f"Part xyz {i}_{jj}_{ii}", name=f'Part xyz {i}_{jj}_{ii}',
description="Child part in the child category of the deleted category", description='Child part in the child category of the deleted category',
category=child, category=child,
) )
) )
@ -438,8 +438,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
# Make sure that we get an error if we try to create part in the structural category # Make sure that we get an error if we try to create part in the structural category
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
part = Part.objects.create( part = Part.objects.create(
name="-", name='-',
description="Part which shall not be created", description='Part which shall not be created',
category=structural_category, category=structural_category,
) )
@ -456,8 +456,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
# Create the test part assigned to a non-structural category # Create the test part assigned to a non-structural category
part = Part.objects.create( part = Part.objects.create(
name="-", name='-',
description="Part which category will be changed to structural", description='Part which category will be changed to structural',
category=non_structural_category, category=non_structural_category,
) )
@ -752,8 +752,8 @@ class PartAPITest(PartAPITestBase):
for color in ['Red', 'Green', 'Blue', 'Yellow', 'Pink', 'Black']: for color in ['Red', 'Green', 'Blue', 'Yellow', 'Pink', 'Black']:
variants.append( variants.append(
Part.objects.create( Part.objects.create(
name=f"{color} Variant", name=f'{color} Variant',
description="Variant part with a specific color", description='Variant part with a specific color',
variant_of=master_part, variant_of=master_part,
category=category, category=category,
) )
@ -839,7 +839,7 @@ class PartAPITest(PartAPITestBase):
# Try to post a new test with the same name (should fail) # Try to post a new test with the same name (should fail)
response = self.post( response = self.post(
url, url,
data={'part': 10004, 'test_name': " newtest", 'description': 'dafsdf'}, data={'part': 10004, 'test_name': ' newtest', 'description': 'dafsdf'},
) )
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@ -971,8 +971,8 @@ class PartAPITest(PartAPITestBase):
for i in range(10): for i in range(10):
gcv = Part.objects.create( gcv = Part.objects.create(
name=f"GC Var {i}", name=f'GC Var {i}',
description="Green chair variant", description='Green chair variant',
variant_of=green_chair, variant_of=green_chair,
) )
@ -1237,10 +1237,10 @@ class PartCreationTests(PartAPITestBase):
"""Test that non-standard ASCII chars are accepted.""" """Test that non-standard ASCII chars are accepted."""
url = reverse('api-part-list') url = reverse('api-part-list')
name = "Kaltgerätestecker" name = 'Kaltgerätestecker'
description = "Gerät Kaltgerätestecker strange chars should get through" description = 'Gerät Kaltgerätestecker strange chars should get through'
data = {"name": name, "description": description, "category": 2} data = {'name': name, 'description': description, 'category': 2}
response = self.post(url, data, expected_code=201) response = self.post(url, data, expected_code=201)
@ -1284,7 +1284,7 @@ class PartCreationTests(PartAPITestBase):
PartCategoryParameterTemplate.objects.create( PartCategoryParameterTemplate.objects.create(
parameter_template=PartParameterTemplate.objects.get(pk=pk), parameter_template=PartParameterTemplate.objects.get(pk=pk),
category=cat, category=cat,
default_value=f"Value {pk}", default_value=f'Value {pk}',
) )
self.assertEqual(cat.parameter_templates.count(), 3) self.assertEqual(cat.parameter_templates.count(), 3)
@ -1630,8 +1630,8 @@ class PartListTests(PartAPITestBase):
for ii in range(100): for ii in range(100):
parts.append( parts.append(
Part( Part(
name=f"Extra part {ii}", name=f'Extra part {ii}',
description="A new part which will appear via the API", description='A new part which will appear via the API',
level=0, level=0,
tree_id=0, tree_id=0,
lft=0, lft=0,
@ -1975,15 +1975,15 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
# First, create some parts # First, create some parts
paint = PartCategory.objects.create( paint = PartCategory.objects.create(
parent=None, name="Paint", description="Paints and such" parent=None, name='Paint', description='Paints and such'
) )
for color in ['Red', 'Green', 'Blue', 'Orange', 'Yellow']: for color in ['Red', 'Green', 'Blue', 'Orange', 'Yellow']:
p = Part.objects.create( p = Part.objects.create(
category=paint, category=paint,
units='litres', units='litres',
name=f"{color} Paint", name=f'{color} Paint',
description=f"Paint which is {color} in color", description=f'Paint which is {color} in color',
) )
# Create multiple supplier parts in different sizes # Create multiple supplier parts in different sizes
@ -1991,7 +1991,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
sp = SupplierPart.objects.create( sp = SupplierPart.objects.create(
part=p, part=p,
supplier=supplier, supplier=supplier,
SKU=f"PNT-{color}-{pk_sz}L", SKU=f'PNT-{color}-{pk_sz}L',
pack_quantity=str(pk_sz), pack_quantity=str(pk_sz),
) )
@ -2137,7 +2137,7 @@ class BomItemTest(InvenTreeAPITestCase):
url = reverse('api-bom-list') url = reverse('api-bom-list')
# Order by increasing quantity # Order by increasing quantity
response = self.get(f"{url}?ordering=+quantity", expected_code=200) response = self.get(f'{url}?ordering=+quantity', expected_code=200)
self.assertEqual(len(response.data), 6) self.assertEqual(len(response.data), 6)
@ -2147,7 +2147,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertTrue(q1 < q2) self.assertTrue(q1 < q2)
# Order by decreasing quantity # Order by decreasing quantity
response = self.get(f"{url}?ordering=-quantity", expected_code=200) response = self.get(f'{url}?ordering=-quantity', expected_code=200)
self.assertEqual(q1, response.data[-1]['quantity']) self.assertEqual(q1, response.data[-1]['quantity'])
self.assertEqual(q2, response.data[0]['quantity']) self.assertEqual(q2, response.data[0]['quantity'])
@ -2247,8 +2247,8 @@ class BomItemTest(InvenTreeAPITestCase):
for ii in range(5): for ii in range(5):
# Create a variant part! # Create a variant part!
variant = Part.objects.create( variant = Part.objects.create(
name=f"Variant_{ii}", name=f'Variant_{ii}',
description="A variant part, with a description", description='A variant part, with a description',
component=True, component=True,
variant_of=sub_part, variant_of=sub_part,
) )
@ -2295,7 +2295,7 @@ class BomItemTest(InvenTreeAPITestCase):
bom_item = BomItem.objects.get(pk=1) bom_item = BomItem.objects.get(pk=1)
# Filter stock items which can be assigned against this stock item # Filter stock items which can be assigned against this stock item
response = self.get(stock_url, {"bom_item": bom_item.pk}, expected_code=200) response = self.get(stock_url, {'bom_item': bom_item.pk}, expected_code=200)
n_items = len(response.data) n_items = len(response.data)
@ -2304,8 +2304,8 @@ class BomItemTest(InvenTreeAPITestCase):
# Let's make some! # Let's make some!
for ii in range(5): for ii in range(5):
sub_part = Part.objects.create( sub_part = Part.objects.create(
name=f"Substitute {ii}", name=f'Substitute {ii}',
description="A substitute part", description='A substitute part',
component=True, component=True,
is_template=False, is_template=False,
assembly=False, assembly=False,
@ -2322,7 +2322,7 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
# We should also have more stock available to allocate against this BOM item! # We should also have more stock available to allocate against this BOM item!
response = self.get(stock_url, {"bom_item": bom_item.pk}, expected_code=200) response = self.get(stock_url, {'bom_item': bom_item.pk}, expected_code=200)
self.assertEqual(len(response.data), n_items + ii + 1) self.assertEqual(len(response.data), n_items + ii + 1)
@ -2355,8 +2355,8 @@ class BomItemTest(InvenTreeAPITestCase):
for i in range(5): for i in range(5):
assy = Part.objects.create( assy = Part.objects.create(
name=f"Assy_{i}", name=f'Assy_{i}',
description="An assembly made of other parts", description='An assembly made of other parts',
active=True, active=True,
assembly=True, assembly=True,
) )
@ -2368,8 +2368,8 @@ class BomItemTest(InvenTreeAPITestCase):
# Create some sub-components # Create some sub-components
for i in range(5): for i in range(5):
cmp = Part.objects.create( cmp = Part.objects.create(
name=f"Component_{i}", name=f'Component_{i}',
description="A sub component", description='A sub component',
active=True, active=True,
component=True, component=True,
) )
@ -2403,8 +2403,8 @@ class BomItemTest(InvenTreeAPITestCase):
for i in range(10): for i in range(10):
# Create a variant part # Create a variant part
vp = Part.objects.create( vp = Part.objects.create(
name=f"Var {i}", name=f'Var {i}',
description="Variant part description field", description='Variant part description field',
variant_of=bom_item.sub_part, variant_of=bom_item.sub_part,
) )
@ -2523,7 +2523,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
p.active = False p.active = False
p.save() p.save()
response = self.delete(reverse("api-part-detail", kwargs={"pk": 1})) response = self.delete(reverse('api-part-detail', kwargs={'pk': 1}))
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
with self.assertRaises(Part.DoesNotExist): with self.assertRaises(Part.DoesNotExist):
@ -2588,7 +2588,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
# Initially no stocktake information available # Initially no stocktake information available
self.assertIsNone(p.latest_stocktake) self.assertIsNone(p.latest_stocktake)
note = f"Note {p.pk}" note = f'Note {p.pk}'
quantity = p.pk + 5 quantity = p.pk + 5
self.post( self.post(

View File

@ -33,9 +33,9 @@ class BomUploadTest(InvenTreeAPITestCase):
for i in range(10): for i in range(10):
parts.append( parts.append(
Part( Part(
name=f"Component {i}", name=f'Component {i}',
IPN=f"CMP_{i}", IPN=f'CMP_{i}',
description="A subcomponent that can be used in a BOM", description='A subcomponent that can be used in a BOM',
component=True, component=True,
assembly=False, assembly=False,
lft=0, lft=0,

View File

@ -70,7 +70,7 @@ class BomItemTest(TestCase):
def test_integer_quantity(self): def test_integer_quantity(self):
"""Test integer validation for BomItem.""" """Test integer validation for BomItem."""
p = Part.objects.create( p = Part.objects.create(
name="test", description="part description", component=True, trackable=True name='test', description='part description', component=True, trackable=True
) )
# Creation of a BOMItem with a non-integer quantity of a trackable Part should fail # Creation of a BOMItem with a non-integer quantity of a trackable Part should fail
@ -157,8 +157,8 @@ class BomItemTest(TestCase):
for ii in range(5): for ii in range(5):
# Create a new part # Create a new part
sub_part = Part.objects.create( sub_part = Part.objects.create(
name=f"Orphan {ii}", name=f'Orphan {ii}',
description="A substitute part for the orphan part", description='A substitute part for the orphan part',
component=True, component=True,
is_template=False, is_template=False,
assembly=False, assembly=False,
@ -196,7 +196,7 @@ class BomItemTest(TestCase):
"""Tests for the 'consumable' BomItem field""" """Tests for the 'consumable' BomItem field"""
# Create an assembly part # Create an assembly part
assembly = Part.objects.create( assembly = Part.objects.create(
name="An assembly", description="Made with parts", assembly=True name='An assembly', description='Made with parts', assembly=True
) )
# No BOM information initially # No BOM information initially
@ -204,16 +204,16 @@ class BomItemTest(TestCase):
# Create some component items # Create some component items
c1 = Part.objects.create( c1 = Part.objects.create(
name="C1", description="Part C1 - this is just the part description" name='C1', description='Part C1 - this is just the part description'
) )
c2 = Part.objects.create( c2 = Part.objects.create(
name="C2", description="Part C2 - this is just the part description" name='C2', description='Part C2 - this is just the part description'
) )
c3 = Part.objects.create( c3 = Part.objects.create(
name="C3", description="Part C3 - this is just the part description" name='C3', description='Part C3 - this is just the part description'
) )
c4 = Part.objects.create( c4 = Part.objects.create(
name="C4", description="Part C4 - this is just the part description" name='C4', description='Part C4 - this is just the part description'
) )
for p in [c1, c2, c3, c4]: for p in [c1, c2, c3, c4]:
@ -261,20 +261,20 @@ class BomItemTest(TestCase):
# Second test: A recursive BOM # Second test: A recursive BOM
part_a = Part.objects.create( part_a = Part.objects.create(
name='Part A', name='Part A',
description="A part which is called A", description='A part which is called A',
assembly=True, assembly=True,
is_template=True, is_template=True,
component=True, component=True,
) )
part_b = Part.objects.create( part_b = Part.objects.create(
name='Part B', name='Part B',
description="A part which is called B", description='A part which is called B',
assembly=True, assembly=True,
component=True, component=True,
) )
part_c = Part.objects.create( part_c = Part.objects.create(
name='Part C', name='Part C',
description="A part which is called C", description='A part which is called C',
assembly=True, assembly=True,
component=True, component=True,
) )

View File

@ -106,7 +106,7 @@ class CategoryTest(TestCase):
letter = chr(ord('A') + idx) letter = chr(ord('A') + idx)
child = PartCategory.objects.create( child = PartCategory.objects.create(
name=letter * 10, description=f"Subcategory {letter}", parent=parent name=letter * 10, description=f'Subcategory {letter}', parent=parent
) )
parent = child parent = child
@ -114,7 +114,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/KKKKKKKKK...OO/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

@ -45,7 +45,7 @@ class TestForwardMigrations(MigratorTestCase):
for name in ['A', 'C', 'E']: for name in ['A', 'C', 'E']:
part = Part.objects.get(name=name) part = Part.objects.get(name=name)
self.assertEqual(part.description, f"My part {name}") self.assertEqual(part.description, f'My part {name}')
class TestBomItemMigrations(MigratorTestCase): class TestBomItemMigrations(MigratorTestCase):

View File

@ -181,7 +181,7 @@ class ParameterTests(TestCase):
template2 = PartParameterTemplate.objects.create( template2 = PartParameterTemplate.objects.create(
name='My Template 2', units='%' name='My Template 2', units='%'
) )
for value in ["1", "1%", "1 percent"]: for value in ['1', '1%', '1 percent']:
param = PartParameter(part=prt, template=template2, data=value) param = PartParameter(part=prt, template=template2, data=value)
param.full_clean() param.full_clean()

View File

@ -180,7 +180,7 @@ class PartTest(TestCase):
def test_str(self): def test_str(self):
"""Test string representation of a Part""" """Test string representation of a Part"""
p = Part.objects.get(pk=100) p = Part.objects.get(pk=100)
self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it? Yes we can!") self.assertEqual(str(p), 'BOB | Bob | A2 - Can we build it? Yes we can!')
def test_duplicate(self): def test_duplicate(self):
"""Test that we cannot create a "duplicate" Part.""" """Test that we cannot create a "duplicate" Part."""

View File

@ -258,8 +258,8 @@ class PartPricingTests(InvenTreeTestCase):
for ii in range(10): for ii in range(10):
# Create a new part for the BOM # Create a new part for the BOM
sub_part = part.models.Part.objects.create( sub_part = part.models.Part.objects.create(
name=f"Sub Part {ii}", name=f'Sub Part {ii}',
description="A sub part for use in a BOM", description='A sub part for use in a BOM',
component=True, component=True,
assembly=False, assembly=False,
) )
@ -403,7 +403,7 @@ class PartPricingTests(InvenTreeTestCase):
# Create some parts # Create some parts
for ii in range(100): for ii in range(100):
part.models.Part.objects.create( part.models.Part.objects.create(
name=f"Part_{ii}", description="A test part" name=f'Part_{ii}', description='A test part'
) )
# Ensure there is no pricing data # Ensure there is no pricing data
@ -424,7 +424,7 @@ class PartPricingTests(InvenTreeTestCase):
but it pointed to a Part instance which was slated to be deleted inside an atomic transaction. but it pointed to a Part instance which was slated to be deleted inside an atomic transaction.
""" """
p = part.models.Part.objects.create( p = part.models.Part.objects.create(
name="my part", description="my part description", active=False name='my part', description='my part description', active=False
) )
# Create some stock items # Create some stock items

View File

@ -105,9 +105,9 @@ class PartImport(FileManagementFormView):
'part/import_wizard/match_references.html', 'part/import_wizard/match_references.html',
] ]
form_steps_description = [ form_steps_description = [
_("Upload File"), _('Upload File'),
_("Match Fields"), _('Match Fields'),
_("Match References"), _('Match References'),
] ]
form_field_map = { form_field_map = {
@ -540,8 +540,8 @@ class PartPricing(AjaxView):
"""View for inspecting part pricing information.""" """View for inspecting part pricing information."""
model = Part model = Part
ajax_template_name = "part/part_pricing.html" ajax_template_name = 'part/part_pricing.html'
ajax_form_title = _("Part Pricing") ajax_form_title = _('Part Pricing')
form_class = part_forms.PartPriceForm form_class = part_forms.PartPriceForm
role_required = ['sales_order.view', 'part.view'] role_required = ['sales_order.view', 'part.view']

View File

@ -49,7 +49,7 @@ class PluginSettingInline(admin.TabularInline):
class PluginConfigAdmin(admin.ModelAdmin): class PluginConfigAdmin(admin.ModelAdmin):
"""Custom admin with restricted id fields.""" """Custom admin with restricted id fields."""
readonly_fields = ["key", "name"] readonly_fields = ['key', 'name']
list_display = [ list_display = [
'name', 'name',
'key', 'key',

View File

@ -227,7 +227,7 @@ def check_plugin(plugin_slug: str, plugin_pk: int) -> InvenTreePlugin:
""" """
# Make sure that a plugin reference is specified # Make sure that a plugin reference is specified
if plugin_slug is None and plugin_pk is None: if plugin_slug is None and plugin_pk is None:
raise NotFound(detail="Plugin not specified") raise NotFound(detail='Plugin not specified')
# Define filter # Define filter
filter = {} filter = {}
@ -342,13 +342,13 @@ class RegistryStatusView(APIView):
for error_detail in errors: for error_detail in errors:
for name, message in error_detail.items(): for name, message in error_detail.items():
error_list.append({ error_list.append({
"stage": stage, 'stage': stage,
"name": name, 'name': name,
"message": message, 'message': message,
}) })
result = PluginSerializers.PluginRegistryStatusSerializer({ result = PluginSerializers.PluginRegistryStatusSerializer({
"registry_errors": error_list 'registry_errors': error_list
}).data }).data
return Response(result) return Response(result)
@ -382,7 +382,7 @@ plugin_api_urls = [
r'<int:pk>/', r'<int:pk>/',
include([ include([
re_path( re_path(
r"^settings/", r'^settings/',
include([ include([
re_path( re_path(
r'^(?P<key>\w+)/', r'^(?P<key>\w+)/',
@ -390,9 +390,9 @@ plugin_api_urls = [
name='api-plugin-setting-detail-pk', name='api-plugin-setting-detail-pk',
), ),
re_path( re_path(
r"^.*$", r'^.*$',
PluginAllSettingList.as_view(), PluginAllSettingList.as_view(),
name="api-plugin-settings", name='api-plugin-settings',
), ),
]), ]),
), ),
@ -419,9 +419,9 @@ plugin_api_urls = [
), ),
# Registry status # Registry status
re_path( re_path(
r"^status/", r'^status/',
RegistryStatusView.as_view(), RegistryStatusView.as_view(),
name="api-plugin-registry-status", name='api-plugin-registry-status',
), ),
# Anything else # Anything else
re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'), re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'),

View File

@ -28,7 +28,7 @@ class PluginAppConfig(AppConfig):
return return
if not canAppAccessDatabase(allow_test=True, allow_plugins=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

@ -21,7 +21,7 @@ class ActionPluginView(APIView):
data = request.data.get('data', None) data = request.data.get('data', None)
if action is None: if action is None:
return Response({'error': _("No action specified")}) return Response({'error': _('No action specified')})
action_plugins = registry.with_mixin('action') action_plugins = registry.with_mixin('action')
for plugin in action_plugins: for plugin in action_plugins:
@ -30,4 +30,4 @@ class ActionPluginView(APIView):
return Response(plugin.get_response(request.user, data=data)) return Response(plugin.get_response(request.user, data=data))
# If we got to here, no matching action was found # If we got to here, no matching action was found
return Response({'error': _("No matching action found"), "action": action}) return Response({'error': _('No matching action found'), 'action': action})

View File

@ -4,7 +4,7 @@
class ActionMixin: class ActionMixin:
"""Mixin that enables custom actions.""" """Mixin that enables custom actions."""
ACTION_NAME = "" ACTION_NAME = ''
class MixinMeta: class MixinMeta:
"""Meta options for this mixin.""" """Meta options for this mixin."""
@ -47,7 +47,7 @@ class ActionMixin:
Default implementation is a simple response which can be overridden. Default implementation is a simple response which can be overridden.
""" """
return { return {
"action": self.action_name(), 'action': self.action_name(),
"result": self.get_result(user, data), 'result': self.get_result(user, data),
"info": self.get_info(user, data), 'info': self.get_info(user, data),
} }

View File

@ -57,7 +57,7 @@ class ActionMixinTests(TestCase):
self.assertEqual(self.plugin.get_result(), False) self.assertEqual(self.plugin.get_result(), False)
self.assertIsNone(self.plugin.get_info()) self.assertIsNone(self.plugin.get_info())
self.assertEqual( self.assertEqual(
self.plugin.get_response(), {"action": '', "result": False, "info": None} self.plugin.get_response(), {'action': '', 'result': False, 'info': None}
) )
# overridden functions # overridden functions
@ -69,9 +69,9 @@ class ActionMixinTests(TestCase):
self.assertEqual( self.assertEqual(
self.action_plugin.get_response(), self.action_plugin.get_response(),
{ {
"action": 'abc123', 'action': 'abc123',
"result": self.ACTION_RETURN + 'result', 'result': self.ACTION_RETURN + 'result',
"info": self.ACTION_RETURN + 'info', 'info': self.ACTION_RETURN + 'info',
}, },
) )
@ -87,7 +87,7 @@ class APITests(InvenTreeTestCase):
self.assertEqual(response.data, {'error': 'No action specified'}) self.assertEqual(response.data, {'error': 'No action specified'})
# Test non-exsisting action # Test non-exsisting action
response = self.client.post('/api/action/', data={'action': "nonexsisting"}) response = self.client.post('/api/action/', data={'action': 'nonexsisting'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
response.data, response.data,

View File

@ -58,7 +58,7 @@ class BarcodeView(CreateAPIView):
Any custom fields passed by the specific serializer Any custom fields passed by the specific serializer
""" """
raise NotImplementedError( raise NotImplementedError(
f"handle_barcode not implemented for {self.__class__}" f'handle_barcode not implemented for {self.__class__}'
) )
def scan_barcode(self, barcode: str, request, **kwargs): def scan_barcode(self, barcode: str, request, **kwargs):
@ -79,11 +79,11 @@ class BarcodeView(CreateAPIView):
if result is None: if result is None:
continue continue
if "error" in result: if 'error' in result:
logger.info( logger.info(
"%s.scan(...) returned an error: %s", '%s.scan(...) returned an error: %s',
current_plugin.__class__.__name__, current_plugin.__class__.__name__,
result["error"], result['error'],
) )
if not response: if not response:
plugin = current_plugin plugin = current_plugin
@ -155,9 +155,9 @@ class BarcodeAssign(BarcodeView):
result = plugin.scan(barcode) result = plugin.scan(barcode)
if result is not None: if result is not None:
result["error"] = _("Barcode matches existing item") result['error'] = _('Barcode matches existing item')
result["plugin"] = plugin.name result['plugin'] = plugin.name
result["barcode_data"] = barcode result['barcode_data'] = barcode
raise ValidationError(result) raise ValidationError(result)
@ -174,20 +174,20 @@ class BarcodeAssign(BarcodeView):
app_label = model._meta.app_label app_label = model._meta.app_label
model_name = model._meta.model_name model_name = model._meta.model_name
table = f"{app_label}_{model_name}" table = f'{app_label}_{model_name}'
if not RuleSet.check_table_permission(request.user, table, "change"): if not RuleSet.check_table_permission(request.user, table, 'change'):
raise PermissionDenied({ raise PermissionDenied({
"error": f"You do not have the required permissions for {table}" 'error': f'You do not have the required permissions for {table}'
}) })
instance.assign_barcode(barcode_data=barcode, barcode_hash=barcode_hash) instance.assign_barcode(barcode_data=barcode, barcode_hash=barcode_hash)
return Response({ return Response({
'success': f"Assigned barcode to {label} instance", 'success': f'Assigned barcode to {label} instance',
label: {'pk': instance.pk}, label: {'pk': instance.pk},
"barcode_data": barcode, 'barcode_data': barcode,
"barcode_hash": barcode_hash, 'barcode_hash': barcode_hash,
}) })
# If we got here, it means that no valid model types were provided # If we got here, it means that no valid model types were provided
@ -238,11 +238,11 @@ class BarcodeUnassign(BarcodeView):
app_label = model._meta.app_label app_label = model._meta.app_label
model_name = model._meta.model_name model_name = model._meta.model_name
table = f"{app_label}_{model_name}" table = f'{app_label}_{model_name}'
if not RuleSet.check_table_permission(request.user, table, "change"): if not RuleSet.check_table_permission(request.user, table, 'change'):
raise PermissionDenied({ raise PermissionDenied({
"error": f"You do not have the required permissions for {table}" 'error': f'You do not have the required permissions for {table}'
}) })
# Unassign the barcode data from the model instance # Unassign the barcode data from the model instance
@ -313,11 +313,11 @@ class BarcodePOAllocate(BarcodeView):
) )
if supplier_parts.count() == 0: if supplier_parts.count() == 0:
raise ValidationError({"error": _("No matching supplier parts found")}) raise ValidationError({'error': _('No matching supplier parts found')})
if supplier_parts.count() > 1: if supplier_parts.count() > 1:
raise ValidationError({ raise ValidationError({
"error": _("Multiple matching supplier parts found") 'error': _('Multiple matching supplier parts found')
}) })
# At this stage, we have a single matching supplier part # At this stage, we have a single matching supplier part
@ -342,7 +342,7 @@ class BarcodePOAllocate(BarcodeView):
manufacturer_part=result.get('manufacturerpart', None), manufacturer_part=result.get('manufacturerpart', None),
) )
result['success'] = _("Matched supplier part") result['success'] = _('Matched supplier part')
result['supplierpart'] = supplier_part.format_matched_response() result['supplierpart'] = supplier_part.format_matched_response()
# TODO: Determine the 'quantity to order' for the supplier part # TODO: Determine the 'quantity to order' for the supplier part
@ -379,24 +379,24 @@ class BarcodePOReceive(BarcodeView):
purchase_order = kwargs.get('purchase_order', None) purchase_order = kwargs.get('purchase_order', None)
location = kwargs.get('location', None) location = kwargs.get('location', None)
plugins = registry.with_mixin("barcode") plugins = registry.with_mixin('barcode')
# Look for a barcode plugin which knows how to deal with this barcode # Look for a barcode plugin which knows how to deal with this barcode
plugin = None plugin = None
response = {"barcode_data": barcode, "barcode_hash": hash_barcode(barcode)} response = {'barcode_data': barcode, 'barcode_hash': hash_barcode(barcode)}
internal_barcode_plugin = next( internal_barcode_plugin = next(
filter(lambda plugin: plugin.name == "InvenTreeBarcode", plugins) filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins)
) )
if result := internal_barcode_plugin.scan(barcode): if result := internal_barcode_plugin.scan(barcode):
if 'stockitem' in result: if 'stockitem' in result:
response["error"] = _("Item has already been received") response['error'] = _('Item has already been received')
raise ValidationError(response) raise ValidationError(response)
# Now, look just for "supplier-barcode" plugins # Now, look just for "supplier-barcode" plugins
plugins = registry.with_mixin("supplier-barcode") plugins = registry.with_mixin('supplier-barcode')
plugin_response = None plugin_response = None
@ -408,11 +408,11 @@ class BarcodePOReceive(BarcodeView):
if result is None: if result is None:
continue continue
if "error" in result: if 'error' in result:
logger.info( logger.info(
"%s.scan_receive_item(...) returned an error: %s", '%s.scan_receive_item(...) returned an error: %s',
current_plugin.__class__.__name__, current_plugin.__class__.__name__,
result["error"], result['error'],
) )
if not plugin_response: if not plugin_response:
plugin = current_plugin plugin = current_plugin
@ -429,9 +429,9 @@ class BarcodePOReceive(BarcodeView):
# A plugin has not been found! # A plugin has not been found!
if plugin is None: if plugin is None:
response["error"] = _("No match for supplier barcode") response['error'] = _('No match for supplier barcode')
raise ValidationError(response) raise ValidationError(response)
elif "error" in response: elif 'error' in response:
raise ValidationError(response) raise ValidationError(response)
else: else:
return Response(response) return Response(response)
@ -583,11 +583,11 @@ barcode_api_urls = [
# Unlink a third-party barcode from an item # Unlink a third-party barcode from an item
path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'), path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'),
# Receive a purchase order item by scanning its barcode # Receive a purchase order item by scanning its barcode
path("po-receive/", BarcodePOReceive.as_view(), name="api-barcode-po-receive"), path('po-receive/', BarcodePOReceive.as_view(), name='api-barcode-po-receive'),
# Allocate parts to a purchase order by scanning their barcode # Allocate parts to a purchase order by scanning their barcode
path("po-allocate/", BarcodePOAllocate.as_view(), name="api-barcode-po-allocate"), path('po-allocate/', BarcodePOAllocate.as_view(), name='api-barcode-po-allocate'),
# Allocate stock to a sales order by scanning barcode # Allocate stock to a sales order by scanning barcode
path("so-allocate/", BarcodeSOAllocate.as_view(), name="api-barcode-so-allocate"), path('so-allocate/', BarcodeSOAllocate.as_view(), name='api-barcode-so-allocate'),
# Catch-all performs barcode 'scan' # Catch-all performs barcode 'scan'
re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'), re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
] ]

View File

@ -23,7 +23,7 @@ class BarcodeMixin:
Custom barcode plugins should use and extend this mixin as necessary. Custom barcode plugins should use and extend this mixin as necessary.
""" """
ACTION_NAME = "" ACTION_NAME = ''
class MixinMeta: class MixinMeta:
"""Meta options for this mixin.""" """Meta options for this mixin."""
@ -62,19 +62,19 @@ class SupplierBarcodeMixin(BarcodeMixin):
""" """
# Set of standard field names which can be extracted from the barcode # Set of standard field names which can be extracted from the barcode
CUSTOMER_ORDER_NUMBER = "customer_order_number" CUSTOMER_ORDER_NUMBER = 'customer_order_number'
SUPPLIER_ORDER_NUMBER = "supplier_order_number" SUPPLIER_ORDER_NUMBER = 'supplier_order_number'
PACKING_LIST_NUMBER = "packing_list_number" PACKING_LIST_NUMBER = 'packing_list_number'
SHIP_DATE = "ship_date" SHIP_DATE = 'ship_date'
CUSTOMER_PART_NUMBER = "customer_part_number" CUSTOMER_PART_NUMBER = 'customer_part_number'
SUPPLIER_PART_NUMBER = "supplier_part_number" SUPPLIER_PART_NUMBER = 'supplier_part_number'
PURCHASE_ORDER_LINE = "purchase_order_line" PURCHASE_ORDER_LINE = 'purchase_order_line'
QUANTITY = "quantity" QUANTITY = 'quantity'
DATE_CODE = "date_code" DATE_CODE = 'date_code'
LOT_CODE = "lot_code" LOT_CODE = 'lot_code'
COUNTRY_OF_ORIGIN = "country_of_origin" COUNTRY_OF_ORIGIN = 'country_of_origin'
MANUFACTURER = "manufacturer" MANUFACTURER = 'manufacturer'
MANUFACTURER_PART_NUMBER = "manufacturer_part_number" MANUFACTURER_PART_NUMBER = 'manufacturer_part_number'
def __init__(self): def __init__(self):
"""Register mixin.""" """Register mixin."""
@ -83,7 +83,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
def get_field_value(self, key, backup_value=None): def get_field_value(self, key, backup_value=None):
"""Return the value of a barcode field.""" """Return the value of a barcode field."""
fields = getattr(self, "barcode_fields", None) or {} fields = getattr(self, 'barcode_fields', None) or {}
return fields.get(key, backup_value) return fields.get(key, backup_value)
@ -125,7 +125,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
""" """
raise NotImplementedError( raise NotImplementedError(
"extract_barcode_fields must be implemented by each plugin" 'extract_barcode_fields must be implemented by each plugin'
) )
def scan(self, barcode_data): def scan(self, barcode_data):
@ -145,16 +145,16 @@ class SupplierBarcodeMixin(BarcodeMixin):
) )
if len(supplier_parts) > 1: if len(supplier_parts) > 1:
return {"error": _("Found multiple matching supplier parts for barcode")} return {'error': _('Found multiple matching supplier parts for barcode')}
elif not supplier_parts: elif not supplier_parts:
return None return None
supplier_part = supplier_parts[0] supplier_part = supplier_parts[0]
data = { data = {
"pk": supplier_part.pk, 'pk': supplier_part.pk,
"api_url": f"{SupplierPart.get_api_url()}{supplier_part.pk}/", 'api_url': f'{SupplierPart.get_api_url()}{supplier_part.pk}/',
"web_url": supplier_part.get_absolute_url(), 'web_url': supplier_part.get_absolute_url(),
} }
return {SupplierPart.barcode_model_type(): data} return {SupplierPart.barcode_model_type(): data}
@ -178,7 +178,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
) )
if len(supplier_parts) > 1: if len(supplier_parts) > 1:
return {"error": _("Found multiple matching supplier parts for barcode")} return {'error': _('Found multiple matching supplier parts for barcode')}
elif not supplier_parts: elif not supplier_parts:
return None return None
@ -196,17 +196,17 @@ class SupplierBarcodeMixin(BarcodeMixin):
if len(matching_orders) > 1: if len(matching_orders) > 1:
return { return {
"error": _(f"Found multiple purchase orders matching '{order}'") 'error': _(f"Found multiple purchase orders matching '{order}'")
} }
if len(matching_orders) == 0: if len(matching_orders) == 0:
return {"error": _(f"No matching purchase order for '{order}'")} return {'error': _(f"No matching purchase order for '{order}'")}
purchase_order = matching_orders.first() purchase_order = matching_orders.first()
if supplier and purchase_order: if supplier and purchase_order:
if purchase_order.supplier != supplier: if purchase_order.supplier != supplier:
return {"error": _("Purchase order does not match supplier")} return {'error': _('Purchase order does not match supplier')}
return self.receive_purchase_order_item( return self.receive_purchase_order_item(
supplier_part, supplier_part,
@ -226,17 +226,17 @@ class SupplierBarcodeMixin(BarcodeMixin):
if not isinstance(self, SettingsMixin): if not isinstance(self, SettingsMixin):
return None return None
if supplier_pk := self.get_setting("SUPPLIER_ID"): if supplier_pk := self.get_setting('SUPPLIER_ID'):
if supplier := Company.objects.get(pk=supplier_pk): if supplier := Company.objects.get(pk=supplier_pk):
return supplier return supplier
else: else:
logger.error( logger.error(
"No company with pk %d (set \"SUPPLIER_ID\" setting to a valid value)", 'No company with pk %d (set "SUPPLIER_ID" setting to a valid value)',
supplier_pk, supplier_pk,
) )
return None return None
if not (supplier_name := getattr(self, "DEFAULT_SUPPLIER_NAME", None)): if not (supplier_name := getattr(self, 'DEFAULT_SUPPLIER_NAME', None)):
return None return None
suppliers = Company.objects.filter( suppliers = Company.objects.filter(
@ -246,7 +246,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
if len(suppliers) != 1: if len(suppliers) != 1:
return None return None
self.set_setting("SUPPLIER_ID", suppliers.first().pk) self.set_setting('SUPPLIER_ID', suppliers.first().pk)
return suppliers.first() return suppliers.first()
@ -260,21 +260,21 @@ class SupplierBarcodeMixin(BarcodeMixin):
if it does not use the standard field names. if it does not use the standard field names.
""" """
return { return {
"K": cls.CUSTOMER_ORDER_NUMBER, 'K': cls.CUSTOMER_ORDER_NUMBER,
"1K": cls.SUPPLIER_ORDER_NUMBER, '1K': cls.SUPPLIER_ORDER_NUMBER,
"11K": cls.PACKING_LIST_NUMBER, '11K': cls.PACKING_LIST_NUMBER,
"6D": cls.SHIP_DATE, '6D': cls.SHIP_DATE,
"9D": cls.DATE_CODE, '9D': cls.DATE_CODE,
"10D": cls.DATE_CODE, '10D': cls.DATE_CODE,
"4K": cls.PURCHASE_ORDER_LINE, '4K': cls.PURCHASE_ORDER_LINE,
"14K": cls.PURCHASE_ORDER_LINE, '14K': cls.PURCHASE_ORDER_LINE,
"P": cls.SUPPLIER_PART_NUMBER, 'P': cls.SUPPLIER_PART_NUMBER,
"1P": cls.MANUFACTURER_PART_NUMBER, '1P': cls.MANUFACTURER_PART_NUMBER,
"30P": cls.SUPPLIER_PART_NUMBER, '30P': cls.SUPPLIER_PART_NUMBER,
"1T": cls.LOT_CODE, '1T': cls.LOT_CODE,
"4L": cls.COUNTRY_OF_ORIGIN, '4L': cls.COUNTRY_OF_ORIGIN,
"1V": cls.MANUFACTURER, '1V': cls.MANUFACTURER,
"Q": cls.QUANTITY, 'Q': cls.QUANTITY,
} }
@classmethod @classmethod
@ -324,10 +324,10 @@ class SupplierBarcodeMixin(BarcodeMixin):
def parse_isoiec_15434_barcode2d(barcode_data: str) -> list[str]: def parse_isoiec_15434_barcode2d(barcode_data: str) -> list[str]:
"""Parse a ISO/IEC 15434 barcode, returning the split data section.""" """Parse a ISO/IEC 15434 barcode, returning the split data section."""
OLD_MOUSER_HEADER = ">[)>06\x1d" OLD_MOUSER_HEADER = '>[)>06\x1d'
HEADER = "[)>\x1e06\x1d" HEADER = '[)>\x1e06\x1d'
TRAILER = "\x1e\x04" TRAILER = '\x1e\x04'
DELIMITER = "\x1d" DELIMITER = '\x1d'
# Some old mouser barcodes start with this messed up header # Some old mouser barcodes start with this messed up header
if barcode_data.startswith(OLD_MOUSER_HEADER): if barcode_data.startswith(OLD_MOUSER_HEADER):
@ -419,7 +419,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
# find incomplete line_items that match the supplier_part # find incomplete line_items that match the supplier_part
line_items = purchase_order.lines.filter( line_items = purchase_order.lines.filter(
part=supplier_part.pk, quantity__gt=F("received") part=supplier_part.pk, quantity__gt=F('received')
) )
if len(line_items) == 1 or not quantity: if len(line_items) == 1 or not quantity:
line_item = line_items[0] line_item = line_items[0]
@ -439,7 +439,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
line_item = line_items.first() line_item = line_items.first()
if not line_item: if not line_item:
return {"error": _("Failed to find pending line item for supplier part")} return {'error': _('Failed to find pending line item for supplier part')}
no_stock_locations = False no_stock_locations = False
if not location: if not location:
@ -457,20 +457,20 @@ class SupplierBarcodeMixin(BarcodeMixin):
no_stock_locations = True no_stock_locations = True
response = { response = {
"lineitem": {"pk": line_item.pk, "purchase_order": purchase_order.pk} 'lineitem': {'pk': line_item.pk, 'purchase_order': purchase_order.pk}
} }
if quantity: if quantity:
response["lineitem"]["quantity"] = quantity response['lineitem']['quantity'] = quantity
if location: if location:
response["lineitem"]["location"] = location.pk response['lineitem']['location'] = location.pk
# if either the quantity is missing or no location is defined/found # if either the quantity is missing or no location is defined/found
# -> return the line_item found, so the client can gather the missing # -> return the line_item found, so the client can gather the missing
# information and complete the action with an 'api-po-receive' call # information and complete the action with an 'api-po-receive' call
if not quantity or (not location and not no_stock_locations): if not quantity or (not location and not no_stock_locations):
response["action_required"] = _( response['action_required'] = _(
"Further information required to receive line item" 'Further information required to receive line item'
) )
return response return response
@ -478,5 +478,5 @@ class SupplierBarcodeMixin(BarcodeMixin):
line_item, location, quantity, user, barcode=barcode line_item, location, quantity, user, barcode=barcode
) )
response["success"] = _("Received purchase order line item") response['success'] = _('Received purchase order line item')
return response return response

View File

@ -86,7 +86,7 @@ class BarcodePOAllocateSerializer(BarcodeSerializer):
"""Validate the provided order""" """Validate the provided order"""
if order.status != PurchaseOrderStatus.PENDING.value: if order.status != PurchaseOrderStatus.PENDING.value:
raise ValidationError(_("Purchase order is not pending")) raise ValidationError(_('Purchase order is not pending'))
return order return order
@ -111,7 +111,7 @@ class BarcodePOReceiveSerializer(BarcodeSerializer):
"""Validate the provided order""" """Validate the provided order"""
if order and order.status != PurchaseOrderStatus.PLACED.value: if order and order.status != PurchaseOrderStatus.PLACED.value:
raise ValidationError(_("Purchase order has not been placed")) raise ValidationError(_('Purchase order has not been placed'))
return order return order
@ -126,7 +126,7 @@ class BarcodePOReceiveSerializer(BarcodeSerializer):
"""Validate the provided location""" """Validate the provided location"""
if location and location.structural: if location and location.structural:
raise ValidationError(_("Cannot select a structural location")) raise ValidationError(_('Cannot select a structural location'))
return location return location
@ -147,7 +147,7 @@ class BarcodeSOAllocateSerializer(BarcodeSerializer):
"""Validate the provided order""" """Validate the provided order"""
if order and order.status != SalesOrderStatus.PENDING.value: if order and order.status != SalesOrderStatus.PENDING.value:
raise ValidationError(_("Sales order is not pending")) raise ValidationError(_('Sales order is not pending'))
return order return order
@ -169,7 +169,7 @@ class BarcodeSOAllocateSerializer(BarcodeSerializer):
"""Validate the provided shipment""" """Validate the provided shipment"""
if shipment and shipment.is_delivered(): if shipment and shipment.is_delivered():
raise ValidationError(_("Shipment has already been delivered")) raise ValidationError(_('Shipment has already been delivered'))
return shipment return shipment

Some files were not shown because too many files have changed in this diff Show More