2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

[Bug] Import fix (#9008) (#9321)

* [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

* Reintroduce typo

- To prevent API change
This commit is contained in:
Oliver 2025-03-17 21:11:27 +11:00 committed by GitHub
parent 3ffbb1cfc7
commit 92f6f8b1f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 75 additions and 49 deletions

View File

@ -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'

View File

@ -244,7 +244,12 @@ class BuildOutputSerializer(serializers.Serializer):
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
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: if not accept_incomplete:
raise ValidationError(_("This build output is not fully allocated")) raise ValidationError(_("This build output is not fully allocated"))
@ -426,6 +431,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(
@ -434,7 +440,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,
) )
@ -528,7 +534,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', [])
@ -541,7 +547,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)
) )
@ -617,7 +623,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
@ -638,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,
@ -711,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),
) )
@ -826,15 +832,18 @@ class BuildCompleteSerializer(serializers.Serializer):
return data return data
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('accept_overallocated', OverallocationChoice.REJECT) == OverallocationChoice.TRIM trim_allocated_stock=data.get(
'accept_overallocated', OverallocationChoice.REJECT
)
== OverallocationChoice.TRIM,
) )

View File

@ -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:

View File

@ -141,6 +141,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)
@ -154,6 +155,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
@ -163,6 +165,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,
@ -554,7 +560,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 = {}
@ -562,8 +568,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)
@ -615,16 +621,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
@ -636,7 +645,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 = {
@ -658,12 +667,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

View File

@ -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:

View File

@ -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()

View File

@ -933,7 +933,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']
@ -956,7 +956,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', ''),
@ -1416,8 +1416,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()
@ -1554,10 +1554,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))
@ -2002,7 +2002,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']
@ -2015,7 +2015,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),
) )

View File

@ -682,7 +682,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."""
@ -1061,8 +1061,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
@ -1122,7 +1122,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:
@ -1221,9 +1223,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()

View File

@ -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()

View File

@ -762,8 +762,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