From 602f071405e5a0c53ddfc7b2b289c682ae589fbf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 02:25:11 +0000 Subject: [PATCH 1/9] Tweak unit testing --- src/backend/InvenTree/common/models.py | 2 +- src/backend/InvenTree/part/test_api.py | 4 +--- src/backend/InvenTree/part/test_param.py | 8 ++------ .../InvenTree/report/templatetags/report.py | 9 +++++++++ src/backend/InvenTree/report/test_tags.py | 20 +++++++++---------- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 7ee07f7175..d60f384fb6 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -2631,7 +2631,7 @@ class Parameter( from plugin import PluginMixinEnum, registry for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): - # Note: The validate_part_parameter function may raise a ValidationError + # Note: The validate_parameter function may raise a ValidationError try: if hasattr(plugin, 'validate_parameter'): result = plugin.validate_parameter(self, self.data) diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index caf0060493..e0f248e866 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -236,9 +236,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase): n = ParameterTemplate.objects.count() # Ensure validation of parameter values is disabled for these checks - InvenTreeSetting.set_setting( - 'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None - ) + InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', False, change_user=None) for template in ParameterTemplate.objects.all(): response = self.post( diff --git a/src/backend/InvenTree/part/test_param.py b/src/backend/InvenTree/part/test_param.py index ae0ecc14b1..2e77055ec0 100644 --- a/src/backend/InvenTree/part/test_param.py +++ b/src/backend/InvenTree/part/test_param.py @@ -252,9 +252,7 @@ class ParameterTests(TestCase): raise ValueError('This test must be refactored...') # Disable enforcing of part parameter units - InvenTreeSetting.set_setting( - 'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None - ) + InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', False, change_user=None) # Invalid units also pass, but will be converted to the template units for value in bad_values: @@ -262,9 +260,7 @@ class ParameterTests(TestCase): param.full_clean() # Enable enforcing of part parameter units - InvenTreeSetting.set_setting( - 'PART_PARAMETER_ENFORCE_UNITS', True, change_user=None - ) + InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', True, change_user=None) for value in bad_values: param = Parameter(content_object=prt, template=template, data=value) diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 146bcd6539..0c1dcab6e6 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -353,6 +353,15 @@ def parameter( ) +@register.simple_tag() +def part_parametr(instance, parameter_name): + """Included for backwards compatibility - use 'parameter' tag instead. + + Ref: https://github.com/inventree/InvenTree/pull/10699 + """ + return parameter(instance, parameter_name) + + @register.simple_tag() def company_image( company: Company, preview: bool = False, thumbnail: bool = False, **kwargs diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py index d5ca46e4a0..1988e43cb9 100644 --- a/src/backend/InvenTree/report/test_tags.py +++ b/src/backend/InvenTree/report/test_tags.py @@ -12,7 +12,7 @@ from django.utils.safestring import SafeString from djmoney.money import Money from PIL import Image -from common.models import InvenTreeSetting +from common.models import InvenTreeSetting, Parameter, ParameterTemplate from InvenTree.config import get_testfolder_dir from InvenTree.unit_test import InvenTreeTestCase from part.models import Part # TODO fix import: PartParameter, PartParameterTemplate @@ -406,16 +406,16 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): def test_part_parameter(self): """Test the part_parameter template tag.""" - # TODO fix import: PartParameter, PartParameterTemplate - # # Test with a valid part - # part = Part.objects.create(name='test', description='test') - # t1 = PartParameterTemplate.objects.create(name='Template 1', units='mm') - # parameter = PartParameter.objects.create(part=part, template=t1, data='test') + # Test with a valid part + part = Part.objects.create(name='test', description='test') + t1 = ParameterTemplate.objects.create(name='Template 1', units='mm') + parameter = Parameter.objects.create(part=part, template=t1, data='test') - # self.assertEqual(report_tags.part_parameter(part, 'name'), None) - # self.assertEqual(report_tags.part_parameter(part, 'Template 1'), parameter) - # # Test with an invalid part - # self.assertEqual(report_tags.part_parameter(None, 'name'), None) + # Note, use the 'parameter' and 'part_parameter' tags interchangeably here + self.assertEqual(report_tags.part_parameter(part, 'name'), None) + self.assertEqual(report_tags.parameter(part, 'Template 1'), parameter) + # Test with an invalid part + self.assertEqual(report_tags.parameter(None, 'name'), None) def test_render_currency(self): """Test the render_currency template tag.""" From 0746a1131f117108413cf9ea74db3feca035d6d3 Mon Sep 17 00:00:00 2001 From: "Karl Q." Date: Tue, 25 Nov 2025 19:26:14 -0800 Subject: [PATCH 2/9] docs: brief writeup for env-vars on webserver in Dockerfile (#10906) * docs: brief writeup for env-vars on webserver in Dockerfile Mates with https://github.com/inventree/InvenTree/pull/10900 nit, fix: spacing 2 -> 1 * docs: relocate And simplify --------- Co-authored-by: Karl Quinsland --- docs/docs/start/docker_install.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/docs/start/docker_install.md b/docs/docs/start/docker_install.md index 3ae72c5082..0e53c85f7d 100644 --- a/docs/docs/start/docker_install.md +++ b/docs/docs/start/docker_install.md @@ -235,6 +235,20 @@ The [Caddy](./docker.md#ssl-certificates) container will automatically generate Any persistent files generated by the Caddy container (such as certificates, etc) will be stored in the `caddy` directory within the external volume. +### Web Server Bind Address + +By default, the Dockerized InvenTree web server binds to all available network interfaces and listens for IPv4 traffic on port 8000. +This can be adjusted using the following environment variables: + +| Environment Variable | Default | +| --- | --- | --- | --- | +| INVENTREE_WEB_ADDR | 0.0.0.0 | +| INVENTREE_WEB_PORT | 8000 | + +These variables are combined in the [Dockerfile](../../../contrib/container/Dockerfile) to build the bind string passed to the InvenTree server on startup. + +To enable IPv6/Dual Stack support, set `INVENTREE_WEB_ADDR` to `[::]` when you create/start the container. + ### Demo Dataset To quickly get started with a [demo dataset](../demo.md), you can run the following command: From 2e7e321564faaa16398e9b27080886090a354498 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 03:28:21 +0000 Subject: [PATCH 3/9] Fix unit test --- src/backend/InvenTree/report/test_tags.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py index 1988e43cb9..6fdaf5cbe4 100644 --- a/src/backend/InvenTree/report/test_tags.py +++ b/src/backend/InvenTree/report/test_tags.py @@ -4,6 +4,7 @@ from decimal import Decimal from zoneinfo import ZoneInfo from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.test import TestCase, override_settings from django.utils import timezone @@ -409,7 +410,11 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): # Test with a valid part part = Part.objects.create(name='test', description='test') t1 = ParameterTemplate.objects.create(name='Template 1', units='mm') - parameter = Parameter.objects.create(part=part, template=t1, data='test') + + content_type = ContentType.objects.get_for_model(Part) + parameter = Parameter.objects.create( + model_type=content_type, model_id=part.pk, template=t1, data='test' + ) # Note, use the 'parameter' and 'part_parameter' tags interchangeably here self.assertEqual(report_tags.part_parameter(part, 'name'), None) From 7620a681e28ca1219c7ffbfc84e88f2155b9cc47 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 03:29:57 +0000 Subject: [PATCH 4/9] Enable bulk-create --- src/backend/InvenTree/common/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 2a9cd5faed..6dea5af93d 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -37,7 +37,7 @@ from common.icons import get_icon_packs from common.settings import get_global_setting from data_exporter.mixins import DataExportViewMixin from generic.states.api import urlpattern as generic_states_api_urls -from InvenTree.api import BulkDeleteMixin, MetadataView +from InvenTree.api import BulkCreateMixin, BulkDeleteMixin, MetadataView from InvenTree.config import CONFIG_LOOKUPS from InvenTree.filters import ( ORDER_FILTER, @@ -898,6 +898,7 @@ class ParameterMixin: class ParameterList( OutputOptionsMixin, ParameterMixin, + BulkCreateMixin, BulkDeleteMixin, DataExportViewMixin, ListCreateAPI, From a4d16f3621081ae1ea0551feb9cb0ff5e7b54356 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 05:54:46 +0000 Subject: [PATCH 5/9] More fixes --- src/backend/InvenTree/part/models.py | 8 ++++---- src/backend/InvenTree/part/test_category.py | 2 +- src/backend/InvenTree/part/test_param.py | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index db10a21557..207f094e50 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -226,7 +226,7 @@ class PartCategory( """Prefectch parts parameters.""" return ( self.get_parts(cascade=cascade) - .prefetch_related('parameters', 'parameters__template') + .prefetch_related('parameters_list', 'parameters_list__template') .all() ) @@ -237,7 +237,7 @@ class PartCategory( parts = prefetch or self.prefetch_parts_parameters(cascade=cascade) for part in parts: - for parameter in part.parameters.all(): + for parameter in part.parameters_list.all(): parameter_name = parameter.template.name if parameter_name not in unique_parameters_names: unique_parameters_names.append(parameter_name) @@ -260,7 +260,7 @@ class PartCategory( if part.IPN: part_parameters['IPN'] = part.IPN - for parameter in part.parameters.all(): + for parameter in part.parameters_list.all(): parameter_name = parameter.template.name parameter_value = parameter.data part_parameters[parameter_name] = parameter_value @@ -3763,7 +3763,7 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): if ( self.default_value and get_global_setting( - 'PART_PARAMETER_ENFORCE_UNITS', True, cache=False, create=False + 'PARAMETER_ENFORCE_UNITS', True, cache=False, create=False ) and self.template.units ): diff --git a/src/backend/InvenTree/part/test_category.py b/src/backend/InvenTree/part/test_category.py index 85d6d6c246..a5bb5e6a2d 100644 --- a/src/backend/InvenTree/part/test_category.py +++ b/src/backend/InvenTree/part/test_category.py @@ -163,7 +163,7 @@ class CategoryTest(TestCase): # Iterate through all parts and parameters for fastener in fasteners: self.assertIsInstance(fastener, Part) - for parameter in fastener.parameters.all(): + for parameter in fastener.parameters_list.all(): self.assertIsInstance(parameter, Parameter) self.assertIsInstance(parameter.template, ParameterTemplate) diff --git a/src/backend/InvenTree/part/test_param.py b/src/backend/InvenTree/part/test_param.py index 2e77055ec0..4e3bae3a70 100644 --- a/src/backend/InvenTree/part/test_param.py +++ b/src/backend/InvenTree/part/test_param.py @@ -249,8 +249,6 @@ class ParameterTests(TestCase): bad_values = ['3 Amps', '-3 zogs', '3.14F'] - raise ValueError('This test must be refactored...') - # Disable enforcing of part parameter units InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', False, change_user=None) From 5408898145d6f38b54abcb7a34c82f06aba23eb6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 06:00:33 +0000 Subject: [PATCH 6/9] More unit test tweaks --- src/backend/InvenTree/report/templatetags/report.py | 7 +++++-- src/backend/InvenTree/report/test_tags.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 0c1dcab6e6..6329b5a9dc 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -343,7 +343,10 @@ def parameter( Returns: A Parameter object, or None if not found """ - if not hasattr(instance, 'parameters'): + if instance is None: + raise ValueError('parameter tag requires a valid Model instance') + + if not isinstance(instance, Model) or not hasattr(instance, 'parameters'): raise TypeError("parameter tag requires a Model with 'parameters' attribute") return ( @@ -354,7 +357,7 @@ def parameter( @register.simple_tag() -def part_parametr(instance, parameter_name): +def part_parameter(instance, parameter_name): """Included for backwards compatibility - use 'parameter' tag instead. Ref: https://github.com/inventree/InvenTree/pull/10699 diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py index 6fdaf5cbe4..337b46e6b2 100644 --- a/src/backend/InvenTree/report/test_tags.py +++ b/src/backend/InvenTree/report/test_tags.py @@ -419,8 +419,14 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): # Note, use the 'parameter' and 'part_parameter' tags interchangeably here self.assertEqual(report_tags.part_parameter(part, 'name'), None) self.assertEqual(report_tags.parameter(part, 'Template 1'), parameter) - # Test with an invalid part - self.assertEqual(report_tags.parameter(None, 'name'), None) + + # Test with a null part + with self.assertRaises(ValueError): + report_tags.parameter(None, 'name') + + # Test with an invalid model type + with self.assertRaises(TypeError): + report_tags.parameter(parameter, 'name') def test_render_currency(self): """Test the render_currency template tag.""" From e9042d368425ca6e936eddc581870c483d8ea4b2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 06:18:16 +0000 Subject: [PATCH 7/9] Enhancements --- src/backend/InvenTree/InvenTree/models.py | 14 ++++++++++++-- src/backend/InvenTree/part/test_param.py | 20 +++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index 5301141881..c8e8eb4ecf 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -451,7 +451,17 @@ class ReferenceIndexingMixin(models.Model): reference_int = models.BigIntegerField(default=0) -class InvenTreeModel(PluginValidationMixin, models.Model): +class ContentTypeMixin: + """Mixin class which supports retrieval of the ContentType for a model instance.""" + + def get_content_type(self): + """Return the ContentType object associated with this model.""" + from django.contrib.contenttypes.models import ContentType + + return ContentType.objects.get_for_model(self.__class__) + + +class InvenTreeModel(ContentTypeMixin, PluginValidationMixin, models.Model): """Base class for InvenTree models, which provides some common functionality. Includes the following mixins by default: @@ -658,7 +668,7 @@ class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin): Attachment.objects.create(**kwargs) -class InvenTreeTree(MPTTModel): +class InvenTreeTree(ContentTypeMixin, MPTTModel): """Provides an abstracted self-referencing tree model, based on the MPTTModel class. Our implementation provides the following key improvements: diff --git a/src/backend/InvenTree/part/test_param.py b/src/backend/InvenTree/part/test_param.py index 4e3bae3a70..c1a536bc25 100644 --- a/src/backend/InvenTree/part/test_param.py +++ b/src/backend/InvenTree/part/test_param.py @@ -369,13 +369,21 @@ class PartParameterTest(InvenTreeAPITestCase): # test that having non unique part/template combinations fails res = self.post(url, data, expected_code=400) + self.assertEqual(len(res.data), 3) self.assertEqual(len(res.data[1]), 0) for err in [res.data[0], res.data[2]]: - self.assertEqual(len(err), 2) + self.assertEqual(len(err), 3) self.assertEqual(str(err['model_id'][0]), 'This field must be unique.') + self.assertEqual(str(err['model_type'][0]), 'This field must be unique.') self.assertEqual(str(err['template'][0]), 'This field must be unique.') - self.assertEqual(Parameter.objects.filter(content_object=part4).count(), 0) + + self.assertEqual( + Parameter.objects.filter( + model_type=part4.get_content_type(), model_id=part4.pk + ).count(), + 0, + ) # Now, create a valid set of parameters data = [ @@ -384,7 +392,13 @@ class PartParameterTest(InvenTreeAPITestCase): ] res = self.post(url, data, expected_code=201) self.assertEqual(len(res.data), 2) - self.assertEqual(Parameter.objects.filter(content_object=part4).count(), 2) + + self.assertEqual( + Parameter.objects.filter( + model_type=part4.get_content_type(), model_id=part4.pk + ).count(), + 2, + ) def test_param_detail(self): """Tests for the Parameter detail endpoint.""" From 8823d3a105dfede888c7ce9d1ebe466c9e1fff7d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 09:10:28 +0000 Subject: [PATCH 8/9] Unit test fixes --- src/backend/InvenTree/order/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 93610e80ed..4a08a2b66c 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -204,8 +204,8 @@ class PurchaseOrderTest(OrderTest): self.LIST_URL, data={'limit': limit}, expected_code=200 ) - # Total database queries must be below 20, independent of the number of results - self.assertLess(len(ctx), 20) + # Total database queries must be below 25, independent of the number of results + self.assertLess(len(ctx), 25) for result in response.data['results']: self.assertIn('total_price', result) @@ -1428,8 +1428,8 @@ class SalesOrderTest(OrderTest): self.LIST_URL, data={'limit': limit}, expected_code=200 ) - # Total database queries must be less than 20 - self.assertLess(len(ctx), 20) + # Total database queries must be less than 25 + self.assertLess(len(ctx), 25) n = len(response.data['results']) From 812979f1799791d94104e7bb444851e412183fa8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 26 Nov 2025 09:42:59 +0000 Subject: [PATCH 9/9] Add some migration tests --- src/backend/InvenTree/part/test_migrations.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/backend/InvenTree/part/test_migrations.py b/src/backend/InvenTree/part/test_migrations.py index 1a15e57ebf..65844b2fd4 100644 --- a/src/backend/InvenTree/part/test_migrations.py +++ b/src/backend/InvenTree/part/test_migrations.py @@ -273,3 +273,81 @@ class TestPartTestParameterMigration(MigratorTestCase): for key, value in self.test_keys.items(): template = PartTestTemplate.objects.get(test_name=value) self.assertEqual(template.key, key) + + +class TestPartParameterDeletion(MigratorTestCase): + """Test for PartParameter deletion migration. + + Ref: https://github.com/inventree/InvenTree/pull/10699 + + In the linked PR: + + 1. The Parameter and ParameterTemplate models are added + 2. Data is migrated from PartParameter to Parameter and PartParameterTemplate to ParameterTemplate + 3. The PartParameter and PartParameterTemplate models are deleted + """ + + UNITS = ['mm', 'Ampere', 'kg'] + + migrate_from = ('part', '0143_alter_part_image') + migrate_to = ('part', '0145_remove_partparametertemplate_selectionlist_and_more') + + def prepare(self): + """Prepare some parts and parameters.""" + Part = self.old_state.apps.get_model('part', 'part') + PartParameter = self.old_state.apps.get_model('part', 'partparameter') + PartParameterTemplate = self.old_state.apps.get_model( + 'part', 'partparametertemplate' + ) + + # Create some parts + for i in range(3): + Part.objects.create( + name=f'Part {i + 1}', + description=f'My part {i + 1}', + level=0, + lft=0, + rght=0, + tree_id=0, + ) + + # Create some parameter templates + for idx, units in enumerate(self.UNITS): + PartParameterTemplate.objects.create( + name=f'Template {idx + 1}', + description=f'Description for template {idx + 1}', + units=units, + ) + + # Create some parameters + for ii, part in enumerate(Part.objects.all()): + for jj, template in enumerate(PartParameterTemplate.objects.all()): + PartParameter.objects.create( + part=part, template=template, data=str(ii * jj) + ) + + self.assertEqual(Part.objects.count(), 3) + self.assertEqual(PartParameterTemplate.objects.count(), 3) + self.assertEqual(PartParameter.objects.count(), 9) + + def test_parameter_deletion(self): + """Test that PartParameter objects have been deleted.""" + # Test that the PartParameter objects have been deleted + with self.assertRaises(ModuleNotFoundError): + self.new_state.apps.get_model('part', 'partparameter') + + # Load the new PartParameter model + ParameterTemplate = self.new_state.apps.get_model('common', 'parametertemplate') + Parameter = self.new_state.apps.get_model('common', 'parameter') + Part = self.new_state.apps.get_model('part', 'part') + + self.assertEqual(ParameterTemplate.objects.count(), 3) + self.assertEqual(Parameter.objects.count(), 9) + self.assertEqual(Part.objects.count(), 3) + + for p in Part.objects.all(): + params = p.parameters_list.all() + self.assertEqual(len(params), 3) + + for unit in self.UNITS: + self.assertTrue(params.filter(units=unit).exists())