diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index b7ce609491..6db9e05719 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -7,6 +7,9 @@ INVENTREE_API_VERSION = 61 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v62 -> 2022-07-05 : https://github.com/inventree/InvenTree/pull/3296 + - Allows search on BOM List API endpoint + v61 -> 2022-06-12 : https://github.com/inventree/InvenTree/pull/3183 - Migrate the "Convert Stock Item" form class to use the API - There is now an API endpoint for converting a stock item to a valid variant diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 757559da06..95a41902d5 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -741,7 +741,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): """Initialization routine for the serializer""" order_detail = kwargs.pop('order_detail', False) part_detail = kwargs.pop('part_detail', True) - item_detail = kwargs.pop('item_detail', False) + item_detail = kwargs.pop('item_detail', True) location_detail = kwargs.pop('location_detail', False) customer_detail = kwargs.pop('customer_detail', False) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 2959206daa..c4cada7430 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -24,6 +24,7 @@ from common.models import InvenTreeSetting from company.models import Company, ManufacturerPart, SupplierPart from InvenTree.api import (APIDownloadMixin, AttachmentMixin, ListCreateDestroyAPIView) +from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.helpers import DownloadFile, increment, isNull, str2bool from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI, @@ -1756,12 +1757,32 @@ class BomList(ListCreateDestroyAPIView): filter_backends = [ DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter, + InvenTreeOrderingFilter, ] filterset_fields = [ ] + search_fields = [ + 'reference', + 'sub_part__name', + 'sub_part__description', + 'sub_part__IPN', + 'sub_part__revision', + 'sub_part__keywords', + 'sub_part__category__name', + ] + + ordering_fields = [ + 'quantity', + 'sub_part', + 'available_stock', + ] + + ordering_field_aliases = { + 'sub_part': 'sub_part__name', + } + class BomImportUpload(CreateAPI): """API endpoint for uploading a complete Bill of Materials. diff --git a/InvenTree/part/fixtures/bom.yaml b/InvenTree/part/fixtures/bom.yaml index ac52452d75..f09d1b8cf4 100644 --- a/InvenTree/part/fixtures/bom.yaml +++ b/InvenTree/part/fixtures/bom.yaml @@ -24,6 +24,7 @@ part: 100 sub_part: 5 quantity: 25 + reference: ABCDE # 3 x Orphan - model: part.bomitem @@ -32,6 +33,7 @@ part: 100 sub_part: 50 quantity: 3 + reference: VWXYZ - model: part.bomitem pk: 5 @@ -39,6 +41,7 @@ part: 1 sub_part: 5 quantity: 3 + reference: LMNOP # Make "Assembly" from "Bob" - model: part.bomitem diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 96057bc9c7..ec3d39abb3 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1647,6 +1647,102 @@ class BomItemTest(InvenTreeAPITestCase): for key in ['available_stock', 'available_substitute_stock']: self.assertTrue(key in el) + def test_bom_list_search(self): + """Test that we can search the BOM list API endpoint""" + + url = reverse('api-bom-list') + + response = self.get(url, expected_code=200) + + self.assertEqual(len(response.data), 6) + + # Limit the results with a search term + response = self.get( + url, + { + 'search': '0805', + }, + expected_code=200, + ) + + self.assertEqual(len(response.data), 3) + + # Search by 'reference' field + for q in ['ABCDE', 'LMNOP', 'VWXYZ']: + response = self.get( + url, + { + 'search': q, + }, + expected_code=200 + ) + + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['reference'], q) + + # Search by nonsense data + response = self.get( + url, + { + 'search': 'xxxxxxxxxxxxxxxxx', + }, + expected_code=200 + ) + + self.assertEqual(len(response.data), 0) + + def test_bom_list_ordering(self): + """Test that the BOM list results can be ordered""" + + url = reverse('api-bom-list') + + # Order by increasing quantity + response = self.get( + f"{url}?ordering=+quantity", + expected_code=200 + ) + + self.assertEqual(len(response.data), 6) + + q1 = response.data[0]['quantity'] + q2 = response.data[-1]['quantity'] + + self.assertTrue(q1 < q2) + + # Order by decreasing quantity + response = self.get( + f"{url}?ordering=-quantity", + expected_code=200, + ) + + self.assertEqual(q1, response.data[-1]['quantity']) + self.assertEqual(q2, response.data[0]['quantity']) + + # Now test ordering by 'sub_part' (which is actually 'sub_part__name') + response = self.get( + url, + { + 'ordering': 'sub_part', + 'sub_part_detail': True, + }, + expected_code=200, + ) + + n1 = response.data[0]['sub_part_detail']['name'] + n2 = response.data[-1]['sub_part_detail']['name'] + + response = self.get( + url, + { + 'ordering': '-sub_part', + 'sub_part_detail': True, + }, + expected_code=200, + ) + + self.assertEqual(n1, response.data[-1]['sub_part_detail']['name']) + self.assertEqual(n2, response.data[0]['sub_part_detail']['name']) + def test_get_bom_detail(self): """Get the detail view for a single BomItem object.""" url = reverse('api-bom-item-detail', kwargs={'pk': 3}) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 0909c02808..04bed69c6c 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -3236,10 +3236,12 @@ function showAllocationSubTable(index, row, element, options) { formatter: function(value, row, index, field) { var text = ''; - if (row.serial != null && row.quantity == 1) { - text = `{% trans "Serial Number" %}: ${row.serial}`; - } else { - text = `{% trans "Quantity" %}: ${row.quantity}`; + var item = row.item_detail; + + var text = `{% trans "Quantity" %}: ${row.quantity}`; + + if (item && item.serial != null && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${item.serial}`; } return renderLink(text, `/stock/item/${row.item}/`); diff --git a/requirements-dev.txt b/requirements-dev.txt index 2fb26e7727..a60d5bd101 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -94,9 +94,9 @@ distlib==0.3.4 \ --hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \ --hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579 # via virtualenv -django==3.2.13 \ - --hash=sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6 \ - --hash=sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf +django==3.2.14 \ + --hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \ + --hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56 # via # -c requirements.txt # django-debug-toolbar diff --git a/requirements.in b/requirements.in index dde42bbd8f..d02436ded1 100644 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,5 @@ # Please keep this list sorted - if you pin a version provide a reason -Django<4 # Django package +Django>=3.2.14,<4 # Django package coreapi # API documentation for djangorestframework cryptography==3.4.8 # Core cryptographic functionality django-allauth # SSO for external providers via OpenID diff --git a/requirements.txt b/requirements.txt index 27b5cf7493..32cfc56fa2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,7 +42,7 @@ defusedxml==0.7.1 # python3-openid diff-match-patch==20200713 # via django-import-export -django==3.2.13 +django==3.2.14 # via # -r requirements.in # django-allauth