Skip to content
Merged
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
27 changes: 16 additions & 11 deletions inventree/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ class InventreeObject(object):
URL = ""

@classmethod
def get_url(cls):
"""Helper method to get the URL associated with this model."""
def get_url(cls, api=None):
"""Helper method to get the URL associated with this model.

Arguments:
api: Optional InvenTreeAPI instance. Subclasses may override this method
to return a different URL based on the server API version.
"""

url = cls.URL

Expand Down Expand Up @@ -97,7 +102,7 @@ def __init__(self, api, pk=None, data=None):
if pk <= 0:
raise ValueError(f"Supplier <pk> value ({pk}) for {self.__class__} must be positive.")

url = self.get_url()
url = self.get_url(api)

self._url = f"{url}/{pk}/"
self._api = api
Expand Down Expand Up @@ -147,18 +152,18 @@ def options(cls, api):
cls.checkApiVersion(api)

response = api.request(
cls.get_url(),
cls.get_url(api),
method='OPTIONS',
)

if not response.status_code == 200:
logger.error(f"OPTIONS for '{cls.URL}' returned code {response.status_code}")
logger.error(f"OPTIONS for '{cls.get_url(api)}' returned code {response.status_code}")
return {}

try:
data = json.loads(response.text)
except json.decoder.JSONDecodeError:
logger.error(f"Error decoding OPTIONS response for '{cls.URL}'")
logger.error(f"Error decoding OPTIONS response for '{cls.get_url(api)}'")
return {}

return data
Expand Down Expand Up @@ -187,7 +192,7 @@ def fieldInfo(cls, field_name, api):
if field_name in fields:
return fields[field_name]
else:
logger.warning(f"Field '{field_name}' not found in OPTIONS request for {cls.URL}")
logger.warning(f"Field '{field_name}' not found in OPTIONS request for {cls.get_url(api)}")
return {}

@classmethod
Expand All @@ -208,7 +213,7 @@ def create(cls, api, data, **kwargs):
if cls.getPkField() in data.keys():
data.pop(cls.getPkField())

response = api.post(cls.URL, data, **kwargs)
response = api.post(cls.get_url(api), data, **kwargs)

if response is None:
logger.error("Error creating new object")
Expand All @@ -225,7 +230,7 @@ def count(cls, api, **kwargs):
# By limiting to a single result, we perform a fast query, but get a total number of results
params['limit'] = 1

response = api.get(url=cls.URL, params=params)
response = api.get(url=cls.get_url(api), params=params)

return response['count']

Expand All @@ -243,7 +248,7 @@ def list(cls, api, **kwargs):
if 'url' in kwargs:
url = kwargs.pop('url')
else:
url = cls.URL
url = cls.get_url(api)

try:
response = api.get(url=url, params=kwargs)
Expand Down Expand Up @@ -423,7 +428,7 @@ def bulkDelete(cls, api: inventree_api.InvenTreeAPI, items=None, filters=None):
data['filters'] = filters

return api.delete(
cls.URL,
cls.get_url(api),
json=data,
)

Expand Down
54 changes: 11 additions & 43 deletions inventree/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import inventree.report
import inventree.stock

# Pricing models were moved to inventree.pricing - re-exported here for backwards compatibility
from inventree.pricing import InternalPrice, PartPricing, SalePrice # noqa: F401

logger = logging.getLogger('inventree')


Expand Down Expand Up @@ -133,6 +136,8 @@ def getInternalPriceList(self):
Returns the InternalPrice list for this part
"""



return InternalPrice.list(self._api, part=self.pk)

def setInternalPrice(self, quantity: int, price: float):
Expand All @@ -148,6 +153,12 @@ def getSalePrice(self):
"""
return SalePrice.list(self._api, part=self.pk)[0].price

def getPricing(self):
"""
Get the cached pricing data for this part
"""
return PartPricing(self._api, part=self.pk)

def getRequirements(self):
"""
Get required amounts from requirements API endpoint for this part
Expand Down Expand Up @@ -210,49 +221,6 @@ class BomItemSubstitute(
URL = "bom/substitute/"


class InternalPrice(inventree.base.InventreeObject):
""" Class representing the InternalPrice model """

URL = 'part/internal-price/'

@classmethod
def setInternalPrice(cls, api, part, quantity: int, price: float):
"""
Set the internal price for this part
"""

data = {
'part': part,
'quantity': quantity,
'price': price,
}

# Send the data to the server
return api.post(cls.URL, data)


class SalePrice(inventree.base.InventreeObject):
""" Class representing the SalePrice model """

URL = 'part/sale-price/'

@classmethod
def setSalePrice(cls, api, part, quantity: int, price: float, price_currency: str):
"""
Set the sale price for this part
"""

data = {
'part': part,
'quantity': quantity,
'price': price,
'price_currency': price_currency,
}

# Send the data to the server
return api.post(cls.URL, data)


class PartRelated(inventree.base.InventreeObject):
""" Class representing a relationship between parts"""

Expand Down
191 changes: 191 additions & 0 deletions inventree/pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
"""Pricing models.

