2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-16 17:28:11 +00:00

Remove old models

This commit is contained in:
Oliver Walters
2025-11-25 07:23:09 +00:00
parent 211c98ed30
commit 72607110be
13 changed files with 223 additions and 546 deletions

View File

@@ -7,8 +7,9 @@ from typing import Any, Optional
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError 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 import QuerySet
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
@@ -550,6 +551,70 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
self.parameters_list.all().delete() self.parameters_list.all().delete()
super().delete(*args, **kwargs) 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): class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin):
"""Provides an abstracted class for managing file attachments. """Provides an abstracted class for managing file attachments.

View File

@@ -167,11 +167,37 @@ def copy_manufacturer_part_parameters(apps, schema_editor):
assert Parameter.objects.filter(model_type=content_type).count() == len(parameters) 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("part", "0132_partparametertemplate_selectionlist"), ("part", "0132_partparametertemplate_selectionlist"),
("common", "0040_parametertemplate_parameter"), ("common", "0040_parametertemplate_parameter"),
("part", "0144_partcategoryparametertemplate_template")
] ]
operations = [ operations = [
@@ -190,6 +216,9 @@ class Migration(migrations.Migration):
migrations.RunPython( migrations.RunPython(
copy_manufacturer_part_parameters, copy_manufacturer_part_parameters,
reverse_code=migrations.RunPython.noop reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
update_category_parameters,
reverse_code=migrations.RunPython.noop
) )
# TODO: Data migration for existing CategoryParameter objects
] ]

View File

@@ -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",
),
]

View File

@@ -585,55 +585,6 @@ class ManufacturerPart(
return s 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): class SupplierPartManager(models.Manager):
"""Define custom SupplierPart objects manager. """Define custom SupplierPart objects manager.

View File

@@ -18,7 +18,6 @@ from django.db import models
from django.db.models import ( from django.db.models import (
Case, Case,
DecimalField, DecimalField,
Exists,
ExpressionWrapper, ExpressionWrapper,
F, F,
FloatField, FloatField,
@@ -517,59 +516,3 @@ def annotate_bom_item_can_build(queryset: QuerySet, reference: str = '') -> Quer
) )
return queryset 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',
)

View File

@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='partparametertemplate', model_name='partparametertemplate',
name='name', 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'),
), ),
] ]

View File

@@ -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",
),
]

View File

@@ -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",
),
]

View File

