2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 12:36:45 +00:00

Merge remote-tracking branch 'inventree/master' into simple-qr-codes

This commit is contained in:
Oliver Walters 2021-02-01 12:24:16 +11:00
commit 7d38507785
25 changed files with 2020 additions and 1474 deletions

View File

@ -203,7 +203,7 @@ INSTALLED_APPS = [
'corsheaders', # Cross-origin Resource Sharing for DRF 'corsheaders', # Cross-origin Resource Sharing for DRF
'crispy_forms', # Improved form rendering 'crispy_forms', # Improved form rendering
'import_export', # Import / export tables to file 'import_export', # Import / export tables to file
'django_cleanup', # Automatically delete orphaned MEDIA files 'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes 'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal 'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing 'markdownx', # Markdown editing

View File

@ -105,6 +105,7 @@ dynamic_javascript_urls = [
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'), url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'), url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'), url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'), url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
] ]

View File

@ -90,7 +90,7 @@ class BarcodeScan(APIView):
if loc is not None: if loc is not None:
response['stocklocation'] = plugin.renderStockLocation(loc) response['stocklocation'] = plugin.renderStockLocation(loc)
response['url'] = reverse('location-detail', kwargs={'pk': loc.id}) response['url'] = reverse('stock-location-detail', kwargs={'pk': loc.id})
match_found = True match_found = True
# Try to associate with a part # Try to associate with a part

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import string
import hashlib import hashlib
import logging import logging
@ -16,9 +17,18 @@ logger = logging.getLogger(__name__)
def hash_barcode(barcode_data): def hash_barcode(barcode_data):
""" """
Calculate an MD5 hash of barcode data Calculate an MD5 hash of barcode data.
HACK: Remove any 'non printable' characters from the hash,
as it seems browers will remove special control characters...
TODO: Work out a way around this!
""" """
printable_chars = filter(lambda x: x in string.printable, barcode_data)
barcode_data = ''.join(list(printable_chars))
hash = hashlib.md5(str(barcode_data).encode()) hash = hashlib.md5(str(barcode_data).encode())
return str(hash.hexdigest()) return str(hash.hexdigest())

View File

