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
|
||||
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."""
|
||||
|
||||
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
|
||||
- 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
|
||||
|
@@ -12,7 +12,6 @@ from data_exporter.mixins import DataExportViewMixin
|
||||
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
|
||||
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin, RetrieveUpdateDestroyAPI
|
||||
|
||||
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.
|
||||
|
||||
- GET: Return list of ManufacturerPart objects
|
||||
@@ -161,24 +184,7 @@ class ManufacturerPartList(DataExportViewMixin, ListCreateDestroyAPIView):
|
||||
|
||||
serializer_class = ManufacturerPartSerializer
|
||||
filterset_class = ManufacturerPartFilter
|
||||
|
||||
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)
|
||||
output_options = ManufacturerOutputOptions
|
||||
|
||||
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."""
|
||||
|
||||
queryset = ManufacturerPartParameter.objects.all()
|
||||
serializer_class = ManufacturerPartParameterSerializer
|
||||
filterset_class = ManufacturerPartParameterFilter
|
||||
|
||||
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)
|
||||
output_options = ManufacturerPartParameterOptions
|
||||
|
||||
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.
|
||||
|
||||
- GET: Retrieve list of SupplierPriceBreak objects
|
||||
@@ -464,6 +482,7 @@ class SupplierPriceBreakList(ListCreateAPI):
|
||||
queryset = SupplierPriceBreak.objects.all()
|
||||
serializer_class = SupplierPriceBreakSerializer
|
||||
filterset_class = SupplierPriceBreakFilter
|
||||
output_options = SupplierPriceBreakOutputOptions
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return annotated queryset for the SupplierPriceBreak list endpoint."""
|
||||
@@ -472,21 +491,6 @@ class SupplierPriceBreakList(ListCreateAPI):
|
||||
|
||||
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
|
||||
|
||||
ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
|
||||
|
@@ -2,7 +2,14 @@
|
||||
|
||||
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 part.models import Part
|
||||
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',
|
||||
)
|
||||
|
||||
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):
|
||||
"""Unit tests for the SupplierPart API endpoints."""
|
||||
@@ -570,23 +605,30 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
||||
sp = SupplierPart.objects.all().first()
|
||||
url = reverse('api-supplier-part-detail', kwargs={'pk': sp.pk})
|
||||
|
||||
response = self.get(url, {'part_detail': 'true'}, expected_code=200)
|
||||
self.assertIn('part_detail', response.data)
|
||||
# Test cases: (parameter_name, response_field_name)
|
||||
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)
|
||||
self.assertIn('supplier_detail', response.data)
|
||||
for param, field in test_cases:
|
||||
# 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)
|
||||
self.assertNotIn('supplier_detail', response.data)
|
||||
|
||||
response = self.get(url, {'manufacturer_detail': 'true'}, expected_code=200)
|
||||
self.assertIn('manufacturer_detail', response.data)
|
||||
|
||||
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)
|
||||
# Test with parameter set to 'false'
|
||||
response = self.get(url, {param: 'false'}, expected_code=200)
|
||||
self.assertNotIn(
|
||||
field,
|
||||
response.data,
|
||||
f"Field '{field}' should not be present when {param}='false'",
|
||||
)
|
||||
|
||||
def test_available(self):
|
||||
"""Tests for updating the 'available' field."""
|
||||
@@ -728,3 +770,73 @@ class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
||||
'api-contact-metadata': Contact,
|
||||
}.items():
|
||||
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