From b3a5dbb5dbdac561067da3790429cd7f66a93671 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 11:29:00 +1000 Subject: [PATCH 01/14] Add django-mptt as a requirement (cherry picked from commit 8c33a9fca11ad9af9c9f1c6ddf2a9dab8d71e2e4) --- InvenTree/InvenTree/settings.py | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 6c9d4a9b55..476be9aa28 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -100,6 +100,7 @@ INSTALLED_APPS = [ 'import_export', # Import / export tables to file 'django_cleanup', # Automatically delete orphaned MEDIA files 'qr_code', # Generate QR codes + 'mptt', # Modified Preorder Tree Traversal ] LOGGING = { diff --git a/requirements.txt b/requirements.txt index cf45d83248..0f94e4cc14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ pillow>=5.0.0 # Image manipulation djangorestframework>=3.6.2 # DRF framework django-cors-headers>=2.5.3 # CORS headers extension for DRF django_filter>=1.0.2 # Extended filtering options +django-mptt>=0.10.0 # Modified Preorder Tree Traversal django-dbbackup==3.2.0 # Database backup / restore functionality coreapi>=2.3.0 # API documentation pygments>=2.2.0 # Syntax highlighting From 2f11fccb73cd87e3f4485535389526a3c33f653b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 14:08:49 +1000 Subject: [PATCH 02/14] Migrate InvenTreeTree to using MPTT model --- InvenTree/InvenTree/models.py | 7 +++- .../migrations/0019_auto_20190908_0404.py | 37 +++++++++++++++++++ .../migrations/0020_auto_20190908_0404.py | 21 +++++++++++ .../migrations/0011_auto_20190908_0404.py | 37 +++++++++++++++++++ .../migrations/0012_auto_20190908_0405.py | 22 +++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 InvenTree/part/migrations/0019_auto_20190908_0404.py create mode 100644 InvenTree/part/migrations/0020_auto_20190908_0404.py create mode 100644 InvenTree/stock/migrations/0011_auto_20190908_0404.py create mode 100644 InvenTree/stock/migrations/0012_auto_20190908_0405.py diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 0c05a2a44b..41ada83134 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -11,10 +11,12 @@ from rest_framework.exceptions import ValidationError from django.db.models.signals import pre_delete from django.dispatch import receiver +from mptt.models import MPTTModel, TreeForeignKey + from .validators import validate_tree_name -class InvenTreeTree(models.Model): +class InvenTreeTree(MPTTModel): """ Provides an abstracted self-referencing tree model for data categories. - Each Category has one parent Category, which can be blank (for a top-level Category). @@ -30,6 +32,9 @@ class InvenTreeTree(models.Model): abstract = True unique_together = ('name', 'parent') + class MPTTMeta: + order_insertion_by = ['name'] + name = models.CharField( blank=False, max_length=100, diff --git a/InvenTree/part/migrations/0019_auto_20190908_0404.py b/InvenTree/part/migrations/0019_auto_20190908_0404.py new file mode 100644 index 0000000000..160bb929b9 --- /dev/null +++ b/InvenTree/part/migrations/0019_auto_20190908_0404.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.5 on 2019-09-08 04:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0018_auto_20190907_0941'), + ] + + operations = [ + migrations.AddField( + model_name='partcategory', + name='level', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='partcategory', + name='lft', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='partcategory', + name='rght', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='partcategory', + name='tree_id', + field=models.PositiveIntegerField(db_index=True, default=0, editable=False), + preserve_default=False, + ), + ] diff --git a/InvenTree/part/migrations/0020_auto_20190908_0404.py b/InvenTree/part/migrations/0020_auto_20190908_0404.py new file mode 100644 index 0000000000..4f27191099 --- /dev/null +++ b/InvenTree/part/migrations/0020_auto_20190908_0404.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.5 on 2019-09-08 04:04 + +from django.db import migrations +from part import models + + +def update_tree(apps, schema_editor): + # Update the PartCategory MPTT model + + models.PartCategory.objects.rebuild() + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0019_auto_20190908_0404'), + ] + + operations = [ + migrations.RunPython(update_tree) + ] diff --git a/InvenTree/stock/migrations/0011_auto_20190908_0404.py b/InvenTree/stock/migrations/0011_auto_20190908_0404.py new file mode 100644 index 0000000000..7fc85d0556 --- /dev/null +++ b/InvenTree/stock/migrations/0011_auto_20190908_0404.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.5 on 2019-09-08 04:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0010_stockitem_build'), + ] + + operations = [ + migrations.AddField( + model_name='stocklocation', + name='level', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='stocklocation', + name='lft', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='stocklocation', + name='rght', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='stocklocation', + name='tree_id', + field=models.PositiveIntegerField(db_index=True, default=0, editable=False), + preserve_default=False, + ), + ] diff --git a/InvenTree/stock/migrations/0012_auto_20190908_0405.py b/InvenTree/stock/migrations/0012_auto_20190908_0405.py new file mode 100644 index 0000000000..52071e5cd4 --- /dev/null +++ b/InvenTree/stock/migrations/0012_auto_20190908_0405.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.5 on 2019-09-08 04:05 + +from django.db import migrations + +from stock import models + + +def update_tree(apps, schema_editor): + # Update the StockLocation MPTT model + + models.StockLocation.objects.rebuild() + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0011_auto_20190908_0404'), + ] + + operations = [ + migrations.RunPython(update_tree) + ] From 4d7fba9f14f1fb61b1b5040d986b163c8b6906df Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 18:57:48 +1000 Subject: [PATCH 03/14] Replace tree functionality with MPTT goodness --- InvenTree/InvenTree/models.py | 45 ++++++----------------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 41ada83134..f7d1670b8a 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -65,59 +65,31 @@ class InvenTreeTree(MPTTModel): """ return 0 - def getUniqueParents(self, unique=None): + def getUniqueParents(self): """ Return a flat set of all parent items that exist above this node. If any parents are repeated (which would be very bad!), the process is halted """ - item = self + return self.get_ancestors() - # Prevent infinite regression - max_parents = 500 - - unique = set() - - while item.parent and max_parents > 0: - max_parents -= 1 - - unique.add(item.parent.id) - item = item.parent - - return unique - - def getUniqueChildren(self, unique=None, include_self=True): + def getUniqueChildren(self, include_self=True): """ Return a flat set of all child items that exist under this node. If any child items are repeated, the repetitions are omitted. """ - if unique is None: - unique = set() - - if self.id in unique: - return unique - - if include_self: - unique.add(self.id) - - # Some magic to get around the limitations of abstract models - contents = ContentType.objects.get_for_model(type(self)) - children = contents.get_all_objects_for_this_type(parent=self.id) - - for child in children: - child.getUniqueChildren(unique) - - return unique + return self.get_descendants(include_self=include_self) @property def has_children(self): """ True if there are any children under this item """ - return self.children.count() > 0 + return self.getUniqueChildren().count() > 0 def getAcceptableParents(self): """ Returns a list of acceptable parent items within this model Acceptable parents are ones which are not underneath this item. Setting the parent of an item to its own child results in recursion. """ + contents = ContentType.objects.get_for_model(type(self)) available = contents.get_all_objects_for_this_type() @@ -141,10 +113,7 @@ class InvenTreeTree(MPTTModel): List of category names from the top level to the parent of this category """ - if self.parent: - return self.parent.parentpath + [self.parent] - else: - return [] + return [a for a in self.get_ancestors()] @property def path(self): From 678157aac4d99be38d3a43dd9f884e93c6406234 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:13:13 +1000 Subject: [PATCH 04/14] Update StockLocation and PartCategory models - Use the MPTT functionality once more --- InvenTree/part/models.py | 27 +++++++++++++++++++-------- InvenTree/stock/models.py | 31 +++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4590a062a8..2970129d6e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -64,21 +64,32 @@ class PartCategory(InvenTreeTree): verbose_name = "Part Category" verbose_name_plural = "Part Categories" + def get_parts(self, cascade=True): + """ Return a queryset for all parts under this category. + + args: + cascade - If True, also look under subcategories (default = True) + """ + + if cascade: + """ Select any parts which exist in this category or any child categories """ + query = Part.objects.filter(category__in=self.getUniqueChildren(include_self=True)) + else: + query = Part.objects.filter(category=self.pk) + + return query + + @property def item_count(self): return self.partcount() - def partcount(self, cascade=True, active=True): + def partcount(self, cascade=True, active=False): """ Return the total part count under this category (including children of child categories) """ - cats = [self.id] - - if cascade: - cats += [cat for cat in self.getUniqueChildren()] - - query = Part.objects.filter(category__in=cats) + query = self.get_parts(cascade=cascade) if active: query = query.filter(active=True) @@ -88,7 +99,7 @@ class PartCategory(InvenTreeTree): @property def has_parts(self): """ True if there are any parts in this category """ - return self.parts.count() > 0 + return self.partcount() > 0 @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 78cef49371..889eaac5bd 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -34,9 +34,6 @@ class StockLocation(InvenTreeTree): def get_absolute_url(self): return reverse('stock-location-detail', kwargs={'pk': self.id}) - def has_items(self): - return self.stock_items.count() > 0 - def format_barcode(self): """ Return a JSON string for formatting a barcode for this StockLocation object """ @@ -49,16 +46,34 @@ class StockLocation(InvenTreeTree): } ) + def get_stock_items(self, cascade=True): + """ Return a queryset for all stock items under this category. + + Args: + cascade: If True, also look under sublocations (default = True) + """ + + if cascade: + query = StockItem.objects.filter(location__in=self.getUniqueChildren(include_self=True)) + else: + query = StockItem.objects.filter(location=self.pk) + + return query + def stock_item_count(self, cascade=True): """ Return the number of StockItem objects which live in or under this category """ - if cascade: - query = StockItem.objects.filter(location__in=self.getUniqueChildren()) - else: - query = StockItem.objects.filter(location=self) + return self.get_stock_items(cascade).count() + + def has_items(self, cascade=True): + """ Return True if there are StockItems existing in this category. + + Args: + cascade: If True, also search an sublocations (default = True) + """ + return self.stock_item_count(cascade) > 0 - return query.count() @property def item_count(self): From 3eb3c43e5c5126e5777b56c49d41480114cf9cca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:19:39 +1000 Subject: [PATCH 05/14] Change foreign keys to TreeForeignKey --- InvenTree/InvenTree/models.py | 10 +++---- .../migrations/0021_auto_20190908_0916.py | 30 +++++++++++++++++++ .../migrations/0022_auto_20190908_0918.py | 20 +++++++++++++ InvenTree/part/models.py | 20 +++++++------ .../migrations/0013_auto_20190908_0916.py | 20 +++++++++++++ .../migrations/0014_auto_20190908_0918.py | 20 +++++++++++++ InvenTree/stock/models.py | 8 +++-- 7 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 InvenTree/part/migrations/0021_auto_20190908_0916.py create mode 100644 InvenTree/part/migrations/0022_auto_20190908_0918.py create mode 100644 InvenTree/stock/migrations/0013_auto_20190908_0916.py create mode 100644 InvenTree/stock/migrations/0014_auto_20190908_0918.py diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index f7d1670b8a..d5b1cddc17 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -48,11 +48,11 @@ class InvenTreeTree(MPTTModel): ) # When a category is deleted, graft the children onto its parent - parent = models.ForeignKey('self', - on_delete=models.DO_NOTHING, - blank=True, - null=True, - related_name='children') + parent = TreeForeignKey('self', + on_delete=models.DO_NOTHING, + blank=True, + null=True, + related_name='children') @property def item_count(self): diff --git a/InvenTree/part/migrations/0021_auto_20190908_0916.py b/InvenTree/part/migrations/0021_auto_20190908_0916.py new file mode 100644 index 0000000000..0427487246 --- /dev/null +++ b/InvenTree/part/migrations/0021_auto_20190908_0916.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.5 on 2019-09-08 09:16 + +from django.db import migrations +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0020_auto_20190908_0404'), + ] + + operations = [ + migrations.AlterField( + model_name='part', + name='category', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory'), + ), + migrations.AlterField( + model_name='part', + name='default_location', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation'), + ), + migrations.AlterField( + model_name='partcategory', + name='default_location', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Default location for parts in this category', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_categories', to='stock.StockLocation'), + ), + ] diff --git a/InvenTree/part/migrations/0022_auto_20190908_0918.py b/InvenTree/part/migrations/0022_auto_20190908_0918.py new file mode 100644 index 0000000000..48782ed545 --- /dev/null +++ b/InvenTree/part/migrations/0022_auto_20190908_0918.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.5 on 2019-09-08 09:18 + +from django.db import migrations +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0021_auto_20190908_0916'), + ] + + operations = [ + migrations.AlterField( + model_name='partcategory', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='part.PartCategory'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 2970129d6e..7cf6776b1d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -25,6 +25,8 @@ from django.contrib.auth.models import User from django.db.models.signals import pre_delete from django.dispatch import receiver +from mptt.models import TreeForeignKey + from datetime import datetime from fuzzywuzzy import fuzz import hashlib @@ -48,7 +50,7 @@ class PartCategory(InvenTreeTree): default_keywords: Default keywords for parts created in this category """ - default_location = models.ForeignKey( + default_location = TreeForeignKey( 'stock.StockLocation', related_name="default_categories", null=True, blank=True, on_delete=models.SET_NULL, @@ -351,10 +353,10 @@ class Part(models.Model): keywords = models.CharField(max_length=250, blank=True, help_text='Part keywords to improve visibility in search results') - category = models.ForeignKey(PartCategory, related_name='parts', - null=True, blank=True, - on_delete=models.DO_NOTHING, - help_text='Part category') + category = TreeForeignKey(PartCategory, related_name='parts', + null=True, blank=True, + on_delete=models.DO_NOTHING, + help_text='Part category') IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number') @@ -364,10 +366,10 @@ class Part(models.Model): image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True) - default_location = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL, - blank=True, null=True, - help_text='Where is this item normally stored?', - related_name='default_parts') + default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL, + blank=True, null=True, + help_text='Where is this item normally stored?', + related_name='default_parts') def get_default_location(self): """ Get the default location for a Part (may be None). diff --git a/InvenTree/stock/migrations/0013_auto_20190908_0916.py b/InvenTree/stock/migrations/0013_auto_20190908_0916.py new file mode 100644 index 0000000000..897d4298c2 --- /dev/null +++ b/InvenTree/stock/migrations/0013_auto_20190908_0916.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.5 on 2019-09-08 09:16 + +from django.db import migrations +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0012_auto_20190908_0405'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='location', + field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation'), + ), + ] diff --git a/InvenTree/stock/migrations/0014_auto_20190908_0918.py b/InvenTree/stock/migrations/0014_auto_20190908_0918.py new file mode 100644 index 0000000000..b6f7411e10 --- /dev/null +++ b/InvenTree/stock/migrations/0014_auto_20190908_0918.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.5 on 2019-09-08 09:18 + +from django.db import migrations +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0013_auto_20190908_0916'), + ] + + operations = [ + migrations.AlterField( + model_name='stocklocation', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockLocation'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 889eaac5bd..6c4571d5a3 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -16,6 +16,8 @@ from django.contrib.auth.models import User from django.db.models.signals import pre_delete from django.dispatch import receiver +from mptt.models import TreeForeignKey + from datetime import datetime from InvenTree import helpers @@ -292,9 +294,9 @@ class StockItem(models.Model): supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL, help_text='Select a matching supplier part for this stock item') - location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING, - related_name='stock_items', blank=True, null=True, - help_text='Where is this stock item located?') + location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING, + related_name='stock_items', blank=True, null=True, + help_text='Where is this stock item located?') belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING, related_name='owned_parts', blank=True, null=True, From b554af5f10a63e77d45a450c80601f6b2556f6cb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:21:40 +1000 Subject: [PATCH 06/14] Fix display of location list --- InvenTree/stock/templates/stock/location_list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/templates/stock/location_list.html b/InvenTree/stock/templates/stock/location_list.html index 90d01c449c..9ce7a1814c 100644 --- a/InvenTree/stock/templates/stock/location_list.html +++ b/InvenTree/stock/templates/stock/location_list.html @@ -7,8 +7,8 @@ Sub-Locations{{ children|length }} {% block collapse_content %}
    {% for child in children %} -
  • {{ child.name }} - {{ child.description }}
  • - {{ child.partcount }} +
  • {{ child.name }} - {{ child.description }} + {{ child.item_count }}
  • {% endfor %}
From 0d6a3d3b28d72a5420b976ed17dff083736cb7da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:24:08 +1000 Subject: [PATCH 07/14] BOM table now refreshes the table rather than the page --- InvenTree/InvenTree/static/script/inventree/bom.js | 4 +++- InvenTree/part/models.py | 1 - InvenTree/stock/models.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 1e66af5c7e..aa64a776f5 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -330,7 +330,9 @@ function loadBomTable(table, options) { }, { method: 'PATCH', - reloadOnSuccess: true + success: function() { + reloadBomTable(table); + } } ); }); diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7cf6776b1d..e9cae5142b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -81,7 +81,6 @@ class PartCategory(InvenTreeTree): return query - @property def item_count(self): return self.partcount() diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 6c4571d5a3..e9220cabcb 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -76,7 +76,6 @@ class StockLocation(InvenTreeTree): """ return self.stock_item_count(cascade) > 0 - @property def item_count(self): """ Simply returns the number of stock items in this location. From a5189b8f3fd4ec2842fc561ccc936ce0b0b4af6d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:28:40 +1000 Subject: [PATCH 08/14] Replace a recursive function --- InvenTree/part/models.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e9cae5142b..73cfa96e2c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -382,13 +382,11 @@ class Part(models.Model): return self.default_location elif self.category: # Traverse up the category tree until we find a default location - cat = self.category + cats = self.category.get_ancestors(ascending=True, include_self=True) - while cat: - if cat.default_location: + for cat in cats: + if cat.defaul_location: return cat.default_location - else: - cat = cat.parent # Default case - no default category found return None From dac61eafa23d64ba37e53887d6e28979f7b2440a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:41:54 +1000 Subject: [PATCH 09/14] Fixed tests - Tree classes now need extra configuration in the fixture - Check for null pk when cleaning a tree node --- InvenTree/InvenTree/models.py | 4 +-- InvenTree/part/fixtures/category.yaml | 34 +++++++++++++++++++++++++- InvenTree/part/models.py | 2 +- InvenTree/part/test_category.py | 4 +-- InvenTree/stock/fixtures/location.yaml | 30 ++++++++++++++++++++++- InvenTree/stock/tests.py | 5 +++- 6 files changed, 71 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index d5b1cddc17..38539d0118 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -82,7 +82,7 @@ class InvenTreeTree(MPTTModel): @property def has_children(self): """ True if there are any children under this item """ - return self.getUniqueChildren().count() > 0 + return self.getUniqueChildren(include_self=False).count() > 0 def getAcceptableParents(self): """ Returns a list of acceptable parent items within this model @@ -157,7 +157,7 @@ class InvenTreeTree(MPTTModel): pass # Ensure that the new parent is not already a child - if self.id in self.getUniqueChildren(include_self=False): + if self.pk is not None and self.id in self.getUniqueChildren(include_self=False): raise ValidationError("Category cannot set a child as parent") def __str__(self): diff --git a/InvenTree/part/fixtures/category.yaml b/InvenTree/part/fixtures/category.yaml index 2f46dcce65..ad40b6b1fd 100644 --- a/InvenTree/part/fixtures/category.yaml +++ b/InvenTree/part/fixtures/category.yaml @@ -6,7 +6,11 @@ name: Electronics description: Electronic components parent: null - default_location: 1 # Home + default_location: 1 + level: 0 + tree_id: 1 + lft: 1 + rght: 12 - model: part.partcategory pk: 2 @@ -15,6 +19,10 @@ description: Resistors parent: 1 default_location: null + level: 1 + tree_id: 1 + lft: 2 + rght: 3 - model: part.partcategory pk: 3 @@ -23,6 +31,10 @@ description: Capacitors parent: 1 default_location: null + level: 1 + tree_id: 1 + lft: 4 + rght: 5 - model: part.partcategory pk: 4 @@ -31,6 +43,10 @@ description: Integrated Circuits parent: 1 default_location: null + level: 1 + tree_id: 1 + lft: 6 + rght: 11 - model: part.partcategory pk: 5 @@ -39,6 +55,10 @@ description: Microcontrollers parent: 4 default_location: null + level: 2 + tree_id: 1 + lft: 7 + rght: 8 - model: part.partcategory pk: 6 @@ -47,6 +67,10 @@ description: Communication interfaces parent: 4 default_location: null + level: 2 + tree_id: 1 + lft: 9 + rght: 10 - model: part.partcategory pk: 7 @@ -54,6 +78,10 @@ name: Mechanical description: Mechanical componenets default_location: null + level: 0 + tree_id: 2 + lft: 1 + rght: 4 - model: part.partcategory pk: 8 @@ -62,3 +90,7 @@ description: Screws, bolts, etc parent: 7 default_location: 5 + level: 1 + tree_id: 2 + lft: 2 + rght: 3 diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 73cfa96e2c..1831962a44 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -385,7 +385,7 @@ class Part(models.Model): cats = self.category.get_ancestors(ascending=True, include_self=True) for cat in cats: - if cat.defaul_location: + if cat.default_location: return cat.default_location # Default case - no default category found diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index d9bb42d546..ef8060afd0 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -48,7 +48,7 @@ class CategoryTest(TestCase): def test_unique_childs(self): """ Test the 'unique_children' functionality """ - childs = self.electronics.getUniqueChildren() + childs = [item.pk for item in self.electronics.getUniqueChildren()] self.assertIn(self.transceivers.id, childs) self.assertIn(self.ic.id, childs) @@ -58,7 +58,7 @@ class CategoryTest(TestCase): def test_unique_parents(self): """ Test the 'unique_parents' functionality """ - parents = self.transceivers.getUniqueParents() + parents = [item.pk for item in self.transceivers.getUniqueParents()] self.assertIn(self.electronics.id, parents) self.assertIn(self.ic.id, parents) diff --git a/InvenTree/stock/fixtures/location.yaml b/InvenTree/stock/fixtures/location.yaml index f34490af29..1d5d1300af 100644 --- a/InvenTree/stock/fixtures/location.yaml +++ b/InvenTree/stock/fixtures/location.yaml @@ -5,6 +5,10 @@ fields: name: 'Home' description: 'My house' + level: 0 + tree_id: 1 + lft: 1 + rght: 6 - model: stock.stocklocation pk: 2 @@ -12,6 +16,10 @@ name: 'Bathroom' description: 'Where I keep my bath' parent: 1 + level: 1 + tree_id: 1 + lft: 2 + rght: 3 - model: stock.stocklocation pk: 3 @@ -19,12 +27,20 @@ name: 'Dining Room' description: 'A table lives here' parent: 1 + level: 0 + tree_id: 1 + lft: 4 + rght: 5 - model: stock.stocklocation pk: 4 fields: name: 'Office' description: 'Place of work' + level: 0 + tree_id: 2 + lft: 1 + rght: 8 - model: stock.stocklocation pk: 5 @@ -32,6 +48,10 @@ name: 'Drawer_1' description: 'In my desk' parent: 4 + level: 0 + tree_id: 2 + lft: 2 + rght: 3 - model: stock.stocklocation pk: 6 @@ -39,10 +59,18 @@ name: 'Drawer_2' description: 'Also in my desk' parent: 4 + level: 0 + tree_id: 2 + lft: 4 + rght: 5 - model: stock.stocklocation pk: 7 fields: name: 'Drawer_3' description: 'Again, in my desk' - parent: 4 \ No newline at end of file + parent: 4 + level: 0 + tree_id: 2 + lft: 6 + rght: 7 \ No newline at end of file diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 437464a77a..95eb1002ad 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -67,15 +67,18 @@ class StockTest(TestCase): # Move one of the drawers self.drawer3.parent = self.home + self.drawer3.save() + self.assertNotEqual(self.drawer3.parent, self.office) self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3') def test_children(self): self.assertTrue(self.office.has_children) + self.assertFalse(self.drawer2.has_children) - childs = self.office.getUniqueChildren() + childs = [item.pk for item in self.office.getUniqueChildren()] self.assertIn(self.drawer1.id, childs) self.assertIn(self.drawer2.id, childs) From 35ebc6923538d4f795e8b812640bb8d1edf62124 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 19:43:10 +1000 Subject: [PATCH 10/14] Backup database as part of the migration process --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7ea3f33b03..91a604ebd9 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ clean: rm -f .coverage # Perform database migrations (after schema changes are made) -migrate: +migrate: backup python3 InvenTree/manage.py makemigrations common python3 InvenTree/manage.py makemigrations company python3 InvenTree/manage.py makemigrations part From 2a203be5ccbb535e0c53bd7bb8f1c61c7bb8fbaf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 20:18:21 +1000 Subject: [PATCH 11/14] Tests for part parameters --- InvenTree/InvenTree/settings.py | 4 +-- InvenTree/part/fixtures/params.yaml | 32 ++++++++++++++++++++++ InvenTree/part/models.py | 12 +++------ InvenTree/part/test_param.py | 42 +++++++++++++++++++++++++++++ InvenTree/part/test_part.py | 5 ++++ 5 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 InvenTree/part/fixtures/params.yaml create mode 100644 InvenTree/part/test_param.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 476be9aa28..2f5572a5f8 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -58,9 +58,7 @@ cors_opt = CONFIG.get('cors', None) if cors_opt: CORS_ORIGIN_ALLOW_ALL = cors_opt.get('allow_all', False) - if CORS_ORIGIN_ALLOW_ALL: - eprint("Warning: CORS requests are allowed for any domain!") - else: + if not CORS_ORIGIN_ALLOW_ALL: CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', []) if DEBUG: diff --git a/InvenTree/part/fixtures/params.yaml b/InvenTree/part/fixtures/params.yaml new file mode 100644 index 0000000000..4827d0987b --- /dev/null +++ b/InvenTree/part/fixtures/params.yaml @@ -0,0 +1,32 @@ +# Create some PartParameter templtes + +- model: part.PartParameterTemplate + pk: 1 + fields: + name: Length + units: mm + +- model: part.PartParameterTemplate + pk: 2 + fields: + name: Width + units: mm + +- model: part.PartParameterTemplate + pk: 3 + fields: + name: Thickness + units: mm + +# And some parameters (requires part.yaml) +- model: part.PartParameter + fields: + part: 1 + template: 1 + data: 4 + +- model: part.PartParameter + fields: + part: 2 + template: 1 + data: 12 \ No newline at end of file diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1831962a44..7a1a31a02a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1065,7 +1065,7 @@ class PartParameterTemplate(models.Model): super().validate_unique(exclude) try: - others = PartParameterTemplate.objects.exclude(id=self.id).filter(name__iexact=self.name) + others = PartParameterTemplate.objects.filter(name__iexact=self.name).exclude(pk=self.pk) if others.exists(): msg = _("Parameter template name must be unique") @@ -1073,11 +1073,6 @@ class PartParameterTemplate(models.Model): except PartParameterTemplate.DoesNotExist: pass - @property - def instance_count(self): - """ Return the number of instances of this Parameter Template """ - return self.instances.count() - name = models.CharField(max_length=100, help_text='Parameter Name', unique=True) units = models.CharField(max_length=25, help_text='Parameter Units', blank=True) @@ -1096,7 +1091,7 @@ class PartParameter(models.Model): def __str__(self): # String representation of a PartParameter (used in the admin interface) return "{part} : {param} = {data}{units}".format( - part=str(self.part), + part=str(self.part.full_name), param=str(self.template.name), data=str(self.data), units=str(self.template.units) @@ -1106,8 +1101,7 @@ class PartParameter(models.Model): # Prevent multiple instances of a parameter for a single part unique_together = ('part', 'template') - part = models.ForeignKey(Part, on_delete=models.CASCADE, - related_name='parameters', help_text='Parent Part') + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters', help_text='Parent Part') template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE, related_name='instances', help_text='Parameter Template') diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py new file mode 100644 index 0000000000..6876a2b5df --- /dev/null +++ b/InvenTree/part/test_param.py @@ -0,0 +1,42 @@ +# Tests for Part Parameters + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase +import django.core.exceptions as django_exceptions + +from .models import PartParameter, PartParameterTemplate + + +class TestParams(TestCase): + + fixtures = [ + 'location', + 'category', + 'part', + 'params' + ] + + def test_str(self): + + t1 = PartParameterTemplate.objects.get(pk=1) + self.assertEquals(str(t1), 'Length (mm)') + + p1 = PartParameter.objects.get(pk=1) + self.assertEqual(str(p1), "M2x4 LPHS : Length = 4mm") + + def test_validate(self): + + n = PartParameterTemplate.objects.all().count() + + t1 = PartParameterTemplate(name='abcde', units='dd') + t1.save() + + self.assertEqual(n + 1, PartParameterTemplate.objects.all().count()) + + # Test that the case-insensitive name throws a ValidationError + with self.assertRaises(django_exceptions.ValidationError): + t3 = PartParameterTemplate(name='aBcde', units='dd') + t3.full_clean() + t3.save() diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index c9028cd406..e6ce9168ee 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -1,3 +1,8 @@ +# Tests for the Part model + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.test import TestCase import os From e4fc44c135dd7d099fd1b8554cd10b4513eb2d6f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 20:36:51 +1000 Subject: [PATCH 12/14] More test --- InvenTree/part/fixtures/part.yaml | 6 +++++- InvenTree/part/models.py | 14 +++----------- InvenTree/part/test_bom_item.py | 2 +- InvenTree/part/test_category.py | 6 ++++++ InvenTree/part/test_part.py | 13 +++++++++++-- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index 77b96a3471..8f4963cbad 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -59,4 +59,8 @@ name: 'Bob' description: 'Can we build it?' assembly: true - purchaseable: false \ No newline at end of file + purchaseable: false + category: 7 + active: False + IPN: BOB + revision: A2 \ No newline at end of file diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7a1a31a02a..f57da3cfdb 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -265,17 +265,9 @@ class Part(models.Model): def set_category(self, category): - if not type(category) == PartCategory: - raise ValidationError({ - 'category': _('Invalid object supplied to part.set_category') - }) - - try: - # Already in this category! - if category == self.category: - return - except PartCategory.DoesNotExist: - pass + # Ignore if the category is already the same + if self.category == category: + return self.category = category self.save() diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index 6c1d91810c..ec29dbaab3 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -19,7 +19,7 @@ class BomItemTest(TestCase): def test_str(self): b = BomItem.objects.get(id=1) - self.assertEqual(str(b), '10 x M2x4 LPHS to make Bob') + self.assertEqual(str(b), '10 x M2x4 LPHS to make BOB | Bob | A2') def test_has_bom(self): self.assertFalse(self.orphan.has_bom) diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index ef8060afd0..db95836107 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -87,6 +87,12 @@ class CategoryTest(TestCase): self.assertEqual(self.electronics.partcount(), 3) + self.assertEqual(self.mechanical.partcount(), 4) + self.assertEqual(self.mechanical.partcount(active=True), 3) + self.assertEqual(self.mechanical.partcount(False), 2) + + self.assertEqual(self.electronics.item_count, self.electronics.partcount()) + def test_delete(self): """ Test that category deletion moves the children properly """ diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index e6ce9168ee..84d9900aff 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -8,7 +8,7 @@ from django.test import TestCase import os from .models import Part -from .models import rename_part_image +from .models import rename_part_image, match_part_names from .templatetags import inventree_extras @@ -44,6 +44,10 @@ class PartTest(TestCase): self.C1 = Part.objects.get(name='C_22N_0805') + def test_str(self): + p = Part.objects.get(pk=100) + self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it?") + def test_metadata(self): self.assertEqual(self.R1.name, 'R_2K2_0805') self.assertEqual(self.R1.get_absolute_url(), '/part/3/') @@ -75,5 +79,10 @@ class PartTest(TestCase): self.assertIn(self.R1.name, barcode) def test_copy(self): - self.R2.deepCopy(self.R1, image=True, bom=True) + + def test_match_names(self): + + matches = match_part_names('M2x5 LPHS') + + self.assertTrue(len(matches) > 0) From d7f969613ef4f12a8d22f8bddd742be8762ca6a6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 20:45:01 +1000 Subject: [PATCH 13/14] Update makefile and docs --- Makefile | 6 ++++-- docs/update.rst | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 91a604ebd9..38446635da 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,10 @@ clean: rm -rf .tox rm -f .coverage +update: backup migrate + # Perform database migrations (after schema changes are made) -migrate: backup +migrate: python3 InvenTree/manage.py makemigrations common python3 InvenTree/manage.py makemigrations company python3 InvenTree/manage.py makemigrations part @@ -64,4 +66,4 @@ backup: python3 InvenTree/manage.py dbbackup python3 InvenTree/manage.py mediabackup -.PHONY: clean migrate superuser install mysql style test coverage docreqs docs backup \ No newline at end of file +.PHONY: clean migrate superuser install mysql style test coverage docreqs docs backup update \ No newline at end of file diff --git a/docs/update.rst b/docs/update.rst index d6ef6a5913..d3266b4126 100644 --- a/docs/update.rst +++ b/docs/update.rst @@ -29,7 +29,13 @@ Perform Migrations Updating the database is as simple as calling the makefile target: -``make migrate`` +``make update`` + +This command performs the following steps: + +* Backup database entries and uploaded media files +* Perform required database schema changes +* Collect required static files Restart Server -------------- From 026108803ed5728d0b831a2b36e0d85c4e287aaa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 20:48:33 +1000 Subject: [PATCH 14/14] More links in the about page --- InvenTree/templates/about.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index da5b4ddc5b..aef286f6cf 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -17,10 +17,10 @@

InvenTree Version Information

- + - +
Version{% inventree_version %}Version{% inventree_version %}
Commit Hash{% inventree_commit %}Commit Hash{% inventree_commit %}