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):
""" 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>";

View File

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

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:
# 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

View File

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