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 abstract = True
unique_together = ('name', 'parent') 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 # When a category is deleted, graft the children onto its parent
parent = models.ForeignKey('self', 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 # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # 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.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from qr_code import urls as qr_code_urls
from company.urls import company_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'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
url(r'^admin/', admin.site.urls, name='inventree-admin'), 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'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'), url(r'^search/', SearchView.as_view(), name='search'),

View File

@ -144,6 +144,43 @@ class AjaxView(AjaxMixin, View):
return self.renderJsonResponse(request) 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): class AjaxCreateView(AjaxMixin, CreateView):
""" An 'AJAXified' CreateView for creating a new object in the db """ 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}, 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( quantity = models.PositiveIntegerField(
default=1, default=1,

View File

@ -42,7 +42,7 @@ class Company(models.Model):
It may be a supplier or a customer (or both). 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') help_text='Company name')
description = models.CharField(max_length=500) description = models.CharField(max_length=500)

View File

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

View File

@ -41,7 +41,15 @@
<hr> <hr>
<div id='button-toolbar'> <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> </div>
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'> <table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
@ -50,6 +58,7 @@
{% endblock %} {% endblock %}
{% block js_load %} {% block js_load %}
{{ block.super }} {{ block.super }}
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
<script type='text/javacript' src="{% static 'script/inventree/stock.js' %}"></script> <script type='text/javacript' src="{% static 'script/inventree/stock.js' %}"></script>
{% endblock %} {% endblock %}
{% block js_ready %} {% block js_ready %}
@ -179,4 +188,9 @@
url: "{% url 'api-part-list' %}", url: "{% url 'api-part-list' %}",
}); });
linkButtonsToSelection(
$("#part-table"),
['#part-options']
);
{% endblock %} {% endblock %}

View File

