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
def copy_templates(old_model, new_model):
"""Copy from one model type to another."""
def copy_part_parameters(apps, schema_editor):
"""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 = []
# Clear out all existing instances
new_model.objects.all().delete()
assert new_model.objects.count() == 0
for template in old_model.objects.all():
templates.append(new_model(
for template in PartParameterTemplate.objects.all():
templates.append(ParameterTemplate(
name=template.name,
description=template.description,
units=template.units,
checkbox=template.checkbox,
choices=template.choices,
selectionlist=template.selectionlist,
model_type='part'
))
if len(templates) > 0:
new_model.objects.bulk_create(templates)
print(f"Migrated {len(templates)} ParameterTemplate instances.")
ParameterTemplate.objects.bulk_create(templates)
print(f"\nMigrated {len(templates)} PartParameterTemplate instances.")
assert new_model.objects.count() == len(templates)
assert ParameterTemplate.objects.filter(model_type='part').count() == len(templates)
# Next, copy PartParameter instances to Parameter instances
parameters = []
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")
for parameter in PartParameter.objects.all():
# Find the corresponding ParameterTemplate
template = ParameterTemplate.objects.get(name=parameter.template.name, model_type='part')
copy_templates(PartParameterTemplate, ParameterTemplate)
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.")
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)
assert Parameter.objects.filter(model_type='part').count() == len(parameters)
class Migration(migrations.Migration):
dependencies = [
("part", "0132_partparametertemplate_selectionlist"),
("common", "0040_parametertemplate"),
("common", "0040_parametertemplate_parameter"),
]
operations = [
migrations.RunPython(
forward_copy_templates,
reverse_code=reverse_copy_templates
)
copy_part_parameters,
reverse_code=migrations.RunPython.noop
),
# TODO: Data migration for ManufacturerPartParameter
]

View File

@@ -7,6 +7,7 @@ import base64
import hashlib
import hmac
import json
import math
import os
import uuid
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.mail import EmailMultiAlternatives, get_connection
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.models import enums
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
import common.validators
import InvenTree.conversion
import InvenTree.fields
import InvenTree.helpers
import InvenTree.models
@@ -2376,6 +2378,7 @@ class ParameterTemplate(
Attributes:
name: The name (key) 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)
checkbox: Is this template a checkbox (boolean) type?
choices: Comma-separated list of choices (if applicable)
@@ -2478,7 +2481,7 @@ class ParameterTemplate(
blank=True,
validators=[common.validators.validate_parameter_template_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(
@@ -2546,9 +2549,6 @@ class Parameter(
class Meta:
"""Meta options for Parameter model."""
# TODO: Make this non-abstract, actually implement...
abstract = True
verbose_name = _('Parameter')
verbose_name_plural = _('Parameters')
unique_together = [['model_type', 'model_id', 'template']]
@@ -2574,6 +2574,11 @@ class Parameter(
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):
"""Validate the Parameter before saving to the database."""
super().clean()
@@ -2589,6 +2594,86 @@ class Parameter(
# 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):
"""Model for storing barcode scans results."""