From fa258f3faedc2215bf632d2044f36113c0f810b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 21 May 2024 22:14:34 +1000 Subject: [PATCH 01/84] Update API level for "modern" report templates --- inventree/label.py | 2 +- inventree/report.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inventree/label.py b/inventree/label.py index e3d29c1d..5c662a5e 100644 --- a/inventree/label.py +++ b/inventree/label.py @@ -9,7 +9,7 @@ # The InvenTree API endpoint changed considerably @ version 197 # Ref: https://github.com/inventree/InvenTree/pull/7074 -MODERN_LABEL_PRINTING_API = 198 +MODERN_LABEL_PRINTING_API = 201 class LabelPrintingMixin: diff --git a/inventree/report.py b/inventree/report.py index aff9e6eb..e34e99f8 100644 --- a/inventree/report.py +++ b/inventree/report.py @@ -5,7 +5,7 @@ # The InvenTree API endpoint changed considerably @ version 197 # Ref: https://github.com/inventree/InvenTree/pull/7074 -MODERN_LABEL_PRINTING_API = 198 +MODERN_LABEL_PRINTING_API = 201 class ReportPrintingMixin: From d12e52a2c3306331a9e10f81531cf7b0f9839409 Mon Sep 17 00:00:00 2001 From: miggland Date: Mon, 16 Dec 2024 23:21:52 +0100 Subject: [PATCH 02/84] Improve compatibility with older versionof InvenTree Server. (#251) * Add MIN_API_VERSION to SalesOrderAllocation model. Workaround for old versions of server * Revert unintended change --- inventree/sales_order.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inventree/sales_order.py b/inventree/sales_order.py index b12c99e2..e7c5189b 100644 --- a/inventree/sales_order.py +++ b/inventree/sales_order.py @@ -196,6 +196,7 @@ class SalesOrderAllocation( ): """Class representing the SalesOrderAllocation database model.""" + MIN_API_VERSION = 267 URL = 'order/so-allocation' def getOrder(self): @@ -275,7 +276,10 @@ def allocations(self): Note: This is an overload of getAllocations() method, for legacy compatibility. """ - return self.getAllocations() + try: + return self.getAllocations() + except NotImplementedError: + return self._data['allocations'] def complete( self, From bc175d4fe072e9d01309153a049af590cbf0aeda Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 10 Jan 2025 18:29:02 +1100 Subject: [PATCH 03/84] CI tweaks (#256) * CI tweaks * Specify cookie setting * Enable debug mode * More verbose logging --- .github/workflows/ci.yaml | 6 +++++- test/docker-compose.yml | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ebc4ddbb..f3acf4d3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,17 +8,21 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INVENTREE_SITE_URL: http://localhost:8000 INVENTREE_DB_ENGINE: django.db.backends.sqlite3 INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3 INVENTREE_MEDIA_ROOT: ../test_inventree_media INVENTREE_STATIC_ROOT: ../test_inventree_static INVENTREE_BACKUP_DIR: ../test_inventree_backup + INVENTREE_COOKIE_SAMESITE: False INVENTREE_ADMIN_USER: testuser INVENTREE_ADMIN_PASSWORD: testpassword INVENTREE_ADMIN_EMAIL: test@test.com INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345 INVENTREE_PYTHON_TEST_USERNAME: testuser INVENTREE_PYTHON_TEST_PASSWORD: testpassword + INVENTREE_DEBUG: True + INVENTREE_LOG_LEVEL: DEBUG strategy: max-parallel: 4 @@ -44,7 +48,7 @@ jobs: invoke install invoke migrate invoke dev.import-fixtures - invoke dev.server -a 127.0.0.1:12345 & + invoke dev.server -a 0.0.0.0:12345 & invoke wait - name: Run Tests run: | diff --git a/test/docker-compose.yml b/test/docker-compose.yml index dba49b31..fa1932ee 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -1,10 +1,8 @@ -version: "3.8" - # Docker compose recipe for spawning a simple InvenTree server instance, # to use for running local tests of the InvenTree python API # We use the latest (master branch) InvenTree code for testing. -# The tests should be targetted at localhost:12345 +# The tests should be targeted at localhost:12345 services: @@ -16,12 +14,14 @@ services: - 12345:8000 environment: - INVENTREE_DEBUG=True + - INVENTREE_SITE_URL=http://localhost:12345 - INVENTREE_DB_ENGINE=sqlite - INVENTREE_DB_NAME=/home/inventree/data/test_db.sqlite3 - INVENTREE_DEBUG_LEVEL=error - INVENTREE_ADMIN_USER=testuser - INVENTREE_ADMIN_PASSWORD=testpassword - INVENTREE_ADMIN_EMAIL=test@test.com + - INVENTREE_COOKIE_SAMESITE=False restart: unless-stopped volumes: - ./data:/home/inventree/data From d508bc619581e5ae6fbfbdb99a6d359d39ef20c4 Mon Sep 17 00:00:00 2001 From: Jacob Felknor Date: Sat, 11 Jan 2025 19:56:36 -0700 Subject: [PATCH 04/84] add expiry date to po line item receive (#254) --- inventree/purchase_order.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/inventree/purchase_order.py b/inventree/purchase_order.py index ff59f4e8..5746f1aa 100644 --- a/inventree/purchase_order.py +++ b/inventree/purchase_order.py @@ -163,7 +163,7 @@ def getOrder(self): """ return PurchaseOrder(self._api, self.order) - def receive(self, quantity=None, status=10, location=None, batch_code='', serial_numbers=''): + def receive(self, quantity=None, status=10, location=None, expiry_date='', batch_code='', serial_numbers=''): """ Mark this line item as received. @@ -184,6 +184,7 @@ def receive(self, quantity=None, status=10, location=None, batch_code='', serial location: Location ID, or a StockLocation item If given, the following arguments are also sent as parameters: + expiry_date batch_code serial_numbers """ @@ -210,6 +211,7 @@ def receive(self, quantity=None, status=10, location=None, batch_code='', serial 'quantity': quantity, 'status': status, 'location': location_id, + 'expiry_date': expiry_date, 'batch_code': batch_code, 'serial_numbers': serial_numbers } From 81ac887cb4c9a6df52b0987b85e241aa099d3350 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 13 Jan 2025 13:33:36 +1100 Subject: [PATCH 05/84] Fix for PurchaseOrderLineItem.receive (#258) * Fix for PurchaseOrderLineItem.receive - Fix default value for "expiry_date" * Refactor PurchaseOrderLineItem.receive --- inventree/purchase_order.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/inventree/purchase_order.py b/inventree/purchase_order.py index 5746f1aa..1accf507 100644 --- a/inventree/purchase_order.py +++ b/inventree/purchase_order.py @@ -163,7 +163,7 @@ def getOrder(self): """ return PurchaseOrder(self._api, self.order) - def receive(self, quantity=None, status=10, location=None, expiry_date='', batch_code='', serial_numbers=''): + def receive(self, quantity=None, status=10, location=None, expiry_date=None, batch_code=None, serial_numbers=None): """ Mark this line item as received. @@ -202,19 +202,28 @@ def receive(self, quantity=None, status=10, location=None, expiry_date='', batch except: # noqa:E722 location_id = int(location) + item_data = { + 'line_item': self.pk, + 'supplier_part': self.part, + 'quantity': quantity, + 'status': status, + 'location': location_id + } + + # Optional fields which may be set + if expiry_date: + item_data['expiry_date'] = expiry_date + + if batch_code: + item_data['batch_code'] = batch_code + + if serial_numbers: + item_data['serial_numbers'] = serial_numbers + # Prepare request data data = { 'items': [ - { - 'line_item': self.pk, - 'supplier_part': self.part, - 'quantity': quantity, - 'status': status, - 'location': location_id, - 'expiry_date': expiry_date, - 'batch_code': batch_code, - 'serial_numbers': serial_numbers - } + item_data, ], 'location': location_id } From ae1f228e0de8b3fb1504ab67fc38571b0d449298 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 13 Jan 2025 14:43:05 +1100 Subject: [PATCH 06/84] Update base.py (#257) Bump version number to 0.17.3 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index 4a56c486..0428bd66 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.17.2" +INVENTREE_PYTHON_VERSION = "0.17.3" logger = logging.getLogger('inventree') From 4038afcf91f06516856b0f1d77471f2f1585d75e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 5 Feb 2025 16:41:09 +1100 Subject: [PATCH 07/84] Fix for https verification (#261) * Fix for https verification - Pass 'verify=' to all API calls * Bump version number to 0.17.4 --- inventree/api.py | 23 +++++++++++++++-------- inventree/base.py | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 38fe6c00..d749c922 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -196,7 +196,12 @@ def testServer(self): logger.info("Checking InvenTree server connection...") try: - response = requests.get(self.api_url, timeout=self.timeout, proxies=self.proxies) + response = requests.get( + self.api_url, + timeout=self.timeout, + proxies=self.proxies, + verify=self.strict + ) except requests.exceptions.ConnectionError as e: logger.fatal(f"Server connection error: {str(type(e))}") return False @@ -583,13 +588,15 @@ def downloadFile(self, url, destination, overwrite=False, params=None, proxies=d auth = self.auth with requests.get( - fullurl, - stream=True, - auth=auth, - headers=headers, - params=params, - timeout=self.timeout, - proxies=self.proxies) as response: + fullurl, + stream=True, + auth=auth, + headers=headers, + params=params, + timeout=self.timeout, + proxies=self.proxies, + verify=self.strict, + ) as response: # Error code if response.status_code >= 300: diff --git a/inventree/base.py b/inventree/base.py index 0428bd66..26c4fc5d 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.17.3" +INVENTREE_PYTHON_VERSION = "0.17.4" logger = logging.getLogger('inventree') From d186356e0dce7f0d61ad0b11c027d81cda17280b Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 23 Feb 2025 20:57:31 +1100 Subject: [PATCH 08/84] Update unit testing (#262) - Remove 'unit' field check - Ref: https://github.com/inventree/InvenTree/pull/9150 --- test/test_company.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_company.py b/test/test_company.py index c6dd7db6..bb051561 100644 --- a/test/test_company.py +++ b/test/test_company.py @@ -51,7 +51,6 @@ def test_fields(self): field_names = company.Company.fieldNames(self.api) for field in [ - 'url', 'name', 'image', 'is_customer', From bdc240f27bf1b28777083c031267b194d0ad51fc Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 7 Mar 2025 14:11:52 +1100 Subject: [PATCH 09/84] Unit test fix (#263) * Update API level for "modern" report templates * Adjust unit testing - Support new report printing API - Ref: https://github.com/inventree/InvenTree/pull/9096 --- test/test_report.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_report.py b/test/test_report.py index 7f309d3f..691eacd9 100644 --- a/test/test_report.py +++ b/test/test_report.py @@ -51,9 +51,7 @@ def test_print_report(self): # Print the report response = build.printReport(template) - for key in ['pk', 'model_type', 'output', 'template']: + for key in ['pk', 'output']: self.assertIn(key, response) self.assertIsNotNone(response['output']) - self.assertEqual(response['template'], template.pk) - self.assertEqual(response['model_type'], build.getModelType()) From 68d414abce4577cf32ffcc94c7dc1fc074d9a863 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 7 Mar 2025 14:57:29 +1100 Subject: [PATCH 10/84] Adjust unit test Ref: https://github.com/inventree/InvenTree/pull/9096 --- test/test_label.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_label.py b/test/test_label.py index be365b81..1ce02a44 100644 --- a/test/test_label.py +++ b/test/test_label.py @@ -64,11 +64,8 @@ def test_label_print(self): response = part.printLabel(template, plugin=plugin) - for key in ['created', 'model_type', 'complete', 'output', 'template', 'plugin']: + for key in ['created', 'complete', 'output']: self.assertIn(key, response) self.assertEqual(response['complete'], True) - self.assertEqual(response['model_type'], 'part') self.assertIsNotNone(response['output']) - self.assertEqual(response['template'], template.pk) - self.assertEqual(response['plugin'], plugin.key) From b50f3ee7afc08bd5232053d0821758427456e0d2 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Thu, 27 Mar 2025 17:07:50 +1100 Subject: [PATCH 11/84] Add new package requirement - Fix SSL issues on Windows --- inventree/base.py | 2 +- setup.py | 20 +++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index 26c4fc5d..a49841a0 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.17.4" +INVENTREE_PYTHON_VERSION = "0.17.5" logger = logging.getLogger('inventree') diff --git a/setup.py b/setup.py index 69442449..0524793a 100644 --- a/setup.py +++ b/setup.py @@ -10,25 +10,15 @@ setuptools.setup( name="inventree", - version=INVENTREE_PYTHON_VERSION, - author="Oliver Walters", - author_email="oliver.henry.walters@gmail.com", - description="Python interface for InvenTree inventory management system", - long_description=long_description, - long_description_content_type='text/markdown', - keywords="bom, bill of materials, stock, inventory, management, barcode", - url="https://github.com/inventree/inventree-python/", - license="MIT", - packages=setuptools.find_packages( exclude=[ 'ci', @@ -36,14 +26,14 @@ 'test', ] ), - install_requires=[ - "requests>=2.27.0" + "requests>=2.27.0", + "pip-system-certs>=4.0", ], - setup_requires=[ "wheel", + "twine", + "wrapt" ], - - python_requires=">=3.8" + python_requires=">=3.9" ) From b11823651e501970ac233b683625c8f34fbe545c Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 28 Mar 2025 13:32:19 +1100 Subject: [PATCH 12/84] Update setup.py Add pinned requirement for urllib3 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0524793a..0bc16d5f 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ install_requires=[ "requests>=2.27.0", "pip-system-certs>=4.0", + "urllib3>=2.3.0" ], setup_requires=[ "wheel", From 092431664c21afd97172172f235d5ccaba33978d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:34:24 +0200 Subject: [PATCH 13/84] update packaging settings to pyproject.toml --- pyproject.toml | 37 +++++++++++++++++++++++++++++++++++++ setup.py | 39 --------------------------------------- 2 files changed, 37 insertions(+), 39 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..106cee5c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "inventree" +dynamic = ["version"] +description = "Python interface for InvenTree inventory management system" +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +authors = [ + { name = "Oliver Walters", email = "oliver.henry.walters@gmail.com" }, +] +keywords = [ + "barcode", + "bill", + "bom", + "inventory", + "management", + "materials", + "of", + "stock", +] +dependencies = [ + "pip-system-certs>=4.0", + "requests>=2.27.0", +] + +[project.urls] +Homepage = "https://github.com/inventree/inventree-python/" + +[tool.setuptools.dynamic] +version = {attr = "inventree.base.INVENTREE_PYTHON_VERSION"} + +[tool.setuptools] +packages = ["inventree"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 0524793a..00000000 --- a/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- - -import setuptools - -from inventree.base import INVENTREE_PYTHON_VERSION - -with open('README.md', encoding='utf-8') as f: - long_description = f.read() - - -setuptools.setup( - name="inventree", - version=INVENTREE_PYTHON_VERSION, - author="Oliver Walters", - author_email="oliver.henry.walters@gmail.com", - description="Python interface for InvenTree inventory management system", - long_description=long_description, - long_description_content_type='text/markdown', - keywords="bom, bill of materials, stock, inventory, management, barcode", - url="https://github.com/inventree/inventree-python/", - license="MIT", - packages=setuptools.find_packages( - exclude=[ - 'ci', - 'scripts', - 'test', - ] - ), - install_requires=[ - "requests>=2.27.0", - "pip-system-certs>=4.0", - ], - setup_requires=[ - "wheel", - "twine", - "wrapt" - ], - python_requires=">=3.9" -) From 0d51623c9eb77ca4c70a7d60098d3913fbb87b2a Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:40:01 +0200 Subject: [PATCH 14/84] make flake8 use pyproject --- pyproject.toml | 12 ++++++++++++ requirements.txt | 1 + 2 files changed, 13 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 106cee5c..ee289588 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,15 @@ version = {attr = "inventree.base.INVENTREE_PYTHON_VERSION"} [tool.setuptools] packages = ["inventree"] + +[tool.flake8] +ignore =[ + 'C901', + # - W293 - blank lines contain whitespace + 'W293', + # - E501 - line too long (82 characters) + 'E501', + 'N802'] +exclude = ['.git','__pycache__','inventree_server','dist','build','test.py'] +max-complexity = 20 + diff --git a/requirements.txt b/requirements.txt index bfc2b0bb..1243ca35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ requests[socks]>=2.21.0 # Python HTTP for humans with proxy support flake8==3.8.4 # PEP checking +flake8-pyproject==1.2.3 # PEP 621 support wheel>=0.34.2 # Building package invoke>=1.4.0 coverage>=6.4.1 # Run tests, measure coverage From a28da3c8d75ff8eef18ccc703a80d3641f5aff8f Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:43:18 +0200 Subject: [PATCH 15/84] update flake8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1243ca35..a8e15e55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ requests[socks]>=2.21.0 # Python HTTP for humans with proxy support -flake8==3.8.4 # PEP checking +flake8==7.2.0 # PEP checking flake8-pyproject==1.2.3 # PEP 621 support wheel>=0.34.2 # Building package invoke>=1.4.0 From 2c25123cf9ce52ccd0c2476c349cd0a12394f625 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:45:27 +0200 Subject: [PATCH 16/84] lower 1 release --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8e15e55..4f1a39de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ requests[socks]>=2.21.0 # Python HTTP for humans with proxy support -flake8==7.2.0 # PEP checking +flake8==7.1.2 # PEP checking flake8-pyproject==1.2.3 # PEP 621 support wheel>=0.34.2 # Building package invoke>=1.4.0 From 63f2f629af0bebbac10a7e840dc258f0537cecda Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:50:08 +0200 Subject: [PATCH 17/84] update build command --- .github/workflows/build.yaml | 4 ++-- .github/workflows/pypi.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0c806f9e..20312d72 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,5 +23,5 @@ jobs: pip install -U -r requirements.txt - name: Build Python Package run: | - pip install --upgrade pip wheel setuptools - python setup.py bdist_wheel --universal + pip install --upgrade pip wheel setuptools build + python3 -m build diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index e7473f48..fb78a669 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -25,10 +25,10 @@ jobs: - name: Install Python Dependencies run: | pip install -U -r requirements.txt - pip install --upgrade wheel setuptools twine + pip install --upgrade wheel setuptools twine build - name: Build Binary run: | - python3 setup.py sdist bdist_wheel --universal + python3 -m build - name: Publish run: | python3 -m twine upload dist/* From bd1274e9a9a554fad7a8df5be104e18b66a05bc9 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:52:59 +0200 Subject: [PATCH 18/84] update setuptools --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7d90c670..7dc2563c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 77.0.3"] +requires = ["setuptools >= 75.3.2"] build-backend = "setuptools.build_meta" [project] From e66fab2ca744e61501e02412efb1667e99aa5efd Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 31 Mar 2025 23:56:29 +0200 Subject: [PATCH 19/84] update syntax for license --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7dc2563c..fc346c49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "inventree" dynamic = ["version"] description = "Python interface for InvenTree inventory management system" readme = "README.md" -license = "MIT" +license = { file = "LICENSE" } requires-python = ">=3.9" authors = [ { name = "Oliver Walters", email = "oliver.henry.walters@gmail.com" }, From 18ebcbb1ee938517cdca0c354b401476addde41c Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 5 Jun 2025 17:51:47 -0400 Subject: [PATCH 20/84] Add BomItemSubstitute --- inventree/part.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/inventree/part.py b/inventree/part.py index 526d780a..f3375485 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -186,6 +186,15 @@ class BomItem( URL = 'bom' +class BomItemSubstitute( + inventree.base.InventreeObject, + inventree.base.MetadataMixin, +): + """Class representing the BomItemSubstitute database model""" + + URL = "bom/substitute" + + class InternalPrice(inventree.base.InventreeObject): """ Class representing the InternalPrice model """ From 80f1d1de9f0425d6f8467e78714f5484376edf51 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 17 Jun 2025 21:20:36 +1000 Subject: [PATCH 21/84] Tweak unit tests Ref: https://github.com/inventree/InvenTree/pull/9798 --- test/test_part.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_part.py b/test/test_part.py index 8ba0a9fe..5d81cb2b 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -709,8 +709,6 @@ def test_get_requirements(self): self.assertIn('allocated_build_order_quantity', req) self.assertIn('required_sales_order_quantity', req) self.assertIn('allocated_sales_order_quantity', req) - self.assertIn('allocated', req) - self.assertIn('required', req) class PartBarcodeTest(InvenTreeTestCase): From b39ca905fa2072c4c38230fcfadd8d79d4abffe1 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 17 Jun 2025 22:32:31 +1000 Subject: [PATCH 22/84] Account for API version in test --- test/test_part.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/test/test_part.py b/test/test_part.py index 5d81cb2b..a733a0f2 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -703,13 +703,31 @@ def test_get_requirements(self): # Check for expected content self.assertIsInstance(req, dict) - self.assertIn('available_stock', req) - self.assertIn('on_order', req) - self.assertIn('required_build_order_quantity', req) - self.assertIn('allocated_build_order_quantity', req) - self.assertIn('required_sales_order_quantity', req) - self.assertIn('allocated_sales_order_quantity', req) + if self.api.api_version < 350: # Ref: https://github.com/inventree/InvenTree/pull/9798 + fields = [ + 'on_order', + 'allocated_build_order_quantity', + 'allocated_sales_order_quantity', + 'required_build_order_quantity', + 'required_sales_order_quantity', + ] + else: + fields = [ + 'total_stock', + 'unallocated_stock', + 'can_build', + 'ordering', + 'building', + 'scheduled_to_build', + 'required_for_build_orders', + 'allocated_to_build_orders', + 'required_for_sales_orders', + 'allocated_to_sales_orders', + ] + + for f in fields: + self.assertIn(f, req) class PartBarcodeTest(InvenTreeTestCase): """Tests for Part barcode functionality""" From fd64643b9360196bd485a6bb355a81228caf6480 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 17 Jun 2025 22:52:02 +1000 Subject: [PATCH 23/84] Fix comment --- test/test_part.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_part.py b/test/test_part.py index a733a0f2..24cff5f5 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -704,7 +704,8 @@ def test_get_requirements(self): # Check for expected content self.assertIsInstance(req, dict) - if self.api.api_version < 350: # Ref: https://github.com/inventree/InvenTree/pull/9798 + # Ref: https://github.com/inventree/InvenTree/pull/9798 + if self.api.api_version < 350: fields = [ 'on_order', 'allocated_build_order_quantity', From e0ecff961944c50fe43c72ea80fe3ffd7430a90b Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 17 Jun 2025 22:54:42 +1000 Subject: [PATCH 24/84] PEP fix --- test/test_part.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_part.py b/test/test_part.py index 24cff5f5..c16e35e2 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -730,6 +730,7 @@ def test_get_requirements(self): for f in fields: self.assertIn(f, req) + class PartBarcodeTest(InvenTreeTestCase): """Tests for Part barcode functionality""" From 47b9a354878b5e04a4c4fac2418c1e8c61ef45be Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Tue, 22 Jul 2025 23:04:23 -0400 Subject: [PATCH 25/84] Add test for creating multiple StockItems at once --- test/test_stock.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/test_stock.py b/test/test_stock.py index 25811451..ca852af5 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -248,6 +248,25 @@ def test_barcode_support(self): item.unassignBarcode() + def test_serialized(self): + """Test serializing multiple objects on create""" + + # Create items with serial numbers + items = StockItem.create( + self.api, + { + "part": 10004, + "quantity": 3, + "serial_numbers": "1005,1006,1007" + } + ) + + self.assertEqual(3, len(items)) + + self.assertEqual('1005', items[0].serial) + self.assertEqual('1006', items[1].serial) + self.assertEqual('1007', items[2].serial) + class StockAdjustTest(InvenTreeTestCase): """Unit tests for stock 'adjustment' actions""" From 260de070eb2d88736f48f77d74886efe8c65aaca Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Tue, 8 Jul 2025 23:37:51 -0400 Subject: [PATCH 26/84] Override StockItem create to accept list of results --- inventree/stock.py | 28 ++++++++++++++++++++++++++++ test/test_api.py | 3 +-- test/test_stock.py | 4 ++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/inventree/stock.py b/inventree/stock.py index ad45df75..3e4c16b4 100644 --- a/inventree/stock.py +++ b/inventree/stock.py @@ -11,6 +11,9 @@ import inventree.report +logger = logging.getLogger('inventree') + + class StockLocation( inventree.base.BarcodeMixin, inventree.base.MetadataMixin, @@ -58,6 +61,31 @@ class StockItem( MODEL_TYPE = 'stockitem' + @classmethod + def create(cls, api, data, **kwargs): + """ Override default create method to support multiple object return. """ + + cls.checkApiVersion(api) + + # Ensure the pk value is None so an existing object is not updated + if cls.getPkField() in data.keys(): + data.pop(cls.getPkField()) + + response = api.post(cls.URL, data, **kwargs) + + if response is None: + logger.error("Error creating new object") + return None + + if isinstance(response, list): + allResponses = [] + for element in response: + allResponses.append(cls(api, data=element)) + return allResponses + + else: + return [cls(api, data=response)] + @classmethod def adjustStockItems(cls, api: inventree.api.InvenTreeAPI, method: str, items: list, **kwargs): """Perform a generic stock 'adjustment' action. diff --git a/test/test_api.py b/test/test_api.py index 243a000f..6ca62ead 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -209,8 +209,7 @@ def test_create_stuff(self): 'part': p.pk, 'quantity': 45, 'notes': 'This is a note', - - }) + })[0] self.assertIsNotNone(s) self.assertEqual(s.part, p.pk) diff --git a/test/test_stock.py b/test/test_stock.py index ca852af5..031f9981 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -426,6 +426,10 @@ def test_assign_stock(self): } ) + # Verify a single result was returned + self.assertEqual(1, len(assignitem)) + assignitem = assignitem[0] + # Assign the item assignitem.assignStock(customer=customer, notes='Sell on the side') From 40761f8bb3c2646218227cae3c5a2e6e4a663663 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Thu, 7 Aug 2025 23:53:20 +1000 Subject: [PATCH 27/84] Tweak unit tests - Ensure that the test keys match existing templates --- test/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_api.py b/test/test_api.py index 243a000f..44f86520 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -273,12 +273,12 @@ def test_add_result(self): 'value': '0x123456', } - result = item.uploadTestResult('firmware', False, **args) + result = item.uploadTestResult('firmwareversion', False, **args) self.assertTrue(result) - item.uploadTestResult('paint', True) - item.uploadTestResult('extra test', False, value='some data') + item.uploadTestResult('temperaturetest', True) + item.uploadTestResult('settingschecksum', False, value='some data') # There should be 3 more test results now! results = item.getTestResults() From 1687db7e0043acb4677d3d08c47ad4cdcc83865d Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Fri, 15 Aug 2025 20:24:35 +1000 Subject: [PATCH 28/84] Tweak unit tests --- test/test_order.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/test_order.py b/test/test_order.py index ef552d7b..838fc725 100644 --- a/test/test_order.py +++ b/test/test_order.py @@ -269,11 +269,15 @@ def test_order_complete_with_receive(self): result = po.receiveAll(location=use_location.pk) # Check the result returned - self.assertIsInstance(result, dict) - self.assertIn('items', result) - self.assertIn('location', result) - # Check that all except one line were marked - self.assertEqual(len(result['items']), len(po.getLineItems()) - 1) + if self.api.api_version < 385: # Ref: https://github.com/inventree/InvenTree/pull/10174/ + self.assertIsInstance(result, dict) + self.assertIn('items', result) + self.assertIn('location', result) + # Check that all except one line were marked + self.assertEqual(len(result['items']), len(po.getLineItems()) - 1) + else: + self.assertIsInstance(result, list) + self.assertEqual(len(result), len(po.getLineItems()) - 1) # Receive all line items again - make sure answer is None # use the StockLocation item here From 3270719aa41be72c00c297603fc68e93d6722212 Mon Sep 17 00:00:00 2001 From: Jacob Felknor Date: Fri, 15 Aug 2025 12:16:44 -0600 Subject: [PATCH 29/84] make pip-system-certs an optional extra --- README.md | 8 ++++++++ pyproject.toml | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 840c0cae..85b26c9e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ The InvenTree python library can be easily installed using PIP: pip install inventree ``` +If you need to rely on system certificates from the OS certificate store instead of the bundled certificates, use + +``` +pip install inventree[system-certs] +``` + +This allows pip and Python applications to verify TLS/SSL connections to servers whose certificates are trusted by your system, and can be helpful if you're using a custom certificate authority (CA) for your InvenTree instance's cert. + ## Documentation Refer to the [InvenTree documentation](https://docs.inventree.org/en/latest/api/python/python/) diff --git a/pyproject.toml b/pyproject.toml index fc346c49..bf6c42be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,12 @@ keywords = [ "stock", ] dependencies = [ - "pip-system-certs>=4.0", - "requests>=2.27.0", + "requests>=2.27.0", "urllib3>=2.3.0", ] +[project.optional-dependencies] +"system-certs" = ["pip-system-certs>=4.0"] + [project.urls] Homepage = "https://github.com/inventree/inventree-python/" From 680e87fd5af238013f63f4064229a54f861fd4b9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Aug 2025 08:23:23 +1000 Subject: [PATCH 30/84] Update base.py Bump version to 0.18.0 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index a49841a0..a60982ff 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.17.5" +INVENTREE_PYTHON_VERSION = "0.18.0" logger = logging.getLogger('inventree') From d5e6077d3362d17a985007520b54b766591e3da6 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:32:47 +1000 Subject: [PATCH 31/84] Improved URL construction --- inventree/api.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index d749c922..8936a065 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -145,17 +145,8 @@ def constructApiUrl(self, endpoint_url): Returns: A fully qualified URL for the subsequent request """ - # Strip leading / character if provided - if endpoint_url.startswith("/"): - endpoint_url = endpoint_url[1:] + return urljoin(self.api_url, endpoint_url) - url = urljoin(self.api_url, endpoint_url) - - # Ensure the API URL ends with a trailing slash - if not url.endswith('/'): - url += '/' - - return url def testAuth(self): """ @@ -169,7 +160,7 @@ def testAuth(self): return False try: - response = self.get('/user/me/') + response = self.get('user/me/') except requests.exceptions.HTTPError as e: logger.fatal(f"Authentication error: {str(type(e))}") return False @@ -254,7 +245,7 @@ def requestToken(self): # Request an auth token from the server try: response = self.get( - '/user/token/', + 'user/token/', params={ 'name': self.token_name, } From 13746b5c4d1fe661141f98bee238bc795dc230e3 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:33:41 +1000 Subject: [PATCH 32/84] Adjust URL tests --- test/test_api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_api.py b/test/test_api.py index 597a2495..2f63973b 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -57,10 +57,11 @@ def test_url_construction(self): a = api.InvenTreeAPI("http://localhost:1234", connect=False) tests = { - 'part': 'http://localhost:1234/api/part/', - '/part': 'http://localhost:1234/api/part/', - '/part/': 'http://localhost:1234/api/part/', + 'part/': 'http://localhost:1234/api/part/', + '/api/stock/': 'http://localhost:1234/api/stock/', + '/plugin/part/': 'http://localhost:1234/plugin/part/', 'order/so/shipment': 'http://localhost:1234/api/order/so/shipment/', + 'https://example.com/': 'https://example.com/', } for endpoint, url in tests.items(): From 71f075bf2874482d4c7dc9aa9a39b8a783e532c9 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:37:04 +1000 Subject: [PATCH 33/84] Fix URL (add trailing slashes) --- inventree/base.py | 16 +++++++++++----- inventree/build.py | 2 +- inventree/company.py | 10 +++++----- inventree/label.py | 2 +- inventree/part.py | 22 +++++++++++----------- inventree/plugin.py | 2 +- inventree/purchase_order.py | 6 +++--- inventree/report.py | 2 +- inventree/return_order.py | 2 +- inventree/sales_order.py | 10 +++++----- inventree/stock.py | 8 ++++---- inventree/user.py | 2 +- 12 files changed, 45 insertions(+), 39 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index a60982ff..88e33264 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -20,9 +20,15 @@ class InventreeObject(object): URL = "" @classmethod - def get_url(cls, api): + def get_url(cls): """Helper method to get the URL associated with this model.""" - return cls.URL + + url = cls.URL + + if not url.endswith('/'): + url += '/' + + return url # Minimum server version for the particular model type MIN_API_VERSION = None @@ -90,7 +96,7 @@ def __init__(self, api, pk=None, data=None): if pk <= 0: raise ValueError(f"Supplier value ({pk}) for {self.__class__} must be positive.") - url = self.get_url(api) + url = self.get_url() self._url = f"{url}/{pk}/" self._api = api @@ -134,7 +140,7 @@ def options(cls, api): cls.checkApiVersion(api) response = api.request( - cls.URL, + cls.get_url(), method='OPTIONS', ) @@ -668,7 +674,7 @@ def _statusupdate(self, status: str, reload=True, data=None, **kwargs): raise ValueError(f"Order stats {status} not supported.") # Set the url - URL = self.URL + f"/{self.pk}/{status}" + URL = self.URL + f"/{self.pk}/{status}/" if data is None: data = {} diff --git a/inventree/build.py b/inventree/build.py index 9b05d82c..9234f170 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -13,7 +13,7 @@ class Build( ): """ Class representing the Build database model """ - URL = 'build' + URL = 'build/' MODEL_TYPE = 'build' def issue(self): diff --git a/inventree/company.py b/inventree/company.py index ab88c194..fc08fd7a 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -25,7 +25,7 @@ class Address(inventree.base.InventreeObject): class Company(inventree.base.ImageMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject): """ Class representing the Company database model """ - URL = 'company' + URL = 'company/' MODEL_TYPE = "company" def getContacts(self, **kwargs): @@ -103,7 +103,7 @@ class SupplierPart(inventree.base.BarcodeMixin, inventree.base.BulkDeleteMixin, - Implements the BulkDeleteMixin """ - URL = 'company/part' + URL = 'company/part/' def getPriceBreaks(self): """ Get a list of price break objects for this SupplierPart """ @@ -122,7 +122,7 @@ class ManufacturerPart( - Implements the BulkDeleteMixin """ - URL = 'company/part/manufacturer' + URL = 'company/part/manufacturer/' MODEL_TYPE = "manufacturerpart" def getParameters(self, **kwargs): @@ -139,10 +139,10 @@ class ManufacturerPartParameter(inventree.base.BulkDeleteMixin, inventree.base.I - Implements the BulkDeleteMixin """ - URL = 'company/part/manufacturer/parameter' + URL = 'company/part/manufacturer/parameter/' class SupplierPriceBreak(inventree.base.InventreeObject): """ Class representing the SupplierPriceBreak database model """ - URL = 'company/price-break' + URL = 'company/price-break/' diff --git a/inventree/label.py b/inventree/label.py index 8beba797..e6ec15a0 100644 --- a/inventree/label.py +++ b/inventree/label.py @@ -161,7 +161,7 @@ def downloadTemplate(self, destination, overwrite=False): class LabelTemplate(LabelFunctions): """Class representing the LabelTemplate database model.""" - URL = 'label/template' + URL = 'label/template/' def __str__(self): """String representation of the LabelTemplate instance.""" diff --git a/inventree/part.py b/inventree/part.py index f3375485..72842901 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -16,7 +16,7 @@ class PartCategoryParameterTemplate(inventree.base.InventreeObject): """A model which link a ParameterTemplate to a PartCategory""" - URL = 'part/category/parameters' + URL = 'part/category/parameters/' def getCategory(self): """Return the referenced PartCategory instance""" @@ -30,7 +30,7 @@ def getTemplate(self): class PartCategory(inventree.base.MetadataMixin, inventree.base.InventreeObject): """ Class representing the PartCategory database model """ - URL = 'part/category' + URL = 'part/category/' def getParts(self, **kwargs): return Part.list(self._api, category=self.pk, **kwargs) @@ -68,7 +68,7 @@ class Part( ): """ Class representing the Part database model """ - URL = 'part' + URL = 'part/' MODEL_TYPE = 'part' def getCategory(self): @@ -149,7 +149,7 @@ def getRequirements(self): class PartTestTemplate(inventree.base.MetadataMixin, inventree.base.InventreeObject): """ Class representing a test template for a Part """ - URL = 'part/test-template' + URL = 'part/test-template/' @classmethod def generateTestKey(cls, test_name): @@ -183,7 +183,7 @@ class BomItem( ): """ Class representing the BomItem database model """ - URL = 'bom' + URL = 'bom/' class BomItemSubstitute( @@ -192,13 +192,13 @@ class BomItemSubstitute( ): """Class representing the BomItemSubstitute database model""" - URL = "bom/substitute" + URL = "bom/substitute/" class InternalPrice(inventree.base.InventreeObject): """ Class representing the InternalPrice model """ - URL = 'part/internal-price' + URL = 'part/internal-price/' @classmethod def setInternalPrice(cls, api, part, quantity: int, price: float): @@ -219,7 +219,7 @@ def setInternalPrice(cls, api, part, quantity: int, price: float): class SalePrice(inventree.base.InventreeObject): """ Class representing the SalePrice model """ - URL = 'part/sale-price' + URL = 'part/sale-price/' @classmethod def setSalePrice(cls, api, part, quantity: int, price: float, price_currency: str): @@ -241,7 +241,7 @@ def setSalePrice(cls, api, part, quantity: int, price: float, price_currency: st class PartRelated(inventree.base.InventreeObject): """ Class representing a relationship between parts""" - URL = 'part/related' + URL = 'part/related/' @classmethod def add_related(cls, api, part1, part2): @@ -266,7 +266,7 @@ def add_related(cls, api, part1, part2): class Parameter(inventree.base.InventreeObject): """class representing the Parameter database model """ - URL = 'part/parameter' + URL = 'part/parameter/' def getunits(self): """ Get the units for this parameter """ @@ -277,4 +277,4 @@ def getunits(self): class ParameterTemplate(inventree.base.InventreeObject): """ class representing the Parameter Template database model""" - URL = 'part/parameter/template' + URL = 'part/parameter/template/' diff --git a/inventree/plugin.py b/inventree/plugin.py index 4d692ee4..3626844f 100644 --- a/inventree/plugin.py +++ b/inventree/plugin.py @@ -6,7 +6,7 @@ class InvenTreePlugin(inventree.base.MetadataMixin, inventree.base.InventreeObject): """Represents a PluginConfig instance on the InvenTree server.""" - URL = 'plugins' + URL = 'plugins/' MIN_API_VERSION = 197 @classmethod diff --git a/inventree/purchase_order.py b/inventree/purchase_order.py index 1accf507..812ff9ab 100644 --- a/inventree/purchase_order.py +++ b/inventree/purchase_order.py @@ -17,7 +17,7 @@ class PurchaseOrder( ): """ Class representing the PurchaseOrder database model """ - URL = 'order/po' + URL = 'order/po/' MODEL_TYPE = 'purchaseorder' def getSupplier(self): @@ -143,7 +143,7 @@ class PurchaseOrderLineItem( ): """ Class representing the PurchaseOrderLineItem database model """ - URL = 'order/po-line' + URL = 'order/po-line/' def getSupplierPart(self): """ @@ -247,7 +247,7 @@ class PurchaseOrderExtraLineItem( ): """ Class representing the PurchaseOrderExtraLineItem database model """ - URL = 'order/po-extra-line' + URL = 'order/po-extra-line/' def getOrder(self): """ diff --git a/inventree/report.py b/inventree/report.py index ae28584b..9c8a1d6f 100644 --- a/inventree/report.py +++ b/inventree/report.py @@ -124,4 +124,4 @@ def downloadTemplate(self, destination, overwrite=False): class ReportTemplate(ReportFunctions): """Class representing the ReportTemplate model.""" - URL = 'report/template' + URL = 'report/template/' diff --git a/inventree/return_order.py b/inventree/return_order.py index c5cf324f..d2be36f0 100644 --- a/inventree/return_order.py +++ b/inventree/return_order.py @@ -18,7 +18,7 @@ class ReturnOrder( ): """Class representing the ReturnOrder database model""" - URL = 'order/ro' + URL = 'order/ro/' MIN_API_VERSION = 104 MODEL_TYPE = 'returnorder' diff --git a/inventree/sales_order.py b/inventree/sales_order.py index e7c5189b..cc2249d9 100644 --- a/inventree/sales_order.py +++ b/inventree/sales_order.py @@ -18,7 +18,7 @@ class SalesOrder( ): """ Class representing the SalesOrder database model """ - URL = 'order/so' + URL = 'order/so/' MODEL_TYPE = 'salesorder' def getCustomer(self): @@ -91,7 +91,7 @@ class SalesOrderLineItem( ): """ Class representing the SalesOrderLineItem database model """ - URL = 'order/so-line' + URL = 'order/so-line/' def getPart(self): """ @@ -182,7 +182,7 @@ class SalesOrderExtraLineItem( ): """ Class representing the SalesOrderExtraLineItem database model """ - URL = 'order/so-extra-line' + URL = 'order/so-extra-line/' def getOrder(self): """ @@ -197,7 +197,7 @@ class SalesOrderAllocation( """Class representing the SalesOrderAllocation database model.""" MIN_API_VERSION = 267 - URL = 'order/so-allocation' + URL = 'order/so-allocation/' def getOrder(self): """Return the SalesOrder to which this SalesOrderAllocation belongs.""" @@ -228,7 +228,7 @@ class SalesOrderShipment( ): """Class representing a shipment for a SalesOrder""" - URL = 'order/so/shipment' + URL = 'order/so/shipment/' def getOrder(self): """Return the SalesOrder to which this SalesOrderShipment belongs.""" diff --git a/inventree/stock.py b/inventree/stock.py index 3e4c16b4..decf4e70 100644 --- a/inventree/stock.py +++ b/inventree/stock.py @@ -23,7 +23,7 @@ class StockLocation( ): """ Class representing the StockLocation database model """ - URL = 'stock/location' + URL = 'stock/location/' MODEL_TYPE = 'stocklocation' def getStockItems(self, **kwargs): @@ -57,7 +57,7 @@ class StockItem( ): """Class representing the StockItem database model.""" - URL = 'stock' + URL = 'stock/' MODEL_TYPE = 'stockitem' @@ -338,7 +338,7 @@ def uploadTestResult(self, test_name, test_result, **kwargs): class StockItemTracking(inventree.base.InventreeObject): """Class representing a StockItem tracking object.""" - URL = 'stock/track' + URL = 'stock/track/' class StockItemTestResult( @@ -356,7 +356,7 @@ class StockItemTestResult( and will be associated with the correct PartTestTemplate on the server. """ - URL = 'stock/test' + URL = 'stock/test/' MODEL_TYPE = 'stockitem' def getTestTemplate(self): diff --git a/inventree/user.py b/inventree/user.py index dc255240..fdc9fc16 100644 --- a/inventree/user.py +++ b/inventree/user.py @@ -10,4 +10,4 @@ class User(inventree.base.InventreeObject): """ Class representing the User database model """ - URL = 'user' + URL = 'user/' From b8850f66c88ba5df2e507798e418e3821b4cfd90 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:38:57 +1000 Subject: [PATCH 34/84] Tweak python version for CI testing --- .github/workflows/build.yaml | 2 +- .github/workflows/pep.yaml | 2 +- .github/workflows/pypi.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 20312d72..2c200a67 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.8] + python-version: [3.10] steps: - name: Checkout Code diff --git a/.github/workflows/pep.yaml b/.github/workflows/pep.yaml index b367602c..e7a1e730 100644 --- a/.github/workflows/pep.yaml +++ b/.github/workflows/pep.yaml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.8] + python-version: [3.11] steps: - name: Checkout Code diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index fb78a669..0d092e76 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -18,7 +18,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.11 - name: Check Release Tag run: | python3 ci/check_version_number.py ${{ github.event.release.tag_name }} From 8a25b13a554512e8391a559f15e30b7315b50ac1 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:40:20 +1000 Subject: [PATCH 35/84] Py 3.11 --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2c200a67..94da4546 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.10] + python-version: [3.11] steps: - name: Checkout Code From 01a37dd0775fbf16e78232a8253eefe4fadd9bf8 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:41:39 +1000 Subject: [PATCH 36/84] Style fixes --- inventree/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/inventree/api.py b/inventree/api.py index 8936a065..366b8a03 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -147,7 +147,6 @@ def constructApiUrl(self, endpoint_url): return urljoin(self.api_url, endpoint_url) - def testAuth(self): """ Checks if the set user credentials or the used token From 65654dcbab76d9a1bc20accca4f2bd6ae0ffb520 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:43:37 +1000 Subject: [PATCH 37/84] Type hinting --- inventree/api.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 366b8a03..358ba686 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -78,7 +78,7 @@ def __init__(self, host=None, **kwargs): if kwargs.get('connect', True): self.connect() - def setHostName(self, host): + def setHostName(self, host: str): """Validate that the provided base URL is valid""" if host is None: @@ -136,7 +136,7 @@ def connect(self): if not self.token: self.requestToken() - def constructApiUrl(self, endpoint_url): + def constructApiUrl(self, endpoint_url: str) -> str: """Construct an API endpoint URL based on the provided API URL. Arguments: @@ -263,14 +263,14 @@ def requestToken(self): return self.token - def request(self, api_url, **kwargs): + def request(self, url: str, **kwargs): """ Perform a URL request to the Inventree API """ if not self.connected: # If we have not established a connection to the server yet, attempt now self.connect() - api_url = self.constructApiUrl(api_url) + api_url = self.constructApiUrl(url) data = kwargs.get('data', kwargs.get('json', {})) files = kwargs.get('files', {}) @@ -391,7 +391,7 @@ def request(self, api_url, **kwargs): return response - def delete(self, url, **kwargs): + def delete(self, url: str, **kwargs): """ Perform a DELETE request. Used to remove a record in the database. """ @@ -410,7 +410,7 @@ def delete(self, url, **kwargs): return response - def post(self, url, data, **kwargs): + def post(self, url: str, data: dict, **kwargs): """ Perform a POST request. Used to create a new record in the database. Args: @@ -447,7 +447,7 @@ def post(self, url, data, **kwargs): return data - def patch(self, url, data, **kwargs): + def patch(self, url: str, data: dict, **kwargs): """ Perform a PATCH request. @@ -485,7 +485,7 @@ def patch(self, url, data, **kwargs): return data - def put(self, url, data, **kwargs): + def put(self, url: str, data: dict, **kwargs): """ Perform a PUT request. Used to update existing records in the database. @@ -521,7 +521,7 @@ def put(self, url, data, **kwargs): return data - def get(self, url, **kwargs): + def get(self, url: str, **kwargs): """ Perform a GET request. For argument information, refer to the 'request' method From f113e6244bd375c3c2175052ecfb165d186e08f9 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:44:17 +1000 Subject: [PATCH 38/84] Bump version to 0.19.0 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index a60982ff..5aa26ace 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.18.0" +INVENTREE_PYTHON_VERSION = "0.19.0" logger = logging.getLogger('inventree') From 01d5266f745ea2439edeb8909762b6d6457c5e99 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 13:49:15 +1000 Subject: [PATCH 39/84] Fix unit test --- test/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_api.py b/test/test_api.py index 2f63973b..277c5c19 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -60,7 +60,7 @@ def test_url_construction(self): 'part/': 'http://localhost:1234/api/part/', '/api/stock/': 'http://localhost:1234/api/stock/', '/plugin/part/': 'http://localhost:1234/plugin/part/', - 'order/so/shipment': 'http://localhost:1234/api/order/so/shipment/', + 'order/so/shipment': 'http://localhost:1234/api/order/so/shipment', 'https://example.com/': 'https://example.com/', } From de44ebd22bfbe0075ef236c9f6c5863f1c98b94e Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 14:23:23 +1000 Subject: [PATCH 40/84] Update invoke msg --- tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks.py b/tasks.py index 5733b6c4..b3bfd0e3 100644 --- a/tasks.py +++ b/tasks.py @@ -200,4 +200,5 @@ def test(c, source=None, update=False, reset=False, debug=False, host=None, user c.run(f'coverage run -m unittest {source}') else: # Automatically discover tests, and run only those + print("Running all unit tests") c.run('coverage run -m unittest discover -s test/') From c37283f91d25900a465b0752f9d77406ddfc3272 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 14:51:38 +1000 Subject: [PATCH 41/84] Fix broken URLs --- inventree/api.py | 2 +- inventree/base.py | 8 ++++---- inventree/label.py | 2 +- inventree/report.py | 2 +- test/test_stock.py | 3 +++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 358ba686..aff7e2b1 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -623,7 +623,7 @@ def scanBarcode(self, barcode_data): barcode_data = json.dumps(barcode_data) response = self.post( - '/barcode/', + 'barcode/', { 'barcode': str(barcode_data), } diff --git a/inventree/base.py b/inventree/base.py index 88e33264..d4f717af 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -549,7 +549,7 @@ class MetadataMixin: - The 'metadata' is not used for any InvenTree business logic - Instead it can be used by plugins for storing arbitrary information - Internally it is stored as a JSON database field - - Metadata is accessed via the API by appending '/metadata/' to the API URL + - Metadata is accessed via the API by appending './metadata/' to the API URL Note: Requires server API version 49 or newer @@ -724,7 +724,7 @@ def assignBarcode(self, barcode_data: str, reload=True): model_type = self.barcodeModelType() response = self._api.post( - '/barcode/link/', + 'barcode/link/', { 'barcode': barcode_data, model_type: self.pk, @@ -736,13 +736,13 @@ def assignBarcode(self, barcode_data: str, reload=True): return response - def unassignBarcode(self, reload=True): + def unassignBarcode(self, reload: bool = True): """Unassign a barcode from this object""" model_type = self.barcodeModelType() response = self._api.post( - '/barcode/unlink/', + 'barcode/unlink/', { model_type: self.pk, } diff --git a/inventree/label.py b/inventree/label.py index e6ec15a0..91d3fe11 100644 --- a/inventree/label.py +++ b/inventree/label.py @@ -39,7 +39,7 @@ def saveOutput(self, output, filename): def printLabel(self, template, plugin=None, destination=None, *args, **kwargs): """Print a label against the provided label template.""" - print_url = '/label/print/' + print_url = 'label/print/' template_id = self.getTemplateId(template) diff --git a/inventree/report.py b/inventree/report.py index 9c8a1d6f..1968e56a 100644 --- a/inventree/report.py +++ b/inventree/report.py @@ -29,7 +29,7 @@ def printReport(self, report, destination=None, *args, **kwargs): If neither plugin nor destination is given, nothing will be done """ - print_url = '/report/print/' + print_url = 'report/print/' template_id = self.getTemplateId(report) response = self._api.post( diff --git a/test/test_stock.py b/test/test_stock.py index 031f9981..1b275e17 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -267,6 +267,9 @@ def test_serialized(self): self.assertEqual('1006', items[1].serial) self.assertEqual('1007', items[2].serial) + # Delete the items after the test + for item in items: + item.delete() class StockAdjustTest(InvenTreeTestCase): """Unit tests for stock 'adjustment' actions""" From 039f655dd36d7d364fcea2df76767ede7d6c05d4 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 14:53:32 +1000 Subject: [PATCH 42/84] Style fix --- test/test_stock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_stock.py b/test/test_stock.py index 1b275e17..51b57ebc 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -271,6 +271,7 @@ def test_serialized(self): for item in items: item.delete() + class StockAdjustTest(InvenTreeTestCase): """Unit tests for stock 'adjustment' actions""" From 9eef99308a3e4576833264be23e63f6e809d6876 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Wed, 3 Sep 2025 15:15:03 +1000 Subject: [PATCH 43/84] Fix URL --- inventree/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/sales_order.py b/inventree/sales_order.py index cc2249d9..c3affe92 100644 --- a/inventree/sales_order.py +++ b/inventree/sales_order.py @@ -249,7 +249,7 @@ def allocateItems(self, items=[]): """ # Customize URL - url = f'order/so/{self.getOrder().pk}/allocate' + url = f'order/so/{self.getOrder().pk}/allocate/' # Create data from given inputs data = { From 95eae88f6b32b0f9d50ee56a3f512b26832e5fbc Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 21:56:44 +1100 Subject: [PATCH 44/84] Add Parameter mixin classes --- inventree/base.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index fb87739b..b7afe1c9 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') @@ -543,6 +543,31 @@ 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 ParameterMixin: + """Mixin class which allows a model class to interact with parameters. + + Ref: https://github.com/inventree/InvenTree/pull/10699 + """ + + def getParameters(self): + """Return a list of parameters associated with this object.""" + + return Parameter.list( + self._api, + model_type=self.getModelType(), + model_id=self.pk + ) + + class MetadataMixin: """Mixin class for models which support a 'metadata' attribute. From a8dec2fc60da6dcd5999ab23430937e35ba4cf4b Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 21:58:52 +1100 Subject: [PATCH 45/84] Add mixin class to existing model types --- inventree/build.py | 1 + inventree/company.py | 11 +++++++++-- inventree/part.py | 1 + inventree/purchase_order.py | 1 + inventree/return_order.py | 1 + inventree/sales_order.py | 1 + 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/inventree/build.py b/inventree/build.py index 9234f170..8bc0c5e7 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 fc08fd7a..dbf7730d 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -22,7 +22,10 @@ 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 +100,10 @@ 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 +119,7 @@ def getPriceBreaks(self): class ManufacturerPart( inventree.base.AttachmentMixin, + inventree.base.ParameterMixin, inventree.base.BulkDeleteMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject, diff --git a/inventree/part.py b/inventree/part.py index 72842901..1cf42a6a 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -60,6 +60,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, diff --git a/inventree/purchase_order.py b/inventree/purchase_order.py index 812ff9ab..02c42330 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 d2be36f0..7534a22d 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 c3affe92..411d3116 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, From 02ffa418637ab9bc437d4790f50ff42adb1c0027 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 22:03:04 +1100 Subject: [PATCH 46/84] Add logic for fetching parameters based on API version --- inventree/base.py | 12 ++++++++++++ inventree/part.py | 28 +++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index b7afe1c9..fcd72626 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -552,6 +552,15 @@ class Parameter(BulkDeleteMixin, InventreeObject): 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. @@ -561,6 +570,9 @@ class ParameterMixin: def getParameters(self): """Return a list of parameters associated with this object.""" + if self._api.get_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(), diff --git a/inventree/part.py b/inventree/part.py index 1cf42a6a..168f7055 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -109,7 +109,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.get_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 """ @@ -265,17 +270,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 From fbbb37fe9bd9d01ee003cdba0f5487301c1f4d1b Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 22:07:01 +1100 Subject: [PATCH 47/84] Updated unit tests --- test/test_part.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/test_part.py b/test/test_part.py index c16e35e2..f51bcab2 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 @@ -186,11 +186,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 +571,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)) From be4486efba44ee8399985f89a350cb78ce722b2b Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 22:10:29 +1100 Subject: [PATCH 48/84] Fix class type --- .github/workflows/ci.yaml | 2 +- inventree/part.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f3acf4d3..c5b41eab 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/part.py b/inventree/part.py index 168f7055..16f5b50c 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -24,7 +24,7 @@ def getCategory(self): def getTemplate(self): """Return the referenced ParameterTemplate instance""" - return ParameterTemplate(self._api, self.parameter_template) + return PartParameterTemplate(self._api, self.parameter_template) class PartCategory(inventree.base.MetadataMixin, inventree.base.InventreeObject): From 0c5918aae98b5a92734aff004eb97d7c13722726 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 22:18:45 +1100 Subject: [PATCH 49/84] pep checks --- inventree/company.py | 11 +++++++++-- setup.cfg | 2 +- test/test_part.py | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/inventree/company.py b/inventree/company.py index dbf7730d..a5fb46f0 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -25,7 +25,10 @@ class Address(inventree.base.InventreeObject): class Company( inventree.base.AttachmentMixin, inventree.base.ParameterMixin, - inventree.base.ImageMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject): + inventree.base.ImageMixin, + inventree.base.MetadataMixin, + inventree.base.InventreeObject +): """ Class representing the Company database model """ URL = 'company/' @@ -103,7 +106,11 @@ def createReturnOrder(self, **kwargs): class SupplierPart( inventree.base.AttachmentMixin, inventree.base.ParameterMixin, - inventree.base.BarcodeMixin, inventree.base.BulkDeleteMixin, inventree.base.MetadataMixin, inventree.base.InventreeObject): + inventree.base.BarcodeMixin, + inventree.base.BulkDeleteMixin, + inventree.base.MetadataMixin, + inventree.base.InventreeObject +): """Class representing the SupplierPart database model - Implements the BulkDeleteMixin diff --git a/setup.cfg b/setup.cfg index d59997bb..e3ab9f28 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_part.py b/test/test_part.py index f51bcab2..64f89f53 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -15,7 +15,7 @@ from test_api import InvenTreeTestCase # noqa: E402 -from inventree.base import Attachment, Parameter, ParameterTemplate # 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, PartParameter, # noqa: E402 @@ -192,7 +192,7 @@ def test_part_get_functions(self): if self.api.api_version >= Parameter.MIN_API_VERSION: functions['getParameters'] = Parameter - else: + else: functions['getParameters'] = PartParameter if self.api.api_version >= Attachment.MIN_API_VERSION: From a92f9ccf29291cc71d074a4b3795b15953cf4dfc Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Mon, 1 Dec 2025 22:51:45 +1100 Subject: [PATCH 50/84] Fix unit tests --- inventree/base.py | 2 +- inventree/part.py | 2 +- test/test_part.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index fcd72626..535231a1 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -570,7 +570,7 @@ class ParameterMixin: def getParameters(self): """Return a list of parameters associated with this object.""" - if self._api.get_api_version() < Parameter.MIN_API_VERSION: + 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( diff --git a/inventree/part.py b/inventree/part.py index 16f5b50c..7f7dd5e2 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -110,7 +110,7 @@ def getStockItems(self, **kwargs): def getParameters(self): """ Return parameters associated with this part """ - if self._api.get_api_version() < inventree.base.Parameter.MIN_API_VERSION: + if self._api.api_version < inventree.base.Parameter.MIN_API_VERSION: # Return legacy PartParameter objects return PartParameter.list(self._api, part=self.pk) diff --git a/test/test_part.py b/test/test_part.py index 64f89f53..53f76ed1 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -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 From add8203cb73e3111532762920dba55cfb4121fb4 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 08:55:47 +1100 Subject: [PATCH 51/84] Fix for ManufacturerPartParameter --- inventree/company.py | 9 ++++-- test/test_company.py | 71 -------------------------------------------- 2 files changed, 7 insertions(+), 73 deletions(-) diff --git a/inventree/company.py b/inventree/company.py index a5fb46f0..a48bac42 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -144,16 +144,21 @@ 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/test/test_company.py b/test/test_company.py index bb051561..dda7bd74 100644 --- a/test/test_company.py +++ b/test/test_company.py @@ -127,77 +127,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 From cf70099e917d8161bbd2de853ee09a8c3b3f1788 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 09:00:49 +1100 Subject: [PATCH 52/84] Support optional filtering kwargs --- inventree/base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index 535231a1..05d42068 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -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=""): @@ -567,7 +568,7 @@ class ParameterMixin: Ref: https://github.com/inventree/InvenTree/pull/10699 """ - def getParameters(self): + def getParameters(self, **kwargs): """Return a list of parameters associated with this object.""" if self._api.api_version < Parameter.MIN_API_VERSION: @@ -576,7 +577,8 @@ def getParameters(self): return Parameter.list( self._api, model_type=self.getModelType(), - model_id=self.pk + model_id=self.pk, + **kwargs ) From 0f8b121aa0dc67d27a95b57d8bbf9e7ed8f1bc19 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 09:16:02 +1100 Subject: [PATCH 53/84] Style fixes --- inventree/company.py | 1 + test/test_company.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/inventree/company.py b/inventree/company.py index a48bac42..29f43c92 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -150,6 +150,7 @@ def getParameters(self, **kwargs): return super().getParameters(**kwargs) + class ManufacturerPartParameter(inventree.base.BulkDeleteMixin, inventree.base.InventreeObject): """Class representing the ManufacturerPartParameter database model. diff --git a/test/test_company.py b/test/test_company.py index dda7bd74..db8d4a82 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: From 658ae1c35a6fbd5e4c0bc141a975465143223b8f Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 09:37:40 +1100 Subject: [PATCH 54/84] Specify model type --- test/test_part.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test_part.py b/test/test_part.py index 53f76ed1..45db27d6 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -589,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) From 4d62e07112d1e1b7b61d4898b8312cca1f7759ef Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 14:35:40 +1100 Subject: [PATCH 55/84] Fix unit test --- test/test_part.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_part.py b/test/test_part.py index 45db27d6..6cb55c2d 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -613,14 +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}) + param = Parameter.create(self.api, data={'model_type': 'part', 'model_id': p.pk, 'template': parametertemplate.pk, 'data': 10}) # Unit should be equal self.assertEqual(param.getunits(), 'kg A') @@ -631,7 +631,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) From 93a862648c4c5ff69fc8848b1a2c53e8ba806543 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 15:18:17 +1100 Subject: [PATCH 56/84] Simplify unit test --- test/test_part.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test_part.py b/test/test_part.py index 6cb55c2d..a48523fb 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -622,9 +622,6 @@ def test_parameters(self): # Define w. required values - integer param = Parameter.create(self.api, data={'model_type': 'part', 'model_id': p.pk, 'template': parametertemplate.pk, 'data': 10}) - # Unit should be equal - self.assertEqual(param.getunits(), 'kg A') - # result should not be None self.assertIsNotNone(param) From 3e8a7ec9a091abbcdd6c625624ad9bf776aff7a9 Mon Sep 17 00:00:00 2001 From: "Asterix\\Oliver" Date: Tue, 2 Dec 2025 16:20:10 +1100 Subject: [PATCH 57/84] Fix references for PartCategoryParameterTemplate --- inventree/part.py | 9 ++++++++- test/test_part.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/inventree/part.py b/inventree/part.py index 7f7dd5e2..8e94240c 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 PartParameterTemplate(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): diff --git a/test/test_part.py b/test/test_part.py index a48523fb..d2e25131 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -150,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, } ) From 3010d20fadc496a58b187836ba2fd56f8ff137cf Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Wed, 3 Dec 2025 16:04:39 -0500 Subject: [PATCH 58/84] Add Group and Owner api objects --- inventree/user.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/inventree/user.py b/inventree/user.py index fdc9fc16..bd358ffa 100644 --- a/inventree/user.py +++ b/inventree/user.py @@ -11,3 +11,15 @@ class User(inventree.base.InventreeObject): """ Class representing the User database model """ URL = 'user/' + + +class Group(inventree.base.InventreeObject): + """Class representing the Group database model""" + + URL = "user/group/" + + +class Owner(inventree.base.InventreeObject): + """Class representing the Owner database model""" + + URL = "user/owner/" From 6afd22315295cd0ce05191b5fa225b6fe8b28669 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 5 Jan 2026 19:17:17 +0100 Subject: [PATCH 59/84] Add support for new metadata endpoint --- inventree/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index 05d42068..342ed654 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -593,10 +593,15 @@ class MetadataMixin: Note: Requires server API version 49 or newer """ + NEW_METADATA_API_VERSION = 436 @property def metadata_url(self): - return os.path.join(self._url, "metadata/") + """Return the metadata URL for this model instance.""" + if self._api.api_version < self.NEW_METADATA_API_VERSION: + return os.path.join(self._url, "metadata/") + model_type = self.getModelType() + return f"metadata/{model_type}/{self.pk}/" def getMetadata(self): """Read model instance metadata""" @@ -623,14 +628,14 @@ def setMetadata(self, data, overwrite=False): if overwrite: return self._api.put( - self.metadata_url, + self.metadata_url, data={ "metadata": data, } ) else: return self._api.patch( - self.metadata_url, + self.metadata_url, data={ "metadata": data } From b98bd1bd1cba11d1f45be5bcadddc2e6c85c16b0 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 6 Jan 2026 01:17:04 +0100 Subject: [PATCH 60/84] fix style --- inventree/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index 342ed654..f01c6e77 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -628,14 +628,14 @@ def setMetadata(self, data, overwrite=False): if overwrite: return self._api.put( - self.metadata_url, + self.metadata_url, data={ "metadata": data, } ) else: return self._api.patch( - self.metadata_url, + self.metadata_url, data={ "metadata": data } From 43c277bd865687356135ddafbc3cb851aad234ac Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 6 Jan 2026 14:07:08 +1100 Subject: [PATCH 61/84] Bump InvenTree Python version to 0.21.0 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index f01c6e77..0fd01ff0 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.20.0" +INVENTREE_PYTHON_VERSION = "0.21.0" logger = logging.getLogger('inventree') From b477db008356cdaa231ff28a8378a335ea92a2b2 Mon Sep 17 00:00:00 2001 From: Jacob Felknor Date: Tue, 6 Jan 2026 11:56:11 -0700 Subject: [PATCH 62/84] update __getattr__ to prevent recursion errors with django_q2 tasks --- inventree/base.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index 05d42068..b46418cc 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -352,11 +352,17 @@ def __contains__(self, name): return name in self._data def __getattr__(self, name): + try: + data = object.__getattribute__(self, "_data") + except AttributeError: + # Appears to happen during pickling. Raise immediately to prevent recursion errors + raise AttributeError(name) - if name in self._data.keys(): - return self._data[name] - else: - return super().__getattribute__(name) + if name in data: + return data[name] + + # if we're in this block, there already wasn't a "normal" attribute with this name. Raise + raise AttributeError(name) def __getitem__(self, name): if name in self._data.keys(): From 08de6106c34db01d3002cc8e967cc2ced3261635 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 7 Jan 2026 09:16:07 +1100 Subject: [PATCH 63/84] Bump INVERTREE_PYTHON_VERSION to 0.21.1 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index 6de52e11..465405aa 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.21.0" +INVENTREE_PYTHON_VERSION = "0.21.1" logger = logging.getLogger('inventree') From c005ef8ca94b7849f0a2bfe085575d6c03a5a5c9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 4 Feb 2026 20:41:21 +1100 Subject: [PATCH 64/84] Enhanced build models support - Add BuildLine - Add BuildItem --- inventree/build.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/inventree/build.py b/inventree/build.py index 8bc0c5e7..50a16b58 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -2,6 +2,7 @@ import inventree.base import inventree.report +import inventree.stock class Build( @@ -48,3 +49,40 @@ def complete( def finish(self, *args, **kwargs): """Alias for complete""" return self.complete(*args, **kwargs) + + def getLines(self, **kwargs): + """ Return the build line items associated with this build order """ + return BuildLine.list(self._api, build=self.pk, **kwargs) + +class BuildLine( + inventree.base.InventreeObject, +): + """ Class representing the BuildLine database model """ + + URL = 'build/line/' + MODEL_TYPE = 'buildline' + + def getBuild(self): + """Return the Build object associated with this line item""" + return Build(self._api, self.build) + + +class BuildItem( + inventree.base.InventreeObject, +): + """ Class representing the BuildItem database model """ + + URL = 'build/item/' + MODEL_TYPE = 'builditem' + + def getBuild(self): + """Return the Build object associated with this build item""" + return Build(self._api, self.build) + + def getBuildLine(self): + """Return the BuildLine object associated with this build item""" + return BuildLine(self._api, self.build_line) + + def getStockItem(self): + """Return the StockItem object associated with this build item""" + return inventree.stock.StockItem(self._api, self.stock_item) From 2d35cf24ac939ca70fab8594b7f9a82a99086d08 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 4 Feb 2026 20:43:46 +1100 Subject: [PATCH 65/84] Additional unit testing --- test/test_build.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/test_build.py b/test/test_build.py index 45929bb8..d98c8227 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -12,7 +12,7 @@ from test_api import InvenTreeTestCase # noqa: E402 from inventree.base import Attachment # noqa: E402 -from inventree.build import Build # noqa: E402 +from inventree.build import Build, BuildLine # noqa: E402 class BuildOrderTest(InvenTreeTestCase): @@ -144,3 +144,16 @@ def test_build_complete(self): # Check status self.assertEqual(build.status, 40) self.assertEqual(build.status_text, 'Complete') + + def test_build_lines(self): + """ Test retrieval of build line items. """ + + build = self.get_build() + + lines = build.getLines() + + self.assertGreater(len(lines), 0) + + for line in lines: + self.assertEqual(line.build, build.pk) + self.assertIsInstance(line, BuildLine) From 221e66cb886a94a197e1a3dce8678ae95ec99ae1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 4 Feb 2026 20:48:10 +1100 Subject: [PATCH 66/84] Style fixes --- inventree/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/inventree/build.py b/inventree/build.py index 50a16b58..d20fb3dd 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -54,6 +54,7 @@ def getLines(self, **kwargs): """ Return the build line items associated with this build order """ return BuildLine.list(self._api, build=self.pk, **kwargs) + class BuildLine( inventree.base.InventreeObject, ): From 8d7d6d0151f81b67958e15bb85a564bf1db9c413 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 4 Feb 2026 22:03:44 +1100 Subject: [PATCH 67/84] Use part which actually has BOM --- test/test_build.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_build.py b/test/test_build.py index d98c8227..b5e7d560 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -36,9 +36,9 @@ def get_build(self): self.api, { "title": "Automated test build", - "part": 25, + "part": 100, "quantity": 100, - "reference": f"BO-{n+1:04d}", + "reference": f"BO-{n + 1:04d}", } ) else: @@ -93,7 +93,7 @@ def test_build_cancel(self): "title": "Automated test build", "part": 25, "quantity": 100, - "reference": f"BO-{n+1:04d}" + "reference": f"BO-{n + 1:04d}" } ) @@ -118,7 +118,7 @@ def test_build_complete(self): "title": "Automated test build", "part": 25, "quantity": 100, - "reference": f"BO-{n+1:04d}" + "reference": f"BO-{n + 1:04d}" } ) From 1758bbe665d589232d3101fa1d428730ac787f28 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 11 Feb 2026 20:27:56 +1100 Subject: [PATCH 68/84] Remove tests for now --- test/test_build.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/test/test_build.py b/test/test_build.py index b5e7d560..f7db8837 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -12,7 +12,7 @@ from test_api import InvenTreeTestCase # noqa: E402 from inventree.base import Attachment # noqa: E402 -from inventree.build import Build, BuildLine # noqa: E402 +from inventree.build import Build # noqa: E402 class BuildOrderTest(InvenTreeTestCase): @@ -144,16 +144,3 @@ def test_build_complete(self): # Check status self.assertEqual(build.status, 40) self.assertEqual(build.status_text, 'Complete') - - def test_build_lines(self): - """ Test retrieval of build line items. """ - - build = self.get_build() - - lines = build.getLines() - - self.assertGreater(len(lines), 0) - - for line in lines: - self.assertEqual(line.build, build.pk) - self.assertIsInstance(line, BuildLine) From 03f7c41c8147d7c4ce52e234e3064f7a32df3778 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 11 Feb 2026 20:42:55 +1100 Subject: [PATCH 69/84] Bump InvenTree Python version to 0.22.0 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index 465405aa..954f3f16 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.21.1" +INVENTREE_PYTHON_VERSION = "0.22.0" logger = logging.getLogger('inventree') From 5289609c115cbabda9178d605f9af2a8fb146e71 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 15 Feb 2026 11:19:05 +1100 Subject: [PATCH 70/84] Add code for supporting build outputs --- inventree/build.py | 92 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/inventree/build.py b/inventree/build.py index d20fb3dd..a7c6158a 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -21,7 +21,7 @@ class Build( def issue(self): """Mark this build as 'issued'.""" return self._statusupdate(status='issue') - + def hold(self): """Mark this build as 'on hold'.""" return self._statusupdate(status='hold') @@ -54,6 +54,94 @@ def getLines(self, **kwargs): """ Return the build line items associated with this build order """ return BuildLine.list(self._api, build=self.pk, **kwargs) + def getBuildOutputs(self, complete: bool = None, **kwargs): + """ Return the build output items associated with this build order + + Arguments: + - complete: If not None, filter the build outputs by their 'complete' status + """ + if complete is not None: + kwargs['complete'] = complete + + # Find stock items which are marked as 'outputs' of this build order + return inventree.stock.StockItem.list( + self._api, + build=self.pk, + **kwargs + ) + + def createBuildOutput(self, **kwargs): + """ Create a new build output (stock item) associated with this build order """ + return self._api.post( + f'{self.URL}create_output/', + data={ + **kwargs + } + ) + + def cancelBuildOutputs(self, outputs): + """ Cancel a build output item associated with this build order + + Arguments: + - outputs: The StockItem object (or list of StockItem objects, or PK(s)) to cancel + """ + + if not isinstance(outputs, list): + outputs = [outputs] + + for idx, output in outputs: + if isinstance(output, inventree.stock.StockItem): + outputs[idx] = output.pk + + return self._api.post( + f'{self.URL}delete-outputs/', + data={ + 'outputs': outputs, + } + ) + + def scrapBuildOutput(self, outputs, **kwargs): + """ Scrap a build output item associated with this build order + + Arguments: + - outputs: The StockItem object (or list of StockItem objects, or PK(s)) to scrap + """ + if not isinstance(outputs, list): + outputs = [outputs] + + for idx, output in outputs: + if isinstance(output, inventree.stock.StockItem): + outputs[idx] = output.pk + + return self._api.post( + f'{self.URL}scrap-outputs/', + data={ + 'build': self.pk, + 'outputs': { + # TODO + }, + **kwargs + } + ) + + def completeBuildOutput(self, stock_item, **kwargs): + """ Mark a build output item as complete + + Arguments: + - stock_item: The StockItem object (or PK) to mark as complete + """ + if isinstance(stock_item, inventree.stock.StockItem): + stock_item = stock_item.pk + + return self._api.post( + f'{self.URL}complete_output/', + data={ + 'build': self.pk, + 'stock_item': stock_item, + **kwargs + } + ) + class BuildLine( inventree.base.InventreeObject, @@ -83,7 +171,7 @@ def getBuild(self): def getBuildLine(self): """Return the BuildLine object associated with this build item""" return BuildLine(self._api, self.build_line) - + def getStockItem(self): """Return the StockItem object associated with this build item""" return inventree.stock.StockItem(self._api, self.stock_item) From 3d8c75163d1f946167fe1169a99428a669cdafc2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 15 Feb 2026 11:36:18 +1100 Subject: [PATCH 71/84] Add unit testing for creating build outputs --- inventree/build.py | 7 +++++-- test/test_build.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/inventree/build.py b/inventree/build.py index a7c6158a..dd4ca675 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -72,13 +72,16 @@ def getBuildOutputs(self, complete: bool = None, **kwargs): def createBuildOutput(self, **kwargs): """ Create a new build output (stock item) associated with this build order """ - return self._api.post( - f'{self.URL}create_output/', + result = self._api.post( + f'{self.URL}{self.pk}/create-output/', data={ **kwargs } ) + # Note: The response is a list of created stock items + return [inventree.stock.StockItem(self._api, item['pk'], item) for item in result] + def cancelBuildOutputs(self, outputs): """ Cancel a build output item associated with this build order diff --git a/test/test_build.py b/test/test_build.py index f7db8837..e215a74a 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -144,3 +144,55 @@ def test_build_complete(self): # Check status self.assertEqual(build.status, 40) self.assertEqual(build.status_text, 'Complete') + + +class BuildOrderOutputTests(InvenTreeTestCase): + """ Unit tests for build output functionality """ + + def setUp(self): + """ Ensure we have a base build order to work with """ + + super().setUp() + + builds = Build.list(self.api) + + self.build = Build.create( + self.api, + { + "title": "A new build order", + "part": 25, + "quantity": 10, + "reference": f"BO-{len(builds) + 1:04d}" + } + ) + + def test_create_build_output(self): + """Test that we can create a build output item""" + + # Initially, there should be no build outputs + outputs = self.build.getBuildOutputs() + self.assertEqual(len(outputs), 0) + + # Let's create 3 new outputs (with serial numbers) + outputs = self.build.createBuildOutput( + quantity=3, + batch_code='TEST-BATCH-001', + serial_numbers='300+' + ) + + self.assertEqual(len(outputs), 3) + self.assertEqual(len(self.build.getBuildOutputs()), 3) + + for output in outputs: + self.assertIsNotNone(output) + self.assertEqual(output.quantity, 1) + self.assertEqual(output.batch, 'TEST-BATCH-001') + self.assertEqual(output.build, self.build.pk) + self.assertEqual(output.part, self.build.part) + self.assertTrue(output.is_building) + + # Directly delete the build output + output.delete() + + # There should now be no build outputs again + self.assertEqual(len(self.build.getBuildOutputs()), 0) From e32b12d49665d9b3e558e10ab63a269b43b8f32e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 15 Feb 2026 22:39:02 +1100 Subject: [PATCH 72/84] Implement remaining functionality --- inventree/build.py | 75 ++++++++++++++++++++++------------------- test/test_build.py | 84 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 36 deletions(-) diff --git a/inventree/build.py b/inventree/build.py index dd4ca675..6c60b037 100644 --- a/inventree/build.py +++ b/inventree/build.py @@ -61,7 +61,7 @@ def getBuildOutputs(self, complete: bool = None, **kwargs): - complete: If not None, filter the build outputs by their 'complete' status """ if complete is not None: - kwargs['complete'] = complete + kwargs['is_building'] = not complete # Find stock items which are marked as 'outputs' of this build order return inventree.stock.StockItem.list( @@ -86,63 +86,68 @@ def cancelBuildOutputs(self, outputs): """ Cancel a build output item associated with this build order Arguments: - - outputs: The StockItem object (or list of StockItem objects, or PK(s)) to cancel + - outputs: The StockItem object (or list of StockItem objects) to cancel """ if not isinstance(outputs, list): outputs = [outputs] - for idx, output in outputs: - if isinstance(output, inventree.stock.StockItem): - outputs[idx] = output.pk - return self._api.post( - f'{self.URL}delete-outputs/', + f'{self.URL}{self.pk}/delete-outputs/', data={ - 'outputs': outputs, + 'outputs': [ + {'output': output.pk} for output in outputs + ] } ) - def scrapBuildOutput(self, outputs, **kwargs): - """ Scrap a build output item associated with this build order + def scrapBuildOutput(self, output, **kwargs): + """ Scrap a single build output item associated with this build order Arguments: - - outputs: The StockItem object (or list of StockItem objects, or PK(s)) to scrap + - output: The StockItem object to scrap """ - if not isinstance(outputs, list): - outputs = [outputs] - for idx, output in outputs: - if isinstance(output, inventree.stock.StockItem): - outputs[idx] = output.pk + data = { + **kwargs, + 'outputs': [ + { + 'output': output.pk, + 'quantity': kwargs.get('quantity', output.quantity), + } + ] + } + + data['location'] = kwargs.get('location', output.location) return self._api.post( - f'{self.URL}scrap-outputs/', - data={ - 'build': self.pk, - 'outputs': { - # TODO - }, - **kwargs - } + f'{self.URL}{self.pk}/scrap-outputs/', + data=data ) - def completeBuildOutput(self, stock_item, **kwargs): - """ Mark a build output item as complete + def completeBuildOutput(self, output, **kwargs): + """ Mark a single build output item as complete Arguments: - - stock_item: The StockItem object (or PK) to mark as complete + - output: The StockItem object to mark as complete """ - if isinstance(stock_item, inventree.stock.StockItem): - stock_item = stock_item.pk + + data = { + **kwargs, + 'outputs': [ + { + 'output': output.pk, + 'quantity': kwargs.get('quantity', output.quantity), + } + ] + } + + # If a location is not specified, use the current location of the stock item + data['location'] = kwargs.get('location', output.location) return self._api.post( - f'{self.URL}complete_output/', - data={ - 'build': self.pk, - 'stock_item': stock_item, - **kwargs - } + f'{self.URL}{self.pk}/complete/', + data=data ) diff --git a/test/test_build.py b/test/test_build.py index e215a74a..914a34ad 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -177,7 +177,7 @@ def test_create_build_output(self): outputs = self.build.createBuildOutput( quantity=3, batch_code='TEST-BATCH-001', - serial_numbers='300+' + serial_numbers='400+' ) self.assertEqual(len(outputs), 3) @@ -196,3 +196,85 @@ def test_create_build_output(self): # There should now be no build outputs again self.assertEqual(len(self.build.getBuildOutputs()), 0) + + def test_cancel_build_output(self): + """ Test that we can cancel a build output item """ + + self.assertEqual(len(self.build.getBuildOutputs()), 0) + + # Create a new build output + output = self.build.createBuildOutput( + quantity=1, + batch_code='TEST-BATCH-001', + serial_numbers='456' + )[0] + + self.assertEqual(len(self.build.getBuildOutputs()), 1) + + self.build.cancelBuildOutputs(output) + self.assertEqual(len(self.build.getBuildOutputs()), 0) + + def test_complete_build_output(self): + """ Test that we can complete a build output item """ + + self.assertEqual(len(self.build.getBuildOutputs()), 0) + + # Create a new build output + output = self.build.createBuildOutput( + quantity=1, + batch_code='TEST-BATCH-001', + serial_numbers='457' + )[0] + + q = self.build.completed + + self.assertTrue(output.is_building) + self.assertEqual(len(self.build.getBuildOutputs()), 1) + + # Complete the build output + self.build.completeBuildOutput(output, location=1) + + self.assertEqual(len(self.build.getBuildOutputs()), 1) + output.reload() + self.assertFalse(output.is_building) + + # Remove the output + output.delete() + self.assertEqual(len(self.build.getBuildOutputs()), 0) + + # The number of "completed" items should have increased by 1 + self.build.reload() + self.assertEqual(self.build.completed, q + 1) + + def test_scrap_build_output(self): + """Test that we can scrap a build output item""" + + self.assertEqual(len(self.build.getBuildOutputs()), 0) + + # Create a new build output + output = self.build.createBuildOutput( + quantity=1, + batch_code='TEST-BATCH-001', + serial_numbers='468' + )[0] + + q = self.build.completed + + self.assertTrue(output.is_building) + self.assertEqual(len(self.build.getBuildOutputs()), 1) + + # Scrap the build output + self.build.scrapBuildOutput(output, location=1, notes='Test scrap') + self.assertEqual(len(self.build.getBuildOutputs()), 1) + self.assertEqual(len(self.build.getBuildOutputs(complete=False)), 0) + self.assertEqual(len(self.build.getBuildOutputs(complete=True)), 1) + + output.reload() + self.assertFalse(output.is_building) + + # Remove the build output + output.delete() + + # The number of "completed" items should not have increased + self.build.reload() + self.assertEqual(self.build.completed, q) From 1cc22757f9375c6c3afe8ea38296caac846215a9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 15 Feb 2026 22:47:55 +1100 Subject: [PATCH 73/84] Update INVENTREE_PYTHON_VERSION to 0.23.0 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index 954f3f16..3312676f 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.22.0" +INVENTREE_PYTHON_VERSION = "0.23.0" logger = logging.getLogger('inventree') From 30ff52a5fea8e637b5693df9578a6c9a0755fb90 Mon Sep 17 00:00:00 2001 From: "Ing. Petr Pecha" Date: Mon, 2 Mar 2026 12:35:49 +0100 Subject: [PATCH 74/84] Allow import SalesOrderAllocation from order.py --- inventree/order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/inventree/order.py b/inventree/order.py index e9cb2fc7..64bc4061 100644 --- a/inventree/order.py +++ b/inventree/order.py @@ -16,3 +16,4 @@ from inventree.sales_order import SalesOrderExtraLineItem # noqa:F401 from inventree.sales_order import SalesOrderLineItem # noqa:F401 from inventree.sales_order import SalesOrderShipment # noqa:F401 +from inventree.sales_order import SalesOrderAllocation # noqa:F401 From 19becd472b9649982272d9ef4c69de1291114cac Mon Sep 17 00:00:00 2001 From: "Ing. Petr Pecha" Date: Mon, 2 Mar 2026 12:47:57 +0100 Subject: [PATCH 75/84] fix spaces before comment --- inventree/order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/order.py b/inventree/order.py index 64bc4061..8228c114 100644 --- a/inventree/order.py +++ b/inventree/order.py @@ -16,4 +16,4 @@ from inventree.sales_order import SalesOrderExtraLineItem # noqa:F401 from inventree.sales_order import SalesOrderLineItem # noqa:F401 from inventree.sales_order import SalesOrderShipment # noqa:F401 -from inventree.sales_order import SalesOrderAllocation # noqa:F401 +from inventree.sales_order import SalesOrderAllocation # noqa:F401 From 50fc16a6a6fbb3f6ba4c819310a70f3e51a4170b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 2 Mar 2026 23:07:22 +1100 Subject: [PATCH 76/84] Bump InvenTree Python version to 0.23.1 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index 3312676f..e224128b 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.23.0" +INVENTREE_PYTHON_VERSION = "0.23.1" logger = logging.getLogger('inventree') From 976929dfeb0ab296a19b558e6a30e5eb4d6a3afa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 Mar 2026 14:50:00 +1100 Subject: [PATCH 77/84] Adjust unit test for bulk delete - Use pk values instead of filters --- test/test_stock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_stock.py b/test/test_stock.py index 51b57ebc..253e0a8a 100644 --- a/test/test_stock.py +++ b/test/test_stock.py @@ -209,9 +209,9 @@ def test_bulk_delete(self): self.assertTrue(len(StockItem.list(self.api, location=3)) >= 10) # Delete *all* items from location 3 - StockItem.bulkDelete(self.api, filters={ - 'location': 3 - }) + items = [item.pk for item in StockItem.list(self.api, location=3)] + + StockItem.bulkDelete(self.api, items=list(items)) loc = StockLocation(self.api, pk=3) items = loc.getStockItems() From 38f69caee7b08a07810b1531ab0c5a47b93c00a1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 13 May 2026 23:13:49 +1000 Subject: [PATCH 78/84] Default to model name if MODEL_TYPE is not defined --- inventree/base.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/inventree/base.py b/inventree/base.py index e224128b..7af8a3de 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -2,9 +2,10 @@ import json import logging -import requests import os +import requests + from . import api as inventree_api INVENTREE_PYTHON_VERSION = "0.23.1" @@ -60,7 +61,7 @@ def pk(self): # Coerce 'pk' values to integer if self.getPkField() == 'pk': val = int(val) - + return val def __str__(self): @@ -92,7 +93,7 @@ def __init__(self, api, pk=None, data=None): pk = int(str(pk).strip()) except Exception: raise TypeError(f"Invalid primary key value '{pk}' for {self.__class__}") - + if pk <= 0: raise ValueError(f"Supplier value ({pk}) for {self.__class__} must be positive.") @@ -113,7 +114,13 @@ def __init__(self, api, pk=None, data=None): @classmethod def getModelType(cls): """Return the model type for this label printing class.""" - return cls.MODEL_TYPE + model_type = cls.MODEL_TYPE + + if not model_type: + # Default to the class name (lower case) if the model type is not explicitly defined + model_type = cls.__name__.lower() + + return model_type @classmethod def checkApiVersion(cls, api): @@ -532,7 +539,7 @@ def uploadAttachment(self, attachment, comment=""): model_type=self.getModelType(), model_id=self.pk ) - + def addLinkAttachment(self, link, comment=""): """Add an external link attachment against this Object. @@ -570,7 +577,7 @@ class ParameterTemplate(InventreeObject): class ParameterMixin: """Mixin class which allows a model class to interact with parameters. - + Ref: https://github.com/inventree/InvenTree/pull/10699 """ @@ -604,9 +611,13 @@ class MetadataMixin: @property def metadata_url(self): """Return the metadata URL for this model instance.""" + + # Legacy metadata API endpoints if self._api.api_version < self.NEW_METADATA_API_VERSION: return os.path.join(self._url, "metadata/") + model_type = self.getModelType() + return f"metadata/{model_type}/{self.pk}/" def getMetadata(self): From fd4d734d4fc6df2242a96b964ee7398be318ce87 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 13 May 2026 23:16:10 +1000 Subject: [PATCH 79/84] Define more MODEL_TYPE data --- inventree/company.py | 3 +++ inventree/part.py | 10 ++++++---- inventree/stock.py | 2 -- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/inventree/company.py b/inventree/company.py index 29f43c92..485062e5 100644 --- a/inventree/company.py +++ b/inventree/company.py @@ -13,6 +13,7 @@ class Contact(inventree.base.InventreeObject): URL = 'company/contact/' MIN_API_VERSION = 104 + MODEL_TYPE = 'contact' class Address(inventree.base.InventreeObject): @@ -20,6 +21,7 @@ class Address(inventree.base.InventreeObject): URL = 'company/address/' MIN_API_VERSION = 126 + MODEL_TYPE = 'address' class Company( @@ -117,6 +119,7 @@ class SupplierPart( """ URL = 'company/part/' + MODEL_TYPE = "supplierpart" def getPriceBreaks(self): """ Get a list of price break objects for this SupplierPart """ diff --git a/inventree/part.py b/inventree/part.py index 8e94240c..a2de68fe 100644 --- a/inventree/part.py +++ b/inventree/part.py @@ -38,6 +38,7 @@ class PartCategory(inventree.base.MetadataMixin, inventree.base.InventreeObject) """ Class representing the PartCategory database model """ URL = 'part/category/' + MODEL_TYPE = 'partcategory' def getParts(self, **kwargs): return Part.list(self._api, category=self.pk, **kwargs) @@ -120,7 +121,7 @@ def getParameters(self): 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): @@ -140,7 +141,7 @@ def setInternalPrice(self, quantity: int, price: float): """ return InternalPrice.setInternalPrice(self._api, self.pk, quantity, price) - + def getSalePrice(self): """ Get sales prices for this part @@ -197,6 +198,7 @@ class BomItem( """ Class representing the BomItem database model """ URL = 'bom/' + MODEL_TYPE = 'bomitem' class BomItemSubstitute( @@ -279,7 +281,7 @@ def add_related(cls, api, part1, part2): 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 @@ -298,7 +300,7 @@ 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 """ diff --git a/inventree/stock.py b/inventree/stock.py index decf4e70..da87a1ef 100644 --- a/inventree/stock.py +++ b/inventree/stock.py @@ -10,7 +10,6 @@ import inventree.part import inventree.report - logger = logging.getLogger('inventree') @@ -58,7 +57,6 @@ class StockItem( """Class representing the StockItem database model.""" URL = 'stock/' - MODEL_TYPE = 'stockitem' @classmethod From 40f2de74d572631bab52c5ab1b8b090fda880fd9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 13 May 2026 23:18:58 +1000 Subject: [PATCH 80/84] Add unit test --- test/test_part.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/test/test_part.py b/test/test_part.py index d2e25131..4d42a3d4 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -15,12 +15,12 @@ from test_api import InvenTreeTestCase # noqa: E402 -from inventree.base import Attachment, Parameter, ParameterTemplate # noqa: E402 +from inventree.base import (Attachment, Parameter, # noqa: E402 + ParameterTemplate) from inventree.company import SupplierPart # noqa: E402 from inventree.part import InternalPrice # noqa: E402 -from inventree.part import (BomItem, PartParameter, # noqa: E402 - Part, - PartCategory, PartCategoryParameterTemplate, +from inventree.part import (BomItem, Part, PartCategory, # noqa: E402 + PartCategoryParameterTemplate, PartParameter, PartRelated, PartTestTemplate) from inventree.stock import StockItem # noqa: E402 @@ -28,6 +28,18 @@ class PartCategoryTest(InvenTreeTestCase): """Tests for PartCategory models""" + def test_metadata(self): + """Fetch metadata for a particular category.""" + + cat = PartCategory.list(self.api, limit=1)[0] + + url = cat.metadata_url + + self.assertEqual(url, f"metadata/partcategory/{cat.pk}/") + + metadata = cat.getMetadata() + self.assertIsInstance(metadata, dict) + def test_part_cats(self): """ Tests for category filtering @@ -74,7 +86,7 @@ def test_elec(self): # Create some new categories under this one for idx in range(3): - name = f"Subcategory {n+idx}" + name = f"Subcategory {n + idx}" cat = PartCategory.create(self.api, { "parent": child.pk, @@ -290,7 +302,7 @@ def test_part_list(self): for i in range(5): prt = Part.create(self.api, { "category": 5, - "name": f"Special Part {n+i}", + "name": f"Special Part {n + i}", "description": "A new part in this category!", }) @@ -741,7 +753,7 @@ def test_get_requirements(self): 'required_for_sales_orders', 'allocated_to_sales_orders', ] - + for f in fields: self.assertIn(f, req) From 675f4278c2e3cd5ab0311e0e62e895c9479a477f Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 May 2026 23:20:22 +1000 Subject: [PATCH 81/84] Bump Python version to 0.23.2 --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index e224128b..c8c973e0 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.23.1" +INVENTREE_PYTHON_VERSION = "0.23.2" logger = logging.getLogger('inventree') From fe629a79e95346cf95da1d7fe86e3c8064d4ce72 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 19 May 2026 22:03:45 +0200 Subject: [PATCH 82/84] make use of newer api --- inventree/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inventree/api.py b/inventree/api.py index aff7e2b1..fca36a9f 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -242,9 +242,13 @@ def requestToken(self): return False # Request an auth token from the server + if self.api_version < 490: + token_url = 'user/token/' + else: + token_url = 'user/me/token/' try: response = self.get( - 'user/token/', + token_url, params={ 'name': self.token_name, } From d610e408eaa5e2379544027e107ff9a24ed3e8b9 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 19 May 2026 22:04:16 +0200 Subject: [PATCH 83/84] update token url --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index b3bfd0e3..d8df9397 100644 --- a/tasks.py +++ b/tasks.py @@ -65,7 +65,7 @@ def check_server(c, host="http://localhost:12345", username="testuser", password auth = HTTPBasicAuth(username=username, password=password) - url = f"{host}/api/user/token/" + url = f"{host}/api/user/me/token/" response = None From ab0913c6aa253643c92dbcdf3a6c70ed9eb20af7 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 19 May 2026 22:16:05 +0200 Subject: [PATCH 84/84] add fallback --- tasks.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index d8df9397..5db905d2 100644 --- a/tasks.py +++ b/tasks.py @@ -65,14 +65,15 @@ def check_server(c, host="http://localhost:12345", username="testuser", password auth = HTTPBasicAuth(username=username, password=password) - url = f"{host}/api/user/me/token/" + token_url = f"{host}/api/user/me/token/" + token_fallback_checked = False response = None while response is None: try: - response = requests.get(url, auth=auth, timeout=0.5) + response = requests.get(token_url, auth=auth, timeout=0.5) except Exception as e: if debug: print("Error:", str(e)) @@ -88,6 +89,16 @@ def check_server(c, host="http://localhost:12345", username="testuser", password else: return False + # Maybe this is an old implementation? Check for X-InvenTree-API header + if response and not token_fallback_checked: + token_version = response.headers.get('X-InvenTree-API', None) + if token_version and int(token_version) < 490: + if debug: + print("No token endpoint, but server is responding - maybe an old implementation?") + token_fallback_checked = True + token_url = f"{host}/api/user/token/" + response = None + if response.status_code != 200: if debug: print(f"Invalid status code: ${response.status_code}")