2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-15 13:42:20 +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:
Reza
2025-10-04 01:48:37 +03:30
committed by GitHub
parent a24b56b066
commit fa1dbb59c1
3 changed files with 193 additions and 72 deletions

View File

@@ -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

View File

@@ -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']

View File

@@ -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)