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

Add definition for Parameter model

This commit is contained in:
Oliver Walters
2025-11-10 11:06:01 +00:00
parent 86cc826671
commit 3ea0be2ef4
4 changed files with 350 additions and 149 deletions

View File

@@ -1,109 +0,0 @@
# Generated by Django 4.2.25 on 2025-10-28 11:11
import common.validators
import InvenTree.models
import InvenTree.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("common", "0039_emailthread_emailmessage"),
]
operations = [
migrations.CreateModel(
name="ParameterTemplate",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"metadata",
models.JSONField(
blank=True,
help_text="JSON metadata field, for use by external plugins",
null=True,
verbose_name="Plugin Metadata",
),
),
(
"name",
models.CharField(
help_text="Parameter Name",
max_length=100,
unique=True,
verbose_name="Name",
),
),
(
"units",
models.CharField(
blank=True,
help_text="Physical units for this parameter",
max_length=25,
validators=[InvenTree.validators.validate_physical_units],
verbose_name="Units",
),
),
(
"description",
models.CharField(
blank=True,
help_text="Parameter description",
max_length=250,
verbose_name="Description",
),
),
(
"model_type",
models.CharField(
blank=True,
default="",
help_text="Target model type for this parameter",
max_length=100,
validators=[common.validators.validate_parameter_template_model_type],
verbose_name="Model type",
),
),
(
"checkbox",
models.BooleanField(
default=False,
help_text="Is this parameter a checkbox?",
verbose_name="Checkbox",
),
),
(
"choices",
models.CharField(
blank=True,
help_text="Valid choices for this parameter (comma-separated)",
max_length=5000,
verbose_name="Choices",
),
),
(
"selectionlist",
models.ForeignKey(
blank=True,
help_text="Selection list for this parameter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="templates",
to="common.selectionlist",
verbose_name="Selection List",
),
),
],
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
]

View File

@@ -0,0 +1,215 @@
# Generated by Django 4.2.25 on 2025-11-10 10:51
import InvenTree.models
import InvenTree.validators
import common.validators
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("common", "0039_emailthread_emailmessage"),
]
operations = [
migrations.CreateModel(
name="ParameterTemplate",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"metadata",
models.JSONField(
blank=True,
help_text="JSON metadata field, for use by external plugins",
null=True,
verbose_name="Plugin Metadata",
),
),
(
"model_type",
models.CharField(
blank=True,
default="",
help_text="Target model type for this parameter template",
max_length=100,
validators=[
common.validators.validate_parameter_template_model_type
],
verbose_name="Model type",
),
),
(
"name",
models.CharField(
help_text="Parameter Name",
max_length=100,
unique=True,
verbose_name="Name",
),
),
(
"units",
models.CharField(
blank=True,
help_text="Physical units for this parameter",
max_length=25,
validators=[InvenTree.validators.validate_physical_units],
verbose_name="Units",
),
),
(
"description",
models.CharField(
blank=True,
help_text="Parameter description",
max_length=250,
verbose_name="Description",
),
),
(
"checkbox",
models.BooleanField(
default=False,
help_text="Is this parameter a checkbox?",
verbose_name="Checkbox",
),
),
(
"choices",
models.CharField(
blank=True,
help_text="Valid choices for this parameter (comma-separated)",
max_length=5000,
verbose_name="Choices",
),
),
(
"selectionlist",
models.ForeignKey(
blank=True,
help_text="Selection list for this parameter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="templates",
to="common.selectionlist",
verbose_name="Selection List",
),
),
],
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name="Parameter",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"metadata",
models.JSONField(
blank=True,
help_text="JSON metadata field, for use by external plugins",
null=True,
verbose_name="Plugin Metadata",
),
),
(
"updated",
models.DateTimeField(
blank=True,
default=None,
help_text="Timestamp of last update",
null=True,
verbose_name="Updated",
),
),
(
"model_type",
models.CharField(
blank=True,
default="",
help_text="Target model type for this parameter",
max_length=100,
validators=[common.validators.validate_parameter_model_type],
verbose_name="Model type",
),
),
(
"model_id",
models.PositiveIntegerField(
help_text="ID of the target model for this parameter",
verbose_name="Model ID",
),
),
(
"data",
models.CharField(
help_text="Parameter Value",
max_length=500,
validators=[django.core.validators.MinLengthValidator(1)],
verbose_name="Data",
),
),
(
"data_numeric",
models.FloatField(blank=True, default=None, null=True),
),
(
"note",
models.CharField(
blank=True,
help_text="Optional note field",
max_length=500,
verbose_name="Note",
),
),
(
"template",
models.ForeignKey(
help_text="Parameter template",
on_delete=django.db.models.deletion.CASCADE,
related_name="parameters",
to="common.parametertemplate",
verbose_name="Template",
),
),
(
"updated_by",
models.ForeignKey(
blank=True,
help_text="User who last updated this object",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated",
to=settings.AUTH_USER_MODEL,
verbose_name="Update By",
),
),
],
options={
"verbose_name": "Parameter",
"verbose_name_plural": "Parameters",
"unique_together": {("model_type", "model_id", "template")},
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
]