@ -1,6 +1,5 @@
{% extends "part/part_base.html" %} {% extends "part/part_base.html" %}
{% load static %} {% load static %}
{% load qr_code %}
{% block details %} {% block details %}
{% include 'part/tabs.html' with tab='detail' %} {% include 'part/tabs.html' with tab='detail' %}
@ -15,15 +14,16 @@
<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">
{% if part.active %}
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li> <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='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li> <li><a href='#' id='stocktake-part' title='Stocktake'>Stocktake</a></li>
<hr> <hr>
{% if part.active %}
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li> <li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %} {% else %}
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li> <li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
{% endif %} {% endif %}
<li><a href='#' id='show-qr-code' title='Generate QR Code'>Show QR Code</a></li>
</ul> </ul>
</div> </div>
</h3> </h3>
@ -116,8 +116,6 @@
</div> </div>
{% endif %} {% endif %}
{% qr_from_text part.format_barcode size="s" image_format="png" error_correction="L" %}
{% endblock %} {% endblock %}
{% block js_load %} {% block js_load %}
@ -129,6 +127,15 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
$("#show-qr-code").click(function() {
launchModalForm(
"{% url 'part-qr' part.id %}",
{
no_post: true,
}
);
});
$("#duplicate-part").click(function() { $("#duplicate-part").click(function() {
launchModalForm( launchModalForm(
"{% url 'part-create' %}", "{% url 'part-create' %}",
@ -150,6 +157,12 @@
}); });
$('#activate-part').click(function() { $('#activate-part').click(function() {
showQuestionDialog(
'Activate Part?',
'Are you sure you wish to reactivate {{ part.name }}?',
{
accept_text: 'Activate',
accept: function() {
inventreeUpdate( inventreeUpdate(
"{% url 'api-part-detail' part.id %}", "{% url 'api-part-detail' part.id %}",
{ {
@ -160,9 +173,19 @@
reloadOnSuccess: true, reloadOnSuccess: true,
} }
); );
}
},
);
}); });
$('#deactivate-part').click(function() { $('#deactivate-part').click(function() {
showQuestionDialog(
'Deactivate Part?',
`Are you sure you wish to deactivate {{ part.name }}?<br>
`,
{
accept_text: 'Deactivate',
accept: function() {
inventreeUpdate( inventreeUpdate(
"{% url 'api-part-detail' part.id %}", "{% url 'api-part-detail' part.id %}",
{ {
@ -173,6 +196,9 @@
reloadOnSuccess: true, reloadOnSuccess: true,
} }
); );
}
}
);
}); });

View File

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

View File

@ -34,8 +34,9 @@ part_attachment_urls = [
part_detail_urls = [ part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), 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'^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'^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'^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'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
@ -44,6 +45,8 @@ part_detail_urls = [
url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), 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'^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'), url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'),
# Any other URLs go to the part detail page # Any other URLs go to the part detail page

View File

@ -27,6 +27,7 @@ from .forms import BomExportForm
from .forms import EditSupplierPartForm from .forms import EditSupplierPartForm
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView
from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import DownloadFile, str2bool
@ -234,6 +235,21 @@ class PartDetail(DetailView):
return context 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): class PartImage(AjaxUpdateView):
""" View for uploading Part image """ """ View for uploading Part image """
@ -395,6 +411,7 @@ class CategoryDelete(AjaxDeleteView):
""" Delete view to delete a PartCategory """ """ Delete view to delete a PartCategory """
model = PartCategory model = PartCategory
ajax_template_name = 'part/category_delete.html' ajax_template_name = 'part/category_delete.html'
ajax_form_title = 'Delete Part Category'
context_object_name = 'category' context_object_name = 'category'
success_url = '/part/' success_url = '/part/'

View File

@ -152,3 +152,18 @@
.part-allocation-overallocated { .part-allocation-overallocated {
background: #ccf5ff; 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 // 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) { function modalEnable(modal, enable=true) {
/* Enable (or disable) modal form elements to prevent user input /* 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. /* Display a modal dialog message box.
* *
* title - Title text * title - Title text
* content - HTML content of the dialog window * content - HTML content of the dialog window
* options: * 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).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0); $(modal + ' .modal-form-content').scrollTop(0);
@ -181,6 +192,54 @@ function showDialog(title, content, options={}) {
$(modal).modal('show'); $(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) { function openModal(options) {
/* Open a modal form, and perform some action based on the provided options object: /* Open a modal form, and perform some action based on the provided options object:
* *
@ -215,7 +274,7 @@ function openModal(options) {
if (options.title) { if (options.title) {
modalSetTitle(modal, options.title); modalSetTitle(modal, options.title);
} else { } else {
modalSetTitle(modal, 'Loading Form Data...'); modalSetTitle(modal, 'Loading Data...');
} }
// Unless the content is explicitly set, display loading message // Unless the content is explicitly set, display loading message
@ -275,12 +334,12 @@ function launchDeleteForm(url, options = {}) {
else { else {
$(modal).modal('hide'); $(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) { error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide'); $(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) { error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide'); $(modal).modal('hide');
showDialog('Error deleting item', renderErrorMessage(xhr)); showAlertDialog('Error deleting item', renderErrorMessage(xhr));
} }
}); });
}); });
@ -365,7 +424,7 @@ function handleModalForm(url, options) {
} }
else { else {
$(modal).modal('hide'); $(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 // There was an error submitting form data via POST
$(modal).modal('hide'); $(modal).modal('hide');
showDialog('Error posting form data', renderErrorMessage(xhr)); showAlertDialog('Error posting form data', renderErrorMessage(xhr));
}, },
complete: function(xhr) { complete: function(xhr) {
//TODO //TODO
@ -396,6 +455,14 @@ function launchModalForm(url, options = {}) {
* an object called 'html_form' * an object called 'html_form'
* *
* If the request is NOT successful, displays an appropriate error message. * 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'; var modal = options.modal || '#modal-form';
@ -427,16 +494,22 @@ function launchModalForm(url, options = {}) {
if (response.html_form) { if (response.html_form) {
injectModalForm(modal, response.html_form); injectModalForm(modal, response.html_form);
if (options.no_post) {
modalShowSubmitButton(modal, false);
} else {
modalShowSubmitButton(modal, true);
handleModalForm(url, options); handleModalForm(url, options);
}
} else { } else {
$(modal).modal('hide'); $(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) { error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide'); $(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): def has_items(self):
return self.stock_items.count() > 0 return self.stock_items.count() > 0
@property
def format_barcode(self): def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockLocation object """ """ Return a JSON string for formatting a barcode for this StockLocation object """
@ -139,7 +138,6 @@ class StockItem(models.Model):
('part', 'serial'), ('part', 'serial'),
] ]
@property
def format_barcode(self): def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockItem. """ Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode 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) date = models.DateTimeField(auto_now_add=True, editable=False)
# Short-form title for this tracking entry # Short-form title for this tracking entry
title = models.CharField(max_length=250) title = models.CharField(blank=False, max_length=250)
# Optional longer description # Optional longer description
notes = models.TextField(blank=True) notes = models.TextField(blank=True)

View File

@ -2,8 +2,6 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% load qr_code %}
<div class='row'> <div class='row'>
<div class='col-sm-6'> <div class='col-sm-6'>
<h3>Stock Item Details</h3> <h3>Stock Item Details</h3>
@ -25,6 +23,8 @@
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li> <li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
{% endif %} {% endif %}
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li> <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> </ul>
</div> </div>
</div> </div>
@ -109,9 +109,6 @@
{% endif %} {% endif %}
</table> </table>
</div> </div>
<div class='col-sm-6'>
{% qr_from_text item.format_barcode size="s" image_format="png" error_correction="L" %}
</div>
</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 %} {% if item.in_stock %}
$("#stock-move").click(function() { $("#stock-move").click(function() {
launchModalForm( launchModalForm(

View File

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

View File

@ -10,6 +10,7 @@ from . import views
stock_location_detail_urls = [ stock_location_detail_urls = [
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), 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 # Anything else
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'), 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'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'), url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'), 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'), 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 django.forms import HiddenInput
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from part.models import Part from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking from .models import StockItem, StockLocation, StockItemTracking
@ -75,6 +76,34 @@ class StockLocationEdit(AjaxUpdateView):
ajax_form_title = 'Edit Stock Location' 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): class StockItemEdit(AjaxUpdateView):
""" """
View for editing details of a single StockItem View for editing details of a single StockItem
@ -205,7 +234,7 @@ class StockLocationDelete(AjaxDeleteView):
model = StockLocation model = StockLocation
success_url = '/stock' success_url = '/stock'
template_name = 'stock/location_delete.html' ajax_template_name = 'stock/location_delete.html'
context_object_name = 'location' context_object_name = 'location'
ajax_form_title = 'Delete Stock Location' ajax_form_title = 'Delete Stock Location'

View File

@ -39,14 +39,33 @@
</div> </div>
</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-dialog'>
<div class='modal-content'> <div class='modal-content'>
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<h3 id='modal-title'>Confirm Item Deletion</h3> <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'>Alert Information</h3>
</div> </div>
<div class='modal-form-content'> <div class='modal-form-content'>
</div> </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>