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 04/11] 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 05/11] 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 06/11] 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' %}
+
+
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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 068c237c6ed9e2ed38a4aa87fafa91c3885f64fa Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Tue, 18 Feb 2020 23:59:37 +1100
Subject: [PATCH 11/11] 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)