diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index e001a318fc..e5a25889ef 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,17 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 308 +INVENTREE_API_VERSION = 309 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v309 - 2025-02-02 : https://github.com/inventree/InvenTree/pull/9008 + - Bug fixes for the "Part" serializer + - Fixes for data import API endpoints + v308 - 2025-02-01 : https://github.com/inventree/InvenTree/pull/9003 - Adds extra detail to the ReportOutput and LabelOutput API endpoints - Allows ordering of output list endpoints diff --git a/src/backend/InvenTree/InvenTree/exceptions.py b/src/backend/InvenTree/InvenTree/exceptions.py index b41b9d0056..a8bebf4534 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 613eb4dec2..550241b3c0 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -253,11 +253,12 @@ class BuildOutputSerializer(serializers.Serializer): # The build output must have all tracked parts allocated 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')) @@ -439,6 +440,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( @@ -447,7 +449,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, ) @@ -531,7 +533,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', []) @@ -544,7 +546,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), ) @@ -619,7 +621,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 @@ -642,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, @@ -715,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), ) @@ -837,13 +839,13 @@ class BuildCompleteSerializer(serializers.Serializer): def save(self): """Complete the specified build output.""" - request = self.context['request'] + request = self.context.get('request') build = self.context['build'] data = self.validated_data build.complete_build( - request.user, + request.user if request else None, trim_allocated_stock=data.get( 'accept_overallocated', OverallocationChoice.REJECT ) diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index 780f99893d..4811ffa2ed 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 82555e91ab..3e0bd6ad41 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -145,6 +145,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) @@ -158,6 +159,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 @@ -167,6 +169,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, @@ -558,7 +564,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 = {} @@ -566,8 +572,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) @@ -617,16 +623,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 @@ -638,7 +647,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 = { @@ -660,12 +669,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 3338e6d7d2..5e5551c284 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 e74f4655cd..97a3fdbe14 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -950,7 +950,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'] @@ -973,7 +973,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', ''), @@ -1434,8 +1434,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() @@ -1572,10 +1572,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)) @@ -2020,7 +2020,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'] @@ -2033,7 +2033,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 bc0ae715ad..4c267e082c 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -683,7 +683,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.""" @@ -760,7 +760,7 @@ class PartSerializer( 'tags', ] - read_only_fields = ['barcode_hash', 'creation_date'] + read_only_fields = ['barcode_hash', 'creation_date', 'creation_user'] tags = TagListSerializerField(required=False) @@ -972,7 +972,9 @@ class PartSerializer( category_default_location = serializers.IntegerField(read_only=True) variant_stock = serializers.FloatField(read_only=True, label=_('Variant Stock')) - minimum_stock = serializers.FloatField() + minimum_stock = serializers.FloatField( + required=False, label=_('Minimum Stock'), default=0 + ) image = InvenTree.serializers.InvenTreeImageSerializerField( required=False, allow_null=True @@ -1062,8 +1064,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 @@ -1123,7 +1125,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: @@ -1222,9 +1226,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() @@ -1979,7 +1982,7 @@ class BomImportUploadSerializer(InvenTree.serializers.DataFileUploadSerializer): class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer): - """Serializer class for exatracting BOM data from an uploaded file. + """Serializer class for extracting BOM data from an uploaded file. The parent class DataFileExtractSerializer does most of the heavy lifting here. 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 63c8a54b42..70ed5c0075 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -764,8 +764,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