mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
remove unneeded branches
fixes SIM102
This commit is contained in:
parent
372e3d417e
commit
f74d41bc07
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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."""
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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."""
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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]):
|
||||||
|
@ -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}
|
||||||
|
@ -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))
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
8
tasks.py
8
tasks.py
@ -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'] = []
|
||||||
|
Loading…
Reference in New Issue
Block a user