diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py new file mode 100644 index 0000000000..46b1a1ee0a --- /dev/null +++ b/InvenTree/InvenTree/exceptions.py @@ -0,0 +1,76 @@ +""" +Custom exception handling for the DRF API +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import traceback +import sys + +from django.conf import settings +from django.core.exceptions import ValidationError as DjangoValidationError +from django.utils.translation import gettext_lazy as _ +from django.views.debug import ExceptionReporter + +from error_report.models import Error + +from rest_framework.exceptions import ValidationError as DRFValidationError +from rest_framework.response import Response +from rest_framework import serializers +import rest_framework.views as drfviews + + +def exception_handler(exc, context): + """ + Custom exception handler for DRF framework. + Ref: https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling + + Catches any errors not natively handled by DRF, and re-throws as an error DRF can handle + """ + + response = None + + # Catch any django validation error, and re-throw a DRF validation error + if isinstance(exc, DjangoValidationError): + exc = DRFValidationError(detail=serializers.as_serializer_error(exc)) + + # Default to the built-in DRF exception handler + response = drfviews.exception_handler(exc, context) + + if response is None: + # DRF handler did not provide a default response for this exception + + if settings.DEBUG: + error_detail = str(exc) + else: + error_detail = _("Error details can be found in the admin panel") + + response_data = { + 'error': type(exc).__name__, + 'error_class': str(type(exc)), + 'detail': error_detail, + 'path': context['request'].path, + 'status_code': 500, + } + + response = Response(response_data, status=500) + + # Log the exception to the database, too + kind, info, data = sys.exc_info() + + Error.objects.create( + kind=kind.__name__, + info=info, + data='\n'.join(traceback.format_exception(kind, info, data)), + path=context['request'].path, + html=ExceptionReporter(context['request'], kind, info, data).get_traceback_html(), + ) + + if response is not None: + # Convert errors returned under the label '__all__' to 'non_field_errors' + if '__all__' in response.data: + response.data['non_field_errors'] = response.data['__all__'] + del response.data['__all__'] + + return response diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index cba4ab4c6a..5c6bca4dea 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -353,7 +353,7 @@ TEMPLATES = [ ] REST_FRAMEWORK = { - 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', + 'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler', 'DATETIME_FORMAT': '%Y-%m-%d %H:%M', 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', diff --git a/InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py b/InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py new file mode 100644 index 0000000000..23915cf9d0 --- /dev/null +++ b/InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-05-16 14:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0067_auto_20220516_1120'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='salesorderallocation', + unique_together=set(), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e918f0a30c..7460e81e56 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -1269,12 +1269,6 @@ class SalesOrderAllocation(models.Model): def get_api_url(): return reverse('api-so-allocation-list') - class Meta: - unique_together = [ - # Cannot allocate any given StockItem to the same line more than once - ('line', 'item'), - ] - def clean(self): """ Validate the SalesOrderAllocation object: diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a58de213ea..47eacc40a0 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -1284,14 +1284,18 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer): with transaction.atomic(): for entry in items: + # Create a new SalesOrderAllocation - order.models.SalesOrderAllocation.objects.create( + allocation = order.models.SalesOrderAllocation( line=entry.get('line_item'), item=entry.get('stock_item'), quantity=entry.get('quantity'), shipment=shipment, ) + allocation.full_clean() + allocation.save() + class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer): """ Serializer for a SalesOrderExtraLine object """ diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index 119376c310..17b1b2fca0 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -225,6 +225,20 @@ function showApiError(xhr, url) { default: title = '{% trans "Unhandled Error Code" %}'; message = `{% trans "Error code" %}: ${xhr.status}`; + + var response = xhr.responseJSON; + + // The server may have provided some extra information about this error + if (response) { + if (response.error) { + title = response.error; + } + + if (response.detail) { + message = response.detail; + } + } + break; }