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:
		| @@ -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 | ||||
|   | ||||
| @@ -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.""" | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -121,6 +121,8 @@ class ParameterAdmin(admin.ModelAdmin): | ||||
|  | ||||
|     list_display = ('part', 'template', 'data') | ||||
|  | ||||
|     readonly_fields = ('updated', 'updated_by') | ||||
|  | ||||
|     autocomplete_fields = ('part', 'template') | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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.""" | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user