2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +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:
Oliver 2025-02-02 07:57:06 +11:00 committed by GitHub
parent eba004d835
commit c077e2b605
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 79 additions and 54 deletions

View File

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

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

View File

@ -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
if request := self.context.get('request'):
accept_incomplete = InvenTree.helpers.str2bool(
self.context['request'].data.get(
'accept_incomplete_allocation', False
)
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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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