diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f3acf4d..c5b41ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,7 +27,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.9] + python-version: [3.11] steps: - name: Checkout Code diff --git a/inventree/base.py b/inventree/base.py index fb87739..05d4206 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.19.0" +INVENTREE_PYTHON_VERSION = "0.20.0" logger = logging.getLogger('inventree') @@ -506,13 +506,14 @@ def download(self, destination, **kwargs): class AttachmentMixin: """Mixin class which allows a model class to interact with attachments.""" - def getAttachments(self): + def getAttachments(self, **kwargs): """Return a list of attachments associated with this object.""" return Attachment.list( self._api, model_type=self.getModelType(), - model_id=self.pk + model_id=self.pk, + **kwargs ) def uploadAttachment(self, attachment, comment=""): @@ -543,6 +544,44 @@ def addLinkAttachment(self, link, comment=""): ) +class Parameter(BulkDeleteMixin, InventreeObject): + """Class representing a custom parameter object.""" + + URL = "parameter/" + + # Ref: https://github.com/inventree/InvenTree/pull/10699 + MIN_API_VERSION = 429 + + +class ParameterTemplate(InventreeObject): + """Class representing a parameter template object.""" + + URL = "parameter/template/" + + # Ref: https://github.com/inventree/InvenTree/pull/10699 + MIN_API_VERSION = 429 + + +class ParameterMixin: + """Mixin class which allows a model class to interact with parameters. + + Ref: https://github.com/inventree/InvenTree/pull/10699 + """ + + def getParameters(self, **kwargs): + """Return a list of parameters associated with this object.""" + + if self._api.api_version < Parameter.MIN_API_VERSION: + raise NotImplementedError(f"Server API Version ({self._api.api_version}) is too old for ParameterMixin, which requires API version >= {Parameter.MIN_API_VERSION}") + + return Parameter.list( + self._api, + model_type=self.getModelType(), + model_id=self.pk, + **kwargs + ) + + class MetadataMixin: """Mixin class for models which support a 'metadata' attribute. diff --git a/inventree/build.py b/inventree/build.py index 9234f17..8bc0c5e 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -6,6 +6,7 @@ class Build( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.StatusMixin, inventree.base.MetadataMixin, inventree.report.ReportPrintingMixin, diff --git a/inventree/company.py b/inventree/company.py index fc08fd7..29f43c9 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -22,7 +22,13 @@ class Address(inventree.base.InventreeObject): MIN_API_VERSION = 126 -class Company(inventree.base.ImageMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject): +class Company( + inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, + inventree.base.ImageMixin, + inventree.base.MetadataMixin, + inventree.base.InventreeObject +): """ Class representing the Company database model """ URL = 'company/' @@ -97,7 +103,14 @@ def createReturnOrder(self, **kwargs): return inventree.order.ReturnOrder.create(self._api, data=kwargs) -class SupplierPart(inventree.base.BarcodeMixin, inventree.base.BulkDeleteMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject): +class SupplierPart( + inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, + inventree.base.BarcodeMixin, + inventree.base.BulkDeleteMixin, + inventree.base.MetadataMixin, + inventree.base.InventreeObject +): """Class representing the SupplierPart database model - Implements the BulkDeleteMixin @@ -113,6 +126,7 @@ def getPriceBreaks(self): class ManufacturerPart( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.BulkDeleteMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject, @@ -130,16 +144,22 @@ def getParameters(self, **kwargs): GET a list of all ManufacturerPartParameter objects for this ManufacturerPart """ - return ManufacturerPartParameter.list(self._api, manufacturer_part=self.pk, **kwargs) + # Support legacy API version which uses a different endpoint + if self._api.api_version < inventree.base.Parameter.MIN_API_VERSION: + return ManufacturerPartParameter.list(self._api, manufacturer_part=self.pk, **kwargs) + + return super().getParameters(**kwargs) class ManufacturerPartParameter(inventree.base.BulkDeleteMixin, inventree.base.InventreeObject): """Class representing the ManufacturerPartParameter database model. - - Implements the BulkDeleteMixin + Note: This class was removed in API version 418 and later. + Ref: https://github.com/inventree/InvenTree/pull/10699 """ URL = 'company/part/manufacturer/parameter/' + MAX_API_VERSION = 428 class SupplierPriceBreak(inventree.base.InventreeObject): diff --git a/inventree/part.py b/inventree/part.py index 7284290..8e94240 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -24,7 +24,14 @@ def getCategory(self): def getTemplate(self): """Return the referenced ParameterTemplate instance""" - return ParameterTemplate(self._api, self.parameter_template) + + template_id = getattr(self, 'template', None) or getattr(self, 'parameter_template', None) + + if self._api.api_version < inventree.base.ParameterTemplate.MIN_API_VERSION: + # Return legacy PartParameterTemplate object + return PartParameterTemplate(self._api, template_id) + + return inventree.base.ParameterTemplate(self._api, template_id) class PartCategory(inventree.base.MetadataMixin, inventree.base.InventreeObject): @@ -60,6 +67,7 @@ def getCategoryParameterTemplates(self, fetch_parent: bool = True) -> list: class Part( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.BarcodeMixin, inventree.base.MetadataMixin, inventree.base.ImageMixin, @@ -108,7 +116,12 @@ def getStockItems(self, **kwargs): def getParameters(self): """ Return parameters associated with this part """ - return Parameter.list(self._api, part=self.pk) + + if self._api.api_version < inventree.base.Parameter.MIN_API_VERSION: + # Return legacy PartParameter objects + return PartParameter.list(self._api, part=self.pk) + + return super().getParameters() def getRelated(self): """ Return related parts associated with this part """ @@ -264,17 +277,30 @@ def add_related(cls, api, part1, part2): return api.post(cls.URL, data) -class Parameter(inventree.base.InventreeObject): - """class representing the Parameter database model """ +class PartParameter(inventree.base.InventreeObject): + """Legacy class representing the PartParameter database model. + + This has now been replaced with the generic Parameter model. + + Ref: https://github.com/inventree/InvenTree/pull/10699 + """ URL = 'part/parameter/' + MAX_API_VERSION = 428 + def getunits(self): """ Get the units for this parameter """ return self._data['template_detail']['units'] -class ParameterTemplate(inventree.base.InventreeObject): - """ class representing the Parameter Template database model""" +class PartParameterTemplate(inventree.base.InventreeObject): + """Legacy class representing the PartParameterTemplate database model. + + This has now been replaced with the generic ParameterTemplate model. + + Ref: https://github.com/inventree/InvenTree/pull/10699 + """ URL = 'part/parameter/template/' + MAX_API_VERSION = 428 diff --git a/inventree/purchase_order.py b/inventree/purchase_order.py index 812ff9a..02c4233 100644 --- a/inventree/purchase_order.py +++ b/inventree/purchase_order.py @@ -10,6 +10,7 @@ class PurchaseOrder( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.MetadataMixin, inventree.base.StatusMixin, inventree.report.ReportPrintingMixin, diff --git a/inventree/return_order.py b/inventree/return_order.py index d2be36f..7534a22 100644 --- a/inventree/return_order.py +++ b/inventree/return_order.py @@ -11,6 +11,7 @@ class ReturnOrder( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.MetadataMixin, inventree.base.StatusMixin, inventree.report.ReportPrintingMixin, diff --git a/inventree/sales_order.py b/inventree/sales_order.py index c3affe9..411d311 100644 --- a/inventree/sales_order.py +++ b/inventree/sales_order.py @@ -11,6 +11,7 @@ class SalesOrder( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.MetadataMixin, inventree.base.StatusMixin, inventree.report.ReportPrintingMixin, diff --git a/setup.cfg b/setup.cfg index d59997b..e3ab9f2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,5 +6,5 @@ ignore = # - E501 - line too long (82 characters) E501 N802 -exclude = .git,__pycache__,inventree_server,dist,build,test.py +exclude = .git,.eggs,__pycache__,inventree_server,dist,build,test.py max-complexity = 20 diff --git a/test/test_company.py b/test/test_company.py index bb05156..db8d4a8 100644 --- a/test/test_company.py +++ b/test/test_company.py @@ -3,8 +3,6 @@ import os import sys -from requests.exceptions import HTTPError - try: import Image except ImportError: @@ -127,77 +125,6 @@ def test_manufacturer_part_create(self): man_parts = company.ManufacturerPart.list(self.api, manufacturer=manufacturer.pk) self.assertEqual(len(man_parts), n + 1) - def test_manufacturer_part_parameters(self): - """ - Test that we can create, retrieve and edit ManufacturerPartParameter objects - """ - - n = len(company.ManufacturerPart.list(self.api)) - - mpn = f"XYZ-12345678-{n}" - - # First, create a new ManufacturerPart - part = company.ManufacturerPart.create(self.api, { - 'manufacturer': 6, - 'part': 1, - 'MPN': mpn, - }) - - self.assertIsNotNone(part) - self.assertEqual(len(company.ManufacturerPart.list(self.api)), n + 1) - - # Part should (initially) not have any parameters - self.assertEqual(len(part.getParameters()), 0) - - # Now, let's create some! - for idx in range(10): - - parameter = company.ManufacturerPartParameter.create(self.api, { - 'manufacturer_part': part.pk, - 'name': f"param {idx}", - 'value': f"{idx}", - }) - - self.assertIsNotNone(parameter) - - # Now should have 10 unique parameters - self.assertEqual(len(part.getParameters()), 10) - - # Attempt to create a duplicate parameter - with self.assertRaises(HTTPError): - company.ManufacturerPartParameter.create(self.api, { - 'manufacturer_part': part.pk, - 'name': 'param 0', - 'value': 'some value', - }) - - self.assertEqual(len(part.getParameters()), 10) - - # Test that we can edit a ManufacturerPartParameter - parameter = part.getParameters()[0] - - self.assertEqual(parameter.value, '0') - - parameter['value'] = 'new value' - parameter.save() - - self.assertEqual(parameter.value, 'new value') - - parameter['value'] = 'dummy value' - parameter.reload() - - self.assertEqual(parameter.value, 'new value') - - # Test that the "list" function works correctly - results = company.ManufacturerPartParameter.list(self.api) - self.assertGreaterEqual(len(results), 10) - - results = company.ManufacturerPartParameter.list(self.api, name='param 1') - self.assertGreaterEqual(len(results), 1) - - results = company.ManufacturerPartParameter.list(self.api, manufacturer_part=part.pk) - self.assertGreaterEqual(len(results), 10) - def test_supplier_part_create(self): """ Test that we can create SupplierPart objects via the API diff --git a/test/test_part.py b/test/test_part.py index c16e35e..d2e2513 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -15,11 +15,11 @@ from test_api import InvenTreeTestCase # noqa: E402 -from inventree.base import Attachment # noqa: E402 +from inventree.base import Attachment, Parameter, ParameterTemplate # noqa: E402 from inventree.company import SupplierPart # noqa: E402 from inventree.part import InternalPrice # noqa: E402 -from inventree.part import (BomItem, Parameter, # noqa: E402 - ParameterTemplate, Part, +from inventree.part import (BomItem, PartParameter, # noqa: E402 + Part, PartCategory, PartCategoryParameterTemplate, PartRelated, PartTestTemplate) from inventree.stock import StockItem # noqa: E402 @@ -130,6 +130,10 @@ def test_caps(self): def test_part_category_parameter_templates(self): """Unit tests for the PartCategoryParameterTemplate model""" + # Ignore these tests for "legacy" Parameter API + if self.api.api_version < Parameter.MIN_API_VERSION: + return + electronics = PartCategory(self.api, pk=3) # Ensure there are some parameter templates associated with this category @@ -146,7 +150,7 @@ def test_part_category_parameter_templates(self): self.api, data={ 'category': electronics.pk, - 'parameter_template': template.pk, + 'template': template.pk, 'default_value': 123, } ) @@ -186,11 +190,15 @@ def test_part_get_functions(self): 'getBomItems': BomItem, 'isUsedIn': BomItem, 'getStockItems': StockItem, - 'getParameters': Parameter, 'getRelated': PartRelated, 'getInternalPriceList': InternalPrice, } + if self.api.api_version >= Parameter.MIN_API_VERSION: + functions['getParameters'] = Parameter + else: + functions['getParameters'] = PartParameter + if self.api.api_version >= Attachment.MIN_API_VERSION: functions['getAttachments'] = Attachment @@ -567,9 +575,11 @@ def test_set_price(self): self.assertEqual(ip_price_clean, test_price) def test_parameters(self): - """ - Test setting and getting Part parameter templates, as well as parameter values - """ + """Test setting and getting PartParameterTemplates, as well as PartParameter values.""" + + # Skip test if modernized Parameter API is not supported + if self.api.api_version < Parameter.MIN_API_VERSION: + return # Count number of existing Parameter Templates existingTemplates = len(ParameterTemplate.list(self.api)) @@ -579,7 +589,15 @@ def test_parameters(self): parametertemplate = ParameterTemplate.create(self.api, data={'units': "kg A"}) # Now create a proper parameter template - parametertemplate = ParameterTemplate.create(self.api, data={'name': f'Test parameter no {existingTemplates}', 'units': "kg A"}) + parametertemplate = ParameterTemplate.create( + self.api, + data={ + 'name': f'Test parameter no {existingTemplates}', + 'description': 'A parameter template for testing', + 'model_type': None, + 'units': "kg A" + } + ) # result should not be None self.assertIsNotNone(parametertemplate) @@ -595,17 +613,14 @@ def test_parameters(self): # Define parameter value for this part - without all required values with self.assertRaises(HTTPError): - Parameter.create(self.api, data={'part': p.pk, 'template': parametertemplate.pk}) + Parameter.create(self.api, data={'model_type': 'part', 'model_id': p.pk, 'template': parametertemplate.pk}) # Define parameter value for this part - without all required values with self.assertRaises(HTTPError): - Parameter.create(self.api, data={'part': p.pk, 'data': 10}) + Parameter.create(self.api, data={'model_type': 'part', 'model_id': p.pk, 'data': 10}) # Define w. required values - integer - param = Parameter.create(self.api, data={'part': p.pk, 'template': parametertemplate.pk, 'data': 10}) - - # Unit should be equal - self.assertEqual(param.getunits(), 'kg A') + param = Parameter.create(self.api, data={'model_type': 'part', 'model_id': p.pk, 'template': parametertemplate.pk, 'data': 10}) # result should not be None self.assertIsNotNone(param) @@ -613,7 +628,7 @@ def test_parameters(self): # Same parameter for same part - should fail # Define w. required values - string with self.assertRaises(HTTPError): - Parameter.create(self.api, data={'part': p.pk, 'template': parametertemplate.pk, 'data': 'String value'}) + Parameter.create(self.api, data={'model_type': 'part', 'model_id': p.pk, 'template': parametertemplate.pk, 'data': 'String value'}) # Number of parameters should be one higher than before self.assertEqual(len(p.getParameters()), existingParameters + 1)