Repository: inventree/InvenTree

Issue:
[FR] List PO/SO/BO by reference in API
### Please verify that this feature request has NOT been suggested before.

- [X] I checked and didn't find similar feature request

### Problem statement

When accessing orders through the API, I always have to find the ID of the order first - ie list them, look for the reference I want, then get the ID.
If the API would accept a reference field, this would be reduced to one step, eg:
`https://demo.inventree.org/api/order/po/?reference=PO0006`


### Suggested solution

Compare to how Parts works: I can request an IPN directly using the URL:
`https://demo.inventree.org/api/part/?IPN=widget.blue`

### Describe alternatives you've considered

List -> search for reference -> return ID

### Examples of other systems

_No response_

### Do you want to develop this?

- [ ] I want to develop this.


Discussion:
@miggland should be reasonably straight-forward as the reference field is guaranteed to be unique (unlike the Part IPN field)
This issue seems stale. Please react to show this is still important.
This is still relevant.

Resolving patch:
diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -2,11 +2,14 @@
 
 
 # InvenTree API version
-INVENTREE_API_VERSION = 83
+INVENTREE_API_VERSION = 84
 
 """
 Increment this API version number whenever there is a significant change to the API that any clients need to know about
 
+v84 -> 2022-12-21: https://github.com/inventree/InvenTree/pull/4083
+    - Add support for listing PO, BO, SO by their reference
+
 v83 -> 2022-11-19 : https://github.com/inventree/InvenTree/pull/3949
     - Add support for structural Stock locations
 
diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -65,6 +65,13 @@ def filter_assigned_to_me(self, queryset, name, value):
 
         return queryset
 
+    # Exact match for reference
+    reference = rest_filters.CharFilter(
+        label='Filter by exact reference',
+        field_name='reference',
+        lookup_expr="iexact"
+    )
+
 
 class BuildList(APIDownloadMixin, ListCreateAPI):
     """API endpoint for accessing a list of Build objects.
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -78,8 +78,15 @@ def get_queryset(self, *args, **kwargs):
     ]
 
 
-class PurchaseOrderFilter(rest_filters.FilterSet):
-    """Custom API filters for the PurchaseOrderList endpoint."""
+class OrderFilter(rest_filters.FilterSet):
+    """Base class for custom API filters for the OrderList endpoint."""
+
+    # Exact match for reference
+    reference = rest_filters.CharFilter(
+        label='Filter by exact reference',
+        field_name='reference',
+        lookup_expr="iexact"
+    )
 
     assigned_to_me = rest_filters.BooleanFilter(label='assigned_to_me', method='filter_assigned_to_me')
 
@@ -97,6 +104,9 @@ def filter_assigned_to_me(self, queryset, name, value):
 
         return queryset
 
+
+class PurchaseOrderFilter(OrderFilter):
+    """Custom API filters for the PurchaseOrderList endpoint."""
     class Meta:
         """Metaclass options."""
 
@@ -106,6 +116,17 @@ class Meta:
         ]
 
 
+class SalesOrderFilter(OrderFilter):
+    """Custom API filters for the SalesOrderList endpoint."""
+    class Meta:
+        """Metaclass options."""
+
+        model = models.SalesOrder
+        fields = [
+            'customer',
+        ]
+
+
 class PurchaseOrderList(APIDownloadMixin, ListCreateAPI):
     """API endpoint for accessing a list of PurchaseOrder objects.
 
@@ -613,6 +634,7 @@ class SalesOrderList(APIDownloadMixin, ListCreateAPI):
 
     queryset = models.SalesOrder.objects.all()
     serializer_class = serializers.SalesOrderSerializer
+    filterset_class = SalesOrderFilter
 
     def create(self, request, *args, **kwargs):
         """Save user information on create."""


Test patch:
diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py
--- a/InvenTree/build/test_api.py
+++ b/InvenTree/build/test_api.py
@@ -63,6 +63,14 @@ def test_get_build_list(self):
         response = self.client.get(url, {'part': 99999}, format='json')
         self.assertEqual(len(response.data), 0)
 
+        # Get a certain reference
+        response = self.client.get(url, {'reference': 'BO-0001'}, format='json')
+        self.assertEqual(len(response.data), 1)
+
+        # Get a certain reference
+        response = self.client.get(url, {'reference': 'BO-9999XX'}, format='json')
+        self.assertEqual(len(response.data), 0)
+
     def test_get_build_item_list(self):
         """Test that we can retrieve list of BuildItem objects."""
         url = reverse('api-build-item-list')
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -72,6 +72,14 @@ def test_po_list(self):
         self.filter({'status': 10}, 3)
         self.filter({'status': 40}, 1)
 
+        # Filter by "reference"
+        self.filter({'reference': 'PO-0001'}, 1)
+        self.filter({'reference': 'PO-9999'}, 0)
+
+        # Filter by "assigned_to_me"
+        self.filter({'assigned_to_me': 1}, 0)
+        self.filter({'assigned_to_me': 0}, 7)
+
         # Filter by "part"
         self.filter({'part': 1}, 2)
         self.filter({'part': 2}, 0)  # Part not assigned to any PO
@@ -878,6 +886,14 @@ def test_so_list(self):
         self.filter({'status': 20}, 1)  # SHIPPED
         self.filter({'status': 99}, 0)  # Invalid
 
+        # Filter by "reference"
+        self.filter({'reference': 'ABC123'}, 1)
+        self.filter({'reference': 'XXX999'}, 0)
+
+        # Filter by "assigned_to_me"
+        self.filter({'assigned_to_me': 1}, 0)
+        self.filter({'assigned_to_me': 0}, 5)
+
     def test_overdue(self):
         """Test "overdue" status."""
         self.filter({'overdue': True}, 0)