Ref: https://github.com/inventree/InvenTree/pull/13034
"""

import inventree.base


class PricingModelMixin:
"""Mixin class for models whose API endpoints moved to the 'pricing' app.

- For servers with API version >= PRICING_API_VERSION, the new URL is used
- For older servers, the LEGACY_URL is used
"""

# API version at which pricing endpoints moved from /api/part/ to /api/pricing/
PRICING_API_VERSION = 561

# URL for the endpoint on older servers
LEGACY_URL = ""

@classmethod
def useLegacyUrl(cls, api=None):
"""Return True if the server requires the legacy (pre-pricing app) URL.

If the API version is unknown, the new URL is assumed.
"""

api_version = getattr(api, 'api_version', None)

return api_version is not None and api_version < cls.PRICING_API_VERSION

@classmethod
def get_url(cls, api=None):
"""Return the URL for this model, based on the server API version."""

if cls.useLegacyUrl(api):
url = cls.LEGACY_URL

if not url.endswith('/'):
url += '/'

return url

return super().get_url(api)


class InternalPrice(PricingModelMixin, inventree.base.InventreeObject):
""" Class representing the InternalPrice model """

URL = 'pricing/internal-price/'
LEGACY_URL = 'part/internal-price/'

@classmethod
def setInternalPrice(cls, api, part, quantity: int, price: float):
"""
Set the internal price for this part
"""

data = {
'part': part,
'quantity': quantity,
'price': price,
}

# Send the data to the server
return api.post(cls.get_url(api), data)


class SalePrice(PricingModelMixin, inventree.base.InventreeObject):
""" Class representing the SalePrice model """

URL = 'pricing/sale-price/'
LEGACY_URL = 'part/sale-price/'

@classmethod
def setSalePrice(cls, api, part, quantity: int, price: float, price_currency: str):
"""
Set the sale price for this part
"""

data = {
'part': part,
'quantity': quantity,
'price': price,
'price_currency': price_currency,
}

# Send the data to the server
return api.post(cls.get_url(api), data)


class PartPricing(PricingModelMixin, inventree.base.InventreeObject):
"""Class representing the cached pricing data for a particular Part.

Unlike other models, this is accessed via the primary key of the linked Part:

- API version >= 561: pricing/part/<part>/
- Older API versions: part/<part>/pricing/

There is no list endpoint, and pricing data cannot be created or deleted.
Writable fields (e.g. override_min, override_max) can be changed via save().
"""

URL = 'pricing/part/'
LEGACY_URL = 'part/'

def __init__(self, api, part, data=None):
"""Instantiate the PartPricing object for a given Part.

Args:
api - The request manager object
part - The Part instance (or primary key value) to which this pricing data belongs
data - JSON representation of the pricing data
"""

self.checkApiVersion(api)

part = getattr(part, 'pk', part)

try:
part = int(str(part).strip())
except Exception:
raise TypeError(f"Invalid part value '{part}' for {self.__class__}")

if part <= 0:
raise ValueError(f"Supplied part value ({part}) for {self.__class__} must be positive.")

self._part = part
self._url = self.get_detail_url(api, part)
self._api = api

if data is None:
data = {}

self._data = data

# If the data are not populated, fetch from server
if len(self._data) == 0:
self.reload()

@classmethod
def get_detail_url(cls, api, part: int):
"""Return the pricing URL for the given Part, based on the server API version."""

if cls.useLegacyUrl(api):
return f"{cls.LEGACY_URL}{part}/pricing/"

return f"{cls.URL}{part}/"

@classmethod
def getPkField(cls):
"""Pricing data is identified by the linked Part."""
return 'part'

def getPkValue(self):
"""Return the primary key of the linked Part."""
return self._part

def updatePricing(self):
"""Request that the server recalculate pricing data for this part.

Note: The server may perform the update in the background,
in which case the returned data may not yet reflect the new pricing.
"""

return self.save(data={'update': True})

@classmethod
def _unsupported(cls, action):
raise NotImplementedError(f"'{action}' is not supported for {cls.__name__} - use Part.getPricing() instead")

@classmethod
def options(cls, api):
cls._unsupported('options')

@classmethod
def create(cls, api, data, **kwargs):
cls._unsupported('create')

@classmethod
def count(cls, api, **kwargs):
cls._unsupported('count')

@classmethod
def list(cls, api, **kwargs):
cls._unsupported('list')

def delete(self):
self._unsupported('delete')
Loading
Loading