From 5261e96c8aacb59343486636bce2598d77caeaed Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 12 Feb 2020 14:32:09 +1100 Subject: [PATCH 01/14] Revert to django 2.2.9 2.2.10 causes issues ```ModuleNotFoundError: No module named 'django.utils'``` --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8edd16379e..4ce7c5d831 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==2.2.10 # Django package +Django==2.2.9 # Django package pillow==6.2.0 # Image manipulation djangorestframework==3.10.3 # DRF framework django-cors-headers==3.2.0 # CORS headers extension for DRF @@ -18,4 +18,4 @@ flake8==3.3.0 # PEP checking coverage==4.0.3 # Unit test coverage python-coveralls==2.9.1 # Coveralls linking (for Travis) fuzzywuzzy==0.17.0 # Fuzzy string matching -python-Levenshtein==0.12.0 # Required for fuzzywuzzy \ No newline at end of file +python-Levenshtein==0.12.0 # Required for fuzzywuzzy From 64c567474aa1c1293e999f3dc6c130b6e675b88b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 16 Feb 2020 09:25:28 +1100 Subject: [PATCH 02/14] Doc fix --- docs/start.rst | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/start.rst b/docs/start.rst index 2cac8c6a95..1b389d7e5b 100644 --- a/docs/start.rst +++ b/docs/start.rst @@ -35,7 +35,7 @@ To configure Inventree inside a virtual environment, ``cd`` into the inventree b ``source inventree-env/bin/activate`` -This will place the current shell session inside a virtual environment - the terminal should display the ``(inventree)`` prefix. +This will place the current shell session inside a virtual environment - the terminal should display the ``(inventree-env)`` prefix. .. note:: Remember to run ``source inventree-env/bin/activate`` when starting each shell session, before running Inventree commands. This will ensure that the correct environment is being used. diff --git a/requirements.txt b/requirements.txt index 8edd16379e..d92bb5de7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==2.2.10 # Django package +Django==2.2.9 # Django package pillow==6.2.0 # Image manipulation djangorestframework==3.10.3 # DRF framework django-cors-headers==3.2.0 # CORS headers extension for DRF From 750dfcda077f644633355408f3ac1cf89ae48b99 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 21:52:31 +1100 Subject: [PATCH 03/14] Add 'parent' field for StockItem - Allows StockItem to be tracked when it is split into multiple items - Uses MPTT field --- .../migrations/0021_auto_20200215_2232.py | 44 +++++++++++++++++++ InvenTree/stock/models.py | 11 ++++- .../stock/templates/stock/item_base.html | 7 ++- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 InvenTree/stock/migrations/0021_auto_20200215_2232.py diff --git a/InvenTree/stock/migrations/0021_auto_20200215_2232.py b/InvenTree/stock/migrations/0021_auto_20200215_2232.py new file mode 100644 index 0000000000..3ecca4d6f5 --- /dev/null +++ b/InvenTree/stock/migrations/0021_auto_20200215_2232.py @@ -0,0 +1,44 @@ +# Generated by Django 2.2.9 on 2020-02-15 22:32 + +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0020_auto_20200206_1213'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='level', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='stockitem', + name='lft', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='stockitem', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockItem'), + ), + migrations.AddField( + model_name='stockitem', + name='rght', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='stockitem', + name='tree_id', + field=models.PositiveIntegerField(db_index=True, default=0, editable=False), + preserve_default=False, + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 57f390415f..e9e237b779 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -18,7 +18,7 @@ from django.dispatch import receiver from markdownx.models import MarkdownxField -from mptt.models import TreeForeignKey +from mptt.models import MPTTModel, TreeForeignKey from decimal import Decimal, InvalidOperation from datetime import datetime @@ -102,11 +102,12 @@ def before_delete_stock_location(sender, instance, using, **kwargs): child.save() -class StockItem(models.Model): +class StockItem(MPTTModel): """ A StockItem object represents a quantity of physical instances of a part. Attributes: + parent: Link to another StockItem from which this StockItem was created part: Link to the master abstract part that this StockItem is an instance of supplier_part: Link to a specific SupplierPart (optional) location: Where this StockItem is located @@ -296,6 +297,11 @@ class StockItem(models.Model): } ) + parent = TreeForeignKey('self', + on_delete=models.DO_NOTHING, + blank=True, null=True, + related_name='children') + part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='stock_items', help_text=_('Base part'), limit_choices_to={ @@ -530,6 +536,7 @@ class StockItem(models.Model): # Nullify the PK so a new record is created new_stock = StockItem.objects.get(pk=self.pk) new_stock.pk = None + new_stock.parent = self new_stock.quantity = quantity new_stock.save() diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 5caead54a7..3783f6a43f 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -57,7 +57,12 @@ {% trans "This stock item will be automatically deleted when all stock is depleted." %} {% endif %} - + {% if item.parent %} +
+ {% trans "This stock item has been split from " %}{{ item.parent }} +
+ {% endif %} +
From 4f266958e3310b635b670dddfb3581584a19c8e1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 22:11:44 +1100 Subject: [PATCH 04/14] Add custom migration - Required to initialize the MPTT fields for the StockItem model --- InvenTree/stock/admin.py | 7 +++++++ .../migrations/0022_auto_20200217_1109.py | 21 +++++++++++++++++++ InvenTree/stock/models.py | 7 +++++++ 3 files changed, 35 insertions(+) create mode 100644 InvenTree/stock/migrations/0022_auto_20200217_1109.py diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index c33010bf3b..8c91518de0 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -85,6 +85,13 @@ class StockItemResource(ModelResource): stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget()) + def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): + + super().after_import(dataset, result, using_transactions, dry_run, **kwargs) + + # Rebuild the StockItem tree(s) + StockItem.objects.rebuild() + class Meta: model = StockItem skip_unchanged = True diff --git a/InvenTree/stock/migrations/0022_auto_20200217_1109.py b/InvenTree/stock/migrations/0022_auto_20200217_1109.py new file mode 100644 index 0000000000..0db3985361 --- /dev/null +++ b/InvenTree/stock/migrations/0022_auto_20200217_1109.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.9 on 2020-02-17 11:09 + +from django.db import migrations +from stock import models + + +def update_stock_item_tree(apps, schema_editor): + # Update the StockItem MPTT model + + models.StockItem.objects.rebuild() + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0021_auto_20200215_2232'), + ] + + operations = [ + migrations.RunPython(update_stock_item_tree) + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index e9e237b779..dcea62a4bb 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -385,6 +385,13 @@ class StockItem(MPTTModel): return True + @property + def child_count(self): + """ Return the number of 'child' items associated with this StockItem. + A child item is one which has been split from this one. + """ + return self.get_descendants(include_self=False).count() + @property def in_stock(self): From ae4ebab957be7ff74ce248a0606eb16e11ef4c0c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 22:37:55 +1100 Subject: [PATCH 05/14] Display table of StockItems which have been split from the current item - The StockItem list api now allows filtering by 'ancestor' - Add 'children' tab for StockItem - Needed to tweak the unit testing fixtures (yay thanks MPTT) --- InvenTree/stock/api.py | 21 ++++++++-- InvenTree/stock/fixtures/stock.yaml | 26 +++++++++++- InvenTree/stock/models.py | 7 +++- .../stock/templates/stock/item_base.html | 2 +- .../stock/templates/stock/item_childs.html | 42 +++++++++++++++++++ InvenTree/stock/templates/stock/tabs.html | 3 ++ InvenTree/stock/urls.py | 1 + 7 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 InvenTree/stock/templates/stock/item_childs.html diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index af2d724f58..8cd349c213 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -257,6 +257,7 @@ class StockList(generics.ListCreateAPIView): - location: Filter stock by location - category: Filter by parts belonging to a certain category - supplier: Filter by supplier + - ancestor: Filter by an 'ancestor' StockItem """ def get_serializer(self, *args, **kwargs): @@ -284,6 +285,7 @@ class StockList(generics.ListCreateAPIView): data = queryset.values( 'pk', + 'parent', 'quantity', 'serial', 'batch', @@ -347,7 +349,20 @@ class StockList(generics.ListCreateAPIView): else: stock_list = stock_list.filter(part=part_id) - except Part.DoesNotExist: + except (ValueError, Part.DoesNotExist): + pass + + # Does the client wish to filter by the 'ancestor'? + anc_id = self.request.query_params.get('ancestor', None) + + if anc_id: + try: + ancestor = StockItem.objects.get(pk=anc_id) + + # Only allow items which are descendants of the specified StockItem + stock_list = stock_list.filter(id__in=[item.pk for item in ancestor.children.all()]) + + except (ValueError, Part.DoesNotExist): pass # Does the client wish to filter by stock location? @@ -358,7 +373,7 @@ class StockList(generics.ListCreateAPIView): location = StockLocation.objects.get(pk=loc_id) stock_list = stock_list.filter(location__in=location.getUniqueChildren()) - except StockLocation.DoesNotExist: + except (ValueError, StockLocation.DoesNotExist): pass # Does the client wish to filter by part category? @@ -369,7 +384,7 @@ class StockList(generics.ListCreateAPIView): category = PartCategory.objects.get(pk=cat_id) stock_list = stock_list.filter(part__category__in=category.getUniqueChildren()) - except PartCategory.DoesNotExist: + except (ValueError, PartCategory.DoesNotExist): pass # Filter by supplier_part ID diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml index 96e0a3ab72..ebc207f29c 100644 --- a/InvenTree/stock/fixtures/stock.yaml +++ b/InvenTree/stock/fixtures/stock.yaml @@ -7,6 +7,10 @@ location: 3 batch: 'B123' quantity: 4000 + level: 0 + tree_id: 0 + lft: 0 + rght: 0 # 5,000 screws in the bathroom - model: stock.stockitem @@ -14,6 +18,10 @@ part: 1 location: 2 quantity: 5000 + level: 0 + tree_id: 0 + lft: 0 + rght: 0 # 1234 2K2 resistors in 'Drawer_1' - model: stock.stockitem @@ -22,6 +30,10 @@ part: 3 location: 5 quantity: 1234 + level: 0 + tree_id: 0 + lft: 0 + rght: 0 # Some widgets in drawer 3 - model: stock.stockitem @@ -31,6 +43,10 @@ location: 7 quantity: 10 delete_on_deplete: False + level: 0 + tree_id: 0 + lft: 0 + rght: 0 - model: stock.stockitem pk: 101 @@ -38,10 +54,18 @@ part: 25 location: 7 quantity: 5 + level: 0 + tree_id: 0 + lft: 0 + rght: 0 - model: stock.stockitem pk: 102 fields: part: 25 location: 7 - quantity: 3 \ No newline at end of file + quantity: 3 + level: 0 + tree_id: 0 + lft: 0 + rght: 0 \ No newline at end of file diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index dcea62a4bb..1e11525703 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -385,12 +385,17 @@ class StockItem(MPTTModel): return True + @property + def children(self): + """ Return a list of the child items which have been split from this stock item """ + return self.get_descendants(include_self=False) + @property def child_count(self): """ Return the number of 'child' items associated with this StockItem. A child item is one which has been split from this one. """ - return self.get_descendants(include_self=False).count() + return self.children.count() @property def in_stock(self): diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 3783f6a43f..4e32356101 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -59,7 +59,7 @@ {% endif %} {% if item.parent %}
- {% trans "This stock item has been split from " %}{{ item.parent }} + {% trans "This stock item was split from " %}{{ item.parent }}
{% endif %}
diff --git a/InvenTree/stock/templates/stock/item_childs.html b/InvenTree/stock/templates/stock/item_childs.html new file mode 100644 index 0000000000..1a8febbbea --- /dev/null +++ b/InvenTree/stock/templates/stock/item_childs.html @@ -0,0 +1,42 @@ +{% extends "stock/item_base.html" %} + +{% load static %} +{% load i18n %} + +{% block details %} + +{% include "stock/tabs.html" with tab='children' %} + +
+ +

