Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-10 08:52:45 +10:00
commit 6ffb6db248
16 changed files with 324 additions and 61 deletions

View File

@ -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): class CompleteBuildForm(HelperForm):
""" Form for marking a Build as complete """ """ Form for marking a Build as complete """

View File

@ -127,10 +127,10 @@ class Build(models.Model):
- If there are multiple StockItems available, ignore (leave up to the user) - If there are multiple StockItems available, ignore (leave up to the user)
Returns: 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(): for item in self.part.bom_items.all():
@ -151,19 +151,37 @@ class Build(models.Model):
# Are there any parts available? # Are there any parts available?
if stock_item.quantity > 0: if stock_item.quantity > 0:
# Only take as many as are available # Only take as many as are available
if stock_item.quantity < q_required: if stock_item.quantity < q_required:
q_required = stock_item.quantity q_required = stock_item.quantity
# Add the item to the allocations list allocation = {
allocations[stock_item] = q_required 'stock_item': stock_item,
'quantity': q_required,
}
allocations.append(allocation)
return allocations return allocations
@transaction.atomic
def unallocateStock(self):
""" Deletes all stock allocations for this build. """
BuildItem.objects.filter(build=self.id).delete()
@transaction.atomic @transaction.atomic
def autoAllocate(self): def autoAllocate(self):
""" Run auto-allocation routine to allocate StockItems to this Build. """ 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() See: getAutoAllocations()
""" """
@ -173,8 +191,8 @@ class Build(models.Model):
# Create a new allocation # Create a new allocation
build_item = BuildItem( build_item = BuildItem(
build=self, build=self,
stock_item=item, stock_item=item['stock_item'],
quantity=allocations[item]) quantity=item['quantity'])
build_item.save() build_item.save()

View File

@ -18,7 +18,8 @@ InvenTree | Allocate Parts
</div> </div>
<div class='col-sm-6'> <div class='col-sm-6'>
<div class='btn-group' style='float: right;'> <div class='btn-group' style='float: right;'>
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button> <button class='btn btn-primary' type='button' title='Automatic allocation' id='auto-allocate-build'>Auto Allocate</button>
<button class='btn btn-warning' type='button' title='Unallocate build stock' id='unallocate-build'>Unallocate</button>
</div> </div>
</div> </div>
</div> </div>
@ -54,12 +55,20 @@ InvenTree | Allocate Parts
{% endfor %} {% endfor %}
$("#complete-build").on('click', function() { $("#auto-allocate-build").on('click', function() {
launchModalForm( launchModalForm(
"{% url 'build-complete' build.id %}", "{% url 'build-auto-allocate' build.id %}",
{
reload: true,
}
);
});
$('#unallocate-build').on('click', function() {
launchModalForm(
"{% url 'build-unallocate' build.id %}",
{ {
reload: true, reload: true,
submit_text: "Complete Build",
} }
); );
}); });

View File

@ -0,0 +1,43 @@
{% extends "modal_form.html" %}
{% block pre_form_content %}
{{ block.super }}
<b>Build: {{ build.title }}</b> - {{ build.quantity }} x {{ build.part.name }}
<br><br>
Automatically allocate stock to this build?
<hr>
{% if allocations %}
<table class='table table-striped table-condensed'>
<tr>
<th></th>
<th>Part</th>
<th>Quantity</th>
<th>Location</th>
</tr>
{% for item in allocations %}
<tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{{ item.stock_item.part.image.url }}'>
<img class='hover-img-large' src='{{ item.stock_item.part.image.url }}'>
</a>
</td>
<td>
{{ item.stock_item.part.name }}<br>
<i>{{ item.stock_item.part.description }}</i>
</td>
<td>{{ item.quantity }}</td>
<td>{{ item.stock_item.location }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<i>No stock could be selected for automatic build allocation.</i>
{% endif %}
{% endblock %}

View File

@ -7,18 +7,42 @@ Are you sure you want to mark this build as complete?
<hr> <hr>
{% if taking %} {% if taking %}
The following items will be removed from stock: The following items will be removed from stock:
<ul>
<table class='table table-striped table-condensed'>
<tr>
<th></th>
<th>Part</th>
<th>Quantity</th>
<th>Location</th>
</tr>
{% for item in taking %} {% for item in taking %}
<li>{{ item.quantity }} x {{ item.stock_item.part.name }} from {{ item.stock_item.location }}</li> <tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{{ item.stock_item.part.image.url }}'>
<img class='hover-img-large' src='{{ item.stock_item.part.image.url }}'>
</a>
</td>
<td>
{{ item.stock_item.part.name }}<br>
<i>{{ item.stock_item.part.description }}</i>
</td>
<td>{{ item.quantity }}</td>
<td>{{ item.stock_item.location }}</td>
</tr>
{% endfor %} {% endfor %}
</ul> </table>
{% else %} {% else %}
No parts have been allocated to this build. No parts have been allocated to this build.
{% endif %} {% endif %}
<hr> <hr>
The following items will be created: The following items will be created:
<ul> <div class='panel panel-default'>
<li>{{ build.quantity }} x {{ build.part.name }}</li> <a class='hover-icon'>
</ul> <img class='hover-img-thumb' src='{{ build.part.image.url }}'>
<img class='hover-img-large' src='{{ build.part.image.url }}'>
</a>
{{ build.quantity }} x {{ build.part.name }}
</div>
{% endblock %} {% endblock %}

View File

@ -17,12 +17,15 @@ InvenTree | Build - {{ build }}
<h3> <h3>
<div style='float: right;'> <div style='float: right;'>
<div class="dropdown" style="float: right;"> <div class="dropdown" style="float: right;">
<a href="{% url 'build-allocate' build.id %}">
<button class='btn btn-info' type='button' title='Allocate Parts' id='build-allocate'>Allocate Parts</button>
</a>
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<span class="caret"></span></button> <span class="caret"></span></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href='#' id='build-edit' title='Edit build'>Edit build</a></li> <li><a href='#' id='build-edit' title='Edit build'>Edit build</a></li>
{% if build.is_active %} {% if build.is_active %}
<li><a href="{% url 'build-allocate' build.id %}" title='Allocate parts'>Allocate Parts</a></li> <li><a href='#' id='build-complete' title='Complete Build'>Complete Build</a></li>
<li><a href='#' id='build-cancel' title='Cancel build'>Cancel build</a></li> <li><a href='#' id='build-cancel' title='Cancel build'>Cancel build</a></li>
{% endif %} {% endif %}
</ul> </ul>
@ -127,4 +130,15 @@ InvenTree | Build - {{ build }}
submit_text: "Cancel Build", submit_text: "Cancel Build",
}); });
}); });
$("#build-complete").on('click', function() {
launchModalForm(
"{% url 'build-complete' build.id %}",
{
reload: true,
submit_text: "Complete Build",
}
);
});
{% endblock %} {% endblock %}

