Repository: inventree/InvenTree

Issue:
[FR] Move download URL endpoints to API namespace
### Please verify that this feature request has NOT been suggested before.

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

### Problem statement

The download endpoints for the BOM and BOM template are currently served under /parts; they should however be housed under /api

### Suggested solution

Move them to /api

### Describe alternatives you've considered

N/A

### Examples of other systems

_No response_

### Do you want to develop this?

- [ ] I want to develop this.
[FR] Move download URL endpoints to API namespace
### Please verify that this feature request has NOT been suggested before.

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

### Problem statement

The download endpoints for the BOM and BOM template are currently served under /parts; they should however be housed under /api

### Suggested solution

Move them to /api

### Describe alternatives you've considered

N/A

### Examples of other systems

_No response_

### Do you want to develop this?

- [ ] I want to develop this.


Discussion:
Needed for https://github.com/inventree/InvenTree/issues/2789
Needed for https://github.com/inventree/InvenTree/issues/2789

Resolving patch:
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -36,6 +36,7 @@
 from stock.models import StockItem, StockLocation
 
 from . import serializers as part_serializers
+from . import views
 from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
                      PartCategory, PartCategoryParameterTemplate,
                      PartInternalPriceBreak, PartParameter,
@@ -2196,6 +2197,9 @@ class BomItemSubstituteDetail(RetrieveUpdateDestroyAPI):
         re_path(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
     ])),
 
+    # BOM template
+    re_path(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='api-bom-upload-template'),
+
     re_path(r'^(?P<pk>\d+)/', include([
 
         # Endpoint for extra serial number information
@@ -2218,6 +2222,15 @@ class BomItemSubstituteDetail(RetrieveUpdateDestroyAPI):
         # Part pricing
         re_path(r'^pricing/', PartPricingDetail.as_view(), name='api-part-pricing'),
 
+        # BOM download
+        re_path(r'^bom-download/?', views.BomDownload.as_view(), name='api-bom-download'),
+
+        # QR code download
+        re_path(r'^qr_code/?', views.PartQRCode.as_view(), name='api-part-qr'),
+
+        # Old pricing endpoint
+        re_path(r'^pricing2/', views.PartPricing.as_view(), name='part-pricing'),
+
         # Part detail endpoint
         re_path(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
     ])),
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -11,14 +11,8 @@
 from . import views
 
 part_detail_urls = [
-    re_path(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
-
-    re_path(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
-
     re_path(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
 
-    re_path(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
-
     # Normal thumbnail with form
     re_path(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'),
 
@@ -40,9 +34,6 @@
     re_path(r'^import/?', views.PartImportTemplate.as_view(), name='part-template-download'),
     re_path(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
 
-    # Download a BOM upload template
-    re_path(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'),
-
     # Individual part using pk
     re_path(r'^(?P<pk>\d+)/', include(part_detail_urls)),
 


Test patch:
diff --git a/InvenTree/part/test_bom_export.py b/InvenTree/part/test_bom_export.py
--- a/InvenTree/part/test_bom_export.py
+++ b/InvenTree/part/test_bom_export.py
@@ -23,11 +23,11 @@ def setUp(self):
         """Perform test setup functions"""
         super().setUp()
 
-        self.url = reverse('bom-download', kwargs={'pk': 100})
+        self.url = reverse('api-bom-download', kwargs={'pk': 100})
 
     def test_bom_template(self):
         """Test that the BOM template can be downloaded from the server."""
-        url = reverse('bom-upload-template')
+        url = reverse('api-bom-upload-template')
 
         # Download an XLS template
         response = self.client.get(url, data={'format': 'xls'})
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py
--- a/InvenTree/part/test_views.py
+++ b/InvenTree/part/test_views.py
@@ -110,7 +110,7 @@ def test_ipn_match(index_result=False, detail_result=False):
 
     def test_bom_download(self):
         """Test downloading a BOM for a valid part."""
-        response = self.client.get(reverse('bom-download', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+        response = self.client.get(reverse('api-bom-download', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
         self.assertEqual(response.status_code, 200)
         self.assertIn('streaming_content', dir(response))
 
@@ -120,12 +120,12 @@ class PartQRTest(PartViewTestCase):
 
     def test_html_redirect(self):
         """A HTML request for a QR code should be redirected (use an AJAX request instead)"""
-        response = self.client.get(reverse('part-qr', args=(1,)))
+        response = self.client.get(reverse('api-part-qr', args=(1,)))
         self.assertEqual(response.status_code, 302)
 
     def test_valid_part(self):
         """Test QR code response for a Part"""
-        response = self.client.get(reverse('part-qr', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+        response = self.client.get(reverse('api-part-qr', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
         self.assertEqual(response.status_code, 200)
 
         data = str(response.content)
@@ -135,6 +135,6 @@ def test_valid_part(self):
 
     def test_invalid_part(self):
         """Test response for an invalid Part ID value"""
-        response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+        response = self.client.get(reverse('api-part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
 
         self.assertEqual(response.status_code, 200)