2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-15 16:58:14 +00:00

Merge branch 'generic-parameters' of https://github.com/schrodingersgat/inventree into pr/SchrodingersGat/10699

This commit is contained in:
Matthias Mair
2025-11-26 13:03:06 +01:00
12 changed files with 170 additions and 38 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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'])

View File

@@ -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
@@ -3761,7 +3761,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
):

View File

@@ -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(

View File

@@ -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)

View File

@@ -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())

View File

@@ -249,12 +249,8 @@ 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(
'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 +258,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)
@@ -375,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 = [
@@ -390,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."""

View File

@@ -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 (
@@ -353,6 +356,15 @@ def parameter(
)
@register.simple_tag()
def part_parameter(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

View File

@@ -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
@@ -12,7 +13,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 +407,26 @@ 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')
# 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)
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)
self.assertEqual(report_tags.parameter(part, 'Template 1'), parameter)
# 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."""