mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-15 05:32:21 +00:00
Refactor API endpoint: Company (4/6) (#10443)
* add output options for ManufacturerPart, ManufacturerPartParameter, and SupplierPriceBreak endpoints * enhance output options for SupplierPart and SupplierPriceBreak APIs * update api_version --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 395
|
INVENTREE_API_VERSION = 396
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v396 -> 2025-10-01 : https://github.com/inventree/InvenTree/pull/10443
|
||||||
|
- Refactors 'part_detail', 'manufacturer_detail' and 'pretty' params in Manufacturer API endpoint
|
||||||
|
- Refactors 'manufacturer_part_detail' param in ManufacturerPartParameter API endpoint
|
||||||
|
- Refactors 'part_detail' and 'supplier_detail' param in SupplierPriceBreak API endpoint
|
||||||
|
|
||||||
v395 -> 2025-10-01 : https://github.com/inventree/InvenTree/pull/10441
|
v395 -> 2025-10-01 : https://github.com/inventree/InvenTree/pull/10441
|
||||||
- Refactors 'parameters', 'category_detail', 'location_detail' and 'path_detail' params in Part API endpoint
|
- Refactors 'parameters', 'category_detail', 'location_detail' and 'path_detail' params in Part API endpoint
|
||||||
- Refactors 'can_build', 'part_detail', 'sub_part_detail' and 'path_detail' params in BOM API endpoint
|
- Refactors 'can_build', 'part_detail', 'sub_part_detail' and 'path_detail' params in BOM API endpoint
|
||||||
|
@@ -12,7 +12,6 @@ from data_exporter.mixins import DataExportViewMixin
|
|||||||
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
||||||
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
|
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
||||||
from InvenTree.helpers import str2bool
|
|
||||||
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin, RetrieveUpdateDestroyAPI
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
@@ -148,7 +147,31 @@ class ManufacturerPartFilter(FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerPartList(DataExportViewMixin, ListCreateDestroyAPIView):
|
class ManufacturerOutputOptions(OutputConfiguration):
|
||||||
|
"""Available output options for the ManufacturerPart endpoints."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Include detailed information about the linked Part in the response',
|
||||||
|
flag='part_detail',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Include detailed information about the Manufacturer in the response',
|
||||||
|
flag='manufacturer_detail',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Format the output with a more readable (pretty) name',
|
||||||
|
flag='pretty',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartList(
|
||||||
|
DataExportViewMixin, OutputOptionsMixin, ListCreateDestroyAPIView
|
||||||
|
):
|
||||||
"""API endpoint for list view of ManufacturerPart object.
|
"""API endpoint for list view of ManufacturerPart object.
|
||||||
|
|
||||||
- GET: Return list of ManufacturerPart objects
|
- GET: Return list of ManufacturerPart objects
|
||||||
@@ -161,24 +184,7 @@ class ManufacturerPartList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|||||||
|
|
||||||
serializer_class = ManufacturerPartSerializer
|
serializer_class = ManufacturerPartSerializer
|
||||||
filterset_class = ManufacturerPartFilter
|
filterset_class = ManufacturerPartFilter
|
||||||
|
output_options = ManufacturerOutputOptions
|
||||||
def get_serializer(self, *args, **kwargs):
|
|
||||||
"""Return serializer instance for this endpoint."""
|
|
||||||
# Do we wish to include extra detail?
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
kwargs['part_detail'] = str2bool(params.get('part_detail', None))
|
|
||||||
kwargs['manufacturer_detail'] = str2bool(
|
|
||||||
params.get('manufacturer_detail', None)
|
|
||||||
)
|
|
||||||
kwargs['pretty'] = str2bool(params.get('pretty', None))
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
|
||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
@@ -224,30 +230,25 @@ class ManufacturerPartParameterFilter(FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerPartParameterList(ListCreateDestroyAPIView):
|
class ManufacturerPartParameterOptions(OutputConfiguration):
|
||||||
|
"""Available output options for the ManufacturerPartParameter endpoints."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Include detailed information about the linked ManufacturerPart in the response',
|
||||||
|
flag='manufacturer_part_detail',
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerPartParameterList(ListCreateDestroyAPIView, OutputOptionsMixin):
|
||||||
"""API endpoint for list view of ManufacturerPartParamater model."""
|
"""API endpoint for list view of ManufacturerPartParamater model."""
|
||||||
|
|
||||||
queryset = ManufacturerPartParameter.objects.all()
|
queryset = ManufacturerPartParameter.objects.all()
|
||||||
serializer_class = ManufacturerPartParameterSerializer
|
serializer_class = ManufacturerPartParameterSerializer
|
||||||
filterset_class = ManufacturerPartParameterFilter
|
filterset_class = ManufacturerPartParameterFilter
|
||||||
|
output_options = ManufacturerPartParameterOptions
|
||||||
def get_serializer(self, *args, **kwargs):
|
|
||||||
"""Return serializer instance for this endpoint."""
|
|
||||||
# Do we wish to include any extra detail?
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
optional_fields = ['manufacturer_part_detail']
|
|
||||||
|
|
||||||
for key in optional_fields:
|
|
||||||
kwargs[key] = str2bool(params.get(key, None))
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
|
||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
@@ -454,7 +455,24 @@ class SupplierPriceBreakFilter(FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SupplierPriceBreakList(ListCreateAPI):
|
class SupplierPriceBreakOutputOptions(OutputConfiguration):
|
||||||
|
"""Available output options for the SupplierPriceBreak endpoints."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Include detailed information about the linked Part in the response',
|
||||||
|
flag='part_detail',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Include detailed information about the Supplier in the response',
|
||||||
|
flag='supplier_detail',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPriceBreakList(OutputOptionsMixin, ListCreateAPI):
|
||||||
"""API endpoint for list view of SupplierPriceBreak object.
|
"""API endpoint for list view of SupplierPriceBreak object.
|
||||||
|
|
||||||
- GET: Retrieve list of SupplierPriceBreak objects
|
- GET: Retrieve list of SupplierPriceBreak objects
|
||||||
@@ -464,6 +482,7 @@ class SupplierPriceBreakList(ListCreateAPI):
|
|||||||
queryset = SupplierPriceBreak.objects.all()
|
queryset = SupplierPriceBreak.objects.all()
|
||||||
serializer_class = SupplierPriceBreakSerializer
|
serializer_class = SupplierPriceBreakSerializer
|
||||||
filterset_class = SupplierPriceBreakFilter
|
filterset_class = SupplierPriceBreakFilter
|
||||||
|
output_options = SupplierPriceBreakOutputOptions
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return annotated queryset for the SupplierPriceBreak list endpoint."""
|
"""Return annotated queryset for the SupplierPriceBreak list endpoint."""
|
||||||
@@ -472,21 +491,6 @@ class SupplierPriceBreakList(ListCreateAPI):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
|
||||||
"""Return serializer instance for this endpoint."""
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
|
|
||||||
kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', False))
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
|
||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
|
||||||
ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
|
ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
|
||||||
|
@@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from company.models import Address, Company, Contact, ManufacturerPart, SupplierPart
|
from company.models import (
|
||||||
|
Address,
|
||||||
|
Company,
|
||||||
|
Contact,
|
||||||
|
ManufacturerPart,
|
||||||
|
SupplierPart,
|
||||||
|
SupplierPriceBreak,
|
||||||
|
)
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from users.permissions import check_user_permission
|
from users.permissions import check_user_permission
|
||||||
@@ -529,6 +536,34 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
|||||||
'https://www.axel-larsson.se/Exego.aspx?p_id=341&ArtNr=0804020E',
|
'https://www.axel-larsson.se/Exego.aspx?p_id=341&ArtNr=0804020E',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_output_options(self):
|
||||||
|
"""Test the output options for SupplierPart detail."""
|
||||||
|
url = reverse('api-manufacturer-part-list')
|
||||||
|
|
||||||
|
# Test cases: (parameter_name, response_field_name)
|
||||||
|
test_cases = [
|
||||||
|
('part_detail', 'part_detail'),
|
||||||
|
('manufacturer_detail', 'manufacturer_detail'),
|
||||||
|
('pretty', 'pretty_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for param, field in test_cases:
|
||||||
|
# Test with parameter set to 'true'
|
||||||
|
response = self.get(url, {param: 'true', 'limit': 1}, expected_code=200)
|
||||||
|
self.assertIn(
|
||||||
|
field,
|
||||||
|
response.data['results'][0],
|
||||||
|
f"Field '{field}' should be present when {param}='true'",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with parameter set to 'false'
|
||||||
|
response = self.get(url, {param: 'false', 'limit': 1}, expected_code=200)
|
||||||
|
self.assertNotIn(
|
||||||
|
field,
|
||||||
|
response.data['results'][0],
|
||||||
|
f"Field '{field}' should not be present when {param}='false'",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SupplierPartTest(InvenTreeAPITestCase):
|
class SupplierPartTest(InvenTreeAPITestCase):
|
||||||
"""Unit tests for the SupplierPart API endpoints."""
|
"""Unit tests for the SupplierPart API endpoints."""
|
||||||
@@ -570,23 +605,30 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
|||||||
sp = SupplierPart.objects.all().first()
|
sp = SupplierPart.objects.all().first()
|
||||||
url = reverse('api-supplier-part-detail', kwargs={'pk': sp.pk})
|
url = reverse('api-supplier-part-detail', kwargs={'pk': sp.pk})
|
||||||
|
|
||||||
response = self.get(url, {'part_detail': 'true'}, expected_code=200)
|
# Test cases: (parameter_name, response_field_name)
|
||||||
self.assertIn('part_detail', response.data)
|
test_cases = [
|
||||||
|
('part_detail', 'part_detail'),
|
||||||
|
('supplier_detail', 'supplier_detail'),
|
||||||
|
('manufacturer_detail', 'manufacturer_detail'),
|
||||||
|
('pretty', 'pretty_name'),
|
||||||
|
]
|
||||||
|
|
||||||
response = self.get(url, {'supplier_detail': 'true'}, expected_code=200)
|
for param, field in test_cases:
|
||||||
self.assertIn('supplier_detail', response.data)
|
# Test with parameter set to 'true'
|
||||||
|
response = self.get(url, {param: 'true'}, expected_code=200)
|
||||||
|
self.assertIn(
|
||||||
|
field,
|
||||||
|
response.data,
|
||||||
|
f"Field '{field}' should be present when {param}='true'",
|
||||||
|
)
|
||||||
|
|
||||||
response = self.get(url, {'supplier_detail': 'false'}, expected_code=200)
|
# Test with parameter set to 'false'
|
||||||
self.assertNotIn('supplier_detail', response.data)
|
response = self.get(url, {param: 'false'}, expected_code=200)
|
||||||
|
self.assertNotIn(
|
||||||
response = self.get(url, {'manufacturer_detail': 'true'}, expected_code=200)
|
field,
|
||||||
self.assertIn('manufacturer_detail', response.data)
|
response.data,
|
||||||
|
f"Field '{field}' should not be present when {param}='false'",
|
||||||
response = self.get(url, {'pretty': 'true'}, expected_code=200)
|
)
|
||||||
self.assertIn('pretty_name', response.data)
|
|
||||||
|
|
||||||
response = self.get(url, {'pretty': 'false'}, expected_code=200)
|
|
||||||
self.assertNotIn('pretty_name', response.data)
|
|
||||||
|
|
||||||
def test_available(self):
|
def test_available(self):
|
||||||
"""Tests for updating the 'available' field."""
|
"""Tests for updating the 'available' field."""
|
||||||
@@ -728,3 +770,73 @@ class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
|||||||
'api-contact-metadata': Contact,
|
'api-contact-metadata': Contact,
|
||||||
}.items():
|
}.items():
|
||||||
self.metatester(apikey, model)
|
self.metatester(apikey, model)
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierPriceBreakAPITest(InvenTreeAPITestCase):
|
||||||
|
"""Unit tests for the SupplierPart price break API."""
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'company',
|
||||||
|
'manufacturer_part',
|
||||||
|
'supplier_part',
|
||||||
|
'price_breaks',
|
||||||
|
]
|
||||||
|
|
||||||
|
roles = ['company.change', 'purchase_order.change', 'part.change']
|
||||||
|
|
||||||
|
def test_output_options(self):
|
||||||
|
"""Test the output options for SupplierPart price break list."""
|
||||||
|
url = reverse('api-part-supplier-price-list')
|
||||||
|
test_cases = [
|
||||||
|
('part_detail', 'part_detail'),
|
||||||
|
('supplier_detail', 'supplier_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for param, field in test_cases:
|
||||||
|
# Test with parameter set to 'true'
|
||||||
|
response = self.get(url, {param: 'true', 'limit': 1}, expected_code=200)
|
||||||
|
self.assertIn(
|
||||||
|
field,
|
||||||
|
response.data['results'][0],
|
||||||
|
f"Field '{field}' should be present when {param}='true'",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with parameter set to 'false'
|
||||||
|
response = self.get(url, {param: 'false', 'limit': 1}, expected_code=200)
|
||||||
|
self.assertNotIn(
|
||||||
|
field,
|
||||||
|
response.data['results'][0],
|
||||||
|
f"Field '{field}' should not be present when {param}='false'",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_supplier_price_break_list(self):
|
||||||
|
"""Test the SupplierPriceBreak API list functionality."""
|
||||||
|
url = reverse('api-part-supplier-price-list')
|
||||||
|
|
||||||
|
# Return *all* SupplierPriceBreaks
|
||||||
|
response = self.get(url, {}, expected_code=200)
|
||||||
|
self.assertEqual(len(response.data), SupplierPriceBreak.objects.count())
|
||||||
|
|
||||||
|
# Filter by supplier part
|
||||||
|
expected = {1: 3, 2: 2, 4: 2} # Based on fixture data
|
||||||
|
|
||||||
|
for part_pk, count in expected.items():
|
||||||
|
response = self.get(url, {'part': part_pk}, expected_code=200)
|
||||||
|
self.assertEqual(len(response.data), count)
|
||||||
|
|
||||||
|
# Test ordering by quantity
|
||||||
|
response = self.get(url, {'ordering': 'quantity'}, expected_code=200)
|
||||||
|
quantities = [item['quantity'] for item in response.data]
|
||||||
|
self.assertEqual(quantities, sorted(quantities))
|
||||||
|
|
||||||
|
# Test ordering by price
|
||||||
|
response = self.get(url, {'ordering': 'price'}, expected_code=200)
|
||||||
|
prices = [float(item['price']) for item in response.data]
|
||||||
|
self.assertEqual(prices, sorted(prices))
|
||||||
|
|
||||||
|
# Test search by supplier name
|
||||||
|
response = self.get(url, {'search': 'ACME'}, expected_code=200)
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
Reference in New Issue
Block a user