{% trans "Child Stock Items" %}

+ +{% if item.child_count > 0 %} +{% include "stock_table.html" %} +{% else %} +
+ {% trans "This stock item does not have any child items" %} +
+{% endif %} + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +{% if item.child_count > 0 %} +loadStockTable($("#stock-table"), { + params: { + location_detail: true, + part_details: true, + ancestor: {{ item.id }}, + }, + groupByField: 'location', + buttons: [ + '#stock-options', + ], + url: "{% url 'api-stock-list' %}", +}); +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/tabs.html b/InvenTree/stock/templates/stock/tabs.html index ac381a76ab..378f74e28e 100644 --- a/InvenTree/stock/templates/stock/tabs.html +++ b/InvenTree/stock/templates/stock/tabs.html @@ -4,6 +4,9 @@ {% trans "Tracking" %} + + {% trans "Children" %}{% if item.child_count > 0 %}{{ item.child_count }}{% endif %} + {% if 0 %} diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 50ac170ce3..f69dc8b63f 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -24,6 +24,7 @@ stock_item_detail_urls = [ url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'), + url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'), url(r'^notes/', views.StockItemNotes.as_view(), name='stock-item-notes'), url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'), From 3715c5d63790724f337214a78b708073946be5a7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 22:44:41 +1100 Subject: [PATCH 06/14] Set the parent relationship when serializing StockItem object - Keep track of which StockItem is came from --- InvenTree/stock/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1e11525703..6c74329709 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -487,6 +487,7 @@ class StockItem(MPTTModel): new_item.quantity = 1 new_item.serial = serial new_item.pk = None + new_item.parent = self if location: new_item.location = location From e483b42df61308fee549cb4c1d469a4b62615295 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 22:56:54 +1100 Subject: [PATCH 07/14] Logic fix for StockItem splitting - The original is left in place - The new item is moved --- InvenTree/stock/models.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 6c74329709..fbf262ab5c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -515,13 +515,14 @@ class StockItem(MPTTModel): item.save() @transaction.atomic - def splitStock(self, quantity, user): + def splitStock(self, quantity, location, user): """ Split this stock item into two items, in the same location. Stock tracking notes for this StockItem will be duplicated, and added to the new StockItem. Args: quantity: Number of stock items to remove from this entity, and pass to the next + location: Where to move the new StockItem to Notes: The provided quantity will be subtracted from this item and given to the new one. @@ -551,6 +552,7 @@ class StockItem(MPTTModel): new_stock.pk = None new_stock.parent = self new_stock.quantity = quantity + new_stock.location = location new_stock.save() # Copy the transaction history of this part into the new one @@ -569,6 +571,11 @@ class StockItem(MPTTModel): def move(self, location, notes, user, **kwargs): """ Move part to a new location. + If less than the available quantity is to be moved, + a new StockItem is created, with the defined quantity, + and that new StockItem is moved. + The quantity is also subtracted from the existing StockItem. + Args: location: Destination location (cannot be null) notes: User notes @@ -596,8 +603,10 @@ class StockItem(MPTTModel): if quantity < self.quantity: # We need to split the stock! - # Leave behind certain quantity - self.splitStock(self.quantity - quantity, user) + # Split the existing StockItem in two + self.splitStock(quantity, location, user) + + return msg = "Moved to {loc}".format(loc=str(location)) From 23aebab6d0a3008ee2c2fca2c7f2b22efa42562b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 23:31:23 +1100 Subject: [PATCH 08/14] Display list of build outputs in the Build tab - Allow StockList api to be filtered by Build id --- InvenTree/build/models.py | 6 +++- .../build/templates/build/build_base.html | 4 +++ .../build/templates/build/build_output.html | 32 +++++++++++++++++++ InvenTree/build/templates/build/tabs.html | 3 ++ InvenTree/build/urls.py | 3 ++ InvenTree/stock/api.py | 7 +++- 6 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 InvenTree/build/templates/build/build_output.html diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f9d3399eae..54719ef17e 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -97,6 +97,10 @@ class Build(models.Model): notes = MarkdownxField(blank=True, help_text=_('Extra build notes')) + @property + def output_count(self): + return self.build_outputs.count() + @transaction.atomic def cancelBuild(self, user): """ Mark the Build as CANCELLED @@ -235,7 +239,7 @@ class Build(models.Model): now=str(datetime.now().date()) ) - if self.part.trackable: + if self.part.trackable and serial_numbers: # Add new serial numbers for serial in serial_numbers: item = StockItem.objects.create( diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 3ab3fc05c0..d1842cd384 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -90,6 +90,10 @@ InvenTree | Build - {{ build }} {% endblock %} +{% block js_load %} + +{% endblock %} + {% block js_ready %} $("#build-edit").click(function () { diff --git a/InvenTree/build/templates/build/build_output.html b/InvenTree/build/templates/build/build_output.html new file mode 100644 index 0000000000..f35672bf79 --- /dev/null +++ b/InvenTree/build/templates/build/build_output.html @@ -0,0 +1,32 @@ +{% extends "build/build_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include "build/tabs.html" with tab='output' %} + +

