mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	[Bug] mport fix (#9008)
* Better handling of request object in serializers * Pass request object through - Required to extract user information * Strip column header during import - Prevent mismatch due to whitespace * Fix for "minimum stock" field * Fix for part serializer * Extract default values on import * Remove outdated migration message * Bump API version
This commit is contained in:
		@@ -1,13 +1,17 @@
 | 
				
			|||||||
"""InvenTree API version information."""
 | 
					"""InvenTree API version information."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# InvenTree API version
 | 
					# 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."""
 | 
					"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INVENTREE_API_TEXT = """
 | 
					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
 | 
					v308 - 2025-02-01 : https://github.com/inventree/InvenTree/pull/9003
 | 
				
			||||||
    - Adds extra detail to the ReportOutput and LabelOutput API endpoints
 | 
					    - Adds extra detail to the ReportOutput and LabelOutput API endpoints
 | 
				
			||||||
    - Allows ordering of output list endpoints
 | 
					    - Allows ordering of output list endpoints
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,9 @@ def log_error(path, error_name=None, error_info=None, error_data=None):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    from error_report.models import Error
 | 
					    from error_report.models import Error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not path:
 | 
				
			||||||
 | 
					        path = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    kind, info, data = sys.exc_info()
 | 
					    kind, info, data = sys.exc_info()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check if the error is on the ignore list
 | 
					    # Check if the error is on the ignore list
 | 
				
			||||||
@@ -104,17 +107,19 @@ def exception_handler(exc, context):
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            error_detail = _('Error details can be found in the admin panel')
 | 
					            error_detail = _('Error details can be found in the admin panel')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = context.get('request')
 | 
				
			||||||
 | 
					        path = request.path if request else ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response_data = {
 | 
					        response_data = {
 | 
				
			||||||
            'error': type(exc).__name__,
 | 
					            'error': type(exc).__name__,
 | 
				
			||||||
            'error_class': str(type(exc)),
 | 
					            'error_class': str(type(exc)),
 | 
				
			||||||
            'detail': error_detail,
 | 
					            'detail': error_detail,
 | 
				
			||||||
            'path': context['request'].path,
 | 
					            'path': path,
 | 
				
			||||||
            'status_code': 500,
 | 
					            'status_code': 500,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = Response(response_data, status=500)
 | 
					        response = Response(response_data, status=500)
 | 
				
			||||||
 | 
					        log_error(path)
 | 
				
			||||||
        log_error(context['request'].path)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if response is not None:
 | 
					    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'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -253,11 +253,12 @@ class BuildOutputSerializer(serializers.Serializer):
 | 
				
			|||||||
            # The build output must have all tracked parts allocated
 | 
					            # The build output must have all tracked parts allocated
 | 
				
			||||||
            if not build.is_output_fully_allocated(output):
 | 
					            if not build.is_output_fully_allocated(output):
 | 
				
			||||||
                # Check if the user has specified that incomplete allocations are ok
 | 
					                # Check if the user has specified that incomplete allocations are ok
 | 
				
			||||||
 | 
					                if request := self.context.get('request'):
 | 
				
			||||||
                    accept_incomplete = InvenTree.helpers.str2bool(
 | 
					                    accept_incomplete = InvenTree.helpers.str2bool(
 | 
				
			||||||
                    self.context['request'].data.get(
 | 
					                        request.data.get('accept_incomplete_allocation', False)
 | 
				
			||||||
                        'accept_incomplete_allocation', False
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    accept_incomplete = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if not accept_incomplete:
 | 
					                if not accept_incomplete:
 | 
				
			||||||
                    raise ValidationError(_('This build output is not fully allocated'))
 | 
					                    raise ValidationError(_('This build output is not fully allocated'))
 | 
				
			||||||
@@ -439,6 +440,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
 | 
				
			|||||||
        """Generate the new build output(s)."""
 | 
					        """Generate the new build output(s)."""
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        build = self.get_build()
 | 
					        build = self.get_build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        build.create_build_output(
 | 
					        build.create_build_output(
 | 
				
			||||||
@@ -447,7 +449,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
 | 
				
			|||||||
            batch=data.get('batch_code', ''),
 | 
					            batch=data.get('batch_code', ''),
 | 
				
			||||||
            location=data.get('location', None),
 | 
					            location=data.get('location', None),
 | 
				
			||||||
            auto_allocate=data.get('auto_allocate', False),
 | 
					            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):
 | 
					    def save(self):
 | 
				
			||||||
        """Save the serializer to scrap the build outputs."""
 | 
					        """Save the serializer to scrap the build outputs."""
 | 
				
			||||||
        build = self.context['build']
 | 
					        build = self.context['build']
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
        outputs = data.get('outputs', [])
 | 
					        outputs = data.get('outputs', [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -544,7 +546,7 @@ class BuildOutputScrapSerializer(serializers.Serializer):
 | 
				
			|||||||
                    output,
 | 
					                    output,
 | 
				
			||||||
                    quantity,
 | 
					                    quantity,
 | 
				
			||||||
                    data.get('location', None),
 | 
					                    data.get('location', None),
 | 
				
			||||||
                    user=request.user,
 | 
					                    user=request.user if request else None,
 | 
				
			||||||
                    notes=data.get('notes', ''),
 | 
					                    notes=data.get('notes', ''),
 | 
				
			||||||
                    discard_allocations=data.get('discard_allocations', False),
 | 
					                    discard_allocations=data.get('discard_allocations', False),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@@ -619,7 +621,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
 | 
				
			|||||||
    def save(self):
 | 
					    def save(self):
 | 
				
			||||||
        """Save the serializer to complete the build outputs."""
 | 
					        """Save the serializer to complete the build outputs."""
 | 
				
			||||||
        build = self.context['build']
 | 
					        build = self.context['build']
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -642,7 +644,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                build.complete_build_output(
 | 
					                build.complete_build_output(
 | 
				
			||||||
                    output,
 | 
					                    output,
 | 
				
			||||||
                    request.user,
 | 
					                    request.user if request else None,
 | 
				
			||||||
                    location=location,
 | 
					                    location=location,
 | 
				
			||||||
                    status=status,
 | 
					                    status=status,
 | 
				
			||||||
                    notes=notes,
 | 
					                    notes=notes,
 | 
				
			||||||
@@ -715,12 +717,12 @@ class BuildCancelSerializer(serializers.Serializer):
 | 
				
			|||||||
    def save(self):
 | 
					    def save(self):
 | 
				
			||||||
        """Cancel the specified build."""
 | 
					        """Cancel the specified build."""
 | 
				
			||||||
        build = self.context['build']
 | 
					        build = self.context['build']
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        build.cancel_build(
 | 
					        build.cancel_build(
 | 
				
			||||||
            request.user,
 | 
					            request.user if request else None,
 | 
				
			||||||
            remove_allocated_stock=data.get('remove_allocated_stock', False),
 | 
					            remove_allocated_stock=data.get('remove_allocated_stock', False),
 | 
				
			||||||
            remove_incomplete_outputs=data.get('remove_incomplete_outputs', False),
 | 
					            remove_incomplete_outputs=data.get('remove_incomplete_outputs', False),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -837,13 +839,13 @@ class BuildCompleteSerializer(serializers.Serializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def save(self):
 | 
					    def save(self):
 | 
				
			||||||
        """Complete the specified build output."""
 | 
					        """Complete the specified build output."""
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        build = self.context['build']
 | 
					        build = self.context['build']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        build.complete_build(
 | 
					        build.complete_build(
 | 
				
			||||||
            request.user,
 | 
					            request.user if request else None,
 | 
				
			||||||
            trim_allocated_stock=data.get(
 | 
					            trim_allocated_stock=data.get(
 | 
				
			||||||
                'accept_overallocated', OverallocationChoice.REJECT
 | 
					                'accept_overallocated', OverallocationChoice.REJECT
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -251,8 +251,8 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
 | 
				
			|||||||
                target['link'] = obj.target_object.get_absolute_url()
 | 
					                target['link'] = obj.target_object.get_absolute_url()
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                # check if user is staff - link to admin
 | 
					                # check if user is staff - link to admin
 | 
				
			||||||
                request = self.context['request']
 | 
					                request = self.context.get('request')
 | 
				
			||||||
                if request.user and request.user.is_staff:
 | 
					                if request and request.user and request.user.is_staff:
 | 
				
			||||||
                    meta = obj.target_object._meta
 | 
					                    meta = obj.target_object._meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -145,6 +145,7 @@ class DataImportSession(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        - Extract column names from the data file
 | 
					        - Extract column names from the data file
 | 
				
			||||||
        - Create a default mapping for each field in the serializer
 | 
					        - 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
 | 
					        # Extract list of column names from the file
 | 
				
			||||||
        self.columns = importer.operations.extract_column_names(self.data_file)
 | 
					        self.columns = importer.operations.extract_column_names(self.data_file)
 | 
				
			||||||
@@ -158,6 +159,7 @@ class DataImportSession(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        matched_columns = set()
 | 
					        matched_columns = set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.field_defaults = self.field_defaults or {}
 | 
				
			||||||
        field_overrides = self.field_overrides or {}
 | 
					        field_overrides = self.field_overrides or {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create a default mapping for each available field in the database
 | 
					        # Create a default mapping for each available field in the database
 | 
				
			||||||
@@ -167,6 +169,10 @@ class DataImportSession(models.Model):
 | 
				
			|||||||
            if field in field_overrides:
 | 
					            if field in field_overrides:
 | 
				
			||||||
                continue
 | 
					                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
 | 
					            # Generate a list of possible column names for this field
 | 
				
			||||||
            field_options = [
 | 
					            field_options = [
 | 
				
			||||||
                field,
 | 
					                field,
 | 
				
			||||||
@@ -558,7 +564,7 @@ class DataImportRow(models.Model):
 | 
				
			|||||||
        if not available_fields:
 | 
					        if not available_fields:
 | 
				
			||||||
            available_fields = self.session.available_fields()
 | 
					            available_fields = self.session.available_fields()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        overrride_values = self.override_values
 | 
					        override_values = self.override_values
 | 
				
			||||||
        default_values = self.default_values
 | 
					        default_values = self.default_values
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = {}
 | 
					        data = {}
 | 
				
			||||||
@@ -566,8 +572,8 @@ class DataImportRow(models.Model):
 | 
				
			|||||||
        # We have mapped column (file) to field (serializer) already
 | 
					        # We have mapped column (file) to field (serializer) already
 | 
				
			||||||
        for field, col in field_mapping.items():
 | 
					        for field, col in field_mapping.items():
 | 
				
			||||||
            # Data override (force value and skip any further checks)
 | 
					            # Data override (force value and skip any further checks)
 | 
				
			||||||
            if field in overrride_values:
 | 
					            if field in override_values:
 | 
				
			||||||
                data[field] = overrride_values[field]
 | 
					                data[field] = override_values[field]
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Default value (if provided)
 | 
					            # Default value (if provided)
 | 
				
			||||||
@@ -617,16 +623,19 @@ class DataImportRow(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return data
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def construct_serializer(self):
 | 
					    def construct_serializer(self, request=None):
 | 
				
			||||||
        """Construct a serializer object for this row."""
 | 
					        """Construct a serializer object for this row."""
 | 
				
			||||||
        if serializer_class := self.session.serializer_class:
 | 
					        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.
 | 
					        """Validate the data in this row against the linked serializer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Arguments:
 | 
					        Arguments:
 | 
				
			||||||
            commit: If True, the data is saved to the database (if validation passes)
 | 
					            commit: If True, the data is saved to the database (if validation passes)
 | 
				
			||||||
 | 
					            request: The request object (if available) for extracting user information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
            True if the data is valid, False otherwise
 | 
					            True if the data is valid, False otherwise
 | 
				
			||||||
@@ -638,7 +647,7 @@ class DataImportRow(models.Model):
 | 
				
			|||||||
            # Row has already been completed
 | 
					            # Row has already been completed
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        serializer = self.construct_serializer()
 | 
					        serializer = self.construct_serializer(request=request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not serializer:
 | 
					        if not serializer:
 | 
				
			||||||
            self.errors = {
 | 
					            self.errors = {
 | 
				
			||||||
@@ -660,12 +669,12 @@ class DataImportRow(models.Model):
 | 
				
			|||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    serializer.save()
 | 
					                    serializer.save()
 | 
				
			||||||
                    self.complete = True
 | 
					                    self.complete = True
 | 
				
			||||||
                    self.save()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    self.session.check_complete()
 | 
					                except ValueError as e:  # Exception as e:
 | 
				
			||||||
 | 
					 | 
				
			||||||
                except Exception as e:
 | 
					 | 
				
			||||||
                    self.errors = {'non_field_errors': str(e)}
 | 
					                    self.errors = {'non_field_errors': str(e)}
 | 
				
			||||||
                    result = False
 | 
					                    result = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.save()
 | 
				
			||||||
 | 
					                self.session.check_complete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return result
 | 
					        return result
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,8 @@ def extract_column_names(data_file) -> list:
 | 
				
			|||||||
    headers = []
 | 
					    headers = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for idx, header in enumerate(data.headers):
 | 
					    for idx, header in enumerate(data.headers):
 | 
				
			||||||
 | 
					        header = header.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if header:
 | 
					        if header:
 | 
				
			||||||
            headers.append(header)
 | 
					            headers.append(header)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -207,8 +207,10 @@ class DataImportAcceptRowSerializer(serializers.Serializer):
 | 
				
			|||||||
        """Complete the provided rows."""
 | 
					        """Complete the provided rows."""
 | 
				
			||||||
        rows = self.validated_data['rows']
 | 
					        rows = self.validated_data['rows']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = self.context.get('request', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for row in rows:
 | 
					        for row in rows:
 | 
				
			||||||
            row.validate(commit=True)
 | 
					            row.validate(commit=True, request=request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if session := self.context.get('session', None):
 | 
					        if session := self.context.get('session', None):
 | 
				
			||||||
            session.check_complete()
 | 
					            session.check_complete()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -950,7 +950,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
 | 
				
			|||||||
        """Perform the actual database transaction to receive purchase order items."""
 | 
					        """Perform the actual database transaction to receive purchase order items."""
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        order = self.context['order']
 | 
					        order = self.context['order']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        items = data['items']
 | 
					        items = data['items']
 | 
				
			||||||
@@ -973,7 +973,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
 | 
				
			|||||||
                        item['line_item'],
 | 
					                        item['line_item'],
 | 
				
			||||||
                        loc,
 | 
					                        loc,
 | 
				
			||||||
                        item['quantity'],
 | 
					                        item['quantity'],
 | 
				
			||||||
                        request.user,
 | 
					                        request.user if request else None,
 | 
				
			||||||
                        status=item['status'],
 | 
					                        status=item['status'],
 | 
				
			||||||
                        barcode=item.get('barcode', ''),
 | 
					                        barcode=item.get('barcode', ''),
 | 
				
			||||||
                        batch_code=item.get('batch_code', ''),
 | 
					                        batch_code=item.get('batch_code', ''),
 | 
				
			||||||
@@ -1434,8 +1434,8 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        user = request.user
 | 
					        user = request.user if request else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Extract shipping date (defaults to today's date)
 | 
					        # Extract shipping date (defaults to today's date)
 | 
				
			||||||
        now = current_date()
 | 
					        now = current_date()
 | 
				
			||||||
@@ -1572,10 +1572,10 @@ class SalesOrderCompleteSerializer(OrderAdjustSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def save(self):
 | 
					    def save(self):
 | 
				
			||||||
        """Save the serializer to complete the SalesOrder."""
 | 
					        """Save the serializer to complete the SalesOrder."""
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user = getattr(request, 'user', None)
 | 
					        user = request.user if request else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.order.ship_order(
 | 
					        self.order.ship_order(
 | 
				
			||||||
            user, allow_incomplete_lines=str2bool(data.get('accept_incomplete', False))
 | 
					            user, allow_incomplete_lines=str2bool(data.get('accept_incomplete', False))
 | 
				
			||||||
@@ -2020,7 +2020,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
 | 
				
			|||||||
    def save(self):
 | 
					    def save(self):
 | 
				
			||||||
        """Saving this serializer marks the returned items as received."""
 | 
					        """Saving this serializer marks the returned items as received."""
 | 
				
			||||||
        order = self.context['order']
 | 
					        order = self.context['order']
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
        items = data['items']
 | 
					        items = data['items']
 | 
				
			||||||
@@ -2033,7 +2033,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
 | 
				
			|||||||
                order.receive_line_item(
 | 
					                order.receive_line_item(
 | 
				
			||||||
                    line_item,
 | 
					                    line_item,
 | 
				
			||||||
                    location,
 | 
					                    location,
 | 
				
			||||||
                    request.user,
 | 
					                    request.user if request else None,
 | 
				
			||||||
                    note=data.get('note', ''),
 | 
					                    note=data.get('note', ''),
 | 
				
			||||||
                    status=item.get('status', None),
 | 
					                    status=item.get('status', None),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -683,7 +683,7 @@ class PartSerializer(
 | 
				
			|||||||
    Used when displaying all details of a single component.
 | 
					    Used when displaying all details of a single component.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import_exclude_fields = ['duplicate']
 | 
					    import_exclude_fields = ['creation_date', 'creation_user', 'duplicate']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        """Metaclass defining serializer fields."""
 | 
					        """Metaclass defining serializer fields."""
 | 
				
			||||||
@@ -760,7 +760,7 @@ class PartSerializer(
 | 
				
			|||||||
            'tags',
 | 
					            'tags',
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        read_only_fields = ['barcode_hash', 'creation_date']
 | 
					        read_only_fields = ['barcode_hash', 'creation_date', 'creation_user']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tags = TagListSerializerField(required=False)
 | 
					    tags = TagListSerializerField(required=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -972,7 +972,9 @@ class PartSerializer(
 | 
				
			|||||||
    category_default_location = serializers.IntegerField(read_only=True)
 | 
					    category_default_location = serializers.IntegerField(read_only=True)
 | 
				
			||||||
    variant_stock = serializers.FloatField(read_only=True, label=_('Variant Stock'))
 | 
					    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(
 | 
					    image = InvenTree.serializers.InvenTreeImageSerializerField(
 | 
				
			||||||
        required=False, allow_null=True
 | 
					        required=False, allow_null=True
 | 
				
			||||||
@@ -1062,8 +1064,8 @@ class PartSerializer(
 | 
				
			|||||||
        instance = super().create(validated_data)
 | 
					        instance = super().create(validated_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Save user information
 | 
					        # Save user information
 | 
				
			||||||
        if self.context['request']:
 | 
					        if request := self.context.get('request'):
 | 
				
			||||||
            instance.creation_user = self.context['request'].user
 | 
					            instance.creation_user = request.user
 | 
				
			||||||
            instance.save()
 | 
					            instance.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Copy data from original Part
 | 
					        # Copy data from original Part
 | 
				
			||||||
@@ -1123,7 +1125,9 @@ class PartSerializer(
 | 
				
			|||||||
                    part=instance, quantity=quantity, location=location
 | 
					                    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
 | 
					        # Create initial supplier information
 | 
				
			||||||
        if initial_supplier:
 | 
					        if initial_supplier:
 | 
				
			||||||
@@ -1222,9 +1226,8 @@ class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
				
			|||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Add in user information automatically
 | 
					        # Add in user information automatically
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        data['user'] = request.user
 | 
					        data['user'] = request.user if request else None
 | 
				
			||||||
 | 
					 | 
				
			||||||
        super().save()
 | 
					        super().save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1979,7 +1982,7 @@ class BomImportUploadSerializer(InvenTree.serializers.DataFileUploadSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer):
 | 
					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.
 | 
					    The parent class DataFileExtractSerializer does most of the heavy lifting here.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,9 +46,7 @@ def convert_legacy_labels(table_name, model_name, template_model):
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            cursor.execute(query)
 | 
					            cursor.execute(query)
 | 
				
			||||||
        except Exception:
 | 
					        except Exception:
 | 
				
			||||||
            # Table likely does not exist
 | 
					            # Table likely does not exist - database was created more recently
 | 
				
			||||||
            if not InvenTree.ready.isInTestMode():
 | 
					 | 
				
			||||||
                print(f"\nLegacy label table {table_name} not found - skipping migration")
 | 
					 | 
				
			||||||
            return 0
 | 
					            return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        rows = cursor.fetchall()
 | 
					        rows = cursor.fetchall()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -764,8 +764,8 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
				
			|||||||
    def save(self):
 | 
					    def save(self):
 | 
				
			||||||
        """Serialize stock item."""
 | 
					        """Serialize stock item."""
 | 
				
			||||||
        item = self.context['item']
 | 
					        item = self.context['item']
 | 
				
			||||||
        request = self.context['request']
 | 
					        request = self.context.get('request')
 | 
				
			||||||
        user = request.user
 | 
					        user = request.user if request else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = self.validated_data
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user