Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 430
INVENTREE_API_VERSION = 431
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""

INVENTREE_API_TEXT = """

v431 -> 2025-12-09 : https://github.com/inventree/InvenTree/pull/10983
- Adds an API to gather dynamic price information for a Part

v430 -> 2025-12-04 : https://github.com/inventree/InvenTree/pull/10699
- Removed the "PartParameter" and "PartParameterTemplate" API endpoints
- Removed the "ManufacturerPartParameter" API endpoint
Expand Down
26 changes: 26 additions & 0 deletions src/backend/InvenTree/part/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,27 @@
return self.serializer_class(**kwargs)


class PartLivePricingDetail(RetrieveAPI):
"""API endpoint for calculating live part pricing data - this might be compute intensive."""

queryset = Part.objects.all()
serializer_class = part_serializers.PartLivePricingSerializer

def retrieve(self, request, *args, **kwargs):
"""Fetch live pricing data for this part."""
quantity = 1 # TODO: retrieve from request parameters

Check warning on line 596 in src/backend/InvenTree/part/api.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=inventree_InvenTree&issues=AZr_pNdLYdKf-cRNCSi1&open=AZr_pNdLYdKf-cRNCSi1&pullRequest=10983
part: Part = self.get_object()
pricing = part.get_price_range(
quantity, buy=True, bom=True, internal=True, purchase=True, info=True
)
serializer = self.get_serializer({
'price_min': pricing[0],
'price_max': pricing[1],
'source': pricing[2],
})
return Response(serializer.data)


class PartSerialNumberDetail(RetrieveAPI):
"""API endpoint for returning extra serial number information about a particular part."""

Expand Down Expand Up @@ -1652,6 +1673,11 @@
),
# Part pricing
path('pricing/', PartPricingDetail.as_view(), name='api-part-pricing'),
path(
'pricing-live/',
PartLivePricingDetail.as_view(),
name='api-part-pricing-live',
),
# Part detail endpoint
path('', PartDetail.as_view(), name='api-part-detail'),
]),
Expand Down
37 changes: 29 additions & 8 deletions src/backend/InvenTree/part/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import re
from datetime import timedelta
from decimal import Decimal, InvalidOperation
from typing import cast
from typing import Literal, cast

from django.conf import settings
from django.contrib.auth.models import User
Expand Down Expand Up @@ -2195,10 +2195,12 @@
bom: Include BOM pricing (default = True)
internal: Include internal pricing (default = False)
"""
price_range = self.get_price_range(quantity, buy, bom, internal)
price_range = self.get_price_range(quantity, buy, bom, internal, info=False)

if price_range is None:
return None
if len(price_range) != 2:
return None

min_price, max_price = price_range

Expand Down Expand Up @@ -2291,7 +2293,15 @@
return (min_price, max_price)

def get_price_range(
self, quantity=1, buy=True, bom=True, internal=False, purchase=False
self, quantity=1, buy=True, bom=True, internal=False, purchase=False, info=False
) -> (
tuple[Decimal | None, Decimal | None]
| tuple[
Decimal | None,
Decimal | None,
Literal['internal', 'purchase', 'bom', 'buy', 'buy/bom'],
]
| None
):
"""Return the price range for this part.

Expand All @@ -2304,30 +2314,41 @@
Returns:
Minimum of the supplier, BOM, internal or purchase price. If no pricing available, returns None
"""

def return_info(r_min, r_max, source: str):
if not info:
return r_min, r_max
return r_min, r_max, source

# only get internal price if set and should be used
if internal and self.has_internal_price_breaks:
internal_price = self.get_internal_price(quantity)
return internal_price, internal_price
return return_info(internal_price, internal_price, 'internal')

# TODO add sales pricing option?

Check warning on line 2328 in src/backend/InvenTree/part/models.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=inventree_InvenTree&issues=AZr_pNYaYdKf-cRNCSi0&open=AZr_pNYaYdKf-cRNCSi0&pullRequest=10983

# only get purchase price if set and should be used
if purchase:
purchase_price = self.get_purchase_price(quantity)
if purchase_price:
return purchase_price
return return_info(*purchase_price, 'purchase') # type: ignore[too-many-positional-arguments]

buy_price_range = self.get_supplier_price_range(quantity) if buy else None
bom_price_range = (
self.get_bom_price_range(quantity, internal=internal) if bom else None
)

if buy_price_range is None:
return bom_price_range
if bom_price_range is None:
return None
return return_info(*bom_price_range, 'bom') # type: ignore[too-many-positional-arguments]

elif bom_price_range is None:
return buy_price_range
return (
return return_info(*buy_price_range, 'buy') # type: ignore[too-many-positional-arguments]
return return_info(
min(buy_price_range[0], bom_price_range[0]),
max(buy_price_range[1], bom_price_range[1]),
'buy/bom',
)

base_cost = models.DecimalField(
Expand Down
12 changes: 12 additions & 0 deletions src/backend/InvenTree/part/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,18 @@ def save(self):
pricing.update_pricing()


class PartLivePricingSerializer(serializers.Serializer):
"""Serializer for Part live pricing information."""

price_min = InvenTree.serializers.InvenTreeMoneySerializer(
allow_null=True, read_only=True
)
price_max = InvenTree.serializers.InvenTreeMoneySerializer(
allow_null=True, read_only=True
)
source = serializers.CharField(allow_null=True, read_only=True)


class PartSerialNumberSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for Part serial number information."""

Expand Down
Loading