@@ -15,11 +15,7 @@ from typing import cast
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import ( from django.core.validators import MaxValueValidator, MinValueValidator
MaxValueValidator,
MinLengthValidator,
MinValueValidator,
)
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F, Q, QuerySet, Sum, UniqueConstraint from django.db.models import F, Q, QuerySet, Sum, UniqueConstraint
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@@ -59,7 +55,7 @@ from company.models import SupplierPart
from InvenTree import helpers, validators from InvenTree import helpers, validators
from InvenTree.exceptions import log_error from InvenTree.exceptions import log_error
from InvenTree.fields import InvenTreeURLField 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 import models as OrderModels
from order.status_codes import ( from order.status_codes import (
PurchaseOrderStatus, PurchaseOrderStatus,
@@ -287,7 +283,7 @@ class PartCategory(
def get_parameter_templates(self): def get_parameter_templates(self):
"""Return parameter templates associated to category.""" """Return parameter templates associated to category."""
prefetch = PartCategoryParameterTemplate.objects.prefetch_related( prefetch = PartCategoryParameterTemplate.objects.prefetch_related(
'category', 'parameter_template' 'category', 'parameter'
) )
return prefetch.filter(category=self.id) return prefetch.filter(category=self.id)
@@ -1028,7 +1024,7 @@ class Part(
self.ensure_trackable() self.ensure_trackable()
def ensure_trackable(self): def ensure_trackable(self):
"""Ensure that trackable is set correctly downline.""" """Ensure that trackable is set correctly downstream."""
if self.trackable: if self.trackable:
for part in self.get_used_in(): for part in self.get_used_in():
if not part.trackable: if not part.trackable:
@@ -2388,36 +2384,6 @@ class Part(
sub.bom_item = bom_item sub.bom_item = bom_item
sub.save() 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 @transaction.atomic
def copy_tests_from(self, other: Part, **kwargs) -> None: def copy_tests_from(self, other: Part, **kwargs) -> None:
"""Copy all test templates from another Part instance. """Copy all test templates from another Part instance.
@@ -2571,36 +2537,6 @@ class Part(
return quantity 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 @property
def has_variants(self): def has_variants(self):
"""Check if this Part object has variants underneath it.""" """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()] 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 <key:value> 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): 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: Attributes:
category: Reference to a single PartCategory object category: Reference to a single PartCategory object
parameter_template: Reference to a single PartParameterTemplate object template: Reference to a single ParameterTemplate object
default_value: The default value for the parameter in the context of the selected default_value: The default value for the parameter in the context of the selected category
category
""" """
@staticmethod @staticmethod
@@ -4133,8 +3727,7 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
constraints = [ constraints = [
UniqueConstraint( UniqueConstraint(
fields=['category', 'parameter_template'], fields=['category', 'template'], name='unique_category_parameter_pair'
name='unique_category_parameter_template_pair',
) )
] ]
@@ -4177,12 +3770,10 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
help_text=_('Part Category'), help_text=_('Part Category'),
) )
parameter_template = models.ForeignKey( template = models.ForeignKey(
PartParameterTemplate, common.models.ParameterTemplate,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='part_categories', related_name='part_categories',
verbose_name=_('Parameter Template'),
help_text=_('Parameter Template'),
) )
default_value = models.CharField( default_value = models.CharField(

View File

@@ -3,6 +3,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
import common.models
import part.models as part_models import part.models as part_models
@@ -61,22 +62,22 @@ class ImportParameter:
name (str): The name of the parameter. name (str): The name of the parameter.
value (str): The value 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 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 name: str
value: str value: str
on_category: Optional[bool] = False on_category: Optional[bool] = False
parameter_template: Optional[part_models.PartParameterTemplate] = None parameter_template: Optional[common.models.ParameterTemplate] = None
def __post_init__(self): def __post_init__(self):
"""Post-initialization to fetch the parameter template if not provided.""" """Post-initialization to fetch the parameter template if not provided."""
if not self.parameter_template: if not self.parameter_template:
try: try:
self.parameter_template = part_models.PartParameterTemplate.objects.get( self.parameter_template = common.models.ParameterTemplate.objects.get(
name__iexact=self.name name__iexact=self.name
) )
except part_models.PartParameterTemplate.DoesNotExist: except common.models.ParameterTemplate.DoesNotExist:
pass pass

View File

@@ -23,7 +23,7 @@ class PartParameterExportOptionsSerializer(serializers.Serializer):
class PartParameterExporter(DataExportMixin, InvenTreePlugin): 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. Extends the "part" export process, to include all associated PartParameter data.
""" """
@@ -93,7 +93,9 @@ class PartParameterExporter(DataExportMixin, InvenTreePlugin):
def prefetch_queryset(self, queryset): def prefetch_queryset(self, queryset):
"""Ensure that the part parameters are prefetched.""" """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 return queryset
@@ -118,7 +120,7 @@ class PartParameterExporter(DataExportMixin, InvenTreePlugin):
for part in parts: for part in parts:
# Extract the part parameters from the serialized data # 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): if template := parameter.get('template_detail', None):
template_id = template['pk'] template_id = template['pk']

View File

@@ -11,6 +11,7 @@ from django import template
from django.apps.registry import apps from django.apps.registry import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Model
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils.safestring import SafeString, mark_safe from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -22,6 +23,7 @@ from PIL import Image
import common.currency import common.currency
import common.icons import common.icons
import common.models
import InvenTree.helpers import InvenTree.helpers
import InvenTree.helpers_model import InvenTree.helpers_model
import report.helpers import report.helpers
@@ -329,19 +331,23 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa
@register.simple_tag() @register.simple_tag()
def part_parameter(part: Part, parameter_name: str) -> Optional[str]: def parameter(
"""Return a PartParameter object for the given part and parameter name. instance: Model, parameter_name: str
) -> Optional[common.models.Parameter]:
"""Return a Parameter object for the given part and parameter name.
Arguments: Arguments:
part: A Part object instance: A Model object
parameter_name: The name of the parameter to retrieve parameter_name: The name of the parameter to retrieve
Returns: Returns:
A PartParameter object, or None if not found A Parameter object, or None if not found
""" """
if type(part) is Part: return (
return part.get_parameter(parameter_name) instance.parameters.prefetch_related('template')
return None .filter(template__name=parameter_name)
.first()
)
@register.simple_tag() @register.simple_tag()

View File

@@ -106,15 +106,12 @@ def get_ruleset_models() -> dict:
'part_partsellpricebreak', 'part_partsellpricebreak',
'part_partinternalpricebreak', 'part_partinternalpricebreak',
'part_parttesttemplate', 'part_parttesttemplate',
'part_partparametertemplate',
'part_partparameter',
'part_partrelated', 'part_partrelated',
'part_partstar', 'part_partstar',
'part_partstocktake', 'part_partstocktake',
'part_partcategorystar', 'part_partcategorystar',
'company_supplierpart', 'company_supplierpart',
'company_manufacturerpart', 'company_manufacturerpart',
'company_manufacturerpartparameter',
], ],
RuleSetEnum.STOCK_LOCATION: ['stock_stocklocation', 'stock_stocklocationtype'], RuleSetEnum.STOCK_LOCATION: ['stock_stocklocation', 'stock_stocklocationtype'],
RuleSetEnum.STOCK: [ RuleSetEnum.STOCK: [