From 72607110befbe27f3370f6ae362767528b42b686 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 25 Nov 2025 07:23:09 +0000 Subject: [PATCH] Remove old models --- src/backend/InvenTree/InvenTree/models.py | 67 ++- .../migrations/0041_auto_20251028_1112.py | 31 +- .../0077_delete_manufacturerpartparameter.py | 17 + src/backend/InvenTree/company/models.py | 49 -- src/backend/InvenTree/part/filters.py | 57 --- .../0071_alter_partparametertemplate_name.py | 2 +- ..._partcategoryparametertemplate_template.py | 30 ++ ...arametertemplate_selectionlist_and_more.py | 45 ++ src/backend/InvenTree/part/models.py | 431 +----------------- .../InvenTree/plugin/base/supplier/helpers.py | 9 +- .../exporter/part_parameter_exporter.py | 8 +- .../InvenTree/report/templatetags/report.py | 20 +- src/backend/InvenTree/users/ruleset.py | 3 - 13 files changed, 223 insertions(+), 546 deletions(-) create mode 100644 src/backend/InvenTree/company/migrations/0077_delete_manufacturerpartparameter.py create mode 100644 src/backend/InvenTree/part/migrations/0144_partcategoryparametertemplate_template.py create mode 100644 src/backend/InvenTree/part/migrations/0145_remove_partparametertemplate_selectionlist_and_more.py diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index 2a0929128d..5301141881 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -7,8 +7,9 @@ from typing import Any, Optional from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.db import models +from django.db import models, transaction from django.db.models import QuerySet from django.db.models.signals import post_save from django.dispatch import receiver @@ -550,6 +551,70 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model): self.parameters_list.all().delete() super().delete(*args, **kwargs) + @transaction.atomic + def copy_parameters_from(self, other, clear=True, **kwargs): + """Copy all parameters from another model instance. + + Arguments: + other: The other model instance to copy parameters from + clear: If True, clear existing parameters before copying + **kwargs: Additional keyword arguments to pass to the Parameter constructor + """ + import common.models + + if clear: + self.parameters_list.all().delete() + + parameters = [] + + content_type = ContentType.objects.get_for_model(self.__class__) + + template_ids = [parameter.template.pk for parameter in other.parameters.all()] + + # Remove all conflicting parameters first + self.parameters_list.filter(template__pk__in=template_ids).delete() + + for parameter in other.parameters.all(): + parameter.pk = None + parameter.model_id = self.pk + parameter.model_type = content_type + + parameters.append(parameter) + + if len(parameters) > 0: + common.models.Parameter.objects.bulk_create(parameters) + + def get_parameter(self, name: str): + """Return a Parameter instance for the given parameter name. + + Args: + name: Name of the parameter template + + Returns: + Parameter instance if found, else None + """ + return self.parameters_list.filter(template__name=name).first() + + def get_parameters(self): + """Return all Parameter instances for this model.""" + return self.parameters_list.all().order_by('template__name') + + def parameters_map(self): + """Return a map (dict) of parameter values associated with this Part instance, of the form. + + Example: + { + "name_1": "value_1", + "name_2": "value_2", + } + """ + params = {} + + for parameter in self.parameters.all().prefetch_related('template'): + params[parameter.template.name] = parameter.data + + return params + class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin): """Provides an abstracted class for managing file attachments. diff --git a/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py b/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py index ac497afe76..584deee84b 100644 --- a/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py +++ b/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py @@ -167,11 +167,37 @@ def copy_manufacturer_part_parameters(apps, schema_editor): assert Parameter.objects.filter(model_type=content_type).count() == len(parameters) +def update_category_parameters(apps, schema_editor): + """Migration for PartCategoryParameterTemplate. + + Copies the contents of the 'parameter_template' field to the new 'template' field + """ + + PartCategoryParameterTemplate = apps.get_model("part", "partcategoryparametertemplate") + ParameterTemplate = apps.get_model("common", "parametertemplate") + + to_update = [] + + for item in PartCategoryParameterTemplate.objects.all(): + # Find a matching template + item.template = ParameterTemplate.objects.get( + name=item.parameter_template.name + ) + + to_update.append(item) + + + if len(to_update) > 0: + PartCategoryParameterTemplate.objects.bulk_update(to_update, ['template']) + print(f"Updated {len(to_update)} PartCategoryParameterTemplate instances.") + + class Migration(migrations.Migration): dependencies = [ ("part", "0132_partparametertemplate_selectionlist"), ("common", "0040_parametertemplate_parameter"), + ("part", "0144_partcategoryparametertemplate_template") ] operations = [ @@ -190,6 +216,9 @@ class Migration(migrations.Migration): migrations.RunPython( copy_manufacturer_part_parameters, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + update_category_parameters, + reverse_code=migrations.RunPython.noop ) - # TODO: Data migration for existing CategoryParameter objects ] diff --git a/src/backend/InvenTree/company/migrations/0077_delete_manufacturerpartparameter.py b/src/backend/InvenTree/company/migrations/0077_delete_manufacturerpartparameter.py new file mode 100644 index 0000000000..4af9571f6d --- /dev/null +++ b/src/backend/InvenTree/company/migrations/0077_delete_manufacturerpartparameter.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.8 on 2025-11-25 07:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("company", "0076_alter_company_image"), + ("common", "0041_auto_20251028_1112"), + ] + + operations = [ + migrations.DeleteModel( + name="ManufacturerPartParameter", + ), + ] diff --git a/src/backend/InvenTree/company/models.py b/src/backend/InvenTree/company/models.py index 3b1a94470b..fbe8ccdd01 100644 --- a/src/backend/InvenTree/company/models.py +++ b/src/backend/InvenTree/company/models.py @@ -585,55 +585,6 @@ class ManufacturerPart( return s -class ManufacturerPartParameter(InvenTree.models.InvenTreeModel): - """A ManufacturerPartParameter represents a key:value parameter for a ManufacturerPart. - - This is used to represent parameters / properties for a particular manufacturer part. - - Each parameter is a simple string (text) value. - """ - - class Meta: - """Metaclass defines extra model options.""" - - verbose_name = _('Manufacturer Part Parameter') - unique_together = ('manufacturer_part', 'name') - - @staticmethod - def get_api_url(): - """Return the API URL associated with the ManufacturerPartParameter model.""" - return reverse('api-manufacturer-part-parameter-list') - - manufacturer_part = models.ForeignKey( - ManufacturerPart, - on_delete=models.CASCADE, - related_name='parameters', - verbose_name=_('Manufacturer Part'), - ) - - name = models.CharField( - max_length=500, - blank=False, - verbose_name=_('Name'), - help_text=_('Parameter name'), - ) - - value = models.CharField( - max_length=500, - blank=False, - verbose_name=_('Value'), - help_text=_('Parameter value'), - ) - - units = models.CharField( - max_length=64, - blank=True, - null=True, - verbose_name=_('Units'), - help_text=_('Parameter units'), - ) - - class SupplierPartManager(models.Manager): """Define custom SupplierPart objects manager. diff --git a/src/backend/InvenTree/part/filters.py b/src/backend/InvenTree/part/filters.py index 294618d892..ad4107566e 100644 --- a/src/backend/InvenTree/part/filters.py +++ b/src/backend/InvenTree/part/filters.py @@ -18,7 +18,6 @@ from django.db import models from django.db.models import ( Case, DecimalField, - Exists, ExpressionWrapper, F, FloatField, @@ -517,59 +516,3 @@ def annotate_bom_item_can_build(queryset: QuerySet, reference: str = '') -> Quer ) return queryset - - -def order_by_parameter( - queryset: QuerySet, template_id: int, ascending: bool = True -) -> QuerySet: - """Order the given queryset by a given template parameter. - - Parts which do not have a value for the given parameter are ordered last. - - Arguments: - queryset: A queryset of Part objects - template_id (int): The ID of the template parameter to order by - ascending (bool): Order by ascending or descending (default = True) - - Returns: - A queryset of Part objects ordered by the given parameter - """ - template_filter = part.models.PartParameter.objects.filter( - template__id=template_id, part_id=OuterRef('id') - ) - - # Annotate the queryset with the parameter value, and whether it exists - queryset = queryset.annotate(parameter_exists=Exists(template_filter)) - - # Annotate the text data value - queryset = queryset.annotate( - parameter_value=Case( - When( - parameter_exists=True, - then=Subquery( - template_filter.values('data')[:1], output_field=models.CharField() - ), - ), - default=Value('', output_field=models.CharField()), - ), - parameter_value_numeric=Case( - When( - parameter_exists=True, - then=Subquery( - template_filter.values('data_numeric')[:1], - output_field=models.FloatField(), - ), - ), - default=Value(0, output_field=models.FloatField()), - ), - ) - - prefix = '' if ascending else '-' - - # Return filtered queryset - - return queryset.order_by( - '-parameter_exists', - f'{prefix}parameter_value_numeric', - f'{prefix}parameter_value', - ) diff --git a/src/backend/InvenTree/part/migrations/0071_alter_partparametertemplate_name.py b/src/backend/InvenTree/part/migrations/0071_alter_partparametertemplate_name.py index fef49e73f6..4fad4a9bf1 100644 --- a/src/backend/InvenTree/part/migrations/0071_alter_partparametertemplate_name.py +++ b/src/backend/InvenTree/part/migrations/0071_alter_partparametertemplate_name.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='partparametertemplate', name='name', - field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, validators=[part.models.validate_template_name], verbose_name='Name'), + field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, validators=[], verbose_name='Name'), ), ] diff --git a/src/backend/InvenTree/part/migrations/0144_partcategoryparametertemplate_template.py b/src/backend/InvenTree/part/migrations/0144_partcategoryparametertemplate_template.py new file mode 100644 index 0000000000..ad889bbda9 --- /dev/null +++ b/src/backend/InvenTree/part/migrations/0144_partcategoryparametertemplate_template.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.8 on 2025-11-25 06:19 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("common", "0040_parametertemplate_parameter"), + ("part", "0143_alter_part_image"), + ] + + operations = [ + migrations.AddField( + model_name="partcategoryparametertemplate", + name="template", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="part_categories", + to="common.parametertemplate", + ), + ), + migrations.RemoveConstraint( + model_name="partcategoryparametertemplate", + name="unique_category_parameter_template_pair", + ), + ] diff --git a/src/backend/InvenTree/part/migrations/0145_remove_partparametertemplate_selectionlist_and_more.py b/src/backend/InvenTree/part/migrations/0145_remove_partparametertemplate_selectionlist_and_more.py new file mode 100644 index 0000000000..ca946691d4 --- /dev/null +++ b/src/backend/InvenTree/part/migrations/0145_remove_partparametertemplate_selectionlist_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2.8 on 2025-11-25 07:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("common", "0041_auto_20251028_1112"), + ("part", "0144_partcategoryparametertemplate_template"), + ] + + operations = [ + migrations.RemoveField( + model_name="partparametertemplate", + name="selectionlist", + ), + migrations.RemoveField( + model_name="partcategoryparametertemplate", + name="parameter_template", + ), + migrations.AlterField( + model_name="partcategoryparametertemplate", + name="template", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="part_categories", + to="common.parametertemplate", + ), + ), + migrations.AddConstraint( + model_name="partcategoryparametertemplate", + constraint=models.UniqueConstraint( + fields=("category", "template"), + name="unique_category_parameter_pair", + ), + ), + migrations.DeleteModel( + name="PartParameter", + ), + migrations.DeleteModel( + name="PartParameterTemplate", + ), + ] diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 8b33ffd438..aad14ac62d 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -15,11 +15,7 @@ from typing import cast from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError -from django.core.validators import ( - MaxValueValidator, - MinLengthValidator, - MinValueValidator, -) +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.db.models import F, Q, QuerySet, Sum, UniqueConstraint from django.db.models.functions import Coalesce @@ -59,7 +55,7 @@ from company.models import SupplierPart from InvenTree import helpers, validators from InvenTree.exceptions import log_error from InvenTree.fields import InvenTreeURLField -from InvenTree.helpers import decimal2money, decimal2string, normalize, str2bool +from InvenTree.helpers import decimal2money, decimal2string, normalize from order import models as OrderModels from order.status_codes import ( PurchaseOrderStatus, @@ -287,7 +283,7 @@ class PartCategory( def get_parameter_templates(self): """Return parameter templates associated to category.""" prefetch = PartCategoryParameterTemplate.objects.prefetch_related( - 'category', 'parameter_template' + 'category', 'parameter' ) return prefetch.filter(category=self.id) @@ -1028,7 +1024,7 @@ class Part( self.ensure_trackable() def ensure_trackable(self): - """Ensure that trackable is set correctly downline.""" + """Ensure that trackable is set correctly downstream.""" if self.trackable: for part in self.get_used_in(): if not part.trackable: @@ -2388,36 +2384,6 @@ class Part( sub.bom_item = bom_item sub.save() - @transaction.atomic - def copy_parameters_from(self, other: Part, **kwargs) -> None: - """Copy all parameter values from another Part instance.""" - clear = kwargs.get('clear', True) - - if clear: - self.get_parameters().delete() - - parameters = [] - - for parameter in other.get_parameters(): - # If this part already has a parameter pointing to the same template, - # delete that parameter from this part first! - - try: - existing = PartParameter.objects.get( - part=self, template=parameter.template - ) - existing.delete() - except PartParameter.DoesNotExist: - pass - - parameter.part = self - parameter.pk = None - - parameters.append(parameter) - - if len(parameters) > 0: - PartParameter.objects.bulk_create(parameters) - @transaction.atomic def copy_tests_from(self, other: Part, **kwargs) -> None: """Copy all test templates from another Part instance. @@ -2571,36 +2537,6 @@ class Part( return quantity - def get_parameter(self, name): - """Return the parameter with the given name. - - If no matching parameter is found, return None. - """ - try: - return self.parameters.get(template__name=name) - except PartParameter.DoesNotExist: - return None - - def get_parameters(self): - """Return all parameters for this part, ordered by name.""" - return self.parameters.order_by('template__name') - - def parameters_map(self): - """Return a map (dict) of parameter values associated with this Part instance, of the form. - - Example: - { - "name_1": "value_1", - "name_2": "value_2", - } - """ - params = {} - - for parameter in self.parameters.all(): - params[parameter.template.name] = parameter.data - - return params - @property def has_variants(self): """Check if this Part object has variants underneath it.""" @@ -3768,357 +3704,15 @@ class PartTestTemplate(InvenTree.models.InvenTreeMetadataModel): return [x.strip() for x in self.choices.split(',') if x.strip()] -def validate_template_name(name): - """Placeholder for legacy function used in migrations.""" - - -class PartParameterTemplate(InvenTree.models.InvenTreeMetadataModel): - """A PartParameterTemplate provides a template for key:value pairs for extra parameters fields/values to be added to a Part. - - This allows users to arbitrarily assign data fields to a Part beyond the built-in attributes. - - Attributes: - name: The name (key) of the Parameter [string] - units: The units of the Parameter [string] - description: Description of the parameter [string] - checkbox: Boolean flag to indicate whether the parameter is a checkbox [bool] - choices: List of valid choices for the parameter [string] - selectionlist: SelectionList that should be used for choices [selectionlist] - """ - - class Meta: - """Metaclass options for the PartParameterTemplate model.""" - - verbose_name = _('Part Parameter Template') - - @staticmethod - def get_api_url(): - """Return the list API endpoint URL associated with the PartParameterTemplate model.""" - return reverse('api-part-parameter-template-list') - - def __str__(self): - """Return a string representation of a PartParameterTemplate instance.""" - s = str(self.name) - if self.units: - s += f' ({self.units})' - return s - - def clean(self): - """Custom cleaning step for this model. - - Checks: - - A 'checkbox' field cannot have 'choices' set - - A 'checkbox' field cannot have 'units' set - """ - super().clean() - - # Check that checkbox parameters do not have units or choices - if self.checkbox: - if self.units: - raise ValidationError({ - 'units': _('Checkbox parameters cannot have units') - }) - - if self.choices: - raise ValidationError({ - 'choices': _('Checkbox parameters cannot have choices') - }) - - # Check that 'choices' are in fact valid - if self.choices is None: - self.choices = '' - else: - self.choices = str(self.choices).strip() - - if self.choices: - choice_set = set() - - for choice in self.choices.split(','): - choice = choice.strip() - - # Ignore empty choices - if not choice: - continue - - if choice in choice_set: - raise ValidationError({'choices': _('Choices must be unique')}) - - choice_set.add(choice) - - def validate_unique(self, exclude=None): - """Ensure that PartParameterTemplates cannot be created with the same name. - - This test should be case-insensitive (which the unique caveat does not cover). - """ - super().validate_unique(exclude) - - try: - others = PartParameterTemplate.objects.filter( - name__iexact=self.name - ).exclude(pk=self.pk) - - if others.exists(): - msg = _('Parameter template name must be unique') - raise ValidationError({'name': msg}) - except PartParameterTemplate.DoesNotExist: - pass - - def get_choices(self): - """Return a list of choices for this parameter template.""" - if self.selectionlist: - return self.selectionlist.get_choices() - - if not self.choices: - return [] - - return [x.strip() for x in self.choices.split(',') if x.strip()] - - name = models.CharField( - max_length=100, - verbose_name=_('Name'), - help_text=_('Parameter Name'), - unique=True, - ) - - units = models.CharField( - max_length=25, - verbose_name=_('Units'), - help_text=_('Physical units for this parameter'), - blank=True, - validators=[validators.validate_physical_units], - ) - - description = models.CharField( - max_length=250, - verbose_name=_('Description'), - help_text=_('Parameter description'), - blank=True, - ) - - checkbox = models.BooleanField( - default=False, - verbose_name=_('Checkbox'), - help_text=_('Is this parameter a checkbox?'), - ) - - choices = models.CharField( - max_length=5000, - verbose_name=_('Choices'), - help_text=_('Valid choices for this parameter (comma-separated)'), - blank=True, - ) - - selectionlist = models.ForeignKey( - common.models.SelectionList, - blank=True, - null=True, - on_delete=models.SET_NULL, - related_name='parameter_templates', - verbose_name=_('Selection List'), - help_text=_('Selection list for this parameter'), - ) - - -@receiver( - post_save, - sender=PartParameterTemplate, - dispatch_uid='post_save_part_parameter_template', -) -def post_save_part_parameter_template(sender, instance, created, **kwargs): - """Callback function when a PartParameterTemplate is created or saved.""" - import part.tasks as part_tasks - - if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): - if not created: - # Schedule a background task to rebuild the parameters against this template - InvenTree.tasks.offload_task( - part_tasks.rebuild_parameters, - instance.pk, - force_async=True, - group='part', - ) - - -class PartParameter( - common.models.UpdatedUserMixin, InvenTree.models.InvenTreeMetadataModel -): - """A PartParameter is a specific instance of a PartParameterTemplate. It assigns a particular parameter pair to a part. - - Attributes: - part: Reference to a single Part object - template: Reference to a single PartParameterTemplate object - data: The data (value) of the Parameter [string] - data_numeric: Numeric value of the parameter (if applicable) [float] - note: Optional note field for the parameter [string] - updated: Timestamp of when the parameter was last updated [datetime] - updated_by: Reference to the User who last updated the parameter [User] - """ - - class Meta: - """Metaclass providing extra model definition.""" - - verbose_name = _('Part Parameter') - # Prevent multiple instances of a parameter for a single part - unique_together = ('part', 'template') - - @staticmethod - def get_api_url(): - """Return the list API endpoint URL associated with the PartParameter model.""" - return reverse('api-part-parameter-list') - - def __str__(self): - """String representation of a PartParameter (used in the admin interface).""" - return f'{self.part.full_name} : {self.template.name} = {self.data} ({self.template.units})' - - def delete(self): - """Custom delete handler for the PartParameter model. - - - Check if the parameter can be deleted - """ - self.check_part_lock() - super().delete() - - def check_part_lock(self): - """Check if the referenced part is locked.""" - # TODO: Potentially control this behaviour via a global setting - - if self.part.locked: - raise ValidationError(_('Parameter cannot be modified - part is locked')) - - def save(self, *args, **kwargs): - """Custom save method for the PartParameter model.""" - # Validate the PartParameter before saving - self.calculate_numeric_value() - - # Check if the part is locked - self.check_part_lock() - - # Convert 'boolean' values to 'True' / 'False' - if self.template.checkbox: - self.data = str2bool(self.data) - self.data_numeric = 1 if self.data else 0 - - super().save(*args, **kwargs) - - def clean(self): - """Validate the PartParameter before saving to the database.""" - super().clean() - - # Validate the parameter data against the template choices - if choices := self.template.get_choices(): - if self.data not in choices: - raise ValidationError({'data': _('Invalid choice for parameter value')}) - - self.calculate_numeric_value() - - # Run custom validation checks (via plugins) - from plugin import PluginMixinEnum, registry - - for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): - # Note: The validate_part_parameter function may raise a ValidationError - try: - result = plugin.validate_part_parameter(self, self.data) - if result: - break - except ValidationError as exc: - # Re-throw the ValidationError against the 'data' field - raise ValidationError({'data': exc.message}) - except Exception: - log_error('validate_part_parameter', plugin=plugin.slug) - - def calculate_numeric_value(self): - """Calculate a numeric value for the parameter data. - - - If a 'units' field is provided, then the data will be converted to the base SI unit. - - Otherwise, we'll try to do a simple float cast - """ - if self.template.units: - try: - self.data_numeric = InvenTree.conversion.convert_physical_value( - self.data, self.template.units - ) - except (ValidationError, ValueError): - self.data_numeric = None - - # No units provided, so try to cast to a float - else: - try: - self.data_numeric = float(self.data) - except ValueError: - self.data_numeric = None - - if self.data_numeric is not None and type(self.data_numeric) is float: - # Prevent out of range numbers, etc - # Ref: https://github.com/inventree/InvenTree/issues/7593 - if math.isnan(self.data_numeric) or math.isinf(self.data_numeric): - self.data_numeric = None - - part = models.ForeignKey( - Part, - on_delete=models.CASCADE, - related_name='parameters', - verbose_name=_('Part'), - help_text=_('Parent Part'), - ) - - template = models.ForeignKey( - PartParameterTemplate, - on_delete=models.CASCADE, - related_name='instances', - verbose_name=_('Template'), - help_text=_('Parameter Template'), - ) - - data = models.CharField( - max_length=500, - verbose_name=_('Data'), - help_text=_('Parameter Value'), - validators=[MinLengthValidator(1)], - ) - - data_numeric = models.FloatField(default=None, null=True, blank=True) - - note = models.CharField( - max_length=500, - blank=True, - verbose_name=_('Note'), - help_text=_('Optional note field'), - ) - - @property - def units(self): - """Return the units associated with the template.""" - return self.template.units - - @property - def name(self): - """Return the name of the template.""" - return self.template.name - - @property - def description(self): - """Return the description of the template.""" - return self.template.description - - @classmethod - def create(cls, part, template, data, save=False): - """Custom save method for the PartParameter class.""" - part_parameter = cls(part=part, template=template, data=data) - if save: - part_parameter.save() - return part_parameter - - class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): - """A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a PartParameterTemplate. + """A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a ParameterTemplate. - Multiple PartParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation. + Multiple ParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation. Attributes: category: Reference to a single PartCategory object - parameter_template: Reference to a single PartParameterTemplate object - default_value: The default value for the parameter in the context of the selected - category + template: Reference to a single ParameterTemplate object + default_value: The default value for the parameter in the context of the selected category """ @staticmethod @@ -4133,8 +3727,7 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): constraints = [ UniqueConstraint( - fields=['category', 'parameter_template'], - name='unique_category_parameter_template_pair', + fields=['category', 'template'], name='unique_category_parameter_pair' ) ] @@ -4177,12 +3770,10 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): help_text=_('Part Category'), ) - parameter_template = models.ForeignKey( - PartParameterTemplate, + template = models.ForeignKey( + common.models.ParameterTemplate, on_delete=models.CASCADE, related_name='part_categories', - verbose_name=_('Parameter Template'), - help_text=_('Parameter Template'), ) default_value = models.CharField( diff --git a/src/backend/InvenTree/plugin/base/supplier/helpers.py b/src/backend/InvenTree/plugin/base/supplier/helpers.py index da4828fd91..de11f5bf1e 100644 --- a/src/backend/InvenTree/plugin/base/supplier/helpers.py +++ b/src/backend/InvenTree/plugin/base/supplier/helpers.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import Optional +import common.models import part.models as part_models @@ -61,22 +62,22 @@ class ImportParameter: name (str): The name of the parameter. value (str): The value of the parameter. on_category (Optional[bool]): Indicates if the parameter is associated with a category. This will be automatically set by InvenTree - parameter_template (Optional[PartParameterTemplate]): The associated parameter template, if any. + parameter_template (Optional[ParameterTemplate]): The associated parameter template, if any. """ name: str value: str on_category: Optional[bool] = False - parameter_template: Optional[part_models.PartParameterTemplate] = None + parameter_template: Optional[common.models.ParameterTemplate] = None def __post_init__(self): """Post-initialization to fetch the parameter template if not provided.""" if not self.parameter_template: try: - self.parameter_template = part_models.PartParameterTemplate.objects.get( + self.parameter_template = common.models.ParameterTemplate.objects.get( name__iexact=self.name ) - except part_models.PartParameterTemplate.DoesNotExist: + except common.models.ParameterTemplate.DoesNotExist: pass diff --git a/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py b/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py index 8ecab0e447..7872c2e59d 100644 --- a/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py +++ b/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py @@ -23,7 +23,7 @@ class PartParameterExportOptionsSerializer(serializers.Serializer): class PartParameterExporter(DataExportMixin, InvenTreePlugin): - """Builtin plugin for exporting PartParameter data. + """Builtin plugin for exporting Part Parameter data. Extends the "part" export process, to include all associated PartParameter data. """ @@ -93,7 +93,9 @@ class PartParameterExporter(DataExportMixin, InvenTreePlugin): def prefetch_queryset(self, queryset): """Ensure that the part parameters are prefetched.""" - queryset = queryset.prefetch_related('parameters', 'parameters__template') + queryset = queryset.prefetch_related( + 'parameters_list', 'parameters_list__template' + ) return queryset @@ -118,7 +120,7 @@ class PartParameterExporter(DataExportMixin, InvenTreePlugin): for part in parts: # Extract the part parameters from the serialized data - for parameter in part.get('parameters', []): + for parameter in part.get('parameters_list', []): if template := parameter.get('template_detail', None): template_id = template['pk'] diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 1455028fdf..13a5db4650 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -11,6 +11,7 @@ from django import template from django.apps.registry import apps from django.conf import settings from django.core.exceptions import ValidationError +from django.db.models import Model from django.db.models.query import QuerySet from django.utils.safestring import SafeString, mark_safe from django.utils.translation import gettext_lazy as _ @@ -22,6 +23,7 @@ from PIL import Image import common.currency import common.icons +import common.models import InvenTree.helpers import InvenTree.helpers_model import report.helpers @@ -329,19 +331,23 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa @register.simple_tag() -def part_parameter(part: Part, parameter_name: str) -> Optional[str]: - """Return a PartParameter object for the given part and parameter name. +def parameter( + instance: Model, parameter_name: str +) -> Optional[common.models.Parameter]: + """Return a Parameter object for the given part and parameter name. Arguments: - part: A Part object + instance: A Model object parameter_name: The name of the parameter to retrieve Returns: - A PartParameter object, or None if not found + A Parameter object, or None if not found """ - if type(part) is Part: - return part.get_parameter(parameter_name) - return None + return ( + instance.parameters.prefetch_related('template') + .filter(template__name=parameter_name) + .first() + ) @register.simple_tag() diff --git a/src/backend/InvenTree/users/ruleset.py b/src/backend/InvenTree/users/ruleset.py index 0fe705bfd5..bfcc303cc5 100644 --- a/src/backend/InvenTree/users/ruleset.py +++ b/src/backend/InvenTree/users/ruleset.py @@ -106,15 +106,12 @@ def get_ruleset_models() -> dict: 'part_partsellpricebreak', 'part_partinternalpricebreak', 'part_parttesttemplate', - 'part_partparametertemplate', - 'part_partparameter', 'part_partrelated', 'part_partstar', 'part_partstocktake', 'part_partcategorystar', 'company_supplierpart', 'company_manufacturerpart', - 'company_manufacturerpartparameter', ], RuleSetEnum.STOCK_LOCATION: ['stock_stocklocation', 'stock_stocklocationtype'], RuleSetEnum.STOCK: [