2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-16 17:28:11 +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

@@ -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."""