diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index de56ca34f7..57c31ff1db 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -27,6 +27,18 @@ class EditBuildForm(HelperForm):
]
+class ConfirmBuildForm(HelperForm):
+ """ Form for auto-allocation of stock to a build """
+
+ confirm = forms.BooleanField(required=False, help_text='Confirm')
+
+ class Meta:
+ model = Build
+ fields = [
+ 'confirm'
+ ]
+
+
class CompleteBuildForm(HelperForm):
""" Form for marking a Build as complete """
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index fedcb76403..0b658dee1a 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -127,10 +127,10 @@ class Build(models.Model):
- If there are multiple StockItems available, ignore (leave up to the user)
Returns:
- A dict object containing the StockItem objects to be allocated (and the quantities)
+ A list object containing the StockItem objects to be allocated (and the quantities)
"""
- allocations = {}
+ allocations = []
for item in self.part.bom_items.all():
@@ -151,19 +151,37 @@ class Build(models.Model):
# Are there any parts available?
if stock_item.quantity > 0:
+
# Only take as many as are available
if stock_item.quantity < q_required:
q_required = stock_item.quantity
- # Add the item to the allocations list
- allocations[stock_item] = q_required
+ allocation = {
+ 'stock_item': stock_item,
+ 'quantity': q_required,
+ }
+
+ allocations.append(allocation)
return allocations
+ @transaction.atomic
+ def unallocateStock(self):
+ """ Deletes all stock allocations for this build. """
+
+ BuildItem.objects.filter(build=self.id).delete()
+
@transaction.atomic
def autoAllocate(self):
""" Run auto-allocation routine to allocate StockItems to this Build.
+ Returns a list of dict objects with keys like:
+
+ {
+ 'stock_item': item,
+ 'quantity': quantity,
+ }
+
See: getAutoAllocations()
"""
@@ -173,8 +191,8 @@ class Build(models.Model):
# Create a new allocation
build_item = BuildItem(
build=self,
- stock_item=item,
- quantity=allocations[item])
+ stock_item=item['stock_item'],
+ quantity=item['quantity'])
build_item.save()
diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html
index 1aa5338551..15061a18b4 100644
--- a/InvenTree/build/templates/build/allocate.html
+++ b/InvenTree/build/templates/build/allocate.html
@@ -18,7 +18,8 @@ InvenTree | Allocate Parts
+
+{% else %}
+No stock could be selected for automatic build allocation.
+{% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html
index 506f766c44..5e9338d656 100644
--- a/InvenTree/build/templates/build/complete.html
+++ b/InvenTree/build/templates/build/complete.html
@@ -7,18 +7,42 @@ Are you sure you want to mark this build as complete?
{% if taking %}
The following items will be removed from stock:
-
+
+
+
+
+
Part
+
Quantity
+
Location
+
{% for item in taking %}
-
{{ item.quantity }} x {{ item.stock_item.part.name }} from {{ item.stock_item.location }}
@@ -127,4 +130,15 @@ InvenTree | Build - {{ build }}
submit_text: "Cancel Build",
});
});
+
+ $("#build-complete").on('click', function() {
+ launchModalForm(
+ "{% url 'build-complete' build.id %}",
+ {
+ reload: true,
+ submit_text: "Complete Build",
+ }
+ );
+ });
+
{% endblock %}
diff --git a/InvenTree/build/templates/build/unallocate.html b/InvenTree/build/templates/build/unallocate.html
new file mode 100644
index 0000000000..503bc354f6
--- /dev/null
+++ b/InvenTree/build/templates/build/unallocate.html
@@ -0,0 +1,9 @@
+{% extends "modal_form.html" %}
+
+{% block pre_form_content %}
+
+{{ block.super }}
+
+Are you sure you wish to unallocate all stock for this build?
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py
index 162b9a613f..3118bd6042 100644
--- a/InvenTree/build/urls.py
+++ b/InvenTree/build/urls.py
@@ -21,6 +21,8 @@ build_detail_urls = [
url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'),
url(r'^cancel/?', views.BuildCancel.as_view(), name='build-cancel'),
url(r'^complete/?', views.BuildComplete.as_view(), name='build-complete'),
+ url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
+ url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
]
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index 19af4b6458..79e3819df5 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -12,7 +12,7 @@ from django.forms import HiddenInput
from part.models import Part
from .models import Build, BuildItem
-from .forms import EditBuildForm, EditBuildItemForm, CompleteBuildForm
+from . import forms
from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView
@@ -67,6 +67,95 @@ class BuildCancel(AjaxView):
}
+class BuildAutoAllocate(AjaxUpdateView):
+ """ View to auto-allocate parts for a build.
+ Follows a simple set of rules to automatically allocate StockItem objects.
+
+ Ref: build.models.Build.getAutoAllocations()
+ """
+
+ model = Build
+ form_class = forms.ConfirmBuildForm
+ context_object_name = 'build'
+ ajax_form_title = 'Allocate Stock'
+ ajax_template_name = 'build/auto_allocate.html'
+
+ def get_context_data(self, *args, **kwargs):
+ """ Get the context data for form rendering. """
+
+ context = {}
+
+ try:
+ build = Build.objects.get(id=self.kwargs['pk'])
+ context['build'] = build
+ context['allocations'] = build.getAutoAllocations()
+ except Build.DoesNotExist:
+ context['error'] = 'No matching buidl found'
+
+ return context
+
+ def post(self, request, *args, **kwargs):
+ """ Handle POST request. Perform auto allocations.
+
+ - If the form validation passes, perform allocations
+ - Otherwise, the form is passed back to the client
+ """
+
+ build = self.get_object()
+ form = self.get_form()
+
+ confirm = request.POST.get('confirm', False)
+
+ valid = False
+
+ if confirm is False:
+ form.errors['confirm'] = ['Confirm stock allocation']
+ form.non_field_errors = 'Check the confirmation box at the bottom of the list'
+ else:
+ build.autoAllocate()
+ valid = True
+
+ data = {
+ 'form_valid': valid,
+ }
+
+ return self.renderJsonResponse(request, form, data, context=self.get_context_data())
+
+
+class BuildUnallocate(AjaxUpdateView):
+ """ View to un-allocate all parts from a build.
+
+ Provides a simple confirmation dialog with a BooleanField checkbox.
+ """
+
+ model = Build
+ form_class = forms.ConfirmBuildForm
+ ajax_form_title = "Unallocate Stock"
+ ajax_template_name = "build/unallocate.html"
+
+ def post(self, request, *args, **kwargs):
+
+ build = self.get_object()
+ form = self.get_form()
+
+ confirm = request.POST.get('confirm', False)
+
+ valid = False
+
+ if confirm is False:
+ form.errors['confirm'] = ['Confirm unallocation of build stock']
+ form.non_field_errors = 'Check the confirmation box'
+ else:
+ build.unallocateStock()
+ valid = True
+
+ data = {
+ 'form_valid': valid,
+ }
+
+ return self.renderJsonResponse(request, form, data)
+
+
class BuildComplete(AjaxUpdateView):
""" View to mark a build as Complete.
@@ -76,7 +165,7 @@ class BuildComplete(AjaxUpdateView):
"""
model = Build
- form_class = CompleteBuildForm
+ form_class = forms.CompleteBuildForm
context_object_name = "build"
ajax_form_title = "Complete Build"
ajax_template_name = "build/complete.html"
@@ -193,7 +282,7 @@ class BuildCreate(AjaxCreateView):
""" View to create a new Build object """
model = Build
context_object_name = 'build'
- form_class = EditBuildForm
+ form_class = forms.EditBuildForm
ajax_form_title = 'Start new Build'
ajax_template_name = 'modal_form.html'
@@ -225,7 +314,7 @@ class BuildUpdate(AjaxUpdateView):
""" View for editing a Build object """
model = Build
- form_class = EditBuildForm
+ form_class = forms.EditBuildForm
context_object_name = 'build'
ajax_form_title = 'Edit Build Details'
ajax_template_name = 'modal_form.html'
@@ -256,7 +345,7 @@ class BuildItemCreate(AjaxCreateView):
""" View for allocating a new part to a build """
model = BuildItem
- form_class = EditBuildItemForm
+ form_class = forms.EditBuildItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Allocate new Part'
@@ -342,7 +431,7 @@ class BuildItemEdit(AjaxUpdateView):
model = BuildItem
ajax_template_name = 'modal_form.html'
- form_class = EditBuildItemForm
+ form_class = forms.EditBuildItemForm
ajax_form_title = 'Edit Stock Allocation'
def get_data(self):
diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html
index 77ffa8dbff..e3bed202c4 100644
--- a/InvenTree/part/templates/part/attachments.html
+++ b/InvenTree/part/templates/part/attachments.html
@@ -30,8 +30,8 @@
{{ attachment.comment }}
-
-
+
+
diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js
index e88e617945..762c6b1d6b 100644
--- a/InvenTree/static/script/inventree/bom.js
+++ b/InvenTree/static/script/inventree/bom.js
@@ -129,7 +129,7 @@ function loadBomTable(table, options) {
if (options.editable) {
cols.push({
formatter: function(value, row, index, field) {
- var bEdit = "";
+ var bEdit = "";
var bDelt = "";
return "
" + bEdit + bDelt + "
";
diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js
index 826e2b1661..4ecd4214a2 100644
--- a/InvenTree/static/script/inventree/build.js
+++ b/InvenTree/static/script/inventree/build.js
@@ -40,8 +40,8 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
formatter: function(value, row, index, field) {
var html = value;
- var bEdit = "";
- var bDel = "";
+ var bEdit = "";
+ var bDel = "";
html += "
" + bEdit + bDel + "
";
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index c040c58fce..f181a6db88 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -34,6 +34,7 @@ class CreateStockItemForm(HelperForm):
'location',
'batch',
'quantity',
+ 'delete_on_deplete',
'status',
'notes',
'URL',
@@ -78,6 +79,7 @@ class EditStockItemForm(HelperForm):
fields = [
'supplier_part',
'batch',
+ 'delete_on_deplete',
'status',
'notes',
'URL',
diff --git a/InvenTree/stock/migrations/0015_stockitem_delete_on_deplete.py b/InvenTree/stock/migrations/0015_stockitem_delete_on_deplete.py
new file mode 100644
index 0000000000..29631b94d8
--- /dev/null
+++ b/InvenTree/stock/migrations/0015_stockitem_delete_on_deplete.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-05-09 12:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0014_auto_20190508_2332'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='stockitem',
+ name='delete_on_deplete',
+ field=models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted'),
+ ),
+ ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index f5ecb1be33..67d92c87e7 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -93,7 +93,7 @@ class StockItem(models.Model):
if add_note:
# This StockItem is being saved for the first time
- self.add_transaction_note(
+ self.addTransactionNote(
'Created stock item',
None,
notes="Created new stock item for part '{p}'".format(p=str(self.part)),
@@ -221,6 +221,8 @@ class StockItem(models.Model):
review_needed = models.BooleanField(default=False)
+ delete_on_deplete = models.BooleanField(default=True, help_text='Delete this Stock Item when stock is depleted')
+
ITEM_OK = 10
ITEM_ATTENTION = 50
ITEM_DAMAGED = 55
@@ -261,7 +263,11 @@ class StockItem(models.Model):
def has_tracking_info(self):
return self.tracking_info.count() > 0
- def add_transaction_note(self, title, user, notes='', system=True):
+ def addTransactionNote(self, title, user, notes='', system=True):
+ """ Generation a stock transaction note for this item.
+
+ Brief automated note detailing a movement or quantity change.
+ """
track = StockItemTracking.objects.create(
item=self,
title=title,
@@ -290,15 +296,39 @@ class StockItem(models.Model):
msg += " (from {loc})".format(loc=str(self.location))
self.location = location
+
+ self.addTransactionNote(msg,
+ user,
+ notes=notes,
+ system=True)
+
self.save()
- self.add_transaction_note(msg,
- user,
- notes=notes,
- system=True)
-
return True
+ @transaction.atomic
+ def updateQuantity(self, quantity):
+ """ Update stock quantity for this item.
+
+ If the quantity has reached zero, this StockItem will be deleted.
+
+ Returns:
+ - True if the quantity was saved
+ - False if the StockItem was deleted
+ """
+
+ if quantity < 0:
+ quantity = 0
+
+ self.quantity = quantity
+
+ if quantity <= 0 and self.delete_on_deplete:
+ self.delete()
+ return False
+ else:
+ self.save()
+ return True
+
@transaction.atomic
def stocktake(self, count, user, notes=''):
""" Perform item stocktake.
@@ -311,15 +341,15 @@ class StockItem(models.Model):
if count < 0 or self.infinite:
return False
- self.quantity = count
self.stocktake_date = datetime.now().date()
self.stocktake_user = user
- self.save()
- self.add_transaction_note('Stocktake - counted {n} items'.format(n=count),
- user,
- notes=notes,
- system=True)
+ if self.updateQuantity(count):
+
+ self.addTransactionNote('Stocktake - counted {n} items'.format(n=count),
+ user,
+ notes=notes,
+ system=True)
return True
@@ -336,14 +366,12 @@ class StockItem(models.Model):
if quantity <= 0 or self.infinite:
return False
- self.quantity += quantity
-
- self.save()
-
- self.add_transaction_note('Added {n} items to stock'.format(n=quantity),
- user,
- notes=notes,
- system=True)
+ if self.updateQuantity(self.quantity + quantity):
+
+ self.addTransactionNote('Added {n} items to stock'.format(n=quantity),
+ user,
+ notes=notes,
+ system=True)
return True
@@ -360,17 +388,12 @@ class StockItem(models.Model):
if quantity <= 0 or self.infinite:
return False
- self.quantity -= quantity
+ if self.updateQuantity(self.quantity - quantity):
- if self.quantity < 0:
- self.quantity = 0
-
- self.save()
-
- self.add_transaction_note('Removed {n} items from stock'.format(n=quantity),
- user,
- notes=notes,
- system=True)
+ self.addTransactionNote('Removed {n} items from stock'.format(n=quantity),
+ user,
+ notes=notes,
+ system=True)
return True
diff --git a/README.md b/README.md
index 502701623e..a47412776f 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,9 @@
# InvenTree
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
-InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action procses and does not require a complex system of work orders or stock transactions.
+InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action process and does not require a complex system of work orders or stock transactions.
-However, complex business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
+However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
## User Documentation