remove unneeded branches

fixes SIM102
This commit is contained in:
Matthias Mair 2024-08-20 01:17:35 +02:00
parent 372e3d417e
commit f74d41bc07
No known key found for this signature in database
GPG Key ID: A593429DDA23B66A
24 changed files with 289 additions and 258 deletions

View File

@ -115,10 +115,9 @@ def exception_handler(exc, context):
log_error(context['request'].path) log_error(context['request'].path)
if response is not None: # Convert errors returned under the label '__all__' to 'non_field_errors'
# Convert errors returned under the label '__all__' to 'non_field_errors' if response is not None and '__all__' in response.data:
if '__all__' in response.data: response.data['non_field_errors'] = response.data['__all__']
response.data['non_field_errors'] = response.data['__all__'] del response.data['__all__']
del response.data['__all__']
return response return response

View File

@ -184,9 +184,12 @@ def getSplashScreen(custom=True):
"""Return the InvenTree splash screen, or a custom splash if available.""" """Return the InvenTree splash screen, or a custom splash if available."""
static_storage = StaticFilesStorage() static_storage = StaticFilesStorage()
if custom and settings.CUSTOM_SPLASH: if (
if static_storage.exists(settings.CUSTOM_SPLASH): custom
return static_storage.url(settings.CUSTOM_SPLASH) and settings.CUSTOM_SPLASH
and static_storage.exists(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')

View File

@ -34,10 +34,9 @@ class Command(BaseCommand):
if x and x.path: if x and x.path:
img_paths.append(x.path) img_paths.append(x.path)
if len(img_paths) > 0: if len(img_paths) > 0 and all(os.path.exists(path) for path in img_paths):
if all(os.path.exists(path) for path in img_paths): # All images exist - skip further work
# All images exist - skip further work return
return
logger.info("Generating thumbnail image for '%s'", img) logger.info("Generating thumbnail image for '%s'", img)

View File

@ -28,10 +28,12 @@ def get_token_from_request(request):
if auth_header := request.headers.get(k, None): if auth_header := request.headers.get(k, None):
auth_header = auth_header.strip().lower().split() auth_header = auth_header.strip().lower().split()
if len(auth_header) > 1: if (
if auth_header[0].strip().lower().replace(':', '') in token_keys: len(auth_header) > 1
token = auth_header[1] and auth_header[0].strip().lower().replace(':', '') in token_keys
return token ):
token = auth_header[1]
return token
return None return None

View File

@ -63,11 +63,14 @@ def init_sentry(dsn, sample_rate, tags):
def report_exception(exc): 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 (
if not any(isinstance(exc, e) for e in sentry_ignore_errors()): settings.SENTRY_ENABLED
logger.info('Reporting exception to sentry.io: %s', exc) and settings.SENTRY_DSN
and not any(isinstance(exc, e) for e in sentry_ignore_errors())
):
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

@ -50,11 +50,10 @@ def check_provider(provider):
if not app: if not app:
return False return False
if allauth.app_settings.SITES_ENABLED: # At least one matching site must be specified
# At least one matching site must be specified if allauth.app_settings.SITES_ENABLED and 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
return True return True

View File

@ -136,12 +136,11 @@ def inventree_in_debug_mode(*args, **kwargs):
@register.simple_tag() @register.simple_tag()
def inventree_show_about(user, *args, **kwargs): def inventree_show_about(user, *args, **kwargs):
"""Return True if the about modal should be shown.""" """Return True if the about modal should be shown."""
if get_global_setting('INVENTREE_RESTRICT_ABOUT'): # Return False if the user is not a superuser, or no user information is provided
# Return False if the user is not a superuser, or no user information is provided return not (
if not user or not user.is_superuser: (get_global_setting('INVENTREE_RESTRICT_ABOUT') and not user)
return False or not user.is_superuser
)
return True
@register.simple_tag() @register.simple_tag()

View File

@ -69,11 +69,10 @@ class AllowedURLValidator(validators.URLValidator):
# Determine if 'strict' URL validation is required (i.e. if the URL must have a schema prefix) # Determine if 'strict' URL validation is required (i.e. if the URL must have a schema prefix)
strict_urls = get_global_setting('INVENTREE_STRICT_URLS', cache=False) strict_urls = get_global_setting('INVENTREE_STRICT_URLS', cache=False)
if not strict_urls: # Allow URLs which do not have a provided schema
# Allow URLs which do not have a provided schema if not strict_urls and '://' not in value:
if '://' not in value: # Validate as if it were http
# Validate as if it were http value = 'http://' + value
value = 'http://' + value
super().__call__(value) super().__call__(value)

View File

@ -426,20 +426,21 @@ class SetPasswordView(AjaxUpdateView):
old_password = request.POST.get('old_password', '') old_password = request.POST.get('old_password', '')
user = self.request.user user = self.request.user
if valid: # Passwords must match
# Passwords must match if valid and p1 != p2:
error = _('Password fields must match')
form.add_error('enter_password', error)
form.add_error('confirm_password', error)
valid = False
if p1 != p2: # Old password must be correct
error = _('Password fields must match') if (
form.add_error('enter_password', error) valid
form.add_error('confirm_password', error) and user.has_usable_password()
valid = False and not user.check_password(old_password)
):
if valid: form.add_error('old_password', _('Wrong password provided'))
# Old password must be correct valid = False
if user.has_usable_password() and not user.check_password(old_password):
form.add_error('old_password', _('Wrong password provided'))
valid = False
if valid: if valid:
try: try:

View File

@ -444,11 +444,10 @@ class SettingsTest(InvenTreeTestCase):
# Any fields marked as 'boolean' must have a default value specified # Any fields marked as 'boolean' must have a default value specified
setting = InvenTreeSetting.get_setting_object(key) setting = InvenTreeSetting.get_setting_object(key)
if setting.is_bool(): if setting.is_bool() and setting.default_value not in [True, False]:
if setting.default_value not in [True, False]: raise ValueError(
raise ValueError( f'Non-boolean default value specified for {key}'
f'Non-boolean default value specified for {key}' ) # pragma: no cover
) # pragma: no cover
def test_global_setting_caching(self): def test_global_setting_caching(self):
"""Test caching operations for the global settings class.""" """Test caching operations for the global settings class."""

View File

@ -730,13 +730,16 @@ class SupplierPart(
raise ValidationError({'pack_quantity': e.messages}) raise ValidationError({'pack_quantity': e.messages})
# Ensure that the linked manufacturer_part points to the same part! # Ensure that the linked manufacturer_part points to the same part!
if self.manufacturer_part and self.part: if (
if self.manufacturer_part.part != self.part: self.manufacturer_part
raise ValidationError({ and self.part
'manufacturer_part': _( and self.manufacturer_part.part != self.part
'Linked manufacturer part must reference the same base part' ):
) raise ValidationError({
}) 'manufacturer_part': _(
'Linked manufacturer part must reference the same base part'
)
})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Overriding save method to connect an existing ManufacturerPart.""" """Overriding save method to connect an existing ManufacturerPart."""
@ -1049,9 +1052,13 @@ class SupplierPriceBreak(common.models.PriceBreak):
) )
def after_save_supplier_price(sender, instance, created, **kwargs): def after_save_supplier_price(sender, instance, created, **kwargs):
"""Callback function when a SupplierPriceBreak is created or updated.""" """Callback function when a SupplierPriceBreak is created or updated."""
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): if (
if instance.part and instance.part.part: InvenTree.ready.canAppAccessDatabase()
instance.part.part.schedule_pricing_update(create=True) and not InvenTree.ready.isImportingData()
and instance.part
and instance.part.part
):
instance.part.part.schedule_pricing_update(create=True)
@receiver( @receiver(
@ -1061,6 +1068,10 @@ def after_save_supplier_price(sender, instance, created, **kwargs):
) )
def after_delete_supplier_price(sender, instance, **kwargs): def after_delete_supplier_price(sender, instance, **kwargs):
"""Callback function when a SupplierPriceBreak is deleted.""" """Callback function when a SupplierPriceBreak is deleted."""
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): if (
if instance.part and instance.part.part: InvenTree.ready.canAppAccessDatabase()
instance.part.part.schedule_pricing_update(create=False) and not InvenTree.ready.isImportingData()
and instance.part
and instance.part.part
):
instance.part.part.schedule_pricing_update(create=False)

View File

@ -1350,15 +1350,13 @@ class OrderCalendarExport(ICalFeed):
# No login yet - check in headers # No login yet - check in headers
if 'authorization' in request.headers: if 'authorization' in request.headers:
auth = request.headers['authorization'].split() auth = request.headers['authorization'].split()
if len(auth) == 2: # NOTE: We are only support basic authentication for now.
# NOTE: We are only support basic authentication for now. if len(auth) == 2 and auth[0].lower() == 'basic':
# uname, passwd = base64.b64decode(auth[1]).decode('ascii').split(':')
if auth[0].lower() == 'basic': user = authenticate(username=uname, password=passwd)
uname, passwd = base64.b64decode(auth[1]).decode('ascii').split(':') if user is not None and user.is_active:
user = authenticate(username=uname, password=passwd) login(request, user)
if user is not None and user.is_active: request.user = user
login(request, user)
request.user = user
# Check again # Check again
if request.user.is_authenticated: if request.user.is_authenticated:

View File

@ -232,19 +232,20 @@ class Order(
super().clean() super().clean()
# Check if a responsible owner is required for this order type # Check if a responsible owner is required for this order type
if self.REQUIRE_RESPONSIBLE_SETTING: if (
if get_global_setting(self.REQUIRE_RESPONSIBLE_SETTING, backup_value=False): self.REQUIRE_RESPONSIBLE_SETTING
if not self.responsible: and get_global_setting(self.REQUIRE_RESPONSIBLE_SETTING, backup_value=False)
raise ValidationError({ and not self.responsible
'responsible': _('Responsible user or group must be specified') ):
}) raise ValidationError({
'responsible': _('Responsible user or group must be specified')
})
# Check that the referenced 'contact' matches the correct 'company' # Check that the referenced 'contact' matches the correct 'company'
if self.company and self.contact: if self.company and self.contact and 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') })
})
def report_context(self): def report_context(self):
"""Generate context data for the reporting interface.""" """Generate context data for the reporting interface."""
@ -783,10 +784,9 @@ class PurchaseOrder(TotalPriceMixin, Order):
# Extract optional packaging field # Extract optional packaging field
packaging = kwargs.get('packaging', None) packaging = kwargs.get('packaging', None)
if not packaging: # Default to the packaging field for the linked supplier part
# Default to the packaging field for the linked supplier part if not packaging and line.part:
if line.part: packaging = line.part.packaging
packaging = line.part.packaging
# Extract optional barcode field # Extract optional barcode field
barcode = kwargs.get('barcode', None) barcode = kwargs.get('barcode', None)
@ -866,10 +866,11 @@ class PurchaseOrder(TotalPriceMixin, Order):
line.save() line.save()
# Has this order been completed? # Has this order been completed?
if len(self.pending_line_items()) == 0: if len(self.pending_line_items()) == 0 and get_global_setting(
if get_global_setting('PURCHASEORDER_AUTO_COMPLETE', True): 'PURCHASEORDER_AUTO_COMPLETE', True
self.received_by = user ):
self.complete_order() # This will save the model self.received_by = user
self.complete_order() # This will save the model
# Issue a notification to interested parties, that this order has been "updated" # Issue a notification to interested parties, that this order has been "updated"
notify_responsible( notify_responsible(
@ -1455,10 +1456,13 @@ class PurchaseOrderLineItem(OrderLineItem):
""" """
super().clean() super().clean()
if self.order.supplier and self.part: # Supplier part *must* point to the same supplier!
# Supplier part *must* point to the same supplier! if (
if self.part.supplier != self.order.supplier: self.order.supplier
raise ValidationError({'part': _('Supplier part must match supplier')}) and self.part
and self.part.supplier != self.order.supplier
):
raise ValidationError({'part': _('Supplier part must match supplier')})
def __str__(self): def __str__(self):
"""Render a string representation of a PurchaseOrderLineItem instance.""" """Render a string representation of a PurchaseOrderLineItem instance."""

View File

@ -91,9 +91,10 @@ def ExportBom(
bom_items.append(item) bom_items.append(item)
if cascade and item.sub_part.assembly: if (
if max_levels is None or level < max_levels: cascade and item.sub_part.assembly and max_levels is None
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1) ) or level < max_levels:
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
top_level_items = part.get_bom_items().order_by('id') top_level_items = part.get_bom_items().order_by('id')

View File

@ -515,11 +515,13 @@ class Part(
if self.active: if self.active:
raise ValidationError(_('Cannot delete this part as it is still active')) raise ValidationError(_('Cannot delete this part as it is still active'))
if not get_global_setting('PART_ALLOW_DELETE_FROM_ASSEMBLY', cache=False): if (
if BomItem.objects.filter(sub_part=self).exists(): not get_global_setting('PART_ALLOW_DELETE_FROM_ASSEMBLY', cache=False)
raise ValidationError( and BomItem.objects.filter(sub_part=self).exists()
_('Cannot delete this part as it is used in an assembly') ):
) raise ValidationError(
_('Cannot delete this part as it is used in an assembly')
)
super().delete() super().delete()
@ -711,13 +713,12 @@ class Part(
'revision': _('Revision code must be specified') 'revision': _('Revision code must be specified')
}) })
if get_global_setting('PART_REVISION_ASSEMBLY_ONLY'): if (
if not self.assembly or not self.revision_of.assembly: get_global_setting('PART_REVISION_ASSEMBLY_ONLY') and not self.assembly
raise ValidationError({ ) or not self.revision_of.assembly:
'revision_of': _( raise ValidationError({
'Revisions are only allowed for assembly parts' 'revision_of': _('Revisions are only allowed for assembly parts')
) })
})
# Cannot have a revision of a "template" part # Cannot have a revision of a "template" part
if self.revision_of.is_template: if self.revision_of.is_template:
@ -2753,13 +2754,15 @@ class PartPricing(common.models.MetaMixin):
sub_part_min = self.convert(sub_part_pricing.overall_min) sub_part_min = self.convert(sub_part_pricing.overall_min)
sub_part_max = self.convert(sub_part_pricing.overall_max) sub_part_max = self.convert(sub_part_pricing.overall_max)
if sub_part_min is not None: if (
if bom_item_min is None or sub_part_min < bom_item_min: sub_part_min is not None and bom_item_min is None
bom_item_min = sub_part_min ) or sub_part_min < bom_item_min:
bom_item_min = sub_part_min
if sub_part_max is not None: if (
if bom_item_max is None or sub_part_max > bom_item_max: sub_part_max is not None and bom_item_max is None
bom_item_max = sub_part_max ) or sub_part_max > bom_item_max:
bom_item_max = sub_part_max
# Update cumulative totals # Update cumulative totals
if bom_item_min is not None: if bom_item_min is not None:
@ -2962,13 +2965,11 @@ class PartPricing(common.models.MetaMixin):
v_min = self.convert(v.pricing.overall_min) v_min = self.convert(v.pricing.overall_min)
v_max = self.convert(v.pricing.overall_max) v_max = self.convert(v.pricing.overall_max)
if v_min is not None: if (v_min is not None and variant_min is None) or v_min < variant_min:
if variant_min is None or v_min < variant_min: variant_min = v_min
variant_min = v_min
if v_max is not None: if (v_max is not None and variant_max is None) or v_max > variant_max:
if variant_max is None or v_max > variant_max: variant_max = v_max
variant_max = v_max
if self.variant_cost_min != variant_min or self.variant_cost_max != variant_max: if self.variant_cost_min != variant_min or self.variant_cost_max != variant_max:
self.price_modified = True self.price_modified = True
@ -3800,12 +3801,15 @@ def post_save_part_parameter_template(sender, instance, created, **kwargs):
"""Callback function when a PartParameterTemplate is created or saved.""" """Callback function when a PartParameterTemplate is created or saved."""
import part.tasks as part_tasks import part.tasks as part_tasks
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): if (
if not created: InvenTree.ready.canAppAccessDatabase()
# Schedule a background task to rebuild the parameters against this template and not InvenTree.ready.isImportingData()
InvenTree.tasks.offload_task( and not created
part_tasks.rebuild_parameters, instance.pk, force_async=True ):
) # Schedule a background task to rebuild the parameters against this template
InvenTree.tasks.offload_task(
part_tasks.rebuild_parameters, instance.pk, force_async=True
)
class PartParameter(InvenTree.models.InvenTreeMetadataModel): class PartParameter(InvenTree.models.InvenTreeMetadataModel):
@ -3922,11 +3926,14 @@ class PartParameter(InvenTree.models.InvenTreeMetadataModel):
except ValueError: except ValueError:
self.data_numeric = None self.data_numeric = None
if self.data_numeric is not None and type(self.data_numeric) is float: # Prevent out of range numbers, etc
# Prevent out of range numbers, etc # Ref: https://github.com/inventree/InvenTree/issues/7593
# Ref: https://github.com/inventree/InvenTree/issues/7593 if (
if math.isnan(self.data_numeric) or math.isinf(self.data_numeric): self.data_numeric is not None
self.data_numeric = None and type(self.data_numeric) is float
and math.isnan(self.data_numeric)
) or math.isinf(self.data_numeric):
self.data_numeric = None
part = models.ForeignKey( part = models.ForeignKey(
Part, Part,
@ -4509,9 +4516,12 @@ def update_bom_build_lines(sender, instance, created, **kwargs):
def update_pricing_after_edit(sender, instance, created, **kwargs): def update_pricing_after_edit(sender, instance, created, **kwargs):
"""Callback function when a part price break is created or updated.""" """Callback function when a part price break is created or updated."""
# Update part pricing *unless* we are importing data # Update part pricing *unless* we are importing data
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): if (
if instance.part: InvenTree.ready.canAppAccessDatabase()
instance.part.schedule_pricing_update(create=True) and not InvenTree.ready.isImportingData()
and instance.part
):
instance.part.schedule_pricing_update(create=True)
@receiver(post_delete, sender=BomItem, dispatch_uid='post_delete_bom_item') @receiver(post_delete, sender=BomItem, dispatch_uid='post_delete_bom_item')
@ -4526,9 +4536,12 @@ def update_pricing_after_edit(sender, instance, created, **kwargs):
def update_pricing_after_delete(sender, instance, **kwargs): def update_pricing_after_delete(sender, instance, **kwargs):
"""Callback function when a part price break is deleted.""" """Callback function when a part price break is deleted."""
# Update part pricing *unless* we are importing data # Update part pricing *unless* we are importing data
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): if (
if instance.part: InvenTree.ready.canAppAccessDatabase()
instance.part.schedule_pricing_update(create=False) and not InvenTree.ready.isImportingData()
and instance.part
):
instance.part.schedule_pricing_update(create=False)
class BomItemSubstitute(InvenTree.models.InvenTreeMetadataModel): class BomItemSubstitute(InvenTree.models.InvenTreeMetadataModel):

View File

@ -563,10 +563,13 @@ class BarcodeSOAllocate(BarcodeView):
sales_order = kwargs['sales_order'] sales_order = kwargs['sales_order']
shipment = self.get_shipment(**kwargs) shipment = self.get_shipment(**kwargs)
if stock_item is not None and line_item is not None: if (
if stock_item.part != line_item.part: stock_item is not None
result['error'] = _('Stock item does not match line item') and line_item is not None
raise ValidationError(result) and stock_item.part != line_item.part
):
result['error'] = _('Stock item does not match line item')
raise ValidationError(result)
quantity = kwargs.get('quantity', None) quantity = kwargs.get('quantity', None)
@ -587,10 +590,13 @@ class BarcodeSOAllocate(BarcodeView):
'quantity': quantity, 'quantity': quantity,
} }
if stock_item is not None and quantity is not None: if (
if stock_item.unallocated_quantity() < quantity: stock_item is not None
response['error'] = _('Insufficient stock available') and quantity is not None
raise ValidationError(response) and stock_item.unallocated_quantity() < quantity
):
response['error'] = _('Insufficient stock available')
raise ValidationError(response)
# If we have sufficient information, we can allocate the stock item # If we have sufficient information, we can allocate the stock item
if all(x is not None for x in [line_item, sales_order, shipment, quantity]): if all(x is not None for x in [line_item, sales_order, shipment, quantity]):

View File

@ -477,9 +477,10 @@ class SupplierBarcodeMixin(BarcodeMixin):
location := supplier_part.part.get_default_location() location := supplier_part.part.get_default_location()
): ):
pass pass
elif StockLocation.objects.count() <= 1: elif StockLocation.objects.count() <= 1 and not (
if not (location := StockLocation.objects.first()): location := StockLocation.objects.first()
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}

View File

@ -195,15 +195,13 @@ class PluginsRegistry:
for plugin in self.plugins.values(): for plugin in self.plugins.values():
if plugin.mixin_enabled(mixin): if plugin.mixin_enabled(mixin):
if active is not None: # Filter by 'active' status of plugin
# Filter by 'active' status of plugin if active is not None and active != plugin.is_active():
if active != plugin.is_active(): continue
continue
if builtin is not None: # Filter by 'builtin' status of plugin
# Filter by 'builtin' status of plugin if builtin is not None and builtin != plugin.is_builtin:
if builtin != plugin.is_builtin: continue
continue
result.append(plugin) result.append(plugin)
@ -396,21 +394,20 @@ class PluginsRegistry:
collected_plugins.append(item) collected_plugins.append(item)
# From this point any plugins are considered "external" and only loaded if plugins are explicitly enabled # From this point any plugins are considered "external" and only loaded if plugins are explicitly enabled
if settings.PLUGINS_ENABLED: # Check if not running in testing mode and apps should be loaded from hooks
# Check if not running in testing mode and apps should be loaded from hooks if (settings.PLUGINS_ENABLED and (not settings.PLUGIN_TESTING)) or (
if (not settings.PLUGIN_TESTING) or ( settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP
settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP ):
): # Collect plugins from setup entry points
# Collect plugins from setup entry points for entry in get_entrypoints():
for entry in get_entrypoints(): try:
try: plugin = entry.load()
plugin = entry.load() plugin.is_package = True
plugin.is_package = True plugin.package_name = getattr(entry.dist, 'name', None)
plugin.package_name = getattr(entry.dist, 'name', None) plugin._get_package_metadata()
plugin._get_package_metadata() collected_plugins.append(plugin)
collected_plugins.append(plugin) except Exception as error: # pragma: no cover
except Exception as error: # pragma: no cover handle_error(error, do_raise=False, log_name='discovery')
handle_error(error, do_raise=False, log_name='discovery')
# Log collected plugins # Log collected plugins
logger.info('Collected %s plugins', len(collected_plugins)) logger.info('Collected %s plugins', len(collected_plugins))

View File

@ -66,24 +66,26 @@ class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
# Print debug message to console (intentional) # Print debug message to console (intentional)
print('Validating model instance:', instance.__class__, f'<{instance.pk}>') print('Validating model instance:', instance.__class__, f'<{instance.pk}>')
if isinstance(instance, part.models.BomItem): if (
if self.get_setting('BOM_ITEM_INTEGER'): isinstance(instance, part.models.BomItem)
if float(instance.quantity) != int(instance.quantity): and self.get_setting('BOM_ITEM_INTEGER')
self.raise_error({ and float(instance.quantity) != int(instance.quantity)
'quantity': 'Bom item quantity must be an integer' ):
}) self.raise_error({'quantity': 'Bom item quantity must be an integer'})
if isinstance(instance, part.models.Part): # If the part description is being updated, prevent it from being reduced in length
# If the part description is being updated, prevent it from being reduced in length if (
isinstance(instance, part.models.Part)
and deltas
and 'description' in deltas
):
old_desc = deltas['description']['old']
new_desc = deltas['description']['new']
if deltas and 'description' in deltas: if len(new_desc) < len(old_desc):
old_desc = deltas['description']['old'] self.raise_error({
new_desc = deltas['description']['new'] 'description': 'Part description cannot be shortened'
})
if len(new_desc) < len(old_desc):
self.raise_error({
'description': 'Part description cannot be shortened'
})
def validate_part_name(self, name: str, part): def validate_part_name(self, name: str, part):
"""Custom validation for Part name field. """Custom validation for Part name field.
@ -126,14 +128,12 @@ class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
These examples are silly, but serve to demonstrate how the feature could be used These examples are silly, but serve to demonstrate how the feature could be used
""" """
if self.get_setting('SERIAL_MUST_BE_PALINDROME'): if self.get_setting('SERIAL_MUST_BE_PALINDROME') and serial != serial[::-1]:
if serial != serial[::-1]: self.raise_error('Serial must be a palindrome')
self.raise_error('Serial must be a palindrome')
if self.get_setting('SERIAL_MUST_MATCH_PART'): # Serial must start with the same letter as the linked part, for some reason
# Serial must start with the same letter as the linked part, for some reason if self.get_setting('SERIAL_MUST_MATCH_PART') and serial[0] != part.name[0]:
if serial[0] != part.name[0]: self.raise_error('Serial number must start with same letter as part')
self.raise_error('Serial number must start with same letter as part')
def validate_batch_code(self, batch_code: str, item): def validate_batch_code(self, batch_code: str, item):
"""Ensure that a particular batch code meets specification. """Ensure that a particular batch code meets specification.

View File

@ -700,39 +700,37 @@ class StockItem(
# The 'supplier_part' field must point to the same part! # The 'supplier_part' field must point to the same part!
try: try:
if self.supplier_part is not None: if self.supplier_part is not None and self.supplier_part.part != self.part:
if self.supplier_part.part != self.part: raise ValidationError({
'supplier_part': _(
f"Part type ('{self.supplier_part.part}') must be {self.part}"
)
})
# A part with a serial number MUST have the quantity set to 1
if self.part is not None and self.serial:
if self.quantity > 1:
raise ValidationError({ raise ValidationError({
'supplier_part': _( 'quantity': _(
f"Part type ('{self.supplier_part.part}') must be {self.part}" 'Quantity must be 1 for item with a serial number'
),
'serial': _(
'Serial number cannot be set if quantity greater than 1'
),
})
if self.quantity == 0:
self.quantity = 1
elif self.quantity > 1:
raise ValidationError({
'quantity': _(
'Quantity must be 1 for item with a serial number'
) )
}) })
if self.part is not None: # Serial numbered items cannot be deleted on depletion
# A part with a serial number MUST have the quantity set to 1 self.delete_on_deplete = False
if self.serial:
if self.quantity > 1:
raise ValidationError({
'quantity': _(
'Quantity must be 1 for item with a serial number'
),
'serial': _(
'Serial number cannot be set if quantity greater than 1'
),
})
if self.quantity == 0:
self.quantity = 1
elif self.quantity > 1:
raise ValidationError({
'quantity': _(
'Quantity must be 1 for item with a serial number'
)
})
# Serial numbered items cannot be deleted on depletion
self.delete_on_deplete = False
except PartModels.Part.DoesNotExist: except PartModels.Part.DoesNotExist:
pass pass

View File

@ -795,14 +795,11 @@ class InstallStockItemSerializer(serializers.Serializer):
parent_item = self.context['item'] parent_item = self.context['item']
parent_part = parent_item.part parent_part = parent_item.part
# Check if the selected part is in the Bill of Materials of the parent item
if get_global_setting( if get_global_setting(
'STOCK_ENFORCE_BOM_INSTALLATION', backup_value=True, cache=False 'STOCK_ENFORCE_BOM_INSTALLATION', backup_value=True, cache=False
): ) and not parent_part.check_if_part_in_bom(stock_item.part):
# Check if the selected part is in the Bill of Materials of the parent item raise ValidationError(_('Selected part is not in the Bill of Materials'))
if not parent_part.check_if_part_in_bom(stock_item.part):
raise ValidationError(
_('Selected part is not in the Bill of Materials')
)
return stock_item return stock_item

View File

@ -95,13 +95,12 @@ class OwnerList(ListAPI):
if not search_match: if not search_match:
continue continue
if is_active is not None: # Skip any users which do not match the required *is_active* value
# Skip any users which do not match the required *is_active* value if is_active is not None and (
if ( result.owner_type.name == 'user'
result.owner_type.name == 'user' and result.owner_id not in matching_user_ids
and result.owner_id not in matching_user_ids ):
): continue
continue
# If we get here, there is no reason *not* to include this result # If we get here, there is no reason *not* to include this result
results.append(result) results.append(result)

View File

@ -34,9 +34,10 @@ logger = logging.getLogger('inventree')
# string representation of a user # string representation of a user
def user_model_str(self): def user_model_str(self):
"""Function to override the default Django User __str__.""" """Function to override the default Django User __str__."""
if get_global_setting('DISPLAY_FULL_NAMES', cache=True): if (
if self.first_name or self.last_name: get_global_setting('DISPLAY_FULL_NAMES', cache=True) and self.first_name
return f'{self.first_name} {self.last_name}' ) or self.last_name:
return f'{self.first_name} {self.last_name}'
return self.username return self.username
@ -421,19 +422,19 @@ class RuleSet(models.Model):
# Work out which roles touch the given table # Work out which roles touch the given table
for role in cls.RULESET_NAMES: for role in cls.RULESET_NAMES:
if table in cls.get_ruleset_models()[role]: if table in cls.get_ruleset_models()[role] and check_user_role(
if check_user_role(user, role, permission): user, role, permission
return True ):
return True
# Check for children models which inherits from parent role # Check for children models which inherits from parent role
for parent, child in cls.RULESET_CHANGE_INHERIT: for parent, child in cls.RULESET_CHANGE_INHERIT:
# Get child model name # Get child model name
parent_child_string = f'{parent}_{child}' parent_child_string = f'{parent}_{child}'
if parent_child_string == table: # Check if parent role has change permission
# Check if parent role has change permission if parent_child_string == table and check_user_role(user, parent, 'change'):
if check_user_role(user, parent, 'change'): return True
return True
# Print message instead of throwing an error # Print message instead of throwing an error
name = getattr(user, 'name', user.pk) name = getattr(user, 'name', user.pk)

View File

@ -628,9 +628,11 @@ def export_records(
model_name = entry.get('model', None) model_name = entry.get('model', None)
# Ignore any temporary settings (start with underscore) # Ignore any temporary settings (start with underscore)
if model_name in ['common.inventreesetting', 'common.inventreeusersetting']: if model_name in [
if entry['fields'].get('key', '').startswith('_'): 'common.inventreesetting',
continue 'common.inventreeusersetting',
] and entry['fields'].get('key', '').startswith('_'):
continue
if model_name == 'auth.group': if model_name == 'auth.group':
entry['fields']['permissions'] = [] entry['fields']['permissions'] = []