View File

@@ -3,60 +3,70 @@
from django.db import migrations from django.db import migrations
def copy_templates(old_model, new_model): def copy_part_parameters(apps, schema_editor):
"""Copy from one model type to another.""" """Forward migration: copy from PartParameterTemplate to ParameterTemplate."""
PartParameterTemplate = apps.get_model("part", "PartParameterTemplate")
ParameterTemplate = apps.get_model("common", "ParameterTemplate")
PartParameter = apps.get_model("part", "PartParameter")
Parameter = apps.get_model("common", "Parameter")
# First, create a ParameterTemplate instance for each PartParameterTemplate
templates = [] templates = []
# Clear out all existing instances for template in PartParameterTemplate.objects.all():
new_model.objects.all().delete() templates.append(ParameterTemplate(
assert new_model.objects.count() == 0
for template in old_model.objects.all():
templates.append(new_model(
name=template.name, name=template.name,
description=template.description, description=template.description,
units=template.units, units=template.units,
checkbox=template.checkbox, checkbox=template.checkbox,
choices=template.choices, choices=template.choices,
selectionlist=template.selectionlist, selectionlist=template.selectionlist,
model_type='part'
))
if len(templates) > 0:
ParameterTemplate.objects.bulk_create(templates)
print(f"\nMigrated {len(templates)} PartParameterTemplate instances.")
assert ParameterTemplate.objects.filter(model_type='part').count() == len(templates)
# Next, copy PartParameter instances to Parameter instances
parameters = []
for parameter in PartParameter.objects.all():
# Find the corresponding ParameterTemplate
template = ParameterTemplate.objects.get(name=parameter.template.name, model_type='part')
parameters.append(Parameter(
template=template,
model_type='part',
model_id=parameter.part.id,
data=parameter.data,
data_numeric=parameter.data_numeric,
note=parameter.note,
updated=parameter.updated,
updated_by=parameter.updated_by
)) ))
if len(parameters) > 0:
Parameter.objects.bulk_create(parameters)
print(f"\nMigrated {len(parameters)} PartParameter instances.")
if len(templates) > 0: assert Parameter.objects.filter(model_type='part').count() == len(parameters)
new_model.objects.bulk_create(templates)
print(f"Migrated {len(templates)} ParameterTemplate instances.")
assert new_model.objects.count() == len(templates)
def forward_copy_templates(apps, schema_editor):
"""Forward migration: copy from PartParameterTemplate to ParameterTemplate."""
PartParameterTemplate = apps.get_model("part", "PartParameterTemplate")
ParameterTemplate = apps.get_model("common", "ParameterTemplate")
copy_templates(PartParameterTemplate, ParameterTemplate)
def reverse_copy_templates(apps, schema_editor):
"""Reverse migration: copy from ParameterTemplate to PartParameterTemplate."""
ParameterTemplate = apps.get_model("common", "ParameterTemplate")
PartParameterTemplate = apps.get_model("part", "PartParameterTemplate")
copy_templates(ParameterTemplate, PartParameterTemplate)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("part", "0132_partparametertemplate_selectionlist"), ("part", "0132_partparametertemplate_selectionlist"),
("common", "0040_parametertemplate"), ("common", "0040_parametertemplate_parameter"),
] ]
operations = [ operations = [
migrations.RunPython( migrations.RunPython(
forward_copy_templates, copy_part_parameters,
reverse_code=reverse_copy_templates reverse_code=migrations.RunPython.noop
) ),
# TODO: Data migration for ManufacturerPartParameter
] ]

