Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-04 21:59:48 +10:00
commit d5ad888b0a
21 changed files with 334 additions and 59 deletions

View File

@ -23,9 +23,16 @@ class InvenTreeTree(models.Model):
abstract = True
unique_together = ('name', 'parent')
name = models.CharField(max_length=100, unique=True)
name = models.CharField(
blank=False,
max_length=100,
unique=True
)
description = models.CharField(max_length=250)
description = models.CharField(
blank=False,
max_length=250
)
# When a category is deleted, graft the children onto its parent
parent = models.ForeignKey('self',

View File

@ -124,6 +124,19 @@ DATABASES = {
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'qr-code': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'qr-code-cache',
'TIMEOUT': 3600
}
}
QR_CODE_CACHE_ALIAS = 'qr-code'
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

View File

@ -8,6 +8,7 @@ Passes URL lookup downstream to each app as required.
from django.conf.urls import url, include
from django.contrib import admin
from django.contrib.auth import views as auth_views
from qr_code import urls as qr_code_urls
from company.urls import company_urls
@ -62,6 +63,8 @@ urlpatterns = [
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
url(r'^admin/', admin.site.urls, name='inventree-admin'),
url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')),
url(r'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'),

View File

@ -144,6 +144,43 @@ class AjaxView(AjaxMixin, View):
return self.renderJsonResponse(request)
class QRCodeView(AjaxView):
""" An 'AJAXified' view for displaying a QR code.
Subclasses should implement the get_qr_data(self) function.
"""
ajax_template_name = "qr_code.html"
def get(self, request, *args, **kwargs):
self.request = request
self.pk = self.kwargs['pk']
return self.renderJsonResponse(request, None, context=self.get_context_data())
def get_qr_data(self):
""" Returns the text object to render to a QR code.
The actual rendering will be handled by the template """
return None
def get_context_data(self):
""" Get context data for passing to the rendering template.
Explicity passes the parameter 'qr_data'
"""
context = {}
qr = self.get_qr_data()
if qr:
context['qr_data'] = qr
else:
context['error_msg'] = 'Error generating QR code'
return context
class AjaxCreateView(AjaxMixin, CreateView):
""" An 'AJAXified' CreateView for creating a new object in the db

View File

@ -87,7 +87,10 @@ class Build(models.Model):
limit_choices_to={'buildable': True},
)
title = models.CharField(max_length=100, help_text='Brief description of the build')
title = models.CharField(
blank=False,
max_length=100,
help_text='Brief description of the build')
quantity = models.PositiveIntegerField(
default=1,

View File

@ -42,7 +42,7 @@ class Company(models.Model):
It may be a supplier or a customer (or both).
"""
name = models.CharField(max_length=100, unique=True,
name = models.CharField(max_length=100, blank=False, unique=True,
help_text='Company name')
description = models.CharField(max_length=500)

View File

@ -180,7 +180,6 @@ class Part(models.Model):
def __str__(self):
return "{n} - {d}".format(n=self.name, d=self.description)
@property
def format_barcode(self):
""" Return a JSON string for formatting a barcode for this Part object """

View File

@ -41,7 +41,15 @@
<hr>
<div id='button-toolbar'>
<button style='float: right;' class='btn btn-success' id='part-create'>New Part</button>
<div class='container-fluid' style="float: right;">
<button class='btn btn-success' id='part-create'>New Part</button>
<div class='dropdown' style='float: right;'>
<button id='part-options' 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='multi-part-category' title='Set Part Category'>Set Category</a></li>
</ul>
</div>
</div>
</div>
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
@ -50,6 +58,7 @@
{% endblock %}
{% block js_load %}
{{ block.super }}
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
<script type='text/javacript' src="{% static 'script/inventree/stock.js' %}"></script>
{% endblock %}
{% block js_ready %}
@ -179,4 +188,9 @@
url: "{% url 'api-part-list' %}",
});
linkButtonsToSelection(
$("#part-table"),
['#part-options']
);
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load qr_code %}
{% block details %}
{% include 'part/tabs.html' with tab='detail' %}
@ -15,15 +14,16 @@
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if part.active %}
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
<hr>
{% if part.active %}
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %}
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
{% endif %}
<li><a href='#' id='show-qr-code' title='Generate QR Code'>Show QR Code</a></li>
</ul>
</div>
</h3>
@ -116,8 +116,6 @@
</div>
{% endif %}
{% qr_from_text part.format_barcode size="s" image_format="png" error_correction="L" %}
{% endblock %}
{% block js_load %}
@ -128,6 +126,15 @@
{% block js_ready %}
{{ block.super }}
$("#show-qr-code").click(function() {
launchModalForm(
"{% url 'part-qr' part.id %}",
{
no_post: true,
}
);
});
$("#duplicate-part").click(function() {
launchModalForm(
@ -150,27 +157,46 @@
});
$('#activate-part').click(function() {
inventreeUpdate(
"{% url 'api-part-detail' part.id %}",
showQuestionDialog(
'Activate Part?',
'Are you sure you wish to reactivate {{ part.name }}?',
{
active: true,
accept_text: 'Activate',
accept: function() {
inventreeUpdate(
"{% url 'api-part-detail' part.id %}",
{
active: true,
},
{
method: 'PATCH',
reloadOnSuccess: true,
}
);
}
},
{
method: 'PATCH',
reloadOnSuccess: true,
}
);
});
$('#deactivate-part').click(function() {
inventreeUpdate(
"{% url 'api-part-detail' part.id %}",
showQuestionDialog(
'Deactivate Part?',
`Are you sure you wish to deactivate {{ part.name }}?<br>
`,
{
active: false,
},
{
method: 'PATCH',
reloadOnSuccess: true,
accept_text: 'Deactivate',
accept: function() {
inventreeUpdate(
"{% url 'api-part-detail' part.id %}",
{
active: false,
},
{
method: 'PATCH',
reloadOnSuccess: true,
}
);
}
}
);
});

View File

@ -22,9 +22,7 @@
</div>
<div class="media-body">
<h4>{{ part.name }}{% if part.active == False %} <i>- INACTIVE</i>{% endif %}</h4>
{% if part.description %}
<p><i>{{ part.description }}</i></p>
{% endif %}
{% if part.IPN %}
<tr>
<td>IPN</td>

View File

@ -34,8 +34,9 @@ part_attachment_urls = [
part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
@ -43,6 +44,8 @@ part_detail_urls = [
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'),

View File

@ -27,6 +27,7 @@ from .forms import BomExportForm
from .forms import EditSupplierPartForm
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView
from InvenTree.helpers import DownloadFile, str2bool
@ -234,6 +235,21 @@ class PartDetail(DetailView):
return context
class PartQRCode(QRCodeView):
""" View for displaying a QR code for a Part object """
ajax_form_title = "Part QR Code"
def get_qr_data(self):
""" Generate QR code data for the Part """
try:
part = Part.objects.get(id=self.pk)
return part.format_barcode()
except Part.DoesNotExist:
return None
class PartImage(AjaxUpdateView):
""" View for uploading Part image """
@ -395,6 +411,7 @@ class CategoryDelete(AjaxDeleteView):
""" Delete view to delete a PartCategory """
model = PartCategory
ajax_template_name = 'part/category_delete.html'
ajax_form_title = 'Delete Part Category'
context_object_name = 'category'
success_url = '/part/'

View File

@ -152,3 +152,18 @@
.part-allocation-overallocated {
background: #ccf5ff;
}
.glyphicon-refresh-animate {
-animation: spin .7s infinite linear;
-webkit-animation: spin2 .7s infinite linear;
}
@-webkit-keyframes spin2 {
from { -webkit-transform: rotate(0deg);}
to { -webkit-transform: rotate(360deg);}
}
@keyframes spin {
from { transform: scale(1) rotate(0deg);}
to { transform: scale(1) rotate(360deg);}
}

View File

@ -24,7 +24,7 @@ function loadingMessageContent() {
*/
// TODO - This can be made a lot better
return '<b>Loading...</b>';
return "<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Waiting for server...";
}
@ -73,6 +73,17 @@ function afterForm(response, options) {
}
}
function modalShowSubmitButton(modal, show=true) {
/* Show (or hide) the 'Submit' button for the given modal form
*/
if (show) {
$(modal).find('#modal-form-submit').show();
} else {
$(modal).find('#modal-form-submit').hide();
}
}
function modalEnable(modal, enable=true) {
/* Enable (or disable) modal form elements to prevent user input
@ -155,16 +166,16 @@ function renderErrorMessage(xhr) {
}
function showDialog(title, content, options={}) {
function showAlertDialog(title, content, options={}) {
/* Display a modal dialog message box.
*
* title - Title text
* content - HTML content of the dialog window
* options:
* modal - modal form to use (default = '#modal-dialog')
* modal - modal form to use (default = '#modal-alert-dialog')
*/
var modal = options.modal || '#modal-dialog';
var modal = options.modal || '#modal-alert-dialog';
$(modal).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0);
@ -181,6 +192,54 @@ function showDialog(title, content, options={}) {
$(modal).modal('show');
}
function showQuestionDialog(title, content, options={}) {
/* Display a modal dialog for user input (Yes/No confirmation dialog)
*
* title - Title text
* content - HTML content of the dialog window
* options:
* modal - Modal target (default = 'modal-question-dialog')
* accept_text - Text for the accept button (default = 'Accept')
* cancel_text - Text for the cancel button (default = 'Cancel')
* accept - Function to run if the user presses 'Accept'
* cancel - Functino to run if the user presses 'Cancel'
*/
var modal = options.modal || '#modal-question-dialog';
$(modal).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0);
});
modalSetTitle(modal, title);
modalSetContent(modal, content);
var accept_text = options.accept_text || 'Accept';
var cancel_text = options.cancel_text || 'Cancel';
$(modal).find('#modal-form-cancel').html(cancel_text);
$(modal).find('#modal-form-accept').html(accept_text);
$(modal).on('click', '#modal-form-accept', function() {
$(modal).modal('hide');
if (options.accept) {
options.accept();
}
});
$(modal).on('click', 'modal-form-cancel', function() {
$(modal).modal('hide');
if (options.cancel) {
options.cancel();
}
});
$(modal).modal('show');
}
function openModal(options) {
/* Open a modal form, and perform some action based on the provided options object:
*
@ -215,7 +274,7 @@ function openModal(options) {
if (options.title) {
modalSetTitle(modal, options.title);
} else {
modalSetTitle(modal, 'Loading Form Data...');
modalSetTitle(modal, 'Loading Data...');
}
// Unless the content is explicitly set, display loading message
@ -275,12 +334,12 @@ function launchDeleteForm(url, options = {}) {
else {
$(modal).modal('hide');
showDialog('Invalid form response', 'JSON response missing HTML data');
showAlertDialog('Invalid form response', 'JSON response missing HTML data');
}
},
error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide');
showDialog('Error requesting form data', renderErrorMessage(xhr));
showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
}
});
@ -299,7 +358,7 @@ function launchDeleteForm(url, options = {}) {
},
error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide');
showDialog('Error deleting item', renderErrorMessage(xhr));
showAlertDialog('Error deleting item', renderErrorMessage(xhr));
}
});
});
@ -365,7 +424,7 @@ function handleModalForm(url, options) {
}
else {
$(modal).modal('hide');
showDialog('Invalid response from server', 'Form data missing from server response');
showAlertDialog('Invalid response from server', 'Form data missing from server response');
}
}
}
@ -378,7 +437,7 @@ function handleModalForm(url, options) {
// There was an error submitting form data via POST
$(modal).modal('hide');
showDialog('Error posting form data', renderErrorMessage(xhr));
showAlertDialog('Error posting form data', renderErrorMessage(xhr));
},
complete: function(xhr) {
//TODO
@ -396,6 +455,14 @@ function launchModalForm(url, options = {}) {
* an object called 'html_form'
*
* If the request is NOT successful, displays an appropriate error message.
*
* options:
*
* modal - Name of the modal (default = '#modal-form')
* data - Data to pass through to the AJAX request to fill the form
* submit_text - Text for the submit button (default = 'Submit')
* close_text - Text for the close button (default = 'Close')
* no_post - If true, only display form data, hide submit button, and disallow POST
*/
var modal = options.modal || '#modal-form';
@ -427,16 +494,22 @@ function launchModalForm(url, options = {}) {
if (response.html_form) {
injectModalForm(modal, response.html_form);
handleModalForm(url, options);
if (options.no_post) {
modalShowSubmitButton(modal, false);
} else {
modalShowSubmitButton(modal, true);
handleModalForm(url, options);
}
} else {
$(modal).modal('hide');
showDialog('Invalid server response', 'JSON response missing form data');
showAlertDialog('Invalid server response', 'JSON response missing form data');
}
},
error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide');
showDialog('Error requesting form data', renderErrorMessage(xhr));
showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
}
};

