Merge pull request #290 from SchrodingersGat/auto-allocation

Auto allocation
This commit is contained in:
Oliver 2019-05-10 08:52:21 +10:00 committed by GitHub
commit 7ba5f7869a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 248 additions and 28 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):
""" 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)
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()

View File

@ -18,7 +18,8 @@ InvenTree | Allocate Parts
</div>
<div class='col-sm-6'>
<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>
@ -54,12 +55,20 @@ InvenTree | Allocate Parts
{% endfor %}
$("#complete-build").on('click', function() {
$("#auto-allocate-build").on('click', function() {
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,
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>
{% if taking %}
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 %}
<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 %}
</ul>
</table>
{% else %}
No parts have been allocated to this build.
{% endif %}
<hr>
The following items will be created:
<ul>
<li>{{ build.quantity }} x {{ build.part.name }}</li>
</ul>
<div class='panel panel-default'>
<a class='hover-icon'>
<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 %}

View File

@ -17,12 +17,15 @@ InvenTree | Build - {{ build }}
<h3>
<div 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
<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href='#' id='build-edit' title='Edit build'>Edit build</a></li>
{% 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>
{% endif %}
</ul>
@ -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 %}

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

View File

@ -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):

View File

@ -30,8 +30,8 @@
<td>{{ attachment.comment }}</td>
<td>
<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-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-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 }})'><span class='glyphicon glyphicon-small glyphicon-trash'></span></button>
</div>
</td>
</tr>

View File

@ -129,7 +129,7 @@ function loadBomTable(table, options) {
if (options.editable) {
cols.push({
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>";
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) {
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 bDel = "<button class='btn btn-danger item-del-button btn-sm' type='button' url='/build/item/" + row.pk + "/delete/'>Delete</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' 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>";