mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Bug fix for ensuring location and category names are unique for common parent (#4361)
* Update Meta class for StockLocation and PartCategory * Migration files * Add extra unique requirements to InvenTreeTree model - unique_together does not work as expected with null values
This commit is contained in:
		| @@ -501,6 +501,34 @@ class InvenTreeTree(MPTTModel): | ||||
|         parent: The item immediately above this one. An item with a null parent is a top-level item | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass defines extra model properties.""" | ||||
|  | ||||
|         abstract = True | ||||
|  | ||||
|     class MPTTMeta: | ||||
|         """Set insert order.""" | ||||
|         order_insertion_by = ['name'] | ||||
|  | ||||
|     def validate_unique(self, exclude=None): | ||||
|         """Validate that this tree instance satisfies our uniqueness requirements. | ||||
|  | ||||
|         Note that a 'unique_together' requirement for ('name', 'parent') is insufficient, | ||||
|         as it ignores cases where parent=None (i.e. top-level items) | ||||
|         """ | ||||
|  | ||||
|         super().validate_unique(exclude) | ||||
|  | ||||
|         results = self.__class__.objects.filter( | ||||
|             name=self.name, | ||||
|             parent=self.parent | ||||
|         ).exclude(pk=self.pk) | ||||
|  | ||||
|         if results.exists(): | ||||
|             raise ValidationError({ | ||||
|                 'name': _('Duplicate names cannot exist under the same parent') | ||||
|             }) | ||||
|  | ||||
|     def api_instance_filters(self): | ||||
|         """Instance filters for InvenTreeTree models.""" | ||||
|         return { | ||||
| @@ -539,18 +567,6 @@ class InvenTreeTree(MPTTModel): | ||||
|             for child in self.get_children(): | ||||
|                 child.save(*args, **kwargs) | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass defines extra model properties.""" | ||||
|  | ||||
|         abstract = True | ||||
|  | ||||
|         # Names must be unique at any given level in the tree | ||||
|         unique_together = ('name', 'parent') | ||||
|  | ||||
|     class MPTTMeta: | ||||
|         """Set insert order.""" | ||||
|         order_insertion_by = ['name'] | ||||
|  | ||||
|     name = models.CharField( | ||||
|         blank=False, | ||||
|         max_length=100, | ||||
|   | ||||
| @@ -66,6 +66,11 @@ class PartCategory(MetadataMixin, InvenTreeTree): | ||||
|         default_keywords: Default keywords for parts created in this category | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass defines extra model properties""" | ||||
|         verbose_name = _("Part Category") | ||||
|         verbose_name_plural = _("Part Categories") | ||||
|  | ||||
|     def delete_recursive(self, *args, **kwargs): | ||||
|         """This function handles the recursive deletion of subcategories depending on kwargs contents""" | ||||
|         delete_parts = kwargs.get('delete_parts', False) | ||||
| @@ -154,11 +159,6 @@ class PartCategory(MetadataMixin, InvenTreeTree): | ||||
|                   "are already assigned to it!")) | ||||
|         super().clean() | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass defines extra model properties""" | ||||
|         verbose_name = _("Part Category") | ||||
|         verbose_name_plural = _("Part Categories") | ||||
|  | ||||
|     def get_parts(self, cascade=True) -> set[Part]: | ||||
|         """Return a queryset for all parts under this category. | ||||
|  | ||||
| @@ -747,7 +747,7 @@ class Part(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel): | ||||
|             return helpers.getBlankThumbnail() | ||||
|  | ||||
|     def validate_unique(self, exclude=None): | ||||
|         """Validate that a part is 'unique'. | ||||
|         """Validate that this Part instance is 'unique'. | ||||
|  | ||||
|         Uniqueness is checked across the following (case insensitive) fields: | ||||
|         - Name | ||||
|   | ||||
							
								
								
									
										17
									
								
								InvenTree/stock/migrations/0093_auto_20230217_2140.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								InvenTree/stock/migrations/0093_auto_20230217_2140.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| # Generated by Django 3.2.16 on 2023-02-17 21:40 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('stock', '0092_alter_stockitem_updated'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name='stocklocation', | ||||
|             options={'verbose_name': 'Stock Location', 'verbose_name_plural': 'Stock Locations'}, | ||||
|         ), | ||||
|     ] | ||||
| @@ -47,6 +47,12 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree): | ||||
|     Stock locations can be hierarchical as required | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass defines extra model properties""" | ||||
|  | ||||
|         verbose_name = _('Stock Location') | ||||
|         verbose_name_plural = _('Stock Locations') | ||||
|  | ||||
|     def delete_recursive(self, *args, **kwargs): | ||||
|         """This function handles the recursive deletion of sub-locations depending on kwargs contents""" | ||||
|         delete_stock_items = kwargs.get('delete_stock_items', False) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user