diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 53ab0aaf14..391f6d1d35 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -49,9 +49,25 @@ {% if roles.stock.change %} - +
+ + +
{% endif %} {% if part.purchaseable %} {% if roles.purchase_order.add %} @@ -272,14 +288,34 @@ printPartLabels([{{ part.pk }}]); }); - $("#part-count").click(function() { - launchModalForm("/stock/adjust/", { - data: { - action: "count", + function adjustPartStock(action) { + inventreeGet( + '{% url "api-stock-list" %}', + { part: {{ part.id }}, + in_stock: true, + allow_variants: true, + part_detail: true, + location_detail: true, }, - reload: true, - }); + { + success: function(items) { + adjustStock(action, items, { + onSuccess: function() { + location.reload(); + } + }); + }, + } + ); + } + + $("#part-move").click(function() { + adjustPartStock('move'); + }); + + $("#part-count").click(function() { + adjustPartStock('count'); }); $("#price-button").click(function() { diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 4a6e7111e8..08e948607a 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -120,9 +120,6 @@ class StockAdjust(APIView): - StockAdd: add stock items - StockRemove: remove stock items - StockTransfer: transfer stock items - - # TODO - This needs serious refactoring!!! - """ queryset = StockItem.objects.none() @@ -143,7 +140,10 @@ class StockAdjust(APIView): elif 'items' in request.data: _items = request.data['items'] else: - raise ValidationError({'items': 'Request must contain list of stock items'}) + _items = [] + + if len(_items) == 0: + raise ValidationError(_('Request must contain list of stock items')) # List of validated items self.items = [] @@ -151,13 +151,22 @@ class StockAdjust(APIView): for entry in _items: if not type(entry) == dict: - raise ValidationError({'error': 'Improperly formatted data'}) + raise ValidationError(_('Improperly formatted data')) + + # Look for a 'pk' value (use 'id' as a backup) + pk = entry.get('pk', entry.get('id', None)) + + try: + pk = int(pk) + except (ValueError, TypeError): + raise ValidationError(_('Each entry must contain a valid integer primary-key')) try: - pk = entry.get('pk', None) item = StockItem.objects.get(pk=pk) - except (ValueError, StockItem.DoesNotExist): - raise ValidationError({'pk': 'Each entry must contain a valid pk field'}) + except (StockItem.DoesNotExist): + raise ValidationError({ + pk: [_('Primary key does not match valid stock item')] + }) if self.allow_missing_quantity and 'quantity' not in entry: entry['quantity'] = item.quantity @@ -165,16 +174,21 @@ class StockAdjust(APIView): try: quantity = Decimal(str(entry.get('quantity', None))) except (ValueError, TypeError, InvalidOperation): - raise ValidationError({'quantity': "Each entry must contain a valid quantity value"}) + raise ValidationError({ + pk: [_('Invalid quantity value')] + }) if quantity < 0: - raise ValidationError({'quantity': 'Quantity field must not be less than zero'}) + raise ValidationError({ + pk: [_('Quantity must not be less than zero')] + }) self.items.append({ 'item': item, 'quantity': quantity }) + # Extract 'notes' field self.notes = str(request.data.get('notes', '')) @@ -228,6 +242,11 @@ class StockRemove(StockAdjust): for item in self.items: + if item['quantity'] > item['item'].quantity: + raise ValidationError({ + item['item'].pk: [_('Specified quantity exceeds stock quantity')] + }) + if item['item'].take_stock(item['quantity'], request.user, notes=self.notes): n += 1 @@ -243,19 +262,24 @@ class StockTransfer(StockAdjust): def post(self, request, *args, **kwargs): - self.get_items(request) - data = request.data try: location = StockLocation.objects.get(pk=data.get('location', None)) except (ValueError, StockLocation.DoesNotExist): - raise ValidationError({'location': 'Valid location must be specified'}) + raise ValidationError({'location': [_('Valid location must be specified')]}) n = 0 + self.get_items(request) + for item in self.items: + if item['quantity'] > item['item'].quantity: + raise ValidationError({ + item['item'].pk: [_('Specified quantity exceeds stock quantity')] + }) + # If quantity is not specified, move the entire stock if item['quantity'] in [0, None]: item['quantity'] = item['item'].quantity @@ -454,13 +478,6 @@ class StockList(generics.ListCreateAPIView): - GET: Return a list of all StockItem objects (with optional query filters) - POST: Create a new StockItem - - Additional query parameters are available: - - location: Filter stock by location - - category: Filter by parts belonging to a certain category - - supplier: Filter by supplier - - ancestor: Filter by an 'ancestor' StockItem - - status: Filter by the StockItem status """ serializer_class = StockItemSerializer @@ -482,7 +499,6 @@ class StockList(generics.ListCreateAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - # TODO - Save the user who created this item item = serializer.save() # A location was *not* specified - try to infer it @@ -1092,47 +1108,41 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = LocationSerializer -stock_endpoints = [ - url(r'^$', StockDetail.as_view(), name='api-stock-detail'), -] - -location_endpoints = [ - url(r'^(?P\d+)/', LocationDetail.as_view(), name='api-location-detail'), - - url(r'^.*$', StockLocationList.as_view(), name='api-location-list'), -] - stock_api_urls = [ - url(r'location/', include(location_endpoints)), + url(r'^location/', include([ + url(r'^(?P\d+)/', LocationDetail.as_view(), name='api-location-detail'), + url(r'^.*$', StockLocationList.as_view(), name='api-location-list'), + ])), - # These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019 - # TODO: Remove server-side forms for stock adjustment!!! - url(r'count/?', StockCount.as_view(), name='api-stock-count'), - url(r'add/?', StockAdd.as_view(), name='api-stock-add'), - url(r'remove/?', StockRemove.as_view(), name='api-stock-remove'), - url(r'transfer/?', StockTransfer.as_view(), name='api-stock-transfer'), + # Endpoints for bulk stock adjustment actions + url(r'^count/', StockCount.as_view(), name='api-stock-count'), + url(r'^add/', StockAdd.as_view(), name='api-stock-add'), + url(r'^remove/', StockRemove.as_view(), name='api-stock-remove'), + url(r'^transfer/', StockTransfer.as_view(), name='api-stock-transfer'), - # Base URL for StockItemAttachment API endpoints + # StockItemAttachment API endpoints url(r'^attachment/', include([ url(r'^(?P\d+)/', StockAttachmentDetail.as_view(), name='api-stock-attachment-detail'), url(r'^$', StockAttachmentList.as_view(), name='api-stock-attachment-list'), ])), - # Base URL for StockItemTestResult API endpoints + # StockItemTestResult API endpoints url(r'^test/', include([ url(r'^(?P\d+)/', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'), url(r'^.*$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'), ])), + # StockItemTracking API endpoints url(r'^track/', include([ url(r'^(?P\d+)/', StockTrackingDetail.as_view(), name='api-stock-tracking-detail'), url(r'^.*$', StockTrackingList.as_view(), name='api-stock-tracking-list'), ])), - url(r'^tree/?', StockCategoryTree.as_view(), name='api-stock-tree'), + url(r'^tree/', StockCategoryTree.as_view(), name='api-stock-tree'), # Detail for a single stock item - url(r'^(?P\d+)/', include(stock_endpoints)), + url(r'^(?P\d+)/', StockDetail.as_view(), name='api-stock-detail'), + # Anything else url(r'^.*$', StockList.as_view(), name='api-stock-list'), ] diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 2eb7695498..979c54ba28 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -534,14 +534,21 @@ $("#barcode-scan-into-location").click(function() { }); function itemAdjust(action) { - launchModalForm("/stock/adjust/", + + inventreeGet( + '{% url "api-stock-detail" item.pk %}', { - data: { - action: action, - item: {{ item.id }}, - }, - reload: true, - follow: true, + part_detail: true, + location_detail: true, + }, + { + success: function(item) { + adjustStock(action, [item], { + onSuccess: function() { + location.reload(); + } + }); + } } ); } diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 6a69be260e..d70d9a44be 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -59,11 +59,23 @@ {% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %} {% if roles.stock.change %} {% endif %} {% if roles.stock_location.change %} @@ -215,14 +227,34 @@ }); {% if location %} - $("#location-count").click(function() { - launchModalForm("/stock/adjust/", { - data: { - action: "count", + + function adjustLocationStock(action) { + inventreeGet( + '{% url "api-stock-list" %}', + { location: {{ location.id }}, - reload: true, + in_stock: true, + part_detail: true, + location_detail: true, + }, + { + success: function(items) { + adjustStock(action, items, { + onSuccess: function() { + location.reload(); + } + }); + } } - }); + ); + } + + $("#location-count").click(function() { + adjustLocationStock('count'); + }); + + $("#location-move").click(function() { + adjustLocationStock('move'); }); $('#print-label').click(function() { diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 729bf25a9b..74f9505c4a 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -7,8 +7,8 @@ from __future__ import unicode_literals from datetime import datetime, timedelta -from rest_framework import status from django.urls import reverse +from rest_framework import status from InvenTree.status_codes import StockStatus from InvenTree.api_tester import InvenTreeAPITestCase @@ -456,30 +456,32 @@ class StocktakeTest(StockAPITestCase): # POST without a PK response = self.post(url, data) - self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'must contain a valid integer primary-key', status_code=status.HTTP_400_BAD_REQUEST) - # POST with a PK but no quantity + # POST with an invalid PK data['items'] = [{ 'pk': 10 }] response = self.post(url, data) - self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'does not match valid stock item', status_code=status.HTTP_400_BAD_REQUEST) + # POST with missing quantity value data['items'] = [{ 'pk': 1234 }] response = self.post(url, data) - self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST) + # POST with an invalid quantity value data['items'] = [{ 'pk': 1234, 'quantity': '10x0d' }] response = self.post(url, data) - self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ 'pk': 1234, diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index c565532739..9494598430 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -105,31 +105,6 @@ class StockItemTest(StockViewTestCase): response = self.client.get(reverse('stock-item-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - def test_adjust_items(self): - url = reverse('stock-adjust') - - # Move items - response = self.client.get(url, {'stock[]': [1, 2, 3, 4, 5], 'action': 'move'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Count part - response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Remove items - response = self.client.get(url, {'location': 1, 'action': 'take'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Add items - response = self.client.get(url, {'item': 1, 'action': 'add'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Blank response - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # TODO - Tests for POST data - def test_edit_item(self): # Test edit view for StockItem response = self.client.get(reverse('stock-item-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index ac9474f805..67101c1f3b 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -64,8 +64,6 @@ stock_urls = [ url(r'^track/', include(stock_tracking_urls)), - url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'), - url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'), url(r'^export/?', views.StockExport.as_view(), name='stock-export'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index c763afe38f..6b64e8b54a 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -749,343 +749,6 @@ class StockItemUninstall(AjaxView, FormMixin): return context -class StockAdjust(AjaxView, FormMixin): - """ View for enacting simple stock adjustments: - - - Take items from stock - - Add items to stock - - Count items - - Move stock - - Delete stock items - - """ - - ajax_template_name = 'stock/stock_adjust.html' - ajax_form_title = _('Adjust Stock') - form_class = StockForms.AdjustStockForm - stock_items = [] - role_required = 'stock.change' - - def get_GET_items(self): - """ Return list of stock items initally requested using GET. - - Items can be retrieved by: - - a) List of stock ID - stock[]=1,2,3,4,5 - b) Parent part - part=3 - c) Parent location - location=78 - d) Single item - item=2 - """ - - # Start with all 'in stock' items - items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) - - # Client provides a list of individual stock items - if 'stock[]' in self.request.GET: - items = items.filter(id__in=self.request.GET.getlist('stock[]')) - - # Client provides a PART reference - elif 'part' in self.request.GET: - items = items.filter(part=self.request.GET.get('part')) - - # Client provides a LOCATION reference - elif 'location' in self.request.GET: - items = items.filter(location=self.request.GET.get('location')) - - # Client provides a single StockItem lookup - elif 'item' in self.request.GET: - items = [StockItem.objects.get(id=self.request.GET.get('item'))] - - # Unsupported query (no items) - else: - items = [] - - for item in items: - - # Initialize quantity to zero for addition/removal - if self.stock_action in ['take', 'add']: - item.new_quantity = 0 - # Initialize quantity at full amount for counting or moving - else: - item.new_quantity = item.quantity - - return items - - def get_POST_items(self): - """ Return list of stock items sent back by client on a POST request """ - - items = [] - - for item in self.request.POST: - if item.startswith('stock-id-'): - - pk = item.replace('stock-id-', '') - q = self.request.POST[item] - - try: - stock_item = StockItem.objects.get(pk=pk) - except StockItem.DoesNotExist: - continue - - stock_item.new_quantity = q - - items.append(stock_item) - - return items - - def get_stock_action_titles(self): - - # Choose form title and action column based on the action - titles = { - 'move': [_('Move Stock Items'), _('Move')], - 'count': [_('Count Stock Items'), _('Count')], - 'take': [_('Remove From Stock'), _('Take')], - 'add': [_('Add Stock Items'), _('Add')], - 'delete': [_('Delete Stock Items'), _('Delete')], - } - - self.ajax_form_title = titles[self.stock_action][0] - self.stock_action_title = titles[self.stock_action][1] - - def get_context_data(self): - - context = super().get_context_data() - - context['stock_items'] = self.stock_items - - context['stock_action'] = self.stock_action.strip().lower() - - self.get_stock_action_titles() - context['stock_action_title'] = self.stock_action_title - - # Quantity column will be read-only in some circumstances - context['edit_quantity'] = not self.stock_action == 'delete' - - return context - - def get_form(self): - - form = super().get_form() - - if not self.stock_action == 'move': - form.fields.pop('destination') - form.fields.pop('set_loc') - - return form - - def get(self, request, *args, **kwargs): - - self.request = request - - # Action - self.stock_action = request.GET.get('action', '').lower() - - # Pick a default action... - if self.stock_action not in ['move', 'count', 'take', 'add', 'delete']: - self.stock_action = 'count' - - # Save list of items! - self.stock_items = self.get_GET_items() - - return self.renderJsonResponse(request, self.get_form()) - - def post(self, request, *args, **kwargs): - - self.request = request - - self.stock_action = request.POST.get('stock_action', 'invalid').strip().lower() - - # Update list of stock items - self.stock_items = self.get_POST_items() - - form = self.get_form() - - valid = form.is_valid() - - for item in self.stock_items: - - try: - item.new_quantity = Decimal(item.new_quantity) - except ValueError: - item.error = _('Must enter integer value') - valid = False - continue - - if item.new_quantity < 0: - item.error = _('Quantity must be positive') - valid = False - continue - - if self.stock_action in ['move', 'take']: - - if item.new_quantity > item.quantity: - item.error = _('Quantity must not exceed {x}').format(x=item.quantity) - valid = False - continue - - confirmed = str2bool(request.POST.get('confirm')) - - if not confirmed: - valid = False - form.add_error('confirm', _('Confirm stock adjustment')) - - data = { - 'form_valid': valid, - } - - if valid: - result = self.do_action(note=form.cleaned_data['note']) - - data['success'] = result - - # Special case - Single Stock Item - # If we deplete the stock item, we MUST redirect to a new view - single_item = len(self.stock_items) == 1 - - if result and single_item: - - # Was the entire stock taken? - item = self.stock_items[0] - - if item.quantity == 0: - # Instruct the form to redirect - data['url'] = reverse('stock-index') - - return self.renderJsonResponse(request, form, data=data, context=self.get_context_data()) - - def do_action(self, note=None): - """ Perform stock adjustment action """ - - if self.stock_action == 'move': - destination = None - - set_default_loc = str2bool(self.request.POST.get('set_loc', False)) - - try: - destination = StockLocation.objects.get(id=self.request.POST.get('destination')) - except StockLocation.DoesNotExist: - pass - except ValueError: - pass - - return self.do_move(destination, set_default_loc, note=note) - - elif self.stock_action == 'add': - return self.do_add(note=note) - - elif self.stock_action == 'take': - return self.do_take(note=note) - - elif self.stock_action == 'count': - return self.do_count(note=note) - - elif self.stock_action == 'delete': - return self.do_delete(note=note) - - else: - return _('No action performed') - - def do_add(self, note=None): - - count = 0 - - for item in self.stock_items: - if item.new_quantity <= 0: - continue - - item.add_stock(item.new_quantity, self.request.user, notes=note) - - count += 1 - - return _('Added stock to {n} items').format(n=count) - - def do_take(self, note=None): - - count = 0 - - for item in self.stock_items: - if item.new_quantity <= 0: - continue - - item.take_stock(item.new_quantity, self.request.user, notes=note) - - count += 1 - - return _('Removed stock from {n} items').format(n=count) - - def do_count(self, note=None): - - count = 0 - - for item in self.stock_items: - - item.stocktake(item.new_quantity, self.request.user, notes=note) - - count += 1 - - return _("Counted stock for {n} items".format(n=count)) - - def do_move(self, destination, set_loc=None, note=None): - """ Perform actual stock movement """ - - count = 0 - - for item in self.stock_items: - # Avoid moving zero quantity - if item.new_quantity <= 0: - continue - - # If we wish to set the destination location to the default one - if set_loc: - item.part.default_location = destination - item.part.save() - - # Do not move to the same location (unless the quantity is different) - if destination == item.location and item.new_quantity == item.quantity: - continue - - item.move(destination, note, self.request.user, quantity=item.new_quantity) - - count += 1 - - # Is ownership control enabled? - stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') - - if stock_ownership_control: - # Fetch destination owner - destination_owner = destination.owner - - if destination_owner: - # Update owner - item.owner = destination_owner - item.save() - - if count == 0: - return _('No items were moved') - - else: - return _('Moved {n} items to {dest}').format( - n=count, - dest=destination.pathstring) - - def do_delete(self): - """ Delete multiple stock items """ - - count = 0 - # note = self.request.POST['note'] - - for item in self.stock_items: - - # TODO - In the future, StockItems should not be 'deleted' - # TODO - Instead, they should be marked as "inactive" - - item.delete() - - count += 1 - - return _("Deleted {n} stock items").format(n=count) - - class StockItemEdit(AjaxUpdateView): """ View for editing details of a single StockItem diff --git a/InvenTree/templates/js/api.js b/InvenTree/templates/js/api.js index 5e8905a1dd..93fa5a41e4 100644 --- a/InvenTree/templates/js/api.js +++ b/InvenTree/templates/js/api.js @@ -1,3 +1,6 @@ +{% load i18n %} +{% load inventree_extras %} + var jQuery = window.$; // using jQuery @@ -138,4 +141,49 @@ function inventreeDelete(url, options={}) { inventreePut(url, {}, options); +} + + +function showApiError(xhr) { + + var title = null; + var message = null; + + switch (xhr.status) { + case 0: // No response + title = '{% trans "No Response" %}'; + message = '{% trans "No response from the InvenTree server" %}'; + break; + case 400: // Bad request + // Note: Normally error code 400 is handled separately, + // and should now be shown here! + title = '{% trans "Error 400: Bad request" %}'; + message = '{% trans "API request returned error code 400" %}'; + break; + case 401: // Not authenticated + title = '{% trans "Error 401: Not Authenticated" %}'; + message = '{% trans "Authentication credentials not supplied" %}'; + break; + case 403: // Permission denied + title = '{% trans "Error 403: Permission Denied" %}'; + message = '{% trans "You do not have the required permissions to access this function" %}'; + break; + case 404: // Resource not found + title = '{% trans "Error 404: Resource Not Found" %}'; + message = '{% trans "The requested resource could not be located on the server" %}'; + break; + case 408: // Timeout + title = '{% trans "Error 408: Timeout" %}'; + message = '{% trans "Connection timeout while requesting data from server" %}'; + break; + default: + title = '{% trans "Unhandled Error Code" %}'; + message = `{% trans "Error code" %}: ${xhr.status}`; + break; + } + + message += "
"; + message += renderErrorMessage(xhr); + + showAlertDialog(title, message); } \ No newline at end of file diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index b7af665393..b71551747c 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -395,11 +395,11 @@ function constructFormBody(fields, options) { for (var name in displayed_fields) { - // Only push names which are actually in the set of fields - if (name in fields) { - field_names.push(name); - } else { - console.log(`WARNING: '${name}' does not match a valid field name.`); + field_names.push(name); + + // Field not specified in the API, but the client wishes to add it! + if (!(name in fields)) { + fields[name] = displayed_fields[name]; } } @@ -422,10 +422,8 @@ function constructFormBody(fields, options) { default: break; } - - var f = constructField(name, field, options); - html += f; + html += constructField(name, field, options); } // TODO: Dynamically create the modals, @@ -441,7 +439,15 @@ function constructFormBody(fields, options) { modalEnable(modal, true); // Insert generated form content - $(modal).find('.modal-form-content').html(html); + $(modal).find('#form-content').html(html); + + if (options.preFormContent) { + $(modal).find('#pre-form-content').html(options.preFormContent); + } + + if (options.postFormContent) { + $(modal).find('#post-form-content').html(options.postFormContent); + } // Clear any existing buttons from the modal $(modal).find('#modal-footer-buttons').html(''); @@ -474,7 +480,21 @@ function constructFormBody(fields, options) { $(modal).on('click', '#modal-form-submit', function() { - submitFormData(fields, options); + // Immediately disable the "submit" button, + // to prevent the form being submitted multiple times! + $(options.modal).find('#modal-form-submit').prop('disabled', true); + + // Run custom code before normal form submission + if (options.beforeSubmit) { + options.beforeSubmit(fields, options); + } + + // Run custom code instead of normal form submission + if (options.onSubmit) { + options.onSubmit(fields, options); + } else { + submitFormData(fields, options); + } }); } @@ -511,10 +531,6 @@ function insertConfirmButton(options) { */ function submitFormData(fields, options) { - // Immediately disable the "submit" button, - // to prevent the form being submitted multiple times! - $(options.modal).find('#modal-form-submit').prop('disabled', true); - // Form data to be uploaded to the server // Only used if file / image upload is required var form_data = new FormData(); @@ -581,47 +597,9 @@ function submitFormData(fields, options) { case 400: // Bad request handleFormErrors(xhr.responseJSON, fields, options); break; - case 0: // No response - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "No Response" %}', - '{% trans "No response from the InvenTree server" %}', - ); - break; - case 401: // Not authenticated - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 401: Not Authenticated" %}', - '{% trans "Authentication credentials not supplied" %}', - ); - break; - case 403: // Permission denied - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 403: Permission Denied" %}', - '{% trans "You do not have the required permissions to access this function" %}', - ); - break; - case 404: // Resource not found - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 404: Resource Not Found" %}', - '{% trans "The requested resource could not be located on the server" %}', - ); - break; - case 408: // Timeout - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 408: Timeout" %}', - '{% trans "Connection timeout while requesting data from server" %}', - ); - break; default: $(options.modal).modal('hide'); - - showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr)); - - console.log(`WARNING: Unhandled response code - ${xhr.status}`); + showApiError(xhr); break; } } @@ -697,6 +675,10 @@ function getFormFieldValue(name, field, options) { // Find the HTML element var el = $(options.modal).find(`#id_${name}`); + if (!el) { + return null; + } + var value = null; switch (field.type) { @@ -834,33 +816,27 @@ function handleFormErrors(errors, fields, options) { } for (field_name in errors) { - if (field_name in fields) { - // Add the 'has-error' class - $(options.modal).find(`#div_id_${field_name}`).addClass('has-error'); + // Add the 'has-error' class + $(options.modal).find(`#div_id_${field_name}`).addClass('has-error'); - var field_dom = $(options.modal).find(`#errors-${field_name}`); // $(options.modal).find(`#id_${field_name}`); + var field_dom = $(options.modal).find(`#errors-${field_name}`); // $(options.modal).find(`#id_${field_name}`); - var field_errors = errors[field_name]; + var field_errors = errors[field_name]; - // Add an entry for each returned error message - for (var idx = field_errors.length-1; idx >= 0; idx--) { + // Add an entry for each returned error message + for (var idx = field_errors.length-1; idx >= 0; idx--) { - var error_text = field_errors[idx]; + var error_text = field_errors[idx]; - var html = ` - - ${error_text} - `; + var html = ` + + ${error_text} + `; - field_dom.append(html); - } - - } else { - console.log(`WARNING: handleFormErrors found no match for field '${field_name}'`); + field_dom.append(html); } } - } @@ -1464,21 +1440,21 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`readonly=''`); } - if (parameters.value) { + if (parameters.value != null) { // Existing value? opts.push(`value='${parameters.value}'`); - } else if (parameters.default) { + } else if (parameters.default != null) { // Otherwise, a defualt value? opts.push(`value='${parameters.default}'`); } // Maximum input length - if (parameters.max_length) { + if (parameters.max_length != null) { opts.push(`maxlength='${parameters.max_length}'`); } // Minimum input length - if (parameters.min_length) { + if (parameters.min_length != null) { opts.push(`minlength='${parameters.min_length}'`); } @@ -1497,8 +1473,13 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`required=''`); } + // Custom mouseover title? + if (parameters.title != null) { + opts.push(`title='${parameters.title}'`); + } + // Placeholder? - if (parameters.placeholder) { + if (parameters.placeholder != null) { opts.push(`placeholder='${parameters.placeholder}'`); } diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index b613ed81f6..b404af364c 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -12,7 +12,6 @@ */ function createNewModal(options={}) { - var id = 1; // Check out what modal forms are already being displayed @@ -39,12 +38,13 @@ function createNewModal(options={}) { - - - - - - \ No newline at end of file + \ No newline at end of file