diff --git a/src/backend/InvenTree/InvenTree/exceptions.py b/src/backend/InvenTree/InvenTree/exceptions.py index cece1162a8..1538b27001 100644 --- a/src/backend/InvenTree/InvenTree/exceptions.py +++ b/src/backend/InvenTree/InvenTree/exceptions.py @@ -33,6 +33,9 @@ def log_error(path, error_name=None, error_info=None, error_data=None): """ from error_report.models import Error + if not path: + path = '' + kind, info, data = sys.exc_info() # Check if the error is on the ignore list @@ -104,17 +107,19 @@ def exception_handler(exc, context): else: error_detail = _('Error details can be found in the admin panel') + request = context.get('request') + path = request.path if request else '' + response_data = { 'error': type(exc).__name__, 'error_class': str(type(exc)), 'detail': error_detail, - 'path': context['request'].path, + 'path': path, 'status_code': 500, } response = Response(response_data, status=500) - - log_error(context['request'].path) + log_error(path) if response is not None: # Convert errors returned under the label '__all__' to 'non_field_errors' diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 845cd584bd..d4526d5338 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -244,7 +244,12 @@ class BuildOutputSerializer(serializers.Serializer): if not build.is_output_fully_allocated(output): # Check if the user has specified that incomplete allocations are ok - accept_incomplete = InvenTree.helpers.str2bool(self.context['request'].data.get('accept_incomplete_allocation', False)) + if request := self.context.get('request'): + accept_incomplete = InvenTree.helpers.str2bool( + request.data.get('accept_incomplete_allocation', False) + ) + else: + accept_incomplete = False if not accept_incomplete: raise ValidationError(_("This build output is not fully allocated")) @@ -426,6 +431,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): """Generate the new build output(s)""" data = self.validated_data + request = self.context.get('request') build = self.get_build() build.create_build_output( @@ -434,7 +440,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): batch=data.get('batch_code', ''), location=data.get('location', None), auto_allocate=data.get('auto_allocate', False), - user=self.context['request'].user, + user=request.user if request else None, ) @@ -528,7 +534,7 @@ class BuildOutputScrapSerializer(serializers.Serializer): def save(self): """Save the serializer to scrap the build outputs""" build = self.context['build'] - request = self.context['request'] + request = self.context.get('request') data = self.validated_data outputs = data.get('outputs', []) @@ -541,7 +547,7 @@ class BuildOutputScrapSerializer(serializers.Serializer): output, quantity, data.get('location', None), - user=request.user, + user=request.user if request else None, notes=data.get('notes', ''), discard_allocations=data.get('discard_allocations', False) ) @@ -617,7 +623,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer): def save(self): """Save the serializer to complete the build outputs.""" build = self.context['build'] - request = self.context['request'] + request = self.context.get('request') data = self.validated_data @@ -638,7 +644,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer): build.complete_build_output( output, - request.user, + request.user if request else None, location=location, status=status, notes=notes, @@ -711,12 +717,12 @@ class BuildCancelSerializer(serializers.Serializer): def save(self): """Cancel the specified build""" build = self.context['build'] - request = self.context['request'] + request = self.context.get('request') data = self.validated_data build.cancel_build( - request.user, + request.user if request else None, remove_allocated_stock=data.get('remove_allocated_stock', False), remove_incomplete_outputs=data.get('remove_incomplete_outputs', False), ) @@ -826,15 +832,18 @@ class BuildCompleteSerializer(serializers.Serializer): return data def save(self): - """Complete the specified build output""" - request = self.context['request'] + """Complete the specified build output.""" + request = self.context.get('request') build = self.context['build'] data = self.validated_data build.complete_build( - request.user, - trim_allocated_stock=data.get('accept_overallocated', OverallocationChoice.REJECT) == OverallocationChoice.TRIM + request.user if request else None, + trim_allocated_stock=data.get( + 'accept_overallocated', OverallocationChoice.REJECT + ) + == OverallocationChoice.TRIM, ) diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index aa6240d8d2..07c0fc87ad 100644 --- a/src/backend/InvenTree/common/serializers.py +++ b/src/backend/InvenTree/common/serializers.py @@ -251,8 +251,8 @@ class NotificationMessageSerializer(InvenTreeModelSerializer): target['link'] = obj.target_object.get_absolute_url() else: # check if user is staff - link to admin - request = self.context['request'] - if request.user and request.user.is_staff: + request = self.context.get('request') + if request and request.user and request.user.is_staff: meta = obj.target_object._meta try: diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index 28e3a94c32..ad06193d37 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -141,6 +141,7 @@ class DataImportSession(models.Model): - Extract column names from the data file - Create a default mapping for each field in the serializer + - Find a default "backup" value for each field (if one exists) """ # Extract list of column names from the file self.columns = importer.operations.extract_column_names(self.data_file) @@ -154,6 +155,7 @@ class DataImportSession(models.Model): matched_columns = set() + self.field_defaults = self.field_defaults or {} field_overrides = self.field_overrides or {} # Create a default mapping for each available field in the database @@ -163,6 +165,10 @@ class DataImportSession(models.Model): if field in field_overrides: continue + # Extract a "default" value for the field, if one exists + if 'default' in field_def: + self.field_defaults[field] = field_def['default'] + # Generate a list of possible column names for this field field_options = [ field, @@ -554,7 +560,7 @@ class DataImportRow(models.Model): if not available_fields: available_fields = self.session.available_fields() - overrride_values = self.override_values + override_values = self.override_values default_values = self.default_values data = {} @@ -562,8 +568,8 @@ class DataImportRow(models.Model): # We have mapped column (file) to field (serializer) already for field, col in field_mapping.items(): # Data override (force value and skip any further checks) - if field in overrride_values: - data[field] = overrride_values[field] + if field in override_values: + data[field] = override_values[field] continue # Default value (if provided) @@ -615,16 +621,19 @@ class DataImportRow(models.Model): return data - def construct_serializer(self): + def construct_serializer(self, request=None): """Construct a serializer object for this row.""" if serializer_class := self.session.serializer_class: - return serializer_class(data=self.serializer_data()) + return serializer_class( + data=self.serializer_data(), context={'request': request} + ) - def validate(self, commit=False) -> bool: + def validate(self, commit=False, request=None) -> bool: """Validate the data in this row against the linked serializer. Arguments: commit: If True, the data is saved to the database (if validation passes) + request: The request object (if available) for extracting user information Returns: True if the data is valid, False otherwise @@ -636,7 +645,7 @@ class DataImportRow(models.Model): # Row has already been completed return True - serializer = self.construct_serializer() + serializer = self.construct_serializer(request=request) if not serializer: self.errors = { @@ -658,12 +667,12 @@ class DataImportRow(models.Model): try: serializer.save() self.complete = True - self.save() - self.session.check_complete() - - except Exception as e: + except ValueError as e: # Exception as e: self.errors = {'non_field_errors': str(e)} result = False + self.save() + self.session.check_complete() + return result diff --git a/src/backend/InvenTree/importer/operations.py b/src/backend/InvenTree/importer/operations.py index d29471b282..e718ec5f7f 100644 --- a/src/backend/InvenTree/importer/operations.py +++ b/src/backend/InvenTree/importer/operations.py @@ -72,6 +72,8 @@ def extract_column_names(data_file) -> list: headers = [] for idx, header in enumerate(data.headers): + header = header.strip() + if header: headers.append(header) else: diff --git a/src/backend/InvenTree/importer/serializers.py b/src/backend/InvenTree/importer/serializers.py index ac68056f55..779a1a0d90 100644 --- a/src/backend/InvenTree/importer/serializers.py +++ b/src/backend/InvenTree/importer/serializers.py @@ -207,8 +207,10 @@ class DataImportAcceptRowSerializer(serializers.Serializer): """Complete the provided rows.""" rows = self.validated_data['rows'] + request = self.context.get('request', None) + for row in rows: - row.validate(commit=True) + row.validate(commit=True, request=request) if session := self.context.get('session', None): session.check_complete() diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index bf6aa32fcf..eae5ff3155 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -933,7 +933,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer): """Perform the actual database transaction to receive purchase order items.""" data = self.validated_data - request = self.context['request'] + request = self.context.get('request') order = self.context['order'] items = data['items'] @@ -956,7 +956,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer): item['line_item'], loc, item['quantity'], - request.user, + request.user if request else None, status=item['status'], barcode=item.get('barcode', ''), batch_code=item.get('batch_code', ''), @@ -1416,8 +1416,8 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer): data = self.validated_data - request = self.context['request'] - user = request.user + request = self.context.get('request') + user = request.user if request else None # Extract shipping date (defaults to today's date) now = current_date() @@ -1554,10 +1554,10 @@ class SalesOrderCompleteSerializer(OrderAdjustSerializer): def save(self): """Save the serializer to complete the SalesOrder.""" - request = self.context['request'] + request = self.context.get('request') data = self.validated_data - user = getattr(request, 'user', None) + user = request.user if request else None self.order.ship_order( user, allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)) @@ -2002,7 +2002,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer): def save(self): """Saving this serializer marks the returned items as received.""" order = self.context['order'] - request = self.context['request'] + request = self.context.get('request') data = self.validated_data items = data['items'] @@ -2015,7 +2015,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer): order.receive_line_item( line_item, location, - request.user, + request.user if request else None, note=data.get('note', ''), status=item.get('status', None), ) diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 66001ff930..cf6dba418c 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -682,7 +682,7 @@ class PartSerializer( Used when displaying all details of a single component. """ - import_exclude_fields = ['duplicate'] + import_exclude_fields = ['creation_date', 'creation_user', 'duplicate'] class Meta: """Metaclass defining serializer fields.""" @@ -1061,8 +1061,8 @@ class PartSerializer( instance = super().create(validated_data) # Save user information - if self.context['request']: - instance.creation_user = self.context['request'].user + if request := self.context.get('request'): + instance.creation_user = request.user instance.save() # Copy data from original Part @@ -1122,7 +1122,9 @@ class PartSerializer( part=instance, quantity=quantity, location=location ) - stockitem.save(user=self.context['request'].user) + request = self.context.get('request', None) + user = request.user if request else None + stockitem.save(user=user) # Create initial supplier information if initial_supplier: @@ -1221,9 +1223,8 @@ class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer): data = self.validated_data # Add in user information automatically - request = self.context['request'] - data['user'] = request.user - + request = self.context.get('request') + data['user'] = request.user if request else None super().save() diff --git a/src/backend/InvenTree/report/migrations/0026_auto_20240422_1301.py b/src/backend/InvenTree/report/migrations/0026_auto_20240422_1301.py index 5a37adf331..11556acee5 100644 --- a/src/backend/InvenTree/report/migrations/0026_auto_20240422_1301.py +++ b/src/backend/InvenTree/report/migrations/0026_auto_20240422_1301.py @@ -46,9 +46,7 @@ def convert_legacy_labels(table_name, model_name, template_model): try: cursor.execute(query) except Exception: - # Table likely does not exist - if not InvenTree.ready.isInTestMode(): - print(f"\nLegacy label table {table_name} not found - skipping migration") + # Table likely does not exist - database was created more recently return 0 rows = cursor.fetchall() diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 6b7ac858b4..0b05bc4df2 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -762,8 +762,8 @@ class SerializeStockItemSerializer(serializers.Serializer): def save(self): """Serialize stock item.""" item = self.context['item'] - request = self.context['request'] - user = request.user + request = self.context.get('request') + user = request.user if request else None data = self.validated_data