View File

@ -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 %}

View File

@ -21,6 +21,8 @@ build_detail_urls = [
url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'), url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'),
url(r'^cancel/?', views.BuildCancel.as_view(), name='build-cancel'), url(r'^cancel/?', views.BuildCancel.as_view(), name='build-cancel'),
url(r'^complete/?', views.BuildComplete.as_view(), name='build-complete'), 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'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
] ]

View File

@ -12,7 +12,7 @@ from django.forms import HiddenInput
from part.models import Part from part.models import Part
from .models import Build, BuildItem from .models import Build, BuildItem
from .forms import EditBuildForm, EditBuildItemForm, CompleteBuildForm from . import forms
from stock.models import StockLocation, StockItem from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView 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): class BuildComplete(AjaxUpdateView):
""" View to mark a build as Complete. """ View to mark a build as Complete.
@ -76,7 +165,7 @@ class BuildComplete(AjaxUpdateView):
""" """
model = Build model = Build
form_class = CompleteBuildForm form_class = forms.CompleteBuildForm
context_object_name = "build" context_object_name = "build"
ajax_form_title = "Complete Build" ajax_form_title = "Complete Build"
ajax_template_name = "build/complete.html" ajax_template_name = "build/complete.html"
@ -193,7 +282,7 @@ class BuildCreate(AjaxCreateView):
""" View to create a new Build object """ """ View to create a new Build object """
model = Build model = Build
context_object_name = 'build' context_object_name = 'build'
form_class = EditBuildForm form_class = forms.EditBuildForm
ajax_form_title = 'Start new Build' ajax_form_title = 'Start new Build'
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
@ -225,7 +314,7 @@ class BuildUpdate(AjaxUpdateView):
""" View for editing a Build object """ """ View for editing a Build object """
model = Build model = Build
form_class = EditBuildForm form_class = forms.EditBuildForm
context_object_name = 'build' context_object_name = 'build'
ajax_form_title = 'Edit Build Details' ajax_form_title = 'Edit Build Details'
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
@ -256,7 +345,7 @@ class BuildItemCreate(AjaxCreateView):
""" View for allocating a new part to a build """ """ View for allocating a new part to a build """
model = BuildItem model = BuildItem
form_class = EditBuildItemForm form_class = forms.EditBuildItemForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Allocate new Part' ajax_form_title = 'Allocate new Part'
@ -342,7 +431,7 @@ class BuildItemEdit(AjaxUpdateView):
model = BuildItem model = BuildItem
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
form_class = EditBuildItemForm form_class = forms.EditBuildItemForm
ajax_form_title = 'Edit Stock Allocation' ajax_form_title = 'Edit Stock Allocation'
def get_data(self): def get_data(self):

View File