@ -71,6 +71,13 @@ class InvenTreeSetting(models.Model):
'choices': djmoney.settings.CURRENCY_CHOICES, 'choices': djmoney.settings.CURRENCY_CHOICES,
}, },
'BARCODE_ENABLE': {
'name': _('Barcode Support'),
'description': _('Enable barcode scanner support'),
'default': True,
'validator': bool,
},
'PART_IPN_REGEX': { 'PART_IPN_REGEX': {
'name': _('IPN Regex'), 'name': _('IPN Regex'),
'description': _('Regular expression pattern for matching Part IPN') 'description': _('Regular expression pattern for matching Part IPN')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -343,11 +343,11 @@ class PurchaseOrder(Order):
stock.save() stock.save()
text = _("Received items")
note = f"{_('Received')} {quantity} {_('items against order')} {str(self)}"
# Add a new transaction note to the newly created stock item # Add a new transaction note to the newly created stock item
stock.addTransactionNote("Received items", user, "Received {q} items against order '{po}'".format( stock.addTransactionNote(text, user, note)
q=quantity,
po=str(self))
)
# Update the number of parts received against the particular line item # Update the number of parts received against the particular line item
line.received += quantity line.received += quantity

View File

@ -6,6 +6,7 @@ Part database model definitions
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import logging
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -51,6 +52,9 @@ import common.models
import part.settings as part_settings import part.settings as part_settings
logger = logging.getLogger(__name__)
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects. """ PartCategory provides hierarchical organization of Part objects.
@ -335,11 +339,14 @@ class Part(MPTTModel):
if self.pk: if self.pk:
previous = Part.objects.get(pk=self.pk) previous = Part.objects.get(pk=self.pk)
if previous.image and not self.image == previous.image: # Image has been changed
if previous.image is not None and not self.image == previous.image:
# Are there any (other) parts which reference the image? # Are there any (other) parts which reference the image?
n_refs = Part.objects.filter(image=previous.image).exclude(pk=self.pk).count() n_refs = Part.objects.filter(image=previous.image).exclude(pk=self.pk).count()
if n_refs == 0: if n_refs == 0:
logger.info(f"Deleting unused image file '{previous.image}'")
previous.image.delete(save=False) previous.image.delete(save=False)
self.clean() self.clean()
@ -710,7 +717,7 @@ class Part(MPTTModel):
null=True, null=True,
blank=True, blank=True,
variations={'thumbnail': (128, 128)}, variations={'thumbnail': (128, 128)},
delete_orphans=True, delete_orphans=False,
) )
default_location = TreeForeignKey( default_location = TreeForeignKey(

View File

@ -44,6 +44,8 @@
<span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/> <span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/>
</button> </button>
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
<!-- Barcode actions menu --> <!-- Barcode actions menu -->
<div class='btn-group'> <div class='btn-group'>
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button> <button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
@ -52,6 +54,7 @@
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li> <li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
</ul> </ul>
</div> </div>
{% endif %}
{% if part.active %} {% if part.active %}
<button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'> <button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'>
<span id='part-price-icon' class='fas fa-dollar-sign'/> <span id='part-price-icon' class='fas fa-dollar-sign'/>

View File

@ -121,12 +121,17 @@ class StockAdjust(APIView):
- StockAdd: add stock items - StockAdd: add stock items
- StockRemove: remove stock items - StockRemove: remove stock items
- StockTransfer: transfer stock items - StockTransfer: transfer stock items
# TODO - This needs serious refactoring!!!
""" """
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
] ]
allow_missing_quantity = False
def get_items(self, request): def get_items(self, request):
""" """
Return a list of items posted to the endpoint. Return a list of items posted to the endpoint.
@ -157,10 +162,13 @@ class StockAdjust(APIView):
except (ValueError, StockItem.DoesNotExist): except (ValueError, StockItem.DoesNotExist):
raise ValidationError({'pk': 'Each entry must contain a valid pk field'}) raise ValidationError({'pk': 'Each entry must contain a valid pk field'})
if self.allow_missing_quantity and 'quantity' not in entry:
entry['quantity'] = item.quantity
try: try:
quantity = Decimal(str(entry.get('quantity', None))) quantity = Decimal(str(entry.get('quantity', None)))
except (ValueError, TypeError, InvalidOperation): except (ValueError, TypeError, InvalidOperation):
raise ValidationError({'quantity': 'Each entry must contain a valid quantity field'}) raise ValidationError({'quantity': "Each entry must contain a valid quantity value"})
if quantity < 0: if quantity < 0:
raise ValidationError({'quantity': 'Quantity field must not be less than zero'}) raise ValidationError({'quantity': 'Quantity field must not be less than zero'})
@ -234,6 +242,8 @@ class StockTransfer(StockAdjust):
API endpoint for performing stock movements API endpoint for performing stock movements
""" """
allow_missing_quantity = True
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.get_items(request) self.get_items(request)

View File

@ -195,11 +195,14 @@ class StockItem(MPTTModel):
super(StockItem, self).save(*args, **kwargs) super(StockItem, self).save(*args, **kwargs)
if add_note: if add_note:
note = f"{_('Created new stock item for')} {str(self.part)}"
# This StockItem is being saved for the first time # This StockItem is being saved for the first time
self.addTransactionNote( self.addTransactionNote(
_('Created stock item'), _('Created stock item'),
user, user,
notes="Created new stock item for part '{p}'".format(p=str(self.part)), note,
system=True system=True
) )
@ -611,9 +614,9 @@ class StockItem(MPTTModel):
""" """
self.addTransactionNote( self.addTransactionNote(
_("Returned from customer") + " " + self.customer.name, _("Returned from customer") + f" {self.customer.name}",
user, user,
notes=_("Returned to location") + " " + location.name, notes=_("Returned to location") + f" {location.name}",
system=True system=True
) )
@ -1000,12 +1003,17 @@ class StockItem(MPTTModel):
# Add a new tracking item for the new stock item # Add a new tracking item for the new stock item
new_stock.addTransactionNote( new_stock.addTransactionNote(
"Split from existing stock", _("Split from existing stock"),
user, user,
"Split {n} from existing stock item".format(n=quantity)) f"{_('Split')} {helpers.normalize(quantity)} {_('items')}"
)
# Remove the specified quantity from THIS stock item # Remove the specified quantity from THIS stock item
self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity)) self.take_stock(
quantity,
user,
f"{_('Split')} {quantity} {_('items into new stock item')}"
)
# Return a copy of the "new" stock item # Return a copy of the "new" stock item
return new_stock return new_stock
@ -1054,10 +1062,10 @@ class StockItem(MPTTModel):
return True return True
msg = "Moved to {loc}".format(loc=str(location)) msg = f"{_('Moved to')} {str(location)}"
if self.location: if self.location:
msg += " (from {loc})".format(loc=str(self.location)) msg += f" ({_('from')} {str(self.location)})"
self.location = location self.location = location
@ -1125,10 +1133,16 @@ class StockItem(MPTTModel):
if self.updateQuantity(count): if self.updateQuantity(count):
self.addTransactionNote('Stocktake - counted {n} items'.format(n=helpers.normalize(count)), n = helpers.normalize(count)
text = f"{_('Counted')} {n} {_('items')}"
self.addTransactionNote(
text,
user, user,
notes=notes, notes=notes,
system=True) system=True
)
return True return True
@ -1154,10 +1168,15 @@ class StockItem(MPTTModel):
if self.updateQuantity(self.quantity + quantity): if self.updateQuantity(self.quantity + quantity):
self.addTransactionNote('Added {n} items to stock'.format(n=helpers.normalize(quantity)), n = helpers.normalize(quantity)
text = f"{_('Added')} {n} {_('items')}"
self.addTransactionNote(
text,
user, user,
notes=notes, notes=notes,
system=True) system=True
)
return True return True
@ -1180,7 +1199,10 @@ class StockItem(MPTTModel):
if self.updateQuantity(self.quantity - quantity): if self.updateQuantity(self.quantity - quantity):
self.addTransactionNote('Removed {n} items from stock'.format(n=helpers.normalize(quantity)), q = helpers.normalize(quantity)
text = f"{_('Removed')} {q} {_('items')}"
self.addTransactionNote(text,
user, user,
notes=notes, notes=notes,
system=True) system=True)

View File

@ -120,6 +120,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</div> </div>
<div class='btn-group action-buttons' role='group'> <div class='btn-group action-buttons' role='group'>
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
<!-- Barcode actions menu --> <!-- Barcode actions menu -->
<div class='btn-group'> <div class='btn-group'>
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button> <button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
@ -127,13 +129,15 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li> <li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
{% if roles.stock.change %} {% if roles.stock.change %}
{% if item.uid %} {% if item.uid %}
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li> <li><a href='#' id='barcode-unlink'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
{% else %} {% else %}
<li><a href='#' id='link-barcode'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li> <li><a href='#' id='barcode-link'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
{% endif %} {% endif %}
<li><a href='#' id='barcode-scan-into-location'><span class='fas fa-sitemap'></span> {% trans "Scan to Location" %}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% endif %}
<!-- Document / label menu --> <!-- Document / label menu -->
{% if item.has_labels or item.has_test_reports %} {% if item.has_labels or item.has_test_reports %}
<div class='btn-group'> <div class='btn-group'>
@ -447,14 +451,18 @@ $("#show-qr-code").click(function() {
}); });
}); });
$("#link-barcode").click(function() { $("#barcode-link").click(function() {
linkBarcodeDialog({{ item.id }}); linkBarcodeDialog({{ item.id }});
}); });
$("#unlink-barcode").click(function() { $("#barcode-unlink").click(function() {
unlinkBarcode({{ item.id }}); unlinkBarcode({{ item.id }});
}); });
$("#barcode-scan-into-location").click(function() {
scanItemsIntoLocation([{{ item.id }}]);
});
{% if item.in_stock %} {% if item.in_stock %}
$("#stock-assign-to-customer").click(function() { $("#stock-assign-to-customer").click(function() {

View File

@ -37,6 +37,8 @@
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
<!-- Barcode actions menu --> <!-- Barcode actions menu -->
{% if location %} {% if location %}
<div class='btn-group'> <div class='btn-group'>
@ -47,6 +49,7 @@
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li> <li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
</ul> </ul>
</div> </div>
{% endif %}
<!-- Check permissions and owner --> <!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %} {% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
{% if roles.stock.change %} {% if roles.stock.change %}

View File

@ -284,7 +284,8 @@ class StockTest(TestCase):
# Check that a tracking item was added # Check that a tracking item was added
track = StockItemTracking.objects.filter(item=it).latest('id') track = StockItemTracking.objects.filter(item=it).latest('id')
self.assertIn('Stocktake', track.title) self.assertIn('Counted', track.title)
self.assertIn('items', track.title)
self.assertIn('Counted items', track.notes) self.assertIn('Counted items', track.notes)
n = it.tracking_info.count() n = it.tracking_info.count()

View File

@ -1114,7 +1114,7 @@ class StockAdjust(AjaxView, FormMixin):
return self.do_delete() return self.do_delete()
else: else:
return 'No action performed' return _('No action performed')
def do_add(self): def do_add(self):
@ -1129,7 +1129,7 @@ class StockAdjust(AjaxView, FormMixin):
count += 1 count += 1
return _("Added stock to {n} items".format(n=count)) return f"{_('Added stock to ')} {count} {_('items')}"
def do_take(self): def do_take(self):
@ -1144,7 +1144,7 @@ class StockAdjust(AjaxView, FormMixin):
count += 1 count += 1
return _("Removed stock from {n} items".format(n=count)) return f"{_('Removed stock from ')} {count} {_('items')}"
def do_count(self): def do_count(self):

View File

@ -21,4 +21,12 @@
</tbody> </tbody>
</table> </table>
<h4>{% trans "Barcode Settings" %}</h4>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %}
</tbody>
</table>
{% endblock %} {% endblock %}

View File

@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -111,7 +113,6 @@ InvenTree
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/filters.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/filters.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script> <script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
@ -126,6 +127,7 @@ InvenTree
<script type='text/javascript' src="{% url 'build.js' %}"></script> <script type='text/javascript' src="{% url 'build.js' %}"></script>
<script type='text/javascript' src="{% url 'order.js' %}"></script> <script type='text/javascript' src="{% url 'order.js' %}"></script>
<script type='text/javascript' src="{% url 'calendar.js' %}"></script> <script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'tables.js' %}"></script>
<script type='text/javascript' src="{% url 'table_filters.js' %}"></script> <script type='text/javascript' src="{% url 'table_filters.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script> <script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
@ -145,9 +147,11 @@ $(document).ready(function () {
showCachedAlerts(); showCachedAlerts();
{% if barcodes %}
$('#barcode-scan').click(function() { $('#barcode-scan').click(function() {
barcodeScanDialog(); barcodeScanDialog();
}); });
{% endif %}
}); });

View File

@ -1,12 +1,14 @@
{% load i18n %} {% load i18n %}
function makeBarcodeInput(placeholderText='') { function makeBarcodeInput(placeholderText='', hintText='') {
/* /*
* Generate HTML for a barcode input * Generate HTML for a barcode input
*/ */
placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}'; placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}';
hintText = hintText || '{% trans "Enter barcode data" %}';
var html = ` var html = `
<div class='form-group'> <div class='form-group'>
<label class='control-label' for='barcode'>{% trans "Barcode" %}</label> <label class='control-label' for='barcode'>{% trans "Barcode" %}</label>
@ -17,7 +19,7 @@ function makeBarcodeInput(placeholderText='') {
</span> </span>
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'> <input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
</div> </div>
<div id='hint_barcode_data' class='help-block'>{% trans "Enter barcode data" %}</div> <div id='hint_barcode_data' class='help-block'>${hintText}</div>
</div> </div>
</div> </div>
`; `;
@ -25,6 +27,81 @@ function makeBarcodeInput(placeholderText='') {
return html; return html;
} }
function makeNotesField(options={}) {
var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}';
var placeholder = options.placeholder || '{% trans "Enter notes" %}';
return `
<div class='form-group'>
<label class='control-label' for='notes'>{% trans "Notes" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='fas fa-sticky-note'></span>
</span>
<input id='notes' class='textinput textInput form-control' type='text' name='notes' placeholder='${placeholder}'>
</div>
<div id='hint_notes' class='help_block'>${tooltip}</div>
</div>
</div>`;
}
/*
* POST data to the server, and handle standard responses.
*/
function postBarcodeData(barcode_data, options={}) {
var modal = options.modal || '#modal-form';
var url = options.url || '/api/barcode/';
var data = options.data || {};
data.barcode = barcode_data;
inventreePut(
url,
data,
{
method: 'POST',
error: function() {
enableBarcodeInput(modal, true);
showBarcodeMessage(modal, '{% trans "Server error" %}');
},
success: function(response, status) {
modalEnable(modal, false);
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
if (options.onScan) {
options.onScan(response);
}
} else if ('error' in response) {
showBarcodeMessage(
modal,
response.error,
'warning'
);
} else {
showBarcodeMessage(
modal,
'{% trans "Unknown response from server" %}',
'warning'
);
}
} else {
// Invalid response returned from server
showInvalidResponseError(modal, response, status);
}
}
}
)
}
function showBarcodeMessage(modal, message, style='danger') { function showBarcodeMessage(modal, message, style='danger') {
@ -43,12 +120,6 @@ function showInvalidResponseError(modal, response, status) {
} }
function clearBarcodeError(modal, message) {
$(modal + ' #barcode-error-message').html('');
}
function enableBarcodeInput(modal, enabled=true) { function enableBarcodeInput(modal, enabled=true) {
var barcode = $(modal + ' #barcode'); var barcode = $(modal + ' #barcode');
@ -87,9 +158,7 @@ function barcodeDialog(title, options={}) {
if (barcode && barcode.length > 0) { if (barcode && barcode.length > 0) {
if (options.onScan) { postBarcodeData(barcode, options);
options.onScan(barcode);
}
} }
} }
@ -189,40 +258,20 @@ function barcodeScanDialog() {
barcodeDialog( barcodeDialog(
"Scan Barcode", "Scan Barcode",
{ {
onScan: function(barcode) { onScan: function(response) {
enableBarcodeInput(modal, false);
inventreePut(
'/api/barcode/',
{
barcode: barcode,
},
{
method: 'POST',
success: function(response, status) {
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
if ('url' in response) { if ('url' in response) {
// Redirect to the URL!
$(modal).modal('hide'); $(modal).modal('hide');
window.location.href = response.url;
}
} else if ('error' in response) { // Redirect to the URL!
showBarcodeMessage(modal, response.error, 'warning'); window.location.href = response.url;
} else { } else {
showBarcodeMessage(modal, "{% trans 'Unknown response from server' %}", 'warning'); showBarcodeMessage(
} modal,
} else { '{% trans "No URL in response" %}',
showInvalidResponseError(modal, response, status); 'warning'
}
},
},
); );
}, }
}
}, },
); );
} }
@ -238,37 +287,14 @@ function linkBarcodeDialog(stockitem, options={}) {
barcodeDialog( barcodeDialog(
"{% trans 'Link Barcode to Stock Item' %}", "{% trans 'Link Barcode to Stock Item' %}",
{ {
onScan: function(barcode) { url: '/api/barcode/link/',
enableBarcodeInput(modal, false); data: {
inventreePut(
'/api/barcode/link/',
{
barcode: barcode,
stockitem: stockitem, stockitem: stockitem,
}, },
{ onScan: function(response) {
method: 'POST',
success: function(response, status) {
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
$(modal).modal('hide'); $(modal).modal('hide');
location.reload(); location.reload();
} else if ('error' in response) {
showBarcodeMessage(modal, response.error, 'warning');
} else {
showBarcodeMessage(modal, "{% trans 'Unknown response from server' %}", warning);
}
} else {
showInvalidResponseError(modal, response, status);
}
},
},
);
} }
} }
); );
@ -386,22 +412,10 @@ function barcodeCheckIn(location_id, options={}) {
var table = `<div class='container' id='items-table-div' style='width: 80%; float: left;'></div>`; var table = `<div class='container' id='items-table-div' style='width: 80%; float: left;'></div>`;
// Extra form fields // Extra form fields
var extra = ` var extra = makeNotesField();
<div class='form-group'>
<label class='control-label' for='notes'>{% trans "Notes" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='fas fa-sticky-note'></span>
</span>
<input id='notes' class='textinput textInput form-control' type='text' name='notes' placeholder='{% trans "Enter notes" %}'>
</div>
<div id='hint_notes' class='help_block'>{% trans "Enter optional notes for stock transfer" %}</div>
</div>
</div>`;
barcodeDialog( barcodeDialog(
"{% trans "Check Stock Items into Location" %}", '{% trans "Check Stock Items into Location" %}',
{ {
headerContent: table, headerContent: table,
preShow: function() { preShow: function() {
@ -414,7 +428,6 @@ function barcodeCheckIn(location_id, options={}) {
extraFields: extra, extraFields: extra,
onSubmit: function() { onSubmit: function() {
// Called when the 'check-in' button is pressed // Called when the 'check-in' button is pressed
var data = {location: location_id}; var data = {location: location_id};
@ -434,7 +447,7 @@ function barcodeCheckIn(location_id, options={}) {
data.items = entries; data.items = entries;
inventreePut( inventreePut(
'{% url 'api-stock-transfer' %}', "{% url 'api-stock-transfer' %}",
data, data,
{ {
method: 'POST', method: 'POST',
@ -446,30 +459,13 @@ function barcodeCheckIn(location_id, options={}) {
showAlertOrCache('alert-success', response.success, true); showAlertOrCache('alert-success', response.success, true);
location.reload(); location.reload();
} else { } else {
showAlertOrCache('alert-success', 'Error transferring stock', false); showAlertOrCache('alert-success', '{% trans "Error transferring stock" %}', false);
} }
} }
} }
); );
}, },
onScan: function(barcode) { onScan: function(response) {
enableBarcodeInput(modal, false);
inventreePut(
'/api/barcode/',
{
barcode: barcode,
},
{
method: 'POST',
error: function() {
enableBarcodeInput(modal, true);
showBarcodeMessage(modal, '{% trans "Server error" %}');
},
success: function(response, status) {
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('stockitem' in response) { if ('stockitem' in response) {
stockitem = response.stockitem; stockitem = response.stockitem;
@ -482,33 +478,135 @@ function barcodeCheckIn(location_id, options={}) {
}); });
if (duplicate) { if (duplicate) {
showBarcodeMessage(modal, "{% trans "Stock Item already scanned" %}", "warning"); showBarcodeMessage(modal, '{% trans "Stock Item already scanned" %}', "warning");
} else { } else {
if (stockitem.location == location_id) { if (stockitem.location == location_id) {
showBarcodeMessage(modal, "{% trans "Stock Item already in this location" %}"); showBarcodeMessage(modal, '{% trans "Stock Item already in this location" %}');
return; return;
} }
// Add this stock item to the list // Add this stock item to the list
items.push(stockitem); items.push(stockitem);
showBarcodeMessage(modal, "{% trans "Added stock item" %}", "success"); showBarcodeMessage(modal, '{% trans "Added stock item" %}', "success");
reloadTable(); reloadTable();
} }
} else { } else {
// Barcode does not match a stock item // Barcode does not match a stock item
showBarcodeMessage(modal, "{% trans "Barcode does not match Stock Item" %}", "warning"); showBarcodeMessage(modal, '{% trans "Barcode does not match Stock Item" %}', "warning");
} }
},
}
);
}
/*
* Display dialog to check a single stock item into a stock location
*/
function scanItemsIntoLocation(item_id_list, options={}) {
var modal = options.modal || '#modal-form';
var stock_location = null;
// Extra form fields
var extra = makeNotesField();
// Header contentfor
var header = `
<div id='header-div'>
</div>
`;
function updateLocationInfo(location) {
var div = $(modal + ' #header-div');
if (stock_location && stock_location.pk) {
div.html(`
<div class='alert alert-block alert-info'>
<b>{% trans "Location" %}</b></br>
${stock_location.name}<br>
<i>${stock_location.description}</i>
</div>
`);
} else { } else {
showInvalidResponseError(modal, response, status); div.html('');
} }
},
},
);
},
} }
barcodeDialog(
'{% trans "Check Into Location" %}',
{
headerContent: header,
extraFields: extra,
preShow: function() {
modalSetSubmitText(modal, '{% trans "Check In" %}');
modalEnable(modal, false);
},
onShow: function() {
},
onSubmit: function() {
// Called when the 'check-in' button is pressed
if (!stock_location) {
return;
}
var items = [];
item_id_list.forEach(function(pk) {
items.push({
pk: pk,
});
})
var data = {
location: stock_location.pk,
notes: $(modal + ' #notes').val(),
items: items,
};
// Send API request
inventreePut(
'{% url "api-stock-transfer" %}',
data,
{
method: 'POST',
success: function(response, status) {
// First hide the modal
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
showAlertOrCache('alert-success', response.success, true);
location.reload();
} else {
showAlertOrCache('alert-danger', '{% trans "Error transferring stock" %}', false);
}
}
}
)
},
onScan: function(response) {
updateLocationInfo(null);
if ('stocklocation' in response) {
// Barcode corresponds to a StockLocation
stock_location = response.stocklocation;
updateLocationInfo(stock_location);
modalEnable(modal, true);
} else {
// Barcode does *NOT* correspond to a StockLocation
showBarcodeMessage(
modal,
'{% trans "Barcode does not match a valid location" %}',
"warning",
); );
} }
}
}
)
}

View File

@ -6,6 +6,7 @@
* Requires api.js to be loaded first * Requires api.js to be loaded first
*/ */
{% settings_value 'BARCODE_ENABLE' as barcodes %}
function stockStatusCodes() { function stockStatusCodes() {
return [ return [
@ -635,6 +636,9 @@ function loadStockTable(table, options) {
table, table,
[ [
'#stock-print-options', '#stock-print-options',
{% if barcodes %}
'#stock-barcode-options',
{% endif %}
'#stock-options', '#stock-options',
] ]
); );
@ -700,6 +704,20 @@ function loadStockTable(table, options) {
printTestReports(items); printTestReports(items);
}) })
{% if barcodes %}
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
})
scanItemsIntoLocation(items);
});
{% endif %}
$('#multi-item-stocktake').click(function() { $('#multi-item-stocktake').click(function() {
stockAdjustment('count'); stockAdjustment('count');
}); });

View File

@ -1,3 +1,5 @@
{% load i18n %}
function editButton(url, text='Edit') { function editButton(url, text='Edit') {
return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>"; return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
} }
@ -264,3 +266,44 @@ function customGroupSorter(sortName, sortOrder, sortData) {
} }
}); });
} }
// Expose default bootstrap table string literals to translation layer
(function ($) {
'use strict';
$.fn.bootstrapTable.locales['en-US-custom'] = {
formatLoadingMessage: function () {
return '{% trans "Loading data" %}';
},
formatRecordsPerPage: function (pageNumber) {
return `${pageNumber} {% trans "rows per page" %}`;
},
formatShowingRows: function (pageFrom, pageTo, totalRows) {
return `{% trans "Showing" %} ${pageFrom} {% trans "to" %} ${pageTo} {% trans "of" %} ${totalRows} {% trans "rows" %}`;
},
formatSearch: function () {
return '{% trans "Search" %}';
},
formatNoMatches: function () {
return '{% trans "No matching results" %}';
},
formatPaginationSwitch: function () {
return '{% trans "Hide/Show pagination" %}';
},
formatRefresh: function () {
return '{% trans "Refresh" %}';
},
formatToggle: function () {
return '{% trans "Toggle" %}';
},
formatColumns: function () {
return '{% trans "Columns" %}';
},
formatAllRows: function () {
return '{% trans "All" %}';
}
};
$.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['en-US-custom']);
})(jQuery);

View File

@ -1,5 +1,9 @@
{% load static %} {% load static %}
{% load inventree_extras %}
{% load i18n %} {% load i18n %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
<nav class="navbar navbar-xs navbar-default navbar-fixed-top "> <nav class="navbar navbar-xs navbar-default navbar-fixed-top ">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header clearfix content-heading"> <div class="navbar-header clearfix content-heading">
@ -46,11 +50,13 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% include "search_form.html" %} {% include "search_form.html" %}
{% if barcodes %}
<li id='navbar-barcode-li'> <li id='navbar-barcode-li'>
<button id='barcode-scan' class='btn btn-default' title='{% trans "Scan Barcode" %}'> <button id='barcode-scan' class='btn btn-default' title='{% trans "Scan Barcode" %}'>
<span class='fas fa-qrcode'></span> <span class='fas fa-qrcode'></span>
</button> </button>
</li> </li>
{% endif %}
<li class='dropdown'> <li class='dropdown'>
<a class='dropdown-toggle' data-toggle='dropdown' href="#"> <a class='dropdown-toggle' data-toggle='dropdown' href="#">
{% if not system_healthy %} {% if not system_healthy %}
@ -61,14 +67,13 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if user.is_staff %} {% if user.is_staff %}
<li><a href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li> <li><a href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
<hr>
{% endif %} {% endif %}
<li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
<li><a href="{% url 'logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li> <li><a href="{% url 'logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
{% else %} {% else %}
<li><a href="{% url 'login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li> <li><a href="{% url 'login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li>
{% endif %} {% endif %}
<hr> <hr>
<li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
<li id='launch-stats'><a href='#'> <li id='launch-stats'><a href='#'>
{% if system_healthy %} {% if system_healthy %}
<span class='fas fa-server'> <span class='fas fa-server'>

View File

@ -1,6 +1,8 @@
{% load i18n %} {% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %} {% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %} {% if owner_control.value == "True" %}
{% authorized_owners location.owner as owners %} {% authorized_owners location.owner as owners %}
@ -19,6 +21,17 @@
<span class='fas fa-plus-circle'></span> <span class='fas fa-plus-circle'></span>
</button> </button>
{% endif %} {% endif %}
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group'>
<button id='stock-barcode-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown' title='{% trans "Barcode Actions" %}'>
<span class='fas fa-qrcode'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a href='#' id='multi-item-barcode-scan-into-location' title='{% trans "Scan to Location" %}'><span class='fas fa-sitemap'></span> {% trans "Scan to Location" %}</a></li>
</ul>
</div>
{% endif %}
<div class='btn-group'> <div class='btn-group'>
<button id='stock-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'> <button id='stock-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
<span class='fas fa-print'></span> <span class='caret'></span> <span class='fas fa-print'></span> <span class='caret'></span>

View File

@ -15,7 +15,7 @@ pygments==2.2.0 # Syntax highlighting
tablib==0.13.0 # Import / export data files tablib==0.13.0 # Import / export data files
django-crispy-forms==1.8.1 # Form helpers django-crispy-forms==1.8.1 # Form helpers
django-import-export==2.0.0 # Data import / export for admin interface django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
django-qr-code==1.2.0 # Generate QR codes django-qr-code==1.2.0 # Generate QR codes
flake8==3.8.3 # PEP checking flake8==3.8.3 # PEP checking
pep8-naming==0.11.1 # PEP naming convention extension pep8-naming==0.11.1 # PEP naming convention extension