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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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 <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):
|
||||
"""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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user