@ -30,8 +30,8 @@
<td>{{ attachment.comment }}</td> <td>{{ attachment.comment }}</td>
<td> <td>
<div class='btn-group' style='float: right;'> <div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'>Edit</button> <button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>
<button type='button' class='btn btn-danger attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'>Delete</button> <button type='button' class='btn btn-danger attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -129,7 +129,7 @@ function loadBomTable(table, options) {
if (options.editable) { if (options.editable) {
cols.push({ cols.push({
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var bEdit = "<button title='Edit BOM Item' class='btn btn-success bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>"; var bEdit = "<button title='Edit BOM Item' class='btn btn-primary bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>";
var bDelt = "<button title='Delete BOM Item' class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>"; var bDelt = "<button title='Delete BOM Item' class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>";
return "<div class='btn-group'>" + bEdit + bDelt + "</div>"; return "<div class='btn-group'>" + bEdit + bDelt + "</div>";

View File

@ -40,8 +40,8 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var html = value; var html = value;
var bEdit = "<button class='btn btn-success item-edit-button btn-sm' type='button' url='/build/item/" + row.pk + "/edit/'>Edit</button>"; var bEdit = "<button class='btn btn-primary item-edit-button btn-sm' type='button' title='Edit stock allocation' url='/build/item/" + row.pk + "/edit/'><span class='glyphicon glyphicon-small glyphicon-pencil'></span></button>";
var bDel = "<button class='btn btn-danger item-del-button btn-sm' type='button' url='/build/item/" + row.pk + "/delete/'>Delete</button>"; var bDel = "<button class='btn btn-danger item-del-button btn-sm' type='button' title='Delete stock allocation' url='/build/item/" + row.pk + "/delete/'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>";
html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>"; html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>";

View File

@ -34,6 +34,7 @@ class CreateStockItemForm(HelperForm):
'location', 'location',
'batch', 'batch',
'quantity', 'quantity',
'delete_on_deplete',
'status', 'status',
'notes', 'notes',
'URL', 'URL',
@ -78,6 +79,7 @@ class EditStockItemForm(HelperForm):
fields = [ fields = [
'supplier_part', 'supplier_part',
'batch', 'batch',
'delete_on_deplete',
'status', 'status',
'notes', 'notes',
'URL', 'URL',

View File

@ -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'),
),
]

View File

@ -93,7 +93,7 @@ class StockItem(models.Model):
if add_note: if add_note:
# This StockItem is being saved for the first time # This StockItem is being saved for the first time
self.add_transaction_note( self.addTransactionNote(
'Created stock item', 'Created stock item',
None, None,
notes="Created new stock item for part '{p}'".format(p=str(self.part)), 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) 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_OK = 10
ITEM_ATTENTION = 50 ITEM_ATTENTION = 50
ITEM_DAMAGED = 55 ITEM_DAMAGED = 55
@ -261,7 +263,11 @@ class StockItem(models.Model):
def has_tracking_info(self): def has_tracking_info(self):
return self.tracking_info.count() > 0 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( track = StockItemTracking.objects.create(
item=self, item=self,
title=title, title=title,
@ -290,13 +296,37 @@ class StockItem(models.Model):
msg += " (from {loc})".format(loc=str(self.location)) msg += " (from {loc})".format(loc=str(self.location))
self.location = location self.location = location
self.save()
self.add_transaction_note(msg, self.addTransactionNote(msg,
user, user,
notes=notes, notes=notes,
system=True) system=True)
self.save()
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 return True
@transaction.atomic @transaction.atomic
@ -311,12 +341,12 @@ class StockItem(models.Model):
if count < 0 or self.infinite: if count < 0 or self.infinite:
return False return False
self.quantity = count
self.stocktake_date = datetime.now().date() self.stocktake_date = datetime.now().date()
self.stocktake_user = user self.stocktake_user = user
self.save()
self.add_transaction_note('Stocktake - counted {n} items'.format(n=count), if self.updateQuantity(count):
self.addTransactionNote('Stocktake - counted {n} items'.format(n=count),
user, user,
notes=notes, notes=notes,
system=True) system=True)
@ -336,11 +366,9 @@ class StockItem(models.Model):
if quantity <= 0 or self.infinite: if quantity <= 0 or self.infinite:
return False return False
self.quantity += quantity if self.updateQuantity(self.quantity + quantity):
self.save() self.addTransactionNote('Added {n} items to stock'.format(n=quantity),
self.add_transaction_note('Added {n} items to stock'.format(n=quantity),
user, user,
notes=notes, notes=notes,
system=True) system=True)
@ -360,14 +388,9 @@ class StockItem(models.Model):
if quantity <= 0 or self.infinite: if quantity <= 0 or self.infinite:
return False return False
self.quantity -= quantity if self.updateQuantity(self.quantity - quantity):
if self.quantity < 0: self.addTransactionNote('Removed {n} items from stock'.format(n=quantity),
self.quantity = 0
self.save()
self.add_transaction_note('Removed {n} items from stock'.format(n=quantity),
user, user,
notes=notes, notes=notes,
system=True) system=True)

View File

@ -3,9 +3,9 @@
# InvenTree # 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 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 ## User Documentation