View File

@@ -7,6 +7,7 @@ import base64
import hashlib import hashlib
import hmac import hmac
import json import json
import math
import os import os
import uuid import uuid
from datetime import timedelta, timezone from datetime import timedelta, timezone
@@ -28,7 +29,7 @@ from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail.utils import DNS_NAME from django.core.mail.utils import DNS_NAME
from django.core.validators import MinValueValidator from django.core.validators import MinLengthValidator, MinValueValidator
from django.db import models, transaction from django.db import models, transaction
from django.db.models import enums from django.db.models import enums
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
@@ -48,6 +49,7 @@ from rest_framework.exceptions import PermissionDenied
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
import common.validators import common.validators
import InvenTree.conversion
import InvenTree.fields import InvenTree.fields
import InvenTree.helpers import InvenTree.helpers
import InvenTree.models import InvenTree.models
@@ -2376,6 +2378,7 @@ class ParameterTemplate(
Attributes: Attributes:
name: The name (key) of the template name: The name (key) of the template
description: A description of the template description: A description of the template
model_type: The type of model to which this template applies (e.g. 'part')
units: The units associated with the template (if applicable) units: The units associated with the template (if applicable)
checkbox: Is this template a checkbox (boolean) type? checkbox: Is this template a checkbox (boolean) type?
choices: Comma-separated list of choices (if applicable) choices: Comma-separated list of choices (if applicable)
@@ -2478,7 +2481,7 @@ class ParameterTemplate(
blank=True, blank=True,
validators=[common.validators.validate_parameter_template_model_type], validators=[common.validators.validate_parameter_template_model_type],
verbose_name=_('Model type'), verbose_name=_('Model type'),
help_text=_('Target model type for this parameter'), help_text=_('Target model type for this parameter template'),
) )
name = models.CharField( name = models.CharField(
@@ -2546,9 +2549,6 @@ class Parameter(
class Meta: class Meta:
"""Meta options for Parameter model.""" """Meta options for Parameter model."""
# TODO: Make this non-abstract, actually implement...
abstract = True
verbose_name = _('Parameter') verbose_name = _('Parameter')
verbose_name_plural = _('Parameters') verbose_name_plural = _('Parameters')
unique_together = [['model_type', 'model_id', 'template']] unique_together = [['model_type', 'model_id', 'template']]
@@ -2574,6 +2574,11 @@ class Parameter(
super().save(*args, **kwargs) super().save(*args, **kwargs)
def delete(self):
"""Perform custom delete checks before deleting a Parameter instance."""
# TODO: Custom delete checks against the model type this is linked to...
super().delete()
def clean(self): def clean(self):
"""Validate the Parameter before saving to the database.""" """Validate the Parameter before saving to the database."""
super().clean() super().clean()
@@ -2589,6 +2594,86 @@ class Parameter(
# TODO: Validate against plugins # TODO: Validate against plugins
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
model_type = models.CharField(
max_length=100,
default='',
blank=True,
validators=[common.validators.validate_parameter_model_type],
verbose_name=_('Model type'),
help_text=_('Target model type for this parameter'),
)
model_id = models.PositiveIntegerField(
verbose_name=_('Model ID'),
help_text=_('ID of the target model for this parameter'),
)
template = models.ForeignKey(
ParameterTemplate,
on_delete=models.CASCADE,
related_name='parameters',
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
class BarcodeScanResult(InvenTree.models.InvenTreeModel): class BarcodeScanResult(InvenTree.models.InvenTreeModel):
"""Model for storing barcode scans results.""" """Model for storing barcode scans results."""