View File

@ -36,7 +36,6 @@ class StockLocation(InvenTreeTree):
def has_items(self):
return self.stock_items.count() > 0
@property
def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockLocation object """
@ -139,7 +138,6 @@ class StockItem(models.Model):
('part', 'serial'),
]
@property
def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode
@ -390,7 +388,7 @@ class StockItemTracking(models.Model):
date = models.DateTimeField(auto_now_add=True, editable=False)
# Short-form title for this tracking entry
title = models.CharField(max_length=250)
title = models.CharField(blank=False, max_length=250)
# Optional longer description
notes = models.TextField(blank=True)

View File

@ -2,8 +2,6 @@
{% load static %}
{% block content %}
{% load qr_code %}
<div class='row'>
<div class='col-sm-6'>
<h3>Stock Item Details</h3>
@ -25,6 +23,8 @@
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
{% endif %}
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li>
<hr>
<li><a href="#" id='item-qr-code' title='Generate QR code'>Show QR code</a></li>
</ul>
</div>
</div>
@ -109,9 +109,6 @@
{% endif %}
</table>
</div>
<div class='col-sm-6'>
{% qr_from_text item.format_barcode size="s" image_format="png" error_correction="L" %}
</div>
</div>
@ -148,6 +145,13 @@
});
});
$("#item-qr-code").click(function() {
launchModalForm("{% url 'stock-item-qr' item.id %}",
{
no_post: true,
});
});
{% if item.in_stock %}
$("#stock-move").click(function() {
launchModalForm(

View File

@ -1,6 +1,5 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load qr_code %}
{% block content %}
<div class='row'>
@ -24,12 +23,13 @@
<ul class="dropdown-menu">
<li><a href="#" id='location-edit' title='Edit stock location'>Edit</a></li>
<li><a href="#" id='location-delete' title='Delete stock location'>Delete</a></li>
<hr>
<li><a href="#" id='location-qr-code' title='Generate QR code'>Show QR code</a></li>
</ul>
</div>
{% qr_from_text location.format_barcode size="s" image_format="png" error_correction="L" %}
{% endif %}
</div>
</h3>
</div>
</h3>
</div>
</div>
@ -43,10 +43,9 @@
<div id='button-toolbar'>
<div class='container-fluid' style='float: right;'>
<button class="btn btn-success" id='item-create'>New Stock Item</span></button>
<button class="btn btn-success" id='item-create'>New Stock Item</button>
<div class="dropdown" style='float: right;'>
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
<span class="caret"></span></button>
<button id='stock-options' 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='multi-item-add' title='Add to selected stock items'>Add stock</a></li>
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
@ -102,6 +101,13 @@
return false;
});
$('#location-qr-code').click(function() {
launchModalForm("{% url 'stock-location-qr' location.id %}",
{
no_post: true,
});
});
{% endif %}
$('#item-create').click(function () {
@ -171,5 +177,4 @@
},
url: "{% url 'api-stock-list' %}",
});
{% endblock %}

View File

@ -10,6 +10,7 @@ from . import views
stock_location_detail_urls = [
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
# Anything else
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
@ -20,6 +21,7 @@ stock_item_detail_urls = [
url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'),
url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'),
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
]

View File

@ -10,6 +10,7 @@ from django.forms.models import model_to_dict
from django.forms import HiddenInput
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking
@ -75,6 +76,34 @@ class StockLocationEdit(AjaxUpdateView):
ajax_form_title = 'Edit Stock Location'
class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """
ajax_form_title = "Stock Location QR code"
def get_qr_data(self):
""" Generate QR code data for the StockLocation """
try:
loc = StockLocation.objects.get(id=self.pk)
return loc.format_barcode()
except StockLocation.DoesNotExist:
return None
class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """
ajax_form_title = "Stock Item QR Code"
def get_qr_data(self):
""" Generate QR code data for the StockItem """
try:
item = StockItem.objects.get(id=self.pk)
return item.format_barcode()
except StockItem.DoesNotExist:
return None
class StockItemEdit(AjaxUpdateView):
"""
View for editing details of a single StockItem
@ -205,7 +234,7 @@ class StockLocationDelete(AjaxDeleteView):
model = StockLocation
success_url = '/stock'
template_name = 'stock/location_delete.html'
ajax_template_name = 'stock/location_delete.html'
context_object_name = 'location'
ajax_form_title = 'Delete Stock Location'

View File

@ -39,14 +39,33 @@
</div>
</div>
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-dialog'>
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-question-dialog'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h3 id='modal-title'>Question Here</h3>
</div>
<div class='modal-form-content'>
</div>
<div class='modal-footer'>
<button type='button' class='btn btn-default' id='modal-form-cancel'>Cancel</button>
<button type='button' class='btn btn-primary' id='modal-form-accept'>Accept</button>
</div>
</div>
</div>
</div>
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-alert-dialog'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h3 id='modal-title'>Confirm Item Deletion</h3>
<h3 id='modal-title'>Alert Information</h3>
</div>
<div class='modal-form-content'>
</div>

View File

@ -0,0 +1,10 @@
{% load qr_code %}
<div class='container' style='width: 80%;'>
{% if qr_data %}
<img src="{% qr_url_from_text qr_data size='m' error_correction='q' %}" alt="QR Code">
{% else %}
<b>Error:</b><br>
{{ error_msg }}
{% endif %}
</div>