2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-30 20:55:42 +00:00

PartParameter updates (#10023)

* Add mixin for storing user who last updated an instance

* Add mixin to "PartParameter" model

* Fix typo

* Fix strings

* Refactor mixin class

* Update part parameter table:

- Add "user" filter
- Add "updated_by" column
- Add "update" column
- Add "note" column

* Fix for updating date

* Add user information when saving parameter

* small refactors

* Bump API version

* Add unit test for "updated" and "updated_by" fields

* Check for 'note' field

* Update docs image
This commit is contained in:
Oliver
2025-07-15 22:34:07 +10:00
committed by GitHub
parent 75ab57bc0b
commit d99ec3e1a1
13 changed files with 203 additions and 22 deletions

View File

@@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 368
INVENTREE_API_VERSION = 369
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v369 -> 2025-07-15 : https://github.com/inventree/InvenTree/pull/10023
- Adds "note", "updated", "updated_by" fields to the PartParameter API endpoints
v368 -> 2025-07-11 : https://github.com/inventree/InvenTree/pull/9673
- Adds 'tax_id' to company model
- Adds 'tax_id' to search fields in the 'CompanyList' API endpoint

View File

@@ -104,6 +104,42 @@ class MetaMixin(models.Model):
)
class UpdatedUserMixin(models.Model):
"""A mixin which stores additional information about the user who created or last modified the object."""
class Meta:
"""Meta options for MetaUserMixin."""
abstract = True
def save(self, *args, **kwargs):
"""Extract the user object from kwargs, if provided."""
if updated_by := kwargs.pop('updated_by', None):
self.updated_by = updated_by
self.updated = InvenTree.helpers.current_time()
super().save(*args, **kwargs)
updated = models.DateTimeField(
verbose_name=_('Updated'),
help_text=_('Timestamp of last update'),
default=None,
blank=True,
null=True,
)
updated_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='%(class)s_updated',
verbose_name=_('Update By'),
help_text=_('User who last updated this object'),
)
class ProjectCode(InvenTree.models.InvenTreeMetadataModel):
"""A ProjectCode is a unique identifier for a project."""

View File

@@ -125,9 +125,7 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
"""
session = super().create(validated_data)
request = self.context.get('request', None)
if request:
if request := self.context.get('request', None):
session.user = request.user
session.save()

View File

@@ -121,6 +121,8 @@ class ParameterAdmin(admin.ModelAdmin):
list_display = ('part', 'template', 'data')
readonly_fields = ('updated', 'updated_by')
autocomplete_fields = ('part', 'template')

View File

@@ -1392,9 +1392,16 @@ class PartParameterAPIMixin:
def get_queryset(self, *args, **kwargs):
"""Override get_queryset method to prefetch related fields."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related('part', 'template')
queryset = queryset.prefetch_related('part', 'template', 'updated_by')
return queryset
def get_serializer_context(self):
"""Pass the 'request' object through to the serializer context."""
context = super().get_serializer_context()
context['request'] = self.request
return context
def get_serializer(self, *args, **kwargs):
"""Return the serializer instance for this API endpoint.
@@ -1420,7 +1427,7 @@ class PartParameterFilter(rest_filters.FilterSet):
"""Metaclass options for the filterset."""
model = PartParameter
fields = ['template']
fields = ['template', 'updated_by']
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(), method='filter_part'
@@ -1453,7 +1460,7 @@ class PartParameterList(PartParameterAPIMixin, DataExportViewMixin, ListCreateAP
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_fields = ['name', 'data', 'part', 'template']
ordering_fields = ['name', 'data', 'part', 'template', 'updated', 'updated_by']
ordering_field_aliases = {
'name': 'template__name',

View File

@@ -0,0 +1,50 @@
# Generated by Django 4.2.23 on 2025-07-15 02:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("part", "0135_alter_part_link"),
]
operations = [
migrations.AddField(
model_name="partparameter",
name="note",
field=models.CharField(
blank=True,
help_text="Optional note field",
max_length=500,
verbose_name="Note",
),
),
migrations.AddField(
model_name="partparameter",
name="updated",
field=models.DateTimeField(
blank=True,
default=None,
help_text="Timestamp of last update",
null=True,
verbose_name="Updated",
),
),
migrations.AddField(
model_name="partparameter",
name="updated_by",
field=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",
),
),
]

View File

@@ -3976,13 +3976,19 @@ def post_save_part_parameter_template(sender, instance, created, **kwargs):
)
class PartParameter(InvenTree.models.InvenTreeMetadataModel):
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:
@@ -4123,6 +4129,13 @@ class PartParameter(InvenTree.models.InvenTreeMetadataModel):
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."""

View File

@@ -410,8 +410,14 @@ class PartParameterSerializer(
'template_detail',
'data',
'data_numeric',
'note',
'updated',
'updated_by',
'updated_by_detail',
]
read_only_fields = ['updated', 'updated_by']
def __init__(self, *args, **kwargs):
"""Custom initialization method for the serializer.
@@ -431,13 +437,27 @@ class PartParameterSerializer(
if not template_detail:
self.fields.pop('template_detail', None)
def save(self):
"""Save the PartParameter instance."""
instance = super().save()
if request := self.context.get('request', None):
# If the request is provided, update the 'updated_by' field
instance.updated_by = request.user
instance.save()
return instance
part_detail = PartBriefSerializer(
source='part', many=False, read_only=True, allow_null=True
)
template_detail = PartParameterTemplateSerializer(
source='template', many=False, read_only=True, allow_null=True
)
updated_by_detail = UserSerializer(source='updated_by', many=False, read_only=True)
class DuplicatePartSerializer(serializers.Serializer):
"""Serializer for specifying options when duplicating a Part.
@@ -1134,9 +1154,8 @@ class PartSerializer(
part=instance, quantity=quantity, location=location
)
request = self.context.get('request', None)
user = request.user if request else None
stockitem.save(user=user)
if request := self.context.get('request', None):
stockitem.save(user=request.user)
# Create initial supplier information
if initial_supplier:
@@ -1302,7 +1321,7 @@ class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
# Add in user information automatically
request = self.context.get('request')
data['user'] = request.user if request else None
super().save()
return super().save()
class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializer):

View File

@@ -1,6 +1,7 @@
"""Various unit tests for Part Parameters."""
import django.core.exceptions as django_exceptions
from django.contrib.auth.models import User
from django.test import TestCase, TransactionTestCase
from django.urls import reverse
@@ -19,7 +20,7 @@ from .models import (
class TestParams(TestCase):
"""Unit test class for testing the PartParameter model."""
fixtures = ['location', 'category', 'part', 'params']
fixtures = ['location', 'category', 'part', 'params', 'users']
def test_str(self):
"""Test the str representation of the PartParameterTemplate model."""
@@ -32,6 +33,18 @@ class TestParams(TestCase):
c1 = PartCategoryParameterTemplate.objects.get(pk=1)
self.assertEqual(str(c1), 'Mechanical | Length | 2.8')
def test_updated(self):
"""Test that the 'updated' field is correctly set."""
p1 = PartParameter.objects.get(pk=1)
self.assertIsNone(p1.updated)
self.assertIsNone(p1.updated_by)
user = User.objects.get(username='sam')
p1.save(updated_by=user)
self.assertIsNotNone(p1.updated)
self.assertEqual(p1.updated_by, user)
def test_validate(self):
"""Test validation for part templates."""
n = PartParameterTemplate.objects.all().count()