mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge remote-tracking branch 'inventree/master' into simple-qr-codes
This commit is contained in:
		| @@ -203,7 +203,7 @@ INSTALLED_APPS = [ | ||||
|     'corsheaders',                          # Cross-origin Resource Sharing for DRF | ||||
|     'crispy_forms',                         # Improved form rendering | ||||
|     '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 | ||||
|     'mptt',                                 # Modified Preorder Tree Traversal | ||||
|     'markdownx',                            # Markdown editing | ||||
|   | ||||
| @@ -105,6 +105,7 @@ dynamic_javascript_urls = [ | ||||
|     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'^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'), | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -90,7 +90,7 @@ class BarcodeScan(APIView): | ||||
|  | ||||
|             if loc is not None: | ||||
|                 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 | ||||
|  | ||||
|             # Try to associate with a part | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| import string | ||||
| import hashlib | ||||
| import logging | ||||
|  | ||||
| @@ -16,9 +17,18 @@ logger = logging.getLogger(__name__) | ||||
|  | ||||
| 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()) | ||||
|     return str(hash.hexdigest()) | ||||
|  | ||||
|   | ||||
| @@ -71,6 +71,13 @@ class InvenTreeSetting(models.Model): | ||||
|             'choices': djmoney.settings.CURRENCY_CHOICES, | ||||
|         }, | ||||
|  | ||||
|         'BARCODE_ENABLE': { | ||||
|             'name': _('Barcode Support'), | ||||
|             'description': _('Enable barcode scanner support'), | ||||
|             'default': True, | ||||
|             'validator': bool, | ||||
|         }, | ||||
|  | ||||
|         'PART_IPN_REGEX': { | ||||
|             'name': _('IPN Regex'), | ||||
|             '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
											
										
									
								
							| @@ -343,11 +343,11 @@ class PurchaseOrder(Order): | ||||
|  | ||||
|             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 | ||||
|             stock.addTransactionNote("Received items", user, "Received {q} items against order '{po}'".format( | ||||
|                 q=quantity, | ||||
|                 po=str(self)) | ||||
|             ) | ||||
|             stock.addTransactionNote(text, user, note) | ||||
|  | ||||
|         # Update the number of parts received against the particular line item | ||||
|         line.received += quantity | ||||
|   | ||||
| @@ -6,6 +6,7 @@ Part database model definitions | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| import os | ||||
| import logging | ||||
|  | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.core.exceptions import ValidationError | ||||
| @@ -51,6 +52,9 @@ import common.models | ||||
| import part.settings as part_settings | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class PartCategory(InvenTreeTree): | ||||
|     """ PartCategory provides hierarchical organization of Part objects. | ||||
|  | ||||
| @@ -335,11 +339,14 @@ class Part(MPTTModel): | ||||
|         if 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? | ||||
|                 n_refs = Part.objects.filter(image=previous.image).exclude(pk=self.pk).count() | ||||
|  | ||||
|                 if n_refs == 0: | ||||
|                     logger.info(f"Deleting unused image file '{previous.image}'") | ||||
|                     previous.image.delete(save=False) | ||||
|  | ||||
|         self.clean() | ||||
| @@ -710,7 +717,7 @@ class Part(MPTTModel): | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         variations={'thumbnail': (128, 128)}, | ||||
|         delete_orphans=True, | ||||
|         delete_orphans=False, | ||||
|     ) | ||||
|  | ||||
|     default_location = TreeForeignKey( | ||||
|   | ||||
| @@ -44,6 +44,8 @@ | ||||
|                 <span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/> | ||||
|             </button> | ||||
|  | ||||
|             {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|             {% if barcodes %} | ||||
|             <!-- Barcode actions menu --> | ||||
|             <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> | ||||
| @@ -52,6 +54,7 @@ | ||||
|                     <li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|             {% endif %} | ||||
|             {% if part.active %} | ||||
|             <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'/> | ||||
|   | ||||
| @@ -121,12 +121,17 @@ class StockAdjust(APIView): | ||||
|     - StockAdd: add stock items | ||||
|     - StockRemove: remove stock items | ||||
|     - StockTransfer: transfer stock items | ||||
|  | ||||
|     # TODO - This needs serious refactoring!!! | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     permission_classes = [ | ||||
|         permissions.IsAuthenticated, | ||||
|     ] | ||||
|  | ||||
|     allow_missing_quantity = False | ||||
|  | ||||
|     def get_items(self, request): | ||||
|         """ | ||||
|         Return a list of items posted to the endpoint. | ||||
| @@ -157,10 +162,13 @@ class StockAdjust(APIView): | ||||
|             except (ValueError, StockItem.DoesNotExist): | ||||
|                 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: | ||||
|                 quantity = Decimal(str(entry.get('quantity', None))) | ||||
|             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: | ||||
|                 raise ValidationError({'quantity': 'Quantity field must not be less than zero'}) | ||||
| @@ -234,6 +242,8 @@ class StockTransfer(StockAdjust): | ||||
|     API endpoint for performing stock movements | ||||
|     """ | ||||
|  | ||||
|     allow_missing_quantity = True | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|  | ||||
|         self.get_items(request) | ||||
|   | ||||
| @@ -195,11 +195,14 @@ class StockItem(MPTTModel): | ||||
|         super(StockItem, self).save(*args, **kwargs) | ||||
|  | ||||
|         if add_note: | ||||
|  | ||||
|             note = f"{_('Created new stock item for')} {str(self.part)}" | ||||
|  | ||||
|             # This StockItem is being saved for the first time | ||||
|             self.addTransactionNote( | ||||
|                 _('Created stock item'), | ||||
|                 user, | ||||
|                 notes="Created new stock item for part '{p}'".format(p=str(self.part)), | ||||
|                 note, | ||||
|                 system=True | ||||
|             ) | ||||
|  | ||||
| @@ -611,9 +614,9 @@ class StockItem(MPTTModel): | ||||
|         """ | ||||
|  | ||||
|         self.addTransactionNote( | ||||
|             _("Returned from customer") + " " + self.customer.name, | ||||
|             _("Returned from customer") + f" {self.customer.name}", | ||||
|             user, | ||||
|             notes=_("Returned to location") + " " + location.name, | ||||
|             notes=_("Returned to location") + f" {location.name}", | ||||
|             system=True | ||||
|         ) | ||||
|  | ||||
| @@ -1000,12 +1003,17 @@ class StockItem(MPTTModel): | ||||
|  | ||||
|         # Add a new tracking item for the new stock item | ||||
|         new_stock.addTransactionNote( | ||||
|             "Split from existing stock", | ||||
|             _("Split from existing stock"), | ||||
|             user, | ||||
|             "Split {n} from existing stock item".format(n=quantity)) | ||||
|             f"{_('Split')} {helpers.normalize(quantity)} {_('items')}" | ||||
|         ) | ||||
|  | ||||
|         # 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 new_stock | ||||
| @@ -1054,10 +1062,10 @@ class StockItem(MPTTModel): | ||||
|  | ||||
|             return True | ||||
|  | ||||
|         msg = "Moved to {loc}".format(loc=str(location)) | ||||
|         msg = f"{_('Moved to')} {str(location)}" | ||||
|          | ||||
|         if self.location: | ||||
|             msg += " (from {loc})".format(loc=str(self.location)) | ||||
|             msg += f" ({_('from')} {str(self.location)})" | ||||
|  | ||||
|         self.location = location | ||||
|  | ||||
| @@ -1125,10 +1133,16 @@ class StockItem(MPTTModel): | ||||
|  | ||||
|         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, | ||||
|                 notes=notes, | ||||
|                                     system=True) | ||||
|                 system=True | ||||
|             ) | ||||
|  | ||||
|         return True | ||||
|  | ||||
| @@ -1154,10 +1168,15 @@ class StockItem(MPTTModel): | ||||
|  | ||||
|         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, | ||||
|                 notes=notes, | ||||
|                                     system=True) | ||||
|                 system=True | ||||
|             ) | ||||
|  | ||||
|         return True | ||||
|  | ||||
| @@ -1180,7 +1199,10 @@ class StockItem(MPTTModel): | ||||
|  | ||||
|         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, | ||||
|                                     notes=notes, | ||||
|                                     system=True) | ||||
|   | ||||
| @@ -120,6 +120,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} | ||||
| </div> | ||||
|  | ||||
| <div class='btn-group action-buttons' role='group'> | ||||
|     {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|     {% if barcodes %} | ||||
|     <!-- Barcode actions menu --> | ||||
|     <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> | ||||
| @@ -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> | ||||
|             {% if roles.stock.change %} | ||||
|             {% 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 %} | ||||
|                 <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 %} | ||||
|             <li><a href='#' id='barcode-scan-into-location'><span class='fas fa-sitemap'></span> {% trans "Scan to Location" %}</a></li> | ||||
|             {% endif %} | ||||
|         </ul> | ||||
|     </div> | ||||
|     {% endif %} | ||||
|     <!-- Document / label menu --> | ||||
|     {% if item.has_labels or item.has_test_reports %} | ||||
|     <div class='btn-group'> | ||||
| @@ -447,14 +451,18 @@ $("#show-qr-code").click(function() { | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| $("#link-barcode").click(function() { | ||||
| $("#barcode-link").click(function() { | ||||
|     linkBarcodeDialog({{ item.id }}); | ||||
| }); | ||||
|  | ||||
| $("#unlink-barcode").click(function() { | ||||
| $("#barcode-unlink").click(function() { | ||||
|     unlinkBarcode({{ item.id }}); | ||||
| }); | ||||
|  | ||||
| $("#barcode-scan-into-location").click(function() { | ||||
|     scanItemsIntoLocation([{{ item.id }}]); | ||||
| }); | ||||
|  | ||||
| {% if item.in_stock %} | ||||
|  | ||||
| $("#stock-assign-to-customer").click(function() { | ||||
|   | ||||
| @@ -37,6 +37,8 @@ | ||||
|                 </button> | ||||
|             {% endif %} | ||||
|         {% endif %}  | ||||
|         {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|         {% if barcodes %} | ||||
|         <!-- Barcode actions menu --> | ||||
|         {% if location %} | ||||
|         <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> | ||||
|             </ul> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|         <!-- Check permissions and owner --> | ||||
|         {% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %} | ||||
|             {% if roles.stock.change %} | ||||
|   | ||||
| @@ -284,7 +284,8 @@ class StockTest(TestCase): | ||||
|         # Check that a tracking item was added | ||||
|         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) | ||||
|  | ||||
|         n = it.tracking_info.count() | ||||
|   | ||||
| @@ -1114,7 +1114,7 @@ class StockAdjust(AjaxView, FormMixin): | ||||
|             return self.do_delete() | ||||
|  | ||||
|         else: | ||||
|             return 'No action performed' | ||||
|             return _('No action performed') | ||||
|  | ||||
|     def do_add(self): | ||||
|          | ||||
| @@ -1129,7 +1129,7 @@ class StockAdjust(AjaxView, FormMixin): | ||||
|  | ||||
|             count += 1 | ||||
|  | ||||
|         return _("Added stock to {n} items".format(n=count)) | ||||
|         return f"{_('Added stock to ')} {count} {_('items')}" | ||||
|  | ||||
|     def do_take(self): | ||||
|  | ||||
| @@ -1144,7 +1144,7 @@ class StockAdjust(AjaxView, FormMixin): | ||||
|  | ||||
|             count += 1 | ||||
|  | ||||
|         return _("Removed stock from {n} items".format(n=count)) | ||||
|         return f"{_('Removed stock from ')} {count} {_('items')}" | ||||
|  | ||||
|     def do_count(self): | ||||
|          | ||||
|   | ||||
| @@ -21,4 +21,12 @@ | ||||
|     </tbody> | ||||
| </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 %} | ||||
| @@ -2,6 +2,8 @@ | ||||
| {% load i18n %} | ||||
| {% load inventree_extras %} | ||||
|  | ||||
| {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|  | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <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/api.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/sidenav.js' %}"></script> | ||||
|  | ||||
| @@ -126,6 +127,7 @@ InvenTree | ||||
| <script type='text/javascript' src="{% url 'build.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 'tables.js' %}"></script> | ||||
| <script type='text/javascript' src="{% url 'table_filters.js' %}"></script> | ||||
|  | ||||
| <script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script> | ||||
| @@ -145,9 +147,11 @@ $(document).ready(function () { | ||||
|      | ||||
|     showCachedAlerts(); | ||||
|  | ||||
|     {% if barcodes %} | ||||
|     $('#barcode-scan').click(function() { | ||||
|         barcodeScanDialog(); | ||||
|     }); | ||||
|     {% endif %} | ||||
|  | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| function makeBarcodeInput(placeholderText='') { | ||||
| function makeBarcodeInput(placeholderText='', hintText='') { | ||||
|     /* | ||||
|      * Generate HTML for a barcode input | ||||
|      */ | ||||
|  | ||||
|     placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}'; | ||||
|  | ||||
|     hintText = hintText || '{% trans "Enter barcode data" %}'; | ||||
|  | ||||
|     var html = ` | ||||
|     <div class='form-group'> | ||||
|         <label class='control-label' for='barcode'>{% trans "Barcode" %}</label> | ||||
| @@ -17,7 +19,7 @@ function makeBarcodeInput(placeholderText='') { | ||||
|                 </span> | ||||
|                 <input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'> | ||||
|             </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> | ||||
|     `; | ||||
| @@ -25,6 +27,81 @@ function makeBarcodeInput(placeholderText='') { | ||||
|     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') { | ||||
|  | ||||
| @@ -43,12 +120,6 @@ function showInvalidResponseError(modal, response, status) { | ||||
| } | ||||
|  | ||||
|  | ||||
| function clearBarcodeError(modal, message) { | ||||
|  | ||||
|     $(modal + ' #barcode-error-message').html(''); | ||||
| } | ||||
|  | ||||
|  | ||||
| function enableBarcodeInput(modal, enabled=true) { | ||||
|  | ||||
|     var barcode = $(modal + ' #barcode'); | ||||
| @@ -87,9 +158,7 @@ function barcodeDialog(title, options={}) { | ||||
|  | ||||
|         if (barcode && barcode.length > 0) { | ||||
|              | ||||
|             if (options.onScan) { | ||||
|                 options.onScan(barcode); | ||||
|             } | ||||
|             postBarcodeData(barcode, options); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -189,40 +258,20 @@ function barcodeScanDialog() { | ||||
|     barcodeDialog( | ||||
|         "Scan Barcode", | ||||
|         { | ||||
|             onScan: function(barcode) { | ||||
|                 enableBarcodeInput(modal, false); | ||||
|                 inventreePut( | ||||
|                     '/api/barcode/', | ||||
|                     { | ||||
|                         barcode: barcode, | ||||
|                     }, | ||||
|                     { | ||||
|                         method: 'POST', | ||||
|                         success: function(response, status) { | ||||
|  | ||||
|                             enableBarcodeInput(modal, true); | ||||
|  | ||||
|                             if (status == 'success') { | ||||
|                                  | ||||
|                                 if ('success' in response) { | ||||
|             onScan: function(response) { | ||||
|                 if ('url' in response) { | ||||
|                                         // Redirect to the URL! | ||||
|                     $(modal).modal('hide'); | ||||
|                                         window.location.href = response.url; | ||||
|                                     } | ||||
|                      | ||||
|                                 } else if ('error' in response) { | ||||
|                                     showBarcodeMessage(modal, response.error, 'warning'); | ||||
|                     // Redirect to the URL! | ||||
|                     window.location.href = response.url; | ||||
|                 } else { | ||||
|                                     showBarcodeMessage(modal, "{% trans 'Unknown response from server' %}", 'warning'); | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 showInvalidResponseError(modal, response, status); | ||||
|                             }       | ||||
|                         }, | ||||
|                     }, | ||||
|                     showBarcodeMessage( | ||||
|                         modal, | ||||
|                         '{% trans "No URL in response" %}', | ||||
|                         'warning' | ||||
|                     ); | ||||
|             },  | ||||
|                 } | ||||
|             }  | ||||
|         }, | ||||
|     );  | ||||
| } | ||||
| @@ -238,37 +287,14 @@ function linkBarcodeDialog(stockitem, options={}) { | ||||
|     barcodeDialog( | ||||
|         "{% trans 'Link Barcode to Stock Item' %}", | ||||
|         { | ||||
|             onScan: function(barcode) { | ||||
|                 enableBarcodeInput(modal, false); | ||||
|                 inventreePut( | ||||
|                     '/api/barcode/link/', | ||||
|                     { | ||||
|                         barcode: barcode, | ||||
|             url: '/api/barcode/link/', | ||||
|             data: { | ||||
|                 stockitem: stockitem, | ||||
|             }, | ||||
|                     { | ||||
|                         method: 'POST', | ||||
|                         success: function(response, status) { | ||||
|             onScan: function(response) { | ||||
|  | ||||
|                             enableBarcodeInput(modal, true); | ||||
|  | ||||
|                             if (status == 'success') { | ||||
|  | ||||
|                                 if ('success' in response) { | ||||
|                 $(modal).modal('hide'); | ||||
|                 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>`; | ||||
|  | ||||
|     // Extra form fields | ||||
|     var extra = ` | ||||
|     <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>`; | ||||
|     var extra = makeNotesField(); | ||||
|  | ||||
|     barcodeDialog( | ||||
|         "{% trans "Check Stock Items into Location" %}", | ||||
|         '{% trans "Check Stock Items into Location" %}', | ||||
|         { | ||||
|             headerContent: table, | ||||
|             preShow: function() { | ||||
| @@ -414,7 +428,6 @@ function barcodeCheckIn(location_id, options={}) { | ||||
|             extraFields: extra, | ||||
|             onSubmit: function() { | ||||
|  | ||||
|  | ||||
|                 // Called when the 'check-in' button is pressed | ||||
|                  | ||||
|                 var data = {location: location_id}; | ||||
| @@ -434,7 +447,7 @@ function barcodeCheckIn(location_id, options={}) { | ||||
|                 data.items = entries; | ||||
|  | ||||
|                 inventreePut( | ||||
|                     '{% url 'api-stock-transfer' %}', | ||||
|                     "{% url 'api-stock-transfer' %}", | ||||
|                     data, | ||||
|                     { | ||||
|                         method: 'POST', | ||||
| @@ -446,30 +459,13 @@ function barcodeCheckIn(location_id, options={}) { | ||||
|                                 showAlertOrCache('alert-success', response.success, true); | ||||
|                                 location.reload(); | ||||
|                             } else { | ||||
|                                 showAlertOrCache('alert-success', 'Error transferring stock', false); | ||||
|                                 showAlertOrCache('alert-success', '{% trans "Error transferring stock" %}', false); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
|             }, | ||||
|             onScan: function(barcode) { | ||||
|                 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') { | ||||
|             onScan: function(response) { | ||||
|                 if ('stockitem' in response) { | ||||
|                     stockitem = response.stockitem; | ||||
|  | ||||
| @@ -482,33 +478,135 @@ function barcodeCheckIn(location_id, options={}) { | ||||
|                     }); | ||||
|  | ||||
|                     if (duplicate) { | ||||
|                                         showBarcodeMessage(modal, "{% trans "Stock Item already scanned" %}", "warning"); | ||||
|                         showBarcodeMessage(modal, '{% trans "Stock Item already scanned" %}', "warning"); | ||||
|                     } else { | ||||
|  | ||||
|                         if (stockitem.location == location_id) { | ||||
|                                             showBarcodeMessage(modal, "{% trans "Stock Item already in this location" %}"); | ||||
|                             showBarcodeMessage(modal, '{% trans "Stock Item already in this location" %}'); | ||||
|                             return; | ||||
|                         } | ||||
|  | ||||
|                         // Add this stock item to the list | ||||
|                         items.push(stockitem); | ||||
|  | ||||
|                                         showBarcodeMessage(modal, "{% trans "Added stock item" %}", "success"); | ||||
|                         showBarcodeMessage(modal, '{% trans "Added stock item" %}', "success"); | ||||
|  | ||||
|                         reloadTable(); | ||||
|                     } | ||||
|  | ||||
|                 } else { | ||||
|                     // 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 { | ||||
|                                 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", | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     ) | ||||
| } | ||||
| @@ -6,6 +6,7 @@ | ||||
|  * Requires api.js to be loaded first | ||||
|  */ | ||||
|  | ||||
| {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|  | ||||
| function stockStatusCodes() { | ||||
|     return [ | ||||
| @@ -635,6 +636,9 @@ function loadStockTable(table, options) { | ||||
|         table, | ||||
|         [ | ||||
|             '#stock-print-options', | ||||
|             {% if barcodes %} | ||||
|             '#stock-barcode-options', | ||||
|             {% endif %} | ||||
|             '#stock-options', | ||||
|         ] | ||||
|     ); | ||||
| @@ -700,6 +704,20 @@ function loadStockTable(table, options) { | ||||
|         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() { | ||||
|         stockAdjustment('count'); | ||||
|     }); | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| {% load i18n %} | ||||
| 
 | ||||
| function editButton(url, text='Edit') { | ||||
|     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); | ||||
| @@ -1,5 +1,9 @@ | ||||
| {% load static %} | ||||
| {% load inventree_extras %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|  | ||||
| <nav class="navbar navbar-xs navbar-default navbar-fixed-top "> | ||||
|   <div class="container-fluid"> | ||||
|     <div class="navbar-header clearfix content-heading"> | ||||
| @@ -46,11 +50,13 @@ | ||||
|       </ul> | ||||
|       <ul class="nav navbar-nav navbar-right"> | ||||
|           {% include "search_form.html" %} | ||||
|           {% if barcodes %} | ||||
|           <li id='navbar-barcode-li'> | ||||
|             <button id='barcode-scan' class='btn btn-default' title='{% trans "Scan Barcode" %}'> | ||||
|                 <span class='fas fa-qrcode'></span> | ||||
|             </button> | ||||
|           </li> | ||||
|           {% endif %} | ||||
|           <li class='dropdown'> | ||||
|             <a class='dropdown-toggle' data-toggle='dropdown' href="#"> | ||||
|               {% if not system_healthy %} | ||||
| @@ -61,14 +67,13 @@ | ||||
|                 {% if user.is_authenticated %} | ||||
|                 {% if user.is_staff %} | ||||
|                 <li><a href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li> | ||||
|                 <hr> | ||||
|                 {% 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> | ||||
|                 {% else %} | ||||
|                 <li><a href="{% url 'login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li> | ||||
|                 {% endif %} | ||||
|                 <hr> | ||||
|                 <li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li> | ||||
|                 <li id='launch-stats'><a href='#'> | ||||
|                   {% if system_healthy %} | ||||
|                   <span class='fas fa-server'> | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| {% load i18n %} | ||||
| {% load inventree_extras %} | ||||
|  | ||||
| {% settings_value 'BARCODE_ENABLE' as barcodes %} | ||||
|  | ||||
| {% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %} | ||||
| {% if owner_control.value == "True" %} | ||||
|     {% authorized_owners location.owner as owners %} | ||||
| @@ -19,6 +21,17 @@ | ||||
|                 <span class='fas fa-plus-circle'></span> | ||||
|             </button> | ||||
|             {% 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'> | ||||
|                 <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> | ||||
|   | ||||
| @@ -15,7 +15,7 @@ pygments==2.2.0                 # Syntax highlighting | ||||
| tablib==0.13.0                  # Import / export data files | ||||
| django-crispy-forms==1.8.1      # Form helpers | ||||
| 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 | ||||
| flake8==3.8.3                   # PEP checking | ||||
| pep8-naming==0.11.1             # PEP naming convention extension | ||||
|   | ||||
		Reference in New Issue
	
	Block a user