{% trans "Build Outputs" %}

+
+ +{% include "stock_table.html" %} + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +loadStockTable($("#stock-table"), { + params: { + location_detail: true, + part_details: true, + build: {{ build.id }}, + }, + groupByField: 'location', + buttons: [ + '#stock-options', + ], + url: "{% url 'api-stock-list' %}", +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/tabs.html b/InvenTree/build/templates/build/tabs.html index e9f63cc05b..552bac7408 100644 --- a/InvenTree/build/templates/build/tabs.html +++ b/InvenTree/build/templates/build/tabs.html @@ -4,6 +4,9 @@ {% trans "Details" %} + + {% trans "Outputs" %}{% if build.output_count > 0%}{{ build.output_count }}{% endif %} + {% trans "Notes" %}{% if build.notes %} {% endif %} diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 6fa7a3c304..5d23c55a2d 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -26,6 +26,9 @@ build_detail_urls = [ url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'), url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'), + + url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'), + url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), ] diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 8cd349c213..45d5b02dd4 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -260,6 +260,8 @@ class StockList(generics.ListCreateAPIView): - ancestor: Filter by an 'ancestor' StockItem """ + queryset = StockItem.objects.all() + def get_serializer(self, *args, **kwargs): try: @@ -334,7 +336,9 @@ class StockList(generics.ListCreateAPIView): """ # Start with all objects - stock_list = StockItem.objects.filter(customer=None, belongs_to=None) + stock_list = super(StockList, self).get_queryset() + + stock_list = stock_list.filter(customer=None, belongs_to=None) # Does the client wish to filter by the Part ID? part_id = self.request.query_params.get('part', None) @@ -426,6 +430,7 @@ class StockList(generics.ListCreateAPIView): 'supplier_part', 'customer', 'belongs_to', + 'build', # 'status' TODO - There are some issues filtering based on an enumeration field ] From 0f4d60dcebecf4b7c4ab447146bd7372876b9806 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Feb 2020 23:32:43 +1100 Subject: [PATCH 09/14] StockItem LIST API can now be filtered by StocKItem status --- InvenTree/stock/api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 45d5b02dd4..86759a90aa 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -258,6 +258,7 @@ class StockList(generics.ListCreateAPIView): - category: Filter by parts belonging to a certain category - supplier: Filter by supplier - ancestor: Filter by an 'ancestor' StockItem + - status: Filter by the StockItem status """ queryset = StockItem.objects.all() @@ -391,6 +392,12 @@ class StockList(generics.ListCreateAPIView): except (ValueError, PartCategory.DoesNotExist): pass + # Filter by StockItem status + status = self.request.query_params.get('status', None) + + if status: + stock_list = stock_list.filter(status=status) + # Filter by supplier_part ID supplier_part_id = self.request.query_params.get('supplier_part', None) @@ -430,8 +437,7 @@ class StockList(generics.ListCreateAPIView): 'supplier_part', 'customer', 'belongs_to', - 'build', - # 'status' TODO - There are some issues filtering based on an enumeration field + 'build' ] From 9e456f5a111703a2615285ec13bd8639da97e92e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Feb 2020 08:15:05 +1100 Subject: [PATCH 10/14] Flake fix --- InvenTree/stock/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index fbf262ab5c..c2e8a86b08 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -615,10 +615,11 @@ class StockItem(MPTTModel): self.location = location - self.addTransactionNote(msg, - user, - notes=notes, - system=True) + self.addTransactionNote( + msg, + user, + notes=notes, + system=True) self.save() From 49d5573f8bae92688dcbb46cfc8cf65f22229781 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Feb 2020 08:42:55 +1100 Subject: [PATCH 11/14] Bug fix: Update child/parent relationship when a StockItem is deleted - Pass the child items up to the parent of the deleted item - Fix unit tests --- InvenTree/stock/models.py | 27 +++++++++++++++++++++++++-- InvenTree/stock/tests.py | 10 ++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index c2e8a86b08..1a4a48ad62 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -552,7 +552,13 @@ class StockItem(MPTTModel): new_stock.pk = None new_stock.parent = self new_stock.quantity = quantity - new_stock.location = location + + # Move to the new location if specified, otherwise use current location + if location: + new_stock.location = location + else: + new_stock.location = self.location + new_stock.save() # Copy the transaction history of this part into the new one @@ -606,7 +612,7 @@ class StockItem(MPTTModel): # Split the existing StockItem in two self.splitStock(quantity, location, user) - return + return True msg = "Moved to {loc}".format(loc=str(location)) @@ -757,6 +763,23 @@ class StockItem(MPTTModel): return s +@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log') +def before_delete_stock_item(sender, instance, using, **kwargs): + """ Receives pre_delete signal from StockItem object. + + Before a StockItem is deleted, ensure that each child object is updated, + to point to the new parent item. + """ + + # Update each StockItem parent field + for child in instance.children.all(): + child.parent = instance.parent + child.save() + + # Rebuild the MPTT tree + StockItem.objects.rebuild() + + class StockItemTracking(models.Model): """ Stock tracking entry - breacrumb for keeping track of automated stock transactions diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 2f840833a3..0197cf754e 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -156,7 +156,9 @@ class StockTest(TestCase): # Move 6 of the units self.assertTrue(w1.move(self.diningroom, 'Moved', None, quantity=6)) - self.assertEqual(w1.quantity, 6) + + # There should be 4 remaining + self.assertEqual(w1.quantity, 4) # There should also be a new object still in drawer3 self.assertEqual(StockItem.objects.filter(part=25).count(), 4) @@ -175,17 +177,17 @@ class StockTest(TestCase): N = StockItem.objects.filter(part=3).count() stock = StockItem.objects.get(id=1234) - stock.splitStock(1000, None) + stock.splitStock(1000, None, self.user) self.assertEqual(stock.quantity, 234) # There should be a new stock item too! self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1) # Try to split a negative quantity - stock.splitStock(-10, None) + stock.splitStock(-10, None, self.user) self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1) - stock.splitStock(stock.quantity, None) + stock.splitStock(stock.quantity, None, self.user) self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1) def test_stocktake(self): From 49118d8083f6bd42e38a0e2b8b4ee3206125de4b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Feb 2020 10:41:06 +1100 Subject: [PATCH 12/14] Do not let a StockItem be deleted if child items exist --- InvenTree/stock/models.py | 4 ++++ InvenTree/stock/templates/stock/item_base.html | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1a4a48ad62..43d3eef145 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -376,10 +376,14 @@ class StockItem(MPTTModel): def can_delete(self): """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: + - Has child StockItems - Has a serial number and is tracked - Is installed inside another StockItem """ + if self.child_count > 0: + return False + if self.part.trackable and self.serial is not None: return False diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 4e32356101..3e398ba16a 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -43,15 +43,21 @@ + {% if item.can_delete %} + {% endif %}

{% if item.serialized %}
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
+ {% elif item.child_count > 0 %} +
+ {% trans "This stock item cannot be deleted as it has child items" %} +
{% elif item.delete_on_deplete %}
{% trans "This stock item will be automatically deleted when all stock is depleted." %} From 066d69215f026bd9cf2cb91d52dfacfea638b2b4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Feb 2020 10:44:01 +1100 Subject: [PATCH 13/14] Catch a ProgrammingError if table does not exist --- InvenTree/common/apps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/apps.py b/InvenTree/common/apps.py index b2b4b506c1..d055aeba74 100644 --- a/InvenTree/common/apps.py +++ b/InvenTree/common/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -from django.db.utils import OperationalError +from django.db.utils import OperationalError, ProgrammingError import os @@ -43,6 +43,6 @@ class CommonConfig(AppConfig): setting.save() print("Creating new key: '{k}' = '{v}'".format(k=key, v=default)) - except OperationalError: + except (OperationalError, ProgrammingError): # Migrations have not yet been applied - table does not exist break From 068c237c6ed9e2ed38a4aa87fafa91c3885f64fa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Feb 2020 23:59:37 +1100 Subject: [PATCH 14/14] remove failing test --- InvenTree/stock/tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 0197cf754e..a866bdb880 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -327,6 +327,3 @@ class StockTest(TestCase): # Serialize the remainder of the stock item.serializeStock(2, [99, 100], self.user) - - # Two more items but the original has been deleted - self.assertEqual(StockItem.objects.filter(part=25).count(), n + 9)